218 lines
6.4 KiB
Go
218 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"example.com/composition/internal/amount"
|
|
"example.com/composition/internal/config"
|
|
"example.com/composition/internal/convert"
|
|
"example.com/composition/internal/domain"
|
|
"example.com/composition/internal/handler"
|
|
"example.com/composition/internal/middleware"
|
|
"example.com/composition/internal/svc"
|
|
"example.com/composition/internal/types"
|
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
|
)
|
|
|
|
func logger(t *testing.T) *logx.Logger {
|
|
t.Helper()
|
|
cfg := logx.Default()
|
|
cfg.Writer = io.Discard
|
|
l, e := logx.New(cfg)
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
t.Cleanup(func() { _ = l.Close() })
|
|
return l
|
|
}
|
|
func TestHTTPComposition(t *testing.T) {
|
|
deps, e := svc.New(context.Background(), logger(t))
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
defer deps.Close()
|
|
app, e := handler.NewApp(deps)
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
defer app.Close()
|
|
for _, path := range []string{"/enterprise", "/tenant/123", "/admin/123"} {
|
|
for _, tc := range []struct {
|
|
name, body string
|
|
status int
|
|
salary, nameValue string
|
|
employer any
|
|
}{
|
|
{"exact", `{"name":"employee","salary":"90071992547409.91","employer":null,"enabled":false}`, 200, "90071992547409.91", "employee", nil},
|
|
{"round", `{"salary":"1.005","employer":"0"}`, 200, "1.01", "", "0.00"},
|
|
{"missing", `{}`, 200, "0.00", "", nil},
|
|
{"null", `{"name":null,"salary":null,"employer":null,"enabled":null}`, 200, "0.00", "", nil},
|
|
{"zero", `{"salary":"0","enabled":false}`, 200, "0.00", "", nil},
|
|
{"invalid", `{"salary":"abc"}`, 400, "", "", nil},
|
|
{"empty", `{"salary":""}`, 400, "", "", nil},
|
|
{"numeric", `{"salary":100}`, 400, "", "", nil},
|
|
{"flat", `{"SalaryFields":{"salary":"1"}}`, 400, "", "", nil},
|
|
{"validation", `{"name":"` + strings.Repeat("x", 81) + `"}`, 400, "", "", nil},
|
|
} {
|
|
t.Run(path+tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", path, strings.NewReader(tc.body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
app.Engine().ServeHTTP(w, req)
|
|
if w.Code != tc.status {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if w.Code == 200 {
|
|
var out map[string]any
|
|
if e := json.Unmarshal(w.Body.Bytes(), &out); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if out["salary"] != tc.salary || out["employeeName"] != tc.nameValue || out["employer"] != tc.employer || out["id"] != "9007199254740993" {
|
|
t.Fatal(out)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
func TestFullFormAndCopySemantics(t *testing.T) {
|
|
name := "employee"
|
|
money := amount.Money("2.00")
|
|
yes := true
|
|
req := types.EnterpriseRequest{Name: &name, Salary: &money, Enabled: &yes, Labels: []string{"one"}}
|
|
first, e := convert.Employee(req)
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
second, e := convert.Employee(types.EnterpriseRequest{})
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if first.EmployeeName != "employee" || !first.Enabled || second.EmployeeName != "" || second.Enabled || second.Salary.String() != "0.00" || second.Employer != nil {
|
|
t.Fatal("full-form update became patch behavior")
|
|
}
|
|
// Direct assignments retain standard Go aliasing, including scalar pointers.
|
|
first.Labels[0] = "shared"
|
|
if req.Labels[0] != "shared" {
|
|
t.Fatal("unexpected implicit deep copy")
|
|
}
|
|
req.Employer = &money
|
|
first, e = convert.Employee(req)
|
|
if e != nil || first.Employer != req.Employer {
|
|
t.Fatal("nullable pointer changed", e)
|
|
}
|
|
bad := "reject"
|
|
failed, e := convert.Employee(types.EnterpriseRequest{Name: &bad})
|
|
if !errors.Is(e, convert.ErrName) || failed.EmployeeName != "" || !strings.Contains(e.Error(), "EmployeeName") {
|
|
t.Fatal("converter cause/zero result lost", e)
|
|
}
|
|
empty, e := convert.ToBatch(domain.Batch{})
|
|
if e != nil || empty.Rows != nil {
|
|
t.Fatal("nil slice changed", e)
|
|
}
|
|
src := domain.Batch{Rows: []domain.Row{}, Fixed: [2]domain.Row{{Value: "a"}, {Value: "b"}}, Optional: &name}
|
|
out, e := convert.ToBatch(src)
|
|
if e != nil || out.Rows == nil || len(out.Rows) != 0 || out.Fixed[1].Value != "b" || out.Optional != src.Optional {
|
|
t.Fatal(out, e)
|
|
}
|
|
src.Rows = []domain.Row{{Value: "one"}}
|
|
out, e = convert.ToBatch(src)
|
|
if e != nil || out.Rows[0].Value != "one" {
|
|
t.Fatal(out, e)
|
|
}
|
|
out.Rows[0].Value = "changed"
|
|
if src.Rows[0].Value != "one" {
|
|
t.Fatal("element mapping reused input slice")
|
|
}
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 32; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
result, err := convert.Employee(types.EnterpriseRequest{Name: &name, Salary: &money})
|
|
if err != nil || result.EmployeeName != name || result.Salary != money {
|
|
t.Error(result, err)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
func TestFactoryLifecycle(t *testing.T) {
|
|
for _, mode := range []string{"fail", "nil", "initFail", "panic"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
before := svc.Closes.Load()
|
|
cfg := config.Default()
|
|
cfg.FactoryMode = mode
|
|
if mode == "panic" {
|
|
func() {
|
|
defer func() {
|
|
if recover() != middleware.ErrFactory {
|
|
t.Error("panic cause changed")
|
|
}
|
|
}()
|
|
_, _ = svc.New(context.Background(), logger(t), cfg)
|
|
}()
|
|
} else {
|
|
deps, err := svc.New(context.Background(), logger(t), cfg)
|
|
if deps != nil || err == nil {
|
|
t.Fatal("factory did not fail closed")
|
|
}
|
|
if mode == "fail" && !errors.Is(err, middleware.ErrFactory) {
|
|
t.Fatal("factory cause lost", err)
|
|
}
|
|
if mode == "initFail" && !errors.Is(err, svc.ErrInit) {
|
|
t.Fatal("init cause lost", err)
|
|
}
|
|
}
|
|
if svc.Closes.Load() != before+1 {
|
|
t.Fatal("dependency not closed exactly once")
|
|
}
|
|
})
|
|
}
|
|
deps, e := svc.New(context.Background(), logger(t))
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if e = deps.Access().Check(context.Background()); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
before := svc.Closes.Load()
|
|
if e = deps.Close(); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if e = deps.Close(); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if svc.Closes.Load() != before+1 {
|
|
t.Fatal("normal close repeated")
|
|
}
|
|
}
|
|
func TestGeneratedEntryListenFailure(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
if err = os.WriteFile(path, []byte("server:\n address: "+listener.Addr().String()+"\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := svc.Closes.Load()
|
|
if err = run(context.Background(), path); err == nil {
|
|
t.Fatal("listen should fail")
|
|
}
|
|
if svc.Closes.Load() != before+1 {
|
|
t.Fatal("listen failure did not close dependencies")
|
|
}
|
|
}
|