Repository: google/go-github
Pull request #1082 (2018-12-24 12:38:09)

Add support for Preview organization projects
One test is failing, will fix it in a day or two

palash25: @gmlewis I am getting a 404 on one of the [tests](https://github.com/google/go-github/pull/1082/files#diff-8cca55506119168606687832f9722742R496) for this [endpoint](https://developer.github.com/v3/projects/collaborators/#review-a-users-permission-level) if you are not too busy could you take a look at this, I think I fresh pair of eyes on the [code](https://github.com/google/go-github/pull/1082/files#diff-286b8d11c96732b135a44f4762921926R554) might help catch my mistake because I have been struggling for the whole day for this but couldn't find whats wrong?

This is the test failure from the travis logs

```
--- FAIL: TestProjectsService_GetPermissionLevel (0.00s)
    projects_test.go:508: Projects.ReviewProjectCollaboratorPermission returned error: GET http://127.0.0.1:39289/api-v3/projects/1/collaborators/u/permission: 404  []
    projects_test.go:519: Projects.ReviewProjectCollaboratorPermission returned <nil>, want &{Permission:0xc0002d2310 User:github.User{Login:"u"}}
```
gmlewis: Hi @palash25 - on line 500 it looks like you are missing a leading slash:
`"projects/1/collaborators/u/permission"` should be `"/projects/1/collaborators/u/permission"`.
palash25: @gmlewis I was afraid it must have been something as stupid as a missing slash :sweat_smile: thanks for your help and sorry for bothering you with such a small issue.

I have updated the PR you can review it now.
palash25: @gmlewis changes made please review

Patch:
diff --git a/github/github-accessors.go b/github/github-accessors.go
index ac4257ea..fe3de66c 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -3612,6 +3612,30 @@ func (i *InstallationToken) GetToken() string {
 	return *i.Token
 }
 
+// GetExpiresAt returns the ExpiresAt field if it's non-nil, zero value otherwise.
+func (i *InteractionRestriction) GetExpiresAt() Timestamp {
+	if i == nil || i.ExpiresAt == nil {
+		return Timestamp{}
+	}
+	return *i.ExpiresAt
+}
+
+// GetLimit returns the Limit field if it's non-nil, zero value otherwise.
+func (i *InteractionRestriction) GetLimit() string {
+	if i == nil || i.Limit == nil {
+		return ""
+	}
+	return *i.Limit
+}
+
+// GetOrigin returns the Origin field if it's non-nil, zero value otherwise.
+func (i *InteractionRestriction) GetOrigin() string {
+	if i == nil || i.Origin == nil {
+		return ""
+	}
+	return *i.Origin
+}
+
 // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
 func (i *Invitation) GetCreatedAt() time.Time {
 	if i == nil || i.CreatedAt == nil {
@@ -4692,6 +4716,14 @@ func (l *ListCheckSuiteResults) GetTotal() int {
 	return *l.Total
 }
 
+// GetAffiliation returns the Affiliation field if it's non-nil, zero value otherwise.
+func (l *ListCollaboratorOptions) GetAffiliation() string {
+	if l == nil || l.Affiliation == nil {
+		return ""
+	}
+	return *l.Affiliation
+}
+
 // GetEffectiveDate returns the EffectiveDate field if it's non-nil, zero value otherwise.
 func (m *MarketplacePendingChange) GetEffectiveDate() Timestamp {
 	if m == nil || m.EffectiveDate == nil {
@@ -6716,6 +6748,14 @@ func (p *ProjectCardOptions) GetArchived() bool {
 	return *p.Archived
 }
 
+// GetPermission returns the Permission field if it's non-nil, zero value otherwise.
+func (p *ProjectCollaboratorOptions) GetPermission() string {
+	if p == nil || p.Permission == nil {
+		return ""
+	}
+	return *p.Permission
+}
+
 // GetCardsURL returns the CardsURL field if it's non-nil, zero value otherwise.
 func (p *ProjectColumn) GetCardsURL() string {
 	if p == nil || p.CardsURL == nil {
@@ -6940,6 +6980,22 @@ func (p *ProjectOptions) GetState() string {
 	return *p.State
 }
 
+// GetPermission returns the Permission field if it's non-nil, zero value otherwise.
+func (p *ProjectPermissionLevel) GetPermission() string {
+	if p == nil || p.Permission == nil {
+		return ""
+	}
+	return *p.Permission
+}
+
+// GetUser returns the User field.
+func (p *ProjectPermissionLevel) GetUser() *User {
+	if p == nil {
+		return nil
+	}
+	return p.User
+}
+
 // GetEnforceAdmins returns the EnforceAdmins field.
 func (p *Protection) GetEnforceAdmins() *AdminEnforcement {
 	if p == nil {
@@ -11076,6 +11132,14 @@ func (t *TeamLDAPMapping) GetURL() string {
 	return *t.URL
 }
 
+// GetPermission returns the Permission field if it's non-nil, zero value otherwise.
+func (t *TeamProjectOptions) GetPermission() string {
+	if t == nil || t.Permission == nil {
+		return ""
+	}
+	return *t.Permission
+}
+
 // GetFragment returns the Fragment field if it's non-nil, zero value otherwise.
 func (t *TextMatch) GetFragment() string {
 	if t == nil || t.Fragment == nil {
diff --git a/github/github.go b/github/github.go
index 19ffecc7..7c0c2c1a 100644
--- a/github/github.go
+++ b/github/github.go
@@ -125,6 +125,9 @@ const (
 
 	// https://developer.github.com/changes/2018-09-05-project-card-events/
 	mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
+
+	// https://developer.github.com/changes/2018-12-18-interactions-preview/
+	mediaTypeRepositoryInteractionsPreview = "application/vnd.github.sombra-preview+json"
 )
 
 // A Client manages communication with the GitHub API.
@@ -157,6 +160,7 @@ type Client struct {
 	Gists          *GistsService
 	Git            *GitService
 	Gitignores     *GitignoresService
+	Interactions   *InteractionsService
 	Issues         *IssuesService
 	Licenses       *LicensesService
 	Marketplace    *MarketplaceService
@@ -249,6 +253,7 @@ func NewClient(httpClient *http.Client) *Client {
 	c.Gists = (*GistsService)(&c.common)
 	c.Git = (*GitService)(&c.common)
 	c.Gitignores = (*GitignoresService)(&c.common)
+	c.Interactions = (*InteractionsService)(&c.common)
 	c.Issues = (*IssuesService)(&c.common)
 	c.Licenses = (*LicensesService)(&c.common)
 	c.Marketplace = &MarketplaceService{client: c}
diff --git a/github/interactions.go b/github/interactions.go
new file mode 100644
index 00000000..b9965491
--- /dev/null
+++ b/github/interactions.go
@@ -0,0 +1,28 @@
+// Copyright 2018 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+// InteractionsService handles communication with the repository and organization related
+// methods of the GitHub API.
+//
+// GitHub API docs: https://developer.github.com/v3/interactions/
+type InteractionsService service
+
+// InteractionRestriction represents the interaction restrictions for repository and organization.
+type InteractionRestriction struct {
+	// Specifies the group of GitHub users who can
+	// comment, open issues, or create pull requests for the given repository.
+	// Possible values are: "existing_users", "contributors_only" and "collaborators_only".
+	Limit *string `json:"limit,omitempty"`
+
+	// Origin specifies the type of the resource to interact with.
+	// Possible values are: "repository" and "organization".
+	Origin *string `json:"origin,omitempty"`
+
+	// ExpiresAt specifies the time after which the interaction restrictions expire.
+	// The default expiry time is 24 hours from the time restriction is created.
+	ExpiresAt *Timestamp `json:"expires_at,omitempty"`
+}
diff --git a/github/interactions_repos.go b/github/interactions_repos.go
new file mode 100644
index 00000000..a9b553b6
--- /dev/null
+++ b/github/interactions_repos.go
@@ -0,0 +1,73 @@
+// Copyright 2018 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"fmt"
+)
+
+// GetRestrictions fetches the interaction restrictions for a repository.
+//
+// GitHub API docs: https://developer.github.com/v3/interactions/repos/#get-interaction-restrictions-for-a-repository
+func (s *InteractionsService) GetRestrictions(ctx context.Context, owner, repo string) (*InteractionRestriction, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/interaction-limits", owner, repo)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeRepositoryInteractionsPreview)
+
+	repositoryInteractions := new(InteractionRestriction)
+
+	resp, err := s.client.Do(ctx, req, repositoryInteractions)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return repositoryInteractions, resp, nil
+}
+
+// UpdateRestrictions adds or updates the interaction restrictions for a repository.
+//
+// GitHub API docs: https://developer.github.com/v3/interactions/repos/#add-or-update-interaction-restrictions-for-a-repository
+func (s *InteractionsService) UpdateRestrictions(ctx context.Context, owner, repo string, interaction *InteractionRestriction) (*InteractionRestriction, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/interaction-limits", owner, repo)
+	req, err := s.client.NewRequest("PUT", u, interaction)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeRepositoryInteractionsPreview)
+
+	repositoryInteractions := new(InteractionRestriction)
+
+	resp, err := s.client.Do(ctx, req, repositoryInteractions)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return repositoryInteractions, resp, nil
+}
+
+// RemoveRestrictions removes the interaction restrictions for a repository.
+//
+// GitHub API docs: https://developer.github.com/v3/interactions/repos/#remove-interaction-restrictions-for-a-repository
+func (s *InteractionsService) RemoveRestrictions(ctx context.Context, owner, repo string) (*Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/interaction-limits", owner, repo)
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeRepositoryInteractionsPreview)
+
+	return s.client.Do(ctx, req, nil)
+}
diff --git a/github/projects.go b/github/projects.go
index 423968ec..c7a68f53 100644
--- a/github/projects.go
+++ b/github/projects.go
@@ -466,3 +466,129 @@ func (s *ProjectsService) MoveProjectCard(ctx context.Context, cardID int64, opt
 
 	return s.client.Do(ctx, req, nil)
 }
+
+// ProjectCollaboratorOptions specifies the optional parameters to the
+// ProjectsService.AddProjectCollaborator method.
+type ProjectCollaboratorOptions struct {
+	// Permission specifies the permission to grant to the collaborator.
+	// Possible values are:
+	//     "read" - can read, but not write to or administer this project.
+	//     "write" - can read and write, but not administer this project.
+	//     "admin" - can read, write and administer this project.
+	//
+	// Default value is "write"
+	Permission *string `json:"permission,omitempty"`
+}
+
+// AddProjectCollaborator adds a collaborator to an organization project and sets
+// their permission level. You must be an organization owner or a project admin to add a collaborator.
+//
+// GitHub API docs: https://developer.github.com/v3/projects/collaborators/#add-user-as-a-collaborator
+func (s *ProjectsService) AddProjectCollaborator(ctx context.Context, id int64, username string, opt *ProjectCollaboratorOptions) (*Response, error) {
+	u := fmt.Sprintf("projects/%v/collaborators/%v", id, username)
+	req, err := s.client.NewRequest("PUT", u, opt)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeProjectsPreview)
+
+	return s.client.Do(ctx, req, nil)
+}
+
+// RemoveProjectCollaborator removes a collaborator from an organization project.
+// You must be an organization owner or a project admin to remove a collaborator.
+//
+// GitHub API docs: https://developer.github.com/v3/projects/collaborators/#remove-user-as-a-collaborator
+func (s *ProjectsService) RemoveProjectCollaborator(ctx context.Context, id int64, username string) (*Response, error) {
+	u := fmt.Sprintf("projects/%v/collaborators/%v", id, username)
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeProjectsPreview)
+
+	return s.client.Do(ctx, req, nil)
+}
+
+// ListCollaboratorOptions specifies the optional parameters to the
+// ProjectsService.ListProjectCollaborators method.
+type ListCollaboratorOptions struct {
+	// Affiliation specifies how collaborators should be filtered by their affiliation.
+	// Possible values are:
+	//     "outside" - All outside collaborators of an organization-owned repository
+	//     "direct" - All collaborators with permissions to an organization-owned repository,
+	//              regardless of organization membership status
+	//     "all" - All collaborators the authenticated user can see
+	//
+	// Default value is "all".
+	Affiliation *string `url:"affiliation,omitempty"`
+
+	ListOptions
+}
+
+// ListProjectCollaborators lists the collaborators for an organization project. For a project,
+// the list of collaborators includes outside collaborators, organization members that are direct
+// collaborators, organization members with access through team memberships, organization members
+// with access through default organization permissions, and organization owners. You must be an
+// organization owner or a project admin to list collaborators.
+//
+// GitHub API docs: https://developer.github.com/v3/projects/collaborators/#list-collaborators
+func (s *ProjectsService) ListProjectCollaborators(ctx context.Context, id int64, opt *ListCollaboratorOptions) ([]*User, *Response, error) {
+	u := fmt.Sprintf("projects/%v/collaborators", id)
+	u, err := addOptions(u, opt)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeProjectsPreview)
+
+	var users []*User
+	resp, err := s.client.Do(ctx, req, &users)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return users, resp, nil
+}
+
+// ProjectPermissionLevel represents the permission level an organization
+// member has for a given project.
+type ProjectPermissionLevel struct {
+	// Possible values: "admin", "write", "read", "none"
+	Permission *string `json:"permission,omitempty"`
+
+	User *User `json:"user,omitempty"`
+}
+
+// ReviewProjectCollaboratorPermission returns the collaborator's permission level for an organization
+// project. Possible values for the permission key: "admin", "write", "read", "none".
+// You must be an organization owner or a project admin to review a user's permission level.
+//
+// GitHub API docs: https://developer.github.com/v3/projects/collaborators/#review-a-users-permission-level
+func (s *ProjectsService) ReviewProjectCollaboratorPermission(ctx context.Context, id int64, username string) (*ProjectPermissionLevel, *Response, error) {
+	u := fmt.Sprintf("projects/%v/collaborators/%v/permission", id, username)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeProjectsPreview)
+
+	ppl := new(ProjectPermissionLevel)
+	resp, err := s.client.Do(ctx, req, ppl)
+	if err != nil {
+		return nil, resp, err
+	}
+	return ppl, resp, nil
+}
diff --git a/github/teams.go b/github/teams.go
index c3773e00..97d038d9 100644
--- a/github/teams.go
+++ b/github/teams.go
@@ -355,3 +355,103 @@ func (s *TeamsService) ListUserTeams(ctx context.Context, opt *ListOptions) ([]*
 
 	return teams, resp, nil
 }
+
+// ListTeamProjects lists the organization projects for a team.
+//
+// GitHub API docs: https://developer.github.com/v3/teams/#list-team-projects
+func (s *TeamsService) ListTeamProjects(ctx context.Context, teamID int64) ([]*Project, *Response, error) {
+	u := fmt.Sprintf("teams/%v/projects", teamID)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
+
+	var projects []*Project
+	resp, err := s.client.Do(ctx, req, &projects)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return projects, resp, nil
+}
+
+// ReviewTeamProjects checks whether a team has read, write, or admin
+// permissions for an organization project.
+//
+// GitHub API docs: https://developer.github.com/v3/teams/#review-a-team-project
+func (s *TeamsService) ReviewTeamProjects(ctx context.Context, teamID, projectID int64) (*Project, *Response, error) {
+	u := fmt.Sprintf("teams/%v/projects/%v", teamID, projectID)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
+
+	projects := &Project{}
+	resp, err := s.client.Do(ctx, req, &projects)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return projects, resp, nil
+}
+
+// TeamProjectOptions specifies the optional parameters to the
+// TeamsService.AddTeamProject method.
+type TeamProjectOptions struct {
+	// Permission specifies the permission to grant to the team for this project.
+	// Possible values are:
+	//     "read" - team members can read, but not write to or administer this project.
+	//     "write" - team members can read and write, but not administer this project.
+	//     "admin" - team members can read, write and administer this project.
+	//
+	Permission *string `json:"permission,omitempty"`
+}
+
+// AddTeamProject adds an organization project to a team. To add a project to a team or
+// update the team's permission on a project, the authenticated user must have admin
+// permissions for the project.
+//
+// GitHub API docs: https://developer.github.com/v3/teams/#add-or-update-team-project
+func (s *TeamsService) AddTeamProject(ctx context.Context, teamID, projectID int64, opt *TeamProjectOptions) (*Response, error) {
+	u := fmt.Sprintf("teams/%v/projects/%v", teamID, projectID)
+	req, err := s.client.NewRequest("PUT", u, opt)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
+
+	return s.client.Do(ctx, req, nil)
+}
+
+// RemoveTeamProject removes an organization project from a team. An organization owner or
+// a team maintainer can remove any project from the team. To remove a project from a team
+// as an organization member, the authenticated user must have "read" access to both the team
+// and project, or "admin" access to the team or project.
+// Note: This endpoint removes the project from the team, but does not delete it.
+//
+// GitHub API docs: https://developer.github.com/v3/teams/#remove-team-project
+func (s *TeamsService) RemoveTeamProject(ctx context.Context, teamID int64, projectID int64) (*Response, error) {
+	u := fmt.Sprintf("teams/%v/projects/%v", teamID, projectID)
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
+
+	return s.client.Do(ctx, req, nil)
+}


Test patch:
diff --git a/github/interactions_repos_test.go b/github/interactions_repos_test.go
new file mode 100644
index 00000000..c1c88b56
--- /dev/null
+++ b/github/interactions_repos_test.go
@@ -0,0 +1,80 @@
+// Copyright 2018 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestInteractionsService_GetRestrictions(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/interaction-limits", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeRepositoryInteractionsPreview)
+		fmt.Fprint(w, `{"origin":"repository"}`)
+	})
+
+	repoInteractions, _, err := client.Interactions.GetRestrictions(context.Background(), "o", "r")
+	if err != nil {
+		t.Errorf("Interactions.GetRestrictions returned error: %v", err)
+	}
+
+	want := &InteractionRestriction{Origin: String("repository")}
+	if !reflect.DeepEqual(repoInteractions, want) {
+		t.Errorf("Interactions.GetRestrictions returned %+v, want %+v", repoInteractions, want)
+	}
+}
+
+func TestInteractionsService_UpdateRestrictions(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	input := &InteractionRestriction{Limit: String("existing_users")}
+
+	mux.HandleFunc("/repos/o/r/interaction-limits", func(w http.ResponseWriter, r *http.Request) {
+		v := new(InteractionRestriction)
+		json.NewDecoder(r.Body).Decode(v)
+
+		testMethod(t, r, "PUT")
+		testHeader(t, r, "Accept", mediaTypeRepositoryInteractionsPreview)
+		if !reflect.DeepEqual(v, input) {
+			t.Errorf("Request body = %+v, want %+v", v, input)
+		}
+		fmt.Fprint(w, `{"origin":"repository"}`)
+	})
+
+	repoInteractions, _, err := client.Interactions.UpdateRestrictions(context.Background(), "o", "r", input)
+	if err != nil {
+		t.Errorf("Interactions.UpdateRestrictions returned error: %v", err)
+	}
+
+	want := &InteractionRestriction{Origin: String("repository")}
+	if !reflect.DeepEqual(repoInteractions, want) {
+		t.Errorf("Interactions.UpdateRestrictions returned %+v, want %+v", repoInteractions, want)
+	}
+}
+
+func TestInteractionsService_RemoveRestrictions(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/interaction-limits", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		testHeader(t, r, "Accept", mediaTypeRepositoryInteractionsPreview)
+	})
+
+	_, err := client.Interactions.RemoveRestrictions(context.Background(), "o", "r")
+	if err != nil {
+		t.Errorf("Interactions.RemoveRestrictions returned error: %v", err)
+	}
+}
diff --git a/github/projects_test.go b/github/projects_test.go
index 9f12272a..88d16ed1 100644
--- a/github/projects_test.go
+++ b/github/projects_test.go
@@ -382,3 +382,140 @@ func TestProjectsService_MoveProjectCard(t *testing.T) {
 		t.Errorf("Projects.MoveProjectCard returned error: %v", err)
 	}
 }
