marky

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

auth_e2e_test.go (4932B)


      1 package internal
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/json"
      6 	"io"
      7 	"net/http"
      8 	"net/http/cookiejar"
      9 	"net/http/httptest"
     10 	"regexp"
     11 	"testing"
     12 )
     13 
     14 func TestFeedsAuthWithToken(t *testing.T) {
     15 	t.Parallel()
     16 
     17 	// Content server to satisfy archiver
     18 	page := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
     19 		w.Header().Set("Content-Type", "text/html")
     20 		_, _ = w.Write([]byte(`<!doctype html><html><head><title>Feed Test</title></head><body><p>content</p></body></html>`))
     21 	}))
     22 	defer page.Close()
     23 
     24 	// App server
     25 	dir := t.TempDir()
     26 	db, err := OpenAtPath(t.Context(), dir+"/auth.db")
     27 	if err != nil {
     28 		t.Fatalf("open db: %v", err)
     29 	}
     30 	t.Cleanup(func() { _ = db.Close() })
     31 	h, err := NewHandler(db)
     32 	if err != nil {
     33 		t.Fatalf("new handler: %v", err)
     34 	}
     35 	srv := httptest.NewServer(h)
     36 	defer srv.Close()
     37 
     38 	// Authenticated client (cookie jar)
     39 	jar, _ := cookiejar.New(nil)
     40 	client := &http.Client{Jar: jar}
     41 
     42 	// Sign up user
     43 	postJSONWithClientAuth[map[string]any](t, client, srv.URL+"/signup", map[string]string{"username": "alice", "password": "pw"}, 200)
     44 
     45 	// Create one public and one private bookmark
     46 	postJSONWithClientAuth[map[string]any](t, client, srv.URL+"/bookmarks", map[string]any{"url": page.URL, "public": true, "title": "Pub"}, 201)
     47 	postJSONWithClientAuth[map[string]any](t, client, srv.URL+"/bookmarks", map[string]any{"url": page.URL + "/p", "public": false, "title": "Priv"}, 201)
     48 
     49 	// Unauthenticated caller sees only public entries in /u:alice.json
     50 	pubOnly := getJSONWithClientAuth[[]map[string]any](t, http.DefaultClient, srv.URL+"/u:alice.json", 200)
     51 	if len(pubOnly) != 1 {
     52 		t.Fatalf("expected 1 public item, got %d", len(pubOnly))
     53 	}
     54 
     55 	// Generate API token
     56 	tokResp := postJSONWithClientAuth[map[string]string](t, client, srv.URL+"/api/token", nil, 200)
     57 	token := tokResp["api_token"]
     58 	if token == "" {
     59 		t.Fatalf("empty api token")
     60 	}
     61 
     62 	// With header token, see both
     63 	req, _ := http.NewRequest(http.MethodGet, srv.URL+"/u:alice.json", nil)
     64 	req.Header.Set("X-API-Token", token)
     65 	res, err := http.DefaultClient.Do(req)
     66 	if err != nil {
     67 		t.Fatalf("get header token: %v", err)
     68 	}
     69 	defer res.Body.Close()
     70 	if res.StatusCode != 200 {
     71 		t.Fatalf("status %d", res.StatusCode)
     72 	}
     73 	var both []map[string]any
     74 	_ = json.NewDecoder(res.Body).Decode(&both)
     75 	if len(both) != 2 {
     76 		t.Fatalf("expected 2 items with header token, got %d", len(both))
     77 	}
     78 
     79 	// With query token, see both
     80 	both2 := getJSONWithClientAuth[[]map[string]any](t, http.DefaultClient, srv.URL+"/u:alice.json?token="+token, 200)
     81 	if len(both2) != 2 {
     82 		t.Fatalf("expected 2 items with query token, got %d", len(both2))
     83 	}
     84 
     85 	// With Basic auth, see both
     86 	req2, _ := http.NewRequest(http.MethodGet, srv.URL+"/u:alice.json", nil)
     87 	req2.SetBasicAuth("alice", token)
     88 	res2, err := http.DefaultClient.Do(req2)
     89 	if err != nil {
     90 		t.Fatalf("get basic: %v", err)
     91 	}
     92 	defer res2.Body.Close()
     93 	if res2.StatusCode != 200 {
     94 		t.Fatalf("status %d", res2.StatusCode)
     95 	}
     96 	var both3 []map[string]any
     97 	_ = json.NewDecoder(res2.Body).Decode(&both3)
     98 	if len(both3) != 2 {
     99 		t.Fatalf("expected 2 items with basic, got %d", len(both3))
    100 	}
    101 
    102 	// RSS unauthenticated: only public (<item> once)
    103 	r1, _ := http.Get(srv.URL + "/u:alice.rss")
    104 	b1, _ := io.ReadAll(r1.Body)
    105 	_ = r1.Body.Close()
    106 	if c := countRSSItems(b1); c != 1 {
    107 		t.Fatalf("expected 1 RSS item unauth, got %d", c)
    108 	}
    109 	// RSS with token header: both
    110 	rreq, _ := http.NewRequest(http.MethodGet, srv.URL+"/u:alice.rss", nil)
    111 	rreq.Header.Set("X-API-Token", token)
    112 	rres, _ := http.DefaultClient.Do(rreq)
    113 	rb, _ := io.ReadAll(rres.Body)
    114 	_ = rres.Body.Close()
    115 	if c := countRSSItems(rb); c != 2 {
    116 		t.Fatalf("expected 2 RSS items with token, got %d", c)
    117 	}
    118 }
    119 
    120 func countRSSItems(b []byte) int {
    121 	re := regexp.MustCompile(`<item\b`)
    122 	return len(re.FindAllIndex(b, -1))
    123 }
    124 
    125 // Helpers copied/adapted from e2e tests
    126 func postJSONWithClientAuth[T any](t *testing.T, client *http.Client, url string, v any, want int) T {
    127 	t.Helper()
    128 	var body io.Reader
    129 	if v != nil {
    130 		b, _ := json.Marshal(v)
    131 		body = bytes.NewReader(b)
    132 	}
    133 	req, _ := http.NewRequest(http.MethodPost, url, body)
    134 	req.Header.Set("Content-Type", "application/json")
    135 	resp, err := client.Do(req)
    136 	if err != nil {
    137 		t.Fatalf("post json: %v", err)
    138 	}
    139 	defer resp.Body.Close()
    140 	if resp.StatusCode != want {
    141 		t.Fatalf("POST %s: got %d want %d", url, resp.StatusCode, want)
    142 	}
    143 	var out T
    144 	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
    145 		t.Fatalf("decode: %v", err)
    146 	}
    147 	return out
    148 }
    149 
    150 func getJSONWithClientAuth[T any](t *testing.T, client *http.Client, url string, want int) T {
    151 	t.Helper()
    152 	resp, err := client.Get(url)
    153 	if err != nil {
    154 		t.Fatalf("get: %v", err)
    155 	}
    156 	defer resp.Body.Close()
    157 	if resp.StatusCode != want {
    158 		t.Fatalf("GET %s: got %d want %d", url, resp.StatusCode, want)
    159 	}
    160 	var out T
    161 	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
    162 		t.Fatalf("decode: %v", err)
    163 	}
    164 	return out
    165 }