mirror of
https://github.com/aljazceru/pubky-core.git
synced 2026-01-12 10:44:21 +01:00
chore: Moved e2e tests / Fixes circular dependency (#88)
* moved e2e tests * moved e2e tests to its own workspace member * fmt
This commit is contained in:
committed by
GitHub
parent
8e1056c397
commit
ca0995cb23
2
.github/workflows/pr-check.yml
vendored
2
.github/workflows/pr-check.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
crate:
|
||||
[pubky, pubky-common, pubky-homeserver, pubky-testnet, http-relay, pkarr-republisher]
|
||||
[pubky, pubky-common, pubky-homeserver, pubky-testnet, http-relay, pkarr-republisher, e2e]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
118
Cargo.lock
generated
118
Cargo.lock
generated
@@ -389,9 +389,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.0"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
@@ -769,6 +769,19 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
|
||||
|
||||
[[package]]
|
||||
name = "e2e"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pkarr",
|
||||
"pubky-common",
|
||||
"pubky-testnet",
|
||||
"reqwest",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
@@ -1997,9 +2010,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkarr"
|
||||
version = "3.5.3"
|
||||
version = "3.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b006293464515e54044b64e1853c893111eb4e579f9f59decba0039d7c27e2f9"
|
||||
checksum = "7288f55e8981cce659ff14e05bbc0ade2d3015e45601ed4eb8ae8736c55c2a5b"
|
||||
dependencies = [
|
||||
"async-compat",
|
||||
"base32",
|
||||
@@ -2495,9 +2508,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.12"
|
||||
version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -3629,33 +3642,38 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3709,13 +3727,29 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
"windows_i686_gnullvm 0.53.0",
|
||||
"windows_i686_msvc 0.53.0",
|
||||
"windows_x86_64_gnu 0.53.0",
|
||||
"windows_x86_64_gnullvm 0.53.0",
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -3728,6 +3762,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -3740,6 +3780,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -3752,12 +3798,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -3770,6 +3828,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
@@ -3782,6 +3846,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
@@ -3794,6 +3864,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
@@ -3806,6 +3882,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.3"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
members = [
|
||||
"pubky",
|
||||
"pubky-*",
|
||||
|
||||
"http-relay",
|
||||
"pkarr-republisher",
|
||||
"examples"
|
||||
"examples",
|
||||
"e2e",
|
||||
]
|
||||
|
||||
# See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352
|
||||
|
||||
13
e2e/Cargo.toml
Normal file
13
e2e/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
pubky-testnet = { path = "../pubky-testnet" }
|
||||
pubky-common = { path = "../pubky-common" }
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
tracing-subscriber = "0.3.19"
|
||||
pkarr = "3.6.0"
|
||||
reqwest = "0.12.15"
|
||||
bytes = "1.10.1"
|
||||
17
e2e/README.md
Normal file
17
e2e/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Pubky End2End Tests
|
||||
|
||||
This workspace member contains Pubky End2End tests. Run them with `cargo test`.
|
||||
|
||||
## Pubky Testing Strategy
|
||||
|
||||
|
||||
### Unit Testing
|
||||
|
||||
Each member of this workspace, for example `pkarr-republisher`, are tested individually
|
||||
in their respective member folder. Dependencies like the `pubky-homeserver`
|
||||
should be mocked. Focus on testing the individual components and not all of Pubky.
|
||||
|
||||
### E2E Testing
|
||||
|
||||
E2E tests cover multiple workspace members. Test full workflows.
|
||||
It's recommended to use `pubky-testnet` which provides a convinient way to run Pubky components.
|
||||
3
e2e/src/lib.rs
Normal file
3
e2e/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// E2E tests
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
447
e2e/src/tests/auth.rs
Normal file
447
e2e/src/tests/auth.rs
Normal file
@@ -0,0 +1,447 @@
|
||||
use pkarr::Keypair;
|
||||
use pubky_common::capabilities::{Capabilities, Capability};
|
||||
use pubky_testnet::Testnet;
|
||||
use reqwest::StatusCode;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_authn() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
client.signout(&keypair.public_key()).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client.session(&keypair.public_key()).await.unwrap();
|
||||
|
||||
assert!(session.is_none());
|
||||
}
|
||||
|
||||
client.signin(&keypair).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authz() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let http_relay = testnet.run_http_relay().await.unwrap();
|
||||
let http_relay_url = http_relay.local_link_url();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
// Third party app side
|
||||
let capabilities: Capabilities = "/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let pubky_auth_request = client.auth_request(http_relay_url, &capabilities).unwrap();
|
||||
|
||||
// Authenticator side
|
||||
{
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.send_auth_token(&keypair, pubky_auth_request.url())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let public_key = pubky_auth_request.response().await.unwrap();
|
||||
|
||||
assert_eq!(&public_key, &pubky);
|
||||
|
||||
let session = client.session(&pubky).await.unwrap().unwrap();
|
||||
assert_eq!(session.capabilities(), &capabilities.0);
|
||||
|
||||
// Test access control enforcement
|
||||
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app/foo"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/foo.bar/file"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_users() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let first_keypair = Keypair::random();
|
||||
let second_keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&first_keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.signup(&second_keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&first_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &first_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
let session = client
|
||||
.session(&second_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &second_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authz_timeout_reconnect() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let http_relay = testnet.run_http_relay().await.unwrap();
|
||||
let http_relay_url = http_relay.local_link_url();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
// Third party app side
|
||||
let capabilities: Capabilities = "/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap();
|
||||
|
||||
let client = testnet
|
||||
.client_builder()
|
||||
.request_timeout(Duration::from_millis(1000))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let pubky_auth_request = client.auth_request(http_relay_url, &capabilities).unwrap();
|
||||
|
||||
// Authenticator side
|
||||
{
|
||||
let url = pubky_auth_request.url().clone();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(400)).await;
|
||||
// loop {
|
||||
client.send_auth_token(&keypair, &url).await.unwrap();
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
let public_key = pubky_auth_request.response().await.unwrap();
|
||||
|
||||
assert_eq!(&public_key, &pubky);
|
||||
|
||||
let session = client.session(&pubky).await.unwrap().unwrap();
|
||||
assert_eq!(session.capabilities(), &capabilities.0);
|
||||
|
||||
// Test access control enforcement
|
||||
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app/foo"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/foo.bar/file"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_signup_with_token() {
|
||||
// 1. Start a test homeserver with closed signups (i.e. signup tokens required)
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver_with_signup_tokens().await.unwrap();
|
||||
|
||||
let admin_password = "admin";
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
let keypair = Keypair::random();
|
||||
|
||||
// 2. Try to signup with an invalid token "AAAAA" and expect failure.
|
||||
let invalid_signup = client
|
||||
.signup(&keypair, &server.public_key(), Some("AAAA-BBBB-CCCC"))
|
||||
.await;
|
||||
assert!(
|
||||
invalid_signup.is_err(),
|
||||
"Signup should fail with an invalid signup token"
|
||||
);
|
||||
|
||||
// 3. Call the admin endpoint to generate a valid signup token.
|
||||
// The admin endpoint is protected via the header "X-Admin-Password"
|
||||
// and the password we set up above.
|
||||
let admin_url = format!(
|
||||
"https://{}/admin/generate_signup_token",
|
||||
server.public_key()
|
||||
);
|
||||
|
||||
// 3.1. Call the admin endpoint *with a WRONG admin password* to ensure we get 401 UNAUTHORIZED.
|
||||
let wrong_password_response = client
|
||||
.get(&admin_url)
|
||||
.header("X-Admin-Password", "wrong_admin_password")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
wrong_password_response.status(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Wrong admin password should return 401"
|
||||
);
|
||||
|
||||
// 3.1 Now call the admin endpoint again, this time with the correct password.
|
||||
let admin_response = client
|
||||
.get(&admin_url)
|
||||
.header("X-Admin-Password", admin_password)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
admin_response.status(),
|
||||
StatusCode::OK,
|
||||
"Admin endpoint should return OK"
|
||||
);
|
||||
let valid_token = admin_response.text().await.unwrap(); // The token string.
|
||||
|
||||
// 4. Now signup with the valid token. Expect success and a session back.
|
||||
let session = client
|
||||
.signup(&keypair, &server.public_key(), Some(&valid_token))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!session.pubky().to_string().is_empty(),
|
||||
"Session should contain a valid public key"
|
||||
);
|
||||
|
||||
// 5. Finally, sign in with the same keypair and verify that a session is returned.
|
||||
let signin_session = client.signin(&keypair).await.unwrap();
|
||||
assert_eq!(
|
||||
signin_session.pubky(),
|
||||
&keypair.public_key(),
|
||||
"Signed-in session should correspond to the same public key"
|
||||
);
|
||||
}
|
||||
|
||||
// This test verifies that when a signin happens immediately after signup,
|
||||
// the record is not republished on signin (its timestamp remains unchanged)
|
||||
// but when a signin happens after the record is “old” (in test, after 1 second),
|
||||
// the record is republished (its timestamp increases).
|
||||
#[tokio::test]
|
||||
async fn test_republish_on_signin() {
|
||||
// Setup the testnet and run a homeserver.
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
// Create a client that will republish conditionally if a record is older than 1 second
|
||||
let client = testnet
|
||||
.client_builder()
|
||||
.max_record_age(Duration::from_secs(1))
|
||||
.build()
|
||||
.unwrap();
|
||||
let keypair = Keypair::random();
|
||||
|
||||
// Signup publishes a new record.
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
// Resolve the record and get its timestamp.
|
||||
let record1 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts1 = record1.timestamp().as_u64();
|
||||
|
||||
// Immediately sign in. This spawns a background task to update the record
|
||||
// with PublishStrategy::IfOlderThan.
|
||||
client.signin(&keypair).await.unwrap();
|
||||
// Wait a short time to let the background task complete.
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
let record2 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts2 = record2.timestamp().as_u64();
|
||||
|
||||
// Because the record is fresh (less than 1 second old in our test configuration),
|
||||
// the background task should not republish it. The timestamp should remain the same.
|
||||
assert_eq!(
|
||||
ts1, ts2,
|
||||
"Record republished too early; timestamps should be equal"
|
||||
);
|
||||
|
||||
// Wait long enough for the record to be considered 'old' (greater than 1 second).
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
// Sign in again. Now the background task should trigger a republish.
|
||||
client.signin(&keypair).await.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
let record3 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts3 = record3.timestamp().as_u64();
|
||||
|
||||
// Now the republished record's timestamp should be greater than before.
|
||||
assert!(
|
||||
ts3 > ts2,
|
||||
"Record was not republished after threshold exceeded"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_republish_homeserver() {
|
||||
// Setup the testnet and run a homeserver.
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
// Create a client that will republish conditionally if a record is older than 1 second
|
||||
let client = testnet
|
||||
.client_builder()
|
||||
.max_record_age(Duration::from_secs(1))
|
||||
.build()
|
||||
.unwrap();
|
||||
let keypair = Keypair::random();
|
||||
|
||||
// Signup publishes a new record.
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
// Resolve the record and get its timestamp.
|
||||
let record1 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts1 = record1.timestamp().as_u64();
|
||||
|
||||
// Immediately call republish_homeserver.
|
||||
// Since the record is fresh, republish should do nothing.
|
||||
client
|
||||
.republish_homeserver(&keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let record2 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts2 = record2.timestamp().as_u64();
|
||||
assert_eq!(
|
||||
ts1, ts2,
|
||||
"Record republished too early; timestamp should be equal"
|
||||
);
|
||||
|
||||
// Wait long enough for the record to be considered 'old'.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
// Call republish_homeserver again; now the record should be updated.
|
||||
client
|
||||
.republish_homeserver(&keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let record3 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts3 = record3.timestamp().as_u64();
|
||||
assert!(
|
||||
ts3 > ts2,
|
||||
"Record was not republished after threshold exceeded"
|
||||
);
|
||||
}
|
||||
32
e2e/src/tests/http.rs
Normal file
32
e2e/src/tests/http.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use pubky_testnet::Testnet;
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get_pubky() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let homeserver = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let response = client
|
||||
.get(format!("https://{}/", homeserver.public_key()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get_icann() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let response = client
|
||||
.request(Default::default(), "https://example.com/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
3
e2e/src/tests/mod.rs
Normal file
3
e2e/src/tests/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod auth;
|
||||
mod http;
|
||||
mod public;
|
||||
794
e2e/src/tests/public.rs
Normal file
794
e2e/src/tests/public.rs
Normal file
@@ -0,0 +1,794 @@
|
||||
use bytes::Bytes;
|
||||
use pkarr::Keypair;
|
||||
use pubky_testnet::Testnet;
|
||||
use reqwest::{Method, StatusCode};
|
||||
|
||||
#[tokio::test]
|
||||
async fn put_get_delete() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
|
||||
client
|
||||
.delete(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unauthorized_put_delete() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let public_key = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{public_key}/pub/foo.txt");
|
||||
let url = url.as_str();
|
||||
|
||||
let other_client = testnet.client_builder().build().unwrap();
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client.delete(url).send().await.unwrap().status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/z.wrong/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/example.com/extra");
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().limit(2).send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
],
|
||||
"normal list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("cc-nested/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor(&format!("pubky://{pubky}/pub/example.com/a.txt"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a full url cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a leading / cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
],
|
||||
"reverse list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
],
|
||||
"reverse list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("d.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"reverse list with limit and cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_shallow() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/");
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
],
|
||||
"normal list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"normal list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.cursor("example.com/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
],
|
||||
"normal list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(3)
|
||||
.cursor("example.com/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
],
|
||||
"normal list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
],
|
||||
"reverse list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
],
|
||||
"reverse list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("file2")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
],
|
||||
"reverse list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("example.con/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"reverse list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_events() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
client.delete(url).send().await.unwrap();
|
||||
}
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let cursor;
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.con"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con"),
|
||||
format!("PUT pubky://{pubky}/pub/file"),
|
||||
format!("DEL pubky://{pubky}/pub/file"),
|
||||
format!("PUT pubky://{pubky}/pub/file2"),
|
||||
format!("DEL pubky://{pubky}/pub/file2"),
|
||||
format!("PUT pubky://{pubky}/pub/z.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/z.com/a.txt"),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_after_event() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/a.com/a.txt");
|
||||
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
let cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = response.bytes().await.unwrap();
|
||||
|
||||
assert_eq!(body.as_ref(), &[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dont_delete_shared_blobs() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let homeserver = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let homeserver_pubky = homeserver.public_key();
|
||||
|
||||
let user_1 = Keypair::random();
|
||||
let user_2 = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&user_1, &homeserver_pubky, None)
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.signup(&user_2, &homeserver_pubky, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user_1_id = user_1.public_key();
|
||||
let user_2_id = user_2.public_key();
|
||||
|
||||
let url_1 = format!("pubky://{user_1_id}/pub/pubky.app/file/file_1");
|
||||
let url_2 = format!("pubky://{user_2_id}/pub/pubky.app/file/file_1");
|
||||
|
||||
let file = vec![1];
|
||||
client.put(&url_1).body(file.clone()).send().await.unwrap();
|
||||
client.put(&url_2).body(file.clone()).send().await.unwrap();
|
||||
|
||||
// Delete file 1
|
||||
client
|
||||
.delete(url_1)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let blob = client
|
||||
.get(url_2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blob, file);
|
||||
|
||||
let feed_url = format!("https://{}/events/", homeserver.public_key());
|
||||
|
||||
let response = client
|
||||
.request(Method::GET, feed_url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
format!("PUT pubky://{user_2_id}/pub/pubky.app/file/file_1",),
|
||||
format!("DEL pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream() {
|
||||
// TODO: test better streaming API
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
let bytes = Bytes::from(vec![0; 1024 * 1024]);
|
||||
|
||||
client.put(url).body(bytes.clone()).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes);
|
||||
|
||||
client.delete(url).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
@@ -23,5 +23,4 @@ pubky = { version = "0.4.2", path = "../pubky" }
|
||||
pubky-common = { version = "0.3.1", path = "../pubky-common" }
|
||||
pubky-homeserver = { version = "0.1.2", path = "../pubky-homeserver" }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.3.19"
|
||||
|
||||
|
||||
@@ -34,4 +34,4 @@ If you need to run the testnet in a separate process, for example to test Pubky
|
||||
1. A local DHT with bootstrapping nodes: `&["localhost:6881"]`
|
||||
3. A Pkarr Relay running on port [15411](pubky_common::constants::testnet_ports::PKARR_RELAY)
|
||||
2. A Homeserver with address is hardcoded to `8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo`
|
||||
4. An HTTP relay running on port [15412](pubky_common::constants::testnet_ports::HTTP_RELAY)
|
||||
4. An HTTP relay running on port [15412](pubky_common::constants::testnet_ports::HTTP_RELAY)
|
||||
@@ -1,150 +1,3 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
//!
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
#![cfg_attr(any(), deny(clippy::unwrap_used))]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use http_relay::HttpRelay;
|
||||
use pubky::{ClientBuilder, Keypair};
|
||||
use pubky_common::timestamp::Timestamp;
|
||||
use pubky_homeserver::Homeserver;
|
||||
use url::Url;
|
||||
|
||||
/// A local test network for Pubky Core development.
|
||||
pub struct Testnet {
|
||||
dht: mainline::Testnet,
|
||||
relays: Vec<pkarr_relay::Relay>,
|
||||
}
|
||||
|
||||
impl Testnet {
|
||||
/// Run a new testnet.
|
||||
pub async fn run() -> Result<Self> {
|
||||
let dht = mainline::Testnet::new(3)?;
|
||||
|
||||
let mut testnet = Self {
|
||||
dht,
|
||||
relays: vec![],
|
||||
};
|
||||
|
||||
testnet.run_pkarr_relay().await?;
|
||||
|
||||
Ok(testnet)
|
||||
}
|
||||
|
||||
/// Create these components with hardcoded configurations:
|
||||
///
|
||||
/// 1. A local DHT with bootstrapping nodes: `&["localhost:6881"]`
|
||||
/// 3. A Pkarr Relay running on port [15411](pubky_common::constants::testnet_ports::PKARR_RELAY)
|
||||
/// 2. A Homeserver with address is hardcoded to `8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo`
|
||||
/// 4. An HTTP relay running on port [15412](pubky_common::constants::testnet_ports::HTTP_RELAY)
|
||||
pub async fn run_with_hardcoded_configurations() -> Result<Self> {
|
||||
let dht = mainline::Testnet::new(3)?;
|
||||
|
||||
dht.leak();
|
||||
|
||||
let storage = std::env::temp_dir().join(Timestamp::now().to_string());
|
||||
|
||||
let mut builder = pkarr_relay::Relay::builder();
|
||||
builder
|
||||
.http_port(15411)
|
||||
.storage(storage.clone())
|
||||
.disable_rate_limiter()
|
||||
.pkarr(|pkarr| {
|
||||
pkarr
|
||||
.request_timeout(Duration::from_millis(100))
|
||||
.bootstrap(&dht.bootstrap)
|
||||
.dht(|builder| {
|
||||
if !dht.bootstrap.first().unwrap().contains("6881") {
|
||||
builder.server_mode().port(6881);
|
||||
}
|
||||
|
||||
builder
|
||||
.bootstrap(&dht.bootstrap)
|
||||
.request_timeout(Duration::from_millis(200))
|
||||
})
|
||||
});
|
||||
let relay = unsafe { builder.run() }.await?;
|
||||
|
||||
let mut builder = Homeserver::builder();
|
||||
builder
|
||||
.keypair(Keypair::from_secret_key(&[0; 32]))
|
||||
.storage(storage)
|
||||
.bootstrap(&dht.bootstrap)
|
||||
.relays(&[relay.local_url()])
|
||||
.domain("localhost")
|
||||
.close_signups()
|
||||
.admin_password("admin".to_string());
|
||||
unsafe { builder.run().await }?;
|
||||
|
||||
HttpRelay::builder().http_port(15412).run().await?;
|
||||
|
||||
let testnet = Self {
|
||||
dht,
|
||||
relays: vec![relay],
|
||||
};
|
||||
|
||||
Ok(testnet)
|
||||
}
|
||||
|
||||
// === Getters ===
|
||||
|
||||
/// Returns a list of DHT bootstrapping nodes.
|
||||
pub fn bootstrap(&self) -> &[String] {
|
||||
&self.dht.bootstrap
|
||||
}
|
||||
|
||||
/// Returns a list of pkarr relays.
|
||||
pub fn relays(&self) -> Box<[Url]> {
|
||||
self.relays.iter().map(|r| r.local_url()).collect()
|
||||
}
|
||||
|
||||
// === Public Methods ===
|
||||
|
||||
/// Run a Pubky Homeserver
|
||||
pub async fn run_homeserver(&self) -> Result<Homeserver> {
|
||||
Homeserver::run_test(&self.dht.bootstrap).await
|
||||
}
|
||||
|
||||
/// Run a Pubky Homeserver that requires signup tokens
|
||||
pub async fn run_homeserver_with_signup_tokens(&self) -> Result<Homeserver> {
|
||||
Homeserver::run_test_with_signup_tokens(&self.dht.bootstrap).await
|
||||
}
|
||||
|
||||
/// Run an HTTP Relay
|
||||
pub async fn run_http_relay(&self) -> Result<HttpRelay> {
|
||||
HttpRelay::builder().run().await
|
||||
}
|
||||
|
||||
/// Create a [ClientBuilder] and configure it to use this local test network.
|
||||
pub fn client_builder(&self) -> ClientBuilder {
|
||||
let bootstrap = self.bootstrap();
|
||||
let relays = self.relays();
|
||||
|
||||
let mut builder = pubky::Client::builder();
|
||||
builder.pkarr(|builder| {
|
||||
builder
|
||||
.bootstrap(bootstrap)
|
||||
.relays(&relays)
|
||||
.expect("testnet relays should be valid urls")
|
||||
});
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
/// Run a new Pkarr relay.
|
||||
///
|
||||
/// You can access the list of relays at [Self::relays].
|
||||
pub async fn run_pkarr_relay(&mut self) -> Result<Url> {
|
||||
let relay = pkarr_relay::Relay::run_test(&self.dht).await?;
|
||||
|
||||
let url = relay.local_url();
|
||||
|
||||
self.relays.push(relay);
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
// Actual testnet exposed in the library
|
||||
mod testnet;
|
||||
pub use testnet::Testnet;
|
||||
|
||||
149
pubky-testnet/src/testnet.rs
Normal file
149
pubky-testnet/src/testnet.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
//!
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
#![cfg_attr(any(), deny(clippy::unwrap_used))]
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use http_relay::HttpRelay;
|
||||
use pubky::{ClientBuilder, Keypair};
|
||||
use pubky_common::timestamp::Timestamp;
|
||||
use pubky_homeserver::Homeserver;
|
||||
use url::Url;
|
||||
|
||||
/// A local test network for Pubky Core development.
|
||||
pub struct Testnet {
|
||||
dht: mainline::Testnet,
|
||||
relays: Vec<pkarr_relay::Relay>,
|
||||
}
|
||||
|
||||
impl Testnet {
|
||||
/// Run a new testnet.
|
||||
pub async fn run() -> Result<Self> {
|
||||
let dht = mainline::Testnet::new(3)?;
|
||||
|
||||
let mut testnet = Self {
|
||||
dht,
|
||||
relays: vec![],
|
||||
};
|
||||
|
||||
testnet.run_pkarr_relay().await?;
|
||||
|
||||
Ok(testnet)
|
||||
}
|
||||
|
||||
/// Create these components with hardcoded configurations:
|
||||
///
|
||||
/// 1. A local DHT with bootstrapping nodes: `&["localhost:6881"]`
|
||||
/// 3. A Pkarr Relay running on port [15411](pubky_common::constants::testnet_ports::PKARR_RELAY)
|
||||
/// 2. A Homeserver with address is hardcoded to `8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo`
|
||||
/// 4. An HTTP relay running on port [15412](pubky_common::constants::testnet_ports::HTTP_RELAY)
|
||||
pub async fn run_with_hardcoded_configurations() -> Result<Self> {
|
||||
let dht = mainline::Testnet::new(3)?;
|
||||
|
||||
dht.leak();
|
||||
|
||||
let storage = std::env::temp_dir().join(Timestamp::now().to_string());
|
||||
|
||||
let mut builder = pkarr_relay::Relay::builder();
|
||||
builder
|
||||
.http_port(15411)
|
||||
.storage(storage.clone())
|
||||
.disable_rate_limiter()
|
||||
.pkarr(|pkarr| {
|
||||
pkarr
|
||||
.request_timeout(Duration::from_millis(100))
|
||||
.bootstrap(&dht.bootstrap)
|
||||
.dht(|builder| {
|
||||
if !dht.bootstrap.first().unwrap().contains("6881") {
|
||||
builder.server_mode().port(6881);
|
||||
}
|
||||
|
||||
builder
|
||||
.bootstrap(&dht.bootstrap)
|
||||
.request_timeout(Duration::from_millis(200))
|
||||
})
|
||||
});
|
||||
let relay = unsafe { builder.run() }.await?;
|
||||
|
||||
let mut builder = Homeserver::builder();
|
||||
builder
|
||||
.keypair(Keypair::from_secret_key(&[0; 32]))
|
||||
.storage(storage)
|
||||
.bootstrap(&dht.bootstrap)
|
||||
.relays(&[relay.local_url()])
|
||||
.domain("localhost")
|
||||
.close_signups()
|
||||
.admin_password("admin".to_string());
|
||||
unsafe { builder.run().await }?;
|
||||
|
||||
HttpRelay::builder().http_port(15412).run().await?;
|
||||
|
||||
let testnet = Self {
|
||||
dht,
|
||||
relays: vec![relay],
|
||||
};
|
||||
|
||||
Ok(testnet)
|
||||
}
|
||||
|
||||
// === Getters ===
|
||||
|
||||
/// Returns a list of DHT bootstrapping nodes.
|
||||
pub fn bootstrap(&self) -> &[String] {
|
||||
&self.dht.bootstrap
|
||||
}
|
||||
|
||||
/// Returns a list of pkarr relays.
|
||||
pub fn relays(&self) -> Box<[Url]> {
|
||||
self.relays.iter().map(|r| r.local_url()).collect()
|
||||
}
|
||||
|
||||
// === Public Methods ===
|
||||
|
||||
/// Run a Pubky Homeserver
|
||||
pub async fn run_homeserver(&self) -> Result<Homeserver> {
|
||||
Homeserver::run_test(&self.dht.bootstrap).await
|
||||
}
|
||||
|
||||
/// Run a Pubky Homeserver that requires signup tokens
|
||||
pub async fn run_homeserver_with_signup_tokens(&self) -> Result<Homeserver> {
|
||||
Homeserver::run_test_with_signup_tokens(&self.dht.bootstrap).await
|
||||
}
|
||||
|
||||
/// Run an HTTP Relay
|
||||
pub async fn run_http_relay(&self) -> Result<HttpRelay> {
|
||||
HttpRelay::builder().run().await
|
||||
}
|
||||
|
||||
/// Create a [ClientBuilder] and configure it to use this local test network.
|
||||
pub fn client_builder(&self) -> ClientBuilder {
|
||||
let bootstrap = self.bootstrap();
|
||||
let relays = self.relays();
|
||||
|
||||
let mut builder = pubky::Client::builder();
|
||||
builder.pkarr(|builder| {
|
||||
builder
|
||||
.bootstrap(bootstrap)
|
||||
.relays(&relays)
|
||||
.expect("testnet relays should be valid urls")
|
||||
});
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
/// Run a new Pkarr relay.
|
||||
///
|
||||
/// You can access the list of relays at [Self::relays].
|
||||
pub async fn run_pkarr_relay(&mut self) -> Result<Url> {
|
||||
let relay = pkarr_relay::Relay::run_test(&self.dht).await?;
|
||||
|
||||
let url = relay.local_url();
|
||||
|
||||
self.relays.push(relay);
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
@@ -381,457 +381,9 @@ impl AuthRequest {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pkarr::Keypair;
|
||||
use pubky_common::capabilities::{Capabilities, Capability};
|
||||
use pubky_testnet::Testnet;
|
||||
use reqwest::StatusCode;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{native::internal::pkarr::PublishStrategy, Client};
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_authn() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
client.signout(&keypair.public_key()).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client.session(&keypair.public_key()).await.unwrap();
|
||||
|
||||
assert!(session.is_none());
|
||||
}
|
||||
|
||||
client.signin(&keypair).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authz() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let http_relay = testnet.run_http_relay().await.unwrap();
|
||||
let http_relay_url = http_relay.local_link_url();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
// Third party app side
|
||||
let capabilities: Capabilities =
|
||||
"/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let pubky_auth_request = client.auth_request(http_relay_url, &capabilities).unwrap();
|
||||
|
||||
// Authenticator side
|
||||
{
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.send_auth_token(&keypair, pubky_auth_request.url())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let public_key = pubky_auth_request.response().await.unwrap();
|
||||
|
||||
assert_eq!(&public_key, &pubky);
|
||||
|
||||
let session = client.session(&pubky).await.unwrap().unwrap();
|
||||
assert_eq!(session.capabilities(), &capabilities.0);
|
||||
|
||||
// Test access control enforcement
|
||||
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app/foo"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/foo.bar/file"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_users() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let first_keypair = Keypair::random();
|
||||
let second_keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&first_keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.signup(&second_keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&first_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &first_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
let session = client
|
||||
.session(&second_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &second_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authz_timeout_reconnect() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let http_relay = testnet.run_http_relay().await.unwrap();
|
||||
let http_relay_url = http_relay.local_link_url();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
// Third party app side
|
||||
let capabilities: Capabilities =
|
||||
"/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap();
|
||||
|
||||
let client = testnet
|
||||
.client_builder()
|
||||
.request_timeout(Duration::from_millis(1000))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let pubky_auth_request = client.auth_request(http_relay_url, &capabilities).unwrap();
|
||||
|
||||
// Authenticator side
|
||||
{
|
||||
let url = pubky_auth_request.url().clone();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(400)).await;
|
||||
// loop {
|
||||
client.send_auth_token(&keypair, &url).await.unwrap();
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
let public_key = pubky_auth_request.response().await.unwrap();
|
||||
|
||||
assert_eq!(&public_key, &pubky);
|
||||
|
||||
let session = client.session(&pubky).await.unwrap().unwrap();
|
||||
assert_eq!(session.capabilities(), &capabilities.0);
|
||||
|
||||
// Test access control enforcement
|
||||
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app/foo"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/foo.bar/file"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_signup_with_token() {
|
||||
// 1. Start a test homeserver with closed signups (i.e. signup tokens required)
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver_with_signup_tokens().await.unwrap();
|
||||
|
||||
let admin_password = "admin";
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
let keypair = Keypair::random();
|
||||
|
||||
// 2. Try to signup with an invalid token "AAAAA" and expect failure.
|
||||
let invalid_signup = client
|
||||
.signup(&keypair, &server.public_key(), Some("AAAA-BBBB-CCCC"))
|
||||
.await;
|
||||
assert!(
|
||||
invalid_signup.is_err(),
|
||||
"Signup should fail with an invalid signup token"
|
||||
);
|
||||
|
||||
// 3. Call the admin endpoint to generate a valid signup token.
|
||||
// The admin endpoint is protected via the header "X-Admin-Password"
|
||||
// and the password we set up above.
|
||||
let admin_url = format!(
|
||||
"https://{}/admin/generate_signup_token",
|
||||
server.public_key()
|
||||
);
|
||||
|
||||
// 3.1. Call the admin endpoint *with a WRONG admin password* to ensure we get 401 UNAUTHORIZED.
|
||||
let wrong_password_response = client
|
||||
.get(&admin_url)
|
||||
.header("X-Admin-Password", "wrong_admin_password")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
wrong_password_response.status(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Wrong admin password should return 401"
|
||||
);
|
||||
|
||||
// 3.1 Now call the admin endpoint again, this time with the correct password.
|
||||
let admin_response = client
|
||||
.get(&admin_url)
|
||||
.header("X-Admin-Password", admin_password)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
admin_response.status(),
|
||||
StatusCode::OK,
|
||||
"Admin endpoint should return OK"
|
||||
);
|
||||
let valid_token = admin_response.text().await.unwrap(); // The token string.
|
||||
|
||||
// 4. Now signup with the valid token. Expect success and a session back.
|
||||
let session = client
|
||||
.signup(&keypair, &server.public_key(), Some(&valid_token))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!session.pubky().to_string().is_empty(),
|
||||
"Session should contain a valid public key"
|
||||
);
|
||||
|
||||
// 5. Finally, sign in with the same keypair and verify that a session is returned.
|
||||
let signin_session = client.signin(&keypair).await.unwrap();
|
||||
assert_eq!(
|
||||
signin_session.pubky(),
|
||||
&keypair.public_key(),
|
||||
"Signed-in session should correspond to the same public key"
|
||||
);
|
||||
}
|
||||
|
||||
// This test verifies that when a signin happens immediately after signup,
|
||||
// the record is not republished on signin (its timestamp remains unchanged)
|
||||
// but when a signin happens after the record is “old” (in test, after 1 second),
|
||||
// the record is republished (its timestamp increases).
|
||||
#[tokio::test]
|
||||
async fn test_republish_on_signin() {
|
||||
// Setup the testnet and run a homeserver.
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
// Create a client that will republish conditionally if a record is older than 1 second
|
||||
let client = testnet
|
||||
.client_builder()
|
||||
.max_record_age(Duration::from_secs(1))
|
||||
.build()
|
||||
.unwrap();
|
||||
let keypair = Keypair::random();
|
||||
|
||||
// Signup publishes a new record.
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
// Resolve the record and get its timestamp.
|
||||
let record1 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts1 = record1.timestamp().as_u64();
|
||||
|
||||
// Immediately sign in. This spawns a background task to update the record
|
||||
// with PublishStrategy::IfOlderThan.
|
||||
client.signin(&keypair).await.unwrap();
|
||||
// Wait a short time to let the background task complete.
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
let record2 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts2 = record2.timestamp().as_u64();
|
||||
|
||||
// Because the record is fresh (less than 1 second old in our test configuration),
|
||||
// the background task should not republish it. The timestamp should remain the same.
|
||||
assert_eq!(
|
||||
ts1, ts2,
|
||||
"Record republished too early; timestamps should be equal"
|
||||
);
|
||||
|
||||
// Wait long enough for the record to be considered 'old' (greater than 1 second).
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
// Sign in again. Now the background task should trigger a republish.
|
||||
client.signin(&keypair).await.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
let record3 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts3 = record3.timestamp().as_u64();
|
||||
|
||||
// Now the republished record's timestamp should be greater than before.
|
||||
assert!(
|
||||
ts3 > ts2,
|
||||
"Record was not republished after threshold exceeded"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_republish_homeserver() {
|
||||
// Setup the testnet and run a homeserver.
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
// Create a client that will republish conditionally if a record is older than 1 second
|
||||
let client = testnet
|
||||
.client_builder()
|
||||
.max_record_age(Duration::from_secs(1))
|
||||
.build()
|
||||
.unwrap();
|
||||
let keypair = Keypair::random();
|
||||
|
||||
// Signup publishes a new record.
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
// Resolve the record and get its timestamp.
|
||||
let record1 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts1 = record1.timestamp().as_u64();
|
||||
|
||||
// Immediately call republish_homeserver.
|
||||
// Since the record is fresh, republish should do nothing.
|
||||
client
|
||||
.republish_homeserver(&keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let record2 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts2 = record2.timestamp().as_u64();
|
||||
assert_eq!(
|
||||
ts1, ts2,
|
||||
"Record republished too early; timestamp should be equal"
|
||||
);
|
||||
|
||||
// Wait long enough for the record to be considered 'old'.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
// Call republish_homeserver again; now the record should be updated.
|
||||
client
|
||||
.republish_homeserver(&keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let record3 = client
|
||||
.pkarr()
|
||||
.resolve_most_recent(&keypair.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
let ts3 = record3.timestamp().as_u64();
|
||||
assert!(
|
||||
ts3 > ts2,
|
||||
"Record was not republished after threshold exceeded"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_homeserver() {
|
||||
let dht = mainline::Testnet::new(3).unwrap();
|
||||
|
||||
@@ -133,39 +133,3 @@ impl Client {
|
||||
self.request(method, url)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pubky_testnet::Testnet;
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get_pubky() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let homeserver = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let response = client
|
||||
.get(format!("https://{}/", homeserver.public_key()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get_icann() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let response = client
|
||||
.request(Default::default(), "https://example.com/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,801 +121,3 @@ impl<'a> ListBuilder<'a> {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bytes::Bytes;
|
||||
use pkarr::Keypair;
|
||||
use pubky_testnet::Testnet;
|
||||
use reqwest::{Method, StatusCode};
|
||||
|
||||
#[tokio::test]
|
||||
async fn put_get_delete() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
|
||||
client
|
||||
.delete(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unauthorized_put_delete() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let public_key = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{public_key}/pub/foo.txt");
|
||||
let url = url.as_str();
|
||||
|
||||
let other_client = testnet.client_builder().build().unwrap();
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client.delete(url).send().await.unwrap().status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/z.wrong/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/example.com/extra");
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().limit(2).send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
],
|
||||
"normal list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("cc-nested/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor(&format!("pubky://{pubky}/pub/example.com/a.txt"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a full url cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a leading / cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
],
|
||||
"reverse list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
],
|
||||
"reverse list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("d.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"reverse list with limit and cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_shallow() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/");
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
],
|
||||
"normal list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"normal list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.cursor("example.com/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
],
|
||||
"normal list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(3)
|
||||
.cursor("example.com/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
],
|
||||
"normal list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
],
|
||||
"reverse list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
],
|
||||
"reverse list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("file2")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
],
|
||||
"reverse list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("example.con/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"reverse list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_events() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
client.delete(url).send().await.unwrap();
|
||||
}
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let cursor;
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.con"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con"),
|
||||
format!("PUT pubky://{pubky}/pub/file"),
|
||||
format!("DEL pubky://{pubky}/pub/file"),
|
||||
format!("PUT pubky://{pubky}/pub/file2"),
|
||||
format!("DEL pubky://{pubky}/pub/file2"),
|
||||
format!("PUT pubky://{pubky}/pub/z.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/z.com/a.txt"),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_after_event() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/a.com/a.txt");
|
||||
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
let cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = response.bytes().await.unwrap();
|
||||
|
||||
assert_eq!(body.as_ref(), &[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dont_delete_shared_blobs() {
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let homeserver = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let homeserver_pubky = homeserver.public_key();
|
||||
|
||||
let user_1 = Keypair::random();
|
||||
let user_2 = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&user_1, &homeserver_pubky, None)
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.signup(&user_2, &homeserver_pubky, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user_1_id = user_1.public_key();
|
||||
let user_2_id = user_2.public_key();
|
||||
|
||||
let url_1 = format!("pubky://{user_1_id}/pub/pubky.app/file/file_1");
|
||||
let url_2 = format!("pubky://{user_2_id}/pub/pubky.app/file/file_1");
|
||||
|
||||
let file = vec![1];
|
||||
client.put(&url_1).body(file.clone()).send().await.unwrap();
|
||||
client.put(&url_2).body(file.clone()).send().await.unwrap();
|
||||
|
||||
// Delete file 1
|
||||
client
|
||||
.delete(url_1)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let blob = client
|
||||
.get(url_2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blob, file);
|
||||
|
||||
let feed_url = format!("https://{}/events/", homeserver.public_key());
|
||||
|
||||
let response = client
|
||||
.request(Method::GET, feed_url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
format!("PUT pubky://{user_2_id}/pub/pubky.app/file/file_1",),
|
||||
format!("DEL pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream() {
|
||||
// TODO: test better streaming API
|
||||
let testnet = Testnet::run().await.unwrap();
|
||||
let server = testnet.run_homeserver().await.unwrap();
|
||||
|
||||
let client = testnet.client_builder().build().unwrap();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&keypair, &server.public_key(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
let bytes = Bytes::from(vec![0; 1024 * 1024]);
|
||||
|
||||
client.put(url).body(bytes.clone()).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes);
|
||||
|
||||
client.delete(url).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user