From a854fef67e4f802dad6f8ccc51e3a6131ce39726 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 23 Mar 2026 00:22:59 -0600 Subject: [PATCH] test(auth/csvauth): regression test for Authenticate token deadlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guards against the v1.2.4 bug (fixed in c32acd5) where Authenticate held a.mux via defer for its full duration, then called loadAndVerifyToken which also tries to acquire a.mux — deadlock on every token auth request. TestAuthenticateTokenNoDeadlock exercises both the bare-token ("", token) and named-username ("api", token) forms with a 1s timeout, so a regression fails fast rather than hanging the suite. --- auth/csvauth/csvauth_test.go | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/auth/csvauth/csvauth_test.go b/auth/csvauth/csvauth_test.go index 360032c..05eddd5 100644 --- a/auth/csvauth/csvauth_test.go +++ b/auth/csvauth/csvauth_test.go @@ -5,8 +5,59 @@ import ( "fmt" "strings" "testing" + "time" ) +// TestAuthenticateTokenNoDeadlock guards against the v1.2.4 regression where +// Authenticate held a.mux via defer and then called loadAndVerifyToken, which +// also tried to acquire a.mux — causing a deadlock on all token auth requests. +// Fixed in c32acd5 (ref(auth/csvauth): don't hold mutex longer than necessary). +func TestAuthenticateTokenNoDeadlock(t *testing.T) { + var key [16]byte + a := New(key[:]) + + const secret = "supersecrettoken" + c := a.NewCredential(PurposeToken, "ci-bot", secret, []string{"plain"}, []string{"deploy"}, "") + if err := a.CacheCredential(*c); err != nil { + t.Fatal(err) + } + + type result struct { + p any + err error + } + + // Authenticate("", token) — the token-as-password form + ch := make(chan result, 1) + go func() { + p, err := a.Authenticate("", secret) + ch <- result{p, err} + }() + select { + case r := <-ch: + if r.err != nil { + t.Fatalf("Authenticate(\"\", token): unexpected error: %v", r.err) + } + case <-time.After(time.Second): + t.Fatal("Authenticate deadlocked — mutex was not released before calling loadAndVerifyToken") + } + + // Authenticate("api", token) — the named-token-username form + ch2 := make(chan result, 1) + go func() { + p, err := a.Authenticate("api", secret) + ch2 <- result{p, err} + }() + select { + case r := <-ch2: + if r.err != nil { + t.Fatalf("Authenticate(\"api\", token): unexpected error: %v", r.err) + } + case <-time.After(time.Second): + t.Fatal("Authenticate deadlocked — mutex was not released before calling loadAndVerifyToken") + } +} + func TestCredentialCreationAndVerification(t *testing.T) { type testCase struct { purpose string