diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index faf6a90e1b..a7ad7ed5c3 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -72,13 +72,13 @@ jobs: go-version-file: go.mod check-latest: true - run: make deps-backend - - run: make backend + - run: GOEXPERIMENT='' make backend env: TAGS: bindata gogit sqlite sqlite_unlock_notify - name: run migration tests run: make test-sqlite-migration - name: run tests - run: make test-sqlite + run: GOEXPERIMENT='' make test-sqlite timeout-minutes: 50 env: TAGS: bindata gogit sqlite sqlite_unlock_notify @@ -142,7 +142,7 @@ jobs: RACE_ENABLED: true GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} - name: unit-tests-gogit - run: make unit-test-coverage test-check + run: GOEXPERIMENT='' make unit-test-coverage test-check env: TAGS: bindata gogit RACE_ENABLED: true diff --git a/Makefile b/Makefile index 637e64210a..fc507367e7 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,10 @@ DIST := dist DIST_DIRS := $(DIST)/binaries $(DIST)/release IMPORT := code.gitea.io/gitea +# By default use go's 1.25 experimental json v2 library when building +# TODO: remove when no longer experimental +export GOEXPERIMENT ?= jsonv2 + GO ?= go SHASUM ?= shasum -a 256 HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) @@ -766,7 +770,7 @@ generate-go: $(TAGS_PREREQ) .PHONY: security-check security-check: - go run $(GOVULNCHECK_PACKAGE) -show color ./... + GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) diff --git a/go.mod b/go.mod index f32c3e08ef..73cbae7637 100644 --- a/go.mod +++ b/go.mod @@ -277,7 +277,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.36.0 // indirect diff --git a/go.sum b/go.sum index 1853693e90..f973e4bca5 100644 --- a/go.sum +++ b/go.sum @@ -848,8 +848,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= diff --git a/modules/assetfs/embed.go b/modules/assetfs/embed.go index 95176372d1..0b544635db 100644 --- a/modules/assetfs/embed.go +++ b/modules/assetfs/embed.go @@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error { if err = embedFiles(meta.Root, fsRootPath, ""); err != nil { return err } - jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL + jsonBuf, err := json.Marshal(meta) if err != nil { return err } _, _ = output.Write([]byte{'\n'}) - _, err = output.Write(jsonBuf) + _, err = output.Write(bytes.TrimSpace(jsonBuf)) return err } diff --git a/modules/json/json.go b/modules/json/json.go index 444dc8526a..deb869619b 100644 --- a/modules/json/json.go +++ b/modules/json/json.go @@ -32,8 +32,7 @@ type Interface interface { } var ( - // DefaultJSONHandler default json handler - DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary} + DefaultJSONHandler = getDefaultJSONHandler() _ Interface = StdJSON{} _ Interface = JSONiter{} diff --git a/modules/json/json_test.go b/modules/json/json_test.go index ace7167913..2fa4da4cf7 100644 --- a/modules/json/json_test.go +++ b/modules/json/json_test.go @@ -4,6 +4,7 @@ package json import ( + "bytes" "testing" "github.com/stretchr/testify/assert" @@ -16,3 +17,12 @@ func TestGiteaDBJSONUnmarshal(t *testing.T) { err = UnmarshalHandleDoubleEncode([]byte(""), &m) assert.NoError(t, err) } + +func TestIndent(t *testing.T) { + buf := &bytes.Buffer{} + err := Indent(buf, []byte(`{"a":1}`), ">", " ") + assert.NoError(t, err) + assert.Equal(t, `{ +> "a": 1 +>}`, buf.String()) +} diff --git a/modules/json/jsonlegacy.go b/modules/json/jsonlegacy.go new file mode 100644 index 0000000000..508e87b6b5 --- /dev/null +++ b/modules/json/jsonlegacy.go @@ -0,0 +1,24 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build !goexperiment.jsonv2 + +package json + +import ( + "io" + + jsoniter "github.com/json-iterator/go" +) + +func getDefaultJSONHandler() Interface { + return JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary} +} + +func MarshalKeepOptionalEmpty(v any) ([]byte, error) { + return DefaultJSONHandler.Marshal(v) +} + +func NewDecoderCaseInsensitive(reader io.Reader) Decoder { + return DefaultJSONHandler.NewDecoder(reader) +} diff --git a/modules/json/jsonv2.go b/modules/json/jsonv2.go new file mode 100644 index 0000000000..0bba2783bc --- /dev/null +++ b/modules/json/jsonv2.go @@ -0,0 +1,92 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build goexperiment.jsonv2 + +package json + +import ( + "bytes" + jsonv1 "encoding/json" //nolint:depguard // this package wraps it + jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it + "io" +) + +// JSONv2 implements Interface via encoding/json/v2 +// Requires GOEXPERIMENT=jsonv2 to be set at build time +type JSONv2 struct { + marshalOptions jsonv2.Options + marshalKeepOptionalEmptyOptions jsonv2.Options + unmarshalOptions jsonv2.Options + unmarshalCaseInsensitiveOptions jsonv2.Options +} + +var jsonV2 JSONv2 + +func init() { + commonMarshalOptions := []jsonv2.Options{ + jsonv2.FormatNilSliceAsNull(true), + jsonv2.FormatNilMapAsNull(true), + } + jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...) + jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2() + + // By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from. + // v1 has a different behavior: if the `""` is from a null pointer, or a Marshal function, it is kept. + // Golang issue: https://github.com/golang/go/issues/75623 encoding/json/v2: unable to make omitempty work with pointer or Optional type with goexperiment.jsonv2 + jsonV2.marshalKeepOptionalEmptyOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsonv1.OmitEmptyWithLegacySemantics(true))...) + + // Some legacy code uses case-insensitive matching (for example: parsing oci.ImageConfig) + jsonV2.unmarshalCaseInsensitiveOptions = jsonv2.JoinOptions(jsonv2.MatchCaseInsensitiveNames(true)) +} + +func getDefaultJSONHandler() Interface { + return &jsonV2 +} + +func MarshalKeepOptionalEmpty(v any) ([]byte, error) { + return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions) +} + +func (j *JSONv2) Marshal(v any) ([]byte, error) { + return jsonv2.Marshal(v, j.marshalOptions) +} + +func (j *JSONv2) Unmarshal(data []byte, v any) error { + return jsonv2.Unmarshal(data, v, j.unmarshalOptions) +} + +func (j *JSONv2) NewEncoder(writer io.Writer) Encoder { + return &jsonV2Encoder{writer: writer, opts: j.marshalOptions} +} + +func (j *JSONv2) NewDecoder(reader io.Reader) Decoder { + return &jsonV2Decoder{reader: reader, opts: j.unmarshalOptions} +} + +// Indent implements Interface using standard library (JSON v2 doesn't have Indent yet) +func (*JSONv2) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { + return jsonv1.Indent(dst, src, prefix, indent) +} + +type jsonV2Encoder struct { + writer io.Writer + opts jsonv2.Options +} + +func (e *jsonV2Encoder) Encode(v any) error { + return jsonv2.MarshalWrite(e.writer, v, e.opts) +} + +type jsonV2Decoder struct { + reader io.Reader + opts jsonv2.Options +} + +func (d *jsonV2Decoder) Decode(v any) error { + return jsonv2.UnmarshalRead(d.reader, v, d.opts) +} + +func NewDecoderCaseInsensitive(reader io.Reader) Decoder { + return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions} +} diff --git a/modules/lfs/http_client_test.go b/modules/lfs/http_client_test.go index 179bcdb29a..90b563ce2d 100644 --- a/modules/lfs/http_client_test.go +++ b/modules/lfs/http_client_test.go @@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) { }, { endpoint: "https://invalid-json-response.io", - expectedError: "invalid json", + expectedError: "/(invalid json|jsontext: invalid character)/", }, { endpoint: "https://valid-batch-request-download.io", @@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) { return nil }) if c.expectedError != "" { - assert.ErrorContains(t, err, c.expectedError) + if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") { + assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error()) + } else { + assert.ErrorContains(t, err, c.expectedError) + } } else { assert.NoError(t, err) } @@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) { }, { endpoint: "https://invalid-json-response.io", - expectedError: "invalid json", + expectedError: "/(invalid json|jsontext: invalid character)/", }, { endpoint: "https://valid-batch-request-upload.io", @@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) { return io.NopCloser(new(bytes.Buffer)), objectError }) if c.expectedError != "" { - assert.ErrorContains(t, err, c.expectedError) + if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") { + assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error()) + } else { + assert.ErrorContains(t, err, c.expectedError) + } } else { assert.NoError(t, err) } diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go index cf81a94cfc..c059294bbb 100644 --- a/modules/optional/serialization_test.go +++ b/modules/optional/serialization_test.go @@ -15,12 +15,17 @@ import ( ) type testSerializationStruct struct { - NormalString string `json:"normal_string" yaml:"normal_string"` - NormalBool bool `json:"normal_bool" yaml:"normal_bool"` - OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` - OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"` + NormalString string `json:"normal_string" yaml:"normal_string"` + NormalBool bool `json:"normal_bool" yaml:"normal_bool"` + OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` + + // It causes an undefined behavior: should the "omitempty" tag only omit "null", or also the empty string? + // The behavior is inconsistent between json and v2 packages, and there is no such use case in Gitea. + // If anyone really needs it, they can use json.MarshalKeepOptionalEmpty to revert the v1 behavior + OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"` + OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"` - OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"` + OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"` } func TestOptionalToJson(t *testing.T) { @@ -32,7 +37,7 @@ func TestOptionalToJson(t *testing.T) { { name: "empty", obj: new(testSerializationStruct), - want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`, + want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_two_string":null}`, }, { name: "some", @@ -44,12 +49,12 @@ func TestOptionalToJson(t *testing.T) { OptTwoBool: optional.None[bool](), OptTwoString: optional.None[string](), }, - want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - b, err := json.Marshal(tc.obj) + b, err := json.MarshalKeepOptionalEmpty(tc.obj) assert.NoError(t, err) assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected") @@ -75,7 +80,7 @@ func TestOptionalFromJson(t *testing.T) { }, { name: "some", - data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`, want: testSerializationStruct{ NormalString: "a string", NormalBool: true, @@ -169,7 +174,7 @@ normal_bool: true optional_bool: false optional_string: "" optional_two_bool: null -optional_twostring: null +optional_two_string: null `, want: testSerializationStruct{ NormalString: "a string", diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index 3ef0684d13..d8a48120af 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -103,7 +103,9 @@ func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) { func parseOCIImageConfig(r io.Reader) (*Metadata, error) { var image oci.Image - if err := json.NewDecoder(r).Decode(&image); err != nil { + // FIXME: JSON-KEY-CASE: here seems a abuse of the case-insensitive decoding feature, spec is case-sensitive + // https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json + if err := json.NewDecoderCaseInsensitive(r).Decode(&image); err != nil { return nil, err } diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index 0f2d702925..2a6389a8f6 100644 --- a/modules/packages/container/metadata_test.go +++ b/modules/packages/container/metadata_test.go @@ -22,6 +22,8 @@ func TestParseImageConfig(t *testing.T) { repositoryURL := "https://gitea.com/gitea" documentationURL := "https://docs.gitea.com" + // FIXME: JSON-KEY-CASE: the test case is not right, the config fields are capitalized in the spec + // https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}` metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI)) diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go index efbef1fc89..822184aa94 100644 --- a/services/webhook/deliver_test.go +++ b/services/webhook/deliver_test.go @@ -131,24 +131,74 @@ func TestWebhookDeliverHookTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) done := make(chan struct{}, 1) + version2Body := `{ + "body": "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", + "msgtype": "", + "format": "org.matrix.custom.html", + "formatted_body": "[test/repo] user1 pushed 2 commits to test:
2020558: commit message - user1
2020558: commit message - user1", + "io.gitea.commits": [ + { + "id": "2020558fe2e34debb818a514715839cabd25e778", + "message": "commit message", + "url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778", + "author": { + "name": "user1", + "email": "user1@localhost", + "username": "user1" + }, + "committer": { + "name": "user1", + "email": "user1@localhost", + "username": "user1" + }, + "verification": null, + "timestamp": "0001-01-01T00:00:00Z", + "added": null, + "removed": null, + "modified": null + }, + { + "id": "2020558fe2e34debb818a514715839cabd25e778", + "message": "commit message", + "url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778", + "author": { + "name": "user1", + "email": "user1@localhost", + "username": "user1" + }, + "committer": { + "name": "user1", + "email": "user1@localhost", + "username": "user1" + }, + "verification": null, + "timestamp": "0001-01-01T00:00:00Z", + "added": null, + "removed": null, + "modified": null + } + ] +}` + + testVersion := 0 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) - switch r.URL.Path { - case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98": - // Version 1 + assert.True(t, strings.HasPrefix(r.URL.Path, "/webhook/")) + assert.Len(t, r.URL.Path, len("/webhook/")+40) // +40 for txnID, a unique ID from payload's sha1 hash + switch testVersion { + case 1: // Version 1 assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) assert.Empty(t, r.Header.Get("Content-Type")) body, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, `{"data": 42}`, string(body)) - case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51": - // Version 2 + case 2: // Version 2 assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) body, err := io.ReadAll(r.Body) assert.NoError(t, err) - assert.Len(t, body, 2147) + assert.JSONEq(t, version2Body, string(body)) default: w.WriteHeader(http.StatusNotFound) @@ -172,6 +222,7 @@ func TestWebhookDeliverHookTask(t *testing.T) { assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook)) t.Run("Version 1", func(t *testing.T) { + testVersion = 1 hookTask := &webhook_model.HookTask{ HookID: hook.ID, EventType: webhook_module.HookEventPush, @@ -198,6 +249,7 @@ func TestWebhookDeliverHookTask(t *testing.T) { data, err := p.JSONPayload() assert.NoError(t, err) + testVersion = 2 hookTask := &webhook_model.HookTask{ HookID: hook.ID, EventType: webhook_module.HookEventPush, diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 3e9163f78c..57b1ece263 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -274,6 +274,7 @@ func getMessageBody(htmlText string) string { // getMatrixTxnID computes the transaction ID to ensure idempotency func getMatrixTxnID(payload []byte) (string, error) { + payload = bytes.TrimSpace(payload) if len(payload) >= matrixPayloadSizeLimit { return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit) } diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go index d36d93c5a7..59b78f4269 100644 --- a/services/webhook/matrix_test.go +++ b/services/webhook/matrix_test.go @@ -4,6 +4,7 @@ package webhook import ( + "strings" "testing" webhook_model "code.gitea.io/gitea/models/webhook" @@ -216,7 +217,9 @@ func TestMatrixJSONPayload(t *testing.T) { require.NoError(t, err) assert.Equal(t, "PUT", req.Method) - assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path) + txnID, ok := strings.CutPrefix(req.URL.Path, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/") + assert.True(t, ok) + assert.Len(t, txnID, 40) // txnID is just a unique ID for a webhook request, it is a sha1 hash from the payload assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) assert.Equal(t, "application/json", req.Header.Get("Content-Type")) var body MatrixPayload diff --git a/tests/integration/api_packages_swift_test.go b/tests/integration/api_packages_swift_test.go index ddc3cb63f3..71bb1befd1 100644 --- a/tests/integration/api_packages_swift_test.go +++ b/tests/integration/api_packages_swift_test.go @@ -318,7 +318,7 @@ func TestPackageSwift(t *testing.T) { AddBasicAuth(user.Name) resp = MakeRequest(t, req, http.StatusOK) - assert.Equal(t, body, resp.Body.String()) + assert.JSONEq(t, body, resp.Body.String()) }) t.Run("PackageVersionMetadata", func(t *testing.T) { diff --git a/tests/integration/api_repo_branch_test.go b/tests/integration/api_repo_branch_test.go index 066eb366b1..2438db72c5 100644 --- a/tests/integration/api_repo_branch_test.go +++ b/tests/integration/api_repo_branch_test.go @@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) { resp = MakeRequest(t, req, http.StatusForbidden) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) - assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs)) + assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs)) resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden) bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) - assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs)) + assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs)) } diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 5896a97ef1..9dfa0ccd5d 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -413,7 +413,8 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { t.Helper() - decoder := json.NewDecoder(resp.Body) + // FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names + decoder := json.NewDecoderCaseInsensitive(resp.Body) require.NoError(t, decoder.Decode(v)) }