package gateway import ( "context" "database/sql" "io" "log/slog" "path/filepath" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/db" ) // 本文件锁定裁决 1.3/10.4.2 的管理面一侧:面板的「工具策略」保存 // (POST /api/v1/admin/plugins/policy)真的**先落库再改内存**——重启后策略仍在。 // 落盘载体是 tool_policies 表(迁移 0012_tool_policy),由 registry 的 // PolicyStore 钩子写入;这里用真实 SQLite 库走完整 HTTP 路径。 // policyEnv 接线一个「策略已接存储」的能力注册表的网关,返回服务端地址、 // JWT 服务与库连接(用例直接查 tool_policies 断言落盘)。 func policyEnv(t *testing.T) (string, *auth.JWTService, *sql.DB) { t.Helper() ctx := context.Background() log := slog.New(slog.NewTextHandler(io.Discard, nil)) conn, err := db.Open(ctx, filepath.Join(t.TempDir(), "cyrene.db"), log) if err != nil { t.Fatalf("open sqlite: %v", err) } t.Cleanup(func() { conn.Close() }) if err := db.MigrateUp(ctx, conn, log); err != nil { t.Fatalf("MigrateUp: %v", err) } store, err := capreg.NewSQLitePolicyStore(conn, log) if err != nil { t.Fatalf("policy store: %v", err) } reg := capreg.New() if err := reg.AttachPolicyStore(ctx, store); err != nil { t.Fatalf("AttachPolicyStore: %v", err) } if err := reg.RegisterTool(stubTool{}); err != nil { t.Fatalf("register stub tool: %v", err) } _, ts, jwt := buildEnv(t, nil, Deps{Registry: reg}) return ts.URL, jwt, conn } // TestAdminSetPolicyPersistsToToolPolicies 锁定「面板保存 → 真落库」: // 走真实 HTTP 端点写入后,tool_policies 里有对应的行。 func TestAdminSetPolicyPersistsToToolPolicies(t *testing.T) { t.Parallel() ctx := context.Background() url, jwt, conn := policyEnv(t) owner := signAny(t, jwt, "acct", auth.RoleOwner) resp, body := doReq(t, "POST", url+"/api/v1/admin/plugins/policy", map[string]any{"name": "echo", "allowInGroup": true, "allowInStranger": false}, owner) if resp.StatusCode != 200 { t.Fatalf("POST policy status = %d, body = %s", resp.StatusCode, body) } var ( group bool stranger bool ) if err := conn.QueryRowContext(ctx, `SELECT allow_in_group, allow_in_stranger FROM tool_policies WHERE name = 'echo'`). Scan(&group, &stranger); err != nil { t.Fatalf("tool_policies 里没有 echo 的策略行(未落盘?): %v", err) } if !group || stranger { t.Fatalf("落盘策略 = {group:%v stranger:%v}, want {true false}", group, stranger) } } // TestAdminSetPolicySurvivesRestart 跨「重启」验证:写完策略后另外构造一个 // 注册表重新 Load,标记仍在(这正是裁决 1.3 要求消灭的「重启即失」)。 func TestAdminSetPolicySurvivesRestart(t *testing.T) { t.Parallel() ctx := context.Background() url, jwt, conn := policyEnv(t) owner := signAny(t, jwt, "acct", auth.RoleOwner) if resp, body := doReq(t, "POST", url+"/api/v1/admin/plugins/policy", map[string]any{"name": "echo", "allowInGroup": false, "allowInStranger": true}, owner); resp.StatusCode != 200 { t.Fatalf("POST policy status = %d, body = %s", resp.StatusCode, body) } // 模拟重启:新注册表 + 新 store,只有 Load 一条路能拿到刚才的标记。 log := slog.New(slog.NewTextHandler(io.Discard, nil)) store, err := capreg.NewSQLitePolicyStore(conn, log) if err != nil { t.Fatalf("policy store (2): %v", err) } rebooted := capreg.New() if err := rebooted.AttachPolicyStore(ctx, store); err != nil { t.Fatalf("AttachPolicyStore (2): %v", err) } if err := rebooted.RegisterTool(stubTool{}); err != nil { t.Fatalf("RegisterTool (2): %v", err) } pol, err := rebooted.Policy("echo") if err != nil { t.Fatalf("Policy (2): %v", err) } if pol.AllowInGroup || !pol.AllowInStranger { t.Fatalf("重启后策略 = %+v, want {group:false stranger:true}", pol) } }