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