Files
fun/regression_test.go
chiyi 41df889a1a v2.0.0: error-channel APIs, correctness fixes, and server hardening
BREAKING CHANGES:
- Guard interface: Guard(ctx Ctx) error; returning error short-circuits
  (subsequent guards and the method no longer execute)
- Wired[T]() (*T, error); New() may return error; failed wiring is sticky
- BindService/BindGuard/BindRoute return error; routes accept guards
  (guards receive merged query/form params as Ctx.State)
- registration panics after Start; Ctx.Ip honors X-Forwarded-For/X-Real-IP
- internal errors sanitized to fixed client messages

FIXES:
- int64 precision loss: /cell data decoded from raw JSON bytes and
  responses serialized with json.Number (no float64 round-trip)
- enum values range-checked before uint8 conversion (256 no longer
  truncates to 0 and slips through)
- logger: files failing name parsing are no longer deleted; log channel
  never blocks request goroutines; ConfigLogger is race-free;
  logWriterWorker unlock bug fixed
- stream writer panics recovered (process no longer crashes);
  streamDone closed exactly once
- generated Go client emits definitions for pointer-to-enum/struct fields
- anonymous structs rejected at registration; isPrivate safe on empty names
- removed dead Result.Id and RequestInfo.Type

ADDITIONS:
- graceful shutdown (Shutdown), StartOn, server timeouts by default
  (Read 60s/Idle 120s/Write off), SetTimeouts/SetMaxConcurrency
- big-int-safe JSON in the generated TS client (>2^53 as BigInt)
- example/ demo services and cmd/genexample artifact generator
2026-09-03 15:32:07 +08:00

