Caddy module to require at-proto authentication and restrict routes to DIDs
3

Configure Feed

Select the types of activity you want to include in your feed.

1package test 2 3import ( 4 "net/http" 5 "strings" 6 "testing" 7 "time" 8 9 "github.com/caddyserver/caddy/v2" 10 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 11 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 12 _ "github.com/caddyserver/caddy/v2/modules/standard" 13 14 _ "tangled.org/vvill.dev/caddy-atproto-auth" // Register modules 15) 16 17func TestCaddyIntegration(t *testing.T) { 18 // 1. Setup Caddyfile 19 input := ` 20 { 21 admin off 22 atproto { 23 storage_path :memory: 24 cookie_secret "my-secret-key-must-be-very-long-and-secure" 25 } 26 } 27 28 :8080 { 29 route /auth/* { 30 uri strip_prefix /auth 31 atproto_portal { 32 domain localhost:8080 33 name "Test Portal" 34 } 35 } 36 37 route /protected/* { 38 atproto_gate { 39 allow @test.bsky.social 40 portal_url http://localhost:8080/auth 41 } 42 respond "Authorized Content" 43 } 44 } 45 ` 46 47 // 2. Parse and Load Config 48 adapter := caddyfile.Adapter{ 49 ServerType: httpcaddyfile.ServerType{}, 50 } 51 52 jsonConfig, warnings, err := adapter.Adapt([]byte(input), nil) 53 if err != nil { 54 t.Fatalf("Failed to adapt config: %v", err) 55 } 56 if len(warnings) > 0 { 57 t.Logf("Warnings: %v", warnings) 58 } 59 60 err = caddy.Load(jsonConfig, true) 61 if err != nil { 62 t.Fatalf("Failed to load caddy: %v", err) 63 } 64 defer caddy.Stop() 65 66 // 3. Helper to simulate requests 67 // Since Caddy is running its own listeners, we can just make HTTP requests to it. 68 // But in a test environment, binding ports might be flaky. 69 // Ideally we'd invoke the handler directly, but getting the handler chain from Caddy is complex. 70 // We'll rely on the real HTTP server since we used :8080. 71 72 // Wait a moment for server start 73 time.Sleep(100 * time.Millisecond) 74 75 baseURL := "http://localhost:8080" 76 client := &http.Client{ 77 CheckRedirect: func(req *http.Request, via []*http.Request) error { 78 return http.ErrUseLastResponse // Don't follow redirects 79 }, 80 } 81 82 t.Run("Unauthorized Access Redirects to Portal", func(t *testing.T) { 83 resp, err := client.Get(baseURL + "/protected/resource") 84 if err != nil { 85 t.Fatalf("Request failed: %v", err) 86 } 87 defer resp.Body.Close() 88 89 if resp.StatusCode != http.StatusFound { 90 t.Errorf("Expected status 302, got %d", resp.StatusCode) 91 } 92 93 location := resp.Header.Get("Location") 94 if !strings.Contains(location, "/auth/login") { 95 t.Errorf("Expected redirect to portal login, got %s", location) 96 } 97 }) 98 99 t.Run("Portal Serves Login Page", func(t *testing.T) { 100 resp, err := client.Get(baseURL + "/auth/login") 101 if err != nil { 102 t.Fatalf("Request failed: %v", err) 103 } 104 defer resp.Body.Close() 105 106 if resp.StatusCode != http.StatusOK { 107 t.Errorf("Expected status 200, got %d", resp.StatusCode) 108 } 109 110 // Verify content type (HTML) 111 ct := resp.Header.Get("Content-Type") 112 if !strings.Contains(ct, "text/html") { 113 t.Errorf("Expected HTML content, got %s", ct) 114 } 115 }) 116 117 t.Run("Portal Serves Metadata", func(t *testing.T) { 118 resp, err := client.Get(baseURL + "/auth/.well-known/oauth-client-metadata.json") 119 if err != nil { 120 t.Fatalf("Request failed: %v", err) 121 } 122 defer resp.Body.Close() 123 124 if resp.StatusCode != http.StatusOK { 125 t.Errorf("Expected status 200, got %d", resp.StatusCode) 126 } 127 128 // Verify content type (JSON) 129 ct := resp.Header.Get("Content-Type") 130 if !strings.Contains(ct, "application/json") { 131 t.Errorf("Expected JSON content, got %s", ct) 132 } 133 }) 134}