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.community.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}