Files
at-container-registry/pkg/atproto/endpoints_test.go

269 lines
7.7 KiB
Go

package atproto
import (
"strings"
"testing"
)
// TestEndpointsFormat validates that all endpoint constants follow the XRPC convention
func TestEndpointsFormat(t *testing.T) {
tests := []struct {
name string
endpoint string
prefix string // Expected namespace prefix (e.g., "io.atcr" or "com.atproto")
}{
// Hold service multipart upload endpoints
{"HoldInitiateUpload", HoldInitiateUpload, "io.atcr.hold"},
{"HoldGetPartUploadURL", HoldGetPartUploadURL, "io.atcr.hold"},
{"HoldUploadPart", HoldUploadPart, "io.atcr.hold"},
{"HoldCompleteUpload", HoldCompleteUpload, "io.atcr.hold"},
{"HoldAbortUpload", HoldAbortUpload, "io.atcr.hold"},
{"HoldNotifyManifest", HoldNotifyManifest, "io.atcr.hold"},
// Hold service crew management endpoints
{"HoldRequestCrew", HoldRequestCrew, "io.atcr.hold"},
// ATProto sync endpoints
{"SyncGetBlob", SyncGetBlob, "com.atproto.sync"},
{"SyncGetRepo", SyncGetRepo, "com.atproto.sync"},
{"SyncGetRecord", SyncGetRecord, "com.atproto.sync"},
{"SyncListBlobs", SyncListBlobs, "com.atproto.sync"},
{"SyncListRepos", SyncListRepos, "com.atproto.sync"},
{"SyncListReposByCollection", SyncListReposByCollection, "com.atproto.sync"},
{"SyncSubscribeRepos", SyncSubscribeRepos, "com.atproto.sync"},
{"SyncGetRepoStatus", SyncGetRepoStatus, "com.atproto.sync"},
{"SyncGetHostStatus", SyncGetHostStatus, "com.atproto.sync"},
{"SyncRequestCrawl", SyncRequestCrawl, "com.atproto.sync"},
// ATProto server endpoints
{"ServerGetServiceAuth", ServerGetServiceAuth, "com.atproto.server"},
{"ServerDescribeServer", ServerDescribeServer, "com.atproto.server"},
{"ServerCreateSession", ServerCreateSession, "com.atproto.server"},
{"ServerRefreshSession", ServerRefreshSession, "com.atproto.server"},
{"ServerGetSession", ServerGetSession, "com.atproto.server"},
// ATProto repo endpoints
{"RepoDescribeRepo", RepoDescribeRepo, "com.atproto.repo"},
{"RepoPutRecord", RepoPutRecord, "com.atproto.repo"},
{"RepoGetRecord", RepoGetRecord, "com.atproto.repo"},
{"RepoListRecords", RepoListRecords, "com.atproto.repo"},
{"RepoDeleteRecord", RepoDeleteRecord, "com.atproto.repo"},
{"RepoUploadBlob", RepoUploadBlob, "com.atproto.repo"},
// ATProto identity endpoints
{"IdentityResolveHandle", IdentityResolveHandle, "com.atproto.identity"},
// Bluesky app endpoints
{"ActorGetProfile", ActorGetProfile, "app.bsky.actor"},
{"ActorGetProfiles", ActorGetProfiles, "app.bsky.actor"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Check that endpoint starts with /xrpc/
if !strings.HasPrefix(tt.endpoint, "/xrpc/") {
t.Errorf("%s = %q, does not start with /xrpc/", tt.name, tt.endpoint)
}
// Check that endpoint contains the expected namespace prefix
if !strings.Contains(tt.endpoint, tt.prefix) {
t.Errorf("%s = %q, does not contain expected prefix %q", tt.name, tt.endpoint, tt.prefix)
}
// Check that endpoint is not empty
if tt.endpoint == "" {
t.Errorf("%s is empty", tt.name)
}
// Check that endpoint follows naming convention: /xrpc/{namespace}.{method}
// Should have at least 3 parts after /xrpc/: namespace.namespace.method
parts := strings.Split(strings.TrimPrefix(tt.endpoint, "/xrpc/"), ".")
if len(parts) < 3 {
t.Errorf("%s = %q, does not follow XRPC convention (expected at least 3 dot-separated parts)", tt.name, tt.endpoint)
}
// Check that method name (last part) is camelCase and not empty
method := parts[len(parts)-1]
if method == "" {
t.Errorf("%s = %q, has empty method name", tt.name, tt.endpoint)
}
if !isLowerCamelCase(method) {
t.Errorf("%s = %q, method %q is not in camelCase", tt.name, tt.endpoint, method)
}
})
}
}
// TestEndpointUniqueness ensures no duplicate endpoint paths
func TestEndpointUniqueness(t *testing.T) {
endpoints := []string{
HoldInitiateUpload,
HoldGetPartUploadURL,
HoldUploadPart,
HoldCompleteUpload,
HoldAbortUpload,
HoldNotifyManifest,
HoldRequestCrew,
SyncGetBlob,
SyncGetRepo,
SyncGetRecord,
SyncListBlobs,
SyncListRepos,
SyncListReposByCollection,
SyncSubscribeRepos,
SyncGetRepoStatus,
SyncGetHostStatus,
SyncRequestCrawl,
ServerGetServiceAuth,
ServerDescribeServer,
ServerCreateSession,
ServerRefreshSession,
ServerGetSession,
RepoDescribeRepo,
RepoPutRecord,
RepoGetRecord,
RepoListRecords,
RepoDeleteRecord,
RepoUploadBlob,
IdentityResolveHandle,
ActorGetProfile,
ActorGetProfiles,
}
seen := make(map[string]bool)
for _, endpoint := range endpoints {
if seen[endpoint] {
t.Errorf("Duplicate endpoint found: %q", endpoint)
}
seen[endpoint] = true
}
}
// TestEndpointNamespaces validates that endpoints are correctly grouped by namespace
func TestEndpointNamespaces(t *testing.T) {
tests := []struct {
name string
endpoints []string
namespace string
}{
{
name: "io.atcr.hold namespace",
endpoints: []string{
HoldInitiateUpload,
HoldGetPartUploadURL,
HoldUploadPart,
HoldCompleteUpload,
HoldAbortUpload,
HoldNotifyManifest,
HoldRequestCrew,
},
namespace: "io.atcr.hold",
},
{
name: "com.atproto.sync namespace",
endpoints: []string{
SyncGetBlob,
SyncGetRepo,
SyncGetRecord,
SyncListBlobs,
SyncListRepos,
SyncListReposByCollection,
SyncSubscribeRepos,
SyncGetRepoStatus,
SyncGetHostStatus,
SyncRequestCrawl,
},
namespace: "com.atproto.sync",
},
{
name: "com.atproto.server namespace",
endpoints: []string{
ServerGetServiceAuth,
ServerDescribeServer,
ServerCreateSession,
ServerRefreshSession,
ServerGetSession,
},
namespace: "com.atproto.server",
},
{
name: "com.atproto.repo namespace",
endpoints: []string{
RepoDescribeRepo,
RepoPutRecord,
RepoGetRecord,
RepoListRecords,
RepoDeleteRecord,
RepoUploadBlob,
},
namespace: "com.atproto.repo",
},
{
name: "com.atproto.identity namespace",
endpoints: []string{
IdentityResolveHandle,
},
namespace: "com.atproto.identity",
},
{
name: "app.bsky.actor namespace",
endpoints: []string{
ActorGetProfile,
ActorGetProfiles,
},
namespace: "app.bsky.actor",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, endpoint := range tt.endpoints {
if !strings.Contains(endpoint, tt.namespace) {
t.Errorf("Endpoint %q should be in namespace %q", endpoint, tt.namespace)
}
}
})
}
}
// TestSpecificEndpoints validates specific endpoint paths are correct
func TestSpecificEndpoints(t *testing.T) {
tests := []struct {
name string
got string
expected string
}{
// Spot check a few critical endpoints
{"HoldInitiateUpload", HoldInitiateUpload, "/xrpc/io.atcr.hold.initiateUpload"},
{"SyncGetBlob", SyncGetBlob, "/xrpc/com.atproto.sync.getBlob"},
{"ServerGetServiceAuth", ServerGetServiceAuth, "/xrpc/com.atproto.server.getServiceAuth"},
{"RepoPutRecord", RepoPutRecord, "/xrpc/com.atproto.repo.putRecord"},
{"IdentityResolveHandle", IdentityResolveHandle, "/xrpc/com.atproto.identity.resolveHandle"},
{"ActorGetProfile", ActorGetProfile, "/xrpc/app.bsky.actor.getProfile"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("%s = %q, expected %q", tt.name, tt.got, tt.expected)
}
})
}
}
// isLowerCamelCase checks if a string follows lowerCamelCase convention
func isLowerCamelCase(s string) bool {
if len(s) == 0 {
return false
}
// First character should be lowercase
if s[0] < 'a' || s[0] > 'z' {
return false
}
// Should not contain underscores or hyphens (common in other naming conventions)
if strings.Contains(s, "_") || strings.Contains(s, "-") {
return false
}
return true
}