diff --git a/package/gitea/source/tests/integration/api_user_heatmap_test.go b/package/gitea/source/tests/integration/api_user_heatmap_test.go index a2353673..f5b60b45 100644 --- a/package/gitea/source/tests/integration/api_user_heatmap_test.go +++ b/package/gitea/source/tests/integration/api_user_heatmap_test.go @@ -4,17 +4,19 @@ package integration import ( - "fmt" "net/http" "testing" "time" activities_model "code.gitea.io/gitea/models/activities" 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/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUserHeatmap(t *testing.T) { @@ -23,17 +25,23 @@ func TestUserHeatmap(t *testing.T) { normalUsername := "user2" 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) 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) resp := MakeRequest(t, req, http.StatusOK) var heatmap []*activities_model.UserHeatmapData 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) } diff --git a/package/gitea/source/tests/integration/heatmap_test.go b/package/gitea/source/tests/integration/heatmap_test.go index 09b0d964..f600fa7b 100644 --- a/package/gitea/source/tests/integration/heatmap_test.go +++ b/package/gitea/source/tests/integration/heatmap_test.go @@ -4,22 +4,40 @@ package integration import ( + "crypto/sha1" + "fmt" "net/http" + "net/http/httptest" "testing" "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/tests" "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) { defer tests.PrepareTestEnv(t)() // 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)) 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") @@ -28,11 +46,15 @@ func TestHeatmapEndpoints(t *testing.T) { req := NewRequest(t, "GET", "/user2/-/heatmap") resp := session.MakeRequest(t, req, http.StatusOK) - var result map[string]any - DecodeJSON(t, resp, &result) - assert.Contains(t, result, "heatmapData") - assert.Contains(t, result, "totalContributions") - assert.Positive(t, result["totalContributions"]) + webHeatmap := testHeatmapDecodeWebResponse(t, resp) + + req = NewRequest(t, "GET", "/api/v1/users/user2/heatmap") + resp = session.MakeRequest(t, req, http.StatusOK) + 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) { @@ -57,3 +79,71 @@ func TestHeatmapEndpoints(t *testing.T) { 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 +} diff --git a/package/gitea/source/tests/integration/privateactivity_test.go b/package/gitea/source/tests/integration/privateactivity_test.go index 157b66d2..89f9e360 100644 --- a/package/gitea/source/tests/integration/privateactivity_test.go +++ b/package/gitea/source/tests/integration/privateactivity_test.go @@ -7,13 +7,16 @@ import ( "fmt" "net/http" "testing" + "time" activities_model "code.gitea.io/gitea/models/activities" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -25,6 +28,8 @@ const ( privateActivityTestUser = "user2" ) +const privateActivityHeatmapOptInUser = "user16" + // org3 is an organization so it is not usable here const privateActivityTestOtherUser = "user4" @@ -42,6 +47,7 @@ func testPrivateActivityDoSomethingForActionEntries(t *testing.T) { Title: "test", }).AddTokenAuth(token) session.MakeRequest(t, req, http.StatusCreated) + testHeatmapSeedContribution(t, 2, 1, "private-activity-action-public", timeutil.TimeStampNow()-900) } // private activity helpers @@ -157,6 +163,58 @@ func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session 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 func TestPrivateActivityIncludePrivateContributionsWebSettingsPersistence(t *testing.T) { @@ -211,6 +269,70 @@ func TestPrivateActivityIncludePrivateContributionsAPISettingsPersistence(t *tes 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) { defer tests.PrepareTestEnv(t)()