213 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|