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.

at codeberg-source 262 lines 7.7 kB view raw
1package atproto 2 3import ( 4 "strings" 5 "testing" 6) 7 8// TestEndpointsFormat validates that all endpoint constants follow the XRPC convention 9func TestEndpointsFormat(t *testing.T) { 10 tests := []struct { 11 name string 12 endpoint string 13 prefix string // Expected namespace prefix (e.g., "io.atcr" or "com.atproto") 14 }{ 15 // Hold service multipart upload endpoints 16 {"HoldInitiateUpload", HoldInitiateUpload, "io.atcr.hold"}, 17 {"HoldGetPartUploadURL", HoldGetPartUploadURL, "io.atcr.hold"}, 18 {"HoldUploadPart", HoldUploadPart, "io.atcr.hold"}, 19 {"HoldCompleteUpload", HoldCompleteUpload, "io.atcr.hold"}, 20 {"HoldAbortUpload", HoldAbortUpload, "io.atcr.hold"}, 21 {"HoldNotifyManifest", HoldNotifyManifest, "io.atcr.hold"}, 22 23 // Hold service crew management endpoints 24 {"HoldRequestCrew", HoldRequestCrew, "io.atcr.hold"}, 25 26 // ATProto sync endpoints 27 {"SyncGetBlob", SyncGetBlob, "com.atproto.sync"}, 28 {"SyncGetRepo", SyncGetRepo, "com.atproto.sync"}, 29 {"SyncGetRecord", SyncGetRecord, "com.atproto.sync"}, 30 {"SyncListRepos", SyncListRepos, "com.atproto.sync"}, 31 {"SyncListReposByCollection", SyncListReposByCollection, "com.atproto.sync"}, 32 {"SyncSubscribeRepos", SyncSubscribeRepos, "com.atproto.sync"}, 33 {"SyncGetRepoStatus", SyncGetRepoStatus, "com.atproto.sync"}, 34 {"SyncRequestCrawl", SyncRequestCrawl, "com.atproto.sync"}, 35 36 // ATProto server endpoints 37 {"ServerGetServiceAuth", ServerGetServiceAuth, "com.atproto.server"}, 38 {"ServerDescribeServer", ServerDescribeServer, "com.atproto.server"}, 39 {"ServerCreateSession", ServerCreateSession, "com.atproto.server"}, 40 {"ServerRefreshSession", ServerRefreshSession, "com.atproto.server"}, 41 {"ServerGetSession", ServerGetSession, "com.atproto.server"}, 42 43 // ATProto repo endpoints 44 {"RepoDescribeRepo", RepoDescribeRepo, "com.atproto.repo"}, 45 {"RepoPutRecord", RepoPutRecord, "com.atproto.repo"}, 46 {"RepoGetRecord", RepoGetRecord, "com.atproto.repo"}, 47 {"RepoListRecords", RepoListRecords, "com.atproto.repo"}, 48 {"RepoDeleteRecord", RepoDeleteRecord, "com.atproto.repo"}, 49 {"RepoUploadBlob", RepoUploadBlob, "com.atproto.repo"}, 50 51 // ATProto identity endpoints 52 {"IdentityResolveHandle", IdentityResolveHandle, "com.atproto.identity"}, 53 54 // Bluesky app endpoints 55 {"ActorGetProfile", ActorGetProfile, "app.bsky.actor"}, 56 {"ActorGetProfiles", ActorGetProfiles, "app.bsky.actor"}, 57 } 58 59 for _, tt := range tests { 60 t.Run(tt.name, func(t *testing.T) { 61 // Check that endpoint starts with /xrpc/ 62 if !strings.HasPrefix(tt.endpoint, "/xrpc/") { 63 t.Errorf("%s = %q, does not start with /xrpc/", tt.name, tt.endpoint) 64 } 65 66 // Check that endpoint contains the expected namespace prefix 67 if !strings.Contains(tt.endpoint, tt.prefix) { 68 t.Errorf("%s = %q, does not contain expected prefix %q", tt.name, tt.endpoint, tt.prefix) 69 } 70 71 // Check that endpoint is not empty 72 if tt.endpoint == "" { 73 t.Errorf("%s is empty", tt.name) 74 } 75 76 // Check that endpoint follows naming convention: /xrpc/{namespace}.{method} 77 // Should have at least 3 parts after /xrpc/: namespace.namespace.method 78 parts := strings.Split(strings.TrimPrefix(tt.endpoint, "/xrpc/"), ".") 79 if len(parts) < 3 { 80 t.Errorf("%s = %q, does not follow XRPC convention (expected at least 3 dot-separated parts)", tt.name, tt.endpoint) 81 } 82 83 // Check that method name (last part) is camelCase and not empty 84 method := parts[len(parts)-1] 85 if method == "" { 86 t.Errorf("%s = %q, has empty method name", tt.name, tt.endpoint) 87 } 88 if !isLowerCamelCase(method) { 89 t.Errorf("%s = %q, method %q is not in camelCase", tt.name, tt.endpoint, method) 90 } 91 }) 92 } 93} 94 95// TestEndpointUniqueness ensures no duplicate endpoint paths 96func TestEndpointUniqueness(t *testing.T) { 97 endpoints := []string{ 98 HoldInitiateUpload, 99 HoldGetPartUploadURL, 100 HoldUploadPart, 101 HoldCompleteUpload, 102 HoldAbortUpload, 103 HoldNotifyManifest, 104 HoldRequestCrew, 105 SyncGetBlob, 106 SyncGetRepo, 107 SyncGetRecord, 108 SyncListRepos, 109 SyncListReposByCollection, 110 SyncSubscribeRepos, 111 SyncGetRepoStatus, 112 SyncRequestCrawl, 113 ServerGetServiceAuth, 114 ServerDescribeServer, 115 ServerCreateSession, 116 ServerRefreshSession, 117 ServerGetSession, 118 RepoDescribeRepo, 119 RepoPutRecord, 120 RepoGetRecord, 121 RepoListRecords, 122 RepoDeleteRecord, 123 RepoUploadBlob, 124 IdentityResolveHandle, 125 ActorGetProfile, 126 ActorGetProfiles, 127 } 128 129 seen := make(map[string]bool) 130 for _, endpoint := range endpoints { 131 if seen[endpoint] { 132 t.Errorf("Duplicate endpoint found: %q", endpoint) 133 } 134 seen[endpoint] = true 135 } 136} 137 138// TestEndpointNamespaces validates that endpoints are correctly grouped by namespace 139func TestEndpointNamespaces(t *testing.T) { 140 tests := []struct { 141 name string 142 endpoints []string 143 namespace string 144 }{ 145 { 146 name: "io.atcr.hold namespace", 147 endpoints: []string{ 148 HoldInitiateUpload, 149 HoldGetPartUploadURL, 150 HoldUploadPart, 151 HoldCompleteUpload, 152 HoldAbortUpload, 153 HoldNotifyManifest, 154 HoldRequestCrew, 155 }, 156 namespace: "io.atcr.hold", 157 }, 158 { 159 name: "com.atproto.sync namespace", 160 endpoints: []string{ 161 SyncGetBlob, 162 SyncGetRepo, 163 SyncGetRecord, 164 SyncListRepos, 165 SyncListReposByCollection, 166 SyncSubscribeRepos, 167 SyncGetRepoStatus, 168 SyncRequestCrawl, 169 }, 170 namespace: "com.atproto.sync", 171 }, 172 { 173 name: "com.atproto.server namespace", 174 endpoints: []string{ 175 ServerGetServiceAuth, 176 ServerDescribeServer, 177 ServerCreateSession, 178 ServerRefreshSession, 179 ServerGetSession, 180 }, 181 namespace: "com.atproto.server", 182 }, 183 { 184 name: "com.atproto.repo namespace", 185 endpoints: []string{ 186 RepoDescribeRepo, 187 RepoPutRecord, 188 RepoGetRecord, 189 RepoListRecords, 190 RepoDeleteRecord, 191 RepoUploadBlob, 192 }, 193 namespace: "com.atproto.repo", 194 }, 195 { 196 name: "com.atproto.identity namespace", 197 endpoints: []string{ 198 IdentityResolveHandle, 199 }, 200 namespace: "com.atproto.identity", 201 }, 202 { 203 name: "app.bsky.actor namespace", 204 endpoints: []string{ 205 ActorGetProfile, 206 ActorGetProfiles, 207 }, 208 namespace: "app.bsky.actor", 209 }, 210 } 211 212 for _, tt := range tests { 213 t.Run(tt.name, func(t *testing.T) { 214 for _, endpoint := range tt.endpoints { 215 if !strings.Contains(endpoint, tt.namespace) { 216 t.Errorf("Endpoint %q should be in namespace %q", endpoint, tt.namespace) 217 } 218 } 219 }) 220 } 221} 222 223// TestSpecificEndpoints validates specific endpoint paths are correct 224func TestSpecificEndpoints(t *testing.T) { 225 tests := []struct { 226 name string 227 got string 228 expected string 229 }{ 230 // Spot check a few critical endpoints 231 {"HoldInitiateUpload", HoldInitiateUpload, "/xrpc/io.atcr.hold.initiateUpload"}, 232 {"SyncGetBlob", SyncGetBlob, "/xrpc/com.atproto.sync.getBlob"}, 233 {"ServerGetServiceAuth", ServerGetServiceAuth, "/xrpc/com.atproto.server.getServiceAuth"}, 234 {"RepoPutRecord", RepoPutRecord, "/xrpc/com.atproto.repo.putRecord"}, 235 {"IdentityResolveHandle", IdentityResolveHandle, "/xrpc/com.atproto.identity.resolveHandle"}, 236 {"ActorGetProfile", ActorGetProfile, "/xrpc/app.bsky.actor.getProfile"}, 237 } 238 239 for _, tt := range tests { 240 t.Run(tt.name, func(t *testing.T) { 241 if tt.got != tt.expected { 242 t.Errorf("%s = %q, expected %q", tt.name, tt.got, tt.expected) 243 } 244 }) 245 } 246} 247 248// isLowerCamelCase checks if a string follows lowerCamelCase convention 249func isLowerCamelCase(s string) bool { 250 if len(s) == 0 { 251 return false 252 } 253 // First character should be lowercase 254 if s[0] < 'a' || s[0] > 'z' { 255 return false 256 } 257 // Should not contain underscores or hyphens (common in other naming conventions) 258 if strings.Contains(s, "_") || strings.Contains(s, "-") { 259 return false 260 } 261 return true 262}