Files
traefikoidc/cmd/oidcgate/endpoints_test.go
T

168 lines
5.1 KiB
Go

package main
import (
"net/http"
"net/http/httptest"
"testing"
)
// stubMiddleware lets us test endpoint wiring without spinning up a full
// traefikoidc instance. Each test injects the behavior it wants.
type stubMiddleware struct {
calls []stubCall
fn func(rw http.ResponseWriter, req *http.Request)
}
type stubCall struct {
path string
header http.Header
}
func (s *stubMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.calls = append(s.calls, stubCall{path: req.URL.Path, header: req.Header.Clone()})
if s.fn != nil {
s.fn(rw, req)
}
}
func TestAuth_RewritesToSentinel_AndConverts302To401(t *testing.T) {
stub := &stubMiddleware{
fn: func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Location", "https://idp.example/authorize?state=abc")
rw.Header().Add("Set-Cookie", "_oidc_state=abc; Path=/")
rw.WriteHeader(http.StatusFound)
},
}
h := newAuthHandler(stub)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/oauth2/auth", nil)
req.Header.Set("X-Forwarded-Uri", "/protected/page")
h.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status: want 401, got %d", rec.Code)
}
if len(stub.calls) != 1 || stub.calls[0].path != sentinelPath {
t.Fatalf("middleware path: want %q, got %v", sentinelPath, stub.calls)
}
if rec.Header().Get("X-Auth-Redirect") == "" {
t.Error("X-Auth-Redirect should carry Location")
}
if got := stub.calls[0].header.Get("X-Forwarded-Uri"); got != "/protected/page" {
t.Errorf("X-Forwarded-Uri must pass through to middleware: want /protected/page, got %q", got)
}
}
func TestAuth_AuthenticatedReturnsHeadersAnd200(t *testing.T) {
stub := &stubMiddleware{
fn: func(rw http.ResponseWriter, req *http.Request) {
// Middleware would stamp X-Forwarded-User on req then call next.
req.Header.Set("X-Forwarded-User", "alice")
newSuccessHandler().ServeHTTP(rw, req)
},
}
h := newAuthHandler(stub)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/oauth2/auth", nil)
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: want 200, got %d", rec.Code)
}
if got := rec.Header().Get("X-Forwarded-User"); got != "alice" {
t.Errorf("X-Forwarded-User mirrored: want alice, got %q", got)
}
}
func TestStart_DelegatesWithSentinel_NoInterception(t *testing.T) {
stub := &stubMiddleware{
fn: func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Location", "https://idp.example/authorize")
rw.WriteHeader(http.StatusFound)
},
}
h := newStartHandler(stub)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/oauth2/start?rd=/back", nil)
h.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("start: 302 must flow through, got %d", rec.Code)
}
if stub.calls[0].path != sentinelPath {
t.Fatalf("start path rewrite: want %q, got %q", sentinelPath, stub.calls[0].path)
}
}
func TestStart_ForwardsRdAsXForwardedURI(t *testing.T) {
stub := &stubMiddleware{
fn: func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusFound) },
}
h := newStartHandler(stub)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/oauth2/start?rd=/back/here", nil)
h.ServeHTTP(rec, req)
if got := stub.calls[0].header.Get("X-Forwarded-Uri"); got != "/back/here" {
t.Fatalf("?rd should become X-Forwarded-Uri: want /back/here, got %q", got)
}
}
func TestStart_RdQueryWinsOverUpstreamHeader(t *testing.T) {
stub := &stubMiddleware{
fn: func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusFound) },
}
h := newStartHandler(stub)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/oauth2/start?rd=/explicit", nil)
req.Header.Set("X-Forwarded-Uri", "/ambient")
h.ServeHTTP(rec, req)
if got := stub.calls[0].header.Get("X-Forwarded-Uri"); got != "/explicit" {
t.Fatalf("?rd= must win over upstream X-Forwarded-Uri: want /explicit, got %q", got)
}
}
func TestCallback_RewritesToConfiguredCallbackURL(t *testing.T) {
var seenPath, seenQuery string
stub := &stubMiddleware{
fn: func(rw http.ResponseWriter, req *http.Request) {
seenPath = req.URL.Path
seenQuery = req.URL.RawQuery
rw.WriteHeader(http.StatusOK)
},
}
h := newCallbackHandler(stub, "/oauth2/callback")
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/oauth2/callback?code=abc&state=xyz", nil)
h.ServeHTTP(rec, req)
if seenPath != "/oauth2/callback" {
t.Fatalf("callback path: want /oauth2/callback, got %q", seenPath)
}
if seenQuery != "code=abc&state=xyz" {
t.Fatalf("callback query must survive rewrite: want code=abc&state=xyz, got %q", seenQuery)
}
}
func TestLogout_RewritesToConfiguredLogoutURL(t *testing.T) {
var seenPath string
stub := &stubMiddleware{
fn: func(rw http.ResponseWriter, req *http.Request) {
seenPath = req.URL.Path
rw.WriteHeader(http.StatusOK)
},
}
h := newLogoutHandler(stub, "/oauth2/logout")
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/oauth2/logout", nil)
h.ServeHTTP(rec, req)
if seenPath != "/oauth2/logout" {
t.Fatalf("logout path: want /oauth2/logout, got %q", seenPath)
}
}