test(package): gitea: verify heatmap privacy endpoints

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-06 22:26:43 +00:00
parent 28dde5b9b1
commit 72168aa8fa
3 changed files with 231 additions and 11 deletions
@@ -4,17 +4,19 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"testing" "testing"
"time" "time"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestUserHeatmap(t *testing.T) { func TestUserHeatmap(t *testing.T) {
@@ -23,17 +25,23 @@ func TestUserHeatmap(t *testing.T) {
normalUsername := "user2" normalUsername := "user2"
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser) token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser)
fakeNow := time.Date(2011, 10, 20, 0, 0, 0, 0, time.Local) fakeNow := time.Date(2011, 10, 21, 0, 0, 0, 0, time.Local)
timeutil.MockSet(fakeNow) timeutil.MockSet(fakeNow)
defer timeutil.MockUnset() defer timeutil.MockUnset()
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/heatmap", normalUsername)). require.NoError(t, db.TruncateBeans(t.Context(), &activities_model.HeatmapContribution{}))
require.NoError(t, user_model.SetIncludePrivateContributions(t.Context(), 2, false))
testHeatmapSeedContribution(t, 2, 1, "api-user2-public-author-date", 1319068800)
testHeatmapSeedContribution(t, 2, 1, "api-user2-public-same-bucket", 1319068850)
testHeatmapSeedContribution(t, 2, 2, "api-user2-private-hidden", 1319070600)
req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", normalUsername).
AddTokenAuth(token) AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
var heatmap []*activities_model.UserHeatmapData var heatmap []*activities_model.UserHeatmapData
DecodeJSON(t, resp, &heatmap) DecodeJSON(t, resp, &heatmap)
var dummyheatmap []*activities_model.UserHeatmapData
dummyheatmap = append(dummyheatmap, &activities_model.UserHeatmapData{Timestamp: 1603227600, Contributions: 1})
assert.Equal(t, dummyheatmap, heatmap) assert.Equal(t, []*activities_model.UserHeatmapData{
{Timestamp: 1319068800, Contributions: 2},
}, heatmap)
} }
@@ -4,22 +4,40 @@
package integration package integration
import ( import (
"crypto/sha1"
"fmt"
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
"time" "time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type testHeatmapWebResponse struct {
HeatmapData [][2]int64 `json:"heatmapData"`
TotalContributions int64 `json:"totalContributions"`
}
func TestHeatmapEndpoints(t *testing.T) { func TestHeatmapEndpoints(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
// Mock time so fixture actions fall within the heatmap's time window // Mock time so fixture actions fall within the heatmap's time window
timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)) timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
defer timeutil.MockUnset() defer timeutil.MockUnset()
require.NoError(t, db.TruncateBeans(t.Context(), &activities_model.HeatmapContribution{}))
require.NoError(t, user_model.SetIncludePrivateContributions(t.Context(), 2, false))
testHeatmapSeedContribution(t, 2, 1, "web-user2-public-one", 1603009800)
testHeatmapSeedContribution(t, 2, 1, "web-user2-public-two", 1603009850)
testHeatmapSeedContribution(t, 2, 2, "web-user2-private-hidden", 1603010700)
session := loginUser(t, "user2") session := loginUser(t, "user2")
@@ -28,11 +46,15 @@ func TestHeatmapEndpoints(t *testing.T) {
req := NewRequest(t, "GET", "/user2/-/heatmap") req := NewRequest(t, "GET", "/user2/-/heatmap")
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any webHeatmap := testHeatmapDecodeWebResponse(t, resp)
DecodeJSON(t, resp, &result)
assert.Contains(t, result, "heatmapData") req = NewRequest(t, "GET", "/api/v1/users/user2/heatmap")
assert.Contains(t, result, "totalContributions") resp = session.MakeRequest(t, req, http.StatusOK)
assert.Positive(t, result["totalContributions"]) var apiHeatmap []*activities_model.UserHeatmapData
DecodeJSON(t, resp, &apiHeatmap)
assert.Equal(t, testHeatmapSumAPIContributions(apiHeatmap), webHeatmap.TotalContributions)
assert.Equal(t, int64(2), webHeatmap.TotalContributions)
}) })
t.Run("OrgDashboard", func(t *testing.T) { t.Run("OrgDashboard", func(t *testing.T) {
@@ -57,3 +79,71 @@ func TestHeatmapEndpoints(t *testing.T) {
assert.Contains(t, result, "totalContributions") assert.Contains(t, result, "totalContributions")
}) })
} }
func testHeatmapSeedContribution(t *testing.T, userID, repoID int64, label string, authorUnix timeutil.TimeStamp) string {
t.Helper()
commitSHA := fmt.Sprintf("%040x", sha1.Sum([]byte(label)))
require.NoError(t, activities_model.UpsertHeatmapContribution(t.Context(), &activities_model.HeatmapContribution{
UserID: userID,
RepoID: repoID,
CommitSHA: commitSHA,
AuthorEmail: fmt.Sprintf("user%d@example.com", userID),
AuthorUnix: authorUnix,
}))
return commitSHA
}
func testHeatmapDecodeWebResponse(t *testing.T, resp *httptest.ResponseRecorder) testHeatmapWebResponse {
t.Helper()
var result testHeatmapWebResponse
DecodeJSON(t, resp, &result)
return result
}
func testHeatmapSumAPIContributions(heatmap []*activities_model.UserHeatmapData) int64 {
var total int64
for _, item := range heatmap {
total += item.Contributions
}
return total
}
func testHeatmapAssertAggregateOnlyAPIResponse(t *testing.T, body []byte) {
t.Helper()
var decoded []map[string]any
require.NoError(t, json.Unmarshal(body, &decoded))
for _, entry := range decoded {
assert.ElementsMatch(t, []string{"timestamp", "contributions"}, testHeatmapMapKeys(entry))
}
}
func testHeatmapAssertAggregateOnlyWebResponse(t *testing.T, body []byte) {
t.Helper()
var decoded map[string]any
require.NoError(t, json.Unmarshal(body, &decoded))
assert.ElementsMatch(t, []string{"heatmapData", "totalContributions"}, testHeatmapMapKeys(decoded))
}
func testHeatmapAssertNoPrivateMetadata(t *testing.T, body []byte, forbidden ...string) {
t.Helper()
response := string(body)
for _, value := range forbidden {
assert.NotContains(t, response, value)
}
for _, value := range []string{"repo_id", "repository_id", "repoID", "commit_sha", "commit_message", "branch", "url", "action_id"} {
assert.NotContains(t, response, value)
}
}
func testHeatmapMapKeys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for key := range m {
keys = append(keys, key)
}
return keys
}
@@ -7,13 +7,16 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"testing" "testing"
"time"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -25,6 +28,8 @@ const (
privateActivityTestUser = "user2" privateActivityTestUser = "user2"
) )
const privateActivityHeatmapOptInUser = "user16"
// org3 is an organization so it is not usable here // org3 is an organization so it is not usable here
const privateActivityTestOtherUser = "user4" const privateActivityTestOtherUser = "user4"
@@ -42,6 +47,7 @@ func testPrivateActivityDoSomethingForActionEntries(t *testing.T) {
Title: "test", Title: "test",
}).AddTokenAuth(token) }).AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusCreated) session.MakeRequest(t, req, http.StatusCreated)
testHeatmapSeedContribution(t, 2, 1, "private-activity-action-public", timeutil.TimeStampNow()-900)
} }
// private activity helpers // private activity helpers
@@ -157,6 +163,58 @@ func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session
return len(items) != 0 return len(items) != 0
} }
func testPrivateActivityHelperGetAPIHeatmapFromPublic(t *testing.T) ([]*activities_model.UserHeatmapData, []byte) {
req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityHeatmapOptInUser)
resp := MakeRequest(t, req, http.StatusOK)
body := resp.Body.Bytes()
var items []*activities_model.UserHeatmapData
DecodeJSON(t, resp, &items)
return items, body
}
func testPrivateActivityHelperGetAPIHeatmapFromSession(t *testing.T, session *TestSession) ([]*activities_model.UserHeatmapData, []byte) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityHeatmapOptInUser).
AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusOK)
body := resp.Body.Bytes()
var items []*activities_model.UserHeatmapData
DecodeJSON(t, resp, &items)
return items, body
}
func testPrivateActivityHelperGetWebHeatmapFromPublic(t *testing.T) (testHeatmapWebResponse, []byte) {
req := NewRequestf(t, "GET", "/%s/-/heatmap", privateActivityHeatmapOptInUser)
resp := MakeRequest(t, req, http.StatusOK)
body := resp.Body.Bytes()
return testHeatmapDecodeWebResponse(t, resp), body
}
func testPrivateActivityHelperSeedHeatmapContributions(t *testing.T) string {
t.Helper()
require.NoError(t, db.TruncateBeans(t.Context(), &activities_model.HeatmapContribution{}))
require.NoError(t, user_model.SetIncludePrivateContributions(t.Context(), 16, false))
testHeatmapSeedContribution(t, 16, 21, "private-activity-public", 1603009800)
privateSHA := testHeatmapSeedContribution(t, 16, 22, "private-activity-private-one", 1603009850)
testHeatmapSeedContribution(t, 16, 22, "private-activity-private-two", 1603010700)
return privateSHA
}
func testPrivateActivityHelperAssertAPIHeatmapTotal(t *testing.T, heatmap []*activities_model.UserHeatmapData, expected int64) {
t.Helper()
assert.Equal(t, expected, testHeatmapSumAPIContributions(heatmap))
}
// check private contribution opt-in settings persistence and ownership // check private contribution opt-in settings persistence and ownership
func TestPrivateActivityIncludePrivateContributionsWebSettingsPersistence(t *testing.T) { func TestPrivateActivityIncludePrivateContributionsWebSettingsPersistence(t *testing.T) {
@@ -211,6 +269,70 @@ func TestPrivateActivityIncludePrivateContributionsAPISettingsPersistence(t *tes
testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false) testPrivateActivityHelperAssertIncludePrivateContributions(t, privateActivityTestUser, false)
} }
func TestPrivateActivityPrivateContributionsHiddenByDefault(t *testing.T) {
defer tests.PrepareTestEnv(t)()
timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
defer timeutil.MockUnset()
testPrivateActivityHelperSeedHeatmapContributions(t)
publicHeatmap, _ := testPrivateActivityHelperGetAPIHeatmapFromPublic(t)
testPrivateActivityHelperAssertAPIHeatmapTotal(t, publicHeatmap, 1)
for _, testCase := range []struct {
name string
username string
}{
{"owner", privateActivityHeatmapOptInUser},
{"admin", privateActivityTestAdmin},
{"collaborator", "user15"},
{"unrelated", privateActivityTestOtherUser},
} {
t.Run(testCase.name, func(t *testing.T) {
session := loginUser(t, testCase.username)
heatmap, _ := testPrivateActivityHelperGetAPIHeatmapFromSession(t, session)
testPrivateActivityHelperAssertAPIHeatmapTotal(t, heatmap, 1)
})
}
}
func TestPrivateActivityIncludePrivateContributionsEndpointAggregatesOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
defer timeutil.MockUnset()
privateSHA := testPrivateActivityHelperSeedHeatmapContributions(t)
ownerSession := loginUser(t, privateActivityHeatmapOptInUser)
testPrivateActivityHelperSetIncludePrivateContributionsViaWeb(t, ownerSession, privateActivityHeatmapOptInUser, true)
apiHeatmap, apiBody := testPrivateActivityHelperGetAPIHeatmapFromPublic(t)
testPrivateActivityHelperAssertAPIHeatmapTotal(t, apiHeatmap, 3)
testHeatmapAssertAggregateOnlyAPIResponse(t, apiBody)
testHeatmapAssertNoPrivateMetadata(t, apiBody,
"big_test_private_3",
"22",
privateSHA,
"private-activity-private-one",
"master",
"/user16/big_test_private_3",
)
webHeatmap, webBody := testPrivateActivityHelperGetWebHeatmapFromPublic(t)
assert.Equal(t, int64(3), webHeatmap.TotalContributions)
testHeatmapAssertAggregateOnlyWebResponse(t, webBody)
testHeatmapAssertNoPrivateMetadata(t, webBody,
"big_test_private_3",
"22",
privateSHA,
"private-activity-private-one",
"master",
"/user16/big_test_private_3",
)
testPrivateActivityHelperSetIncludePrivateContributionsViaWeb(t, ownerSession, privateActivityHeatmapOptInUser, false)
apiHeatmap, _ = testPrivateActivityHelperGetAPIHeatmapFromPublic(t)
testPrivateActivityHelperAssertAPIHeatmapTotal(t, apiHeatmap, 1)
}
func TestPrivateActivityIncludePrivateContributionsWebSettingsAuthBoundary(t *testing.T) { func TestPrivateActivityIncludePrivateContributionsWebSettingsAuthBoundary(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()