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