+
+func TestProjectsService_AddProjectCollaborator(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	opt := &ProjectCollaboratorOptions{
+		Permission: String("admin"),
+	}
+
+	mux.HandleFunc("/projects/1/collaborators/u", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PUT")
+		testHeader(t, r, "Accept", mediaTypeProjectsPreview)
+
+		v := &ProjectCollaboratorOptions{}
+		json.NewDecoder(r.Body).Decode(v)
+		if !reflect.DeepEqual(v, opt) {
+			t.Errorf("Request body = %+v, want %+v", v, opt)
+		}
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	_, err := client.Projects.AddProjectCollaborator(context.Background(), 1, "u", opt)
+	if err != nil {
+		t.Errorf("Projects.AddProjectCollaborator returned error: %v", err)
+	}
+}
+
+func TestProjectsService_AddCollaborator_invalidUser(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	_, err := client.Projects.AddProjectCollaborator(context.Background(), 1, "%", nil)
+	testURLParseError(t, err)
+}
+
+func TestProjectsService_RemoveCollaborator(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/projects/1/collaborators/u", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		testHeader(t, r, "Accept", mediaTypeProjectsPreview)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	_, err := client.Projects.RemoveProjectCollaborator(context.Background(), 1, "u")
+	if err != nil {
+		t.Errorf("Projects.RemoveProjectCollaborator returned error: %v", err)
+	}
+}
+
+func TestProjectsService_RemoveCollaborator_invalidUser(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	_, err := client.Projects.RemoveProjectCollaborator(context.Background(), 1, "%")
+	testURLParseError(t, err)
+}
+
+func TestProjectsService_ListCollaborators(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/projects/1/collaborators", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeProjectsPreview)
+		testFormValues(t, r, values{"page": "2"})
+		fmt.Fprintf(w, `[{"id":1}, {"id":2}]`)
+	})
+
+	opt := &ListCollaboratorOptions{
+		ListOptions: ListOptions{Page: 2},
+	}
+	users, _, err := client.Projects.ListProjectCollaborators(context.Background(), 1, opt)
+	if err != nil {
+		t.Errorf("Projects.ListProjectCollaborators returned error: %v", err)
+	}
+
+	want := []*User{{ID: Int64(1)}, {ID: Int64(2)}}
+	if !reflect.DeepEqual(users, want) {
+		t.Errorf("Projects.ListProjectCollaborators returned %+v, want %+v", users, want)
+	}
+}
+
+func TestProjectsService_ListCollaborators_withAffiliation(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/projects/1/collaborators", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeProjectsPreview)
+		testFormValues(t, r, values{"affiliation": "all", "page": "2"})
+		fmt.Fprintf(w, `[{"id":1}, {"id":2}]`)
+	})
+
+	opt := &ListCollaboratorOptions{
+		ListOptions: ListOptions{Page: 2},
+		Affiliation: String("all"),
+	}
+	users, _, err := client.Projects.ListProjectCollaborators(context.Background(), 1, opt)
+	if err != nil {
+		t.Errorf("Projects.ListProjectCollaborators returned error: %v", err)
+	}
+
+	want := []*User{{ID: Int64(1)}, {ID: Int64(2)}}
+	if !reflect.DeepEqual(users, want) {
+		t.Errorf("Projects.ListProjectCollaborators returned %+v, want %+v", users, want)
+	}
+}
+
+func TestProjectsService_GetPermissionLevel(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/projects/1/collaborators/u/permission", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeProjectsPreview)
+		fmt.Fprintf(w, `{"permission":"admin","user":{"login":"u"}}`)
+	})
+
+	ppl, _, err := client.Projects.ReviewProjectCollaboratorPermission(context.Background(), 1, "u")
+	if err != nil {
+		t.Errorf("Projects.ReviewProjectCollaboratorPermission returned error: %v", err)
+	}
+
+	want := &ProjectPermissionLevel{
+		Permission: String("admin"),
+		User: &User{
+			Login: String("u"),
+		},
+	}
+
+	if !reflect.DeepEqual(ppl, want) {
+		t.Errorf("Projects.ReviewProjectCollaboratorPermission returned %+v, want %+v", ppl, want)
+	}
+}
diff --git a/github/teams_test.go b/github/teams_test.go
index 89708b4f..e793b3c1 100644
--- a/github/teams_test.go
+++ b/github/teams_test.go
@@ -463,3 +463,92 @@ func TestTeamsService_ListPendingTeamInvitations(t *testing.T) {
 		t.Errorf("Teams.ListPendingTeamInvitations returned %+v, want %+v", invitations, want)
 	}
 }
