mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	use experimental go json v2 library (#35392)
details: https://pkg.go.dev/encoding/json/v2 --------- Co-authored-by: techknowlogick <matti@mdranta.net> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		
							
								
								
									
										6
									
								
								.github/workflows/pull-db-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/pull-db-tests.yml
									
									
									
									
										vendored
									
									
								
							| @@ -72,13 +72,13 @@ jobs: | |||||||
|           go-version-file: go.mod |           go-version-file: go.mod | ||||||
|           check-latest: true |           check-latest: true | ||||||
|       - run: make deps-backend |       - run: make deps-backend | ||||||
|       - run: make backend |       - run: GOEXPERIMENT='' make backend | ||||||
|         env: |         env: | ||||||
|           TAGS: bindata gogit sqlite sqlite_unlock_notify |           TAGS: bindata gogit sqlite sqlite_unlock_notify | ||||||
|       - name: run migration tests |       - name: run migration tests | ||||||
|         run: make test-sqlite-migration |         run: make test-sqlite-migration | ||||||
|       - name: run tests |       - name: run tests | ||||||
|         run: make test-sqlite |         run: GOEXPERIMENT='' make test-sqlite | ||||||
|         timeout-minutes: 50 |         timeout-minutes: 50 | ||||||
|         env: |         env: | ||||||
|           TAGS: bindata gogit sqlite sqlite_unlock_notify |           TAGS: bindata gogit sqlite sqlite_unlock_notify | ||||||
| @@ -142,7 +142,7 @@ jobs: | |||||||
|           RACE_ENABLED: true |           RACE_ENABLED: true | ||||||
|           GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} |           GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} | ||||||
|       - name: unit-tests-gogit |       - name: unit-tests-gogit | ||||||
|         run: make unit-test-coverage test-check |         run: GOEXPERIMENT='' make unit-test-coverage test-check | ||||||
|         env: |         env: | ||||||
|           TAGS: bindata gogit |           TAGS: bindata gogit | ||||||
|           RACE_ENABLED: true |           RACE_ENABLED: true | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
									
									
									
									
								
							| @@ -18,6 +18,10 @@ DIST := dist | |||||||
