Files
RenewCMS/test/cors_middleware_test.go
T

226 lines
8.6 KiB
Go

package test
import (
"RenewCMS/api/middleware"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type corsExchange struct {
rec *httptest.ResponseRecorder
reached bool
}
func corsRequest(origins []string, method string, headers map[string]string) corsExchange {
exchange := corsExchange{}
handler := middleware.CORS(origins)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
exchange.reached = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(method, "/v1/article", nil)
for name, value := range headers {
req.Header.Set(name, value)
}
exchange.rec = httptest.NewRecorder()
handler.ServeHTTP(exchange.rec, req)
return exchange
}
func corsPreflight(origins []string, origin string, method string, requestHeaders string) corsExchange {
headers := map[string]string{"Origin": origin, "Access-Control-Request-Method": method}
if requestHeaders != "" {
headers["Access-Control-Request-Headers"] = requestHeaders
}
return corsRequest(origins, http.MethodOptions, headers)
}
func TestCORSActualRequests(t *testing.T) {
origins := []string{"https://app.example.com", "https://*.preview.example.com"}
t.Run("An allowed origin is echoed and the exposed headers are listed", func(t *testing.T) {
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://app.example.com"})
if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://app.example.com" {
t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin"))
}
if got.rec.Header().Get("Access-Control-Expose-Headers") != "Link, X-Total-Count" {
t.Errorf("unexpected expose-headers %q", got.rec.Header().Get("Access-Control-Expose-Headers"))
}
if !got.reached {
t.Error("the request must reach the handler")
}
})
t.Run("Credentials are never allowed", func(t *testing.T) {
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://app.example.com"})
if got.rec.Header().Get("Access-Control-Allow-Credentials") != "" {
t.Error("Access-Control-Allow-Credentials must not be set")
}
})
t.Run("The match is case-insensitive", func(t *testing.T) {
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "HTTPS://APP.EXAMPLE.COM"})
if got.rec.Header().Get("Access-Control-Allow-Origin") == "" {
t.Error("expected the origin to be allowed")
}
})
t.Run("A wildcard subdomain pattern matches", func(t *testing.T) {
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://pr-12.preview.example.com"})
if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://pr-12.preview.example.com" {
t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin"))
}
})
t.Run("A wildcard pattern does not match a look-alike", func(t *testing.T) {
for _, origin := range []string{"https://preview.example.com", "https://x.preview.example.com.evil.net", "http://x.preview.example.com"} {
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": origin})
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" {
t.Errorf("origin %q must not be allowed", origin)
}
}
})
t.Run("An unknown origin gets no CORS headers but still reaches the handler", func(t *testing.T) {
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://evil.example.net"})
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || !got.reached {
t.Errorf("unexpected exchange %v reached=%v", got.rec.Header(), got.reached)
}
})
t.Run("A request with no Origin is untouched", func(t *testing.T) {
got := corsRequest(origins, http.MethodGet, nil)
if got.rec.Header().Get("Vary") != "" || got.rec.Header().Get("Access-Control-Allow-Origin") != "" || !got.reached {
t.Errorf("unexpected headers %v", got.rec.Header())
}
})
t.Run("Responses vary on Origin so shared caches stay correct", func(t *testing.T) {
got := corsRequest(origins, http.MethodGet, map[string]string{"Origin": "https://evil.example.net"})
if got.rec.Header().Get("Vary") != "Origin" {
t.Errorf("unexpected Vary %q", got.rec.Header().Get("Vary"))
}
})
t.Run("A lone star allows any origin", func(t *testing.T) {
got := corsRequest([]string{"*"}, http.MethodGet, map[string]string{"Origin": "https://anything.example.net"})
if got.rec.Header().Get("Access-Control-Allow-Origin") != "https://anything.example.net" {
t.Errorf("unexpected allow-origin %q", got.rec.Header().Get("Access-Control-Allow-Origin"))
}
})
t.Run("Blank entries never match the empty origin", func(t *testing.T) {
got := corsRequest([]string{"", " "}, http.MethodGet, map[string]string{"Origin": "https://app.example.com"})
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" {
t.Error("blank entries must be ignored")
}
})
}
func TestCORSPreflight(t *testing.T) {
origins := []string{"https://app.example.com"}
t.Run("An allowed preflight is answered 204 without reaching the handler", func(t *testing.T) {
got := corsPreflight(origins, "https://app.example.com", http.MethodPatch, "Content-Type, Authorization")
header := got.rec.Header()
if got.rec.Code != http.StatusNoContent || got.reached {
t.Fatalf("expected 204 and no handler, got %d reached=%v", got.rec.Code, got.reached)
}
if header.Get("Access-Control-Allow-Origin") != "https://app.example.com" {
t.Errorf("unexpected allow-origin %q", header.Get("Access-Control-Allow-Origin"))
}
if header.Get("Access-Control-Allow-Methods") != "PATCH" {
t.Errorf("unexpected allow-methods %q", header.Get("Access-Control-Allow-Methods"))
}
if header.Get("Access-Control-Max-Age") != "300" {
t.Errorf("unexpected max-age %q", header.Get("Access-Control-Max-Age"))
}
for _, allowed := range []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"} {
if !strings.Contains(header.Get("Access-Control-Allow-Headers"), allowed) {
t.Errorf("allow-headers misses %s: %q", allowed, header.Get("Access-Control-Allow-Headers"))
}
}
})
t.Run("Every supported method is allowed", func(t *testing.T) {
for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} {
got := corsPreflight(origins, "https://app.example.com", method, "")
if got.rec.Header().Get("Access-Control-Allow-Methods") != method {
t.Errorf("method %s not allowed", method)
}
}
})
t.Run("The requested method is matched case-insensitively", func(t *testing.T) {
got := corsPreflight(origins, "https://app.example.com", "patch", "")
if got.rec.Header().Get("Access-Control-Allow-Methods") != "PATCH" {
t.Errorf("unexpected allow-methods %q", got.rec.Header().Get("Access-Control-Allow-Methods"))
}
})
t.Run("A method outside the list is refused", func(t *testing.T) {
got := corsPreflight(origins, "https://app.example.com", "TRACE", "")
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || got.rec.Header().Get("Access-Control-Allow-Methods") != "" {
t.Errorf("unexpected headers %v", got.rec.Header())
}
})
t.Run("A header outside the list is refused", func(t *testing.T) {
got := corsPreflight(origins, "https://app.example.com", "GET", "Content-Type, X-Evil")
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" {
t.Errorf("unexpected headers %v", got.rec.Header())
}
})
t.Run("Requested headers are matched case-insensitively and tolerate spacing", func(t *testing.T) {
got := corsPreflight(origins, "https://app.example.com", "GET", "content-type ,AUTHORIZATION,")
if got.rec.Header().Get("Access-Control-Allow-Origin") == "" {
t.Error("expected the preflight to be allowed")
}
})
t.Run("An unknown origin is refused", func(t *testing.T) {
got := corsPreflight(origins, "https://evil.example.net", "GET", "")
if got.rec.Header().Get("Access-Control-Allow-Origin") != "" || got.reached {
t.Errorf("unexpected exchange %v reached=%v", got.rec.Header(), got.reached)
}
})
t.Run("The preflight response varies on every request header it depends on", func(t *testing.T) {
got := corsPreflight(origins, "https://app.example.com", "GET", "")
vary := strings.Join(got.rec.Header().Values("Vary"), ",")
for _, field := range []string{"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"} {
if !strings.Contains(vary, field) {
t.Errorf("Vary misses %s: %q", field, vary)
}
}
})
t.Run("An OPTIONS request without a requested method is treated as a normal request", func(t *testing.T) {
got := corsRequest(origins, http.MethodOptions, map[string]string{"Origin": "https://app.example.com"})
if !got.reached {
t.Error("expected the handler to be reached")
}
})
}