Closes #8, #9, #10; partially addresses #11. middlewares (#8): - Log: log once from the Middleware closure after the handler returns instead of spawning a goroutine per Write (data race on the shared LogWriter, multiple log lines per request). Bodiless responses (204/304/redirects) are now logged too, and the status-class switch has a default branch so a stray sub-100 status can't nil-deref the event. - logWriter forwards http.Flusher / http.Hijacker / io.ReaderFrom so streaming handlers and the file server keep their fast paths. - Both middlewares use the closure idiom rather than storing next on a shared instance, so they're safe to register on more than one chain. - CorsOrigin now honours its argument: a comma-separated allow-list, with "*" meaning allow-any. Wired to a new --allow-origin flag that defaults to "*" (unchanged behaviour out of the box). ReadFile (#9): - Add the missing returns after GetFile / open failures (the old code fell through to os.ReadFile(nil path) and wrote a second body). - Hand off to http.ServeContent for Content-Type, Range and If-Modified-Since, and set Content-Disposition: inline so the embedded audio player streams instead of downloading (the download button already uses the HTML download attribute). lib.Coalesce (#10): - Constrain to comparable and compare against a fresh zero value. The old IsZero type switch compared an any against untyped constants, so every narrow numeric type (int8..int64, uint*, float32) reported non-zero and never fell back. IsZero is removed (was unused elsewhere). tests (#11, high-value/easy set): - lib.Coalesce table across every numeric width, string, bool, pointer. - database.Migrate against a temp sqlite: schema created, recorded once, second call is a no-op. - types.NewFile: id = sha256(path), hash = sha256(contents), scan fields copied, and the missing-file path still yields an id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
70 lines
1.6 KiB
Go
70 lines
1.6 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func testDB(t *testing.T) *DB {
|
|
t.Helper()
|
|
|
|
conn, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "test.sqlite3"))
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
t.Cleanup(func() { conn.Close() })
|
|
|
|
return New(context.Background(), zerolog.Nop(), conn)
|
|
}
|
|
|
|
func TestMigrate(t *testing.T) {
|
|
db := testDB(t)
|
|
|
|
if err := db.Migrate(); err != nil {
|
|
t.Fatalf("first Migrate: %v", err)
|
|
}
|
|
|
|
// The files table exists and is queryable.
|
|
var n int
|
|
if err := db.conn.QueryRow(`select count(*) from files`).Scan(&n); err != nil {
|
|
t.Fatalf("query files after migrate: %v", err)
|
|
}
|
|
|
|
// The migration is recorded exactly once.
|
|
if err := db.conn.QueryRow(
|
|
`select count(*) from schema_migrations where name = ?`, "0.1.0_create_files",
|
|
).Scan(&n); err != nil {
|
|
t.Fatalf("query schema_migrations: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("schema_migrations has %d rows for 0.1.0_create_files, want 1", n)
|
|
}
|
|
}
|
|
|
|
func TestMigrateIsIdempotent(t *testing.T) {
|
|
db := testDB(t)
|
|
|
|
if err := db.Migrate(); err != nil {
|
|
t.Fatalf("first Migrate: %v", err)
|
|
}
|
|
// A second call must be a no-op: re-running "create table files" would
|
|
// error, so a clean return proves the already-applied migration is skipped.
|
|
if err := db.Migrate(); err != nil {
|
|
t.Fatalf("second Migrate: %v", err)
|
|
}
|
|
|
|
var n int
|
|
if err := db.conn.QueryRow(`select count(*) from schema_migrations`).Scan(&n); err != nil {
|
|
t.Fatalf("query schema_migrations: %v", err)
|
|
}
|
|
if n != len(migrations) {
|
|
t.Fatalf("schema_migrations has %d rows, want %d", n, len(migrations))
|
|
}
|
|
}
|