| DIST_DIRS := $(DIST)/binaries $(DIST)/release | DIST_DIRS := $(DIST)/binaries $(DIST)/release | ||||||
| IMPORT := code.gitea.io/gitea | 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 | GO ?= go | ||||||
| SHASUM ?= shasum -a 256 | SHASUM ?= shasum -a 256 | ||||||
| HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) | HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) | ||||||
| @@ -766,7 +770,7 @@ generate-go: $(TAGS_PREREQ) | |||||||
|  |  | ||||||
| .PHONY: security-check | .PHONY: security-check | ||||||
| security-check: | security-check: | ||||||
| 	go run $(GOVULNCHECK_PACKAGE) -show color ./... | 	GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... | ||||||
|  |  | ||||||
| $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) | $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) | ||||||
| ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) | ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -277,7 +277,7 @@ require ( | |||||||
| 	go.uber.org/zap v1.27.0 // indirect | 	go.uber.org/zap v1.27.0 // indirect | ||||||
| 	go.uber.org/zap/exp v0.3.0 // indirect | 	go.uber.org/zap/exp v0.3.0 // indirect | ||||||
| 	go4.org v0.0.0-20230225012048-214862532bf5 // 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/mod v0.27.0 // indirect | ||||||
| 	golang.org/x/time v0.12.0 // indirect | 	golang.org/x/time v0.12.0 // indirect | ||||||
| 	golang.org/x/tools v0.36.0 // indirect | 	golang.org/x/tools v0.36.0 // indirect | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								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-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-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-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-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= | ||||||
| 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/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-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.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | ||||||
| golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= | golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= | ||||||
|   | |||||||
| @@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error { | |||||||
| 	if err = embedFiles(meta.Root, fsRootPath, ""); err != nil { | 	if err = embedFiles(meta.Root, fsRootPath, ""); err != nil { | ||||||
| 		return err | 		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 { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	_, _ = output.Write([]byte{'\n'}) | 	_, _ = output.Write([]byte{'\n'}) | ||||||
| 	_, err = output.Write(jsonBuf) | 	_, err = output.Write(bytes.TrimSpace(jsonBuf)) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|   | |||||||
| @@ -32,8 +32,7 @@ type Interface interface { | |||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	// DefaultJSONHandler default json handler | 	DefaultJSONHandler = getDefaultJSONHandler() | ||||||
| 	DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary} |  | ||||||
|  |  | ||||||
| 	_ Interface = StdJSON{} | 	_ Interface = StdJSON{} | ||||||
| 	_ Interface = JSONiter{} | 	_ Interface = JSONiter{} | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| package json | package json | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @@ -16,3 +17,12 @@ func TestGiteaDBJSONUnmarshal(t *testing.T) { | |||||||
| 	err = UnmarshalHandleDoubleEncode([]byte(""), &m) | 	err = UnmarshalHandleDoubleEncode([]byte(""), &m) | ||||||
| 	assert.NoError(t, err) | 	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()) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								modules/json/jsonlegacy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								modules/json/jsonlegacy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  | } | ||||||
							
								
								
									
										92
									
								
								modules/json/jsonv2.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								modules/json/jsonv2.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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} | ||||||
|  | } | ||||||
| @@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			endpoint:      "https://invalid-json-response.io", | 			endpoint:      "https://invalid-json-response.io", | ||||||
| 			expectedError: "invalid json", | 			expectedError: "/(invalid json|jsontext: invalid character)/", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			endpoint:      "https://valid-batch-request-download.io", | 			endpoint:      "https://valid-batch-request-download.io", | ||||||
| @@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) { | |||||||
| 				return nil | 				return nil | ||||||
| 			}) | 			}) | ||||||
| 			if c.expectedError != "" { | 			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 { | 			} else { | ||||||
| 				assert.NoError(t, err) | 				assert.NoError(t, err) | ||||||
| 			} | 			} | ||||||
| @@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			endpoint:      "https://invalid-json-response.io", | 			endpoint:      "https://invalid-json-response.io", | ||||||
| 			expectedError: "invalid json", | 			expectedError: "/(invalid json|jsontext: invalid character)/", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			endpoint:      "https://valid-batch-request-upload.io", | 			endpoint:      "https://valid-batch-request-upload.io", | ||||||
| @@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) { | |||||||
| 				return io.NopCloser(new(bytes.Buffer)), objectError | 				return io.NopCloser(new(bytes.Buffer)), objectError | ||||||
| 			}) | 			}) | ||||||
| 			if c.expectedError != "" { | 			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 { | 			} else { | ||||||
| 				assert.NoError(t, err) | 				assert.NoError(t, err) | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -15,12 +15,17 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type testSerializationStruct struct { | type testSerializationStruct struct { | ||||||
| 	NormalString string                  `json:"normal_string" yaml:"normal_string"` | 	NormalString string                `json:"normal_string" yaml:"normal_string"` | ||||||
| 	NormalBool   bool                    `json:"normal_bool" yaml:"normal_bool"` | 	NormalBool   bool                  `json:"normal_bool" yaml:"normal_bool"` | ||||||
| 	OptBool      optional.Option[bool]   `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` | 	OptBool      optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` | ||||||
| 	OptString    optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,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"` | 	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) { | func TestOptionalToJson(t *testing.T) { | ||||||
| @@ -32,7 +37,7 @@ func TestOptionalToJson(t *testing.T) { | |||||||
| 		{ | 		{ | ||||||
| 			name: "empty", | 			name: "empty", | ||||||
| 			obj:  new(testSerializationStruct), | 			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", | 			name: "some", | ||||||
| @@ -44,12 +49,12 @@ func TestOptionalToJson(t *testing.T) { | |||||||
| 				OptTwoBool:   optional.None[bool](), | 				OptTwoBool:   optional.None[bool](), | ||||||
| 				OptTwoString: optional.None[string](), | 				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 { | 	for _, tc := range tests { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		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.NoError(t, err) | ||||||
| 			assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected") | 			assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected") | ||||||
|  |  | ||||||
| @@ -75,7 +80,7 @@ func TestOptionalFromJson(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name: "some", | 			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{ | 			want: testSerializationStruct{ | ||||||
| 				NormalString: "a string", | 				NormalString: "a string", | ||||||
| 				NormalBool:   true, | 				NormalBool:   true, | ||||||
| @@ -169,7 +174,7 @@ normal_bool: true | |||||||
| optional_bool: false | optional_bool: false | ||||||
| optional_string: "" | optional_string: "" | ||||||
| optional_two_bool: null | optional_two_bool: null | ||||||
| optional_twostring: null | optional_two_string: null | ||||||
| `, | `, | ||||||
| 			want: testSerializationStruct{ | 			want: testSerializationStruct{ | ||||||
| 				NormalString: "a string", | 				NormalString: "a string", | ||||||
|   | |||||||
| @@ -103,7 +103,9 @@ func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) { | |||||||
|  |  | ||||||
| func parseOCIImageConfig(r io.Reader) (*Metadata, error) { | func parseOCIImageConfig(r io.Reader) (*Metadata, error) { | ||||||
| 	var image oci.Image | 	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 | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,6 +22,8 @@ func TestParseImageConfig(t *testing.T) { | |||||||
| 	repositoryURL := "https://gitea.com/gitea" | 	repositoryURL := "https://gitea.com/gitea" | ||||||
| 	documentationURL := "https://docs.gitea.com" | 	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"}]}` | 	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)) | 	metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI)) | ||||||
|   | |||||||
| @@ -131,24 +131,74 @@ func TestWebhookDeliverHookTask(t *testing.T) { | |||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	done := make(chan struct{}, 1) | 	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": "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] user1 pushed 2 commits to <a href=\"http://localhost:3000/test/repo/src/branch/test\">test</a>:<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: 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) { | 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		assert.Equal(t, "PUT", r.Method) | 		assert.Equal(t, "PUT", r.Method) | ||||||
| 		switch r.URL.Path { | 		assert.True(t, strings.HasPrefix(r.URL.Path, "/webhook/")) | ||||||
| 		case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98": | 		assert.Len(t, r.URL.Path, len("/webhook/")+40) // +40 for txnID, a unique ID from payload's sha1 hash | ||||||
| 			// Version 1 | 		switch testVersion { | ||||||
|  | 		case 1: // Version 1 | ||||||
| 			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) | 			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) | ||||||
| 			assert.Empty(t, r.Header.Get("Content-Type")) | 			assert.Empty(t, r.Header.Get("Content-Type")) | ||||||
| 			body, err := io.ReadAll(r.Body) | 			body, err := io.ReadAll(r.Body) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 			assert.Equal(t, `{"data": 42}`, string(body)) | 			assert.Equal(t, `{"data": 42}`, string(body)) | ||||||
|  |  | ||||||
| 		case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51": | 		case 2: // Version 2 | ||||||
| 			// Version 2 |  | ||||||
| 			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) | 			assert.Equal(t, "push", r.Header.Get("X-GitHub-Event")) | ||||||
| 			assert.Equal(t, "application/json", r.Header.Get("Content-Type")) | 			assert.Equal(t, "application/json", r.Header.Get("Content-Type")) | ||||||
| 			body, err := io.ReadAll(r.Body) | 			body, err := io.ReadAll(r.Body) | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 			assert.Len(t, body, 2147) | 			assert.JSONEq(t, version2Body, string(body)) | ||||||
|  |  | ||||||
| 		default: | 		default: | ||||||
| 			w.WriteHeader(http.StatusNotFound) | 			w.WriteHeader(http.StatusNotFound) | ||||||
| @@ -172,6 +222,7 @@ func TestWebhookDeliverHookTask(t *testing.T) { | |||||||
| 	assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook)) | 	assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook)) | ||||||
|  |  | ||||||
| 	t.Run("Version 1", func(t *testing.T) { | 	t.Run("Version 1", func(t *testing.T) { | ||||||
|  | 		testVersion = 1 | ||||||
| 		hookTask := &webhook_model.HookTask{ | 		hookTask := &webhook_model.HookTask{ | ||||||
| 			HookID:         hook.ID, | 			HookID:         hook.ID, | ||||||
| 			EventType:      webhook_module.HookEventPush, | 			EventType:      webhook_module.HookEventPush, | ||||||
| @@ -198,6 +249,7 @@ func TestWebhookDeliverHookTask(t *testing.T) { | |||||||
| 		data, err := p.JSONPayload() | 		data, err := p.JSONPayload() | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 		testVersion = 2 | ||||||
| 		hookTask := &webhook_model.HookTask{ | 		hookTask := &webhook_model.HookTask{ | ||||||
| 			HookID:         hook.ID, | 			HookID:         hook.ID, | ||||||
| 			EventType:      webhook_module.HookEventPush, | 			EventType:      webhook_module.HookEventPush, | ||||||
|   | |||||||
| @@ -274,6 +274,7 @@ func getMessageBody(htmlText string) string { | |||||||
|  |  | ||||||
| // getMatrixTxnID computes the transaction ID to ensure idempotency | // getMatrixTxnID computes the transaction ID to ensure idempotency | ||||||
| func getMatrixTxnID(payload []byte) (string, error) { | func getMatrixTxnID(payload []byte) (string, error) { | ||||||
|  | 	payload = bytes.TrimSpace(payload) | ||||||
| 	if len(payload) >= matrixPayloadSizeLimit { | 	if len(payload) >= matrixPayloadSizeLimit { | ||||||
| 		return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit) | 		return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| package webhook | package webhook | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||||
| @@ -216,7 +217,9 @@ func TestMatrixJSONPayload(t *testing.T) { | |||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
|  |  | ||||||
| 	assert.Equal(t, "PUT", req.Method) | 	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, "sha256=", req.Header.Get("X-Hub-Signature-256")) | ||||||
| 	assert.Equal(t, "application/json", req.Header.Get("Content-Type")) | 	assert.Equal(t, "application/json", req.Header.Get("Content-Type")) | ||||||
| 	var body MatrixPayload | 	var body MatrixPayload | ||||||
|   | |||||||
| @@ -318,7 +318,7 @@ func TestPackageSwift(t *testing.T) { | |||||||
| 			AddBasicAuth(user.Name) | 			AddBasicAuth(user.Name) | ||||||
| 		resp = MakeRequest(t, req, http.StatusOK) | 		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) { | 	t.Run("PackageVersionMetadata", func(t *testing.T) { | ||||||
|   | |||||||
| @@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) { | |||||||
| 	resp = MakeRequest(t, req, http.StatusForbidden) | 	resp = MakeRequest(t, req, http.StatusForbidden) | ||||||
| 	bs, err = io.ReadAll(resp.Body) | 	bs, err = io.ReadAll(resp.Body) | ||||||
| 	assert.NoError(t, err) | 	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) | 	resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden) | ||||||
| 	bs, err = io.ReadAll(resp.Body) | 	bs, err = io.ReadAll(resp.Body) | ||||||
| 	assert.NoError(t, err) | 	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)) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -413,7 +413,8 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { | |||||||
| func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { | func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { | ||||||
| 	t.Helper() | 	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)) | 	require.NoError(t, decoder.Decode(v)) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user