feat: implement Havenllo single-board kanban (Go+SQLite backend, Preact/Vite frontend)
- Single fixed 'Haven' board: lists (To do/In progress/Done) + cards, no multi-board - Go 1.24 backend, CGO-free modernc.org/sqlite, embedded SPA, REST /api - Preact + Vite + TS frontend, native HTML5 drag-and-drop, optimistic UI, dark Haven theme - PVC (nfs-client) for SQLite, root container on NFS, simple nginx ingress havenllo.haven - Dockerfile multi-arch build, Gitea CI via pipeline-actions@main
This commit is contained in:
69
internal/database/database_test.go
Normal file
69
internal/database/database_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"havenllo/internal/database"
|
||||
)
|
||||
|
||||
func TestOpenMigratesSeedsAndEnforcesCascade(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
path := filepath.Join(t.TempDir(), "havenllo.db")
|
||||
db, err := database.Open(ctx, path)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var count int
|
||||
if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM lists").Scan(&count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Fatalf("seeded lists = %d, want 3", count)
|
||||
}
|
||||
var listID int64
|
||||
if err := db.QueryRowContext(ctx, "SELECT id FROM lists ORDER BY position LIMIT 1").Scan(&listID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO cards (list_id, title, description, position, done, created_at, updated_at)
|
||||
VALUES (?, 'test', '', 1024, 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')`, listID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, "DELETE FROM lists WHERE id = ?", listID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM cards").Scan(&count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("cascade left %d cards", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenIsIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
path := filepath.Join(t.TempDir(), "havenllo.db")
|
||||
db, err := database.Open(ctx, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = db.Close()
|
||||
db, err = database.Open(ctx, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
var count int
|
||||
if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations").Scan(&count); err != nil && err != sql.ErrNoRows {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("migrations = %d, want 1", count)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user