// Package todotest tests the app from the outside: real HTTP requests // against the real router, wired exactly as cmd/todo/main.go wires it. package todotest import ( "io" "log/slog" "net/http" "net/http/httptest" "net/url" "os" "regexp" "strings" "testing" "amadan.net/rastrillo/rastrillo/budget" "amadan.net/rastrillo/rastrillo/db" "amadan.net/rastrillo/rastrillo/dbtest" todo "todo/internal/todo" ) // testOrigin is the origin newApp wires: the app's own. const testOrigin = "http://app.test" // schema is migrated once per test binary and copied for each test. var schema = dbtest.FromSet(todo.BootSchema) func TestMain(m *testing.M) { code := budget.Main(m) schema.Remove() os.Exit(code) } // newApp builds the whole app over its own copy of the migrated // database, exactly as main.go does. func newApp(t *testing.T) http.Handler { t.Helper() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, err := db.Open(schema.Path(t), logger) if err != nil { t.Fatalf("db.Open: %v", err) } t.Cleanup(func() { d.Close() }) mux, err := todo.App(d, testOrigin, logger) if err != nil { t.Fatalf("App: %v", err) } return mux } // browser is one visitor: a cookie jar in front of the app. type browser struct { t *testing.T h http.Handler cookies map[string]*http.Cookie } func newBrowser(t *testing.T, h http.Handler) *browser { return &browser{t: t, h: h, cookies: map[string]*http.Cookie{}} } func (b *browser) do(req *http.Request) *httptest.ResponseRecorder { b.t.Helper() for _, c := range b.cookies { req.AddCookie(c) } rec := httptest.NewRecorder() b.h.ServeHTTP(rec, req) for _, c := range rec.Result().Cookies() { if c.MaxAge < 0 { delete(b.cookies, c.Name) } else { b.cookies[c.Name] = c } } return rec } func (b *browser) get(target string) *httptest.ResponseRecorder { b.t.Helper() return b.do(httptest.NewRequest(http.MethodGet, target, nil)) } // post sends a form the way the shared suite does: no Origin header. func (b *browser) post(target string, form url.Values, headers ...string) *httptest.ResponseRecorder { b.t.Helper() req := httptest.NewRequest(http.MethodPost, target, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") for i := 0; i+1 < len(headers); i += 2 { req.Header.Set(headers[i], headers[i+1]) } return b.do(req) } // add posts a new todo and fails the test unless it 303s. func (b *browser) add(title string) { b.t.Helper() rec := b.post("/todos", url.Values{"title": {title}, "filter": {"all"}}) if rec.Code != http.StatusSeeOther { b.t.Fatalf("POST /todos: status %d, want 303", rec.Code) } } var itemRE = regexp.MustCompile(`(?s)