Seed from go-template-repo, renamed to simplexcalc
The template's files at a77fd30, without its history or LICENSE, after script/rename simplexcalc. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/simplexcalc/internal/database"
|
||||
)
|
||||
|
||||
// open returns a migrated scratch database in a directory the test
|
||||
// framework removes afterwards.
|
||||
func open(t *testing.T) *database.Database {
|
||||
t.Helper()
|
||||
|
||||
db, err := database.NewForTest(t.Context(), t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("opening test database: %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
closeErr := db.Close()
|
||||
if closeErr != nil {
|
||||
t.Errorf("closing test database: %v", closeErr)
|
||||
}
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// TestMigrationsApplyFromClean is the claim the healthcheck and the
|
||||
// container both rest on: an empty directory becomes a usable schema
|
||||
// with no operator step in between.
|
||||
func TestMigrationsApplyFromClean(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := open(t)
|
||||
|
||||
versions, err := db.AppliedVersions(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("reading applied versions: %v", err)
|
||||
}
|
||||
|
||||
// 000 (the ledger) and 001 (widgets), which is every file the
|
||||
// schema directory currently embeds.
|
||||
if len(versions) != 2 || versions[0] != 0 || versions[1] != 1 {
|
||||
t.Fatalf("applied versions = %v, want [0 1]", versions)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrationsAreIdempotent: a restart re-runs Migrate against a
|
||||
// database that already has the schema, and must change nothing. A
|
||||
// migration runner that fails here takes the service down on every
|
||||
// second start.
|
||||
func TestMigrationsAreIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
ctx := t.Context()
|
||||
|
||||
first, err := database.NewForTest(ctx, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("first open: %v", err)
|
||||
}
|
||||
|
||||
_, err = first.CreateWidget(ctx, "survivor", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("creating widget: %v", err)
|
||||
}
|
||||
|
||||
err = first.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("closing: %v", err)
|
||||
}
|
||||
|
||||
second, err := database.NewForTest(ctx, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("reopening and re-migrating: %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = second.Close() }()
|
||||
|
||||
versions, err := second.AppliedVersions(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("reading applied versions: %v", err)
|
||||
}
|
||||
|
||||
if len(versions) != 2 {
|
||||
t.Errorf("re-running migrations changed the ledger: %v", versions)
|
||||
}
|
||||
|
||||
// The data has to still be there: a migration runner that "fixes"
|
||||
// an already-migrated database by recreating tables is worse than
|
||||
// one that fails.
|
||||
count, err := second.CountWidgets(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("counting: %v", err)
|
||||
}
|
||||
|
||||
if count != 1 {
|
||||
t.Errorf("widget count = %d after reopen, want 1", count)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWidgetRoundTrip exercises the query layer against the real
|
||||
// schema, including the timestamp format shared between Go and the SQL
|
||||
// DEFAULT.
|
||||
func TestWidgetRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := open(t)
|
||||
ctx := t.Context()
|
||||
|
||||
created, err := db.CreateWidget(ctx, "widget one", 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("creating widget: %v", err)
|
||||
}
|
||||
|
||||
if created.ID == "" {
|
||||
t.Error("created widget has no id")
|
||||
}
|
||||
|
||||
widgets, err := db.ListWidgets(ctx, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("listing widgets: %v", err)
|
||||
}
|
||||
|
||||
if len(widgets) != 1 {
|
||||
t.Fatalf("listed %d widgets, want 1", len(widgets))
|
||||
}
|
||||
|
||||
got := widgets[0]
|
||||
if got.ID != created.ID || got.Name != "widget one" || got.SizeBytes != 4096 {
|
||||
t.Errorf("round trip lost data: %+v", got)
|
||||
}
|
||||
|
||||
if got.CreatedAt.IsZero() {
|
||||
t.Error("created_at did not survive the round trip")
|
||||
}
|
||||
}
|
||||
|
||||
// TestListWidgetsRespectsLimit: the index query is bounded, and the
|
||||
// bound has to actually bind.
|
||||
func TestListWidgetsRespectsLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := open(t)
|
||||
ctx := t.Context()
|
||||
|
||||
for range 5 {
|
||||
_, err := db.CreateWidget(ctx, "w", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("creating widget: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
widgets, err := db.ListWidgets(ctx, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("listing widgets: %v", err)
|
||||
}
|
||||
|
||||
if len(widgets) != 2 {
|
||||
t.Errorf("limit 2 returned %d rows", len(widgets))
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseMigrationVersion covers the naming contract the schema
|
||||
// directory has to keep. A file this rejects is a file that would
|
||||
// otherwise be silently skipped.
|
||||
func TestParseMigrationVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
good := map[string]int{
|
||||
"000.sql": 0,
|
||||
"001_widgets.sql": 1,
|
||||
"017_thing.sql": 17,
|
||||
}
|
||||
|
||||
for name, want := range good {
|
||||
got, err := database.ParseMigrationVersion(name)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", name, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if got != want {
|
||||
t.Errorf("%s: version = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range []string{"widgets.sql", "_001.sql", "v1_widgets.sql"} {
|
||||
_, err := database.ParseMigrationVersion(name)
|
||||
if err == nil {
|
||||
t.Errorf("%s: wanted a rejection, got none", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenCreatesFile: Open must produce a database at the path it was
|
||||
// given, not somewhere else.
|
||||
func TestOpenCreatesFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
db, err := database.Open(t.Context(), filepath.Join(dir, "explicit.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("opening: %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
err = db.PingContext(t.Context())
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("pinging the opened database: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user