Files
go-omni/cmd/go-omni/apigen/testdata/composition/consumer_test.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")
}
}