+
+func TestTeamsService_ListProjects(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	mux.HandleFunc("/teams/1/projects", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", strings.Join(acceptHeaders, ", "))
+		fmt.Fprint(w, `[{"id":1}]`)
+	})
+
+	projects, _, err := client.Teams.ListTeamProjects(context.Background(), 1)
+	if err != nil {
+		t.Errorf("Teams.ListTeamProjects returned error: %v", err)
+	}
+
+	want := []*Project{{ID: Int64(1)}}
+	if !reflect.DeepEqual(projects, want) {
+		t.Errorf("Teams.ListTeamProjects returned %+v, want %+v", projects, want)
+	}
+}
+
+func TestTeamsService_ReviewProjects(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	mux.HandleFunc("/teams/1/projects/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", strings.Join(acceptHeaders, ", "))
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	project, _, err := client.Teams.ReviewTeamProjects(context.Background(), 1, 1)
+	if err != nil {
+		t.Errorf("Teams.ReviewTeamProjects returned error: %v", err)
+	}
+
+	want := &Project{ID: Int64(1)}
+	if !reflect.DeepEqual(project, want) {
+		t.Errorf("Teams.ReviewTeamProjects returned %+v, want %+v", project, want)
+	}
+}
+
+func TestTeamsService_AddTeamProject(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	opt := &TeamProjectOptions{
+		Permission: String("admin"),
+	}
+
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	mux.HandleFunc("/teams/1/projects/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PUT")
+		testHeader(t, r, "Accept", strings.Join(acceptHeaders, ", "))
+
+		v := &TeamProjectOptions{}
+		json.NewDecoder(r.Body).Decode(v)
+		if !reflect.DeepEqual(v, opt) {
+			t.Errorf("Request body = %+v, want %+v", v, opt)
+		}
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	_, err := client.Teams.AddTeamProject(context.Background(), 1, 1, opt)
+	if err != nil {
+		t.Errorf("Teams.AddTeamProject returned error: %v", err)
+	}
+}
+
+func TestTeamsService_RemoveTeamProject(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	acceptHeaders := []string{mediaTypeNestedTeamsPreview, mediaTypeProjectsPreview}
+	mux.HandleFunc("/teams/1/projects/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		testHeader(t, r, "Accept", strings.Join(acceptHeaders, ", "))
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	_, err := client.Teams.RemoveTeamProject(context.Background(), 1, 1)
+	if err != nil {
+		t.Errorf("Teams.RemoveTeamProject returned error: %v", err)
+	}
+}