//go:build linux || freebsd // +build linux freebsd package auth import ( "context" "encoding/base64" "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) // ============================================================================ // basic.go tests // ============================================================================ func TestBasicAuth(t *testing.T) { t.Run("sets header correctly", func(t *testing.T) { req, _ := http.NewRequest("GET", "https://example.com/resource", nil) BasicAuth(req, "alice", "secret123") auth := req.Header.Get("Authorization") if auth == "" { t.Fatal("expected Authorization header to be set") } if !strings.HasPrefix(auth, "Basic ") { t.Fatalf("expected Basic prefix, got %q", auth) } encoded := strings.TrimPrefix(auth, "Basic ") decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { t.Fatalf("failed to decode base64: %v", err) } expected := "alice:secret123" if string(decoded) != expected { t.Fatalf("expected decoded credentials %q, got %q", expected, string(decoded)) } }) t.Run("empty username does nothing", func(t *testing.T) { req, _ := http.NewRequest("GET", "https://example.com/resource", nil) BasicAuth(req, "", "secret123") if h := req.Header.Get("Authorization"); h != "" { t.Fatalf("expected no Authorization header when username is empty, got %q", h) } }) t.Run("empty password still sets header", func(t *testing.T) { req, _ := http.NewRequest("GET", "https://example.com/resource", nil) BasicAuth(req, "bob", "") auth := req.Header.Get("Authorization") if auth == "" { t.Fatal("expected Authorization header to be set even with empty password") } }) t.Run("colon in password", func(t *testing.T) { req, _ := http.NewRequest("GET", "https://example.com/resource", nil) BasicAuth(req, "charlie", "pass:word:with:colons") encoded := strings.TrimPrefix(req.Header.Get("Authorization"), "Basic ") decoded, _ := base64.StdEncoding.DecodeString(encoded) if string(decoded) != "charlie:pass:word:with:colons" { t.Fatalf("password with colons not preserved: %q", string(decoded)) } }) } func TestParseURLAuth(t *testing.T) { t.Run("valid URL with credentials", func(t *testing.T) { u, _ := url.Parse("https://user:pass@example.com/path") username, password, ok := ParseURLAuth(u) if !ok { t.Fatal("expected auth to be found") } if username != "user" { t.Fatalf("expected username 'user', got %q", username) } if password != "pass" { t.Fatalf("expected password 'pass', got %q", password) } }) t.Run("URL without credentials", func(t *testing.T) { u, _ := url.Parse("https://example.com/path") _, _, ok := ParseURLAuth(u) if ok { t.Fatal("expected no auth for URL without credentials") } }) t.Run("nil URL", func(t *testing.T) { _, _, ok := ParseURLAuth(nil) if ok { t.Fatal("expected no auth for nil URL") } }) t.Run("username only (no password)", func(t *testing.T) { u, _ := url.Parse("https://user@example.com/path") username, password, ok := ParseURLAuth(u) if !ok { t.Fatal("expected auth for username-only URL") } if username != "user" { t.Fatalf("expected username 'user', got %q", username) } _ = password // password can be empty; we just check ok is true }) t.Run("empty username in URL", func(t *testing.T) { u, _ := url.Parse("https://:pass@example.com/path") _, _, ok := ParseURLAuth(u) if ok { t.Fatal("expected no auth for empty username") } }) t.Run("special characters in credentials", func(t *testing.T) { u, _ := url.Parse("https://user%40domain:pass%21%40%23@example.com/path") username, password, ok := ParseURLAuth(u) if !ok { t.Fatal("expected auth with special characters") } if username != "user@domain" { t.Fatalf("expected decoded username 'user@domain', got %q", username) } if password != "pass!@#" { t.Fatalf("expected decoded password 'pass!@#', got %q", password) } }) } func TestStripAuthFromURL(t *testing.T) { t.Run("removes credentials", func(t *testing.T) { u, _ := url.Parse("https://user:pass@example.com/path?q=v") clean := StripAuthFromURL(u) if clean.User != nil { t.Fatal("expected user info to be nil") } if clean.String() != "https://example.com/path?q=v" { t.Fatalf("unexpected stripped URL: %s", clean.String()) } }) t.Run("URL without credentials unchanged", func(t *testing.T) { u, _ := url.Parse("https://example.com/path") clean := StripAuthFromURL(u) if clean.User != nil { t.Fatal("expected no user info") } if clean.String() != "https://example.com/path" { t.Fatalf("unexpected stripped URL: %s", clean.String()) } }) t.Run("nil URL", func(t *testing.T) { clean := StripAuthFromURL(nil) if clean != nil { t.Fatal("expected nil for nil input") } }) t.Run("does not modify original URL", func(t *testing.T) { original, _ := url.Parse("https://user:pass@example.com/path") clean := StripAuthFromURL(original) if original.User == nil { t.Fatal("original URL should be unmodified") } _ = clean // just verifying original not mutated }) t.Run("preserves path and query", func(t *testing.T) { u, _ := url.Parse("https://alice:s3cret@host.com/api/v1?key=val&foo=bar") clean := StripAuthFromURL(u) want := "https://host.com/api/v1?key=val&foo=bar" got := clean.String() if got != want { t.Fatalf("expected %q, got %q", want, got) } }) t.Run("preserves port", func(t *testing.T) { u, _ := url.Parse("https://u:p@host.com:8443/path") clean := StripAuthFromURL(u) if clean.Port() != "8443" { t.Fatalf("expected port 8443, got %q", clean.Port()) } }) } func TestMaskPassword(t *testing.T) { t.Run("empty password", func(t *testing.T) { if m := MaskPassword(""); m != "" { t.Fatalf("expected empty string, got %q", m) } }) t.Run("short password (<=4 chars)", func(t *testing.T) { if m := MaskPassword("ab"); m != "****" { t.Fatalf("expected '****', got %q", m) } if m := MaskPassword("1234"); m != "****" { t.Fatalf("expected '****', got %q", m) } }) t.Run("longer password shows first 2 chars", func(t *testing.T) { m := MaskPassword("hello123") if !strings.HasPrefix(m, "he") { t.Fatalf("expected prefix 'he', got %q", m) } if len(m) != len("hello123") { t.Fatalf("expected masked length %d, got %d", len("hello123"), len(m)) } // Count asterisks asterisks := strings.Count(m, "*") if asterisks != len("hello123")-2 { t.Fatalf("expected %d asterisks, got %d", len("hello123")-2, asterisks) } }) t.Run("exactly 5 chars shows first 2 chars + 3 asterisks", func(t *testing.T) { m := MaskPassword("abcde") want := "ab***" if m != want { t.Fatalf("expected %q, got %q", want, m) } }) t.Run("unicode password still shows first 2 runes", func(t *testing.T) { m := MaskPassword("abčdef") if !strings.HasPrefix(m, "ab") { t.Fatalf("expected prefix 'ab', got %q", m) } }) } func TestGetAuthForURL(t *testing.T) { makeURL := func(raw string) *url.URL { u, _ := url.Parse(raw) return u } t.Run("CLI has highest priority", func(t *testing.T) { u := makeURL("https://urluser:urlpass@example.com/path") user, pass := GetAuthForURL("cliuser", "clipass", "cfguser", "cfgpass", u) if user != "cliuser" || pass != "clipass" { t.Fatalf("expected CLI credentials, got %q:%q", user, pass) } }) t.Run("URL credentials are second priority", func(t *testing.T) { u := makeURL("https://urluser:urlpass@example.com/path") user, pass := GetAuthForURL("", "", "cfguser", "cfgpass", u) if user != "urluser" || pass != "urlpass" { t.Fatalf("expected URL credentials, got %q:%q", user, pass) } }) t.Run("Config credentials are third priority", func(t *testing.T) { u := makeURL("https://example.com/path") user, pass := GetAuthForURL("", "", "cfguser", "cfgpass", u) if user != "cfguser" || pass != "cfgpass" { t.Fatalf("expected config credentials, got %q:%q", user, pass) } }) t.Run("no credentials available", func(t *testing.T) { u := makeURL("https://example.com/path") user, pass := GetAuthForURL("", "", "", "", u) if user != "" || pass != "" { t.Fatalf("expected empty credentials, got %q:%q", user, pass) } }) t.Run("CLI with empty password still wins", func(t *testing.T) { u := makeURL("https://urluser:urlpass@example.com/path") user, pass := GetAuthForURL("cliuser", "", "cfguser", "cfgpass", u) if user != "cliuser" || pass != "" { t.Fatalf("expected CLI username with empty password, got %q:%q", user, pass) } }) t.Run("nil URL falls back to config", func(t *testing.T) { user, pass := GetAuthForURL("", "", "cfguser", "cfgpass", nil) if user != "cfguser" || pass != "cfgpass" { t.Fatalf("expected config credentials for nil URL, got %q:%q", user, pass) } }) t.Run("nil URL with CLI still returns CLI", func(t *testing.T) { user, pass := GetAuthForURL("cliuser", "clipass", "", "", nil) if user != "cliuser" || pass != "clipass" { t.Fatalf("expected CLI credentials for nil URL, got %q:%q", user, pass) } }) t.Run("all empty returns empty", func(t *testing.T) { user, pass := GetAuthForURL("", "", "", "", nil) if user != "" || pass != "" { t.Fatalf("expected empty for all empty inputs + nil URL, got %q:%q", user, pass) } }) } // ============================================================================ // digest.go tests // ============================================================================ func TestParseDigestParams(t *testing.T) { t.Run("simple key=value pairs", func(t *testing.T) { params := parseDigestParams(`realm="testrealm@host.com", nonce="abc123"`) if params["realm"] != "testrealm@host.com" { t.Fatalf("expected realm 'testrealm@host.com', got %q", params["realm"]) } if params["nonce"] != "abc123" { t.Fatalf("expected nonce 'abc123', got %q", params["nonce"]) } }) t.Run("unquoted values", func(t *testing.T) { params := parseDigestParams(`qop=auth, algorithm=MD5`) if params["qop"] != "auth" { t.Fatalf("expected qop 'auth', got %q", params["qop"]) } if params["algorithm"] != "MD5" { t.Fatalf("expected algorithm 'MD5', got %q", params["algorithm"]) } }) t.Run("mixed quoted and unquoted", func(t *testing.T) { params := parseDigestParams(`realm="My Realm", algorithm=MD5, qop=auth, nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"`) if params["realm"] != "My Realm" { t.Fatalf("expected realm 'My Realm', got %q", params["realm"]) } if params["algorithm"] != "MD5" { t.Fatalf("expected algorithm 'MD5', got %q", params["algorithm"]) } if params["qop"] != "auth" { t.Fatalf("expected qop 'auth', got %q", params["qop"]) } if params["nonce"] != "dcd98b7102dd2f0e8b11d0f600bfb0c093" { t.Fatalf("unexpected nonce: %q", params["nonce"]) } }) t.Run("empty header", func(t *testing.T) { params := parseDigestParams("") if len(params) != 0 { t.Fatalf("expected empty map, got %v", params) } }) t.Run("whitespace handling", func(t *testing.T) { params := parseDigestParams(` realm = "spacey realm" , nonce = "abc" `) if params["realm"] != "spacey realm" { t.Fatalf("expected realm 'spacey realm', got %q", params["realm"]) } if params["nonce"] != "abc" { t.Fatalf("expected nonce 'abc', got %q", params["nonce"]) } }) t.Run("opaque parameter", func(t *testing.T) { params := parseDigestParams(`realm="r", nonce="n", opaque="abc123"`) if params["opaque"] != "abc123" { t.Fatalf("expected opaque 'abc123', got %q", params["opaque"]) } }) t.Run("stale parameter", func(t *testing.T) { params := parseDigestParams(`realm="r", nonce="n", stale=TRUE`) if params["stale"] != "TRUE" { t.Fatalf("expected stale 'TRUE', got %q", params["stale"]) } }) } func TestNewDigestAuthHandler(t *testing.T) { h := NewDigestAuthHandler("testuser", "testpass") if h == nil { t.Fatal("expected non-nil handler") } if h.username != "testuser" { t.Fatalf("expected username 'testuser', got %q", h.username) } if h.password != "testpass" { t.Fatalf("expected password 'testpass', got %q", h.password) } } func TestDigestHandleResponse(t *testing.T) { t.Run("successful digest auth response with qop=auth", func(t *testing.T) { handler := NewDigestAuthHandler("Mufasa", "Circle Of Life") req, _ := http.NewRequest("GET", "/dir/index.html", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", algorithm=MD5, qop=auth`) // We need to let HandleResponse use its own GenerateCnonce, but we need // to manually set up the params to include nc and cnonce. However, the // code reads nc and cnonce from the parsed params, which come from the // WWW-Authenticate header. In real usage these are client-generated. // For this test, we'll include them in the header to exercise the qop path. resp.Header.Set("WWW-Authenticate", `Digest realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", algorithm=MD5, qop=auth, nc=00000001, cnonce="0a4f113b"`) newReq, err := handler.HandleResponse(req, resp) if err != nil { t.Fatalf("unexpected error: %v", err) } authHeader := newReq.Header.Get("Authorization") if authHeader == "" { t.Fatal("expected Authorization header") } if !strings.HasPrefix(authHeader, "Digest ") { t.Fatalf("expected Digest prefix, got %q", authHeader) } // Verify key components are present if !strings.Contains(authHeader, `username="Mufasa"`) { t.Fatalf("expected username in auth header: %s", authHeader) } if !strings.Contains(authHeader, `realm="testrealm@host.com"`) { t.Fatalf("expected realm in auth header: %s", authHeader) } if !strings.Contains(authHeader, `nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"`) { t.Fatalf("expected nonce in auth header: %s", authHeader) } if !strings.Contains(authHeader, `uri="/dir/index.html"`) { t.Fatalf("expected uri in auth header: %s", authHeader) } if !strings.Contains(authHeader, `response="`) { t.Fatalf("expected response in auth header: %s", authHeader) } if !strings.Contains(authHeader, `qop=auth`) { t.Fatalf("expected qop=auth in auth header: %s", authHeader) } if !strings.Contains(authHeader, `nc=00000001`) { t.Fatalf("expected nc in auth header: %s", authHeader) } if !strings.Contains(authHeader, `cnonce="0a4f113b"`) { t.Fatalf("expected cnonce in auth header: %s", authHeader) } // Verify original request was not mutated if req.Header.Get("Authorization") != "" { t.Fatal("original request should not be modified") } }) t.Run("no qop (RFC 2617 style)", func(t *testing.T) { handler := NewDigestAuthHandler("Mufasa", "Circle Of Life") req, _ := http.NewRequest("GET", "/dir/index.html", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", algorithm=MD5`) newReq, err := handler.HandleResponse(req, resp) if err != nil { t.Fatalf("unexpected error: %v", err) } authHeader := newReq.Header.Get("Authorization") if authHeader == "" { t.Fatal("expected Authorization header") } // Should NOT contain qop, nc, or cnonce parameters if strings.Contains(authHeader, `qop=`) && strings.Contains(authHeader, `nc=`) { t.Fatalf("no-qop mode should not include qop/nc/cnonce, got: %s", authHeader) } }) t.Run("not a digest auth challenge", func(t *testing.T) { handler := NewDigestAuthHandler("user", "pass") req, _ := http.NewRequest("GET", "/", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", "Basic realm=\"test\"") _, err := handler.HandleResponse(req, resp) if err == nil { t.Fatal("expected error for non-digest challenge") } if !strings.Contains(err.Error(), "not a digest auth challenge") { t.Fatalf("unexpected error message: %v", err) } }) t.Run("missing required parameters returns error", func(t *testing.T) { handler := NewDigestAuthHandler("user", "pass") req, _ := http.NewRequest("GET", "/", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest algorithm=MD5`) _, err := handler.HandleResponse(req, resp) if err == nil { t.Fatal("expected error for missing required params") } }) t.Run("no WWW-Authenticate header", func(t *testing.T) { handler := NewDigestAuthHandler("user", "pass") req, _ := http.NewRequest("GET", "/", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } // No WWW-Authenticate header at all _, err := handler.HandleResponse(req, resp) if err == nil { t.Fatal("expected error when no WWW-Authenticate header") } }) t.Run("with opaque parameter", func(t *testing.T) { handler := NewDigestAuthHandler("user", "pass") req, _ := http.NewRequest("GET", "/resource", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest realm="r", nonce="n", opaque="abc123xyz"`) newReq, err := handler.HandleResponse(req, resp) if err != nil { t.Fatalf("unexpected error: %v", err) } authHeader := newReq.Header.Get("Authorization") if !strings.Contains(authHeader, `opaque="abc123xyz"`) { t.Fatalf("expected opaque in auth header: %s", authHeader) } }) t.Run("qop=auth-int with cnonce and nc", func(t *testing.T) { handler := NewDigestAuthHandler("user", "pass") req, _ := http.NewRequest("POST", "/submit", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest realm="test", nonce="abc123", algorithm=MD5, qop=auth-int, nc=00000002, cnonce="deadbeef"`) newReq, err := handler.HandleResponse(req, resp) if err != nil { t.Fatalf("unexpected error: %v", err) } authHeader := newReq.Header.Get("Authorization") if !strings.Contains(authHeader, `qop=auth-int`) { t.Fatalf("expected qop=auth-int, got: %s", authHeader) } if !strings.Contains(authHeader, `nc=00000002`) { t.Fatalf("expected nc=00000002, got: %s", authHeader) } if !strings.Contains(authHeader, `cnonce="deadbeef"`) { t.Fatalf("expected cnonce, got: %s", authHeader) } }) t.Run("uri includes query string (RFC 7616 request-uri)", func(t *testing.T) { // RFC 7616 §3.4 (and RFC 2617 §3.2.2.3) require the digest uri to // be the request-uri (path + query), not just the path. Prior to // the fix, HandleResponse used req.URL.Path which strips the // query string and caused digest auth to fail for any URL that // carried a token in the query. handler := NewDigestAuthHandler("user", "pass") req, _ := http.NewRequest("GET", "/api/resource?token=abc&id=42", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest realm="r", nonce="n", qop=auth, nc=00000001, cnonce="0a4f"`) // Inject a cnonce so the test is deterministic; HandleResponse // will use the one from the challenge header when present. // (The handler's own cnonce is generated only if the challenge // omits it.) newReq, err := handler.HandleResponse(req, resp) if err != nil { t.Fatalf("HandleResponse: %v", err) } auth := newReq.Header.Get("Authorization") wantURI := `uri="/api/resource?token=abc&id=42"` if !strings.Contains(auth, wantURI) { t.Errorf("expected Authorization to contain %s, got: %s", wantURI, auth) } // And the bare path must NOT appear without the query. if strings.Contains(auth, `uri="/api/resource"`) && !strings.Contains(auth, wantURI) { t.Errorf("Authorization contains bare path instead of request-uri: %s", auth) } }) } func TestGenerateCnonce(t *testing.T) { cnonce := GenerateCnonce() if cnonce == "" { t.Fatal("expected non-empty cnonce") } // Should be a hex string (each byte -> 2 hex chars, 16 bytes -> 32) if len(cnonce) != 32 { t.Fatalf("expected cnonce length 32, got %d: %q", len(cnonce), cnonce) } // Should only contain hex characters for _, r := range cnonce { if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { t.Fatalf("cnonce contains non-hex character %c in %q", r, cnonce) } } // Should produce different values on each call (cryptographically random) cnonce2 := GenerateCnonce() if cnonce == cnonce2 { t.Fatal("expected two calls to produce different cnonces") } } func TestMd5Sum(t *testing.T) { tests := []struct { input string expected string }{ {"", "d41d8cd98f00b204e9800998ecf8427e"}, {"hello", "5d41402abc4b2a76b9719d911017c592"}, {"Mufasa:testrealm@host.com:Circle Of Life", "939e7578ed9e3c518a452acee763bce9"}, {"abc", "900150983cd24fb0d6963f7d28e17f72"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got := md5Sum(tt.input) if got != tt.expected { t.Fatalf("md5Sum(%q) = %q, want %q", tt.input, got, tt.expected) } }) } } func TestDigestParamsGet(t *testing.T) { p := digestParams{ "realm": "test", "qop": "auth", } t.Run("existing key", func(t *testing.T) { if v := p.get("realm"); v != "test" { t.Fatalf("expected 'test', got %q", v) } }) t.Run("missing key returns empty string", func(t *testing.T) { if v := p.get("nonexistent"); v != "" { t.Fatalf("expected empty string, got %q", v) } }) t.Run("empty map returns empty string", func(t *testing.T) { empty := digestParams{} if v := empty.get("anything"); v != "" { t.Fatalf("expected empty string, got %q", v) } }) } // ============================================================================ // oauth.go tests // ============================================================================ func TestNewOAuthClient(t *testing.T) { t.Run("defaults to Bearer token type", func(t *testing.T) { cfg := &OAuthConfig{ ClientID: "myclient", TokenURL: "https://example.com/token", } client := NewOAuthClient(cfg) if client == nil { t.Fatal("expected non-nil client") } if client.config.TokenType != "Bearer" { t.Fatalf("expected TokenType 'Bearer', got %q", client.config.TokenType) } }) t.Run("preserves custom token type", func(t *testing.T) { cfg := &OAuthConfig{ ClientID: "myclient", TokenURL: "https://example.com/token", TokenType: "MAC", } client := NewOAuthClient(cfg) if client.config.TokenType != "MAC" { t.Fatalf("expected TokenType 'MAC', got %q", client.config.TokenType) } }) t.Run("uses 30-second timeout", func(t *testing.T) { cfg := &OAuthConfig{} client := NewOAuthClient(cfg) if client.client == nil { t.Fatal("expected non-nil http client") } if client.client.Timeout != 30*time.Second { t.Fatalf("expected timeout 30s, got %v", client.client.Timeout) } }) t.Run("empty config still creates valid client", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{}) if client == nil { t.Fatal("expected non-nil client") } }) } func TestIsTokenValid(t *testing.T) { t.Run("no access token", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{}) if client.IsTokenValid() { t.Fatal("expected false when no access token") } }) t.Run("zero expiry means no expiry (always valid)", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ AccessToken: "sometoken", }) if !client.IsTokenValid() { t.Fatal("expected true when token has no expiry") } }) t.Run("token with future expiry is valid", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ AccessToken: "sometoken", Expiry: time.Now().Add(1 * time.Hour), }) if !client.IsTokenValid() { t.Fatal("expected true for token expiring in 1 hour") } }) t.Run("token expiring within 30 seconds is invalid", func(t *testing.T) { // The function checks: now + 30s < expiry → valid // So if expiry is 15s from now, then now+30s > now+15s → invalid client := NewOAuthClient(&OAuthConfig{ AccessToken: "sometoken", Expiry: time.Now().Add(15 * time.Second), }) if client.IsTokenValid() { t.Fatal("expected false for token expiring in 15 seconds") } }) t.Run("expired token is invalid", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ AccessToken: "sometoken", Expiry: time.Now().Add(-1 * time.Hour), }) if client.IsTokenValid() { t.Fatal("expected false for expired token") } }) t.Run("token exactly at 30 second boundary is valid", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ AccessToken: "sometoken", Expiry: time.Now().Add(31 * time.Second), }) if !client.IsTokenValid() { t.Fatal("expected true for token at 31 seconds") } }) } func TestGetAuthorizationURL(t *testing.T) { t.Run("basic authorization URL", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ ClientID: "client123", AuthURL: "https://provider.com/oauth/authorize", RedirectURI: "https://myapp.com/callback", }) authURL := client.GetAuthorizationURL("state123") parsed, err := url.Parse(authURL) if err != nil { t.Fatalf("invalid URL: %v", err) } q := parsed.Query() if q.Get("client_id") != "client123" { t.Fatalf("expected client_id 'client123', got %q", q.Get("client_id")) } if q.Get("redirect_uri") != "https://myapp.com/callback" { t.Fatalf("expected redirect_uri 'https://myapp.com/callback', got %q", q.Get("redirect_uri")) } if q.Get("response_type") != "code" { t.Fatalf("expected response_type 'code', got %q", q.Get("response_type")) } if q.Get("state") != "state123" { t.Fatalf("expected state 'state123', got %q", q.Get("state")) } // Scope should not be present when empty if q.Has("scope") { t.Fatal("expected no scope when scopes are empty") } }) t.Run("with scopes", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ ClientID: "client123", AuthURL: "https://provider.com/oauth/authorize", RedirectURI: "https://myapp.com/callback", Scopes: []string{"read", "write"}, }) authURL := client.GetAuthorizationURL("xyz") parsed, _ := url.Parse(authURL) q := parsed.Query() if q.Get("scope") != "read write" { t.Fatalf("expected scope 'read write', got %q", q.Get("scope")) } }) t.Run("empty AuthURL returns empty string", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ ClientID: "client123", }) if authURL := client.GetAuthorizationURL("state"); authURL != "" { t.Fatalf("expected empty URL, got %q", authURL) } }) t.Run("AuthURL with existing query parameters uses &", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ ClientID: "client123", AuthURL: "https://provider.com/oauth/authorize?existing=param", RedirectURI: "https://myapp.com/callback", }) authURL := client.GetAuthorizationURL("state456") if !strings.Contains(authURL, "&") { t.Fatalf("expected & to append params when URL already has query: %s", authURL) } if !strings.Contains(authURL, "existing=param") { t.Fatalf("expected existing params preserved: %s", authURL) } }) t.Run("AuthURL without query uses ?", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ ClientID: "client123", AuthURL: "https://provider.com/oauth/authorize", RedirectURI: "https://myapp.com/callback", }) authURL := client.GetAuthorizationURL("state789") if !strings.Contains(authURL, "?") { t.Fatalf("expected ? in URL: %s", authURL) } // After the first param it's still ? not & parts := strings.SplitN(authURL, "?", 2) if len(parts) == 2 && strings.Contains(parts[1], "?") { t.Fatalf("URL should have only one ?: %s", authURL) } }) } func TestUpdateToken(t *testing.T) { t.Run("full update", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{}) before := time.Now() resp := &TokenResponse{ AccessToken: "new_access_token", TokenType: "Bearer", RefreshToken: "new_refresh_token", ExpiresIn: 3600, } client.UpdateToken(resp) if client.config.AccessToken != "new_access_token" { t.Fatalf("expected access_token 'new_access_token', got %q", client.config.AccessToken) } if client.config.TokenType != "Bearer" { t.Fatalf("expected token_type 'Bearer', got %q", client.config.TokenType) } if client.config.RefreshToken != "new_refresh_token" { t.Fatalf("expected refresh_token 'new_refresh_token', got %q", client.config.RefreshToken) } // Expiry should be ~3600s from now expectedExpiry := before.Add(3600 * time.Second) if client.config.Expiry.Before(expectedExpiry.Add(-2*time.Second)) || client.config.Expiry.After(expectedExpiry.Add(2*time.Second)) { t.Fatalf("expected expiry around %v, got %v", expectedExpiry, client.config.Expiry) } }) t.Run("empty refresh token does not overwrite existing", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ RefreshToken: "existing_refresh", AccessToken: "old_token", }) resp := &TokenResponse{ AccessToken: "new_token", TokenType: "Bearer", // RefreshToken is empty } client.UpdateToken(resp) if client.config.RefreshToken != "existing_refresh" { t.Fatalf("expected refresh_token to remain 'existing_refresh', got %q", client.config.RefreshToken) } }) t.Run("zero ExpiresIn does not change expiry", func(t *testing.T) { originalExpiry := time.Now().Add(30 * time.Minute) client := NewOAuthClient(&OAuthConfig{ Expiry: originalExpiry, }) resp := &TokenResponse{ AccessToken: "new_token", TokenType: "Bearer", ExpiresIn: 0, } client.UpdateToken(resp) if !client.config.Expiry.Equal(originalExpiry) { t.Fatalf("expected expiry unchanged, got %v vs %v", client.config.Expiry, originalExpiry) } }) t.Run("negative ExpiresIn does not change expiry", func(t *testing.T) { originalExpiry := time.Now().Add(30 * time.Minute) client := NewOAuthClient(&OAuthConfig{ Expiry: originalExpiry, }) resp := &TokenResponse{ AccessToken: "new_token", TokenType: "Bearer", ExpiresIn: -1, } client.UpdateToken(resp) if !client.config.Expiry.Equal(originalExpiry) { t.Fatalf("expected expiry unchanged for negative ExpiresIn") } }) } func TestApplyToken(t *testing.T) { t.Run("sets Authorization header", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ AccessToken: "my_access_token", TokenType: "Bearer", }) req, _ := http.NewRequest("GET", "https://api.example.com/resource", nil) client.ApplyToken(req) auth := req.Header.Get("Authorization") if auth != "Bearer my_access_token" { t.Fatalf("expected 'Bearer my_access_token', got %q", auth) } }) t.Run("uses custom token type", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ AccessToken: "token123", TokenType: "MAC", }) req, _ := http.NewRequest("GET", "https://api.example.com/resource", nil) client.ApplyToken(req) auth := req.Header.Get("Authorization") if auth != "MAC token123" { t.Fatalf("expected 'MAC token123', got %q", auth) } }) t.Run("empty access token does nothing", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ TokenType: "Bearer", }) req, _ := http.NewRequest("GET", "https://api.example.com/resource", nil) client.ApplyToken(req) if h := req.Header.Get("Authorization"); h != "" { t.Fatalf("expected no Authorization header, got %q", h) } }) } func TestRequestTokenClientCredentials(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Fatalf("expected POST, got %s", r.Method) } if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { t.Fatalf("expected form content type, got %s", r.Header.Get("Content-Type")) } if r.Header.Get("Accept") != "application/json" { t.Fatalf("expected Accept application/json, got %s", r.Header.Get("Accept")) } if err := r.ParseForm(); err != nil { t.Fatalf("failed to parse form: %v", err) } if r.Form.Get("grant_type") != "client_credentials" { t.Fatalf("expected grant_type 'client_credentials', got %q", r.Form.Get("grant_type")) } if r.Form.Get("client_id") != "test_client" { t.Fatalf("expected client_id 'test_client', got %q", r.Form.Get("client_id")) } if r.Form.Get("client_secret") != "test_secret" { t.Fatalf("expected client_secret 'test_secret', got %q", r.Form.Get("client_secret")) } if r.Form.Get("scope") != "read write" { t.Fatalf("expected scope 'read write', got %q", r.Form.Get("scope")) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"access_token":"new_access_token","token_type":"Bearer","expires_in":3600}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", ClientSecret: "test_secret", TokenURL: server.URL, Scopes: []string{"read", "write"}, GrantType: "client_credentials", }) tokenResp, err := client.RequestToken(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if tokenResp.AccessToken != "new_access_token" { t.Fatalf("expected access_token 'new_access_token', got %q", tokenResp.AccessToken) } if tokenResp.TokenType != "Bearer" { t.Fatalf("expected token_type 'Bearer', got %q", tokenResp.TokenType) } if tokenResp.ExpiresIn != 3600 { t.Fatalf("expected expires_in 3600, got %d", tokenResp.ExpiresIn) } } func TestRequestTokenRefreshToken(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { t.Fatalf("parse form: %v", err) } if r.Form.Get("grant_type") != "refresh_token" { t.Fatalf("expected refresh_token grant type, got %q", r.Form.Get("grant_type")) } if r.Form.Get("refresh_token") != "my_refresh_token" { t.Fatalf("expected refresh_token 'my_refresh_token', got %q", r.Form.Get("refresh_token")) } if r.Form.Get("client_id") != "test_client" { t.Fatalf("expected client_id 'test_client'") } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"access_token":"refreshed_token","token_type":"Bearer","expires_in":7200,"refresh_token":"new_refresh_token"}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", ClientSecret: "test_secret", TokenURL: server.URL, RefreshToken: "my_refresh_token", GrantType: "refresh_token", }) tokenResp, err := client.RequestToken(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if tokenResp.AccessToken != "refreshed_token" { t.Fatalf("expected 'refreshed_token', got %q", tokenResp.AccessToken) } if tokenResp.RefreshToken != "new_refresh_token" { t.Fatalf("expected 'new_refresh_token', got %q", tokenResp.RefreshToken) } } func TestRequestTokenErrors(t *testing.T) { t.Run("server error", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, `{"error":"invalid_request","error_description":"Missing parameter"}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", TokenURL: server.URL, GrantType: "client_credentials", }) _, err := client.RequestToken(context.Background()) if err == nil { t.Fatal("expected error for bad request") } if !strings.Contains(err.Error(), "400") { t.Fatalf("expected status code in error: %v", err) } }) t.Run("oauth error in response body", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"error":"invalid_grant","error_description":"Token expired"}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", TokenURL: server.URL, GrantType: "client_credentials", }) _, err := client.RequestToken(context.Background()) if err == nil { t.Fatal("expected error for OAuth error in body") } if !strings.Contains(err.Error(), "invalid_grant") { t.Fatalf("expected error message containing 'invalid_grant', got: %v", err) } }) t.Run("invalid response JSON", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `this is not json`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", TokenURL: server.URL, GrantType: "client_credentials", }) _, err := client.RequestToken(context.Background()) if err == nil { t.Fatal("expected error for invalid JSON") } }) t.Run("sensitive fields outside the structured error are not leaked", func(t *testing.T) { // Some providers echo the client_secret (or other secrets) back // in the JSON body alongside the structured error fields. The // error string must only contain the structured fields, never the // raw body. const leakedSecret = "SECRET_TOKEN_DO_NOT_LEAK" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, `{"error":"invalid_request","error_description":"missing parameter","client_secret":"%s"}`, leakedSecret) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", ClientSecret: leakedSecret, TokenURL: server.URL, GrantType: "client_credentials", }) _, err := client.RequestToken(context.Background()) if err == nil { t.Fatal("expected error") } if strings.Contains(err.Error(), leakedSecret) { t.Errorf("error message leaked secret: %v", err) } }) t.Run("long unstructured body is truncated", func(t *testing.T) { // Generate a body longer than the 512-byte truncation threshold // and assert that the error string does not contain the full body. longBody := strings.Repeat("x", 2048) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, longBody) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", TokenURL: server.URL, GrantType: "client_credentials", }) _, err := client.RequestToken(context.Background()) if err == nil { t.Fatal("expected error") } if strings.Contains(err.Error(), longBody) { t.Errorf("error message contains the full unstructured body (no truncation): len=%d", len(err.Error())) } if !strings.Contains(err.Error(), "(truncated)") { t.Errorf("error message should mark the body as truncated, got: %v", err) } }) } func TestRefreshToken(t *testing.T) { t.Run("successful refresh", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { t.Fatalf("parse form: %v", err) } if r.Form.Get("grant_type") != "refresh_token" { t.Fatalf("expected grant_type 'refresh_token', got %q", r.Form.Get("grant_type")) } if r.Form.Get("refresh_token") != "my_rt" { t.Fatalf("expected refresh_token 'my_rt', got %q", r.Form.Get("refresh_token")) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"access_token":"new_at","token_type":"Bearer","expires_in":1800}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", ClientSecret: "test_secret", TokenURL: server.URL, RefreshToken: "my_rt", GrantType: "client_credentials", // should be overridden }) err := client.RefreshToken(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if client.config.AccessToken != "new_at" { t.Fatalf("expected access_token 'new_at', got %q", client.config.AccessToken) } // Original grant type should be restored if client.config.GrantType != "client_credentials" { t.Fatalf("expected original grant_type restored, got %q", client.config.GrantType) } }) t.Run("no refresh token available", func(t *testing.T) { client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", TokenURL: "https://example.com/token", }) err := client.RefreshToken(context.Background()) if err == nil { t.Fatal("expected error when no refresh token") } if !strings.Contains(err.Error(), "no refresh token available") { t.Fatalf("unexpected error message: %v", err) } }) } func TestExchangeCode(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Fatalf("expected POST, got %s", r.Method) } if err := r.ParseForm(); err != nil { t.Fatalf("parse form: %v", err) } if r.Form.Get("grant_type") != "authorization_code" { t.Fatalf("expected grant_type 'authorization_code', got %q", r.Form.Get("grant_type")) } if r.Form.Get("code") != "auth_code_123" { t.Fatalf("expected code 'auth_code_123', got %q", r.Form.Get("code")) } if r.Form.Get("redirect_uri") != "https://myapp.com/callback" { t.Fatalf("unexpected redirect_uri: %q", r.Form.Get("redirect_uri")) } if r.Form.Get("client_id") != "test_client" { t.Fatalf("unexpected client_id: %q", r.Form.Get("client_id")) } if r.Form.Get("client_secret") != "test_secret" { t.Fatalf("unexpected client_secret: %q", r.Form.Get("client_secret")) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"access_token":"exchanged_token","token_type":"Bearer","expires_in":3600,"refresh_token":"rt_from_exchange"}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", ClientSecret: "test_secret", TokenURL: server.URL, RedirectURI: "https://myapp.com/callback", }) tokenResp, err := client.ExchangeCode(context.Background(), "auth_code_123") if err != nil { t.Fatalf("unexpected error: %v", err) } if tokenResp.AccessToken != "exchanged_token" { t.Fatalf("expected 'exchanged_token', got %q", tokenResp.AccessToken) } if tokenResp.RefreshToken != "rt_from_exchange" { t.Fatalf("expected 'rt_from_exchange', got %q", tokenResp.RefreshToken) } } func TestExchangeCodeErrors(t *testing.T) { t.Run("server returns error", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, `invalid request`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", TokenURL: server.URL, }) _, err := client.ExchangeCode(context.Background(), "code") if err == nil { t.Fatal("expected error") } }) t.Run("OAuth error in body", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"error":"invalid_grant","error_description":"Code expired"}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", TokenURL: server.URL, }) _, err := client.ExchangeCode(context.Background(), "code") if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "invalid_grant") { t.Fatalf("expected 'invalid_grant' in error, got: %v", err) } }) t.Run("sensitive fields outside the structured error are not leaked", func(t *testing.T) { // Mirror of the RequestToken test: a code exchange failure that // returns the client_secret in the body must not surface that // secret in the error string. const leakedSecret = "EXCHANGE_SECRET_DO_NOT_LEAK" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, `{"error":"invalid_grant","error_description":"bad code","client_secret":"%s"}`, leakedSecret) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "test_client", ClientSecret: leakedSecret, TokenURL: server.URL, }) _, err := client.ExchangeCode(context.Background(), "code") if err == nil { t.Fatal("expected error") } if strings.Contains(err.Error(), leakedSecret) { t.Errorf("error message leaked secret: %v", err) } }) } // ============================================================================ // Integration-style test: full OAuth client_credentials flow // ============================================================================ func TestOAuthFullClientCredentialsFlow(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"access_token":"integ_token","token_type":"Bearer","expires_in":3600}`) })) defer server.Close() client := NewOAuthClient(&OAuthConfig{ ClientID: "integ_client", ClientSecret: "integ_secret", TokenURL: server.URL, GrantType: "client_credentials", }) // 1. Initially no token if client.IsTokenValid() { t.Fatal("should be invalid before request") } // 2. Request token tokenResp, err := client.RequestToken(context.Background()) if err != nil { t.Fatalf("RequestToken failed: %v", err) } // 3. Update client client.UpdateToken(tokenResp) // 4. Now token should be valid if !client.IsTokenValid() { t.Fatal("token should be valid after update") } // 5. Apply token to a request req, _ := http.NewRequest("GET", "https://api.example.com/data", nil) client.ApplyToken(req) auth := req.Header.Get("Authorization") if auth != "Bearer integ_token" { t.Fatalf("expected 'Bearer integ_token', got %q", auth) } // 6. Verify config values are stored if client.config.AccessToken != "integ_token" { t.Fatalf("expected access_token 'integ_token', got %q", client.config.AccessToken) } if client.config.TokenType != "Bearer" { t.Fatalf("expected TokenType 'Bearer', got %q", client.config.TokenType) } if client.config.Expiry.IsZero() { t.Fatal("expected non-zero expiry") } } // ============================================================================ // Digest auth: verify the response hash calculation via HandleResponse // Using known test vectors from RFC 2617 // ============================================================================ func TestDigestAuthRFC2617KnownValues(t *testing.T) { // RFC 2617 example: // username="Mufasa", // realm="testrealm@host.com", // password="Circle Of Life" // nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093" // method="GET" // uri="/dir/index.html" // qop=auth // nc=00000001 // cnonce="0a4f113b" // // HA1 = MD5("Mufasa:testrealm@host.com:Circle Of Life") // = "dcd98b7102dd2f0e8b11d0f600bfb0c093" // HA2 = MD5("GET:/dir/index.html") // = "7d4b6a4c1f7c7e1a9a3e1a6c5b6f9d8e" (not from RFC, computed separately) // response = MD5(HA1:nonce:nc:cnonce:qop:HA2) handler := NewDigestAuthHandler("Mufasa", "Circle Of Life") req, _ := http.NewRequest("GET", "/dir/index.html", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", algorithm=MD5, qop=auth, nc=00000001, cnonce="0a4f113b"`) newReq, err := handler.HandleResponse(req, resp) if err != nil { t.Fatalf("HandleResponse error: %v", err) } authHeader := newReq.Header.Get("Authorization") // Verify RFC 2617 known values if !strings.Contains(authHeader, `username="Mufasa"`) { t.Errorf("missing username") } if !strings.Contains(authHeader, `realm="testrealm@host.com"`) { t.Errorf("missing realm") } if !strings.Contains(authHeader, `nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"`) { t.Errorf("missing nonce") } if !strings.Contains(authHeader, `uri="/dir/index.html"`) { t.Errorf("missing uri") } if !strings.Contains(authHeader, `qop=auth`) { t.Errorf("missing qop") } if !strings.Contains(authHeader, `nc=00000001`) { t.Errorf("missing nc") } if !strings.Contains(authHeader, `cnonce="0a4f113b"`) { t.Errorf("missing cnonce") } if !strings.Contains(authHeader, `response="`) { t.Errorf("missing response") } } func TestDigestAuthBuildAuthHeader(t *testing.T) { handler := NewDigestAuthHandler("user", "pass") // Test the private buildAuthHeader method via HandleResponse req, _ := http.NewRequest("POST", "/api/data", nil) resp := &http.Response{ StatusCode: 401, Header: make(http.Header), } resp.Header.Set("WWW-Authenticate", `Digest realm="api@example.com", nonce="abc123", algorithm=MD5, qop=auth, nc=00000005, cnonce="cnonce123"`) newReq, err := handler.HandleResponse(req, resp) if err != nil { t.Fatalf("unexpected error: %v", err) } authHeader := newReq.Header.Get("Authorization") // Check the response format if !strings.HasPrefix(authHeader, "Digest ") { t.Fatalf("should start with 'Digest '") } // Each parameter should be comma-separated parts := strings.Split(authHeader[len("Digest "):], ", ") if len(parts) < 6 { t.Fatalf("unexpected number of params: %d in %s", len(parts), authHeader) } }