+1
.env.example
+1
.env.example
+516
-2
Cargo.lock
+516
-2
Cargo.lock
···
91
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
92
93
[[package]]
94
name = "async-compression"
95
version = "0.4.34"
96
source = "registry+https://github.com/rust-lang/crates.io-index"
···
104
]
105
106
[[package]]
107
name = "async-trait"
108
version = "0.1.89"
109
source = "registry+https://github.com/rust-lang/crates.io-index"
···
217
218
[[package]]
219
name = "base64"
220
version = "0.22.1"
221
source = "registry+https://github.com/rust-lang/crates.io-index"
222
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
···
269
]
270
271
[[package]]
272
name = "bon"
273
version = "3.8.1"
274
source = "registry+https://github.com/rust-lang/crates.io-index"
···
345
"serde_json",
346
"sha2",
347
"sqlx",
348
"tokio",
349
"tracing",
350
"tracing-subscriber",
···
935
]
936
937
[[package]]
938
name = "dotenvy"
939
version = "0.15.7"
940
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1073
]
1074
1075
[[package]]
1076
name = "event-listener"
1077
version = "5.4.1"
1078
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1088
version = "2.3.0"
1089
source = "registry+https://github.com/rust-lang/crates.io-index"
1090
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1091
1092
[[package]]
1093
name = "ff"
···
1675
]
1676
1677
[[package]]
1678
name = "hyper-rustls"
1679
version = "0.27.7"
1680
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1692
]
1693
1694
[[package]]
1695
name = "hyper-tls"
1696
version = "0.6.0"
1697
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1734
]
1735
1736
[[package]]
1737
name = "iana-time-zone"
1738
version = "0.1.64"
1739
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1968
"thiserror 1.0.69",
1969
"tokio",
1970
"unsigned-varint 0.7.2",
1971
]
1972
1973
[[package]]
···
2662
"openssl-probe",
2663
"openssl-sys",
2664
"schannel",
2665
-
"security-framework",
2666
"security-framework-sys",
2667
"tempfile",
2668
]
···
2699
]
2700
2701
[[package]]
2702
name = "num-bigint"
2703
version = "0.4.6"
2704
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2725
]
2726
2727
[[package]]
2728
name = "num-conv"
2729
version = "0.1.0"
2730
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2746
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
2747
dependencies = [
2748
"autocfg",
2749
"num-integer",
2750
"num-traits",
2751
]
···
2932
]
2933
2934
[[package]]
2935
name = "pem"
2936
version = "3.0.6"
2937
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3054
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
3055
3056
[[package]]
3057
name = "potential_utf"
3058
version = "0.1.4"
3059
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3146
"syn 2.0.111",
3147
"version_check",
3148
"yansi",
3149
]
3150
3151
[[package]]
···
3520
source = "registry+https://github.com/rust-lang/crates.io-index"
3521
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
3522
dependencies = [
3523
"once_cell",
3524
"ring",
3525
"rustls-pki-types",
···
3529
]
3530
3531
[[package]]
3532
name = "rustls-pki-types"
3533
version = "1.13.1"
3534
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3643
dependencies = [
3644
"bitflags",
3645
"core-foundation 0.9.4",
3646
"core-foundation-sys",
3647
"libc",
3648
"security-framework-sys",
···
3861
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
3862
3863
[[package]]
3864
name = "signature"
3865
version = "2.2.0"
3866
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4125
"chrono",
4126
"crc",
4127
"dotenvy",
4128
-
"etcetera",
4129
"futures-channel",
4130
"futures-core",
4131
"futures-util",
···
4252
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
4253
4254
[[package]]
4255
name = "subtle"
4256
version = "2.6.1"
4257
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4351
]
4352
4353
[[package]]
4354
name = "thiserror"
4355
version = "1.0.69"
4356
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4488
"libc",
4489
"mio",
4490
"pin-project-lite",
4491
"socket2 0.6.1",
4492
"tokio-macros",
4493
"windows-sys 0.61.2",
···
4550
]
4551
4552
[[package]]
4553
name = "tower"
4554
version = "0.5.2"
4555
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4557
dependencies = [
4558
"futures-core",
4559
"futures-util",
4560
"pin-project-lite",
4561
"sync_wrapper",
4562
"tokio",
4563
"tower-layer",
4564
"tower-service",
4565
"tracing",
···
4764
version = "0.9.0"
4765
source = "registry+https://github.com/rust-lang/crates.io-index"
4766
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
4767
4768
[[package]]
4769
name = "url"
···
5019
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
5020
5021
[[package]]
5022
name = "winapi-util"
5023
version = "0.1.11"
5024
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5026
dependencies = [
5027
"windows-sys 0.61.2",
5028
]
5029
5030
[[package]]
5031
name = "windows"
···
5495
version = "0.6.2"
5496
source = "registry+https://github.com/rust-lang/crates.io-index"
5497
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
5498
5499
[[package]]
5500
name = "xml5ever"
···
91
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
92
93
[[package]]
94
+
name = "astral-tokio-tar"
95
+
version = "0.5.6"
96
+
source = "registry+https://github.com/rust-lang/crates.io-index"
97
+
checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5"
98
+
dependencies = [
99
+
"filetime",
100
+
"futures-core",
101
+
"libc",
102
+
"portable-atomic",
103
+
"rustc-hash",
104
+
"tokio",
105
+
"tokio-stream",
106
+
"xattr",
107
+
]
108
+
109
+
[[package]]
110
name = "async-compression"
111
version = "0.4.34"
112
source = "registry+https://github.com/rust-lang/crates.io-index"
···
120
]
121
122
[[package]]
123
+
name = "async-stream"
124
+
version = "0.3.6"
125
+
source = "registry+https://github.com/rust-lang/crates.io-index"
126
+
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
127
+
dependencies = [
128
+
"async-stream-impl",
129
+
"futures-core",
130
+
"pin-project-lite",
131
+
]
132
+
133
+
[[package]]
134
+
name = "async-stream-impl"
135
+
version = "0.3.6"
136
+
source = "registry+https://github.com/rust-lang/crates.io-index"
137
+
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
138
+
dependencies = [
139
+
"proc-macro2",
140
+
"quote",
141
+
"syn 2.0.111",
142
+
]
143
+
144
+
[[package]]
145
name = "async-trait"
146
version = "0.1.89"
147
source = "registry+https://github.com/rust-lang/crates.io-index"
···
255
256
[[package]]
257
name = "base64"
258
+
version = "0.21.7"
259
+
source = "registry+https://github.com/rust-lang/crates.io-index"
260
+
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
261
+
262
+
[[package]]
263
+
name = "base64"
264
version = "0.22.1"
265
source = "registry+https://github.com/rust-lang/crates.io-index"
266
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
···
313
]
314
315
[[package]]
316
+
name = "bollard"
317
+
version = "0.19.4"
318
+
source = "registry+https://github.com/rust-lang/crates.io-index"
319
+
checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b"
320
+
dependencies = [
321
+
"async-stream",
322
+
"base64 0.22.1",
323
+
"bitflags",
324
+
"bollard-buildkit-proto",
325
+
"bollard-stubs",
326
+
"bytes",
327
+
"chrono",
328
+
"futures-core",
329
+
"futures-util",
330
+
"hex",
331
+
"home",
332
+
"http",
333
+
"http-body-util",
334
+
"hyper",
335
+
"hyper-named-pipe",
336
+
"hyper-rustls",
337
+
"hyper-util",
338
+
"hyperlocal",
339
+
"log",
340
+
"num",
341
+
"pin-project-lite",
342
+
"rand 0.9.2",
343
+
"rustls",
344
+
"rustls-native-certs",
345
+
"rustls-pemfile",
346
+
"rustls-pki-types",
347
+
"serde",
348
+
"serde_derive",
349
+
"serde_json",
350
+
"serde_repr",
351
+
"serde_urlencoded",
352
+
"thiserror 2.0.17",
353
+
"tokio",
354
+
"tokio-stream",
355
+
"tokio-util",
356
+
"tonic",
357
+
"tower-service",
358
+
"url",
359
+
"winapi",
360
+
]
361
+
362
+
[[package]]
363
+
name = "bollard-buildkit-proto"
364
+
version = "0.7.0"
365
+
source = "registry+https://github.com/rust-lang/crates.io-index"
366
+
checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad"
367
+
dependencies = [
368
+
"prost",
369
+
"prost-types",
370
+
"tonic",
371
+
"tonic-prost",
372
+
"ureq",
373
+
]
374
+
375
+
[[package]]
376
+
name = "bollard-stubs"
377
+
version = "1.49.1-rc.28.4.0"
378
+
source = "registry+https://github.com/rust-lang/crates.io-index"
379
+
checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623"
380
+
dependencies = [
381
+
"base64 0.22.1",
382
+
"bollard-buildkit-proto",
383
+
"bytes",
384
+
"chrono",
385
+
"prost",
386
+
"serde",
387
+
"serde_json",
388
+
"serde_repr",
389
+
"serde_with",
390
+
]
391
+
392
+
[[package]]
393
name = "bon"
394
version = "3.8.1"
395
source = "registry+https://github.com/rust-lang/crates.io-index"
···
466
"serde_json",
467
"sha2",
468
"sqlx",
469
+
"testcontainers",
470
+
"testcontainers-modules",
471
"tokio",
472
"tracing",
473
"tracing-subscriber",
···
1058
]
1059
1060
[[package]]
1061
+
name = "docker_credential"
1062
+
version = "1.3.2"
1063
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1064
+
checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8"
1065
+
dependencies = [
1066
+
"base64 0.21.7",
1067
+
"serde",
1068
+
"serde_json",
1069
+
]
1070
+
1071
+
[[package]]
1072
name = "dotenvy"
1073
version = "0.15.7"
1074
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1207
]
1208
1209
[[package]]
1210
+
name = "etcetera"
1211
+
version = "0.11.0"
1212
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1213
+
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
1214
+
dependencies = [
1215
+
"cfg-if",
1216
+
"windows-sys 0.61.2",
1217
+
]
1218
+
1219
+
[[package]]
1220
name = "event-listener"
1221
version = "5.4.1"
1222
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1232
version = "2.3.0"
1233
source = "registry+https://github.com/rust-lang/crates.io-index"
1234
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1235
+
1236
+
[[package]]
1237
+
name = "ferroid"
1238
+
version = "0.8.7"
1239
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1240
+
checksum = "e0e9414a6ae93ef993ce40a1e02944f13d4508e2bf6f1ced1580ce6910f08253"
1241
+
dependencies = [
1242
+
"portable-atomic",
1243
+
"rand 0.9.2",
1244
+
"web-time",
1245
+
]
1246
1247
[[package]]
1248
name = "ff"
···
1830
]
1831
1832
[[package]]
1833
+
name = "hyper-named-pipe"
1834
+
version = "0.1.0"
1835
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1836
+
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
1837
+
dependencies = [
1838
+
"hex",
1839
+
"hyper",
1840
+
"hyper-util",
1841
+
"pin-project-lite",
1842
+
"tokio",
1843
+
"tower-service",
1844
+
"winapi",
1845
+
]
1846
+
1847
+
[[package]]
1848
name = "hyper-rustls"
1849
version = "0.27.7"
1850
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1862
]
1863
1864
[[package]]
1865
+
name = "hyper-timeout"
1866
+
version = "0.5.2"
1867
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1868
+
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
1869
+
dependencies = [
1870
+
"hyper",
1871
+
"hyper-util",
1872
+
"pin-project-lite",
1873
+
"tokio",
1874
+
"tower-service",
1875
+
]
1876
+
1877
+
[[package]]
1878
name = "hyper-tls"
1879
version = "0.6.0"
1880
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1917
]
1918
1919
[[package]]
1920
+
name = "hyperlocal"
1921
+
version = "0.9.1"
1922
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1923
+
checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
1924
+
dependencies = [
1925
+
"hex",
1926
+
"http-body-util",
1927
+
"hyper",
1928
+
"hyper-util",
1929
+
"pin-project-lite",
1930
+
"tokio",
1931
+
"tower-service",
1932
+
]
1933
+
1934
+
[[package]]
1935
name = "iana-time-zone"
1936
version = "0.1.64"
1937
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2166
"thiserror 1.0.69",
2167
"tokio",
2168
"unsigned-varint 0.7.2",
2169
+
]
2170
+
2171
+
[[package]]
2172
+
name = "itertools"
2173
+
version = "0.14.0"
2174
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2175
+
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
2176
+
dependencies = [
2177
+
"either",
2178
]
2179
2180
[[package]]
···
2869
"openssl-probe",
2870
"openssl-sys",
2871
"schannel",
2872
+
"security-framework 2.11.1",
2873
"security-framework-sys",
2874
"tempfile",
2875
]
···
2906
]
2907
2908
[[package]]
2909
+
name = "num"
2910
+
version = "0.4.3"
2911
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2912
+
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
2913
+
dependencies = [
2914
+
"num-bigint",
2915
+
"num-complex",
2916
+
"num-integer",
2917
+
"num-iter",
2918
+
"num-rational",
2919
+
"num-traits",
2920
+
]
2921
+
2922
+
[[package]]
2923
name = "num-bigint"
2924
version = "0.4.6"
2925
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2946
]
2947
2948
[[package]]
2949
+
name = "num-complex"
2950
+
version = "0.4.6"
2951
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2952
+
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
2953
+
dependencies = [
2954
+
"num-traits",
2955
+
]
2956
+
2957
+
[[package]]
2958
name = "num-conv"
2959
version = "0.1.0"
2960
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2976
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
2977
dependencies = [
2978
"autocfg",
2979
+
"num-integer",
2980
+
"num-traits",
2981
+
]
2982
+
2983
+
[[package]]
2984
+
name = "num-rational"
2985
+
version = "0.4.2"
2986
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2987
+
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
2988
+
dependencies = [
2989
+
"num-bigint",
2990
"num-integer",
2991
"num-traits",
2992
]
···
3173
]
3174
3175
[[package]]
3176
+
name = "parse-display"
3177
+
version = "0.9.1"
3178
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3179
+
checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
3180
+
dependencies = [
3181
+
"parse-display-derive",
3182
+
"regex",
3183
+
"regex-syntax",
3184
+
]
3185
+
3186
+
[[package]]
3187
+
name = "parse-display-derive"
3188
+
version = "0.9.1"
3189
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3190
+
checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
3191
+
dependencies = [
3192
+
"proc-macro2",
3193
+
"quote",
3194
+
"regex",
3195
+
"regex-syntax",
3196
+
"structmeta",
3197
+
"syn 2.0.111",
3198
+
]
3199
+
3200
+
[[package]]
3201
name = "pem"
3202
version = "3.0.6"
3203
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3320
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
3321
3322
[[package]]
3323
+
name = "portable-atomic"
3324
+
version = "1.11.1"
3325
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3326
+
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
3327
+
3328
+
[[package]]
3329
name = "potential_utf"
3330
version = "0.1.4"
3331
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3418
"syn 2.0.111",
3419
"version_check",
3420
"yansi",
3421
+
]
3422
+
3423
+
[[package]]
3424
+
name = "prost"
3425
+
version = "0.14.1"
3426
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3427
+
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
3428
+
dependencies = [
3429
+
"bytes",
3430
+
"prost-derive",
3431
+
]
3432
+
3433
+
[[package]]
3434
+
name = "prost-derive"
3435
+
version = "0.14.1"
3436
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3437
+
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
3438
+
dependencies = [
3439
+
"anyhow",
3440
+
"itertools",
3441
+
"proc-macro2",
3442
+
"quote",
3443
+
"syn 2.0.111",
3444
+
]
3445
+
3446
+
[[package]]
3447
+
name = "prost-types"
3448
+
version = "0.14.1"
3449
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3450
+
checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
3451
+
dependencies = [
3452
+
"prost",
3453
]
3454
3455
[[package]]
···
3824
source = "registry+https://github.com/rust-lang/crates.io-index"
3825
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
3826
dependencies = [
3827
+
"log",
3828
"once_cell",
3829
"ring",
3830
"rustls-pki-types",
···
3834
]
3835
3836
[[package]]
3837
+
name = "rustls-native-certs"
3838
+
version = "0.8.2"
3839
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3840
+
checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
3841
+
dependencies = [
3842
+
"openssl-probe",
3843
+
"rustls-pki-types",
3844
+
"schannel",
3845
+
"security-framework 3.5.1",
3846
+
]
3847
+
3848
+
[[package]]
3849
+
name = "rustls-pemfile"
3850
+
version = "2.2.0"
3851
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3852
+
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
3853
+
dependencies = [
3854
+
"rustls-pki-types",
3855
+
]
3856
+
3857
+
[[package]]
3858
name = "rustls-pki-types"
3859
version = "1.13.1"
3860
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3969
dependencies = [
3970
"bitflags",
3971
"core-foundation 0.9.4",
3972
+
"core-foundation-sys",
3973
+
"libc",
3974
+
"security-framework-sys",
3975
+
]
3976
+
3977
+
[[package]]
3978
+
name = "security-framework"
3979
+
version = "3.5.1"
3980
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3981
+
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
3982
+
dependencies = [
3983
+
"bitflags",
3984
+
"core-foundation 0.10.1",
3985
"core-foundation-sys",
3986
"libc",
3987
"security-framework-sys",
···
4200
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
4201
4202
[[package]]
4203
+
name = "signal-hook-registry"
4204
+
version = "1.4.7"
4205
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4206
+
checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
4207
+
dependencies = [
4208
+
"libc",
4209
+
]
4210
+
4211
+
[[package]]
4212
name = "signature"
4213
version = "2.2.0"
4214
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4473
"chrono",
4474
"crc",
4475
"dotenvy",
4476
+
"etcetera 0.8.0",
4477
"futures-channel",
4478
"futures-core",
4479
"futures-util",
···
4600
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
4601
4602
[[package]]
4603
+
name = "structmeta"
4604
+
version = "0.3.0"
4605
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4606
+
checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
4607
+
dependencies = [
4608
+
"proc-macro2",
4609
+
"quote",
4610
+
"structmeta-derive",
4611
+
"syn 2.0.111",
4612
+
]
4613
+
4614
+
[[package]]
4615
+
name = "structmeta-derive"
4616
+
version = "0.3.0"
4617
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4618
+
checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
4619
+
dependencies = [
4620
+
"proc-macro2",
4621
+
"quote",
4622
+
"syn 2.0.111",
4623
+
]
4624
+
4625
+
[[package]]
4626
name = "subtle"
4627
version = "2.6.1"
4628
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4722
]
4723
4724
[[package]]
4725
+
name = "testcontainers"
4726
+
version = "0.26.0"
4727
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4728
+
checksum = "a347cac4368ba4f1871743adb27dc14829024d26b1763572404726b0b9943eb8"
4729
+
dependencies = [
4730
+
"astral-tokio-tar",
4731
+
"async-trait",
4732
+
"bollard",
4733
+
"bytes",
4734
+
"docker_credential",
4735
+
"either",
4736
+
"etcetera 0.11.0",
4737
+
"ferroid",
4738
+
"futures",
4739
+
"itertools",
4740
+
"log",
4741
+
"memchr",
4742
+
"parse-display",
4743
+
"pin-project-lite",
4744
+
"serde",
4745
+
"serde_json",
4746
+
"serde_with",
4747
+
"thiserror 2.0.17",
4748
+
"tokio",
4749
+
"tokio-stream",
4750
+
"tokio-util",
4751
+
"url",
4752
+
]
4753
+
4754
+
[[package]]
4755
+
name = "testcontainers-modules"
4756
+
version = "0.14.0"
4757
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4758
+
checksum = "5e75e78ff453128a2c7da9a5d5a3325ea34ea214d4bf51eab3417de23a4e5147"
4759
+
dependencies = [
4760
+
"testcontainers",
4761
+
]
4762
+
4763
+
[[package]]
4764
name = "thiserror"
4765
version = "1.0.69"
4766
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4898
"libc",
4899
"mio",
4900
"pin-project-lite",
4901
+
"signal-hook-registry",
4902
"socket2 0.6.1",
4903
"tokio-macros",
4904
"windows-sys 0.61.2",
···
4961
]
4962
4963
[[package]]
4964
+
name = "tonic"
4965
+
version = "0.14.2"
4966
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4967
+
checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
4968
+
dependencies = [
4969
+
"async-trait",
4970
+
"axum",
4971
+
"base64 0.22.1",
4972
+
"bytes",
4973
+
"h2",
4974
+
"http",
4975
+
"http-body",
4976
+
"http-body-util",
4977
+
"hyper",
4978
+
"hyper-timeout",
4979
+
"hyper-util",
4980
+
"percent-encoding",
4981
+
"pin-project",
4982
+
"socket2 0.6.1",
4983
+
"sync_wrapper",
4984
+
"tokio",
4985
+
"tokio-stream",
4986
+
"tower",
4987
+
"tower-layer",
4988
+
"tower-service",
4989
+
"tracing",
4990
+
]
4991
+
4992
+
[[package]]
4993
+
name = "tonic-prost"
4994
+
version = "0.14.2"
4995
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4996
+
checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67"
4997
+
dependencies = [
4998
+
"bytes",
4999
+
"prost",
5000
+
"tonic",
5001
+
]
5002
+
5003
+
[[package]]
5004
name = "tower"
5005
version = "0.5.2"
5006
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5008
dependencies = [
5009
"futures-core",
5010
"futures-util",
5011
+
"indexmap 2.12.1",
5012
"pin-project-lite",
5013
+
"slab",
5014
"sync_wrapper",
5015
"tokio",
5016
+
"tokio-util",
5017
"tower-layer",
5018
"tower-service",
5019
"tracing",
···
5218
version = "0.9.0"
5219
source = "registry+https://github.com/rust-lang/crates.io-index"
5220
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
5221
+
5222
+
[[package]]
5223
+
name = "ureq"
5224
+
version = "3.1.4"
5225
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5226
+
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
5227
+
dependencies = [
5228
+
"base64 0.22.1",
5229
+
"log",
5230
+
"percent-encoding",
5231
+
"rustls",
5232
+
"rustls-pki-types",
5233
+
"ureq-proto",
5234
+
"utf-8",
5235
+
"webpki-roots 1.0.4",
5236
+
]
5237
+
5238
+
[[package]]
5239
+
name = "ureq-proto"
5240
+
version = "0.5.3"
5241
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5242
+
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
5243
+
dependencies = [
5244
+
"base64 0.22.1",
5245
+
"http",
5246
+
"httparse",
5247
+
"log",
5248
+
]
5249
5250
[[package]]
5251
name = "url"
···
5501
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
5502
5503
[[package]]
5504
+
name = "winapi"
5505
+
version = "0.3.9"
5506
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5507
+
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
5508
+
dependencies = [
5509
+
"winapi-i686-pc-windows-gnu",
5510
+
"winapi-x86_64-pc-windows-gnu",
5511
+
]
5512
+
5513
+
[[package]]
5514
+
name = "winapi-i686-pc-windows-gnu"
5515
+
version = "0.4.0"
5516
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5517
+
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
5518
+
5519
+
[[package]]
5520
name = "winapi-util"
5521
version = "0.1.11"
5522
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5524
dependencies = [
5525
"windows-sys 0.61.2",
5526
]
5527
+
5528
+
[[package]]
5529
+
name = "winapi-x86_64-pc-windows-gnu"
5530
+
version = "0.4.0"
5531
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5532
+
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
5533
5534
[[package]]
5535
name = "windows"
···
5999
version = "0.6.2"
6000
source = "registry+https://github.com/rust-lang/crates.io-index"
6001
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
6002
+
6003
+
[[package]]
6004
+
name = "xattr"
6005
+
version = "1.6.1"
6006
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6007
+
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
6008
+
dependencies = [
6009
+
"libc",
6010
+
"rustix",
6011
+
]
6012
6013
[[package]]
6014
name = "xml5ever"
+4
Cargo.toml
+4
Cargo.toml
+48
-75
TODO.md
+48
-75
TODO.md
···
1
-
# Implementation TODOs
2
3
-
Lewis' special big boy todofile
4
5
-
## 1. Server Infrastructure & Health
6
- [x] Health Check
7
- [x] Implement `GET /health` endpoint (returns "OK").
8
- [x] Server Description
9
- [x] Implement `com.atproto.server.describeServer` (returns available user domains).
10
11
## 2. Authentication & Account Management (`com.atproto.server`)
12
- [x] Account Creation
13
- [x] Implement `com.atproto.server.createAccount`.
14
- [x] Validate handle format (reject invalid characters).
15
-
- [x] Create DID for new user.
16
-
- [x] Initialize user repository.
17
- [x] Return access JWT and DID.
18
-
- [x] MST stuff I think...
19
-
20
- [x] Session Management
21
- [x] Implement `com.atproto.server.createSession` (Login).
22
-
- [x] Validate identifier (handle/email) and password.
23
-
- [x] Return access JWT, refresh JWT, and DID.
24
- [x] Implement `com.atproto.server.getSession`.
25
-
- [x] Verify JWT validity.
26
- [x] Implement `com.atproto.server.refreshSession`.
27
- [x] Implement `com.atproto.server.deleteSession` (Logout).
28
-
- [x] Invalidate current session/token.
29
30
## 3. Repository Operations (`com.atproto.repo`)
31
- [ ] Record CRUD
32
- [ ] Implement `com.atproto.repo.createRecord`.
33
-
- [ ] Generate `rkey` if not provided.
34
-
- [ ] Validate schema against Lexicon.
35
-
- [ ] Handle `swapCommit` for optimistic locking.
36
- [ ] Implement `com.atproto.repo.putRecord`.
37
-
- [ ] Handle create vs update logic.
38
-
- [ ] Validate `repo` matches authenticated user.
39
-
- [ ] Validate record schema (e.g., missing required fields).
40
- [ ] Implement `com.atproto.repo.getRecord`.
41
-
- [ ] Handle missing params (400 Bad Request).
42
-
- [ ] Handle non-existent record (404 Not Found).
43
- [ ] Implement `com.atproto.repo.deleteRecord`.
44
- [ ] Implement `com.atproto.repo.listRecords`.
45
-
- [ ] Support pagination (`limit`, `cursor`).
46
- [ ] Blob Management
47
- [ ] Implement `com.atproto.repo.uploadBlob`.
48
-
- [ ] Enforce authentication.
49
-
- [ ] Validate MIME types (reject unsupported).
50
-
- [ ] Return blob reference (`$link`).
51
-
- [ ] Repo Meta
52
-
- [ ] Implement `com.atproto.repo.describeRepo`.
53
54
-
## 4. Actor & Profile (`app.bsky.actor`)
55
-
- [ ] Profile Management
56
-
- [ ] Implement `app.bsky.actor.getProfile`.
57
-
- [ ] Resolve handle to DID.
58
-
- [ ] Return profile record data.
59
-
- [ ] Discovery
60
-
- [ ] Implement `app.bsky.actor.searchActors`.
61
62
-
## 5. Feed & Timeline (`app.bsky.feed`)
63
-
- [ ] Feed Retrieval
64
-
- [ ] Implement `app.bsky.feed.getTimeline`.
65
-
- [ ] Implement `app.bsky.feed.getAuthorFeed`.
66
-
- [ ] Filter by actor.
67
-
- [ ] Respect mutes (if viewer is authenticated).
68
-
- [ ] Implement `app.bsky.feed.getPostThread`.
69
-
- [ ] Construct thread tree (parents, replies).
70
-
- [ ] Handle deleted posts (return `notFoundPost` view).
71
-
- [ ] Record Types
72
-
- [ ] Support `app.bsky.feed.post` record type.
73
-
- [ ] Support `app.bsky.feed.like` record type.
74
-
- [ ] Support `app.bsky.embed.images` in posts.
75
-
76
-
## 6. Social Graph (`app.bsky.graph`)
77
-
- [ ] Relationships
78
-
- [ ] Implement `app.bsky.graph.getFollows`.
79
-
- [ ] Implement `app.bsky.graph.getFollowers`.
80
-
- [ ] Implement `app.bsky.graph.getMutes`.
81
-
- [ ] Implement `app.bsky.graph.getBlocks`.
82
-
- [ ] Record Types
83
-
- [ ] Support `app.bsky.graph.follow` record type.
84
-
- [ ] Support `app.bsky.graph.mute` record type.
85
-
86
-
## 7. Notifications (`app.bsky.notification`)
87
-
- [ ] Notification Management
88
-
- [ ] Implement `app.bsky.notification.listNotifications`.
89
-
- [ ] Aggregate notifications (likes, follows, replies).
90
-
- [ ] Implement `app.bsky.notification.getUnreadCount`.
91
-
- [ ] Track read state.
92
-
- [ ] Reset count on list/read.
93
-
94
-
## 8. Identity (`com.atproto.identity`)
95
- [ ] Resolution
96
-
- [ ] Implement `com.atproto.identity.resolveHandle`.
97
98
-
## 9. Sync & Federation (`com.atproto.sync`)
99
-
- [ ] Data Export
100
-
- [ ] Implement `com.atproto.sync.getRepo` (Export CAR file).
101
-
- [ ] Implement `com.atproto.sync.getBlocks`.
102
103
-
## 10. General Requirements
104
- [ ] Validation
105
-
- [ ] Ensure all endpoints validate input parameters.
106
-
- [ ] Ensure proper error codes (400, 401, 404, 409).
107
-
- [ ] Concurrency
108
-
- [ ] Ensure thread safety for repo updates.
···
1
+
# PDS Implementation TODOs
2
3
+
Lewis' corrected big boy todofile
4
5
+
## 1. Server Infrastructure & Proxying
6
- [x] Health Check
7
- [x] Implement `GET /health` endpoint (returns "OK").
8
- [x] Server Description
9
- [x] Implement `com.atproto.server.describeServer` (returns available user domains).
10
+
- [x] XRPC Proxying
11
+
- [x] Implement strict forwarding for all `app.bsky.*` and `chat.bsky.*` requests to an appview.
12
+
- [x] Forward Auth headers correctly.
13
+
- [x] Handle AppView errors/timeouts gracefully.
14
15
## 2. Authentication & Account Management (`com.atproto.server`)
16
- [x] Account Creation
17
- [x] Implement `com.atproto.server.createAccount`.
18
- [x] Validate handle format (reject invalid characters).
19
+
- [x] Create DID for new user (PLC directory).
20
+
- [x] Initialize user repository (Root commit).
21
- [x] Return access JWT and DID.
22
+
- [ ] Create DID for new user (did:web).
23
- [x] Session Management
24
- [x] Implement `com.atproto.server.createSession` (Login).
25
- [x] Implement `com.atproto.server.getSession`.
26
- [x] Implement `com.atproto.server.refreshSession`.
27
- [x] Implement `com.atproto.server.deleteSession` (Logout).
28
29
## 3. Repository Operations (`com.atproto.repo`)
30
- [ ] Record CRUD
31
- [ ] Implement `com.atproto.repo.createRecord`.
32
+
- [ ] Validate schema against Lexicon (just structure, not complex logic).
33
+
- [ ] Generate `rkey` (TID) if not provided.
34
+
- [ ] Handle MST (Merkle Search Tree) insertion.
35
+
- [ ] **Trigger Firehose Event**.
36
- [ ] Implement `com.atproto.repo.putRecord`.
37
- [ ] Implement `com.atproto.repo.getRecord`.
38
- [ ] Implement `com.atproto.repo.deleteRecord`.
39
- [ ] Implement `com.atproto.repo.listRecords`.
40
+
- [ ] Implement `com.atproto.repo.describeRepo`.
41
- [ ] Blob Management
42
- [ ] Implement `com.atproto.repo.uploadBlob`.
43
+
- [ ] Store blob (S3).
44
+
- [ ] return `blob` ref (CID + MimeType).
45
46
+
## 4. Sync & Federation (`com.atproto.sync`)
47
+
- [ ] The Firehose (WebSocket)
48
+
- [ ] Implement `com.atproto.sync.subscribeRepos`.
49
+
- [ ] Broadcast real-time commit events.
50
+
- [ ] Handle cursor replay (backfill).
51
+
- [ ] Bulk Export
52
+
- [ ] Implement `com.atproto.sync.getRepo` (Return full CAR file of repo).
53
+
- [ ] Implement `com.atproto.sync.getBlocks` (Return specific blocks via CIDs).
54
+
- [ ] Implement `com.atproto.sync.getLatestCommit`.
55
+
- [ ] Implement `com.atproto.sync.getRecord` (Sync version, distinct from repo.getRecord).
56
+
- [ ] Blob Sync
57
+
- [ ] Implement `com.atproto.sync.getBlob`.
58
+
- [ ] Implement `com.atproto.sync.listBlobs`.
59
+
- [ ] Crawler Interaction
60
+
- [ ] Implement `com.atproto.sync.requestCrawl` (Notify relays to index us).
61
62
+
## 5. Identity (`com.atproto.identity`)
63
- [ ] Resolution
64
+
- [ ] Implement `com.atproto.identity.resolveHandle` (Can be internal or proxy to PLC).
65
+
- [ ] Implement `/.well-known/did.json` (Depends on supporting did:web).
66
67
+
## 6. Record Schema Validation
68
+
- [ ] `app.bsky.feed.post`
69
+
- [ ] `app.bsky.feed.like`
70
+
- [ ] `app.bsky.feed.repost`
71
+
- [ ] `app.bsky.graph.follow`
72
+
- [ ] `app.bsky.graph.block`
73
+
- [ ] `app.bsky.actor.profile`
74
+
- [ ] Other app(view) validation too!!!
75
76
+
## 7. General Requirements
77
+
- [ ] IPLD & MST
78
+
- [ ] Implement Merkle Search Tree (MST) logic for repo signing.
79
+
- [ ] Implement CAR (Content Addressable Archives) encoding/decoding.
80
- [ ] Validation
81
+
- [ ] DID PLC Operations (Sign rotation keys).
+23
justfile
+23
justfile
···
···
1
+
# Run all tests with correct threading models
2
+
test: test-proxy test-lifecycle test-others
3
+
4
+
# Proxy tests modify environment variables, so must run single-threaded
5
+
# TODO: figure out how to run in parallel
6
+
test-proxy:
7
+
cargo test --test proxy -- --test-threads=1
8
+
9
+
# Lifecycle tests involve complex state mutations, run single-threaded to be safe
10
+
# TODO: figure out how to run in parallel
11
+
test-lifecycle:
12
+
cargo test --test lifecycle -- --test-threads=1
13
+
14
+
test-others:
15
+
cargo test --lib
16
+
cargo test --test actor
17
+
cargo test --test feed
18
+
cargo test --test graph
19
+
cargo test --test identity
20
+
cargo test --test notification
21
+
cargo test --test repo
22
+
cargo test --test server
23
+
cargo test --test sync
+1
src/api/mod.rs
+1
src/api/mod.rs
+84
src/api/proxy.rs
+84
src/api/proxy.rs
···
···
1
+
use axum::{
2
+
extract::{Path, Query},
3
+
http::{HeaderMap, Method, StatusCode},
4
+
response::{IntoResponse, Response},
5
+
body::Bytes,
6
+
};
7
+
use reqwest::Client;
8
+
use tracing::{info, error};
9
+
use std::collections::HashMap;
10
+
11
+
pub async fn proxy_handler(
12
+
Path(method): Path<String>,
13
+
method_verb: Method,
14
+
headers: HeaderMap,
15
+
Query(params): Query<HashMap<String, String>>,
16
+
body: Bytes,
17
+
) -> Response {
18
+
19
+
let proxy_header = headers.get("atproto-proxy")
20
+
.and_then(|h| h.to_str().ok())
21
+
.map(|s| s.to_string());
22
+
23
+
let appview_url = match proxy_header {
24
+
Some(url) => url,
25
+
None => match std::env::var("APPVIEW_URL") {
26
+
Ok(url) => url,
27
+
Err(_) => return (StatusCode::BAD_GATEWAY, "No upstream AppView configured").into_response(),
28
+
},
29
+
};
30
+
31
+
let target_url = format!("{}/xrpc/{}", appview_url, method);
32
+
33
+
info!("Proxying {} request to {}", method_verb, target_url);
34
+
35
+
let client = Client::new();
36
+
37
+
let mut request_builder = client
38
+
.request(method_verb, &target_url)
39
+
.query(¶ms);
40
+
41
+
for (key, value) in headers.iter() {
42
+
if key != "host" && key != "content-length" {
43
+
request_builder = request_builder.header(key, value);
44
+
}
45
+
}
46
+
47
+
request_builder = request_builder.body(body);
48
+
49
+
match request_builder.send().await {
50
+
Ok(resp) => {
51
+
let status = resp.status();
52
+
let headers = resp.headers().clone();
53
+
let body = match resp.bytes().await {
54
+
Ok(b) => b,
55
+
Err(e) => {
56
+
error!("Error reading proxy response body: {:?}", e);
57
+
return (StatusCode::BAD_GATEWAY, "Error reading upstream response").into_response();
58
+
}
59
+
};
60
+
61
+
let mut response_builder = Response::builder().status(status);
62
+
63
+
for (key, value) in headers.iter() {
64
+
response_builder = response_builder.header(key, value);
65
+
}
66
+
67
+
match response_builder.body(axum::body::Body::from(body)) {
68
+
Ok(r) => r,
69
+
Err(e) => {
70
+
error!("Error building proxy response: {:?}", e);
71
+
(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
72
+
}
73
+
}
74
+
},
75
+
Err(e) => {
76
+
error!("Error sending proxy request: {:?}", e);
77
+
if e.is_timeout() {
78
+
(StatusCode::GATEWAY_TIMEOUT, "Upstream Timeout").into_response()
79
+
} else {
80
+
(StatusCode::BAD_GATEWAY, "Upstream Error").into_response()
81
+
}
82
+
}
83
+
}
84
+
}
+19
src/api/server.rs
+19
src/api/server.rs
···
14
use jacquard::types::{string::Tid, did::Did, integer::LimitedU32};
15
use std::sync::Arc;
16
17
+
pub async fn describe_server() -> impl IntoResponse {
18
+
let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string());
19
+
let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
20
+
21
+
Json(json!({
22
+
"availableUserDomains": domains
23
+
}))
24
+
}
25
+
26
+
pub async fn health(State(state): State<AppState>) -> impl IntoResponse {
27
+
match sqlx::query("SELECT 1").execute(&state.db).await {
28
+
Ok(_) => (StatusCode::OK, "OK"),
29
+
Err(e) => {
30
+
error!("Health check failed: {:?}", e);
31
+
(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")
32
+
}
33
+
}
34
+
}
35
+
36
#[derive(Deserialize)]
37
pub struct CreateAccountInput {
38
pub handle: String,
+24
src/lib.rs
+24
src/lib.rs
···
···
1
+
pub mod api;
2
+
pub mod state;
3
+
pub mod auth;
4
+
pub mod repo;
5
+
6
+
use axum::{
7
+
routing::{get, post, any},
8
+
Router,
9
+
};
10
+
use state::AppState;
11
+
12
+
pub fn app(state: AppState) -> Router {
13
+
Router::new()
14
+
.route("/health", get(api::server::health))
15
+
.route("/xrpc/com.atproto.server.describeServer", get(api::server::describe_server))
16
+
.route("/xrpc/com.atproto.server.createAccount", post(api::server::create_account))
17
+
.route("/xrpc/com.atproto.server.createSession", post(api::server::create_session))
18
+
.route("/xrpc/com.atproto.server.getSession", get(api::server::get_session))
19
+
.route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session))
20
+
.route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session))
21
+
.route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record))
22
+
.route("/xrpc/{*method}", any(api::proxy::proxy_handler))
23
+
.with_state(state)
24
+
}
+3
-45
src/main.rs
+3
-45
src/main.rs
···
1
-
mod api;
2
-
mod state;
3
-
mod auth;
4
-
mod repo;
5
-
6
-
use axum::{
7
-
extract::State,
8
-
routing::{get, post},
9
-
Router,
10
-
Json,
11
-
response::IntoResponse,
12
-
http::StatusCode,
13
-
};
14
-
use serde_json::json;
15
use std::net::SocketAddr;
16
-
use state::AppState;
17
-
use tracing::{info, error};
18
19
#[tokio::main]
20
async fn main() {
···
36
37
let state = AppState::new(pool);
38
39
-
let app = Router::new()
40
-
.route("/health", get(health))
41
-
.route("/xrpc/com.atproto.server.describeServer", get(describe_server))
42
-
.route("/xrpc/com.atproto.server.createAccount", post(api::server::create_account))
43
-
.route("/xrpc/com.atproto.server.createSession", post(api::server::create_session))
44
-
.route("/xrpc/com.atproto.server.getSession", get(api::server::get_session))
45
-
.route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session))
46
-
.route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session))
47
-
.route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record))
48
-
.with_state(state);
49
50
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
51
info!("listening on {}", addr);
52
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
53
axum::serve(listener, app).await.unwrap();
54
}
55
-
56
-
async fn health(State(state): State<AppState>) -> impl IntoResponse {
57
-
match sqlx::query("SELECT 1").execute(&state.db).await {
58
-
Ok(_) => (StatusCode::OK, "OK"),
59
-
Err(e) => {
60
-
error!("Health check failed: {:?}", e);
61
-
(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")
62
-
}
63
-
}
64
-
}
65
-
66
-
async fn describe_server() -> impl IntoResponse {
67
-
let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string());
68
-
let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect();
69
-
70
-
Json(json!({
71
-
"availableUserDomains": domains
72
-
}))
73
-
}
···
1
use std::net::SocketAddr;
2
+
use bspds::state::AppState;
3
+
use tracing::info;
4
5
#[tokio::main]
6
async fn main() {
···
22
23
let state = AppState::new(pool);
24
25
+
let app = bspds::app(state);
26
27
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
28
info!("listening on {}", addr);
29
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
30
axum::serve(listener, app).await.unwrap();
31
}
+2
-2
tests/actor.rs
+2
-2
tests/actor.rs
···
8
let params = [
9
("actor", AUTH_DID),
10
];
11
-
let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
25
("q", "test"),
26
("limit", "10"),
27
];
28
-
let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", BASE_URL))
29
.query(¶ms)
30
.bearer_auth(AUTH_TOKEN)
31
.send()
···
8
let params = [
9
("actor", AUTH_DID),
10
];
11
+
let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
25
("q", "test"),
26
("limit", "10"),
27
];
28
+
let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", base_url().await))
29
.query(¶ms)
30
.bearer_auth(AUTH_TOKEN)
31
.send()
+70
-4
tests/common/mod.rs
+70
-4
tests/common/mod.rs
···
5
use std::collections::HashMap;
6
#[allow(unused_imports)]
7
use std::time::Duration;
8
9
-
pub const BASE_URL: &str = "http://127.0.0.1:3000";
10
#[allow(dead_code)]
11
pub const AUTH_TOKEN: &str = "test-token";
12
#[allow(dead_code)]
···
20
Client::new()
21
}
22
23
#[allow(dead_code)]
24
pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
25
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
26
.header(header::CONTENT_TYPE, mime)
27
.bearer_auth(AUTH_TOKEN)
28
.body(data)
···
59
"record": record
60
});
61
62
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
63
.bearer_auth(AUTH_TOKEN)
64
.json(&payload)
65
.send()
···
84
"password": "password"
85
});
86
87
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
88
.json(&payload)
89
.send()
90
.await
···
5
use std::collections::HashMap;
6
#[allow(unused_imports)]
7
use std::time::Duration;
8
+
use std::sync::OnceLock;
9
+
use bspds::state::AppState;
10
+
use sqlx::postgres::PgPoolOptions;
11
+
use tokio::net::TcpListener;
12
+
use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt};
13
+
use testcontainers_modules::postgres::Postgres;
14
15
+
static SERVER_URL: OnceLock<String> = OnceLock::new();
16
+
static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
17
+
18
#[allow(dead_code)]
19
pub const AUTH_TOKEN: &str = "test-token";
20
#[allow(dead_code)]
···
28
Client::new()
29
}
30
31
+
pub async fn base_url() -> &'static str {
32
+
SERVER_URL.get_or_init(|| {
33
+
let (tx, rx) = std::sync::mpsc::channel();
34
+
35
+
std::thread::spawn(move || {
36
+
if std::env::var("DOCKER_HOST").is_err() {
37
+
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
38
+
let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
39
+
if podman_sock.exists() {
40
+
unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); }
41
+
}
42
+
}
43
+
}
44
+
45
+
let rt = tokio::runtime::Runtime::new().unwrap();
46
+
rt.block_on(async move {
47
+
let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres");
48
+
let connection_string = format!(
49
+
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
50
+
container.get_host_port_ipv4(5432).await.expect("Failed to get port")
51
+
);
52
+
53
+
DB_CONTAINER.set(container).ok();
54
+
55
+
let url = spawn_app(connection_string).await;
56
+
tx.send(url).unwrap();
57
+
std::future::pending::<()>().await;
58
+
});
59
+
});
60
+
61
+
rx.recv().expect("Failed to start test server")
62
+
})
63
+
}
64
+
65
+
async fn spawn_app(database_url: String) -> String {
66
+
let pool = PgPoolOptions::new()
67
+
.connect(&database_url)
68
+
.await
69
+
.expect("Failed to connect to Postgres. Make sure the database is running.");
70
+
71
+
sqlx::migrate!("./migrations")
72
+
.run(&pool)
73
+
.await
74
+
.expect("Failed to run migrations");
75
+
76
+
let state = AppState::new(pool);
77
+
let app = bspds::app(state);
78
+
79
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
80
+
let addr = listener.local_addr().unwrap();
81
+
82
+
tokio::spawn(async move {
83
+
axum::serve(listener, app).await.unwrap();
84
+
});
85
+
86
+
format!("http://{}", addr)
87
+
}
88
+
89
#[allow(dead_code)]
90
pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value {
91
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
92
.header(header::CONTENT_TYPE, mime)
93
.bearer_auth(AUTH_TOKEN)
94
.body(data)
···
125
"record": record
126
});
127
128
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
129
.bearer_auth(AUTH_TOKEN)
130
.json(&payload)
131
.send()
···
150
"password": "password"
151
});
152
153
+
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
154
.json(&payload)
155
.send()
156
.await
+3
-3
tests/feed.rs
+3
-3
tests/feed.rs
···
8
async fn test_get_timeline() {
9
let client = client();
10
let params = [("limit", "30")];
11
-
let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", BASE_URL))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
25
("actor", AUTH_DID),
26
("limit", "30")
27
];
28
-
let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
29
.query(¶ms)
30
.bearer_auth(AUTH_TOKEN)
31
.send()
···
42
params.insert("uri", "at://did:plc:other/app.bsky.feed.post/3k12345");
43
params.insert("depth", "5");
44
45
-
let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
46
.query(¶ms)
47
.bearer_auth(AUTH_TOKEN)
48
.send()
···
8
async fn test_get_timeline() {
9
let client = client();
10
let params = [("limit", "30")];
11
+
let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", base_url().await))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
25
("actor", AUTH_DID),
26
("limit", "30")
27
];
28
+
let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
29
.query(¶ms)
30
.bearer_auth(AUTH_TOKEN)
31
.send()
···
42
params.insert("uri", "at://did:plc:other/app.bsky.feed.post/3k12345");
43
params.insert("depth", "5");
44
45
+
let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
46
.query(¶ms)
47
.bearer_auth(AUTH_TOKEN)
48
.send()
+4
-4
tests/graph.rs
+4
-4
tests/graph.rs
···
8
let params = [
9
("actor", AUTH_DID),
10
];
11
-
let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
24
let params = [
25
("actor", AUTH_DID),
26
];
27
-
let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", BASE_URL))
28
.query(¶ms)
29
.bearer_auth(AUTH_TOKEN)
30
.send()
···
40
let params = [
41
("limit", "25"),
42
];
43
-
let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", BASE_URL))
44
.query(¶ms)
45
.bearer_auth(AUTH_TOKEN)
46
.send()
···
57
let params = [
58
("limit", "25"),
59
];
60
-
let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", BASE_URL))
61
.query(¶ms)
62
.bearer_auth(AUTH_TOKEN)
63
.send()
···
8
let params = [
9
("actor", AUTH_DID),
10
];
11
+
let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
24
let params = [
25
("actor", AUTH_DID),
26
];
27
+
let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", base_url().await))
28
.query(¶ms)
29
.bearer_auth(AUTH_TOKEN)
30
.send()
···
40
let params = [
41
("limit", "25"),
42
];
43
+
let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", base_url().await))
44
.query(¶ms)
45
.bearer_auth(AUTH_TOKEN)
46
.send()
···
57
let params = [
58
("limit", "25"),
59
];
60
+
let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", base_url().await))
61
.query(¶ms)
62
.bearer_auth(AUTH_TOKEN)
63
.send()
+1
-1
tests/identity.rs
+1
-1
tests/identity.rs
+42
-42
tests/lifecycle.rs
+42
-42
tests/lifecycle.rs
···
30
}
31
});
32
33
-
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
34
.bearer_auth(AUTH_TOKEN)
35
.json(&create_payload)
36
.send()
···
47
("collection", collection),
48
("rkey", &rkey),
49
];
50
-
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
51
.query(¶ms)
52
.send()
53
.await
···
71
}
72
});
73
74
-
let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
75
.bearer_auth(AUTH_TOKEN)
76
.json(&update_payload)
77
.send()
···
81
assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record");
82
83
84
-
let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
85
.query(¶ms)
86
.send()
87
.await
···
98
"rkey": rkey
99
});
100
101
-
let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
102
.bearer_auth(AUTH_TOKEN)
103
.json(&delete_payload)
104
.send()
···
108
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record");
109
110
111
-
let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
112
.query(¶ms)
113
.send()
114
.await
···
157
}
158
});
159
160
-
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
161
.bearer_auth(AUTH_TOKEN)
162
.json(&create_payload)
163
.send()
···
172
("collection", collection),
173
("rkey", &rkey),
174
];
175
-
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
176
.query(¶ms)
177
.send()
178
.await
···
202
}
203
});
204
205
-
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
206
.bearer_auth(AUTH_TOKEN)
207
.json(&create_payload)
208
.send()
···
219
let params_get_follows = [
220
("actor", AUTH_DID),
221
];
222
-
let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
223
.query(¶ms_get_follows)
224
.bearer_auth(AUTH_TOKEN)
225
.send()
···
243
"rkey": rkey
244
});
245
246
-
let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
247
.bearer_auth(AUTH_TOKEN)
248
.json(&delete_payload)
249
.send()
···
253
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record");
254
255
256
-
let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
257
.query(¶ms_get_follows)
258
.bearer_auth(AUTH_TOKEN)
259
.send()
···
290
}
291
});
292
293
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
294
.bearer_auth(AUTH_TOKEN)
295
.json(&payload)
296
.send()
···
308
("limit", "2"),
309
];
310
311
-
let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
312
.query(¶ms_page1)
313
.send()
314
.await
···
330
("cursor", cursor),
331
];
332
333
-
let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
334
.query(¶ms_page2)
335
.send()
336
.await
···
351
"collection": collection,
352
"rkey": rkey
353
});
354
-
client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
355
.bearer_auth(AUTH_TOKEN)
356
.json(&delete_payload)
357
.send()
···
386
let params = [
387
("uri", &root_uri),
388
];
389
-
let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
390
.query(¶ms)
391
.bearer_auth(AUTH_TOKEN)
392
.send()
···
410
411
412
let collection = "app.bsky.feed.post";
413
-
client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
414
.bearer_auth(AUTH_TOKEN)
415
.json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey }))
416
.send().await.expect("Failed to delete reply");
417
418
-
client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
419
.bearer_auth(AUTH_TOKEN)
420
.json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey }))
421
.send().await.expect("Failed to delete root post");
···
436
"password": password
437
});
438
439
-
let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
440
.json(&create_account_payload)
441
.send()
442
.await
···
455
"password": password
456
});
457
458
-
let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
459
.json(&session_payload)
460
.send()
461
.await
···
479
}
480
});
481
482
-
let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
483
.bearer_auth(&session_jwt)
484
.json(&profile_payload)
485
.send()
···
492
let params_get_profile = [
493
("actor", &handle),
494
];
495
-
let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL))
496
.query(¶ms_get_profile)
497
.send()
498
.await
···
506
assert_eq!(profile_body["displayName"], "E2E Test User");
507
508
509
-
let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL))
510
.bearer_auth(&session_jwt)
511
.send()
512
.await
···
515
assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session");
516
517
518
-
let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL))
519
.bearer_auth(&session_jwt)
520
.send()
521
.await
···
536
"email": email,
537
"password": password
538
});
539
-
let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
540
.json(&create_account_payload)
541
.send()
542
.await
···
557
"description": "A user created by the e2e test suite."
558
}
559
});
560
-
let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
561
.bearer_auth(&new_jwt)
562
.json(&profile_payload)
563
.send()
···
581
"record": record
582
});
583
584
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
585
.bearer_auth(jwt)
586
.json(&payload)
587
.send()
···
609
"rkey": rkey
610
});
611
612
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
613
.bearer_auth(jwt)
614
.json(&payload)
615
.send()
···
640
).await;
641
let post_ref = json!({ "uri": post_uri, "cid": post_cid });
642
643
-
let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
644
.bearer_auth(&user_a_jwt)
645
.send().await.expect("getUnreadCount 1 failed");
646
let count_body_1: Value = count_res_1.json().await.expect("count 1 not json");
···
677
678
tokio::time::sleep(Duration::from_millis(500)).await;
679
680
-
let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
681
.bearer_auth(&user_a_jwt)
682
.send().await.expect("getUnreadCount 2 failed");
683
let count_body_2: Value = count_res_2.json().await.expect("count 2 not json");
684
assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions");
685
686
-
let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL))
687
.bearer_auth(&user_a_jwt)
688
.send().await.expect("listNotifications failed");
689
let list_body: Value = list_res.json().await.expect("list not json");
···
699
assert!(has_like, "Notification list missing 'like'");
700
assert!(has_reply, "Notification list missing 'reply'");
701
702
-
let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
703
.bearer_auth(&user_a_jwt)
704
.send().await.expect("getUnreadCount 3 failed");
705
let count_body_3: Value = count_res_3.json().await.expect("count 3 not json");
···
727
).await;
728
729
let feed_params_1 = [("actor", &user_b_did)];
730
-
let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
731
.query(&feed_params_1)
732
.bearer_auth(&user_a_jwt)
733
.send().await.expect("getAuthorFeed 1 failed");
···
749
let mute_rkey = mute_uri.split('/').last().unwrap();
750
751
let feed_params_2 = [("actor", &user_b_did)];
752
-
let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
753
.query(&feed_params_2)
754
.bearer_auth(&user_a_jwt)
755
.send().await.expect("getAuthorFeed 2 failed");
···
765
).await;
766
767
let feed_params_3 = [("actor", &user_b_did)];
768
-
let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
769
.query(&feed_params_3)
770
.bearer_auth(&user_a_jwt)
771
.send().await.expect("getAuthorFeed 3 failed");
···
783
784
let (user_did, user_jwt) = setup_new_user("user-conflict").await;
785
786
-
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
787
.query(&[
788
("repo", &user_did),
789
("collection", &"app.bsky.actor.profile".to_string()),
···
803
},
804
"swapCommit": cid_v1 // <-- Correctly point to v1
805
});
806
-
let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
807
.bearer_auth(&user_jwt)
808
.json(&update_payload_v2)
809
.send().await.expect("putRecord v2 failed");
···
821
},
822
"swapCommit": cid_v1
823
});
824
-
let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
825
.bearer_auth(&user_jwt)
826
.json(&update_payload_v3_stale)
827
.send().await.expect("putRecord v3 (stale) failed");
···
842
},
843
"swapCommit": cid_v2 // <-- Correct
844
});
845
-
let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
846
.bearer_auth(&user_jwt)
847
.json(&update_payload_v3_good)
848
.send().await.expect("putRecord v3 (good) failed");
···
894
}),
895
).await;
896
897
-
let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
898
.query(&[("uri", &p1_uri)])
899
.bearer_auth(&user_a_jwt)
900
.send().await.expect("getThread 1 failed");
···
914
&p2_rkey,
915
).await;
916
917
-
let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
918
.query(&[("uri", &p1_uri)])
919
.bearer_auth(&user_a_jwt)
920
.send().await.expect("getThread 2 failed");
···
30
}
31
});
32
33
+
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
34
.bearer_auth(AUTH_TOKEN)
35
.json(&create_payload)
36
.send()
···
47
("collection", collection),
48
("rkey", &rkey),
49
];
50
+
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
51
.query(¶ms)
52
.send()
53
.await
···
71
}
72
});
73
74
+
let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
75
.bearer_auth(AUTH_TOKEN)
76
.json(&update_payload)
77
.send()
···
81
assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record");
82
83
84
+
let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
85
.query(¶ms)
86
.send()
87
.await
···
98
"rkey": rkey
99
});
100
101
+
let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
102
.bearer_auth(AUTH_TOKEN)
103
.json(&delete_payload)
104
.send()
···
108
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record");
109
110
111
+
let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
112
.query(¶ms)
113
.send()
114
.await
···
157
}
158
});
159
160
+
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
161
.bearer_auth(AUTH_TOKEN)
162
.json(&create_payload)
163
.send()
···
172
("collection", collection),
173
("rkey", &rkey),
174
];
175
+
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
176
.query(¶ms)
177
.send()
178
.await
···
202
}
203
});
204
205
+
let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
206
.bearer_auth(AUTH_TOKEN)
207
.json(&create_payload)
208
.send()
···
219
let params_get_follows = [
220
("actor", AUTH_DID),
221
];
222
+
let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await))
223
.query(¶ms_get_follows)
224
.bearer_auth(AUTH_TOKEN)
225
.send()
···
243
"rkey": rkey
244
});
245
246
+
let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
247
.bearer_auth(AUTH_TOKEN)
248
.json(&delete_payload)
249
.send()
···
253
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record");
254
255
256
+
let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await))
257
.query(¶ms_get_follows)
258
.bearer_auth(AUTH_TOKEN)
259
.send()
···
290
}
291
});
292
293
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
294
.bearer_auth(AUTH_TOKEN)
295
.json(&payload)
296
.send()
···
308
("limit", "2"),
309
];
310
311
+
let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
312
.query(¶ms_page1)
313
.send()
314
.await
···
330
("cursor", cursor),
331
];
332
333
+
let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
334
.query(¶ms_page2)
335
.send()
336
.await
···
351
"collection": collection,
352
"rkey": rkey
353
});
354
+
client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
355
.bearer_auth(AUTH_TOKEN)
356
.json(&delete_payload)
357
.send()
···
386
let params = [
387
("uri", &root_uri),
388
];
389
+
let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
390
.query(¶ms)
391
.bearer_auth(AUTH_TOKEN)
392
.send()
···
410
411
412
let collection = "app.bsky.feed.post";
413
+
client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
414
.bearer_auth(AUTH_TOKEN)
415
.json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey }))
416
.send().await.expect("Failed to delete reply");
417
418
+
client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
419
.bearer_auth(AUTH_TOKEN)
420
.json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey }))
421
.send().await.expect("Failed to delete root post");
···
436
"password": password
437
});
438
439
+
let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
440
.json(&create_account_payload)
441
.send()
442
.await
···
455
"password": password
456
});
457
458
+
let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
459
.json(&session_payload)
460
.send()
461
.await
···
479
}
480
});
481
482
+
let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
483
.bearer_auth(&session_jwt)
484
.json(&profile_payload)
485
.send()
···
492
let params_get_profile = [
493
("actor", &handle),
494
];
495
+
let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await))
496
.query(¶ms_get_profile)
497
.send()
498
.await
···
506
assert_eq!(profile_body["displayName"], "E2E Test User");
507
508
509
+
let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await))
510
.bearer_auth(&session_jwt)
511
.send()
512
.await
···
515
assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session");
516
517
518
+
let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await))
519
.bearer_auth(&session_jwt)
520
.send()
521
.await
···
536
"email": email,
537
"password": password
538
});
539
+
let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
540
.json(&create_account_payload)
541
.send()
542
.await
···
557
"description": "A user created by the e2e test suite."
558
}
559
});
560
+
let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
561
.bearer_auth(&new_jwt)
562
.json(&profile_payload)
563
.send()
···
581
"record": record
582
});
583
584
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
585
.bearer_auth(jwt)
586
.json(&payload)
587
.send()
···
609
"rkey": rkey
610
});
611
612
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
613
.bearer_auth(jwt)
614
.json(&payload)
615
.send()
···
640
).await;
641
let post_ref = json!({ "uri": post_uri, "cid": post_cid });
642
643
+
let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
644
.bearer_auth(&user_a_jwt)
645
.send().await.expect("getUnreadCount 1 failed");
646
let count_body_1: Value = count_res_1.json().await.expect("count 1 not json");
···
677
678
tokio::time::sleep(Duration::from_millis(500)).await;
679
680
+
let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
681
.bearer_auth(&user_a_jwt)
682
.send().await.expect("getUnreadCount 2 failed");
683
let count_body_2: Value = count_res_2.json().await.expect("count 2 not json");
684
assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions");
685
686
+
let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await))
687
.bearer_auth(&user_a_jwt)
688
.send().await.expect("listNotifications failed");
689
let list_body: Value = list_res.json().await.expect("list not json");
···
699
assert!(has_like, "Notification list missing 'like'");
700
assert!(has_reply, "Notification list missing 'reply'");
701
702
+
let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
703
.bearer_auth(&user_a_jwt)
704
.send().await.expect("getUnreadCount 3 failed");
705
let count_body_3: Value = count_res_3.json().await.expect("count 3 not json");
···
727
).await;
728
729
let feed_params_1 = [("actor", &user_b_did)];
730
+
let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
731
.query(&feed_params_1)
732
.bearer_auth(&user_a_jwt)
733
.send().await.expect("getAuthorFeed 1 failed");
···
749
let mute_rkey = mute_uri.split('/').last().unwrap();
750
751
let feed_params_2 = [("actor", &user_b_did)];
752
+
let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
753
.query(&feed_params_2)
754
.bearer_auth(&user_a_jwt)
755
.send().await.expect("getAuthorFeed 2 failed");
···
765
).await;
766
767
let feed_params_3 = [("actor", &user_b_did)];
768
+
let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await))
769
.query(&feed_params_3)
770
.bearer_auth(&user_a_jwt)
771
.send().await.expect("getAuthorFeed 3 failed");
···
783
784
let (user_did, user_jwt) = setup_new_user("user-conflict").await;
785
786
+
let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
787
.query(&[
788
("repo", &user_did),
789
("collection", &"app.bsky.actor.profile".to_string()),
···
803
},
804
"swapCommit": cid_v1 // <-- Correctly point to v1
805
});
806
+
let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
807
.bearer_auth(&user_jwt)
808
.json(&update_payload_v2)
809
.send().await.expect("putRecord v2 failed");
···
821
},
822
"swapCommit": cid_v1
823
});
824
+
let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
825
.bearer_auth(&user_jwt)
826
.json(&update_payload_v3_stale)
827
.send().await.expect("putRecord v3 (stale) failed");
···
842
},
843
"swapCommit": cid_v2 // <-- Correct
844
});
845
+
let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
846
.bearer_auth(&user_jwt)
847
.json(&update_payload_v3_good)
848
.send().await.expect("putRecord v3 (good) failed");
···
894
}),
895
).await;
896
897
+
let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
898
.query(&[("uri", &p1_uri)])
899
.bearer_auth(&user_a_jwt)
900
.send().await.expect("getThread 1 failed");
···
914
&p2_rkey,
915
).await;
916
917
+
let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await))
918
.query(&[("uri", &p1_uri)])
919
.bearer_auth(&user_a_jwt)
920
.send().await.expect("getThread 2 failed");
+2
-2
tests/notification.rs
+2
-2
tests/notification.rs
···
8
let params = [
9
("limit", "30"),
10
];
11
-
let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
21
#[tokio::test]
22
async fn test_get_unread_count() {
23
let client = client();
24
-
let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
25
.bearer_auth(AUTH_TOKEN)
26
.send()
27
.await
···
8
let params = [
9
("limit", "30"),
10
];
11
+
let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await))
12
.query(¶ms)
13
.bearer_auth(AUTH_TOKEN)
14
.send()
···
21
#[tokio::test]
22
async fn test_get_unread_count() {
23
let client = client();
24
+
let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await))
25
.bearer_auth(AUTH_TOKEN)
26
.send()
27
.await
+96
tests/proxy.rs
+96
tests/proxy.rs
···
···
1
+
mod common;
2
+
3
+
use axum::{
4
+
routing::any,
5
+
Router,
6
+
extract::Request,
7
+
http::StatusCode,
8
+
};
9
+
use tokio::net::TcpListener;
10
+
use reqwest::Client;
11
+
use std::sync::Arc;
12
+
13
+
async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String, String, Option<String>)>) {
14
+
let (tx, rx) = tokio::sync::mpsc::channel(10);
15
+
let tx = Arc::new(tx);
16
+
17
+
let app = Router::new().fallback(any(move |req: Request| {
18
+
let tx = tx.clone();
19
+
async move {
20
+
let method = req.method().to_string();
21
+
let uri = req.uri().to_string();
22
+
let auth = req.headers().get("Authorization")
23
+
.and_then(|h| h.to_str().ok())
24
+
.map(|s| s.to_string());
25
+
26
+
let _ = tx.send((method, uri, auth)).await;
27
+
(StatusCode::OK, "Mock Response")
28
+
}
29
+
}));
30
+
31
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
32
+
let addr = listener.local_addr().unwrap();
33
+
34
+
tokio::spawn(async move {
35
+
axum::serve(listener, app).await.unwrap();
36
+
});
37
+
38
+
(format!("http://{}", addr), rx)
39
+
}
40
+
41
+
#[tokio::test]
42
+
async fn test_proxy_via_header() {
43
+
let app_url = common::base_url().await;
44
+
let (upstream_url, mut rx) = spawn_mock_upstream().await;
45
+
let client = Client::new();
46
+
47
+
let res = client.get(format!("{}/xrpc/com.example.test", app_url))
48
+
.header("atproto-proxy", &upstream_url)
49
+
.header("Authorization", "Bearer test-token")
50
+
.send()
51
+
.await
52
+
.unwrap();
53
+
54
+
assert_eq!(res.status(), StatusCode::OK);
55
+
56
+
let (method, uri, auth) = rx.recv().await.expect("Upstream should receive request");
57
+
assert_eq!(method, "GET");
58
+
assert_eq!(uri, "/xrpc/com.example.test");
59
+
assert_eq!(auth, Some("Bearer test-token".to_string()));
60
+
}
61
+
62
+
#[tokio::test]
63
+
async fn test_proxy_via_env_var() {
64
+
let (upstream_url, mut rx) = spawn_mock_upstream().await;
65
+
66
+
unsafe { std::env::set_var("APPVIEW_URL", &upstream_url); }
67
+
68
+
let app_url = common::base_url().await;
69
+
let client = Client::new();
70
+
71
+
let res = client.get(format!("{}/xrpc/com.example.envtest", app_url))
72
+
.send()
73
+
.await
74
+
.unwrap();
75
+
76
+
assert_eq!(res.status(), StatusCode::OK);
77
+
78
+
let (method, uri, _) = rx.recv().await.expect("Upstream should receive request");
79
+
assert_eq!(method, "GET");
80
+
assert_eq!(uri, "/xrpc/com.example.envtest");
81
+
}
82
+
83
+
#[tokio::test]
84
+
async fn test_proxy_missing_config() {
85
+
unsafe { std::env::remove_var("APPVIEW_URL"); }
86
+
87
+
let app_url = common::base_url().await;
88
+
let client = Client::new();
89
+
90
+
let res = client.get(format!("{}/xrpc/com.example.fail", app_url))
91
+
.send()
92
+
.await
93
+
.unwrap();
94
+
95
+
assert_eq!(res.status(), StatusCode::BAD_GATEWAY);
96
+
}
+16
-16
tests/repo.rs
+16
-16
tests/repo.rs
···
15
("rkey", "self"),
16
];
17
18
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
19
.query(¶ms)
20
.send()
21
.await
···
36
("rkey", "nonexistent"),
37
];
38
39
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
40
.query(¶ms)
41
.send()
42
.await
···
51
#[ignore]
52
async fn test_upload_blob_no_auth() {
53
let client = client();
54
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
55
.header(header::CONTENT_TYPE, "text/plain")
56
.body("no auth")
57
.send()
···
68
async fn test_upload_blob_success() {
69
let client = client();
70
let (token, _) = create_account_and_login(&client).await;
71
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
72
.header(header::CONTENT_TYPE, "text/plain")
73
.bearer_auth(token)
74
.body("This is our blob data")
···
92
"record": {}
93
});
94
95
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
96
.json(&payload)
97
.send()
98
.await
···
120
}
121
});
122
123
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
124
.bearer_auth(token)
125
.json(&payload)
126
.send()
···
142
("repo", "did:plc:12345"),
143
];
144
145
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
146
.query(¶ms)
147
.send()
148
.await
···
156
#[ignore]
157
async fn test_upload_blob_bad_token() {
158
let client = client();
159
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
160
.header(header::CONTENT_TYPE, "text/plain")
161
.bearer_auth(BAD_AUTH_TOKEN)
162
.body("This is our blob data")
···
187
}
188
});
189
190
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
191
.bearer_auth(token)
192
.json(&payload)
193
.send()
···
215
}
216
});
217
218
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
219
.bearer_auth(token)
220
.json(&payload)
221
.send()
···
231
async fn test_upload_blob_unsupported_mime_type() {
232
let client = client();
233
let (token, _) = create_account_and_login(&client).await;
234
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL))
235
.header(header::CONTENT_TYPE, "application/xml")
236
.bearer_auth(token)
237
.body("<xml>not an image</xml>")
···
252
("collection", "app.bsky.feed.post"),
253
("limit", "10"),
254
];
255
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
256
.query(¶ms)
257
.send()
258
.await
···
270
"collection": "app.bsky.feed.post",
271
"rkey": "some_post_to_delete"
272
});
273
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
274
.bearer_auth(token)
275
.json(&payload)
276
.send()
···
287
let params = [
288
("repo", did.as_str()),
289
];
290
-
let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", BASE_URL))
291
.query(¶ms)
292
.send()
293
.await
···
310
}
311
});
312
313
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
314
.json(&payload)
315
.bearer_auth(token) // Assuming auth is required
316
.send()
···
340
}
341
});
342
343
-
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
344
.json(&payload)
345
.bearer_auth(token) // Assuming auth is required
346
.send()
···
15
("rkey", "self"),
16
];
17
18
+
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
19
.query(¶ms)
20
.send()
21
.await
···
36
("rkey", "nonexistent"),
37
];
38
39
+
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
40
.query(¶ms)
41
.send()
42
.await
···
51
#[ignore]
52
async fn test_upload_blob_no_auth() {
53
let client = client();
54
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
55
.header(header::CONTENT_TYPE, "text/plain")
56
.body("no auth")
57
.send()
···
68
async fn test_upload_blob_success() {
69
let client = client();
70
let (token, _) = create_account_and_login(&client).await;
71
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
72
.header(header::CONTENT_TYPE, "text/plain")
73
.bearer_auth(token)
74
.body("This is our blob data")
···
92
"record": {}
93
});
94
95
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
96
.json(&payload)
97
.send()
98
.await
···
120
}
121
});
122
123
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
124
.bearer_auth(token)
125
.json(&payload)
126
.send()
···
142
("repo", "did:plc:12345"),
143
];
144
145
+
let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
146
.query(¶ms)
147
.send()
148
.await
···
156
#[ignore]
157
async fn test_upload_blob_bad_token() {
158
let client = client();
159
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
160
.header(header::CONTENT_TYPE, "text/plain")
161
.bearer_auth(BAD_AUTH_TOKEN)
162
.body("This is our blob data")
···
187
}
188
});
189
190
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
191
.bearer_auth(token)
192
.json(&payload)
193
.send()
···
215
}
216
});
217
218
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
219
.bearer_auth(token)
220
.json(&payload)
221
.send()
···
231
async fn test_upload_blob_unsupported_mime_type() {
232
let client = client();
233
let (token, _) = create_account_and_login(&client).await;
234
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
235
.header(header::CONTENT_TYPE, "application/xml")
236
.bearer_auth(token)
237
.body("<xml>not an image</xml>")
···
252
("collection", "app.bsky.feed.post"),
253
("limit", "10"),
254
];
255
+
let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
256
.query(¶ms)
257
.send()
258
.await
···
270
"collection": "app.bsky.feed.post",
271
"rkey": "some_post_to_delete"
272
});
273
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
274
.bearer_auth(token)
275
.json(&payload)
276
.send()
···
287
let params = [
288
("repo", did.as_str()),
289
];
290
+
let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", base_url().await))
291
.query(¶ms)
292
.send()
293
.await
···
310
}
311
});
312
313
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
314
.json(&payload)
315
.bearer_auth(token) // Assuming auth is required
316
.send()
···
340
}
341
});
342
343
+
let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
344
.json(&payload)
345
.bearer_auth(token) // Assuming auth is required
346
.send()
+11
-11
tests/server.rs
+11
-11
tests/server.rs
···
7
#[tokio::test]
8
async fn test_health() {
9
let client = client();
10
-
let res = client.get(format!("{}/health", BASE_URL))
11
.send()
12
.await
13
.expect("Failed to send request");
···
19
#[tokio::test]
20
async fn test_describe_server() {
21
let client = client();
22
-
let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", BASE_URL))
23
.send()
24
.await
25
.expect("Failed to send request");
···
39
"email": format!("{}@example.com", handle),
40
"password": "password"
41
});
42
-
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
43
.json(&payload)
44
.send()
45
.await;
···
49
"password": "password"
50
});
51
52
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
53
.json(&payload)
54
.send()
55
.await
···
67
"password": "password"
68
});
69
70
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
71
.json(&payload)
72
.send()
73
.await
···
86
"password": "password"
87
});
88
89
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
90
.json(&payload)
91
.send()
92
.await
···
98
#[tokio::test]
99
async fn test_get_session() {
100
let client = client();
101
-
let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL))
102
.bearer_auth(AUTH_TOKEN)
103
.send()
104
.await
···
117
"email": format!("{}@example.com", handle),
118
"password": "password"
119
});
120
-
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
121
.json(&payload)
122
.send()
123
.await;
···
126
"identifier": handle,
127
"password": "password"
128
});
129
-
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
130
.json(&login_payload)
131
.send()
132
.await
···
137
let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string();
138
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
139
140
-
let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", BASE_URL))
141
.bearer_auth(&refresh_jwt)
142
.send()
143
.await
···
154
#[tokio::test]
155
async fn test_delete_session() {
156
let client = client();
157
-
let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL))
158
.bearer_auth(AUTH_TOKEN)
159
.send()
160
.await
···
7
#[tokio::test]
8
async fn test_health() {
9
let client = client();
10
+
let res = client.get(format!("{}/health", base_url().await))
11
.send()
12
.await
13
.expect("Failed to send request");
···
19
#[tokio::test]
20
async fn test_describe_server() {
21
let client = client();
22
+
let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base_url().await))
23
.send()
24
.await
25
.expect("Failed to send request");
···
39
"email": format!("{}@example.com", handle),
40
"password": "password"
41
});
42
+
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
43
.json(&payload)
44
.send()
45
.await;
···
49
"password": "password"
50
});
51
52
+
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
53
.json(&payload)
54
.send()
55
.await
···
67
"password": "password"
68
});
69
70
+
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
71
.json(&payload)
72
.send()
73
.await
···
86
"password": "password"
87
});
88
89
+
let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
90
.json(&payload)
91
.send()
92
.await
···
98
#[tokio::test]
99
async fn test_get_session() {
100
let client = client();
101
+
let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await))
102
.bearer_auth(AUTH_TOKEN)
103
.send()
104
.await
···
117
"email": format!("{}@example.com", handle),
118
"password": "password"
119
});
120
+
let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await))
121
.json(&payload)
122
.send()
123
.await;
···
126
"identifier": handle,
127
"password": "password"
128
});
129
+
let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await))
130
.json(&login_payload)
131
.send()
132
.await
···
137
let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string();
138
let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string();
139
140
+
let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base_url().await))
141
.bearer_auth(&refresh_jwt)
142
.send()
143
.await
···
154
#[tokio::test]
155
async fn test_delete_session() {
156
let client = client();
157
+
let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await))
158
.bearer_auth(AUTH_TOKEN)
159
.send()
160
.await
+2
-2
tests/sync.rs
+2
-2
tests/sync.rs
···
8
let params = [
9
("did", AUTH_DID),
10
];
11
-
let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", BASE_URL))
12
.query(¶ms)
13
.send()
14
.await
···
24
("did", AUTH_DID),
25
// "cids" would be a list of CIDs
26
];
27
-
let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", BASE_URL))
28
.query(¶ms)
29
.send()
30
.await
···
8
let params = [
9
("did", AUTH_DID),
10
];
11
+
let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", base_url().await))
12
.query(¶ms)
13
.send()
14
.await
···
24
("did", AUTH_DID),
25
// "cids" would be a list of CIDs
26
];
27
+
let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", base_url().await))
28
.query(¶ms)
29
.send()
30
.await