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:
@@ -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)()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user