Files
Tosone 9ec5d37b28 refactor(server): move request validators under server
pkg/validators is the gin request-validation layer: it registers gin's
binding.Validator and is used by the HTTP handlers, so move it to
pkg/server/validators where the rest of the HTTP layer lives.

The only non-server consumer was pkg/background/daemon, which called
validators.Initialize() even though the worker never uses gin binding
validation. Drop that call so the worker does not depend on the server
package; the server still initializes validators through
handlers.Initialize.
2026-09-27 14:57:02 +08:00

606 lines
14 KiB
Go

// Copyright 2024 sigma
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils_test
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"sync"
"testing"
"github.com/gin-gonic/gin"
dtspecv1 "github.com/opencontainers/distribution-spec/specs-go/v1"
"github.com/opencontainers/go-digest"
"github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-sigma/sigma/pkg/api"
"github.com/go-sigma/sigma/pkg/api/enums"
"github.com/go-sigma/sigma/pkg/consts"
"github.com/go-sigma/sigma/pkg/dal/models"
"github.com/go-sigma/sigma/pkg/server/errcode"
"github.com/go-sigma/sigma/pkg/server/validators"
"github.com/go-sigma/sigma/pkg/utils"
)
func TestPanicIf(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()
utils.PanicIf(fmt.Errorf("test panic"))
}
func TestGetContentLength(t *testing.T) {
_, err := utils.GetContentLength(nil)
assert.Error(t, err)
req, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
if err != nil {
t.Fatal(err)
}
length, err := utils.GetContentLength(req)
if err != nil {
t.Fatal(err)
}
if length != 0 {
t.Errorf("expected 0, got %d", length)
}
req.Header.Set("Content-Length", "123")
length, err = utils.GetContentLength(req)
if err != nil {
t.Fatal(err)
}
if length != 123 {
t.Errorf("expected 123, got %d", length)
}
req.Header.Set("Content-Length", "abc")
_, err = utils.GetContentLength(req)
if err == nil {
t.Errorf("expected error, got nil")
}
}
func TestGenPathByDigest(t *testing.T) {
dgest, err := digest.Parse("sha256:08e7660f72aaa312f2ad1e13bc35afd988fa476052fd83296e0702e31ea00141")
assert.NoError(t, err)
assert.Equal(t, "08/e7/660f72aaa312f2ad1e13bc35afd988fa476052fd83296e0702e31ea00141", utils.GenPathByDigest(dgest))
assert.Equal(t, "sigma/blobs/08/e7/660f72aaa312f2ad1e13bc35afd988fa476052fd83296e0702e31ea00141", utils.GenBlobPathByDigest(dgest))
assert.Equal(t, "sigma/manifests/08/e7/660f72aaa312f2ad1e13bc35afd988fa476052fd83296e0702e31ea00141", utils.GenManifestPathByDigest(dgest))
}
func TestBindValidate(t *testing.T) {
require.NoError(t, validators.Initialize())
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"username":"test","password":"123498712311Aa!","email":"test@xx.com"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = req
type User struct {
Username string `json:"username" validate:"required,alphanum,min=2,max=20"`
Password string `json:"password" validate:"required,min=6,max=20"`
Email string `json:"email" validate:"required,email"`
}
var user User
err := utils.BindValidate(c, &user)
assert.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/",
bytes.NewBufferString(`{"username":"","password":"123498712311Aa!","email":"test@xx.com"}`))
req.Header.Set("Content-Type", "application/json")
rec = httptest.NewRecorder()
c, _ = gin.CreateTestContext(rec)
c.Request = req
err = utils.BindValidate(c, &user)
assert.Error(t, err)
}
func TestNormalizePagination(t *testing.T) {
type args struct {
in api.Pagination
}
tests := []struct {
name string
args args
want api.Pagination
}{
{
name: "test 1",
args: args{
in: api.Pagination{
Page: new(int(0)),
Limit: new(int(0)),
},
},
want: api.Pagination{
Page: new(int(1)),
Limit: new(int(10)),
},
},
{
name: "test 2",
args: args{
in: api.Pagination{
Page: new(int(-1)),
Limit: new(int(0)),
},
},
want: api.Pagination{
Page: new(int(1)),
Limit: new(int(10)),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := utils.NormalizePagination(tt.args.in); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NormalizePagination() = %v, want %v", got, tt.want)
}
})
}
}
func TestTrimHTTP(t *testing.T) {
type args struct {
in string
}
tests := []struct {
name string
args args
want string
}{
{
name: "common",
args: args{
in: "http://localhost:8080",
},
want: "localhost:8080",
},
{
name: "common-1",
args: args{
in: "https://localhost:8080",
},
want: "localhost:8080",
},
{
name: "common-2",
args: args{
in: "localhost:8080",
},
want: "localhost:8080",
},
{
name: "common-3",
args: args{
in: "localhost:8080/",
},
want: "localhost:8080",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := utils.TrimHTTP(tt.args.in); got != tt.want {
t.Errorf("TrimHTTP() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsDir(t *testing.T) {
convey.Convey("Check if given path is a directory", t, func() {
convey.Convey("Pass a file name", func() {
convey.So(utils.IsDir("file.go"), convey.ShouldEqual, false)
})
convey.Convey("Pass a directory name", func() {
convey.So(utils.IsDir("ptr"), convey.ShouldEqual, true)
})
convey.Convey("Pass a invalid path", func() {
convey.So(utils.IsDir("foo"), convey.ShouldEqual, false)
})
})
}
func TestIsFile(t *testing.T) {
if !utils.IsFile("utils.go") {
t.Errorf("IsExist:\n Expect => %v\n Got => %v\n", true, false)
}
if utils.IsFile("ptr") {
t.Errorf("IsExist:\n Expect => %v\n Got => %v\n", false, true)
}
if utils.IsFile("files.go") {
t.Errorf("IsExist:\n Expect => %v\n Got => %v\n", false, true)
}
}
func TestIsExist(t *testing.T) {
convey.Convey("Check if file or directory exists", t, func() {
convey.Convey("Pass a file name that exists", func() {
convey.So(utils.IsExist("utils.go"), convey.ShouldEqual, true)
})
convey.Convey("Pass a directory name that exists", func() {
convey.So(utils.IsExist("ptr"), convey.ShouldEqual, true)
})
convey.Convey("Pass a directory name that does not exist", func() {
convey.So(utils.IsExist(".hg"), convey.ShouldEqual, false)
})
})
}
func TestDirWithSlash(t *testing.T) {
type args struct {
id string
}
tests := []struct {
name string
args args
want string
}{
{
name: "common",
args: args{
id: "",
},
want: "",
},
{
name: "common",
args: args{
id: "1",
},
want: "1",
},
{
name: "common",
args: args{
id: "12",
},
want: "12",
},
{
name: "common",
args: args{
id: "123",
},
want: "12/3",
},
{
name: "common",
args: args{
id: "1234",
},
want: "12/34",
},
{
name: "common",
args: args{
id: "12345",
},
want: "12/34/5",
},
{
name: "common",
args: args{
id: "123456789",
},
want: "12/34/56789",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := utils.DirWithSlash(tt.args.id); got != tt.want {
t.Errorf("DirWithSlash() = %v, want %v", got, tt.want)
}
})
}
}
func TestStringsJoin(t *testing.T) {
type args struct {
strs []enums.OciPlatform
sep string
}
tests := []struct {
name string
args args
want string
}{
{
name: "common",
args: args{
strs: []enums.OciPlatform{},
sep: ",",
},
want: "",
},
{
name: "common",
args: args{
strs: []enums.OciPlatform{
enums.OciPlatformLinuxAmd64,
},
sep: ",",
},
want: "linux/amd64",
},
{
name: "common",
args: args{
strs: []enums.OciPlatform{
enums.OciPlatformLinux386,
enums.OciPlatformLinuxAmd64,
},
sep: ",",
},
want: "linux/386,linux/amd64",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := utils.StringsJoin(tt.args.strs, tt.args.sep); got != tt.want {
t.Errorf("StringsJoin() = %v, want %v", got, tt.want)
}
})
}
}
func TestUnwrapJoinedErrors(t *testing.T) {
type args struct {
err error
}
tests := []struct {
name string
args args
want string
}{
{
name: "normal",
args: args{
err: fmt.Errorf("normal error"),
},
want: "normal error",
},
{
name: "joined",
args: args{
err: errors.Join(fmt.Errorf("normal error"), fmt.Errorf("normal error2")),
},
want: "normal error: normal error2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := utils.UnwrapJoinedErrors(tt.args.err); got != tt.want {
t.Errorf("UnwrapJoinedErrors() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetUserFromCtx(t *testing.T) {
type args struct {
c *gin.Context
}
tests := []struct {
name string
format utils.UserCtxErrorFormat
args func(rec *httptest.ResponseRecorder) args
want *models.User
wantBool bool
respBody string
}{
{
name: "normal",
format: utils.UserCtxErrorHTTP,
args: func(rec *httptest.ResponseRecorder) args {
req := httptest.NewRequest(http.MethodPost, "/", nil)
c, _ := gin.CreateTestContext(rec)
c.Request = req
c.Set(consts.ContextUser, &models.User{Username: "test", Password: new("test"), Email: new("test@gmail.com")})
return args{c: c}
},
want: &models.User{Username: "test", Password: new("test"), Email: new("test@gmail.com")},
wantBool: false,
respBody: "",
},
{
name: "http-missing-user",
format: utils.UserCtxErrorHTTP,
args: func(rec *httptest.ResponseRecorder) args {
req := httptest.NewRequest(http.MethodPost, "/", nil)
c, _ := gin.CreateTestContext(rec)
c.Request = req
return args{c: c}
},
wantBool: true,
respBody: string(utils.MustMarshal(errcode.HTTPErrCodeUnauthorized)),
},
{
name: "http-invalid-user",
format: utils.UserCtxErrorHTTP,
args: func(rec *httptest.ResponseRecorder) args {
req := httptest.NewRequest(http.MethodPost, "/", nil)
c, _ := gin.CreateTestContext(rec)
c.Request = req
c.Set(consts.ContextUser, "test")
return args{c: c}
},
wantBool: true,
respBody: string(utils.MustMarshal(errcode.HTTPErrCodeUnauthorized)),
},
{
name: "distribution-normal",
format: utils.UserCtxErrorDistribution,
args: func(rec *httptest.ResponseRecorder) args {
req := httptest.NewRequest(http.MethodPost, "/", nil)
c, _ := gin.CreateTestContext(rec)
c.Request = req
c.Set(consts.ContextUser, &models.User{Username: "test", Password: new("test"), Email: new("test@gmail.com")})
return args{c: c}
},
want: &models.User{Username: "test", Password: new("test"), Email: new("test@gmail.com")},
wantBool: false,
respBody: "",
},
{
name: "distribution-missing-user",
format: utils.UserCtxErrorDistribution,
args: func(rec *httptest.ResponseRecorder) args {
req := httptest.NewRequest(http.MethodPost, "/", nil)
c, _ := gin.CreateTestContext(rec)
c.Request = req
return args{c: c}
},
wantBool: true,
respBody: string(utils.MustMarshal(dtspecv1.ErrorResponse{Errors: []dtspecv1.ErrorInfo{
{
Code: errcode.DSErrCodeUnauthorized.Code,
Message: errcode.DSErrCodeUnauthorized.Title,
Detail: errcode.DSErrCodeUnauthorized.Description,
},
}})),
},
{
name: "distribution-invalid-user",
format: utils.UserCtxErrorDistribution,
args: func(rec *httptest.ResponseRecorder) args {
req := httptest.NewRequest(http.MethodPost, "/", nil)
c, _ := gin.CreateTestContext(rec)
c.Request = req
c.Set(consts.ContextUser, "test")
return args{c: c}
},
wantBool: true,
respBody: string(utils.MustMarshal(dtspecv1.ErrorResponse{Errors: []dtspecv1.ErrorInfo{
{
Code: errcode.DSErrCodeUnauthorized.Code,
Message: errcode.DSErrCodeUnauthorized.Title,
Detail: errcode.DSErrCodeUnauthorized.Description,
},
}})),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rec := httptest.NewRecorder()
got, gotBool := utils.GetUserFromCtx(tt.args(rec).c, tt.format)
if gotBool != tt.wantBool {
t.Errorf("GetUserFromCtx() bool = %v, want %v", gotBool, tt.wantBool)
}
result := strings.TrimSpace(rec.Body.String())
if !reflect.DeepEqual(result, tt.respBody) {
t.Errorf("GetUserFromCtx() body = %v, want %v", result, tt.respBody)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetUserFromCtx() = %v, want %v", got, tt.want)
}
})
}
}
func TestOnceWithErr(t *testing.T) {
type args struct {
once *sync.Once
fn func() error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "normal",
args: args{
once: &sync.Once{},
fn: func() error {
fmt.Println("do something")
return nil
},
},
wantErr: false,
},
{
name: "panic",
args: args{
once: &sync.Once{},
fn: func() error {
panic("panic something")
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := utils.OnceWithErr(tt.args.once, tt.args.fn); (err != nil) != tt.wantErr {
t.Errorf("OnceWithErr() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGenRsaPriKey(t *testing.T) {
type args struct {
length int
}
tests := []struct {
name string
args args
eval func(*testing.T, string)
wantErr bool
}{
{
name: "normal",
args: args{length: 1024},
eval: func(t *testing.T, s string) {
pemBytes, err := base64.StdEncoding.DecodeString(s)
require.NoError(t, err)
block, _ := pem.Decode(pemBytes)
require.False(t, block == nil || block.Type != "RSA PRIVATE KEY")
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
require.NoError(t, err)
require.Equal(t, 1024, privateKey.N.BitLen())
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := utils.GenRsaPriKey(tt.args.length)
if (err != nil) != tt.wantErr {
t.Errorf("GenRsaPriKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.eval != nil {
tt.eval(t, got)
}
})
}
}