update_server/internal/http/api_keys_integration_test.go
2026-06-10 20:51:17 +03:00

213 lines
6.8 KiB
Go

package httpserver_test
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strconv"
"strings"
"testing"
)
func TestAdminAPIKeyFlowRevealsRawKeyOnceAndSupportsRevocation(t *testing.T) {
t.Parallel()
router, _, store := newTestRouterWithStore(t)
sessionCookie := loginAsAdmin(t, router)
projectLocation := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
"name": {"Desktop App"},
"slug": {"desktop-app"},
}, http.StatusSeeOther, sessionCookie)
projectID := extractResourceID(t, projectLocation, "/admin/projects/")
submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
"name": {"Mobile App"},
"slug": {"mobile-app"},
}, http.StatusSeeOther, sessionCookie)
tagLocation := submitForm(t, router, http.MethodPost, "/admin/tags", url.Values{
"name": {"Windows"},
"slug": {"windows"},
}, http.StatusSeeOther, sessionCookie)
tagID := extractResourceID(t, tagLocation, "/admin/tags/")
submitForm(t, router, http.MethodPost, "/admin/projects/"+projectID+"/tags", url.Values{
"tag_id": {tagID},
}, http.StatusSeeOther, sessionCookie)
csrfCookie := ensureCSRFCookie(t, router, sessionCookie)
recorder := performRequest(t, router, http.MethodPost, "/admin/api-keys", url.Values{
"name": {"Windows Clients"},
"description": {"Download access for Windows builds."},
"scope_mode": {"tag_allow_list"},
"can_download": {"1"},
"can_manage_projects": {"1"},
"tag_id": {tagID},
"csrf_token": {csrfCookie.Value},
}, sessionCookie, csrfCookie)
if recorder.Code != http.StatusOK {
t.Fatalf("expected create api key page, got %d with body %s", recorder.Code, recorder.Body.String())
}
assertHeaderContains(t, recorder, "Cache-Control", "no-store")
assertHeaderEquals(t, recorder, "Pragma", "no-cache")
assertHeaderEquals(t, recorder, "Expires", "0")
body := recorder.Body.String()
rawKey := findRawAPIKey(t, body)
if !strings.Contains(body, rawKey) {
t.Fatal("expected raw api key to be shown on creation response")
}
assertBodyContains(t, body, `value="Windows Clients"`)
assertBodyContains(t, body, "Download access for Windows builds.")
assertBodyContains(t, body, `option value="tag_allow_list" selected`)
assertBodyContains(t, body, `name="can_download" value="1" checked`)
assertBodyContains(t, body, `name="can_manage_projects" value="1" checked`)
assertBodyContains(t, body, `name="tag_id" value="`+tagID+`" checked`)
assertBodyContains(t, body, "/api/v1/projects")
assertBodyContains(t, body, "/api/v1/projects/desktop-app/releases/latest")
assertBodyContains(t, body, "Authorization: Bearer")
keys, err := store.APIKeys.List(context.Background())
if err != nil {
t.Fatalf("list api keys: %v", err)
}
if len(keys) != 1 {
t.Fatalf("expected one api key, got %d", len(keys))
}
keyID := keys[0].APIKey.ID
storedKey, err := store.APIKeys.GetByID(context.Background(), keyID)
if err != nil {
t.Fatalf("load stored api key: %v", err)
}
if storedKey.KeyHash == rawKey {
t.Fatal("expected stored api key hash to differ from the raw key")
}
accessibleProjects, err := store.APIKeys.ListAccessibleProjects(context.Background(), keyID, storedKey.ScopeMode)
if err != nil {
t.Fatalf("list accessible projects: %v", err)
}
if len(accessibleProjects) != 1 || accessibleProjects[0].Name != "Desktop App" {
t.Fatalf("expected only Desktop App to be accessible, got %+v", accessibleProjects)
}
recorder = performRequest(t, router, http.MethodGet, "/admin/api-keys/"+itoa64(keyID), nil, sessionCookie)
if recorder.Code != http.StatusOK {
t.Fatalf("expected api key detail page to load, got %d", recorder.Code)
}
if strings.Contains(recorder.Body.String(), rawKey) {
t.Fatal("expected raw api key to disappear after the creation response")
}
submitForm(t, router, http.MethodPost, "/admin/api-keys/"+itoa64(keyID)+"/activate", url.Values{
"state": {"revoke"},
}, http.StatusSeeOther, sessionCookie)
storedKey, err = store.APIKeys.GetByID(context.Background(), keyID)
if err != nil {
t.Fatalf("reload revoked api key: %v", err)
}
if storedKey.IsActive {
t.Fatal("expected api key to be revoked")
}
}
func TestAdminAPIKeyCreateResponseShowsPersistedProjectRules(t *testing.T) {
t.Parallel()
router, _, _ := newTestRouterWithStore(t)
sessionCookie := loginAsAdmin(t, router)
projectLocation := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
"name": {"Desktop App"},
"slug": {"desktop-app"},
}, http.StatusSeeOther, sessionCookie)
projectID := extractResourceID(t, projectLocation, "/admin/projects/")
submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
"name": {"Mobile App"},
"slug": {"mobile-app"},
}, http.StatusSeeOther, sessionCookie)
csrfCookie := ensureCSRFCookie(t, router, sessionCookie)
recorder := performRequest(t, router, http.MethodPost, "/admin/api-keys", url.Values{
"name": {"Desktop Only"},
"description": {"Project allow-list clients."},
"scope_mode": {"project_allow_list"},
"can_download": {"1"},
"project_id": {projectID},
"csrf_token": {csrfCookie.Value},
}, sessionCookie, csrfCookie)
if recorder.Code != http.StatusOK {
t.Fatalf("expected create api key page, got %d with body %s", recorder.Code, recorder.Body.String())
}
body := recorder.Body.String()
assertBodyContains(t, body, `value="Desktop Only"`)
assertBodyContains(t, body, "Project allow-list clients.")
assertBodyContains(t, body, `option value="project_allow_list" selected`)
assertBodyContains(t, body, `name="project_id" value="`+projectID+`" checked`)
assertBodyContains(t, body, "/api/v1/projects")
assertBodyContains(t, body, "/api/v1/projects/desktop-app/releases/latest")
assertHeaderContains(t, recorder, "Cache-Control", "no-store")
}
func findRawAPIKey(t *testing.T, body string) string {
t.Helper()
re := regexp.MustCompile(`upsk_[A-Za-z0-9_-]+`)
matches := re.FindAllString(body, -1)
if len(matches) == 0 {
t.Fatal("expected raw api key in response body")
}
longest := matches[0]
for _, match := range matches[1:] {
if len(match) > len(longest) {
longest = match
}
}
return longest
}
func itoa64(value int64) string {
return strconv.FormatInt(value, 10)
}
func assertBodyContains(t *testing.T, body, fragment string) {
t.Helper()
if !strings.Contains(body, fragment) {
t.Fatalf("expected response body to contain %q", fragment)
}
}
func assertHeaderContains(t *testing.T, recorder *httptest.ResponseRecorder, key, fragment string) {
t.Helper()
if value := recorder.Header().Get(key); !strings.Contains(value, fragment) {
t.Fatalf("expected %s header to contain %q, got %q", key, fragment, value)
}
}
func assertHeaderEquals(t *testing.T, recorder *httptest.ResponseRecorder, key, want string) {
t.Helper()
if value := recorder.Header().Get(key); value != want {
t.Fatalf("expected %s header %q, got %q", key, want, value)
}
}