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}