feat: complete contract-driven business development and MySQL integration

This commit is contained in:
2026-09-24 21:56:04 +08:00
parent f9afb8f8dd
commit f5478cdc76
108 changed files with 6557 additions and 550 deletions

View File

@@ -0,0 +1,22 @@
package middleware
import (
"context"
"git.yuntuo.hk/k2safe/go-omni/httpx"
)
// This package deliberately imports no svc; the dependency is supplied explicitly.
type AccessChecker interface {
CheckAccess(context.Context, string) error
}
func NewRequireAccess(checker AccessChecker) httpx.Middleware {
return func(next httpx.Handler) httpx.Handler {
return func(c *httpx.Context) error {
if err := checker.CheckAccess(c.Context(), c.Header("Authorization")); err != nil {
return err
}
return next(c)
}
}
}

View File

@@ -0,0 +1,32 @@
package svc
import (
"context"
"example.com/business/internal/middleware"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"sync/atomic"
)
// A local fixture checks wiring; production authentication is project-owned.
type accessClient struct{ closed atomic.Bool }
func (c *accessClient) Close() error { c.closed.Store(true); return nil }
func (c *accessClient) CheckAccess(ctx context.Context, token string) error {
if c.closed.Load() {
return apierrors.New(503, "CLOSED", "closed")
}
if httpx.Metadata(ctx).Route == "" {
return apierrors.New(500, "NO_METADATA", "missing route")
}
if token == "reject" {
return apierrors.New(403, "DENIED", "denied")
}
return nil
}
func (c *ServiceContext) initAccess(ctx context.Context) error {
c.Client = &accessClient{}
c.OnClose(c.Client.Close)
c.Middlewares["RequireAccess"] = middleware.NewRequireAccess(c.Client)
return nil
}

View File

@@ -0,0 +1,119 @@
package main
import (
"bytes"
"context"
"encoding/json"
"example.com/business/internal/config"
"example.com/business/internal/handler"
"example.com/business/internal/svc"
"git.yuntuo.hk/k2safe/go-omni/logx"
"io"
"mime/multipart"
"net/http/httptest"
"strings"
"testing"
)
func TestShapesAndPolicy(t *testing.T) {
cfg := config.Default()
cfg.Server.Log.Writer = io.Discard
logger, err := logx.New(cfg.Server.Log)
if err != nil {
t.Fatal(err)
}
defer logger.Close()
deps, err := svc.New(context.Background(), logger, cfg)
if err != nil {
t.Fatal(err)
}
defer deps.Close()
app, err := handler.NewApp(deps)
if err != nil {
t.Fatal(err)
}
defer app.Close()
for _, tt := range []struct {
method, path, body, media string
status int
data string
raw bool
}{
{"GET", "/list", "", "", 200, `[]`, false},
{"GET", "/list?mode=full&tags=a&tags=b", "", "", 200, `[{"name":"one"}]`, false},
{"GET", "/nullable", "", "", 200, `null`, false},
{"GET", "/nullable?mode=full", "", "", 200, `{"name":"one"}`, false},
{"POST", "/nothing", "", "", 200, `null`, false},
{"GET", "/rawNull", "", "", 200, `null`, true},
{"GET", "/rawArray", "", "", 200, `[]`, true},
{"DELETE", "/noContent", "", "", 204, "", true},
{"POST", "/empty", `{}`, "application/json", 200, `null`, false},
{"POST", "/empty", ``, "application/json", 400, "", false},
{"POST", "/empty", `{"unknown":1}`, "application/json", 400, "", false},
{"POST", "/absent", ``, "", 200, `null`, false},
{"POST", "/absent", `{}`, "application/json", 400, "", false},
{"POST", "/optional", ``, "", 200, `{"name":""}`, false},
{"POST", "/optional", `{}`, "application/json", 400, "", false},
{"POST", "/echo", `{"name":"ok"}`, "application/json", 200, `{"name":"ok"}`, false},
{"POST", "/echo", `{"name":"ok"}`, "application/json; charset=latin1", 415, "", false},
{"POST", "/echo", "{\"name\":\"\xff\"}", "application/json", 400, "", false},
{"GET", "/list?unknown=1", "", "", 400, "", false},
{"GET", "/rawNull?unknown=1", "", "", 400, "", false},
{"POST", "/tolerant?unknown=1", "{\"name\":\"\xff\"}", "application/json; charset=latin1", 200, `{"name":"<22>"}`, false},
} {
t.Run(tt.method+tt.path+tt.body+tt.media, func(t *testing.T) {
r := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.body))
if tt.media != "" {
r.Header.Set("Content-Type", tt.media)
}
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
if w.Code != tt.status {
t.Fatalf("%d %s", w.Code, w.Body)
}
if tt.status >= 400 {
return
}
data := strings.TrimSpace(w.Body.String())
if !tt.raw {
var obj map[string]json.RawMessage
if err := json.Unmarshal(w.Body.Bytes(), &obj); err != nil {
t.Fatal(err)
}
v, ok := obj["data"]
if !ok {
t.Fatal("missing data")
}
data = string(v)
}
if data != tt.data {
t.Fatalf("data %s expected %s", data, tt.data)
}
})
}
var body bytes.Buffer
form := multipart.NewWriter(&body)
_ = form.WriteField("title", "title")
_ = form.WriteField("metadata", `{"name":"meta"}`)
f, err := form.CreateFormFile("attachment", "a.txt")
if err != nil {
t.Fatal(err)
}
_, _ = io.WriteString(f, "content")
_ = form.Close()
r := httptest.NewRequest("POST", "/upload", &body)
r.Header.Set("Content-Type", form.FormDataContentType())
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
if w.Code != 200 || !strings.Contains(w.Body.String(), "title:meta:content") {
t.Fatal(w.Code, w.Body.String())
}
denied := httptest.NewRequest("GET", "/list", nil)
denied.Header.Set("Authorization", "reject")
result := httptest.NewRecorder()
app.ServeHTTP(result, denied)
if result.Code != 403 {
t.Fatal("injected middleware did not run", result.Code, result.Body.String())
}
}

View File

@@ -0,0 +1,42 @@
package demo
import (
"context"
"example.com/business/internal/types"
"io"
)
func (s *Logic) List(ctx context.Context, req *types.Query) ([]types.Item, error) {
if req.Mode == "full" {
return []types.Item{{Name: "one"}}, nil
}
return nil, nil
}
func (s *Logic) Nullable(ctx context.Context, req *types.Query) (*types.Item, error) {
if req.Mode == "full" {
return &types.Item{Name: "one"}, nil
}
return nil, nil
}
func (s *Logic) Nothing(ctx context.Context) error { return nil }
func (s *Logic) RawNull(ctx context.Context) error { return nil }
func (s *Logic) RawArray(ctx context.Context) ([]types.Item, error) { return nil, nil }
func (s *Logic) Empty(ctx context.Context) error { return nil }
func (s *Logic) Absent(ctx context.Context) error { return nil }
func (s *Logic) Optional(ctx context.Context, req *types.Item) (*types.Item, error) { return req, nil }
func (s *Logic) Echo(ctx context.Context, req *types.Item) (*types.Item, error) { return req, nil }
func (s *Logic) Tolerant(ctx context.Context, req *types.Item) (*types.Item, error) { return req, nil }
func (s *Logic) NoContent(ctx context.Context) error { return nil }
func (s *Logic) Upload(ctx context.Context, req *types.Upload) (*types.Item, error) {
f, err := req.Attachment.Open()
if err != nil {
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return &types.Item{Name: req.Title + ":" + req.Metadata.Name + ":" + string(data)}, nil
}