Stage 7 · Master
Phase 4 — Authentication Service
Testing
Build auth-service's test suite around the security properties that actually matter — replay detection, lockout thresholds, alg rejection, and cross-org RBAC — rather than treating tests as a coverage checkbox after the fact.
A Passing Test Suite That Misses Every Real Risk Is Worse Than None
It would be easy to write tests that only exercise Hash-then-Verify-with-the-correct-password and call it done. Every defense built in this module exists specifically to stop an adversarial input, so the test suite's job is to encode exactly those adversarial cases as permanent regressions the next contributor cannot silently reintroduce.
package credential
import "testing"
func TestHashVerify_RoundTrip(t *testing.T) {
encoded, err := Hash("correct horse battery staple", DefaultCost)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
ok, err := Verify("correct horse battery staple", encoded)
if err != nil || !ok {
t.Fatalf("Verify() = %v, %v; want true, nil", ok, err)
}
}
func TestVerify_WrongPasswordRejected(t *testing.T) {
encoded, _ := Hash("correct horse battery staple", DefaultCost)
ok, err := Verify("wrong password entirely", encoded)
if err != nil {
t.Fatalf("Verify() unexpected error = %v", err)
}
if ok {
t.Fatal("Verify() = true for an incorrect password; want false")
}
}
func TestNeedsRehash_DetectsLowerCost(t *testing.T) {
encoded, _ := Hash("some password", 10)
if !NeedsRehash(encoded, DefaultCost) {
t.Fatal("NeedsRehash() = false for a hash created below the current cost")
}
}
func TestHash_RejectsPasswordBeyondBcryptLimit(t *testing.T) {
password := string(make([]byte, 73))
if _, err := Hash(password, DefaultCost); err != ErrPasswordTooLong {
t.Fatalf("Hash() error = %v; want ErrPasswordTooLong", err)
}
}Encoding the Replay Rule as an Unbreakable Regression
package token
import (
"sync"
"testing"
)
func TestRotate_ReplayRevokesFamily(t *testing.T) {
store := newInMemoryRefreshStore()
raw, _ := IssueFamily(t.Context(), store, testUserID, testOrgID)
rotatedOnce, rec, err := Rotate(t.Context(), store, raw)
if err != nil {
t.Fatalf("first Rotate() unexpected error = %v", err)
}
// Presenting the now-rotated original token simulates a stolen copy
// surfacing after the legitimate client already rotated.
_, _, err = Rotate(t.Context(), store, raw)
if err != ErrReplayDetected {
t.Fatalf("Rotate() on reused token error = %v; want ErrReplayDetected", err)
}
// The legitimate client's own valid, rotated token must ALSO now be
// rejected — proving the whole family was revoked, not just the reused one.
_, _, err = Rotate(t.Context(), store, rotatedOnce)
if err == nil {
t.Fatal("Rotate() on the legitimate rotated token succeeded after a replay; family should be fully revoked")
}
_ = rec
}
func TestRotate_ConcurrentUseHasOneWinner(t *testing.T) {
store := newInMemoryRefreshStore()
raw, _ := IssueFamily(t.Context(), store, testUserID, testOrgID)
start := make(chan struct{})
results := make(chan error, 2)
var wg sync.WaitGroup
for range 2 {
wg.Add(1)
go func() {
defer wg.Done()
<-start
_, _, err := Rotate(t.Context(), store, raw)
results <- err
}()
}
close(start)
wg.Wait()
close(results)
successes, replays := 0, 0
for err := range results {
switch err {
case nil:
successes++
case ErrReplayDetected:
replays++
default:
t.Fatalf("unexpected Rotate error: %v", err)
}
}
if successes != 1 || replays != 1 {
t.Fatalf("successes=%d replays=%d; want one atomic winner and one replay", successes, replays)
}
}
func TestIssueAndVerify_RoundTrip(t *testing.T) {
keys := testKeySet(t)
raw, err := Issue(keys, testUserID, testOrgID, "board_member", false)
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
claims, err := Verify(keys, raw)
if err != nil {
t.Fatalf("Verify() error = %v", err)
}
if claims.OrgID != testOrgID || claims.Role != "board_member" {
t.Fatalf("claims = %+v; want org_id=%s role=board_member", claims, testOrgID)
}
}
func TestVerify_RejectsNoneAlgorithm(t *testing.T) {
keys := testKeySet(t)
forged := "eyJhbGciOiJub25lIn0.eyJzdWIiOiJhdHRhY2tlciJ9."
if _, err := Verify(keys, forged); err == nil {
t.Fatal("Verify() accepted a forged alg=none token; alg allowlist should reject it")
}
}TestVerify_RejectsNoneAlgorithm is the regression test the alg-confusion discussion in the jwt lesson demanded explicitly — without it, a future refactor that accidentally drops jwt.WithValidMethods would pass every other test in this file while reopening a critical vulnerability.
Table-Driven Tests for the Two Middleware Layers
func TestMembershipDelete_RoleAndOrgMatrix(t *testing.T) {
cases := []struct {
name string
role string
tokenOrgID string
pathOrgID string
wantStatus int
}{
{"resident denied", "resident", orgA, orgA, http.StatusForbidden},
{"board_member same org allowed", "board_member", orgA, orgA, http.StatusNoContent},
{"board_member cross org denied", "board_member", orgA, orgB, http.StatusForbidden},
{"platform admin cross org allowed", "org_admin", orgA, orgB, http.StatusNoContent}, // via IsPlatformAdmin claim, not role
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// construct request with a token carrying tc.role/tc.tokenOrgID,
// call handler against tc.pathOrgID, assert tc.wantStatus
})
}
}The 'platform admin cross org allowed' case is deliberately named to make clear it passes via the IsPlatformAdmin claim specifically, not via any role — a reviewer scanning this table should never mistake it for a role-based cross-org allowance, which this platform never permits.
Running the Full Auth Suite Scoped to Its Own Module
Applied exercise
Prove a failed successor insert rolls the transaction back
The concurrent winner test now protects the compare-and-set rule. The remaining failure path is an insert error after the current row is locked but before replaced_by is updated.
- Inject a failure for the successor INSERT inside PostgresRefreshStore.RotateAtomic.
- Assert Rotate returns the injected error and the original row still has replaced_by IS NULL.
- Retry with the failure removed and assert the same original token can rotate exactly once.
Deliverable
An integration test proving rollback preserves the original token when the successor cannot be inserted.
Completion checks
- The assertion reads replaced_by from Postgres after the failed call.
- The retry succeeds once, showing the failed transaction consumed no token state.
Why does the test suite include TestVerify_RejectsNoneAlgorithm as an explicit, named test rather than relying on TestIssueAndVerify_RoundTrip alone?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.