248 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fun
// 本文件是 v1.3.3 之后五项修复的回归测试:
// 1. 枚举越界值先截断后判范围,256/512 等被洗成合法小值绕过校验
// 2. /cell 请求 data 经 map[string]any 往返,int64 超过 2^53 精度丢失
// 3. Guard 无 error 返回,无法干净短路
// 4. 依赖装配无 error 通道且容器留半初始化实例
// 5. 日志解析失败的文件名直接删除用户文件
import (
"errors"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
// ---- 1. 枚举:越界值必须被拒绝,不能截断洗白 ----
func TestEnumOutOfRangeRejected(t *testing.T) {
typ := reflect.TypeFor[BugStatus]() // Names: A, B → 合法值 0/1
mustReject := []any{
float64(-1), float64(2), float64(3), float64(255),
float64(256), float64(257), float64(258), float64(1e9), float64(1.5),
uint16(256), uint32(300), int(-2), int64(999),
}
for _, v := range mustReject {
if err := checkEnumValue(typ, v, "BugStatus"); err == nil {
t.Fatalf("value %v (%T) should be rejected", v, v)
}
}
mustPass := []any{float64(0), float64(1), uint8(1), int(0), int64(1)}
for _, v := range mustPass {
if err := checkEnumValue(typ, v, "BugStatus"); err != nil {
t.Fatalf("value %v (%T) should pass: %v", v, v, err)
}
}
}
// ---- 2. int64 精度:原始字节解码,不经 float64 ----
type PrecisionDto struct {
Id int64
}
type PrecisionSvc struct{}
func (s *PrecisionSvc) Get(dto PrecisionDto) (int64, error) { return dto.Id, nil }
func TestInt64PrecisionInvoke(t *testing.T) {
f := New()
if err := f.BindService(&PrecisionSvc{}); err != nil {
t.Fatal(err)
}
const big = int64(9007199254740993) // 2^53+1float64 无法精确表示
c := &Ctx{Ip: "1", MethodName: "Get", ServiceName: "PrecisionSvc"}
data := map[string]any{"id": float64(big)} // 校验视图只查存在性
c.Data = &data
c.rawData = []byte(`{"id":9007199254740993}`) // HTTP 路径的真实输入
var streamCh chan any
var streamDone chan struct{}
res, err := f.invoke(c, &streamCh, &streamDone)
if err != nil {
t.Fatalf("invoke: %v", err)
}
if got := (*res.Data).(int64); got != big {
t.Fatalf("precision lost: got %d want %d", got, big)
}
}
func TestInt64PrecisionE2E(t *testing.T) {
f := New()
if err := f.BindService(&PrecisionSvc{}); err != nil {
t.Fatal(err)
}
go f.Start(39011)
time.Sleep(300 * time.Millisecond)
resp, err := http.Post("http://127.0.0.1:39011/cell", "application/json",
strings.NewReader(`{"serviceName":"PrecisionSvc","methodName":"Get","data":{"id":9007199254740993}}`))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body := make([]byte, 512)
n, _ := resp.Body.Read(body)
if !strings.Contains(string(body[:n]), "9007199254740993") {
t.Fatalf("int64 precision lost in response: %s", string(body[:n]))
}
}
// ---- 3. Guard:返回 error 短路,业务方法不得执行 ----
var guardOrderLog []string
type OrderFirstGuard struct{}
func (g *OrderFirstGuard) Guard(ctx Ctx) error {
guardOrderLog = append(guardOrderLog, "first")
return nil
}
type OrderRejectGuard struct{}
func (g *OrderRejectGuard) Guard(ctx Ctx) error {
guardOrderLog = append(guardOrderLog, "reject")
return Error(4003, "denied")
}
type OrderLastGuard struct{}
func (g *OrderLastGuard) Guard(ctx Ctx) error {
guardOrderLog = append(guardOrderLog, "last") // 短路后不应执行
return nil
}
var guardedMethodRan bool
type GuardedSvc struct {
Ctx
}
func (s *GuardedSvc) Ping() error {
guardedMethodRan = true
return nil
}
func TestGuardShortCircuit(t *testing.T) {
f := New()
if err := f.BindService(&GuardedSvc{}, &OrderFirstGuard{}, &OrderRejectGuard{}, &OrderLastGuard{}); err != nil {
t.Fatal(err)
}
guardOrderLog = nil
guardedMethodRan = false
c := &Ctx{Ip: "1", MethodName: "Ping", ServiceName: "GuardedSvc"}
var streamCh chan any
var streamDone chan struct{}
_, err := f.invoke(c, &streamCh, &streamDone)
if err == nil {
t.Fatal("expected guard rejection")
}
if guardedMethodRan {
t.Fatal("business method must not run after guard rejection")
}
if len(guardOrderLog) != 2 || guardOrderLog[0] != "first" || guardOrderLog[1] != "reject" {
t.Fatalf("guards after rejection must not run: %v", guardOrderLog)
}
var result Result[any]
if !errors.As(err, &result) {
t.Fatalf("error should carry Result, got %T", err)
}
if result.Code == nil || *result.Code != 4003 || result.Status != errorCode {
t.Fatalf("unexpected result: %+v", result)
}
}
// ---- 4. 依赖装配:error 通道 + 粘性失败 ----
var wOkBoxNewCalled bool
type WOkBox struct{}
func (b *WOkBox) New() error { wOkBoxNewCalled = true; return nil }
type WFailBox struct{}
func (b *WFailBox) New() error { return errors.New("connect refused") }
type WParentBox struct {
Fail *WFailBox `fun:"auto"`
}
type WSvc struct {
Ctx
Fail *WFailBox
}
func (s *WSvc) Ping() error { return nil }
func TestWiredErrorAndStickyFailure(t *testing.T) {
old := fun
fun = New()
defer func() { fun = old }()
wOkBoxNewCalled = false
if _, err := Wired[WOkBox](); err != nil {
t.Fatalf("ok box: %v", err)
}
if !wOkBoxNewCalled {
t.Fatal("New() not called")
}
_, err := Wired[WFailBox]()
if err == nil || !strings.Contains(err.Error(), "connect refused") {
t.Fatalf("expected failure, got %v", err)
}
// 粘性错误:重复 Wired 返回同一错误,不重试
if _, err2 := Wired[WFailBox](); err2 == nil || err2.Error() != err.Error() {
t.Fatalf("sticky failure expected, got %v then %v", err, err2)
}
// 失败类型不暴露半初始化实例
if entry, ok := fun.boxes.Load(reflect.TypeFor[*WFailBox]()); ok {
if e := entry.(boxEntry); e.err == nil {
t.Fatal("failed box must not hold a usable instance")
}
}
}
func TestWiredNestedFailurePropagates(t *testing.T) {
old := fun
fun = New()
defer func() { fun = old }()
if _, err := Wired[WParentBox](); err == nil || !strings.Contains(err.Error(), "connect refused") {
t.Fatalf("nested failure should propagate, got %v", err)
}
}
func TestBindServiceWireFailurePropagates(t *testing.T) {
f := New()
if err := f.BindService(&WSvc{}); err == nil {
t.Fatal("BindService should propagate wiring failure")
}
}
// ---- 5. 日志:解析失败的文件名不删除 ----
func TestLoggerKeepsUnknownFiles(t *testing.T) {
dir := t.TempDir()
keep := filepath.Join(dir, "notes.txt")
if err := os.WriteFile(keep, []byte("keep me"), 0644); err != nil {
t.Fatal(err)
}
ConfigLogger(Logger{Level: TraceLevel, Mode: FileMode, LogFilePath: dir, ExpireLogsDays: 3})
defer ConfigLogger(Logger{Level: TraceLevel, Mode: TerminalMode})
// 写入路径:getNextLogFile 遍历目录解析文件名,旧逻辑会删除解析失败的文件
fileLogger("[test] hello")
// 清理路径
cleanupExpiredLogs()
if _, err := os.Stat(keep); err != nil {
t.Fatalf("unknown file deleted: %v", err)
}
}