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