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)
}
}