mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-04 15:29:21 +02: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
|
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 != "" {
|
||||||
|
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)
|
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 != "" {
|
||||||
|
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)
|
assert.ErrorContains(t, err, c.expectedError)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,14 @@ 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"`
|
||||||
|
|
||||||
|
// 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"`
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user