package api_test import ( "net/http" "testing" "time" ) func TestAPIKey_DefaultsToNeverExpiring(t *testing.T) { s := newTS(t) var key struct { Key string `json:"key"` ExpiresAt *string `json:"expires_at"` } decode(t, s.req(t, http.MethodPost, "/api/users/1/api-keys", map[string]string{"name": "no-expiry"}), &key) if key.ExpiresAt != nil { t.Errorf("expires_at = %v, want nil (unset expires_in_days means never expires)", *key.ExpiresAt) } } func TestAPIKey_ExpiresInDaysSetsExpiresAt(t *testing.T) { s := newTS(t) var key struct { ID int64 `json:"id"` ExpiresAt *string `json:"expires_at"` } decode(t, s.req(t, http.MethodPost, "/api/users/1/api-keys", map[string]any{"name": "rotates", "expires_in_days": 30}), &key) if key.ExpiresAt == nil { t.Fatal("expires_at = nil, want a timestamp roughly 30 days out") } got, err := time.Parse(time.RFC3339, *key.ExpiresAt) if err != nil { t.Fatalf("parse expires_at: %v", err) } want := time.Now().AddDate(0, 0, 30) if diff := want.Sub(got).Abs(); diff > time.Hour { t.Errorf("expires_at = %v, want close to %v (30 days out)", got, want) } } func TestAPIKey_ExpiresInDaysRejectsOutOfRange(t *testing.T) { s := newTS(t) for _, days := range []int{-1, 3651} { resp := s.req(t, http.MethodPost, "/api/users/1/api-keys", map[string]any{"name": "bad", "expires_in_days": days}) resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expires_in_days=%d: status = %d, want %d", days, resp.StatusCode, http.StatusBadRequest) } } } func TestAPIKey_AnExpiredKeyCannotAuthenticate(t *testing.T) { s := newTS(t) var key struct { ID int64 `json:"id"` Key string `json:"key"` } decode(t, s.req(t, http.MethodPost, "/api/users/1/api-keys", map[string]any{"name": "soon-expired", "expires_in_days": 1}), &key) // A fresh key works... req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/me", nil) req.Header.Set("Authorization", "Bearer "+key.Key) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("GET /api/me: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("fresh key: status = %d, want %d", resp.StatusCode, http.StatusOK) } // ...and stops working once its expiry has passed. s.exec(t, "UPDATE api_keys SET expires_at = $1 WHERE id = $2", time.Now().Add(-time.Hour).Unix(), key.ID) req2, _ := http.NewRequest(http.MethodGet, s.URL+"/api/me", nil) req2.Header.Set("Authorization", "Bearer "+key.Key) resp2, err := http.DefaultClient.Do(req2) if err != nil { t.Fatalf("GET /api/me: %v", err) } defer resp2.Body.Close() if resp2.StatusCode != http.StatusUnauthorized { t.Errorf("expired key: status = %d, want %d", resp2.StatusCode, http.StatusUnauthorized) } } func TestAPIKey_ListNeverReturnsTheRawKey(t *testing.T) { s := newTS(t) decode(t, s.req(t, http.MethodPost, "/api/users/1/api-keys", map[string]string{"name": "listed"}), new(struct { Key string `json:"key"` })) var keys []struct { ID int64 `json:"id"` Name string `json:"name"` Key string `json:"key"` } decode(t, s.req(t, http.MethodGet, "/api/users/1/api-keys", nil), &keys) found := false for _, k := range keys { if k.Name == "listed" { found = true } if k.Key != "" { t.Errorf("key %d (%s): raw key present in listing", k.ID, k.Name) } } if !found { t.Error("the key just created does not appear in the listing") } } func TestAPIKey_ListIsSelfOrAdmin(t *testing.T) { s := newTS(t) a := newTeam(t, s, "apikeys-a") resp := a.call(http.MethodGet, "/api/users/1/api-keys", nil) defer resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Errorf("status = %d, want %d (not self, not an admin)", resp.StatusCode, http.StatusForbidden) } }