This commit is contained in:
delete 2026-06-10 20:51:17 +03:00
commit b15b95781c
108 changed files with 14802 additions and 0 deletions

View file

@ -0,0 +1,194 @@
package httpserver_test
import (
"net/http"
"net/url"
"testing"
"update_server/internal/apikeys"
"update_server/internal/config"
"update_server/internal/db"
)
func TestAdminPOSTRejectsMissingOrInvalidCSRFToken(t *testing.T) {
t.Parallel()
router, _, _ := newTestRouterWithStore(t)
sessionCookie := loginAsAdmin(t, router)
missingRecorder := performRequest(t, router, http.MethodPost, "/admin/projects", url.Values{
"name": {"Desktop App"},
"slug": {"desktop-app"},
}, sessionCookie)
if missingRecorder.Code != http.StatusForbidden {
t.Fatalf("expected missing csrf token to return 403, got %d with body %s", missingRecorder.Code, missingRecorder.Body.String())
}
csrfCookie := ensureCSRFCookie(t, router, sessionCookie)
invalidRecorder := performRequest(t, router, http.MethodPost, "/admin/projects", url.Values{
"name": {"Desktop App"},
"slug": {"desktop-app"},
"csrf_token": {"definitely-wrong"},
}, sessionCookie, csrfCookie)
if invalidRecorder.Code != http.StatusForbidden {
t.Fatalf("expected invalid csrf token to return 403, got %d with body %s", invalidRecorder.Code, invalidRecorder.Body.String())
}
location := submitForm(t, router, http.MethodPost, "/admin/projects", url.Values{
"name": {"Desktop App"},
"slug": {"desktop-app"},
}, http.StatusSeeOther, sessionCookie)
if location == "" {
t.Fatal("expected valid csrf-protected form to redirect")
}
}
func TestSecurityHeadersAndCookieDefaults(t *testing.T) {
t.Parallel()
router, cfg, store := newTestRouterWithConfig(t, func(cfg *config.Config) {
cfg.BaseURL = "https://updates.example.com"
cfg.SecureCookies = true
})
loginPageRecorder := performRequest(t, router, http.MethodGet, "/admin/login", nil)
if loginPageRecorder.Code != http.StatusOK {
t.Fatalf("expected login page to load, got %d", loginPageRecorder.Code)
}
assertHeaderContains(t, loginPageRecorder, "Cache-Control", "no-store")
assertHeaderEquals(t, loginPageRecorder, "X-Frame-Options", "DENY")
assertHeaderEquals(t, loginPageRecorder, "X-Content-Type-Options", "nosniff")
assertHeaderEquals(t, loginPageRecorder, "Referrer-Policy", "no-referrer")
assertHeaderContains(t, loginPageRecorder, "Content-Security-Policy", "frame-ancestors 'none'")
assertHeaderContains(t, loginPageRecorder, "Strict-Transport-Security", "max-age=31536000")
csrfCookie := ensureCSRFCookie(t, router)
if csrfCookie.Path != "/admin" {
t.Fatalf("expected csrf cookie path /admin, got %q", csrfCookie.Path)
}
if !csrfCookie.HttpOnly {
t.Fatal("expected csrf cookie to be HttpOnly")
}
if !csrfCookie.Secure {
t.Fatal("expected csrf cookie to be Secure when APP_BASE_URL is https")
}
if csrfCookie.SameSite != http.SameSiteStrictMode {
t.Fatalf("expected csrf cookie SameSite=Strict, got %v", csrfCookie.SameSite)
}
loginRecorder := performRequest(t, router, http.MethodPost, "/admin/login", url.Values{
"email": {"admin@example.com"},
"password": {"correct horse battery staple"},
"next": {"/admin"},
"csrf_token": {csrfCookie.Value},
}, csrfCookie)
if loginRecorder.Code != http.StatusSeeOther {
t.Fatalf("expected login to redirect, got %d with body %s", loginRecorder.Code, loginRecorder.Body.String())
}
var sessionCookie *http.Cookie
for _, cookie := range loginRecorder.Result().Cookies() {
if cookie.Name == cfg.SessionCookieName {
sessionCookie = cookie
break
}
}
if sessionCookie == nil {
t.Fatal("expected session cookie after login")
}
if sessionCookie.Path != "/admin" {
t.Fatalf("expected session cookie path /admin, got %q", sessionCookie.Path)
}
if !sessionCookie.HttpOnly {
t.Fatal("expected session cookie to be HttpOnly")
}
if !sessionCookie.Secure {
t.Fatal("expected session cookie to be Secure")
}
if sessionCookie.SameSite != http.SameSiteLaxMode {
t.Fatalf("expected session cookie SameSite=Lax, got %v", sessionCookie.SameSite)
}
keyResult, err := apikeys.NewService(store).Create(t.Context(), apikeys.CreateParams{
Name: "Clients",
ScopeMode: db.ScopeModeAllProjects,
CanDownload: true,
})
if err != nil {
t.Fatalf("create api key: %v", err)
}
apiRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+keyResult.RawKey)
if apiRecorder.Code != http.StatusOK {
t.Fatalf("expected api project listing to succeed, got %d with body %s", apiRecorder.Code, apiRecorder.Body.String())
}
assertHeaderContains(t, apiRecorder, "Cache-Control", "no-store")
assertHeaderContains(t, apiRecorder, "Vary", "Authorization")
assertHeaderEquals(t, apiRecorder, "X-Content-Type-Options", "nosniff")
assertHeaderEquals(t, apiRecorder, "X-Frame-Options", "DENY")
assertHeaderContains(t, apiRecorder, "X-Robots-Tag", "noindex")
}
func TestLoginRateLimitReturnsTooManyRequests(t *testing.T) {
t.Parallel()
router, _, _ := newTestRouterWithConfig(t, func(cfg *config.Config) {
cfg.LoginRateLimitPerMinute = 60
cfg.LoginRateLimitBurst = 2
})
csrfCookie := ensureCSRFCookie(t, router)
form := url.Values{
"email": {"admin@example.com"},
"password": {"wrong-password"},
"next": {"/admin"},
"csrf_token": {csrfCookie.Value},
}
for attempt := 0; attempt < 2; attempt++ {
recorder := performRequest(t, router, http.MethodPost, "/admin/login", form, csrfCookie)
if recorder.Code != http.StatusUnauthorized {
t.Fatalf("expected attempt %d to return 401, got %d with body %s", attempt+1, recorder.Code, recorder.Body.String())
}
}
limitedRecorder := performRequest(t, router, http.MethodPost, "/admin/login", form, csrfCookie)
if limitedRecorder.Code != http.StatusTooManyRequests {
t.Fatalf("expected limited login to return 429, got %d with body %s", limitedRecorder.Code, limitedRecorder.Body.String())
}
assertHeaderContains(t, limitedRecorder, "Retry-After", "1")
}
func TestClientAPIRateLimitReturnsTooManyRequests(t *testing.T) {
t.Parallel()
router, _, store := newTestRouterWithConfig(t, func(cfg *config.Config) {
cfg.ClientRateLimitPerMinute = 60
cfg.ClientRateLimitBurst = 2
})
keyResult, err := apikeys.NewService(store).Create(t.Context(), apikeys.CreateParams{
Name: "Clients",
ScopeMode: db.ScopeModeAllProjects,
CanDownload: true,
})
if err != nil {
t.Fatalf("create api key: %v", err)
}
for attempt := 0; attempt < 2; attempt++ {
recorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+keyResult.RawKey)
if recorder.Code != http.StatusOK {
t.Fatalf("expected api attempt %d to return 200, got %d with body %s", attempt+1, recorder.Code, recorder.Body.String())
}
}
limitedRecorder := performAPIRequest(t, router, http.MethodGet, "/api/v1/projects", "Bearer "+keyResult.RawKey)
if limitedRecorder.Code != http.StatusTooManyRequests {
t.Fatalf("expected api rate limit to return 429, got %d with body %s", limitedRecorder.Code, limitedRecorder.Body.String())
}
assertHeaderContains(t, limitedRecorder, "Retry-After", "1")
}