mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 14:03:24 +09:00
Merge remote-tracking branch 'origin/main' into various-sec-fixes
# Conflicts: # routers/api/v1/api.go
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
@@ -61,6 +62,44 @@ func TestAPIGetTrackedTimes(t *testing.T) {
|
||||
assert.Equal(t, int64(6), filterAPITimes[1].ID)
|
||||
}
|
||||
|
||||
// TestAPIGetTrackedTimesNonExistentUserFilter ensures filtering by a user that
|
||||
// does not exist returns a clean 404 instead of panicking (nil pointer dereference).
|
||||
func TestAPIGetTrackedTimesNonExistentUserFilter(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
assert.NoError(t, issue2.LoadRepo(t.Context()))
|
||||
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
url string
|
||||
}{
|
||||
{"repository level", fmt.Sprintf("/api/v1/repos/%s/%s/times?user=nonexistentuser", user2.Name, issue2.Repo.Name)},
|
||||
{"issue level", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/times?user=nonexistentuser", user2.Name, issue2.Repo.Name, issue2.Index)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", tc.url).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
assert.True(t, json.Valid(resp.Body.Bytes()), "response body must be a single JSON value, got: %s", resp.Body.Bytes())
|
||||
|
||||
var apiError api.APIError
|
||||
DecodeJSON(t, resp, &apiError)
|
||||
assert.Contains(t, apiError.Message, "user does not exist")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("existing user", func(t *testing.T) {
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/times?user=%s", user2.Name, issue2.Repo.Name, user2.Name).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, api.TrackedTimeList{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIDeleteTrackedTime(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package integration
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
@@ -62,3 +63,113 @@ func TestAPICompareBranches(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIDownloadCompareDiffOrPatch(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
session := loginUser(t, "user2")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
t.Run("BranchToBranchDiff", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=diff").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
body := resp.Body.String()
|
||||
assert.Contains(t, body, "diff --git ")
|
||||
})
|
||||
|
||||
t.Run("BranchToBranchPatch", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=patch").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
body := resp.Body.String()
|
||||
assert.True(t, strings.HasPrefix(body, "From "), "patch output should start with a format-patch header, got: %q", body[:min(40, len(body))])
|
||||
})
|
||||
|
||||
t.Run("CommitToCommitDiff", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f?output=diff").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "diff --git ")
|
||||
})
|
||||
|
||||
t.Run("BranchToCommitDiff", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// 8babce96... is the head of remove-files-b; pairing it with add-csv guarantees a non-empty diff.
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...8babce967f21b9dfa6987f943b91093dac58a4f0?output=diff").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "diff --git ")
|
||||
})
|
||||
|
||||
t.Run("TwoDotSeparator", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv..remove-files-b?output=diff").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "diff --git ")
|
||||
})
|
||||
|
||||
t.Run("SlashedBranchName", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// user2/repo1's `feature/1` branch contains a slash; the route must match it
|
||||
// without URL-encoding. master and feature/1 happen to share a SHA in the fixture,
|
||||
// so we only assert the route resolves (200 OK) rather than checking diff content.
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/compare/master...feature/1?output=diff").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
})
|
||||
|
||||
t.Run("UnknownOutputReturnsJSON", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Only "diff"/"patch" switch to raw output; any other value falls through to JSON.
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b?output=foo").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiResp := DecodeJSON(t, resp, &api.Compare{})
|
||||
assert.Equal(t, 2, apiResp.TotalCommits)
|
||||
})
|
||||
|
||||
t.Run("SingleRefImplicitBase", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// No `...`/`..` separator: parseCompareInfo defaults the base to the
|
||||
// repo's PR target branch (master for repo20) and compares it against
|
||||
// the given head.
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv?output=diff").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
assert.Contains(t, resp.Body.String(), "diff --git ")
|
||||
})
|
||||
|
||||
t.Run("PrivateRepoAnonymous", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// repo16 is private; an unauthenticated request must not leak its existence.
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/compare/master...good-sign?output=diff")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("CrossRepoFork", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
|
||||
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
|
||||
user13Sess := loginUser(t, "user13")
|
||||
user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
_, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "cross-repo-diff"}, map[string]string{"hello.txt": "hi\n"})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:cross-repo-diff?output=diff").AddTokenAuth(user13Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
assert.Contains(t, resp.Body.String(), "diff --git ")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/structs"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/tests"
|
||||
@@ -303,3 +305,61 @@ func TestAPIGetTeamRepo(t *testing.T) {
|
||||
AddTokenAuth(token5)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPITeamVisibilityAccess(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
insertTestTeam := func(t *testing.T, orgID int64, name string, visibility structs.VisibleType) *organization.Team {
|
||||
t.Helper()
|
||||
team := &organization.Team{
|
||||
OrgID: orgID,
|
||||
LowerName: name,
|
||||
Name: name,
|
||||
AccessMode: perm.AccessModeRead,
|
||||
Visibility: visibility,
|
||||
}
|
||||
assert.NoError(t, db.Insert(t.Context(), team))
|
||||
return team
|
||||
}
|
||||
|
||||
limitedTeam := insertTestTeam(t, 3, "limited-team", structs.VisibleTypeLimited)
|
||||
|
||||
// Org member who can read a limited team must not mutate its repos without membership.
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
token := getUserToken(t, user4.Name, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req := NewRequestf(t, "PUT", "/api/v1/teams/%d/repos/org3/repo3", limitedTeam.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
publicTeam := insertTestTeam(t, 23, "public-team", structs.VisibleTypePublic)
|
||||
|
||||
// Public team in a private org must not be readable by outsiders.
|
||||
outsider := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
token = getUserToken(t, outsider.Name, auth_model.AccessTokenScopeReadOrganization)
|
||||
req = NewRequestf(t, "GET", "/api/v1/teams/%d", publicTeam.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// Member lookup must require org membership even for public teams.
|
||||
req = NewRequestf(t, "GET", "/api/v1/teams/%d/members/%s", publicTeam.ID, outsider.Name).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// A limited team's repo list must not leak repos the viewer cannot access.
|
||||
// repo3 is private; user28 is an org3 member (team12, no repo access) who can
|
||||
// read the limited team but has no access to repo3.
|
||||
assert.NoError(t, db.Insert(t.Context(), &organization.TeamRepo{OrgID: 3, TeamID: limitedTeam.ID, RepoID: 3}))
|
||||
user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
|
||||
token28 := getUserToken(t, user28.Name, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/teams/%d/repos", limitedTeam.ID).AddTokenAuth(token28)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var repos []*api.Repository
|
||||
DecodeJSON(t, resp, &repos)
|
||||
for _, r := range repos {
|
||||
assert.NotEqual(t, int64(3), r.ID, "must not leak inaccessible private repo3")
|
||||
}
|
||||
|
||||
// The single-repo lookup must not confirm an inaccessible repo's existence.
|
||||
req = NewRequestf(t, "GET", "/api/v1/teams/%d/repos/org3/repo3", limitedTeam.ID).AddTokenAuth(token28)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAPIGetCurrentToken tests getting metadata of the currently authenticated token
|
||||
func TestAPIGetCurrentToken(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("Success with all scopes", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-get-current-token-all", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
currentToken := DecodeJSON(t, resp, &api.CurrentAccessToken{})
|
||||
assert.Equal(t, accessToken.ID, currentToken.ID)
|
||||
assert.Equal(t, accessToken.Name, currentToken.Name)
|
||||
assert.Equal(t, user.ID, currentToken.User.ID)
|
||||
assert.Equal(t, user.Name, currentToken.User.Login)
|
||||
})
|
||||
|
||||
t.Run("Success with limited scopes", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-get-current-token-limited", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeReadRepository})
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
currentToken := DecodeJSON(t, resp, &api.CurrentAccessToken{})
|
||||
assert.Equal(t, accessToken.ID, currentToken.ID)
|
||||
assert.Equal(t, accessToken.Name, currentToken.Name)
|
||||
assert.Equal(t, user.ID, currentToken.User.ID)
|
||||
assert.Equal(t, user.Name, currentToken.User.Login)
|
||||
})
|
||||
|
||||
t.Run("Bad token", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth("this does not exist")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/token")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPITokenSelfService tests delete operations on token
|
||||
func TestAPITokenSelfService(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("Success then verify deleted", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-delete-current-token", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
|
||||
|
||||
// Delete the token via the endpoint
|
||||
req := NewRequest(t, "DELETE", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Verify the token is deleted
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: accessToken.ID})
|
||||
|
||||
// Verify the token can no longer be used for GET
|
||||
req = NewRequest(t, "GET", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
// Verify the token can no longer be used for DELETE
|
||||
req = NewRequest(t, "DELETE", "/api/v1/token").
|
||||
AddTokenAuth(accessToken.Token)
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Bad token", func(t *testing.T) {
|
||||
req := NewRequest(t, "DELETE", "/api/v1/token").
|
||||
AddTokenAuth("this does not exist")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
req = NewRequest(t, "DELETE", "/api/v1/token")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
@@ -167,6 +167,53 @@ Hello from 2
|
||||
assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length())
|
||||
}
|
||||
|
||||
func TestCompareDownloadDiffOrPatch(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
t.Run("BranchToBranchDiff", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b.diff")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
assert.Contains(t, resp.Body.String(), "diff --git ")
|
||||
})
|
||||
|
||||
t.Run("BranchToBranchPatch", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b.patch")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||
assert.True(t, strings.HasPrefix(resp.Body.String(), "From "), "patch output should start with a format-patch header")
|
||||
})
|
||||
|
||||
t.Run("SingleRefImplicitBase", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv.diff")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "diff --git ")
|
||||
})
|
||||
|
||||
t.Run("InvalidBaseRef", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo20/compare/does-not-exist...remove-files-b.diff")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("PrivateRepoAnonymous", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// repo16 is private; an unauthenticated request must not leak its existence.
|
||||
req := NewRequest(t, "GET", "/user2/repo16/compare/master...good-sign.diff")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompareCodeExpand(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
@@ -5,13 +5,12 @@ package integration
|
||||
|
||||
import (
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// HTMLDoc struct
|
||||
@@ -53,36 +52,10 @@ func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string
|
||||
|
||||
func assertHTMLEq(t testing.TB, expected, actual string) {
|
||||
t.Helper()
|
||||
if expected == actual {
|
||||
if expected == actual { // fast path
|
||||
return
|
||||
}
|
||||
exp, err := html.Parse(strings.NewReader(expected))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
act, err := html.Parse(strings.NewReader(actual))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
var normalize func(n *html.Node)
|
||||
normalize = func(n *html.Node) {
|
||||
slices.SortFunc(n.Attr, func(a, b html.Attribute) int {
|
||||
if cmp := strings.Compare(a.Namespace, b.Namespace); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
if cmp := strings.Compare(a.Key, b.Key); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return strings.Compare(a.Val, b.Val)
|
||||
})
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
normalize(c)
|
||||
}
|
||||
}
|
||||
normalize(exp)
|
||||
normalize(act)
|
||||
var expNormalized, actNormalized strings.Builder
|
||||
assert.NoError(t, html.Render(&expNormalized, exp))
|
||||
assert.NoError(t, html.Render(&actNormalized, act))
|
||||
assert.Equal(t, expNormalized.String(), actNormalized.String())
|
||||
exp := test.NormalizeHTMLAttributes(t, expected)
|
||||
act := test.NormalizeHTMLAttributes(t, actual)
|
||||
assert.Equal(t, exp, act)
|
||||
}
|
||||
|
||||
@@ -1272,7 +1272,7 @@ Commit description.
|
||||
commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`,
|
||||
},
|
||||
},
|
||||
expectedMessage: `* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...`,
|
||||
expectedMessage: "* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...\n\n",
|
||||
},
|
||||
{
|
||||
name: "Test Co-authored-by",
|
||||
|
||||
Reference in New Issue
Block a user