427 lines
11 KiB
Go
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])
|
|
}
|
|
}
|