Files
forge/tests/integration/api_public_only_test.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

185 lines
7.3 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"strconv"
"testing"
auth_model "github.com/hanzoai/git/models/auth"
"github.com/hanzoai/git/models/organization"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/models/unittest"
user_model "github.com/hanzoai/git/models/user"
api "github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUserReposPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/v1/user/repos").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var repos []api.Repository
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
req = NewRequest(t, "GET", "/v1/users/user2/repos").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
func repoNames(repos []api.Repository) []string {
names := make([]string, 0, len(repos))
for _, repo := range repos {
names = append(names, repo.FullName)
}
return names
}
func TestAPIRepoByIDPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/v1/repositories/1").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/v1/repositories/2").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
req := NewRequest(t, "GET", "/v1/users/user2/activities/feeds").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var activities []api.Activity
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/v1/users/user2/activities/feeds").
AddTokenAuth(publicToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
req = NewRequest(t, "GET", "/v1/orgs/org3/activities/feeds").
AddTokenAuth(orgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/v1/orgs/org3/activities/feeds").
AddTokenAuth(publicOrgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
}
func TestAPIOrgPermissionsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// user2 is a member of the private org private_org35
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "private_org35"})
// a full org-scoped token can read the membership permissions
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequestf(t, "GET", "/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// a public-only token must not disclose permissions for a private org
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
req = NewRequestf(t, "GET", "/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(publicToken)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPITeamReposPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// team 1 (Owners of org3) has access to the private repos org3/repo3 and org3/repo5
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
privateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
privateRepo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5})
// a full org+repo scoped token sees the private repos
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository)
req := NewRequestf(t, "GET", "/v1/teams/%d/repos", team.ID).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
assert.Contains(t, repoNames(repos), privateRepo.FullName())
// a public-only token must not receive any private repo
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req = NewRequestf(t, "GET", "/v1/teams/%d/repos", team.ID).AddTokenAuth(publicToken)
resp = MakeRequest(t, req, http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), privateRepo.FullName())
assert.NotContains(t, repoNames(repos), privateRepo2.FullName())
// the total-count header must match the filtered page, otherwise it leaks the
// number of hidden private repos
assert.Equal(t, strconv.Itoa(len(repos)), resp.Header().Get("X-Total-Count"))
// the single-repo endpoint must not confirm a private repo for a public-only token
req = NewRequestf(t, "GET", "/v1/teams/%d/repos/%s", team.ID, privateRepo.FullName()).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequestf(t, "GET", "/v1/teams/%d/repos/%s", team.ID, privateRepo.FullName()).AddTokenAuth(publicToken)
MakeRequest(t, req, http.StatusNotFound)
}
func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) {
t.Helper()
for _, activity := range activities {
assert.False(t, activity.IsPrivate)
if activity.Repo != nil {
assert.False(t, activity.Repo.Private)
}
}
}
// TestAPIRepoLimitedOwnerPublicOnly ensures a public-only token cannot reach a public repo
// owned by a limited-visibility owner (which is not reachable anonymously).
func TestAPIRepoLimitedOwnerPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// limited_org (limited visibility) owns the non-private repo public_repo_on_limited_org
const url = "/v1/repos/limited_org/public_repo_on_limited_org"
// a public-only token is confined to genuinely public resources
publicOnlyToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", url).AddTokenAuth(publicOnlyToken)
MakeRequest(t, req, http.StatusNotFound)
// a normal token can still reach the limited owner's public repo
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
req = NewRequest(t, "GET", url).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
}