Address Gadfly review findings on Phase 0
Applied the fixes warranted by the adversarial review of PR #21 (all findings graded in the gadfly store): Store (highest impact): - buildDSN always merges busy_timeout/journal_mode/foreign_keys pragmas into the DSN, even for file: URIs or paths with a query string — the prior passthrough silently disabled FK enforcement for those PANSY_DB values. Also escape '#' in the path and tighten in-memory detection to an exact ':memory:' token or mode=memory param (no substring misfire). - migrate.go: detect duplicate migration versions with a clear error; wrap appliedVersions' rows.Err() with the store: prefix. API: - SetTrustedProxies failure now falls back to trust-none instead of leaving gin's trust-everyone default (X-Forwarded-For spoofing). - SPA: 404 embedded directories (was a directory listing), 404 bare /api, add X-Content-Type-Options: nosniff, cache index.html bytes once at startup, single fs.Stat existence check, shared writeAPIError helper. - Move gin.SetMode out of package init() into New(). Config: validate PANSY_PORT range (fall back to 8080 with a warning). Web: - api.ts: serialize the request body before the fetch try so a JSON.stringify failure isn't misreported as a network error. - AppShell: move state-specific/conflicting utilities into active/inactiveProps so concatenated Tailwind classes don't collide. Tests: added store DSN-pragma/memory-detection cases and SPA directory-404 case. go build/vet/test clean (CGO off); web tsc + build clean; re-verified in a browser (SPA, deep links, /assets 404, nav). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
@@ -3,6 +3,7 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -102,6 +103,63 @@ func TestCheckConstraintRejectsBadEnum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestForeignKeysEnforcedForFileURIDSN(t *testing.T) {
|
||||
// A file: URI (or any path with query params) must still get foreign_keys(1):
|
||||
// the earlier passthrough silently dropped the pragmas for these DSNs.
|
||||
p := filepath.Join(t.TempDir(), "uri.db")
|
||||
db, err := Open("file:" + p + "?_pragma=synchronous(1)")
|
||||
if err != nil {
|
||||
t.Fatalf("Open file: URI: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
if err := db.Migrate(context.Background()); err != nil {
|
||||
t.Fatalf("Migrate: %v", err)
|
||||
}
|
||||
_, err = db.SQL().Exec(
|
||||
`INSERT INTO plantings (object_id, plant_id, x_cm, y_cm, radius_cm) VALUES (999, 999, 0, 0, 10)`,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected foreign-key violation for file: URI DSN, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDSNAlwaysIncludesPragmas(t *testing.T) {
|
||||
for _, in := range []string{
|
||||
"./pansy.db",
|
||||
"/data/pansy.db",
|
||||
"file:/data/pansy.db",
|
||||
"file:/data/pansy.db?cache=shared",
|
||||
"/tmp/weird?name.db",
|
||||
} {
|
||||
dsn, _ := buildDSN(in)
|
||||
for _, want := range []string{"busy_timeout", "journal_mode", "foreign_keys"} {
|
||||
if !strings.Contains(dsn, want) {
|
||||
t.Errorf("buildDSN(%q) = %q, missing %s pragma", in, dsn, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDSNDoesNotDuplicateUserPragma(t *testing.T) {
|
||||
// A user-supplied busy_timeout must not be duplicated by our default.
|
||||
dsn, _ := buildDSN("file:/data/x.db?_pragma=busy_timeout(9000)")
|
||||
if strings.Count(dsn, "busy_timeout") != 1 {
|
||||
t.Errorf("buildDSN kept both user and default busy_timeout: %q", dsn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDSNMemoryDetection(t *testing.T) {
|
||||
if _, mem := buildDSN(":memory:"); !mem {
|
||||
t.Error(":memory: not detected as in-memory")
|
||||
}
|
||||
if _, mem := buildDSN("file:x.db?mode=memory"); !mem {
|
||||
t.Error("mode=memory not detected as in-memory")
|
||||
}
|
||||
if _, mem := buildDSN("/var/lib/pansy/pansy.db"); mem {
|
||||
t.Error("file path wrongly detected as in-memory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInMemoryDBIsCoherentAcrossQueries(t *testing.T) {
|
||||
// A bare ":memory:" DSN gives each connection its own DB; Open must pin the
|
||||
// pool so the migrated schema is visible to subsequent queries.
|
||||
|
||||
Reference in New Issue
Block a user