misaki/db/db_test.go
q 119b6d73c4
Some checks failed
Build & push Docker image / build (push) Has been cancelled
totp 2fac
2026-11-07 11:43:40 -05:00

427 lines
11 KiB
Go

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])
}
}