init
This commit is contained in:
commit
b15b95781c
108 changed files with 14802 additions and 0 deletions
213
internal/http/api_keys_integration_test.go
Normal file
213
internal/http/api_keys_integration_test.go
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue