Files
Agent-System-Backend/internal/store/project_fence_test.go
T
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes.

Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending.

Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete.

Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
2026-10-14 07:33:54 +00:00

184 lines
6.7 KiB
Go

package store
import (
"errors"
"testing"
"time"
)
func TestProjectAccountCheckedMutationsRespectEffectiveStateAndOwner(t *testing.T) {
st := openTestStore(t)
t.Logf("project account fence dialect=%s", st.Dialect())
for _, account := range []struct {
owner string
admin bool
}{{"admin", true}, {"alice", false}, {"bob", false}} {
if err := st.CreateAccount(account.owner, "", "", account.admin); err != nil {
t.Fatal(err)
}
}
if _, err := st.CreateProjectAccountChecked("missing", "forbidden", false); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("missing actor created a project: %v", err)
}
project, err := st.CreateProjectAccountChecked("alice", "original", false)
if err != nil || project.Owner != "alice" {
t.Fatalf("active owner could not create project: %+v %v", project, err)
}
for _, mutate := range []func() error{
func() error { return st.RenameProjectAccountChecked("bob", project.ID, "foreign", false) },
func() error {
return st.UpdateProjectSettingsAccountChecked("bob", project.ID, map[string]any{"foreign": true}, false)
},
func() error { return st.DeleteProjectAccountChecked("bob", project.ID, false) },
} {
if err := mutate(); !errors.Is(err, ErrNotFound) {
t.Fatalf("foreign actor changed project: %v", err)
}
}
if err := st.RenameProjectAccountChecked("alice", project.ID, "renamed", false); err != nil {
t.Fatal(err)
}
if err := st.UpdateProjectSettingsAccountChecked("alice", project.ID, map[string]any{"value": "retained"}, false); err != nil {
t.Fatal(err)
}
deletable, err := st.CreateProjectAccountChecked("bob", "temporary", false)
if err != nil {
t.Fatal(err)
}
child, err := st.CreateConversation("bob", deletable.ID, "child", "")
if err != nil {
t.Fatal(err)
}
if err := st.DeleteProjectAccountChecked("bob", deletable.ID, false); err != nil {
t.Fatal(err)
}
if _, err := st.GetProject("bob", deletable.ID); !errors.Is(err, ErrNotFound) {
t.Fatalf("successful delete left project behind: %v", err)
}
if _, err := st.GetConversation("bob", child.ID); !errors.Is(err, ErrNotFound) {
t.Fatalf("project deletion did not cascade to its conversation: %v", err)
}
account, err := st.GetAccountByOwner("alice")
if err != nil {
t.Fatal(err)
}
if _, err := st.SetAccountEnabled("admin", account.ID, account.StateVersion, false); err != nil {
t.Fatal(err)
}
if _, err := st.CreateProjectAccountChecked("alice", "late", false); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("disabled actor created a project: %v", err)
}
for _, mutate := range []func() error{
func() error { return st.RenameProjectAccountChecked("alice", project.ID, "late", false) },
func() error {
return st.UpdateProjectSettingsAccountChecked("alice", project.ID, map[string]any{"late": true}, false)
},
func() error { return st.DeleteProjectAccountChecked("alice", project.ID, false) },
} {
if err := mutate(); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("disabled actor changed project: %v", err)
}
}
got, err := st.GetProject("alice", project.ID)
if err != nil || got.Name != "renamed" || got.Settings["value"] != "retained" || got.Settings["late"] != nil {
t.Fatalf("rejected mutations changed project: %+v %v", got, err)
}
projects, err := st.ListProjects("alice")
if err != nil || len(projects) != 1 {
t.Fatalf("rejected create wrote a project: %+v %v", projects, err)
}
if err := st.DeleteProjectAccountChecked("bob", "missing-project", false); !errors.Is(err, ErrNotFound) {
t.Fatalf("missing project delete: %v", err)
}
if err := st.DeleteProjectAccountChecked("admin", project.ID, false); !errors.Is(err, ErrNotFound) {
t.Fatalf("administrator crossed project ownership boundary: %v", err)
}
}
func TestProjectAccountCheckedImplicitDefaultRequiresExplicitAllowance(t *testing.T) {
st := openTestStore(t)
if _, err := st.CreateProjectAccountChecked(OwnerDefault, "rejected", false); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("implicit default admitted without authorization: %v", err)
}
project, err := st.CreateProjectAccountChecked(OwnerDefault, "local", true)
if err != nil || project.Owner != OwnerDefault {
t.Fatalf("implicit local owner was rejected: %+v %v", project, err)
}
if err := st.RenameProjectAccountChecked(OwnerDefault, project.ID, "renamed", true); err != nil {
t.Fatal(err)
}
if err := st.UpdateProjectSettingsAccountChecked(OwnerDefault, project.ID, map[string]any{"local": true}, true); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("admin", "", "", true); err != nil {
t.Fatal(err)
}
if err := st.EnsureUser(OwnerDefault, ""); err != nil {
t.Fatal(err)
}
account, err := st.GetAccountByOwner(OwnerDefault)
if err != nil {
t.Fatal(err)
}
if _, err := st.SetAccountEnabled("admin", account.ID, account.StateVersion, false); err != nil {
t.Fatal(err)
}
if err := st.DeleteProjectAccountChecked(OwnerDefault, project.ID, true); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("explicitly disabled local owner deleted project: %v", err)
}
}
func TestPGProjectCreateAccountFenceSerializesWithDisable(t *testing.T) {
st := openTestStore(t)
if st.Dialect() != "postgres" {
t.Skip("real PostgreSQL required")
}
other := reopenAccountModelStore(t, st)
if err := st.CreateAccount("admin", "", "", true); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("alice", "", "", false); err != nil {
t.Fatal(err)
}
account, err := st.GetAccountByOwner("alice")
if err != nil {
t.Fatal(err)
}
release := holdPGAccountWrite(t, st, "projects", "INSERT", "project_create")
type result struct {
owner string
err error
}
createDone := make(chan result, 1)
go func() {
project, err := st.CreateProjectAccountChecked("alice", "first", false)
createDone <- result{project.Owner, err}
}()
waitForPGAccountWrite(t, st, "project_create")
disableDone := make(chan error, 1)
go func() {
_, err := other.SetAccountEnabled("admin", account.ID, account.StateVersion, false)
disableDone <- err
}()
select {
case err := <-disableDone:
t.Fatalf("disable committed ahead of paused final project insert: %v", err)
case <-time.After(100 * time.Millisecond):
}
release()
created := awaitPGAccountResult(t, createDone)
if created.err != nil || created.owner != "alice" {
t.Fatalf("project insert did not complete before disable: %+v", created)
}
if err := awaitPGAccountResult(t, disableDone); err != nil {
t.Fatal(err)
}
if _, err := st.CreateProjectAccountChecked("alice", "late", false); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("late project write passed after disable: %v", err)
}
projects, err := st.ListProjects("alice")
if err != nil || len(projects) != 1 || projects[0].Name != "first" {
t.Fatalf("project writes crossed disable boundary: %+v %v", projects, err)
}
}