226 lines
8.6 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|