Why: the U4 branch is 89 commits behind main, which now carries the shared document renderer, two-trees project work, the SQLite instance guard and exec env layers (PR #209 merged asc76d8bd); it conflicted in storage migrations, host capabilities and the extension surface. The user authorized syncing U4 after the renderer line on 2026-10-18. Evidence: content is the agent-exec transport 3b64eb5 (cursor worker, unit backend-iam-u4-onto-main-c76d8bd-r1, run on a checkout whose tree equals main, receipt /tmp/cleanup-20261018/render/u4-backend-onto-run.json, SELF). Host check: main migrations 1-48 unchanged (migrate.go has no deleted lines versus main), U4 migrations moved from 35-36 to 49-50 with identical SQL, surface.txt is the sorted union, deps stay at main's pins. Impact: databases that ran unmerged U4 builds with the old numbering must be recreated. SurfaceVersion becomes 2.25.0-experimental. Main's admin password account creation now requires the system.users.create action like the other U4 account writes. Refs: Backend mainc76d8bd, PR #209 Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
4.6 KiB
Go
123 lines
4.6 KiB
Go
package store
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRolePolicyMigrationSupportsOneRoleForManyUsers(t *testing.T) {
|
|
st := openTestStore(t)
|
|
if err := st.CreateAccount("alice", "Alice", "hash", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.CreateAccount("bob", "Bob", "hash", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var aliceID, bobID string
|
|
if err := st.db.QueryRow(`SELECT id FROM users WHERE owner='alice'`).Scan(&aliceID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.db.QueryRow(`SELECT id FROM users WHERE owner='bob'`).Scan(&bobID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := nowISO()
|
|
if _, err := st.db.Exec(st.q(`INSERT INTO roles(role_id,name,normalized_name,description,enabled,role_version,created_by,updated_by,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?)`), "role_engineer", "软件工程师", "软件工程师", "负责软件研发", true, 1, aliceID, aliceID, now, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, binding := range []struct{ id, userID string }{{"bind_alice", aliceID}, {"bind_bob", bobID}} {
|
|
if _, err := st.db.Exec(st.q(`INSERT INTO user_role_bindings(binding_id,user_id,role_id,active,binding_version,assigned_by,assigned_at,ended_at)
|
|
VALUES(?,?,?,?,?,?,?,?)`), binding.id, binding.userID, "role_engineer", true, 1, aliceID, now, ""); err != nil {
|
|
t.Fatalf("assign shared role to %s: %v", binding.userID, err)
|
|
}
|
|
}
|
|
var count int
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM user_role_bindings WHERE role_id='role_engineer' AND active=TRUE`).Scan(&count); err != nil || count != 2 {
|
|
t.Fatalf("active role bindings=%d err=%v", count, err)
|
|
}
|
|
}
|
|
|
|
func TestRolePolicyMigrationRejectsTwoActiveRolesForOneUser(t *testing.T) {
|
|
st := openTestStore(t)
|
|
if err := st.CreateAccount("alice", "Alice", "hash", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var userID string
|
|
if err := st.db.QueryRow(`SELECT id FROM users WHERE owner='alice'`).Scan(&userID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := nowISO()
|
|
for _, roleID := range []string{"role_engineer", "role_reviewer"} {
|
|
if _, err := st.db.Exec(st.q(`INSERT INTO roles(role_id,name,normalized_name,description,enabled,role_version,created_by,updated_by,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?)`), roleID, roleID, roleID, "", true, 1, userID, userID, now, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := st.db.Exec(st.q(`INSERT INTO user_role_bindings(binding_id,user_id,role_id,active,binding_version,assigned_by,assigned_at,ended_at)
|
|
VALUES(?,?,?,?,?,?,?,?)`), "bind_1", userID, "role_engineer", true, 1, userID, now, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := st.db.Exec(st.q(`INSERT INTO user_role_bindings(binding_id,user_id,role_id,active,binding_version,assigned_by,assigned_at,ended_at)
|
|
VALUES(?,?,?,?,?,?,?,?)`), "bind_2", userID, "role_reviewer", true, 1, userID, now, "")
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "unique") {
|
|
t.Fatalf("second active role must hit unique constraint: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRolePolicyMigrationCanRollbackAndReapply(t *testing.T) {
|
|
st := openTestStore(t)
|
|
if err := st.MigrateDown(48); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.db.Exec(`SELECT role_id FROM roles LIMIT 1`); err == nil {
|
|
t.Fatal("roles table remained after v49 rollback")
|
|
}
|
|
if err := st.migrate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.db.Exec(`SELECT role_id FROM roles LIMIT 1`); err != nil {
|
|
t.Fatalf("roles table missing after reapply: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRoleAuthorityImportStageUpgradesExistingV49(t *testing.T) {
|
|
st := openTestStore(t)
|
|
if err := st.MigrateDown(49); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.db.Exec(`SELECT role_id FROM roles LIMIT 1`); err != nil {
|
|
t.Fatalf("v49 role authority was removed: %v", err)
|
|
}
|
|
if _, err := st.db.Exec(`SELECT state FROM role_authority_import_state WHERE id=1`); err == nil {
|
|
t.Fatal("v50 staging table remained after rollback to v49")
|
|
}
|
|
if err := st.migrate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var state string
|
|
if err := st.db.QueryRow(`SELECT state FROM role_authority_import_state WHERE id=1`).Scan(&state); err != nil || state != "idle" {
|
|
t.Fatalf("v49 to v50 state=%q err=%v", state, err)
|
|
}
|
|
}
|
|
|
|
func TestRolePolicyMigrationSeedsLegacyAdministratorGrants(t *testing.T) {
|
|
st := openTestStore(t)
|
|
if err := st.MigrateDown(48); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.CreateAccount("admin", "Admin", "hash", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.migrate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var grants int
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM system_grants g JOIN users u ON u.id=g.user_id
|
|
WHERE u.owner='admin' AND g.allowed=TRUE`).Scan(&grants); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if grants != 12 {
|
|
t.Fatalf("legacy administrator grants=%d", grants)
|
|
}
|
|
}
|