A community based topic aggregation platform built on atproto
1package tests
2
3import (
4 "strings"
5 "testing"
6
7 lexicon "github.com/bluesky-social/indigo/atproto/lexicon"
8)
9
10func TestLexiconSchemaValidation(t *testing.T) {
11 // Create a new catalog
12 catalog := lexicon.NewBaseCatalog()
13
14 // Load all schemas from the lexicon directory
15 schemaPath := "../internal/atproto/lexicon"
16 if err := catalog.LoadDirectory(schemaPath); err != nil {
17 t.Fatalf("Failed to load lexicon schemas: %v", err)
18 }
19
20 // Test that we can resolve our key schemas
21 expectedSchemas := []string{
22 "social.coves.actor.profile",
23 "social.coves.actor.subscription",
24 "social.coves.actor.membership",
25 "social.coves.community.profile",
26 "social.coves.community.rules",
27 "social.coves.community.wiki",
28 "social.coves.post.text",
29 "social.coves.post.image",
30 "social.coves.post.video",
31 "social.coves.post.article",
32 "social.coves.richtext.facet",
33 "social.coves.embed.image",
34 "social.coves.embed.video",
35 "social.coves.embed.external",
36 "social.coves.embed.post",
37 "social.coves.interaction.vote",
38 "social.coves.interaction.tag",
39 "social.coves.interaction.comment",
40 "social.coves.interaction.share",
41 "social.coves.moderation.vote",
42 "social.coves.moderation.tribunalVote",
43 "social.coves.moderation.ruleProposal",
44 }
45
46 for _, schemaID := range expectedSchemas {
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 "text": "This is a test post",
141 "tags": []string{"test", "golang"},
142 "language": "en",
143 "contentWarnings": []string{},
144 "createdAt": "2025-01-09T14:30:00Z",
145 },
146 shouldFail: false,
147 },
148 {
149 name: "Invalid post record - invalid enum value",
150 recordType: "social.coves.post.record",
151 recordData: map[string]interface{}{
152 "$type": "social.coves.post.record",
153 "community": "did:plc:programming123",
154 "postType": "invalid-type",
155 "title": "Test Post",
156 "text": "This is a test post",
157 "tags": []string{"test"},
158 "language": "en",
159 "contentWarnings": []string{},
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}