From ca0995cb23dd88929316dff99b49b5e2ee48a925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Severin=20Alexander=20B=C3=BChler?= <8782386+SeverinAlexB@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:15:33 +0200 Subject: [PATCH] chore: Moved e2e tests / Fixes circular dependency (#88) * moved e2e tests * moved e2e tests to its own workspace member * fmt --- .github/workflows/pr-check.yml | 2 +- Cargo.lock | 118 ++++- Cargo.toml | 4 +- e2e/Cargo.toml | 13 + e2e/README.md | 17 + e2e/src/lib.rs | 3 + e2e/src/tests/auth.rs | 447 ++++++++++++++++++ e2e/src/tests/http.rs | 32 ++ e2e/src/tests/mod.rs | 3 + e2e/src/tests/public.rs | 794 ++++++++++++++++++++++++++++++++ pubky-testnet/Cargo.toml | 3 +- pubky-testnet/README.md | 2 +- pubky-testnet/src/lib.rs | 153 +------ pubky-testnet/src/testnet.rs | 149 ++++++ pubky/src/native/api/auth.rs | 448 ------------------ pubky/src/native/api/http.rs | 36 -- pubky/src/native/api/public.rs | 798 --------------------------------- 17 files changed, 1566 insertions(+), 1456 deletions(-) create mode 100644 e2e/Cargo.toml create mode 100644 e2e/README.md create mode 100644 e2e/src/lib.rs create mode 100644 e2e/src/tests/auth.rs create mode 100644 e2e/src/tests/http.rs create mode 100644 e2e/src/tests/mod.rs create mode 100644 e2e/src/tests/public.rs create mode 100644 pubky-testnet/src/testnet.rs diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 0d54f3b..dc186d8 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 9a7bfdd..ad75cb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 661a42d..99ec038 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml new file mode 100644 index 0000000..37367ee --- /dev/null +++ b/e2e/Cargo.toml @@ -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" \ No newline at end of file diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..66ec527 --- /dev/null +++ b/e2e/README.md @@ -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. \ No newline at end of file diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs new file mode 100644 index 0000000..e695d0d --- /dev/null +++ b/e2e/src/lib.rs @@ -0,0 +1,3 @@ +// E2E tests +#[cfg(test)] +mod tests; diff --git a/e2e/src/tests/auth.rs b/e2e/src/tests/auth.rs new file mode 100644 index 0000000..abd831d --- /dev/null +++ b/e2e/src/tests/auth.rs @@ -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" + ); +} diff --git a/e2e/src/tests/http.rs b/e2e/src/tests/http.rs new file mode 100644 index 0000000..d32fd75 --- /dev/null +++ b/e2e/src/tests/http.rs @@ -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); +} diff --git a/e2e/src/tests/mod.rs b/e2e/src/tests/mod.rs new file mode 100644 index 0000000..af911fc --- /dev/null +++ b/e2e/src/tests/mod.rs @@ -0,0 +1,3 @@ +mod auth; +mod http; +mod public; diff --git a/e2e/src/tests/public.rs b/e2e/src/tests/public.rs new file mode 100644 index 0000000..18e1906 --- /dev/null +++ b/e2e/src/tests/public.rs @@ -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::>(); + + 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::>(); + + 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::>(); + + 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::>(); + + 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); +} diff --git a/pubky-testnet/Cargo.toml b/pubky-testnet/Cargo.toml index 7aa9251..2ea71c6 100644 --- a/pubky-testnet/Cargo.toml +++ b/pubky-testnet/Cargo.toml @@ -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" + diff --git a/pubky-testnet/README.md b/pubky-testnet/README.md index 44eb00f..190f5ba 100644 --- a/pubky-testnet/README.md +++ b/pubky-testnet/README.md @@ -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) \ No newline at end of file diff --git a/pubky-testnet/src/lib.rs b/pubky-testnet/src/lib.rs index 7a9b75b..ef8cd3d 100644 --- a/pubky-testnet/src/lib.rs +++ b/pubky-testnet/src/lib.rs @@ -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, -} - -impl Testnet { - /// Run a new testnet. - pub async fn run() -> Result { - 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 { - 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::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::run_test_with_signup_tokens(&self.dht.bootstrap).await - } - - /// Run an HTTP Relay - pub async fn run_http_relay(&self) -> Result { - 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 { - 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; diff --git a/pubky-testnet/src/testnet.rs b/pubky-testnet/src/testnet.rs new file mode 100644 index 0000000..8b1b533 --- /dev/null +++ b/pubky-testnet/src/testnet.rs @@ -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, +} + +impl Testnet { + /// Run a new testnet. + pub async fn run() -> Result { + 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 { + 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::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::run_test_with_signup_tokens(&self.dht.bootstrap).await + } + + /// Run an HTTP Relay + pub async fn run_http_relay(&self) -> Result { + 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 { + let relay = pkarr_relay::Relay::run_test(&self.dht).await?; + + let url = relay.local_url(); + + self.relays.push(relay); + + Ok(url) + } +} diff --git a/pubky/src/native/api/auth.rs b/pubky/src/native/api/auth.rs index a7d644b..2483479 100644 --- a/pubky/src/native/api/auth.rs +++ b/pubky/src/native/api/auth.rs @@ -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(); diff --git a/pubky/src/native/api/http.rs b/pubky/src/native/api/http.rs index 1cf4b0e..9ab02f9 100644 --- a/pubky/src/native/api/http.rs +++ b/pubky/src/native/api/http.rs @@ -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); - } -} diff --git a/pubky/src/native/api/public.rs b/pubky/src/native/api/public.rs index 0b2596f..aa24a2f 100644 --- a/pubky/src/native/api/public.rs +++ b/pubky/src/native/api/public.rs @@ -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::>(); - - 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::>(); - - 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::>(); - - 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::>(); - - 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); - } -}