package auth_test import ( "context" "encoding/json" "fmt" "io" "net" "net/http" "testing" "time" "sourcecraft.dev/bigbes/lethe/internal/config" "sourcecraft.dev/bigbes/lethe/internal/platform/observability" "sourcecraft.dev/bigbes/lethe/internal/server/auth" ) // freeLoopbackPort picks an available TCP port on 127.0.0.1 by briefly // binding and then releasing it. Port-race is acceptable at test scale. func freeLoopbackPort(t *testing.T) int { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("freeLoopbackPort: Listen: %v", err) } port := ln.Addr().(*net.TCPAddr).Port ln.Close() return port } // newTestLogger builds an initialised observability.Logger for use in tests. func newTestLogger(t *testing.T) *observability.Logger { t.Helper() logger := &observability.Logger{Cfg: config.LoggingConfig{Level: "info", Format: "json"}} if err := logger.Init(context.Background()); err != nil { t.Fatalf("logger.Init: %v", err) } return logger } // TestOIDCDevStub_InitStartsListener_TokenVerifies verifies that: // 1. OIDCDevStub.Init starts an HTTP listener on the configured bind address. // 2. The listener satisfies OIDC discovery so OIDCVerifier.Init succeeds. // 3. GET /dev/token?sub=alice returns a JWT that OIDCVerifier.Verify accepts // and resolves to username "alice". func TestOIDCDevStub_InitStartsListener_TokenVerifies(t *testing.T) { port := freeLoopbackPort(t) bind := fmt.Sprintf("127.0.0.1:%d", port) issuer := "http://" + bind cfg := config.AuthConfig{ AllowedUsers: []string{"alice"}, OIDC: config.OIDCConfig{ Enabled: true, Issuer: issuer, Audience: "lethe", UsernameClaim: "preferred_username", DevStub: config.OIDCDevStubConfig{ Enabled: true, Bind: bind, TokenTTL: time.Hour, }, }, } stub := &auth.OIDCDevStub{ Cfg: cfg, Log: newTestLogger(t), } ctx := context.Background() if err := stub.Init(ctx); err != nil { t.Fatalf("OIDCDevStub.Init: %v", err) } t.Cleanup(func() { shutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := stub.Destroy(shutCtx); err != nil { t.Logf("OIDCDevStub.Destroy: %v", err) } }) // Prove the listener answers OIDC discovery by constructing a real verifier. verifier := &auth.OIDCVerifier{ Cfg: config.AuthConfig{ OIDC: config.OIDCConfig{ Enabled: true, Issuer: issuer, Audience: "lethe", UsernameClaim: "preferred_username", }, }, } if err := verifier.Init(ctx); err != nil { t.Fatalf("OIDCVerifier.Init: %v", err) } // Fetch a token from the /dev/token endpoint. resp, err := http.Get(fmt.Sprintf("http://%s/dev/token?sub=alice", bind)) if err != nil { t.Fatalf("GET /dev/token: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Fatalf("/dev/token status = %d; body = %s", resp.StatusCode, body) } var tokenResp struct { Token string `json:"token"` } if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { t.Fatalf("decode token response: %v", err) } if tokenResp.Token == "" { t.Fatal("token is empty") } // Verify the token resolves to alice. user, err := verifier.Verify(ctx, tokenResp.Token) if err != nil { t.Fatalf("verifier.Verify: %v", err) } if user != "alice" { t.Errorf("Verify returned user = %q; want alice", user) } } // TestOIDCDevStub_DisabledIsNoop verifies that when DevStub.Enabled is false // Init returns nil without starting any listener. func TestOIDCDevStub_DisabledIsNoop(t *testing.T) { port := freeLoopbackPort(t) bind := fmt.Sprintf("127.0.0.1:%d", port) issuer := "http://" + bind cfg := config.AuthConfig{ AllowedUsers: []string{"alice"}, OIDC: config.OIDCConfig{ Enabled: true, Issuer: issuer, Audience: "lethe", UsernameClaim: "preferred_username", DevStub: config.OIDCDevStubConfig{ Enabled: false, Bind: bind, }, }, } stub := &auth.OIDCDevStub{ Cfg: cfg, Log: newTestLogger(t), } ctx := context.Background() if err := stub.Init(ctx); err != nil { t.Fatalf("OIDCDevStub.Init (disabled): %v", err) } // Give the goroutine scheduler a moment, then assert nothing is listening. time.Sleep(50 * time.Millisecond) conn, err := net.DialTimeout("tcp", bind, 100*time.Millisecond) if err == nil { conn.Close() t.Errorf("expected no listener on %s when DevStub.Enabled=false, but dialled successfully", bind) } // Destroy on a no-op stub must also be safe. shutCtx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := stub.Destroy(shutCtx); err != nil { t.Errorf("OIDCDevStub.Destroy (disabled): %v", err) } }