+661
Diff
round #1
+291
crates/tranquil-lexicon/src/test_schemas.rs
+291
crates/tranquil-lexicon/src/test_schemas.rs
···
1
+
use crate::registry::LexiconRegistry;
2
+
use crate::schema::LexiconDoc;
3
+
4
+
pub(crate) fn test_registry() -> LexiconRegistry {
5
+
let mut registry = LexiconRegistry::new();
6
+
all().into_iter().for_each(|doc| registry.register(doc));
7
+
registry
8
+
}
9
+
10
+
fn parse(json: serde_json::Value) -> LexiconDoc {
11
+
serde_json::from_value(json).expect("invalid test schema JSON")
12
+
}
13
+
14
+
fn all() -> Vec<LexiconDoc> {
15
+
[
16
+
basic_schema(),
17
+
profile_schema(),
18
+
with_ref_schema(),
19
+
strong_ref_schema(),
20
+
with_reply_schema(),
21
+
images_schema(),
22
+
external_schema(),
23
+
with_gate_schema(),
24
+
with_did_schema(),
25
+
nullable_schema(),
26
+
required_nullable_schema(),
27
+
]
28
+
.into()
29
+
}
30
+
31
+
fn basic_schema() -> LexiconDoc {
32
+
parse(serde_json::json!({
33
+
"lexicon": 1,
34
+
"id": "com.test.basic",
35
+
"defs": {
36
+
"main": {
37
+
"type": "record",
38
+
"record": {
39
+
"type": "object",
40
+
"required": ["text", "createdAt"],
41
+
"properties": {
42
+
"text": {"type": "string", "maxLength": 100, "maxGraphemes": 50},
43
+
"createdAt": {"type": "string", "format": "datetime"},
44
+
"count": {"type": "integer", "minimum": 0, "maximum": 100},
45
+
"active": {"type": "boolean"},
46
+
"tags": {
47
+
"type": "array", "maxLength": 3,
48
+
"items": {"type": "string", "maxLength": 50}
49
+
},
50
+
"langs": {
51
+
"type": "array", "maxLength": 2,
52
+
"items": {"type": "string", "format": "language"}
53
+
}
54
+
}
55
+
}
56
+
}
57
+
}
58
+
}))
59
+
}
60
+
61
+
fn profile_schema() -> LexiconDoc {
62
+
parse(serde_json::json!({
63
+
"lexicon": 1,
64
+
"id": "com.test.profile",
65
+
"defs": {
66
+
"main": {
67
+
"type": "record",
68
+
"record": {
69
+
"type": "object",
70
+
"properties": {
71
+
"displayName": {"type": "string", "maxGraphemes": 10, "maxLength": 100},
72
+
"description": {"type": "string", "maxGraphemes": 50, "maxLength": 500},
73
+
"avatar": {"type": "blob", "accept": ["image/png", "image/jpeg"], "maxSize": 1000000}
74
+
}
75
+
}
76
+
}
77
+
}
78
+
}))
79
+
}
80
+
81
+
fn with_ref_schema() -> LexiconDoc {
82
+
parse(serde_json::json!({
83
+
"lexicon": 1,
84
+
"id": "com.test.withref",
85
+
"defs": {
86
+
"main": {
87
+
"type": "record",
88
+
"record": {
89
+
"type": "object",
90
+
"required": ["subject", "createdAt"],
91
+
"properties": {
92
+
"subject": {"type": "ref", "ref": "com.test.strongref"},
93
+
"createdAt": {"type": "string", "format": "datetime"}
94
+
}
95
+
}
96
+
}
97
+
}
98
+
}))
99
+
}
100
+
101
+
fn strong_ref_schema() -> LexiconDoc {
102
+
parse(serde_json::json!({
103
+
"lexicon": 1,
104
+
"id": "com.test.strongref",
105
+
"defs": {
106
+
"main": {
107
+
"type": "object",
108
+
"required": ["uri", "cid"],
109
+
"properties": {
110
+
"uri": {"type": "string", "format": "at-uri"},
111
+
"cid": {"type": "string", "format": "cid"}
112
+
}
113
+
}
114
+
}
115
+
}))
116
+
}
117
+
118
+
fn with_reply_schema() -> LexiconDoc {
119
+
parse(serde_json::json!({
120
+
"lexicon": 1,
121
+
"id": "com.test.withreply",
122
+
"defs": {
123
+
"main": {
124
+
"type": "record",
125
+
"record": {
126
+
"type": "object",
127
+
"required": ["text", "createdAt"],
128
+
"properties": {
129
+
"text": {"type": "string"},
130
+
"createdAt": {"type": "string", "format": "datetime"},
131
+
"reply": {"type": "ref", "ref": "#replyRef"},
132
+
"embed": {
133
+
"type": "union",
134
+
"refs": ["com.test.images", "com.test.external"]
135
+
}
136
+
}
137
+
}
138
+
},
139
+
"replyRef": {
140
+
"type": "object",
141
+
"required": ["root", "parent"],
142
+
"properties": {
143
+
"root": {"type": "ref", "ref": "com.test.strongref"},
144
+
"parent": {"type": "ref", "ref": "com.test.strongref"}
145
+
}
146
+
}
147
+
}
148
+
}))
149
+
}
150
+
151
+
fn images_schema() -> LexiconDoc {
152
+
parse(serde_json::json!({
153
+
"lexicon": 1,
154
+
"id": "com.test.images",
155
+
"defs": {
156
+
"main": {
157
+
"type": "object",
158
+
"required": ["images"],
159
+
"properties": {
160
+
"images": {
161
+
"type": "array", "maxLength": 4,
162
+
"items": {"type": "ref", "ref": "#image"}
163
+
}
164
+
}
165
+
},
166
+
"image": {
167
+
"type": "object",
168
+
"required": ["image", "alt"],
169
+
"properties": {
170
+
"image": {"type": "blob", "accept": ["image/*"], "maxSize": 1000000},
171
+
"alt": {"type": "string"}
172
+
}
173
+
}
174
+
}
175
+
}))
176
+
}
177
+
178
+
fn external_schema() -> LexiconDoc {
179
+
parse(serde_json::json!({
180
+
"lexicon": 1,
181
+
"id": "com.test.external",
182
+
"defs": {
183
+
"main": {
184
+
"type": "object",
185
+
"required": ["external"],
186
+
"properties": {
187
+
"external": {"type": "ref", "ref": "#external"}
188
+
}
189
+
},
190
+
"external": {
191
+
"type": "object",
192
+
"required": ["uri", "title", "description"],
193
+
"properties": {
194
+
"uri": {"type": "string", "format": "uri"},
195
+
"title": {"type": "string"},
196
+
"description": {"type": "string"}
197
+
}
198
+
}
199
+
}
200
+
}))
201
+
}
202
+
203
+
fn with_gate_schema() -> LexiconDoc {
204
+
parse(serde_json::json!({
205
+
"lexicon": 1,
206
+
"id": "com.test.withgate",
207
+
"defs": {
208
+
"main": {
209
+
"type": "record",
210
+
"record": {
211
+
"type": "object",
212
+
"required": ["post", "createdAt"],
213
+
"properties": {
214
+
"post": {"type": "string", "format": "at-uri"},
215
+
"createdAt": {"type": "string", "format": "datetime"},
216
+
"rules": {
217
+
"type": "array", "maxLength": 5,
218
+
"items": {"type": "union", "refs": ["#disableRule"]}
219
+
}
220
+
}
221
+
}
222
+
},
223
+
"disableRule": {
224
+
"type": "object",
225
+
"properties": {}
226
+
}
227
+
}
228
+
}))
229
+
}
230
+
231
+
fn with_did_schema() -> LexiconDoc {
232
+
parse(serde_json::json!({
233
+
"lexicon": 1,
234
+
"id": "com.test.withdid",
235
+
"defs": {
236
+
"main": {
237
+
"type": "record",
238
+
"record": {
239
+
"type": "object",
240
+
"required": ["subject", "createdAt"],
241
+
"properties": {
242
+
"subject": {"type": "string", "format": "did"},
243
+
"createdAt": {"type": "string", "format": "datetime"}
244
+
}
245
+
}
246
+
}
247
+
}
248
+
}))
249
+
}
250
+
251
+
fn nullable_schema() -> LexiconDoc {
252
+
parse(serde_json::json!({
253
+
"lexicon": 1,
254
+
"id": "com.test.nullable",
255
+
"defs": {
256
+
"main": {
257
+
"type": "record",
258
+
"record": {
259
+
"type": "object",
260
+
"required": ["name"],
261
+
"nullable": ["value"],
262
+
"properties": {
263
+
"name": {"type": "string"},
264
+
"value": {"type": "string"}
265
+
}
266
+
}
267
+
}
268
+
}
269
+
}))
270
+
}
271
+
272
+
fn required_nullable_schema() -> LexiconDoc {
273
+
parse(serde_json::json!({
274
+
"lexicon": 1,
275
+
"id": "com.test.requirednullable",
276
+
"defs": {
277
+
"main": {
278
+
"type": "record",
279
+
"record": {
280
+
"type": "object",
281
+
"required": ["name", "value"],
282
+
"nullable": ["value"],
283
+
"properties": {
284
+
"name": {"type": "string"},
285
+
"value": {"type": "string"}
286
+
}
287
+
}
288
+
}
289
+
}
290
+
}))
291
+
}
+370
crates/tranquil-lexicon/tests/resolve_integration.rs
+370
crates/tranquil-lexicon/tests/resolve_integration.rs
···
1
+
#![cfg(feature = "resolve")]
2
+
3
+
use std::time::Duration;
4
+
5
+
use serde_json::json;
6
+
use tranquil_lexicon::{
7
+
ResolveError, fetch_schema_from_pds, resolve_lexicon_from_did, resolve_pds_endpoint,
8
+
};
9
+
use wiremock::matchers::{method, path, query_param};
10
+
use wiremock::{Mock, MockServer, ResponseTemplate};
11
+
12
+
fn mock_did_document(did: &str, pds_endpoint: &str) -> serde_json::Value {
13
+
json!({
14
+
"@context": ["https://www.w3.org/ns/did/v1"],
15
+
"id": did,
16
+
"service": [{
17
+
"id": "#atproto_pds",
18
+
"type": "AtprotoPersonalDataServer",
19
+
"serviceEndpoint": pds_endpoint
20
+
}]
21
+
})
22
+
}
23
+
24
+
fn mock_lexicon_schema(nsid: &str) -> serde_json::Value {
25
+
json!({
26
+
"lexicon": 1,
27
+
"id": nsid,
28
+
"defs": {
29
+
"main": {
30
+
"type": "record",
31
+
"key": "tid",
32
+
"record": {
33
+
"type": "object",
34
+
"required": ["text", "createdAt"],
35
+
"properties": {
36
+
"text": {
37
+
"type": "string",
38
+
"maxLength": 1000,
39
+
"maxGraphemes": 100
40
+
},
41
+
"createdAt": {
42
+
"type": "string",
43
+
"format": "datetime"
44
+
}
45
+
}
46
+
}
47
+
}
48
+
}
49
+
})
50
+
}
51
+
52
+
fn mock_get_record_response(nsid: &str) -> serde_json::Value {
53
+
json!({
54
+
"uri": format!("at://did:plc:test123/com.atproto.lexicon.schema/{}", nsid),
55
+
"cid": "bafyreiabcdef",
56
+
"value": mock_lexicon_schema(nsid)
57
+
})
58
+
}
59
+
60
+
#[tokio::test]
61
+
async fn test_resolve_pds_endpoint_from_plc() {
62
+
let plc_server = MockServer::start().await;
63
+
let did = "did:plc:testabcdef123";
64
+
65
+
Mock::given(method("GET"))
66
+
.and(path(format!("/{}", did)))
67
+
.respond_with(
68
+
ResponseTemplate::new(200)
69
+
.set_body_json(mock_did_document(did, "https://pds.example.com")),
70
+
)
71
+
.mount(&plc_server)
72
+
.await;
73
+
74
+
let endpoint = resolve_pds_endpoint(did, Some(&plc_server.uri()))
75
+
.await
76
+
.unwrap();
77
+
assert_eq!(endpoint, "https://pds.example.com");
78
+
}
79
+
80
+
#[tokio::test]
81
+
async fn test_resolve_pds_endpoint_no_pds_service() {
82
+
let plc_server = MockServer::start().await;
83
+
let did = "did:plc:nopds123";
84
+
85
+
Mock::given(method("GET"))
86
+
.and(path(format!("/{}", did)))
87
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
88
+
"id": did,
89
+
"service": [{
90
+
"type": "AtprotoLabeler",
91
+
"serviceEndpoint": "https://labeler.example.com"
92
+
}]
93
+
})))
94
+
.mount(&plc_server)
95
+
.await;
96
+
97
+
let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await;
98
+
assert!(matches!(result, Err(ResolveError::NoPdsEndpoint { .. })));
99
+
}
100
+
101
+
#[tokio::test]
102
+
async fn test_resolve_pds_endpoint_plc_not_found() {
103
+
let plc_server = MockServer::start().await;
104
+
let did = "did:plc:missing123";
105
+
106
+
Mock::given(method("GET"))
107
+
.and(path(format!("/{}", did)))
108
+
.respond_with(ResponseTemplate::new(404).set_body_string("not found"))
109
+
.mount(&plc_server)
110
+
.await;
111
+
112
+
let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await;
113
+
assert!(result.is_err());
114
+
}
115
+
116
+
#[tokio::test]
117
+
async fn test_resolve_pds_endpoint_unsupported_did_method() {
118
+
let result = resolve_pds_endpoint("did:key:z6MkTest", None).await;
119
+
assert!(matches!(result, Err(ResolveError::DidResolution { .. })));
120
+
}
121
+
122
+
#[tokio::test]
123
+
async fn test_resolve_pds_endpoint_multiple_services_picks_pds() {
124
+
let plc_server = MockServer::start().await;
125
+
let did = "did:plc:multi123";
126
+
127
+
Mock::given(method("GET"))
128
+
.and(path(format!("/{}", did)))
129
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
130
+
"id": did,
131
+
"service": [
132
+
{
133
+
"type": "AtprotoLabeler",
134
+
"serviceEndpoint": "https://labeler.example.com"
135
+
},
136
+
{
137
+
"type": "BskyNotificationService",
138
+
"serviceEndpoint": "https://notify.example.com"
139
+
},
140
+
{
141
+
"type": "AtprotoPersonalDataServer",
142
+
"serviceEndpoint": "https://pds.example.com"
143
+
}
144
+
]
145
+
})))
146
+
.mount(&plc_server)
147
+
.await;
148
+
149
+
let endpoint = resolve_pds_endpoint(did, Some(&plc_server.uri()))
150
+
.await
151
+
.unwrap();
152
+
assert_eq!(endpoint, "https://pds.example.com");
153
+
}
154
+
155
+
#[tokio::test]
156
+
async fn test_fetch_schema_from_pds_success() {
157
+
let pds_server = MockServer::start().await;
158
+
let did = "did:plc:schemahost123";
159
+
let nsid = "com.example.custom.post";
160
+
161
+
Mock::given(method("GET"))
162
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
163
+
.and(query_param("repo", did))
164
+
.and(query_param("collection", "com.atproto.lexicon.schema"))
165
+
.and(query_param("rkey", nsid))
166
+
.respond_with(ResponseTemplate::new(200).set_body_json(mock_get_record_response(nsid)))
167
+
.mount(&pds_server)
168
+
.await;
169
+
170
+
let doc = fetch_schema_from_pds(&pds_server.uri(), did, nsid)
171
+
.await
172
+
.unwrap();
173
+
assert_eq!(doc.id, nsid);
174
+
assert_eq!(doc.lexicon, 1);
175
+
assert!(doc.defs.contains_key("main"));
176
+
}
177
+
178
+
#[tokio::test]
179
+
async fn test_fetch_schema_missing_value_field() {
180
+
let pds_server = MockServer::start().await;
181
+
let did = "did:plc:test123";
182
+
let nsid = "com.example.missing";
183
+
184
+
Mock::given(method("GET"))
185
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
186
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
187
+
"uri": "at://did:plc:test123/com.atproto.lexicon.schema/com.example.missing",
188
+
"cid": "bafyreiabcdef"
189
+
})))
190
+
.mount(&pds_server)
191
+
.await;
192
+
193
+
let result = fetch_schema_from_pds(&pds_server.uri(), did, nsid).await;
194
+
assert!(matches!(result, Err(ResolveError::SchemaFetch { .. })));
195
+
}
196
+
197
+
#[tokio::test]
198
+
async fn test_fetch_schema_invalid_lexicon_json() {
199
+
let pds_server = MockServer::start().await;
200
+
let did = "did:plc:test123";
201
+
let nsid = "com.example.bad";
202
+
203
+
Mock::given(method("GET"))
204
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
205
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
206
+
"uri": "at://test",
207
+
"cid": "bafyreiabcdef",
208
+
"value": {
209
+
"not_a_lexicon": true
210
+
}
211
+
})))
212
+
.mount(&pds_server)
213
+
.await;
214
+
215
+
let result = fetch_schema_from_pds(&pds_server.uri(), did, nsid).await;
216
+
assert!(matches!(result, Err(ResolveError::InvalidSchema(_))));
217
+
}
218
+
219
+
#[tokio::test]
220
+
async fn test_full_chain_plc_to_schema() {
221
+
let plc_server = MockServer::start().await;
222
+
let pds_server = MockServer::start().await;
223
+
let did = "did:plc:fullchain123";
224
+
let nsid = "com.example.social.post";
225
+
226
+
Mock::given(method("GET"))
227
+
.and(path(format!("/{}", did)))
228
+
.respond_with(
229
+
ResponseTemplate::new(200).set_body_json(mock_did_document(did, &pds_server.uri())),
230
+
)
231
+
.mount(&plc_server)
232
+
.await;
233
+
234
+
Mock::given(method("GET"))
235
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
236
+
.and(query_param("repo", did))
237
+
.and(query_param("collection", "com.atproto.lexicon.schema"))
238
+
.and(query_param("rkey", nsid))
239
+
.respond_with(ResponseTemplate::new(200).set_body_json(mock_get_record_response(nsid)))
240
+
.mount(&pds_server)
241
+
.await;
242
+
243
+
let doc = resolve_lexicon_from_did(nsid, did, Some(&plc_server.uri()))
244
+
.await
245
+
.unwrap();
246
+
assert_eq!(doc.id, nsid);
247
+
assert_eq!(doc.lexicon, 1);
248
+
}
249
+
250
+
#[tokio::test]
251
+
async fn test_full_chain_schema_id_mismatch_rejected() {
252
+
let plc_server = MockServer::start().await;
253
+
let pds_server = MockServer::start().await;
254
+
let did = "did:plc:mismatch123";
255
+
let nsid = "com.example.requested.type";
256
+
257
+
Mock::given(method("GET"))
258
+
.and(path(format!("/{}", did)))
259
+
.respond_with(
260
+
ResponseTemplate::new(200).set_body_json(mock_did_document(did, &pds_server.uri())),
261
+
)
262
+
.mount(&plc_server)
263
+
.await;
264
+
265
+
Mock::given(method("GET"))
266
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
267
+
.respond_with(
268
+
ResponseTemplate::new(200)
269
+
.set_body_json(mock_get_record_response("com.example.different.type")),
270
+
)
271
+
.mount(&pds_server)
272
+
.await;
273
+
274
+
let result = resolve_lexicon_from_did(nsid, did, Some(&plc_server.uri())).await;
275
+
assert!(matches!(result, Err(ResolveError::InvalidSchema(_))));
276
+
}
277
+
278
+
#[tokio::test]
279
+
async fn test_full_chain_bad_lexicon_version_rejected() {
280
+
let plc_server = MockServer::start().await;
281
+
let pds_server = MockServer::start().await;
282
+
let did = "did:plc:badver123";
283
+
let nsid = "com.example.versioned.type";
284
+
285
+
Mock::given(method("GET"))
286
+
.and(path(format!("/{}", did)))
287
+
.respond_with(
288
+
ResponseTemplate::new(200).set_body_json(mock_did_document(did, &pds_server.uri())),
289
+
)
290
+
.mount(&plc_server)
291
+
.await;
292
+
293
+
Mock::given(method("GET"))
294
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
295
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
296
+
"uri": "at://test",
297
+
"cid": "bafyreiabcdef",
298
+
"value": {
299
+
"lexicon": 2,
300
+
"id": nsid,
301
+
"defs": {}
302
+
}
303
+
})))
304
+
.mount(&pds_server)
305
+
.await;
306
+
307
+
let result = resolve_lexicon_from_did(nsid, did, Some(&plc_server.uri())).await;
308
+
assert!(matches!(result, Err(ResolveError::InvalidSchema(_))));
309
+
}
310
+
311
+
#[tokio::test]
312
+
async fn test_pds_trailing_slash_handled() {
313
+
let pds_server = MockServer::start().await;
314
+
let did = "did:plc:slash123";
315
+
let nsid = "com.example.slash.test";
316
+
317
+
Mock::given(method("GET"))
318
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
319
+
.and(query_param("repo", did))
320
+
.and(query_param("rkey", nsid))
321
+
.respond_with(ResponseTemplate::new(200).set_body_json(mock_get_record_response(nsid)))
322
+
.mount(&pds_server)
323
+
.await;
324
+
325
+
let pds_url_with_slash = format!("{}/", pds_server.uri());
326
+
let doc = fetch_schema_from_pds(&pds_url_with_slash, did, nsid)
327
+
.await
328
+
.unwrap();
329
+
assert_eq!(doc.id, nsid);
330
+
}
331
+
332
+
#[tokio::test]
333
+
async fn test_fetch_schema_error_status_gives_meaningful_error() {
334
+
let pds_server = MockServer::start().await;
335
+
let did = "did:plc:test123";
336
+
let nsid = "com.example.notfound";
337
+
338
+
Mock::given(method("GET"))
339
+
.and(path("/xrpc/com.atproto.repo.getRecord"))
340
+
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
341
+
"error": "RecordNotFound",
342
+
"message": "record not found"
343
+
})))
344
+
.mount(&pds_server)
345
+
.await;
346
+
347
+
let result = fetch_schema_from_pds(&pds_server.uri(), did, nsid).await;
348
+
let err = result.unwrap_err();
349
+
let err_msg = err.to_string();
350
+
assert!(
351
+
!err_msg.contains("missing 'value' field"),
352
+
"a 400 response should report the HTTP status, not a parse error. got: {}",
353
+
err_msg
354
+
);
355
+
}
356
+
357
+
#[tokio::test]
358
+
async fn test_plc_server_timeout() {
359
+
let plc_server = MockServer::start().await;
360
+
let did = "did:plc:timeout123";
361
+
362
+
Mock::given(method("GET"))
363
+
.and(path(format!("/{}", did)))
364
+
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(30)))
365
+
.mount(&plc_server)
366
+
.await;
367
+
368
+
let result = resolve_pds_endpoint(did, Some(&plc_server.uri())).await;
369
+
assert!(result.is_err());
370
+
}
History
2 rounds
0 comments
oyster.cafe
submitted
#1
1 commit
expand
collapse
test(lexicon): test schemas and resolution integration tests
expand 0 comments
pull request successfully merged
oyster.cafe
submitted
#0
1 commit
expand
collapse
test(lexicon): test schemas and resolution integration tests