marky

Lightweight bookmarking + plaintext archive
Log | Files | Refs | README | LICENSE

commit 42f1ecfc4ab7fe7a3c0644091b8cf4c4b6876d8b
parent d1c9e11a7457eb2d7822cfb223a0080461f71714
Author: David Eisinger <[email protected]>
Date:   Mon,  6 Oct 2025 23:47:21 -0400

Add more tests

Diffstat:
Ainternal/api_auth_test.go | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/bookmarks_api_test.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/more_api_tests.go | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 273 insertions(+), 0 deletions(-)

diff --git a/internal/api_auth_test.go b/internal/api_auth_test.go @@ -0,0 +1,52 @@ +package internal + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "testing" +) + +func TestSignupLoginLogoutMe(t *testing.T) { + t.Parallel() + db, err := OpenAtPath(t.Context(), t.TempDir()+"/authz.db") + if err != nil { t.Fatalf("open: %v", err) } + t.Cleanup(func(){ _ = db.Close() }) + h, _ := NewHandler(db) + srv := httptest.NewServer(h) + defer srv.Close() + + jar, _ := cookiejar.New(nil) + client := &http.Client{Jar: jar} + + // /me unauth + resp, _ := http.Get(srv.URL+"/me") + if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("/me unauth got %d", resp.StatusCode) } + + // signup + b, _ := json.Marshal(map[string]string{"username":"bob","password":"pw"}) + r, _ := http.NewRequest(http.MethodPost, srv.URL+"/signup", bytes.NewReader(b)) + r.Header.Set("Content-Type","application/json") + rs, _ := client.Do(r) + if rs.StatusCode != http.StatusOK { t.Fatalf("signup status %d", rs.StatusCode) } + rs.Body.Close() + + // /me auth + r2, _ := client.Get(srv.URL+"/me") + if r2.StatusCode != http.StatusOK { t.Fatalf("/me auth %d", r2.StatusCode) } + r2.Body.Close() + + // logout + r3, _ := http.NewRequest(http.MethodPost, srv.URL+"/logout", nil) + rs3, _ := client.Do(r3) + if rs3.StatusCode != http.StatusNoContent { t.Fatalf("logout %d", rs3.StatusCode) } + rs3.Body.Close() + + // /me again + r4, _ := client.Get(srv.URL+"/me") + if r4.StatusCode != http.StatusUnauthorized { t.Fatalf("/me after logout %d", r4.StatusCode) } + r4.Body.Close() +} + diff --git a/internal/bookmarks_api_test.go b/internal/bookmarks_api_test.go @@ -0,0 +1,97 @@ +package internal + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "strings" + "testing" +) + +func TestBookmarksCRUDAndUnauthorized(t *testing.T) { + t.Parallel() + page := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type","text/html") + io.WriteString(w, "<!doctype html><html><head><title>CRUD Title</title></head><body>body</body></html>") + })) + defer page.Close() + + db, err := OpenAtPath(t.Context(), t.TempDir()+"/crud.db") + if err != nil { t.Fatalf("open: %v", err) } + t.Cleanup(func(){ _ = db.Close() }) + h, _ := NewHandler(db) + srv := httptest.NewServer(h) + defer srv.Close() + + // Unauth GET /bookmarks → 401 + if resp, _ := http.Get(srv.URL+"/bookmarks"); resp.StatusCode != http.StatusUnauthorized { + t.Fatalf("/bookmarks unauth %d", resp.StatusCode) + } + // Unauth POST /bookmarks → 401 + rp, _ := http.NewRequest(http.MethodPost, srv.URL+"/bookmarks", strings.NewReader("{}")) + rp.Header.Set("Content-Type","application/json") + if rs, _ := http.DefaultClient.Do(rp); rs.StatusCode != http.StatusUnauthorized { + t.Fatalf("post unauth %d", rs.StatusCode) + } + + // Sign up client + jar, _ := cookiejar.New(nil) + client := &http.Client{Jar: jar} + body, _ := json.Marshal(map[string]string{"username":"crud","password":"pw"}) + rq, _ := http.NewRequest(http.MethodPost, srv.URL+"/signup", bytes.NewReader(body)) + rq.Header.Set("Content-Type","application/json") + rsu, _ := client.Do(rq) + if rsu.StatusCode != http.StatusOK { t.Fatalf("signup %d", rsu.StatusCode) } + rsu.Body.Close() + + // Wrong content type → 415 + bad, _ := http.NewRequest(http.MethodPost, srv.URL+"/bookmarks", strings.NewReader("not json")) + bad.Header.Set("Content-Type","text/plain") + rbad, _ := client.Do(bad) + if rbad.StatusCode != http.StatusBadRequest { t.Fatalf("ct status %d", rbad.StatusCode) } + rbad.Body.Close() + + // Create + create := map[string]any{"url": page.URL} + cbody, _ := json.Marshal(create) + rc, _ := http.NewRequest(http.MethodPost, srv.URL+"/bookmarks", bytes.NewReader(cbody)) + rc.Header.Set("Content-Type","application/json") + rcr, _ := client.Do(rc) + if rcr.StatusCode != http.StatusCreated { t.Fatalf("create %d", rcr.StatusCode) } + var created map[string]any + _ = json.NewDecoder(rcr.Body).Decode(&created) + rcr.Body.Close() + id := int(created["id"].(float64)) + if created["title"].(string) != "CRUD Title" { t.Fatalf("title inferred") } + + // Duplicate POST should 200 and touch updated_at + rc2, _ := http.NewRequest(http.MethodPost, srv.URL+"/bookmarks", bytes.NewReader(cbody)) + rc2.Header.Set("Content-Type","application/json") + rcr2, _ := client.Do(rc2) + if rcr2.StatusCode != http.StatusOK { t.Fatalf("dup %d", rcr2.StatusCode) } + rcr2.Body.Close() + + // GET list and RSS + if _, err := client.Get(srv.URL+"/bookmarks"); err != nil { t.Fatalf("list err: %v", err) } + if resp, _ := client.Get(srv.URL+"/bookmarks.xml"); resp.StatusCode != 200 { t.Fatalf("rss %d", resp.StatusCode) } + + // PUT update (preserve description when omitted) + put := map[string]any{"url": page.URL, "title":"Changed"} + pb, _ := json.Marshal(put) + pr, _ := http.NewRequest(http.MethodPut, srv.URL+"/bookmarks/"+strconvItoa(id), bytes.NewReader(pb)) + pr.Header.Set("Content-Type","application/json") + prr, _ := client.Do(pr) + if prr.StatusCode != 200 { t.Fatalf("put %d", prr.StatusCode) } + var upd map[string]any + _ = json.NewDecoder(prr.Body).Decode(&upd) + prr.Body.Close() + if upd["title"].(string) != "Changed" { t.Fatalf("put title") } + + // DELETE + dr, _ := http.NewRequest(http.MethodDelete, srv.URL+"/bookmarks/"+strconvItoa(id), nil) + drr, _ := client.Do(dr) + if drr.StatusCode != 204 { t.Fatalf("delete %d", drr.StatusCode) } +} diff --git a/internal/more_api_tests.go b/internal/more_api_tests.go @@ -0,0 +1,124 @@ +package internal + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "testing" + "strconv" +) + +func TestPublicUserListingJSON(t *testing.T) { + t.Parallel() + db, _ := OpenAtPath(t.Context(), t.TempDir()+"/u.db") + t.Cleanup(func(){ _ = db.Close() }) + h, _ := NewHandler(db) + srv := httptest.NewServer(h) + defer srv.Close() + + // Unknown user + if resp, _ := http.Get(srv.URL+"/u/nouser" ); resp.StatusCode != http.StatusNotFound { + t.Fatalf("/u/nouser expected 404, got %d", resp.StatusCode) + } + + // Create user and ensure empty list + _, _ = db.CreateUser(t.Context(), "eve", "pw") + if resp, _ := http.Get(srv.URL+"/u/eve" ); resp.StatusCode != 200 { + t.Fatalf("/u/eve %d", resp.StatusCode) + } +} + +func TestBookmarksParamValidationAndRSSAccept(t *testing.T) { + t.Parallel() + page := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type","text/html") + io.WriteString(w, "<!doctype html><html><head><title>P</title></head><body/></html>") + })) + defer page.Close() + + db, _ := OpenAtPath(t.Context(), t.TempDir()+"/p.db") + t.Cleanup(func(){ _ = db.Close() }) + h, _ := NewHandler(db) + srv := httptest.NewServer(h) + defer srv.Close() + + jar, _ := cookiejar.New(nil) + client := &http.Client{Jar: jar} + // signup + body, _ := json.Marshal(map[string]string{"username":"param","password":"pw"}) + req, _ := http.NewRequest(http.MethodPost, srv.URL+"/signup", bytes.NewReader(body)) + req.Header.Set("Content-Type","application/json") + rs, _ := client.Do(req) + if rs.StatusCode != 200 { t.Fatalf("signup %d", rs.StatusCode) } + rs.Body.Close() + // One bookmark + b2, _ := json.Marshal(map[string]any{"url": page.URL}) + rq, _ := http.NewRequest(http.MethodPost, srv.URL+"/bookmarks", bytes.NewReader(b2)) + rq.Header.Set("Content-Type","application/json") + cr, _ := client.Do(rq) + if cr.StatusCode != 201 { t.Fatalf("create %d", cr.StatusCode) } + cr.Body.Close() + + // invalid limit + if resp, _ := client.Get(srv.URL+"/bookmarks?limit=abc"); resp.StatusCode != http.StatusBadRequest { + t.Fatalf("limit status %d", resp.StatusCode) + } + // invalid offset + if resp, _ := client.Get(srv.URL+"/bookmarks?offset=-1"); resp.StatusCode != http.StatusBadRequest { + t.Fatalf("offset status %d", resp.StatusCode) + } + // Accept RSS + req2, _ := http.NewRequest(http.MethodGet, srv.URL+"/bookmarks", nil) + req2.Header.Set("Accept","application/rss+xml") + rs2, _ := client.Do(req2) + if rs2.StatusCode != 200 { t.Fatalf("accept rss %d", rs2.StatusCode) } + rs2.Body.Close() +} + +func TestArchiveFetchOwnerOnly(t *testing.T) { + t.Parallel() + // Page to fetch + page := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type","text/html") + io.WriteString(w, "<!doctype html><html><head><title>A</title></head><body/></html>") + })) + defer page.Close() + + db, _ := OpenAtPath(t.Context(), t.TempDir()+"/arc.db") + t.Cleanup(func(){ _ = db.Close() }) + h, _ := NewHandler(db) + srv := httptest.NewServer(h) + defer srv.Close() + + // signup owner + j1, _ := cookiejar.New(nil) + c1 := &http.Client{Jar: j1} + s1, _ := json.Marshal(map[string]string{"username":"own","password":"pw"}) + rq1, _ := http.NewRequest(http.MethodPost, srv.URL+"/signup", bytes.NewReader(s1)) + rq1.Header.Set("Content-Type","application/json") + _ , _ = c1.Do(rq1) + // create via DB without archive + u, _ := db.GetUserByUsername(t.Context(), "own") + bmk, err := db.CreateBookmark(t.Context(), u.ID, page.URL+"/priv", "PVT", nil) + if err != nil { t.Fatalf("create raw: %v", err) } + // owner can fetch with ?fetch=1 + resp, _ := c1.Get(srv.URL+"/bookmarks/"+strconv.Itoa(int(bmk.ID))+"/archive?fetch=1") + if resp.StatusCode != 200 { t.Fatalf("owner fetch %d", resp.StatusCode) } + resp.Body.Close() + + // signup other user + j2, _ := cookiejar.New(nil) + c2 := &http.Client{Jar: j2} + s2, _ := json.Marshal(map[string]string{"username":"other","password":"pw"}) + rq2, _ := http.NewRequest(http.MethodPost, srv.URL+"/signup", bytes.NewReader(s2)) + rq2.Header.Set("Content-Type","application/json") + _ , _ = c2.Do(rq2) + // other user cannot see private archive + resp2, _ := c2.Get(srv.URL+"/bookmarks/"+strconv.Itoa(int(bmk.ID))+"/archive") + if resp2.StatusCode != 404 { t.Fatalf("other archive %d", resp2.StatusCode) } + resp2.Body.Close() +} +// no helper needed; use strconv.Itoa inline