package controlplane import ( "context" "errors" "path/filepath" "testing" ) func testUserStore(t *testing.T) *UserStore { t.Helper() db, err := openSQLite(filepath.Join(t.TempDir(), "users.sqlite")) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) store, err := NewUserStore(db) if err != nil { t.Fatal(err) } return store } func TestUserStoreBootstrapAndAuthentication(t *testing.T) { store := testUserStore(t) ctx := context.Background() if err := store.BootstrapAdmins(ctx, map[string]string{"root": "bootstrap-password"}); err != nil { t.Fatal(err) } user, ok, err := store.Authenticate(ctx, "ROOT", "bootstrap-password") if err != nil || !ok { t.Fatalf("expected case-insensitive bootstrap login, got ok=%v err=%v", ok, err) } if user.Role != RoleAdmin || !user.Active { t.Fatalf("unexpected bootstrap user: %+v", user) } if _, ok, err := store.Authenticate(ctx, "root", "wrong-password"); err != nil || ok { t.Fatalf("expected invalid password, got ok=%v err=%v", ok, err) } if err := store.BootstrapAdmins(ctx, map[string]string{"root": "changed-password"}); err != nil { t.Fatal(err) } if _, ok, err := store.Authenticate(ctx, "root", "bootstrap-password"); err != nil || !ok { t.Fatalf("bootstrap must not overwrite persisted credentials: ok=%v err=%v", ok, err) } } func TestUserStoreAssignmentsAndLastAdministrator(t *testing.T) { store := testUserStore(t) ctx := context.Background() if err := store.BootstrapAdmins(ctx, map[string]string{"root": "bootstrap-password"}); err != nil { t.Fatal(err) } member, err := store.Create(ctx, WebUser{ Username: "operator-1", DisplayName: "Operator One", Role: RoleMember, NodeIDs: []string{"node-a", "node-a"}, }, "member-password-1") if err != nil { t.Fatal(err) } if len(member.NodeIDs) != 1 || member.NodeIDs[0] != "node-a" { t.Fatalf("expected de-duplicated Node assignment, got %v", member.NodeIDs) } if _, err := store.Create(ctx, WebUser{Username: "OPERATOR-1", DisplayName: "Duplicate", Role: RoleMember}, "member-password-2"); !errors.Is(err, ErrUserAlreadyExists) { t.Fatalf("expected case-insensitive duplicate rejection, got %v", err) } if _, err := store.Update(ctx, member.ID, UserUpdate{ DisplayName: "Operator Updated", Role: RoleMember, Active: false, NodeIDs: []string{"node-b"}, }); err != nil { t.Fatal(err) } updated, err := store.Get(ctx, member.ID) if err != nil { t.Fatal(err) } if updated.Active || len(updated.NodeIDs) != 1 || updated.NodeIDs[0] != "node-b" { t.Fatalf("user update did not persist: %+v", updated) } admins, err := store.List(ctx) if err != nil { t.Fatal(err) } for _, admin := range admins { if admin.Role == RoleAdmin { if _, err := store.Update(ctx, admin.ID, UserUpdate{DisplayName: admin.DisplayName, Role: RoleMember, Active: true}); !errors.Is(err, ErrLastAdministrator) { t.Fatalf("expected last administrator protection, got %v", err) } } } }