package server import ( "net/http" "net/http/httptest" "strings" "testing" "time" "agent-system-backend/internal/store" ) func TestBorrowWritesCannotCommitAfterAccountDisable(t *testing.T) { for _, tc := range []struct { name, path, body, table string }{ {"secret", "/api/secrets", `{"service":"late","value":"hidden"}`, "secret_keys"}, {"context", "/api/context-entries", `{"content":"late"}`, "context_entries"}, {"command", "/api/prompt-commands", `{"name":"late","template":"late"}`, "user_commands"}, } { t.Run(tc.name, func(t *testing.T) { srv, st := newAuthTestServer(t) h := srv.Handler() _ = bootAdmin(t, h, st) key, err := createLegacyAPIKeyFixture(t, st, "actor", "borrow") if err != nil { t.Fatal(err) } account, err := st.GetAccountByOwner("actor") if err != nil { t.Fatal(err) } body := &pausedRequestBody{reader: strings.NewReader(tc.body), entered: make(chan struct{}), release: make(chan struct{})} req := httptest.NewRequest(http.MethodPost, tc.path, body) req.Header.Set("Authorization", "Bearer "+key) rec := httptest.NewRecorder() done := make(chan struct{}) go func() { h.ServeHTTP(rec, req) close(done) }() select { case <-body.entered: case <-time.After(3 * time.Second): t.Fatal("request did not reach its body") } if _, err := st.SetAccountEnabled("admin", account.ID, account.StateVersion, false); err != nil { t.Fatal(err) } close(body.release) select { case <-done: case <-time.After(3 * time.Second): t.Fatal("request did not finish") } if rec.Code != http.StatusForbidden { t.Fatalf("late write status=%d body=%s", rec.Code, rec.Body.String()) } var count int if err := st.DB().QueryRow("SELECT COUNT(*) FROM " + tc.table + " WHERE owner='actor'").Scan(&count); err != nil || count != 0 { t.Fatalf("late rows=%d err=%v", count, err) } }) } } func TestBorrowImplicitLocalCannotWriteWhenExplicitlyDisabled(t *testing.T) { srv, st := newTestServer(t) if err := st.CreateAccount("admin", "", "", true); err != nil { t.Fatal(err) } secret, err := st.AddSecret(store.OwnerDefault, "service", "original", "hidden") if err != nil { t.Fatal(err) } entry, err := st.SaveContextEntry(store.OwnerDefault, store.ContextEntry{Content: "original"}) if err != nil { t.Fatal(err) } command, err := st.SaveUserCommand(store.OwnerDefault, store.UserCommand{Name: "original", Template: "original"}) if err != nil { t.Fatal(err) } if err := st.EnsureUser(store.OwnerDefault, ""); err != nil { t.Fatal(err) } account, err := st.GetAccountByOwner(store.OwnerDefault) if err != nil { t.Fatal(err) } if _, err := st.SetAccountEnabled("admin", account.ID, account.StateVersion, false); err != nil { t.Fatal(err) } for _, tc := range []struct{ method, path, body string }{ {http.MethodPost, "/api/secrets", `{"service":"late","value":"hidden"}`}, {http.MethodPost, "/api/secrets/" + secret.ID + "/activate", ""}, {http.MethodPatch, "/api/secrets/" + secret.ID, `{"label":"late"}`}, {http.MethodDelete, "/api/secrets/" + secret.ID, ""}, {http.MethodPost, "/api/context-entries", `{"content":"late"}`}, {http.MethodDelete, "/api/context-entries/" + entry.ID, ""}, {http.MethodPost, "/api/prompt-commands", `{"name":"late","template":"late"}`}, {http.MethodDelete, "/api/prompt-commands/" + command.ID, ""}, } { rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))) if rec.Code != http.StatusForbidden { t.Fatalf("disabled local account wrote %s: %d %s", tc.path, rec.Code, rec.Body.String()) } } if value, ok := st.ActiveSecret(store.OwnerDefault, "service"); !ok || value != "hidden" { t.Fatalf("rejected secret mutations changed active value: %q %v", value, ok) } entries, err := st.ListContextEntries(store.OwnerDefault, "", "") if err != nil || len(entries) != 1 || entries[0].Content != "original" { t.Fatalf("rejected context mutations changed rows: %+v %v", entries, err) } commands, err := st.ListUserCommands(store.OwnerDefault) if err != nil || len(commands) != 1 || commands[0].Template != "original" { t.Fatalf("rejected command mutations changed rows: %+v %v", commands, err) } srv.appendUserPromptCommands(store.OwnerDefault, nil) for _, registered := range srv.Bus.List() { if registered.Spec.ID == "user.original" { t.Fatal("disabled account registered a command during listing") } } } func TestBorrowPromptCommandBusFollowsCommittedWrite(t *testing.T) { srv, st := newTestServer(t) h := srv.Handler() rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/prompt-commands", strings.NewReader(`{"name":"local","template":"hello"}`))) if rec.Code != http.StatusOK { t.Fatalf("save command: %d %s", rec.Code, rec.Body.String()) } commands, err := st.ListUserCommands(store.OwnerDefault) if err != nil || len(commands) != 1 { t.Fatalf("saved command missing: %+v %v", commands, err) } registered := false for _, command := range srv.Bus.List() { registered = registered || command.Spec.ID == "user.local" } if !registered { t.Fatal("committed command was not registered in the Bus") } rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodDelete, "/api/prompt-commands/"+commands[0].ID, nil)) if rec.Code != http.StatusOK { t.Fatalf("delete command: %d %s", rec.Code, rec.Body.String()) } for _, command := range srv.Bus.List() { if command.Spec.ID == "user.local" { t.Fatal("deleted command remained registered in the Bus") } } }