180 lines
5.3 KiB
Go
180 lines
5.3 KiB
Go
package identity
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"agent-system-backend/internal/store"
|
||
)
|
||
|
||
// capture 把 stdout/stderr 重定向到管道并在 fn 返回后给出捕获文本。
|
||
func capture(t *testing.T, fn func()) (stdout, stderr string) {
|
||
t.Helper()
|
||
oldOut, oldErr := os.Stdout, os.Stderr
|
||
outR, outW, err1 := os.Pipe()
|
||
errR, errW, err2 := os.Pipe()
|
||
if err1 != nil || err2 != nil {
|
||
t.Fatalf("pipe: %v %v", err1, err2)
|
||
}
|
||
os.Stdout, os.Stderr = outW, errW
|
||
defer func() { os.Stdout, os.Stderr = oldOut, oldErr }()
|
||
done := make(chan struct{})
|
||
go func() {
|
||
defer close(done)
|
||
bo, _ := io.ReadAll(outR)
|
||
be, _ := io.ReadAll(errR)
|
||
stdout, stderr = string(bo), string(be)
|
||
}()
|
||
fn()
|
||
outW.Close()
|
||
errW.Close()
|
||
<-done
|
||
return stdout, stderr
|
||
}
|
||
|
||
// 探测契约(svc.sh svc_bin_has_identity):无参输出 usage 形态——同时含
|
||
// "identity" 与 "ensure",无服务态痕迹;未知子命令同样不落服务路径。
|
||
func TestUsageAndUnknownExitWithoutServe(t *testing.T) {
|
||
out, errS := capture(t, func() { Run(nil) })
|
||
_ = out
|
||
if !strings.Contains(errS, "identity") || !strings.Contains(errS, "ensure") {
|
||
t.Fatalf("usage 须同时含 identity 与 ensure:%q", errS)
|
||
}
|
||
if rc := Run(nil); rc != 2 {
|
||
t.Fatalf("无参 rc=%d,期望 2", rc)
|
||
}
|
||
if rc := Run([]string{"bogus", "--instance-id", "x"}); rc != 2 {
|
||
t.Fatalf("未知子命令 rc=%d,期望 2", rc)
|
||
}
|
||
}
|
||
|
||
func TestCapabilities(t *testing.T) {
|
||
out, _ := capture(t, func() { Run([]string{"capabilities"}) })
|
||
var doc Document
|
||
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &doc); err != nil {
|
||
t.Fatalf("capabilities 不是 JSON:%v %q", err, out)
|
||
}
|
||
if doc.Protocol != 1 || doc.Identity.Open != "existing-no-migrate" || doc.CLI.UnknownSubcommand != "reject" {
|
||
t.Fatalf("能力文档异常:%+v", doc)
|
||
}
|
||
if doc.Schema == nil || doc.Schema.Target != store.SchemaTarget() || doc.Schema.CompatFloor != store.SchemaCompatFloor() {
|
||
t.Fatalf("schema 能力异常:%+v", doc.Schema)
|
||
}
|
||
}
|
||
|
||
func migratedData(t *testing.T) (string, int) {
|
||
t.Helper()
|
||
dir := t.TempDir()
|
||
data := filepath.Join(dir, "data")
|
||
st, err := store.OpenSQLite(filepath.Join(data, "agent-system.db"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ver, err := st.SchemaVersion()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
st.Close()
|
||
return data, ver
|
||
}
|
||
|
||
func TestEnsureVerifyRoundTripSQLite(t *testing.T) {
|
||
data, before := migratedData(t)
|
||
args := []string{"--data", data}
|
||
id := "svc-t1-abcdef123456"
|
||
if rc := Run(append([]string{"ensure", "--instance-id", id}, args...)); rc != 0 {
|
||
t.Fatalf("ensure rc=%d", rc)
|
||
}
|
||
out, _ := capture(t, func() {
|
||
if rc := Run(append([]string{"verify", "--instance-id", id}, args...)); rc != 0 {
|
||
t.Fatalf("verify rc=%d", rc)
|
||
}
|
||
})
|
||
if !strings.Contains(out, "schema_version=") {
|
||
t.Fatalf("verify 未带 schema_version:%q", out)
|
||
}
|
||
st, err := store.OpenIdentity("", data)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer st.Close()
|
||
after, err := st.SchemaVersion()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if after != before {
|
||
t.Fatalf("身份命令改变了 schema:%d → %d", before, after)
|
||
}
|
||
if rc := Run(append([]string{"verify", "--instance-id", "svc-t1-zzzzzz999999"}, args...)); rc != 1 {
|
||
t.Fatalf("错 id 须 rc=1,得 %d", rc)
|
||
}
|
||
if rc := Run(append([]string{"verify", "--instance-id", id, "--dbref", "pg"}, args...)); rc != 1 {
|
||
t.Fatalf("错 dbref 须 rc=1,得 %d", rc)
|
||
}
|
||
}
|
||
|
||
func TestVerifyMissingDBCreatesNothing(t *testing.T) {
|
||
dir := t.TempDir()
|
||
missing := filepath.Join(dir, "absent")
|
||
if rc := Run([]string{"verify", "--instance-id", "svc-missing-aaaa", "--data", missing}); rc == 0 {
|
||
t.Fatal("缺库不应成功")
|
||
}
|
||
if _, err := os.Stat(missing); !os.IsNotExist(err) {
|
||
t.Fatalf("缺库路径被创建:%v", err)
|
||
}
|
||
if _, err := os.Stat(filepath.Join(missing, "agent-system.db")); !os.IsNotExist(err) {
|
||
t.Fatal("缺库时创建了数据库文件")
|
||
}
|
||
}
|
||
|
||
func TestDSNFileMode(t *testing.T) {
|
||
dir := t.TempDir()
|
||
good := filepath.Join(dir, "dsn")
|
||
if err := os.WriteFile(good, []byte("postgres://example\n"), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.Chmod(good, 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := ResolveDSN("", good)
|
||
if err != nil || got != "postgres://example" {
|
||
t.Fatalf("0600 文件:%q %v", got, err)
|
||
}
|
||
loose := filepath.Join(dir, "loose")
|
||
if err := os.WriteFile(loose, []byte("postgres://x"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.Chmod(loose, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := ResolveDSN("", loose); err == nil {
|
||
t.Fatal("0644 的 dsn 文件必须拒绝")
|
||
}
|
||
_, errS := capture(t, func() { ResolveDSN("postgres://flag", "") })
|
||
if !strings.Contains(errS, "警告") {
|
||
t.Fatalf("--dsn 应告警:%q", errS)
|
||
}
|
||
}
|
||
|
||
// DSN 交付纪律:空的 AGENT_SYSTEM_DSN 不干扰已有 SQLite 库。
|
||
func TestDSNEnvFallback(t *testing.T) {
|
||
t.Setenv("AGENT_SYSTEM_DSN", "")
|
||
data, _ := migratedData(t)
|
||
id := "svc-dsnenv-abcdef123456"
|
||
if rc := Run([]string{"ensure", "--instance-id", id, "--data", data}); rc != 0 {
|
||
t.Fatalf("ensure rc=%d", rc)
|
||
}
|
||
if rc := Run([]string{"verify", "--instance-id", id, "--data", data}); rc != 0 {
|
||
t.Fatalf("verify rc=%d", rc)
|
||
}
|
||
t.Setenv("AGENT_SYSTEM_DSN", "postgres://from-env")
|
||
got, err := ResolveDSN("", "")
|
||
if err != nil || got != "postgres://from-env" {
|
||
t.Fatalf("环境 DSN:%q %v", got, err)
|
||
}
|
||
}
|