A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
72
fork

Configure Feed

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

clean up duplicate functionality around converting hold did to url

+220 -416
+1 -25
pkg/appview/storage/manifest_store.go
··· 134 134 manifestRecord.ManifestBlob = blobRef 135 135 manifestRecord.HoldDID = s.ctx.HoldDID // Primary reference (DID) 136 136 137 - // Resolve hold endpoint from DID for backward compatibility 138 - if holdEndpoint, err := resolveDIDToHTTPSEndpoint(s.ctx.HoldDID); err == nil { 139 - manifestRecord.HoldEndpoint = holdEndpoint // Legacy reference (URL) for backward compat 140 - } 141 - 142 137 // Extract Dockerfile labels from config blob and add to annotations 143 138 // Only for image manifests (not manifest lists which don't have config blobs) 144 139 isManifestList := strings.Contains(manifestRecord.MediaType, "manifest.list") || ··· 283 278 return configJSON.Config.Labels, nil 284 279 } 285 280 286 - // resolveDIDToHTTPSEndpoint resolves a DID to an HTTPS endpoint 287 - // Currently supports did:web only (e.g., did:web:hold01.atcr.io → https://hold01.atcr.io) 288 - func resolveDIDToHTTPSEndpoint(did string) (string, error) { 289 - if !strings.HasPrefix(did, "did:web:") { 290 - return "", fmt.Errorf("only did:web is supported, got: %s", did) 291 - } 292 - 293 - // Extract hostname from did:web 294 - hostname := strings.TrimPrefix(did, "did:web:") 295 - 296 - // Handle port notation (did:web:example.com:8080 → https://example.com:8080) 297 - hostname = strings.ReplaceAll(hostname, ":", ":") 298 - 299 - return "https://" + hostname, nil 300 - } 301 - 302 281 // notifyHoldAboutManifest notifies the hold service about a manifest upload 303 282 // This enables the hold to create layer records and Bluesky posts 304 283 func (s *ManifestStore) notifyHoldAboutManifest(ctx context.Context, manifestRecord *atproto.ManifestRecord, tag, manifestDigest string) error { ··· 309 288 310 289 // Resolve hold DID to HTTP endpoint 311 290 // For did:web, this is straightforward (e.g., did:web:hold01.atcr.io → https://hold01.atcr.io) 312 - holdEndpoint, err := resolveDIDToHTTPSEndpoint(s.ctx.HoldDID) 313 - if err != nil { 314 - return fmt.Errorf("failed to resolve hold DID %s: %w", s.ctx.HoldDID, err) 315 - } 291 + holdEndpoint := atproto.ResolveHoldURL(s.ctx.HoldDID) 316 292 317 293 // Use service token from middleware (already cached and validated) 318 294 serviceToken := s.ctx.ServiceToken
-48
pkg/appview/storage/manifest_store_test.go
··· 912 912 }) 913 913 } 914 914 } 915 - 916 - // TestResolveDIDToHTTPSEndpoint tests DID to HTTPS URL conversion 917 - func TestResolveDIDToHTTPSEndpoint(t *testing.T) { 918 - tests := []struct { 919 - name string 920 - did string 921 - want string 922 - wantErr bool 923 - }{ 924 - { 925 - name: "did:web without port", 926 - did: "did:web:hold01.atcr.io", 927 - want: "https://hold01.atcr.io", 928 - wantErr: false, 929 - }, 930 - { 931 - name: "did:web with port", 932 - did: "did:web:localhost:8080", 933 - want: "https://localhost:8080", 934 - wantErr: false, 935 - }, 936 - { 937 - name: "did:plc not supported", 938 - did: "did:plc:abc123", 939 - want: "", 940 - wantErr: true, 941 - }, 942 - { 943 - name: "invalid did format", 944 - did: "not-a-did", 945 - want: "", 946 - wantErr: true, 947 - }, 948 - } 949 - 950 - for _, tt := range tests { 951 - t.Run(tt.name, func(t *testing.T) { 952 - got, err := resolveDIDToHTTPSEndpoint(tt.did) 953 - if (err != nil) != tt.wantErr { 954 - t.Errorf("resolveDIDToHTTPSEndpoint() error = %v, wantErr %v", err, tt.wantErr) 955 - return 956 - } 957 - if got != tt.want { 958 - t.Errorf("resolveDIDToHTTPSEndpoint() = %v, want %v", got, tt.want) 959 - } 960 - }) 961 - } 962 - }
+31
pkg/atproto/resolver.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "strings" 6 7 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 8 9 ) 10 + 11 + // ResolveHoldURL converts a hold identifier (DID or URL) to an HTTP/HTTPS URL 12 + // Handles both formats for backward compatibility: 13 + // - DID format: did:web:hold01.atcr.io → https://hold01.atcr.io 14 + // - DID with port: did:web:172.28.0.3:8080 → http://172.28.0.3:8080 15 + // - URL format: https://hold.example.com → https://hold.example.com (passthrough) 16 + func ResolveHoldURL(holdIdentifier string) string { 17 + // If it's already a URL (has scheme), return as-is 18 + if strings.HasPrefix(holdIdentifier, "http://") || strings.HasPrefix(holdIdentifier, "https://") { 19 + return holdIdentifier 20 + } 21 + 22 + // If it's a DID, convert to URL 23 + if after, ok := strings.CutPrefix(holdIdentifier, "did:web:"); ok { 24 + hostname := after 25 + 26 + // Use HTTP for localhost/IP addresses with ports, HTTPS for domains 27 + if strings.Contains(hostname, ":") || 28 + strings.Contains(hostname, "127.0.0.1") || 29 + strings.Contains(hostname, "localhost") || 30 + // Check if it's an IP address (contains only digits and dots in first part) 31 + (len(hostname) > 0 && hostname[0] >= '0' && hostname[0] <= '9') { 32 + return "http://" + hostname 33 + } 34 + return "https://" + hostname 35 + } 36 + 37 + // Fallback: assume it's a hostname and use HTTPS 38 + return "https://" + holdIdentifier 39 + } 9 40 10 41 // ResolveDIDToPDS resolves a DID to its PDS endpoint. 11 42 // Uses the shared identity directory with cache TTL and event-driven invalidation.
+186
pkg/atproto/resolver_test.go
··· 6 6 "testing" 7 7 ) 8 8 9 + func TestResolveHoldURL(t *testing.T) { 10 + tests := []struct { 11 + name string 12 + holdIdentifier string 13 + want string 14 + }{ 15 + // URL passthrough tests 16 + { 17 + name: "http URL passthrough", 18 + holdIdentifier: "http://hold.example.com", 19 + want: "http://hold.example.com", 20 + }, 21 + { 22 + name: "https URL passthrough", 23 + holdIdentifier: "https://hold.example.com", 24 + want: "https://hold.example.com", 25 + }, 26 + { 27 + name: "http URL with port passthrough", 28 + holdIdentifier: "http://hold.example.com:8080", 29 + want: "http://hold.example.com:8080", 30 + }, 31 + { 32 + name: "https URL with port passthrough", 33 + holdIdentifier: "https://hold.example.com:8443", 34 + want: "https://hold.example.com:8443", 35 + }, 36 + { 37 + name: "http URL with path passthrough", 38 + holdIdentifier: "http://hold.example.com/some/path", 39 + want: "http://hold.example.com/some/path", 40 + }, 41 + 42 + // did:web to HTTPS (domain names) 43 + { 44 + name: "did:web domain to https", 45 + holdIdentifier: "did:web:hold01.atcr.io", 46 + want: "https://hold01.atcr.io", 47 + }, 48 + { 49 + name: "did:web subdomain to https", 50 + holdIdentifier: "did:web:my-hold.example.com", 51 + want: "https://my-hold.example.com", 52 + }, 53 + { 54 + name: "did:web simple domain to https", 55 + holdIdentifier: "did:web:example.com", 56 + want: "https://example.com", 57 + }, 58 + 59 + // did:web to HTTP (ports) 60 + { 61 + name: "did:web with port to http", 62 + holdIdentifier: "did:web:172.28.0.3:8080", 63 + want: "http://172.28.0.3:8080", 64 + }, 65 + { 66 + name: "did:web domain with port to http", 67 + holdIdentifier: "did:web:hold.example.com:8080", 68 + want: "http://hold.example.com:8080", 69 + }, 70 + { 71 + name: "did:web localhost with port to http", 72 + holdIdentifier: "did:web:localhost:8080", 73 + want: "http://localhost:8080", 74 + }, 75 + 76 + // did:web to HTTP (localhost) 77 + { 78 + name: "did:web localhost to http", 79 + holdIdentifier: "did:web:localhost", 80 + want: "http://localhost", 81 + }, 82 + 83 + // did:web to HTTP (127.0.0.1) 84 + { 85 + name: "did:web 127.0.0.1 to http", 86 + holdIdentifier: "did:web:127.0.0.1", 87 + want: "http://127.0.0.1", 88 + }, 89 + { 90 + name: "did:web 127.0.0.1 with port to http", 91 + holdIdentifier: "did:web:127.0.0.1:8080", 92 + want: "http://127.0.0.1:8080", 93 + }, 94 + 95 + // did:web to HTTP (IP addresses) 96 + { 97 + name: "did:web IPv4 address to http", 98 + holdIdentifier: "did:web:192.168.1.1", 99 + want: "http://192.168.1.1", 100 + }, 101 + { 102 + name: "did:web IPv4 with port to http", 103 + holdIdentifier: "did:web:10.0.0.5:3000", 104 + want: "http://10.0.0.5:3000", 105 + }, 106 + { 107 + name: "did:web private IP to http", 108 + holdIdentifier: "did:web:172.16.0.1", 109 + want: "http://172.16.0.1", 110 + }, 111 + 112 + // Fallback behavior (plain hostname) 113 + { 114 + name: "plain hostname fallback to https", 115 + holdIdentifier: "hold.example.com", 116 + want: "https://hold.example.com", 117 + }, 118 + { 119 + name: "plain single word fallback to https", 120 + holdIdentifier: "myhold", 121 + want: "https://myhold", 122 + }, 123 + 124 + // Edge cases 125 + { 126 + name: "empty string fallback", 127 + holdIdentifier: "", 128 + want: "https://", 129 + }, 130 + { 131 + name: "did:web empty hostname", 132 + holdIdentifier: "did:web:", 133 + want: "https://", 134 + }, 135 + { 136 + name: "just did:web prefix", 137 + holdIdentifier: "did:web", 138 + want: "https://did:web", 139 + }, 140 + } 141 + 142 + for _, tt := range tests { 143 + t.Run(tt.name, func(t *testing.T) { 144 + got := ResolveHoldURL(tt.holdIdentifier) 145 + if got != tt.want { 146 + t.Errorf("ResolveHoldURL(%q) = %q, want %q", tt.holdIdentifier, got, tt.want) 147 + } 148 + }) 149 + } 150 + } 151 + 152 + // TestResolveHoldURLRoundTrip tests that converting back and forth works 153 + func TestResolveHoldURLRoundTrip(t *testing.T) { 154 + tests := []struct { 155 + name string 156 + input string 157 + wantHTTP bool // true if result should be http, false for https 158 + }{ 159 + {"domain to https and idempotent", "did:web:hold.atcr.io", false}, 160 + {"IP to http and idempotent", "did:web:192.168.1.1", true}, 161 + {"port to http and idempotent", "did:web:example.com:8080", true}, 162 + } 163 + 164 + for _, tt := range tests { 165 + t.Run(tt.name, func(t *testing.T) { 166 + // First conversion 167 + first := ResolveHoldURL(tt.input) 168 + 169 + // Second conversion (should be idempotent since output is URL) 170 + second := ResolveHoldURL(first) 171 + 172 + if first != second { 173 + t.Errorf("ResolveHoldURL is not idempotent: first=%q, second=%q", first, second) 174 + } 175 + 176 + // Verify correct protocol 177 + if tt.wantHTTP { 178 + if !hasPrefix(first, "http://") { 179 + t.Errorf("Expected http:// prefix, got %q", first) 180 + } 181 + } else { 182 + if !hasPrefix(first, "https://") { 183 + t.Errorf("Expected https:// prefix, got %q", first) 184 + } 185 + } 186 + }) 187 + } 188 + } 189 + 190 + // Helper function to check prefix 191 + func hasPrefix(s, prefix string) bool { 192 + return len(s) >= len(prefix) && s[:len(prefix)] == prefix 193 + } 194 + 9 195 // TestResolveIdentity tests resolving identifiers to DID, handle, and PDS endpoint 10 196 func TestResolveIdentity(t *testing.T) { 11 197 tests := []struct {
-33
pkg/atproto/utils.go
··· 1 - package atproto 2 - 3 - import "strings" 4 - 5 - // ResolveHoldURL converts a hold identifier (DID or URL) to an HTTP/HTTPS URL 6 - // Handles both formats for backward compatibility: 7 - // - DID format: did:web:hold01.atcr.io → https://hold01.atcr.io 8 - // - DID with port: did:web:172.28.0.3:8080 → http://172.28.0.3:8080 9 - // - URL format: https://hold.example.com → https://hold.example.com (passthrough) 10 - func ResolveHoldURL(holdIdentifier string) string { 11 - // If it's already a URL (has scheme), return as-is 12 - if strings.HasPrefix(holdIdentifier, "http://") || strings.HasPrefix(holdIdentifier, "https://") { 13 - return holdIdentifier 14 - } 15 - 16 - // If it's a DID, convert to URL 17 - if after, ok := strings.CutPrefix(holdIdentifier, "did:web:"); ok { 18 - hostname := after 19 - 20 - // Use HTTP for localhost/IP addresses with ports, HTTPS for domains 21 - if strings.Contains(hostname, ":") || 22 - strings.Contains(hostname, "127.0.0.1") || 23 - strings.Contains(hostname, "localhost") || 24 - // Check if it's an IP address (contains only digits and dots in first part) 25 - (len(hostname) > 0 && hostname[0] >= '0' && hostname[0] <= '9') { 26 - return "http://" + hostname 27 - } 28 - return "https://" + hostname 29 - } 30 - 31 - // Fallback: assume it's a hostname and use HTTPS 32 - return "https://" + holdIdentifier 33 - }
-189
pkg/atproto/utils_test.go
··· 1 - package atproto 2 - 3 - import "testing" 4 - 5 - func TestResolveHoldURL(t *testing.T) { 6 - tests := []struct { 7 - name string 8 - holdIdentifier string 9 - want string 10 - }{ 11 - // URL passthrough tests 12 - { 13 - name: "http URL passthrough", 14 - holdIdentifier: "http://hold.example.com", 15 - want: "http://hold.example.com", 16 - }, 17 - { 18 - name: "https URL passthrough", 19 - holdIdentifier: "https://hold.example.com", 20 - want: "https://hold.example.com", 21 - }, 22 - { 23 - name: "http URL with port passthrough", 24 - holdIdentifier: "http://hold.example.com:8080", 25 - want: "http://hold.example.com:8080", 26 - }, 27 - { 28 - name: "https URL with port passthrough", 29 - holdIdentifier: "https://hold.example.com:8443", 30 - want: "https://hold.example.com:8443", 31 - }, 32 - { 33 - name: "http URL with path passthrough", 34 - holdIdentifier: "http://hold.example.com/some/path", 35 - want: "http://hold.example.com/some/path", 36 - }, 37 - 38 - // did:web to HTTPS (domain names) 39 - { 40 - name: "did:web domain to https", 41 - holdIdentifier: "did:web:hold01.atcr.io", 42 - want: "https://hold01.atcr.io", 43 - }, 44 - { 45 - name: "did:web subdomain to https", 46 - holdIdentifier: "did:web:my-hold.example.com", 47 - want: "https://my-hold.example.com", 48 - }, 49 - { 50 - name: "did:web simple domain to https", 51 - holdIdentifier: "did:web:example.com", 52 - want: "https://example.com", 53 - }, 54 - 55 - // did:web to HTTP (ports) 56 - { 57 - name: "did:web with port to http", 58 - holdIdentifier: "did:web:172.28.0.3:8080", 59 - want: "http://172.28.0.3:8080", 60 - }, 61 - { 62 - name: "did:web domain with port to http", 63 - holdIdentifier: "did:web:hold.example.com:8080", 64 - want: "http://hold.example.com:8080", 65 - }, 66 - { 67 - name: "did:web localhost with port to http", 68 - holdIdentifier: "did:web:localhost:8080", 69 - want: "http://localhost:8080", 70 - }, 71 - 72 - // did:web to HTTP (localhost) 73 - { 74 - name: "did:web localhost to http", 75 - holdIdentifier: "did:web:localhost", 76 - want: "http://localhost", 77 - }, 78 - 79 - // did:web to HTTP (127.0.0.1) 80 - { 81 - name: "did:web 127.0.0.1 to http", 82 - holdIdentifier: "did:web:127.0.0.1", 83 - want: "http://127.0.0.1", 84 - }, 85 - { 86 - name: "did:web 127.0.0.1 with port to http", 87 - holdIdentifier: "did:web:127.0.0.1:8080", 88 - want: "http://127.0.0.1:8080", 89 - }, 90 - 91 - // did:web to HTTP (IP addresses) 92 - { 93 - name: "did:web IPv4 address to http", 94 - holdIdentifier: "did:web:192.168.1.1", 95 - want: "http://192.168.1.1", 96 - }, 97 - { 98 - name: "did:web IPv4 with port to http", 99 - holdIdentifier: "did:web:10.0.0.5:3000", 100 - want: "http://10.0.0.5:3000", 101 - }, 102 - { 103 - name: "did:web private IP to http", 104 - holdIdentifier: "did:web:172.16.0.1", 105 - want: "http://172.16.0.1", 106 - }, 107 - 108 - // Fallback behavior (plain hostname) 109 - { 110 - name: "plain hostname fallback to https", 111 - holdIdentifier: "hold.example.com", 112 - want: "https://hold.example.com", 113 - }, 114 - { 115 - name: "plain single word fallback to https", 116 - holdIdentifier: "myhold", 117 - want: "https://myhold", 118 - }, 119 - 120 - // Edge cases 121 - { 122 - name: "empty string fallback", 123 - holdIdentifier: "", 124 - want: "https://", 125 - }, 126 - { 127 - name: "did:web empty hostname", 128 - holdIdentifier: "did:web:", 129 - want: "https://", 130 - }, 131 - { 132 - name: "just did:web prefix", 133 - holdIdentifier: "did:web", 134 - want: "https://did:web", 135 - }, 136 - } 137 - 138 - for _, tt := range tests { 139 - t.Run(tt.name, func(t *testing.T) { 140 - got := ResolveHoldURL(tt.holdIdentifier) 141 - if got != tt.want { 142 - t.Errorf("ResolveHoldURL(%q) = %q, want %q", tt.holdIdentifier, got, tt.want) 143 - } 144 - }) 145 - } 146 - } 147 - 148 - // TestResolveHoldURLRoundTrip tests that converting back and forth works 149 - func TestResolveHoldURLRoundTrip(t *testing.T) { 150 - tests := []struct { 151 - name string 152 - input string 153 - wantHTTP bool // true if result should be http, false for https 154 - }{ 155 - {"domain to https and idempotent", "did:web:hold.atcr.io", false}, 156 - {"IP to http and idempotent", "did:web:192.168.1.1", true}, 157 - {"port to http and idempotent", "did:web:example.com:8080", true}, 158 - } 159 - 160 - for _, tt := range tests { 161 - t.Run(tt.name, func(t *testing.T) { 162 - // First conversion 163 - first := ResolveHoldURL(tt.input) 164 - 165 - // Second conversion (should be idempotent since output is URL) 166 - second := ResolveHoldURL(first) 167 - 168 - if first != second { 169 - t.Errorf("ResolveHoldURL is not idempotent: first=%q, second=%q", first, second) 170 - } 171 - 172 - // Verify correct protocol 173 - if tt.wantHTTP { 174 - if !hasPrefix(first, "http://") { 175 - t.Errorf("Expected http:// prefix, got %q", first) 176 - } 177 - } else { 178 - if !hasPrefix(first, "https://") { 179 - t.Errorf("Expected https:// prefix, got %q", first) 180 - } 181 - } 182 - }) 183 - } 184 - } 185 - 186 - // Helper function to check prefix 187 - func hasPrefix(s, prefix string) bool { 188 - return len(s) >= len(prefix) && s[:len(prefix)] == prefix 189 - }
+2 -36
pkg/auth/hold_remote.go
··· 9 9 "log/slog" 10 10 "net/http" 11 11 "net/url" 12 - "strings" 13 12 "sync" 14 13 "time" 15 14 ··· 219 218 // fetchCaptainRecordFromXRPC queries the hold's XRPC endpoint for captain record 220 219 func (a *RemoteHoldAuthorizer) fetchCaptainRecordFromXRPC(ctx context.Context, holdDID string) (*atproto.CaptainRecord, error) { 221 220 // Resolve DID to URL 222 - holdURL, err := a.resolveDIDToURL(holdDID) 223 - if err != nil { 224 - return nil, fmt.Errorf("failed to resolve hold DID: %w", err) 225 - } 221 + holdURL := atproto.ResolveHoldURL(holdDID) 226 222 227 223 // Build XRPC request URL 228 224 // GET /xrpc/com.atproto.repo.getRecord?repo={did}&collection=io.atcr.hold.captain&rkey=self ··· 326 322 // isCrewMemberNoCache queries XRPC without caching (internal helper) 327 323 func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID, userDID string) (bool, error) { 328 324 // Resolve DID to URL 329 - holdURL, err := a.resolveDIDToURL(holdDID) 330 - if err != nil { 331 - return false, fmt.Errorf("failed to resolve hold DID: %w", err) 332 - } 325 + holdURL := atproto.ResolveHoldURL(holdDID) 333 326 334 327 // Build XRPC request URL 335 328 // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew ··· 405 398 } 406 399 407 400 return CheckWriteAccessWithCaptain(captain, userDID, isCrew), nil 408 - } 409 - 410 - // resolveDIDToURL converts a did:web DID to an HTTP/HTTPS URL 411 - // Example: did:web:hold01.atcr.io → https://hold01.atcr.io 412 - // Example (test mode): did:web:172.28.0.3:8080 → http://172.28.0.3:8080 413 - func (a *RemoteHoldAuthorizer) resolveDIDToURL(did string) (string, error) { 414 - // Handle did:web format 415 - if !strings.HasPrefix(did, "did:web:") { 416 - return "", fmt.Errorf("only did:web is supported, got: %s", did) 417 - } 418 - 419 - // Extract hostname from did:web:hostname 420 - hostname := strings.TrimPrefix(did, "did:web:") 421 - 422 - // In test mode OR for local addresses, use HTTP instead of HTTPS 423 - // This matches the logic in pkg/appview/storage/proxy_blob_store.go:resolveHoldURL 424 - if a.testMode || 425 - strings.Contains(hostname, ":") || 426 - strings.Contains(hostname, "127.0.0.1") || 427 - strings.Contains(hostname, "localhost") || 428 - // Check if it's an IP address (contains only digits and dots) 429 - (len(hostname) > 0 && (hostname[0] >= '0' && hostname[0] <= '9')) { 430 - return "http://" + hostname, nil 431 - } 432 - 433 - // Convert to HTTPS URL for production domains 434 - return "https://" + hostname, nil 435 401 } 436 402 437 403 // nullString converts a string to sql.NullString
-85
pkg/auth/hold_remote_test.go
··· 52 52 return testDB 53 53 } 54 54 55 - func TestResolveDIDToURL_ProductionDomain(t *testing.T) { 56 - remote := &RemoteHoldAuthorizer{ 57 - testMode: false, 58 - } 59 - 60 - url, err := remote.resolveDIDToURL("did:web:hold01.atcr.io") 61 - if err != nil { 62 - t.Fatalf("resolveDIDToURL() error = %v", err) 63 - } 64 - 65 - expected := "https://hold01.atcr.io" 66 - if url != expected { 67 - t.Errorf("Expected URL %q, got %q", expected, url) 68 - } 69 - } 70 - 71 - func TestResolveDIDToURL_LocalhostHTTP(t *testing.T) { 72 - remote := &RemoteHoldAuthorizer{ 73 - testMode: false, 74 - } 75 - 76 - tests := []struct { 77 - name string 78 - did string 79 - expected string 80 - }{ 81 - { 82 - name: "localhost", 83 - did: "did:web:localhost:8080", 84 - expected: "http://localhost:8080", 85 - }, 86 - { 87 - name: "127.0.0.1", 88 - did: "did:web:127.0.0.1:8080", 89 - expected: "http://127.0.0.1:8080", 90 - }, 91 - { 92 - name: "IP address", 93 - did: "did:web:172.28.0.3:8080", 94 - expected: "http://172.28.0.3:8080", 95 - }, 96 - } 97 - 98 - for _, tt := range tests { 99 - t.Run(tt.name, func(t *testing.T) { 100 - url, err := remote.resolveDIDToURL(tt.did) 101 - if err != nil { 102 - t.Fatalf("resolveDIDToURL() error = %v", err) 103 - } 104 - 105 - if url != tt.expected { 106 - t.Errorf("Expected URL %q, got %q", tt.expected, url) 107 - } 108 - }) 109 - } 110 - } 111 - 112 - func TestResolveDIDToURL_TestMode(t *testing.T) { 113 - remote := &RemoteHoldAuthorizer{ 114 - testMode: true, 115 - } 116 - 117 - // In test mode, even production domains should use HTTP 118 - url, err := remote.resolveDIDToURL("did:web:hold01.atcr.io") 119 - if err != nil { 120 - t.Fatalf("resolveDIDToURL() error = %v", err) 121 - } 122 - 123 - expected := "http://hold01.atcr.io" 124 - if url != expected { 125 - t.Errorf("Expected HTTP URL in test mode, got %q", url) 126 - } 127 - } 128 - 129 - func TestResolveDIDToURL_InvalidDID(t *testing.T) { 130 - remote := &RemoteHoldAuthorizer{ 131 - testMode: false, 132 - } 133 - 134 - _, err := remote.resolveDIDToURL("did:plc:invalid") 135 - if err == nil { 136 - t.Error("Expected error for non-did:web DID") 137 - } 138 - } 139 - 140 55 func TestFetchCaptainRecordFromXRPC(t *testing.T) { 141 56 // Create mock HTTP server 142 57 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {