137 lines
4.5 KiB
Go
137 lines
4.5 KiB
Go
package http
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func corsHandler(t *testing.T, cfg CORSConfig) http.Handler {
|
|
t.Helper()
|
|
|
|
middleware, err := CORS(cfg)
|
|
if err != nil {
|
|
t.Fatalf("CORS(%+v): %v", cfg, err)
|
|
}
|
|
|
|
return middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
}
|
|
|
|
func request(h http.Handler, method, origin string, preflight bool) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest(method, "/jobs", nil)
|
|
if origin != "" {
|
|
r.Header.Set("Origin", origin)
|
|
}
|
|
if preflight {
|
|
r.Header.Set("Access-Control-Request-Method", "POST")
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
|
|
// A wildcard origin combined with credentials would let any site issue
|
|
// credentialed requests and read the responses, so it must not be constructible.
|
|
func TestCORSRejectsWildcardWithCredentials(t *testing.T) {
|
|
if _, err := CORS(CORSConfig{AllowedOrigins: []string{"*"}, AllowCredentials: true}); err == nil {
|
|
t.Fatal("expected an error for wildcard origins combined with credentials")
|
|
}
|
|
}
|
|
|
|
func TestCORSRejectsSchemelessOrigin(t *testing.T) {
|
|
if _, err := CORS(CORSConfig{AllowedOrigins: []string{"app.example.com"}}); err == nil {
|
|
t.Fatal("expected an error for an origin without a scheme")
|
|
}
|
|
}
|
|
|
|
func TestCORSDoesNotEchoDisallowedOrigin(t *testing.T) {
|
|
h := corsHandler(t, CORSConfig{
|
|
AllowedOrigins: []string{"https://app.example.com"},
|
|
AllowCredentials: true,
|
|
})
|
|
|
|
w := request(h, http.MethodGet, "https://evil.example.com", false)
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
|
t.Errorf("Access-Control-Allow-Origin = %q, want empty for a disallowed origin", got)
|
|
}
|
|
if got := w.Header().Get("Access-Control-Allow-Credentials"); got != "" {
|
|
t.Errorf("Access-Control-Allow-Credentials = %q, want empty for a disallowed origin", got)
|
|
}
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d: the handler should still run", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestCORSAllowsListedOrigin(t *testing.T) {
|
|
h := corsHandler(t, CORSConfig{
|
|
AllowedOrigins: []string{"https://app.example.com"},
|
|
AllowCredentials: true,
|
|
})
|
|
|
|
w := request(h, http.MethodGet, "https://app.example.com", false)
|
|
|
|
if got, want := w.Header().Get("Access-Control-Allow-Origin"), "https://app.example.com"; got != want {
|
|
t.Errorf("Access-Control-Allow-Origin = %q, want %q", got, want)
|
|
}
|
|
if got, want := w.Header().Get("Access-Control-Allow-Credentials"), "true"; got != want {
|
|
t.Errorf("Access-Control-Allow-Credentials = %q, want %q", got, want)
|
|
}
|
|
// Without Vary, a shared cache could serve one origin's response to another.
|
|
if got, want := w.Header().Get("Vary"), "Origin"; got != want {
|
|
t.Errorf("Vary = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCORSOriginMatchIsCaseInsensitive(t *testing.T) {
|
|
h := corsHandler(t, CORSConfig{AllowedOrigins: []string{"https://App.Example.com"}})
|
|
|
|
w := request(h, http.MethodGet, "https://app.example.com", false)
|
|
|
|
if w.Header().Get("Access-Control-Allow-Origin") == "" {
|
|
t.Error("origin differing only in case should match the allowlist")
|
|
}
|
|
}
|
|
|
|
// An unconfigured service must fail closed rather than allowing every origin.
|
|
func TestCORSEmptyAllowlistDeniesCrossOrigin(t *testing.T) {
|
|
h := corsHandler(t, CORSConfig{})
|
|
|
|
w := request(h, http.MethodGet, "https://anything.example.com", false)
|
|
|
|
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
|
t.Errorf("Access-Control-Allow-Origin = %q, want empty when no origins are configured", got)
|
|
}
|
|
}
|
|
|
|
func TestCORSPreflightShortCircuits(t *testing.T) {
|
|
h := corsHandler(t, CORSConfig{AllowedOrigins: []string{"https://app.example.com"}})
|
|
|
|
w := request(h, http.MethodOptions, "https://app.example.com", true)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNoContent)
|
|
}
|
|
// Defaults must apply to a directly constructed config, not only to one
|
|
// built by LoadCORSConfigFromEnv.
|
|
if w.Header().Get("Access-Control-Allow-Methods") == "" {
|
|
t.Error("preflight response is missing Access-Control-Allow-Methods")
|
|
}
|
|
if w.Header().Get("Access-Control-Allow-Headers") == "" {
|
|
t.Error("preflight response is missing Access-Control-Allow-Headers")
|
|
}
|
|
}
|
|
|
|
func TestCORSPlainOptionsReachesHandler(t *testing.T) {
|
|
h := corsHandler(t, CORSConfig{AllowedOrigins: []string{"https://app.example.com"}})
|
|
|
|
// OPTIONS without Access-Control-Request-Method is not a preflight.
|
|
w := request(h, http.MethodOptions, "https://app.example.com", false)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|