A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
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) {