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, _ := filepath.Rel(schemaPath, filePath)
44 relPath = strings.TrimSuffix(relPath, ".json")
45 schemaID := strings.ReplaceAll(relPath, string(filepath.Separator), ".")
46
47 t.Run(schemaID, func(t *testing.T) {
48 if _, err := catalog.Resolve(schemaID); err != nil {
49 t.Errorf("Failed to resolve schema %s: %v", schemaID, err)
50 }
51 })
52 }
53}
54
55func TestLexiconCrossReferences(t *testing.T) {
56 // Create a new catalog
57 catalog := lexicon.NewBaseCatalog()
58
59 // Load all schemas
60 if err := catalog.LoadDirectory("../internal/atproto/lexicon"); err != nil {
61 t.Fatalf("Failed to load lexicon schemas: %v", err)
62 }
63
64 // Test specific cross-references that should work
65 crossRefs := map[string]string{
66 "social.coves.richtext.facet#byteSlice": "byteSlice definition in facet schema",
67 "social.coves.actor.profile#geoLocation": "geoLocation definition in actor profile",
68 "social.coves.community.rules#rule": "rule definition in community rules",
69 }
70
71 for ref, description := range crossRefs {
72 t.Run(ref, func(t *testing.T) {
73 if _, err := catalog.Resolve(ref); err != nil {
74 t.Errorf("Failed to resolve cross-reference %s (%s): %v", ref, description, err)
75 }
76 })
77 }
78}
79
80func TestValidateRecord(t *testing.T) {
81 // Create a new catalog
82 catalog := lexicon.NewBaseCatalog()
83
84 // Load all schemas
85 if err := catalog.LoadDirectory("../internal/atproto/lexicon"); err != nil {
86 t.Fatalf("Failed to load lexicon schemas: %v", err)
87 }
88
89 // Test cases for ValidateRecord
90 tests := []struct {
91 name string
92 recordType string
93 recordData map[string]interface{}
94 shouldFail bool
95 errorContains string
96 }{
97 {
98 name: "Valid actor profile",
99 recordType: "social.coves.actor.profile",
100 recordData: map[string]interface{}{
101 "$type": "social.coves.actor.profile",
102 "handle": "alice.example.com",
103 "displayName": "Alice Johnson",
104 "createdAt": "2024-01-15T10:30:00Z",
105 },
106 shouldFail: false,
107 },
108 {
109 name: "Invalid actor profile - missing required field",
110 recordType: "social.coves.actor.profile",
111 recordData: map[string]interface{}{
112 "$type": "social.coves.actor.profile",
113 "displayName": "Alice Johnson",
114 },
115 shouldFail: true,
116 errorContains: "required field missing: handle",
117 },
118 {
119 name: "Valid community profile",
120 recordType: "social.coves.community.profile",
121 recordData: map[string]interface{}{
122 "$type": "social.coves.community.profile",
123 "name": "programming",
124 "displayName": "Programming Community",
125 "creator": "did:plc:creator123",
126 "moderationType": "moderator",
127 "federatedFrom": "coves",
128 "createdAt": "2023-12-01T08:00:00Z",
129 },
130 shouldFail: false,
131 },
132 {
133 name: "Valid post record",
134 recordType: "social.coves.post.record",
135 recordData: map[string]interface{}{
136 "$type": "social.coves.post.record",
137 "community": "did:plc:programming123",
138 "postType": "text",
139 "title": "Test Post",
140 "content": "This is a test post",
141 "createdAt": "2025-01-09T14:30:00Z",
142 },
143 shouldFail: false,
144 },
145 {
146 name: "Invalid post record - invalid enum value",
147 recordType: "social.coves.post.record",
148 recordData: map[string]interface{}{
149 "$type": "social.coves.post.record",
150 "community": "did:plc:programming123",
151 "postType": "invalid-type",
152 "title": "Test Post",
153 "content": "This is a test post",
154 "createdAt": "2025-01-09T14:30:00Z",
155 },
156 shouldFail: true,
157 errorContains: "string val not in required enum",
158 },
159 }
160
161 for _, tt := range tests {
162 t.Run(tt.name, func(t *testing.T) {
163 err := lexicon.ValidateRecord(&catalog, tt.recordData, tt.recordType, lexicon.AllowLenientDatetime)
164
165 if tt.shouldFail {
166 if err == nil {
167 t.Errorf("Expected validation to fail but it passed")
168 } else if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) {
169 t.Errorf("Expected error to contain '%s', got: %v", tt.errorContains, err)
170 }
171 } else {
172 if err != nil {
173 t.Errorf("Expected validation to pass but got error: %v", err)
174 }
175 }
176 })
177 }
178}
179
180func contains(s, substr string) bool {
181 return len(s) >= len(substr) && (s == substr || len(s) > 0 && strings.Contains(s, substr))
182}
183
184func TestValidateRecordWithStrictMode(t *testing.T) {
185 // Create a new catalog
186 catalog := lexicon.NewBaseCatalog()
187
188 // Load all schemas
189 if err := catalog.LoadDirectory("../internal/atproto/lexicon"); err != nil {
190 t.Fatalf("Failed to load lexicon schemas: %v", err)
191 }
192
193 // Test with strict validation flags
194 recordData := map[string]interface{}{
195 "$type": "social.coves.actor.profile",
196 "handle": "alice.example.com",
197 "displayName": "Alice Johnson",
198 "createdAt": "2024-01-15T10:30:00", // Missing timezone
199 }
200
201 // Should fail with strict validation
202 err := lexicon.ValidateRecord(&catalog, recordData, "social.coves.actor.profile", lexicon.StrictRecursiveValidation)
203 if err == nil {
204 t.Error("Expected strict validation to fail on datetime without timezone")
205 }
206
207 // Should pass with lenient datetime validation
208 err = lexicon.ValidateRecord(&catalog, recordData, "social.coves.actor.profile", lexicon.AllowLenientDatetime)
209 if err != nil {
210 t.Errorf("Expected lenient validation to pass, got error: %v", err)
211 }
212}