A community based topic aggregation platform built on atproto
1package tests
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 lexicon "github.com/bluesky-social/indigo/atproto/lexicon"
10)
11
12func TestLexiconSchemaValidation(t *testing.T) {
13 // Create a new catalog
14 catalog := lexicon.NewBaseCatalog()
15
16 // Load all schemas from the lexicon directory
17 schemaPath := "../internal/atproto/lexicon"
18 if err := catalog.LoadDirectory(schemaPath); err != nil {
19 t.Fatalf("Failed to load lexicon schemas: %v", err)
20 }
21
22 // Walk through the directory and find all lexicon files
23 var lexiconFiles []string
24 err := filepath.Walk(schemaPath, func(path string, info os.FileInfo, err error) error {
25 if err != nil {
26 return err
27 }
28 if strings.HasSuffix(path, ".json") && !info.IsDir() {
29 lexiconFiles = append(lexiconFiles, path)
30 }
31 return nil
32 })
33 if err != nil {
34 t.Fatalf("Failed to walk directory: %v", err)
35 }
36
37 t.Logf("Found %d lexicon files to validate", len(lexiconFiles))
38
39 // Extract schema IDs from file paths and test resolution
40 for _, filePath := range lexiconFiles {
41 // Convert file path to schema ID
42 // e.g., ../internal/atproto/lexicon/social/coves/actor/profile.json -> social.coves.actor.profile
43 relPath, err := filepath.Rel(schemaPath, filePath)
44 if err != nil {
45 t.Fatalf("Failed to get relative path for %s: %v", filePath, err)
46 }
47 relPath = strings.TrimSuffix(relPath, ".json")
48 schemaID := strings.ReplaceAll(relPath, string(filepath.Separator), ".")
49
50 t.Run(schemaID, func(t *testing.T) {
51 if _, resolveErr := catalog.Resolve(schemaID); resolveErr != nil {
52 t.Errorf("Failed to resolve schema %s: %v", schemaID, resolveErr)
53 }
54 })
55 }
56}
57
58func TestLexiconCrossReferences(t *testing.T) {
59 // Create a new catalog
60 catalog := lexicon.NewBaseCatalog()
61
62 // Load all schemas
63 if err := catalog.LoadDirectory("../internal/atproto/lexicon"); err != nil {
64 t.Fatalf("Failed to load lexicon schemas: %v", err)
65 }
66
67 // Test specific cross-references that should work
68 crossRefs := map[string]string{
69 "social.coves.richtext.facet#byteSlice": "byteSlice definition in facet schema",
70 "social.coves.actor.profile#geoLocation": "geoLocation definition in actor profile",
71 "social.coves.community.rules#rule": "rule definition in community rules",
72 }
73
74 for ref, description := range crossRefs {
75 t.Run(ref, func(t *testing.T) {
76 if _, err := catalog.Resolve(ref); err != nil {
77 t.Errorf("Failed to resolve cross-reference %s (%s): %v", ref, description, err)
78 }
79 })
80 }
81}
82
83func TestValidateRecord(t *testing.T) {
84 // Create a new catalog
85 catalog := lexicon.NewBaseCatalog()
86
87 // Load all schemas
88 if err := catalog.LoadDirectory("../internal/atproto/lexicon"); err != nil {
89 t.Fatalf("Failed to load lexicon schemas: %v", err)
90 }
91
92 // Test cases for ValidateRecord
93 tests := []struct {
94 recordData map[string]interface{}
95 name string
96 recordType string
97 errorContains string
98 shouldFail bool
99 }{
100 {
101 name: "Valid actor profile",
102 recordType: "social.coves.actor.profile",
103 recordData: map[string]interface{}{
104 "$type": "social.coves.actor.profile",
105 "handle": "alice.example.com",
106 "displayName": "Alice Johnson",
107 "createdAt": "2024-01-15T10:30:00Z",
108 },
109 shouldFail: false,
110 },
111 {
112 name: "Invalid actor profile - missing required field",
113 recordType: "social.coves.actor.profile",
114 recordData: map[string]interface{}{
115 "$type": "social.coves.actor.profile",
116 "displayName": "Alice Johnson",
117 },
118 shouldFail: true,
119 errorContains: "required field missing: handle",
120 },
121 {
122 name: "Valid community profile",
123 recordType: "social.coves.community.profile",
124 recordData: map[string]interface{}{
125 "$type": "social.coves.community.profile",
126 "handle": "programming.communities.coves.social",
127 "name": "programming",
128 "displayName": "Programming Community",
129 "createdBy": "did:plc:creator123",
130 "hostedBy": "did:plc:coves123",
131 "visibility": "public",
132 "moderationType": "moderator",
133 "federatedFrom": "coves",
134 "createdAt": "2023-12-01T08:00:00Z",
135 },
136 shouldFail: false,
137 },
138 {
139 name: "Valid post record",
140 recordType: "social.coves.post.record",
141 recordData: map[string]interface{}{
142 "$type": "social.coves.post.record",
143 "community": "did:plc:programming123",
144 "postType": "text",
145 "title": "Test Post",
146 "content": "This is a test post",
147 "createdAt": "2025-01-09T14:30:00Z",
148 },
149 shouldFail: false,
150 },
151 {
152 name: "Invalid post record - invalid enum value",
153 recordType: "social.coves.post.record",
154 recordData: map[string]interface{}{
155 "$type": "social.coves.post.record",
156 "community": "did:plc:programming123",
157 "postType": "invalid-type",
158 "title": "Test Post",
159 "content": "This is a test post",
160 "createdAt": "2025-01-09T14:30:00Z",
161 },
162 shouldFail: true,
163 errorContains: "string val not in required enum",
164 },
165 }
166
167 for _, tt := range tests {
168 t.Run(tt.name, func(t *testing.T) {
169 err := lexicon.ValidateRecord(&catalog, tt.recordData, tt.recordType, lexicon.AllowLenientDatetime)
170
171 if tt.shouldFail {
172 if err == nil {
173 t.Errorf("Expected validation to fail but it passed")
174 } else if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) {
175 t.Errorf("Expected error to contain '%s', got: %v", tt.errorContains, err)
176 }
177 } else {
178 if err != nil {
179 t.Errorf("Expected validation to pass but got error: %v", err)
180 }
181 }
182 })
183 }
184}
185
186func contains(s, substr string) bool {
187 return len(s) >= len(substr) && (s == substr || len(s) > 0 && strings.Contains(s, substr))
188}
189
190func TestValidateRecordWithStrictMode(t *testing.T) {
191 // Create a new catalog
192 catalog := lexicon.NewBaseCatalog()
193
194 // Load all schemas
195 if err := catalog.LoadDirectory("../internal/atproto/lexicon"); err != nil {
196 t.Fatalf("Failed to load lexicon schemas: %v", err)
197 }
198
199 // Test with strict validation flags
200 recordData := map[string]interface{}{
201 "$type": "social.coves.actor.profile",
202 "handle": "alice.example.com",
203 "displayName": "Alice Johnson",
204 "createdAt": "2024-01-15T10:30:00", // Missing timezone
205 }
206
207 // Should fail with strict validation
208 err := lexicon.ValidateRecord(&catalog, recordData, "social.coves.actor.profile", lexicon.StrictRecursiveValidation)
209 if err == nil {
210 t.Error("Expected strict validation to fail on datetime without timezone")
211 }
212
213 // Should pass with lenient datetime validation
214 err = lexicon.ValidateRecord(&catalog, recordData, "social.coves.actor.profile", lexicon.AllowLenientDatetime)
215 if err != nil {
216 t.Errorf("Expected lenient validation to pass, got error: %v", err)
217 }
218}