Files
Agent-System-Backend/internal/intentgate/gate_test.go
T
weichen 9a545b1e69 Add admin-issued accounts with forced first-login password change
Why: the user ruled out self-registration (2026-09-30): accounts are
created only by admins who set an initial password; first login (and
first login after any admin reset) must force a password change.

Evidence:
- migration v10 auth_accounts (users password/must_change/is_admin +
  sessions table; upgrade test walks v1<->v10 both directions).
- argon2id hash/verify with PHC encoding; store tests cover hash
  roundtrip, duplicate-owner conflict, must-change lifecycle, session
  keep/except semantics.
- e2e TestAdminAccountFlow: static login page reachable without auth,
  wrong password 401, login sets HttpOnly cookie + mustChangePassword,
  admin creates user (409 on duplicate), non-admin create 403, forced
  change clears flag and old password dies, admin reset revokes sessions
  and re-arms forced change, API-key channel untouched, 5-fail backoff
  429, logout kills session. Full go test ./... green.

Impact: middleware now accepts Bearer key OR session cookie and exempts
static + /api/auth/login (also fixes the latent case where the login
page itself could 401). Embedded no-auth mode unchanged.

Refs: proposal 2026-09-30-ui-redesign-and-auth.md section 5 (admin-issued
variant, user decision 2026-09-30)
2026-09-30 11:15:22 +00:00

68 lines
1.9 KiB
Go

package intentgate
import (
"context"
"encoding/json"
"testing"
)
func raw(t *testing.T, v any) json.RawMessage {
t.Helper()
b, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
return b
}
// spec 采用 agent-intent overlay 形态(matcher 嵌套)。
func demoSpec(t *testing.T) json.RawMessage {
return raw(t, map[string]any{
"id": "demo.show", "domain": "demo", "version": 1,
"description": "示例意图",
"matcher": map[string]any{"includes_any": []string{"示例", "演示"}},
})
}
func TestGateMatchAndFallback(t *testing.T) {
g := New()
if _, id := g.Match(context.Background(), "任意"); id != "" || g.Enabled() {
t.Error("empty gate should passthrough")
}
if err := g.Rebuild(context.Background(), []json.RawMessage{demoSpec(t)}); err != nil {
t.Fatalf("rebuild: %v", err)
}
if !g.Enabled() || g.SpecCount() != 1 {
t.Fatalf("enabled=%v specs=%d", g.Enabled(), g.SpecCount())
}
branch, top := g.Match(context.Background(), "来个示例看看")
if branch != "matched" || top != "demo.show" {
t.Errorf("want matched/demo_intent, got %s/%s", branch, top)
}
branch, _ = g.Match(context.Background(), "完全无关的一句话")
if branch != "fallback" {
t.Errorf("want fallback, got %s", branch)
}
}
// fail-closed:坏 spec 保留上一快照。
func TestGateFailClosed(t *testing.T) {
g := New()
if err := g.Rebuild(context.Background(), []json.RawMessage{demoSpec(t)}); err != nil {
t.Fatal(err)
}
if err := g.Rebuild(context.Background(), []json.RawMessage{json.RawMessage(`{"id":`)}); err == nil {
t.Error("broken spec should error")
}
if !g.Enabled() || g.SpecCount() != 1 {
t.Error("previous snapshot must be kept on failed rebuild")
}
// 空重建 = 显式清空(卸载全部插件意图)。
if err := g.Rebuild(context.Background(), nil); err != nil {
t.Fatal(err)
}
if g.Enabled() {
t.Error("empty rebuild should disable gate")
}
}