Add skeleton: config, migrations, startup sweep, two listeners
Step 1 of the build order in docs/decisions.md. Boots, applies migrations before serving, and serves a health check and an empty admin page. - 001_init.sql is the full schema from docs/spec.md, including the check constraints and indexes the old app lacked - The startup sweep fails submissions left mid-conversion by a restart; an in-process goroutine dies with the process and those rows would otherwise say converting forever - Admin is Basic Auth from env on its own listener, fatal at startup when ADMIN_PASSWORD is unset
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestRequireAdmin(t *testing.T) {
|
||||
a := &app{cfg: config{adminUser: "admin", adminPass: "s3cret"}}
|
||||
h := a.requireAdmin(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
|
||||
for _, tc := range []struct {
|
||||
name, user, pass string
|
||||
auth bool
|
||||
want int
|
||||
}{
|
||||
{name: "no credentials", want: http.StatusUnauthorized},
|
||||
{name: "wrong password", user: "admin", pass: "hunter2", auth: true, want: http.StatusUnauthorized},
|
||||
{name: "wrong user", user: "root", pass: "s3cret", auth: true, want: http.StatusUnauthorized},
|
||||
{name: "correct", user: "admin", pass: "s3cret", auth: true, want: http.StatusTeapot},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := httptest.NewRequest("GET", "/admin", nil)
|
||||
if tc.auth {
|
||||
r.SetBasicAuth(tc.user, tc.pass)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != tc.want {
|
||||
t.Fatalf("status = %d, want %d", w.Code, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Set TEST_DATABASE_URL to run this against a throwaway database.
|
||||
func TestMigrateIsIdempotent(t *testing.T) {
|
||||
url := os.Getenv("TEST_DATABASE_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
if _, err := pool.Exec(ctx, `drop schema public cascade; create schema public`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range 2 {
|
||||
if err := migrate(ctx, pool); err != nil {
|
||||
t.Fatalf("migrate run %d: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
if err := sweep(ctx, pool); err != nil {
|
||||
t.Fatalf("sweep: %v", err)
|
||||
}
|
||||
|
||||
var n int
|
||||
if err := pool.QueryRow(ctx, `select count(*) from schema_migrations`).Scan(&n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("applied migrations = %d, want 1", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user