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