package main import ( "encoding/json" "fmt" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "path/filepath" "testing" ) // newTestServer richtet eine frische, isolierte DB (eigene Datei je Test) und // einen httptest-Server mit dem echten Routing ein. func newTestServer(t *testing.T) *httptest.Server { t.Helper() dsn := filepath.Join(t.TempDir(), "test.db") if err := initDB(dsn); err != nil { t.Fatalf("initDB: %v", err) } t.Cleanup(func() { db.Close() }) srv := httptest.NewServer(routes()) t.Cleanup(srv.Close) return srv } // newClient liefert einen HTTP-Client mit Cookie-Jar (hält Session-Cookies). func newClient(t *testing.T) *http.Client { t.Helper() jar, err := cookiejar.New(nil) if err != nil { t.Fatal(err) } return &http.Client{Jar: jar} } func postForm(t *testing.T, c *http.Client, urlStr string, form url.Values) *http.Response { t.Helper() resp, err := c.PostForm(urlStr, form) if err != nil { t.Fatalf("POST %s: %v", urlStr, err) } return resp } // registerAndLogin legt einen Nutzer an, loggt ihn ein und gibt den Client // (mit gesetztem Session-Cookie) zurück. func registerAndLogin(t *testing.T, srv *httptest.Server, username string) *http.Client { t.Helper() c := newClient(t) resp := postForm(t, c, srv.URL+"/auth/newuser", url.Values{ "user": {username}, "pass1": {"supersecret1"}, "pass2": {"supersecret1"}, }) resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("register %s: status %d", username, resp.StatusCode) } resp = postForm(t, c, srv.URL+"/auth/login", url.Values{ "user": {username}, "pass": {"supersecret1"}, "timeout": {"86400"}, }) resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("login %s: status %d", username, resp.StatusCode) } return c } // createEntry erstellt einen Beitrag und liefert dessen pid. func createEntry(t *testing.T, c *http.Client, srv *httptest.Server, content string) int64 { t.Helper() resp := postForm(t, c, srv.URL+"/entry/create", url.Values{"content": {content}}) defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("create entry: status %d", resp.StatusCode) } var out struct { PID int64 `json:"pid"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatalf("decode create: %v", err) } return out.PID } func TestRegisterLoginHeaderbar(t *testing.T) { srv := newTestServer(t) c := registerAndLogin(t, srv, "alice") resp, err := c.Get(srv.URL + "/auth/headerbar") if err != nil { t.Fatal(err) } defer resp.Body.Close() var h struct { LoggedIn bool `json:"loggedin"` } json.NewDecoder(resp.Body).Decode(&h) if !h.LoggedIn { t.Fatal("erwartete loggedin=true nach Login") } // Anonymer Client ist nicht eingeloggt. anon := newClient(t) resp2, err := anon.Get(srv.URL + "/auth/headerbar") if err != nil { t.Fatal(err) } defer resp2.Body.Close() var h2 struct { LoggedIn bool `json:"loggedin"` } json.NewDecoder(resp2.Body).Decode(&h2) if h2.LoggedIn { t.Fatal("anonymer Client sollte nicht eingeloggt sein") } } func TestFeedEmptyThenPopulated(t *testing.T) { srv := newTestServer(t) resp, err := http.Get(srv.URL + "/entry/feed/0") if err != nil { t.Fatal(err) } var empty []map[string]any json.NewDecoder(resp.Body).Decode(&empty) resp.Body.Close() if len(empty) != 0 { t.Fatalf("erwartete leeren Feed, bekam %d", len(empty)) } alice := registerAndLogin(t, srv, "alice") createEntry(t, alice, srv, "hallo welt") resp, err = http.Get(srv.URL + "/entry/feed/0") if err != nil { t.Fatal(err) } var posts []struct { Content string `json:"content"` Username string `json:"username"` } json.NewDecoder(resp.Body).Decode(&posts) resp.Body.Close() if len(posts) != 1 || posts[0].Content != "hallo welt" || posts[0].Username != "alice" { t.Fatalf("unerwarteter Feed: %+v", posts) } } func TestCreateEntryRequiresAuth(t *testing.T) { srv := newTestServer(t) anon := newClient(t) resp := postForm(t, anon, srv.URL+"/entry/create", url.Values{"content": {"x"}}) resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("create ohne Login: erwartet 401, bekam %d", resp.StatusCode) } } type tally struct { Left int `json:"left"` Right int `json:"right"` Selected string `json:"selected"` } func TestVoting(t *testing.T) { srv := newTestServer(t) alice := registerAndLogin(t, srv, "alice") bob := registerAndLogin(t, srv, "bob") pid := createEntry(t, alice, srv, "vote me") vote := func(c *http.Client, mode string) tally { resp := postForm(t, c, fmt.Sprintf("%s/entry/%d/vote", srv.URL, pid), url.Values{"mode": {mode}}) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("vote %s: status %d", mode, resp.StatusCode) } var tl tally json.NewDecoder(resp.Body).Decode(&tl) return tl } if got := vote(alice, "left"); got.Left != 1 || got.Selected != "left" { t.Fatalf("alice left: %+v", got) } if got := vote(bob, "right"); got.Left != 1 || got.Right != 1 { t.Fatalf("bob right: %+v", got) } if got := vote(alice, "left"); got.Left != 0 || got.Selected != "none" { t.Fatalf("alice toggle off: %+v", got) } if got := vote(alice, "right"); got.Right != 2 || got.Selected != "right" { t.Fatalf("alice switch: %+v", got) } } func TestVoteRequiresAuthAndValidMode(t *testing.T) { srv := newTestServer(t) alice := registerAndLogin(t, srv, "alice") pid := createEntry(t, alice, srv, "x") voteURL := fmt.Sprintf("%s/entry/%d/vote", srv.URL, pid) anon := newClient(t) resp := postForm(t, anon, voteURL, url.Values{"mode": {"left"}}) resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("anon vote: erwartet 401, bekam %d", resp.StatusCode) } resp = postForm(t, alice, voteURL, url.Values{"mode": {"up"}}) resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("ungültiger Modus: erwartet 400, bekam %d", resp.StatusCode) } } func TestDeleteUser(t *testing.T) { srv := newTestServer(t) alice := registerAndLogin(t, srv, "alice") // Falsches Passwort -> 403. resp := postForm(t, alice, srv.URL+"/user/delete", url.Values{"pass1": {"falsch"}}) resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("falsches Passwort: erwartet 403, bekam %d", resp.StatusCode) } // Richtiges Passwort -> 200. resp = postForm(t, alice, srv.URL+"/user/delete", url.Values{"pass1": {"supersecret1"}}) resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("delete: erwartet 200, bekam %d", resp.StatusCode) } // Login danach schlägt fehl. fresh := newClient(t) resp = postForm(t, fresh, srv.URL+"/auth/login", url.Values{ "user": {"alice"}, "pass": {"supersecret1"}, "timeout": {"86400"}, }) resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("Login nach Löschen: erwartet 401, bekam %d", resp.StatusCode) } }