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}