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 }