mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 08:34:30 +01: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:
		
							parent
							
								
									8106d95577
								
							
						
					
					
						commit
						151ef80e28
					
				
							
								
								
									
										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
 | 
			
		||||
          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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								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))),)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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-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=
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,8 +32,7 @@ type Interface interface {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// DefaultJSONHandler default json handler
 | 
			
		||||
	DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
 | 
			
		||||
	DefaultJSONHandler = getDefaultJSONHandler()
 | 
			
		||||
 | 
			
		||||
	_ Interface = StdJSON{}
 | 
			
		||||
	_ Interface = JSONiter{}
 | 
			
		||||
 | 
			
		||||
@ -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())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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",
 | 
			
		||||
			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)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
 | 
			
		||||
@ -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": "[<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) {
 | 
			
		||||
		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,
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user