package db_test import ( "database/sql" "path/filepath" "testing" "time" "misaki/db" ) func newTestDB(t *testing.T) *db.DB { t.Helper() d, err := db.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatalf("db.Open: %v", err) } t.Cleanup(func() { d.Close() }) return d } func TestCreateUser(t *testing.T) { d := newTestDB(t) id, err := d.CreateUser("alice", "hashedpass", "hello world") if err != nil { t.Fatalf("CreateUser: %v", err) } if id <= 0 { t.Errorf("expected positive ID, got %d", id) } if _, err := d.CreateUser("alice", "hash2", ""); err == nil { t.Error("expected error for duplicate username, got nil") } } func TestGetUserByUsername(t *testing.T) { d := newTestDB(t) d.CreateUser("alice", "hash", "my note") u, err := d.GetUserByUsername("alice") if err != nil { t.Fatalf("GetUserByUsername: %v", err) } if u.Username != "alice" || u.PassHash != "hash" { t.Errorf("unexpected user: %+v", u) } if u.Status != "pending" { t.Errorf("expected pending, got %q", u.Status) } if u.Note != "my note" { t.Errorf("Note: got %q, want %q", u.Note, "my note") } if _, err := d.GetUserByUsername("nobody"); err == nil { t.Error("expected error for non-existent user, got nil") } } func TestGetUserByID(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("bob", "hash", "my biography") u, err := d.GetUserByID(id) if err != nil { t.Fatalf("GetUserByID: %v", err) } if u.ID != id || u.Username != "bob" { t.Errorf("unexpected user: %+v", u) } if u.Note != "my biography" { t.Errorf("Note: got %q, want %q", u.Note, "my biography") } if _, err := d.GetUserByID(99999); err == nil { t.Error("expected error for non-existent ID, got nil") } } func TestSetUserStatus(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("carol", "hash", "") for _, status := range []string{"approved", "rejected", "pending"} { if err := d.SetUserStatus(id, status); err != nil { t.Fatalf("SetUserStatus(%q): %v", status, err) } u, _ := d.GetUserByID(id) if u.Status != status { t.Errorf("expected %q, got %q", status, u.Status) } } } func TestListUsers(t *testing.T) { d := newTestDB(t) d.CreateUser("p1", "h", "bio1") id2, _ := d.CreateUser("a1", "h", "bio2") d.SetUserStatus(id2, "approved") pending, err := d.ListUsers("pending") if err != nil { t.Fatalf("ListUsers pending: %v", err) } if len(pending) != 1 || pending[0].Username != "p1" { t.Errorf("unexpected pending: %+v", pending) } if pending[0].Note != "bio1" { t.Errorf("Note not populated in ListUsers: got %q", pending[0].Note) } approved, _ := d.ListUsers("approved") if len(approved) != 1 || approved[0].Username != "a1" { t.Errorf("unexpected approved: %+v", approved) } } func TestUpdateUserNote(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("dave", "hash", "original") if err := d.UpdateUserNote(id, "updated note"); err != nil { t.Fatalf("UpdateUserNote: %v", err) } u, _ := d.GetUserByID(id) if u.Note != "updated note" { t.Errorf("got %q, want %q", u.Note, "updated note") } } func TestSetUserAdmin(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("eve", "hash", "") d.SetUserStatus(id, "approved") if err := d.SetUserAdmin(id, true); err != nil { t.Fatalf("SetUserAdmin true: %v", err) } u, _ := d.GetUserByID(id) if !u.IsAdmin { t.Error("expected IsAdmin=true") } d.SetUserAdmin(id, false) u, _ = d.GetUserByID(id) if u.IsAdmin { t.Error("expected IsAdmin=false after revoke") } } func TestDeleteUser(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("frank", "hash", "") d.SetUserStatus(id, "approved") d.CreateSession("tok", id, time.Now().Add(time.Hour), "pwd") if err := d.DeleteUser(id); err != nil { t.Fatalf("DeleteUser: %v", err) } if _, err := d.GetUserByID(id); err == nil { t.Error("expected error after user deletion, got nil") } if _, err := d.GetSession("tok"); err == nil { t.Error("expected session to be deleted with user") } } func TestCreateAndGetSession(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("grace", "hash", "") expires := time.Now().Add(time.Hour) if err := d.CreateSession("token1", id, expires, "pwd"); err != nil { t.Fatalf("CreateSession: %v", err) } sess, err := d.GetSession("token1") if err != nil { t.Fatalf("GetSession: %v", err) } if sess.UserID != id { t.Errorf("unexpected session: %+v", sess) } if sess.Token == "token1" { t.Error("session token stored in plaintext") } } func TestSessionTokenMigration(t *testing.T) { path := filepath.Join(t.TempDir(), "old.db") raw, err := sql.Open("sqlite3", path) if err != nil { t.Fatal(err) } for _, stmt := range []string{ `CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, pass_hash TEXT NOT NULL, note TEXT DEFAULT '', status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP)`, `CREATE TABLE sessions (token TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), expires_at DATETIME NOT NULL)`, `INSERT INTO users (id, username, pass_hash) VALUES (1, 'old', 'hash')`, } { if _, err := raw.Exec(stmt); err != nil { t.Fatal(err) } } if _, err := raw.Exec(`INSERT INTO sessions VALUES ('oldtok', 1, ?)`, time.Now().Add(time.Hour)); err != nil { t.Fatal(err) } raw.Close() for i := 0; i < 2; i++ { d, err := db.Open(path) if err != nil { t.Fatalf("open %d: %v", i, err) } sess, err := d.GetSession("oldtok") if err != nil || sess.UserID != 1 { t.Fatalf("open %d: pre-migration session lost: %v", i, err) } if sess.Token == "oldtok" { t.Fatalf("open %d: token not hashed", i) } d.Close() } } func TestGetUserByUsernameFold(t *testing.T) { d := newTestDB(t) d.CreateUser("Alice", "hash", "") u, err := d.GetUserByUsernameFold("alice") if err != nil || u.Username != "Alice" { t.Fatalf("fold lookup: %v %+v", err, u) } } func TestListServiceUsernames(t *testing.T) { d := newTestDB(t) a, _ := d.CreateUser("alice", "hash", "") b, _ := d.CreateUser("bob", "hash", "") c, _ := d.CreateUser("carol", "hash", "") d.SetUserStatus(a, "approved") d.SetUserStatus(b, "approved") d.SetUserServiceEnabled(a, "prosody", true) d.SetUserServiceEnabled(b, "ergo", true) d.SetUserServiceEnabled(c, "prosody", true) names, err := d.ListServiceUsernames("prosody") if err != nil { t.Fatal(err) } if len(names) != 1 || names[0] != "alice" { t.Errorf("want [alice], got %v", names) } } func TestSession_Expired(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("heidi", "hash", "") d.CreateSession("expiredtok", id, time.Now().Add(-time.Hour), "pwd") if _, err := d.GetSession("expiredtok"); err == nil { t.Error("expected error for expired session, got nil") } } func TestDeleteSession(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("ivan", "hash", "") d.CreateSession("tok2", id, time.Now().Add(time.Hour), "pwd") if err := d.DeleteSession("tok2"); err != nil { t.Fatalf("DeleteSession: %v", err) } if _, err := d.GetSession("tok2"); err == nil { t.Error("expected error after session deletion, got nil") } } func TestPruneExpiredSessions(t *testing.T) { d := newTestDB(t) id, _ := d.CreateUser("judy", "hash", "") d.CreateSession("live", id, time.Now().Add(time.Hour), "pwd") d.CreateSession("dead", id, time.Now().Add(-time.Hour), "pwd") if err := d.PruneExpiredSessions(); err != nil { t.Fatalf("PruneExpiredSessions: %v", err) } if _, err := d.GetSession("live"); err != nil { t.Errorf("live session should survive pruning: %v", err) } if _, err := d.GetSession("dead"); err == nil { t.Error("dead session should be pruned, but was found") } } func TestListServices(t *testing.T) { d := newTestDB(t) svcs, err := d.ListServices() if err != nil { t.Fatalf("ListServices: %v", err) } if len(svcs) == 0 { t.Fatal("expected seeded services, got none") } names := map[string]bool{} for _, s := range svcs { names[s.Name] = true } for _, want := range []string{"mail", "ergo", "cerca", "mumble", "forgejo", "akkoma"} { if !names[want] { t.Errorf("expected service %q to be seeded", want) } } } func TestSetServiceEnabled(t *testing.T) { d := newTestDB(t) if err := d.SetServiceEnabled("mail", true); err != nil { t.Fatalf("enable: %v", err) } svcs, _ := d.ListServices() for _, s := range svcs { if s.Name == "mail" && !s.Enabled { t.Error("expected mail to be enabled") } } if err := d.SetServiceEnabled("mail", false); err != nil { t.Fatalf("disable: %v", err) } svcs, _ = d.ListServices() for _, s := range svcs { if s.Name == "mail" && s.Enabled { t.Error("expected mail to be disabled") } } } func TestApplyForService(t *testing.T) { d := newTestDB(t) d.SetServiceEnabled("mail", true) id, _ := d.CreateUser("karl", "hash", "") if err := d.ApplyForService(id, "mail"); err != nil { t.Fatalf("first apply: %v", err) } if err := d.ApplyForService(id, "mail"); err != db.ErrAlreadyApplied { t.Errorf("second apply: expected ErrAlreadyApplied, got %v", err) } } func TestGetServicesForUser(t *testing.T) { d := newTestDB(t) d.SetServiceEnabled("mail", true) d.SetServiceEnabled("ergo", true) id, _ := d.CreateUser("leo", "hash", "") d.ApplyForService(id, "mail") views, err := d.GetServicesForUser(id) if err != nil { t.Fatalf("GetServicesForUser: %v", err) } found := map[string]*db.UserServiceView{} for _, v := range views { found[v.Service] = v } if found["mail"] == nil || !found["mail"].Applied { t.Error("expected mail Applied=true") } if found["ergo"] != nil && found["ergo"].Applied { t.Error("expected ergo Applied=false") } if found["mumble"] != nil || found["transfer"] != nil { t.Error("display services should be excluded from GetServicesForUser") } } func TestSetUserServiceEnabled(t *testing.T) { d := newTestDB(t) d.SetServiceEnabled("ergo", true) id, _ := d.CreateUser("mike", "hash", "") d.ApplyForService(id, "ergo") if err := d.SetUserServiceEnabled(id, "ergo", true); err != nil { t.Fatalf("grant: %v", err) } svcs, _ := d.ListUserServices(id) if len(svcs) != 1 || !svcs[0].Granted { t.Errorf("expected granted: %+v", svcs) } if err := d.SetUserServiceEnabled(id, "ergo", false); err != nil { t.Fatalf("revoke: %v", err) } svcs, _ = d.ListUserServices(id) if len(svcs) != 1 || svcs[0].Granted { t.Errorf("expected revoked: %+v", svcs) } } func TestSetUserServiceData(t *testing.T) { d := newTestDB(t) d.SetServiceEnabled("cerca", true) id, _ := d.CreateUser("nina", "hash", "") d.ApplyForService(id, "cerca") link := "https://forum.example.com/register/abc123" if err := d.SetUserServiceData(id, "cerca", link); err != nil { t.Fatalf("SetUserServiceData: %v", err) } svcs, _ := d.ListUserServices(id) if len(svcs) != 1 || svcs[0].Data != link { t.Errorf("unexpected data: %+v", svcs) } } func TestListUserServices(t *testing.T) { d := newTestDB(t) d.SetServiceEnabled("werc", true) d.SetServiceEnabled("mail", true) id, _ := d.CreateUser("olive", "hash", "") d.ApplyForService(id, "werc") d.SetUserServiceEnabled(id, "werc", true) svcs, err := d.ListUserServices(id) if err != nil { t.Fatalf("ListUserServices: %v", err) } if len(svcs) != 1 { t.Fatalf("expected 1 row, got %d: %+v", len(svcs), svcs) } if svcs[0].Service != "werc" || !svcs[0].Applied || !svcs[0].Granted { t.Errorf("unexpected service row: %+v", svcs[0]) } }