Compare commits

..
1 Commits
Author SHA1 Message Date
Lewis 17b3338b31 tranquil-store: make it seem a little less 'experimental'
Lewis: May this revision serve well! <did:plc:3fwecdnvtcscjnrx2p4n7alz>
2026-09-22 21:10:36 +03:00
108 changed files with 1831 additions and 2125 deletions
+33 -9
View File
@@ -53,15 +53,27 @@ heavy-load-tests = { max-threads = 4 }
io-heavy-sim = { max-threads = 2 } io-heavy-sim = { max-threads = 2 }
[[profile.default.overrides]] [[profile.default.overrides]]
filter = "binary(it) and test(/^ripple_cluster::/)" filter = "test(/import_with_verification/) | test(/plc_migration/)"
test-group = "serial-env-tests" test-group = "serial-env-tests"
[[profile.default.overrides]] [[profile.default.overrides]]
filter = "package(tranquil-signal) and test(/^tests::/)" filter = "binary(handle_domains)"
test-group = "serial-env-tests" test-group = "serial-env-tests"
[[profile.default.overrides]] [[profile.default.overrides]]
filter = "binary(it) and test(/^whole_story::/)" filter = "binary(ripple_cluster)"
test-group = "serial-env-tests"
[[profile.default.overrides]]
filter = "package(tranquil-signal)"
test-group = "serial-env-tests"
[[profile.default.overrides]]
filter = "package(tranquil-config)"
test-group = "serial-env-tests"
[[profile.default.overrides]]
filter = "binary(whole_story)"
test-group = "heavy-load-tests" test-group = "heavy-load-tests"
[[profile.default.overrides]] [[profile.default.overrides]]
@@ -69,7 +81,7 @@ filter = "test(/two_node_stress_concurrent_load/)"
test-group = "heavy-load-tests" test-group = "heavy-load-tests"
[[profile.default.overrides]] [[profile.default.overrides]]
filter = "binary(it) and test(/^repo_lifecycle::/)" filter = "binary(repo_lifecycle)"
test-group = "heavy-load-tests" test-group = "heavy-load-tests"
[[profile.default.overrides]] [[profile.default.overrides]]
@@ -91,7 +103,7 @@ filter = "binary(gauntlet_smoke)"
slow-timeout = { period = "300s", terminate-after = 8 } slow-timeout = { period = "300s", terminate-after = 8 }
[[profile.default.overrides]] [[profile.default.overrides]]
filter = "binary(compaction_restart) | binary(mst_refcount_integrity) | binary(it) and test(/^gc_compaction_restart::/)" filter = "binary(compaction_restart) | binary(mst_refcount_integrity) | binary(gc_compaction_restart)"
slow-timeout = { period = "120s", terminate-after = 4 } slow-timeout = { period = "120s", terminate-after = 4 }
[[profile.default.overrides]] [[profile.default.overrides]]
@@ -105,15 +117,27 @@ slow-timeout = { period = "300s", terminate-after = 4 }
test-group = "io-heavy-sim" test-group = "io-heavy-sim"
[[profile.ci.overrides]] [[profile.ci.overrides]]
filter = "binary(it) and test(/^ripple_cluster::/)" filter = "test(/import_with_verification/) | test(/plc_migration/)"
test-group = "serial-env-tests" test-group = "serial-env-tests"
[[profile.ci.overrides]] [[profile.ci.overrides]]
filter = "package(tranquil-signal) and test(/^tests::/)" filter = "binary(handle_domains)"
test-group = "serial-env-tests" test-group = "serial-env-tests"
[[profile.ci.overrides]] [[profile.ci.overrides]]
filter = "binary(it) and test(/^whole_story::/)" filter = "binary(ripple_cluster)"
test-group = "serial-env-tests"
[[profile.ci.overrides]]
filter = "package(tranquil-signal)"
test-group = "serial-env-tests"
[[profile.ci.overrides]]
filter = "package(tranquil-config)"
test-group = "serial-env-tests"
[[profile.ci.overrides]]
filter = "binary(whole_story)"
test-group = "heavy-load-tests" test-group = "heavy-load-tests"
[[profile.ci.overrides]] [[profile.ci.overrides]]
@@ -121,7 +145,7 @@ filter = "test(/two_node_stress_concurrent_load/)"
test-group = "heavy-load-tests" test-group = "heavy-load-tests"
[[profile.ci.overrides]] [[profile.ci.overrides]]
filter = "binary(it) and test(/^repo_lifecycle::/)" filter = "binary(repo_lifecycle)"
test-group = "heavy-load-tests" test-group = "heavy-load-tests"
[[profile.ci.overrides]] [[profile.ci.overrides]]
-25
View File
@@ -1,25 +0,0 @@
when:
- event: ["push", "pull_request"]
branch: ["main"]
engine: microvm
image: nixos
environment:
CARGO_INCREMENTAL: "0"
dependencies:
- rustup
- gcc
- pkg-config
- openssl
- protobuf
- just
steps:
- name: fmt
command: cargo fmt -- --check
- name: check
command: cargo check --all-targets
- name: clippy
command: just clippy
@@ -0,0 +1,21 @@
when:
- event: ["push", "manual"]
branch: ["main"]
engine: nixery
dependencies:
nixpkgs:
- nix
- cachix
- gnugrep
steps:
- name: Authenticate
command: |
cachix authtoken "$CACHIX_AUTH_TOKEN"
- name: Build and push aarch64
command: |
SUBS="--option extra-substituters https://tranquil.cachix.org --option extra-trusted-public-keys tranquil.cachix.org-1:PoO+mGL6a6LcJiPakMDHN4E218/ei/7v2sxeDtNkSRg="
nix-store -qR --include-outputs $(nix-store -qd $(nix build .#packages.x86_64-linux.tranquil-pds-aarch64 $SUBS --print-out-paths --no-link)) | grep -v '\.drv$' | cachix push tranquil
@@ -0,0 +1,28 @@
when:
- event: ["push", "manual"]
branch: ["main"]
engine: nixery
dependencies:
nixpkgs:
- nix
- cachix
- gnugrep
steps:
- name: Authenticate
command: |
cachix authtoken "$CACHIX_AUTH_TOKEN"
- name: Build and push x86_64
command: |
SUBS="--option extra-substituters https://tranquil.cachix.org --option extra-trusted-public-keys tranquil.cachix.org-1:PoO+mGL6a6LcJiPakMDHN4E218/ei/7v2sxeDtNkSRg="
nix-store -qR --include-outputs $(nix-store -qd $(nix build .#packages.x86_64-linux.tranquil-pds $SUBS --print-out-paths --no-link)) | grep -v '\.drv$' | cachix push tranquil
nix-store -qR --include-outputs $(nix-store -qd $(nix build .#packages.x86_64-linux.tranquil-frontend $SUBS --print-out-paths --no-link)) | grep -v '\.drv$' | cachix push tranquil
- name: Build and push devShell
command: |
SUBS="--option extra-substituters https://tranquil.cachix.org --option extra-trusted-public-keys tranquil.cachix.org-1:PoO+mGL6a6LcJiPakMDHN4E218/ei/7v2sxeDtNkSRg="
nix develop $SUBS --profile dev-profile -c true
cachix push tranquil dev-profile
+24 -20
View File
@@ -1,32 +1,36 @@
when: when:
- event: [ "manual" ]
- event: [ "push" ] - event: [ "push" ]
branch: [ "main" ] branch: [ "main" ]
engine: nixery engine: nixery
environment:
KANIKO_IMAGE: ghcr.io/osscontainertools/kaniko:v1.28.3@sha256:779f463aaa3219151ceff518249aa043a60bcb09a17fab58b915f63c678ecde6
dependencies: dependencies:
nixpkgs: nixpkgs:
- go-containerregistry - kaniko
- gnutar
environment:
DOCKER_CONFIG: "/kaniko/.docker"
steps: steps:
- name: build and push - name: Configure Kaniko
command: | command: |
set -euo pipefail mkdir -p /kaniko/.docker/
crane export --platform linux/amd64 "$KANIKO_IMAGE" - | tar -xf - -C /tmp kaniko/executor echo "{
export DOCKER_CONFIG=/tmp/docker-config \"auths\": {
mkdir -p "$DOCKER_CONFIG" \"https://atcr.io/v1\":{
echo "{\"auths\":{\"https://atcr.io/v1\":{\"auth\":\"$ATCR_CREDENTIALS\"}}}" > "$DOCKER_CONFIG/config.json" \"auth\": \"$ATCR_CREDENTIALS\"
/tmp/kaniko/executor \ }
--context "$(pwd)" \ }
--dockerfile "$(pwd)/Dockerfile" \ }" > /kaniko/.docker/config.json
--ignore-path "$(pwd)" \
--ignore-path /tangled \ - name: Build image
--ignore-path /nix \ command: |
--ignore-path /tmp/docker-config \ executor \
--destination "atcr.io/tranquil.farm/tranquil-pds:$TANGLED_COMMIT_SHA" \ --context=$(pwd) \
--ignore-path=$(pwd) \
--dockerfile=$(pwd)/Dockerfile \
--destination="atcr.io/tranquil.farm/tranquil-pds:latest" \
--destination="atcr.io/tranquil.farm/tranquil-pds:$TANGLED_COMMIT_SHA" \
--push-retry=3 \ --push-retry=3 \
--build-arg CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 --skip-push-permission-check
-25
View File
@@ -1,25 +0,0 @@
when:
- event: ["push", "pull_request"]
branch: ["main"]
engine: microvm
image: nixos
environment:
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
TMPDIR: "/dev/shm"
dependencies:
- rustup
- gcc
- pkg-config
- openssl
- protobuf
- just
- cargo-nextest
steps:
- name: test
command: just test
Generated
+1091 -1200
View File
File diff suppressed because it is too large Load Diff
+3 -21
View File
@@ -63,6 +63,8 @@ backon = "1"
bincode = { version = "2", features = ["serde"] } bincode = { version = "2", features = ["serde"] }
anyhow = "1.0" anyhow = "1.0"
async-trait = "0.1" async-trait = "0.1"
aws-config = "1.8"
aws-sdk-s3 = "1.118"
axum = { version = "0.8", features = ["ws", "macros"] } axum = { version = "0.8", features = ["ws", "macros"] }
base32 = "0.5" base32 = "0.5"
base64 = "0.22" base64 = "0.22"
@@ -78,7 +80,7 @@ ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
foca = { version = "1", features = ["bincode-codec", "tracing"] } foca = { version = "1", features = ["bincode-codec", "tracing"] }
futures = "0.3" futures = "0.3"
futures-util = "0.3" futures-util = "0.3"
governor = { version = "0.10", default-features = false, features = ["std", "dashmap", "jitter"] } governor = "0.10"
h3 = "0.0.8" h3 = "0.0.8"
h3-quinn = "0.0.10" h3-quinn = "0.0.10"
hex = "0.4" hex = "0.4"
@@ -113,9 +115,6 @@ regex = "1"
rsa = "0.9" rsa = "0.9"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
rustls-pemfile = "2" rustls-pemfile = "2"
s3 = { version = "0.1.36", features = ["multipart"] }
s3s = "0.17"
s3s-fs = "0.17"
secrecy = { version = "0.10", features = ["serde"] } secrecy = { version = "0.10", features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-webpki-roots", "http2", "charset", "macos-system-configuration"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-webpki-roots", "http2", "charset", "macos-system-configuration"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
@@ -171,20 +170,3 @@ lto = "fat"
codegen-units = 1 codegen-units = 1
debug = 1 debug = 1
strip = false strip = false
# Optimize all the hashing so it runs faster in tests.
[profile.dev.package.bcrypt]
opt-level = 3
[profile.dev.package.blowfish]
opt-level = 3
[profile.dev.package.sha2]
opt-level = 3
[profile.dev.package.md-5]
opt-level = 3
# Set the default to optimized so fortify works in CI.
[profile.dev.package.tikv-jemalloc-sys]
opt-level = 1
-1
View File
@@ -33,7 +33,6 @@ RUN mkdir -p /stage/var/lib/tranquil-pds/blobs /stage/var/lib/tranquil-pds/store
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
WORKDIR /app WORKDIR /app
ARG SLIM="false" ARG SLIM="false"
ARG CARGO_PROFILE_RELEASE_CODEGEN_UNITS
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY .sqlx ./.sqlx COPY .sqlx ./.sqlx
COPY crates/tranquil-types ./crates/tranquil-types COPY crates/tranquil-types ./crates/tranquil-types
-1
View File
@@ -51,4 +51,3 @@ webauthn-rs = { workspace = true }
[features] [features]
bsky = ["bsky-support"] bsky = ["bsky-support"]
bsky-support = [] bsky-support = []
low-bcrypt-cost = []
+3 -9
View File
@@ -1,4 +1,4 @@
use bcrypt::hash; use bcrypt::{DEFAULT_COST, hash};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use std::collections::HashMap; use std::collections::HashMap;
use tracing::{error, warn}; use tracing::{error, warn};
@@ -8,12 +8,6 @@ use tranquil_pds::api::error::DbResultExt;
use tranquil_pds::types::{AtIdentifier, Did, Handle, PasswordHash}; use tranquil_pds::types::{AtIdentifier, Did, Handle, PasswordHash};
use tranquil_types::{DiscordUsername, SignalUsername, TelegramUsername}; use tranquil_types::{DiscordUsername, SignalUsername, TelegramUsername};
#[cfg(not(feature = "low-bcrypt-cost"))]
const PASSWORD_HASH_COST: u32 = bcrypt::DEFAULT_COST;
// Use a lower bcrypt cost in tests.
#[cfg(feature = "low-bcrypt-cost")]
const PASSWORD_HASH_COST: u32 = 4;
pub struct ResolvedRepo { pub struct ResolvedRepo {
pub user_id: uuid::Uuid, pub user_id: uuid::Uuid,
pub did: Did, pub did: Did,
@@ -284,7 +278,7 @@ pub async fn verify_credential(
} }
pub fn hash_or_internal_error(value: &str) -> Result<PasswordHash, ApiError> { pub fn hash_or_internal_error(value: &str) -> Result<PasswordHash, ApiError> {
bcrypt::hash(value, PASSWORD_HASH_COST) bcrypt::hash(value, DEFAULT_COST)
.map(PasswordHash::new) .map(PasswordHash::new)
.map_err(|e| { .map_err(|e| {
error!("Bcrypt hash error: {:?}", e); error!("Bcrypt hash error: {:?}", e);
@@ -294,7 +288,7 @@ pub fn hash_or_internal_error(value: &str) -> Result<PasswordHash, ApiError> {
pub async fn hash_password_async(password: &str) -> Result<PasswordHash, ApiError> { pub async fn hash_password_async(password: &str) -> Result<PasswordHash, ApiError> {
let password = password.to_string(); let password = password.to_string();
tokio::task::spawn_blocking(move || hash(password, PASSWORD_HASH_COST)) tokio::task::spawn_blocking(move || hash(password, DEFAULT_COST))
.await .await
.map_err(|e| { .map_err(|e| {
error!("Failed to spawn blocking task: {:?}", e); error!("Failed to spawn blocking task: {:?}", e);
-1
View File
@@ -755,7 +755,6 @@ impl fmt::Display for RepoBackend {
#[config(layer_attr(serde(deny_unknown_fields)))] #[config(layer_attr(serde(deny_unknown_fields)))]
pub struct StorageConfig { pub struct StorageConfig {
/// Storage backend: `filesystem` or `s3`. /// Storage backend: `filesystem` or `s3`.
/// If you set `s3` you must also provide `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
#[config(env = "BLOB_STORAGE_BACKEND", default = "filesystem")] #[config(env = "BLOB_STORAGE_BACKEND", default = "filesystem")]
pub backend: String, pub backend: String,
@@ -395,7 +395,7 @@ async fn test_fetch_schema_error_status_gives_meaningful_error() {
} }
#[tokio::test] #[tokio::test]
async fn jail_test_plc_server_timeout() { async fn test_plc_server_timeout() {
let plc_server = MockServer::start().await; let plc_server = MockServer::start().await;
let did = "did:plc:timeout123"; let did = "did:plc:timeout123";
+3 -2
View File
@@ -90,7 +90,7 @@ tranquil-infra = { workspace = true, features = ["testing"] }
tempfile = "3" tempfile = "3"
ciborium = { workspace = true } ciborium = { workspace = true }
ctor = { workspace = true } ctor = { workspace = true }
testcontainers = { workspace = true, features = ["watchdog"] } testcontainers = { workspace = true }
testcontainers-modules = { workspace = true } testcontainers-modules = { workspace = true }
tranquil-ripple = { workspace = true } tranquil-ripple = { workspace = true }
tranquil-sync = { workspace = true } tranquil-sync = { workspace = true }
@@ -100,5 +100,6 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] }
wiremock = { workspace = true } wiremock = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
tokio-tungstenite = { workspace = true } tokio-tungstenite = { workspace = true }
s3 = { workspace = true } aws-config = { workspace = true }
aws-sdk-s3 = { workspace = true }
redis = { workspace = true } redis = { workspace = true }
+1 -3
View File
@@ -251,9 +251,7 @@ impl AppState {
let mut state = match cfg.storage.repo_backend() { let mut state = match cfg.storage.repo_backend() {
tranquil_config::RepoBackend::TranquilStore => { tranquil_config::RepoBackend::TranquilStore => {
tracing::info!( tracing::info!("tranquil-store repo backend active. Thank you so much for believing in us!");
"tranquil-store repo backend active. Thank you so much for believing in us!"
);
Self::from_store(shutdown).await Self::from_store(shutdown).await
} }
tranquil_config::RepoBackend::Postgres => { tranquil_config::RepoBackend::Postgres => {
@@ -1,4 +1,6 @@
use crate::common::*; mod common;
mod helpers;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,4 +1,5 @@
use crate::common::{base_url, client, create_account_and_login, get_test_repos, user_id_of}; mod common;
use common::{base_url, client, create_account_and_login, get_test_repos, user_id_of};
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::{CommsChannel, CommsType}; use tranquil_db_traits::{CommsChannel, CommsType};
use tranquil_types::{Did, Recipient}; use tranquil_types::{Did, Recipient};
@@ -1,4 +1,5 @@
use crate::common::{base_url, client, create_account_and_login}; mod common;
use common::{base_url, client, create_account_and_login};
use serde_json::{Value, json}; use serde_json::{Value, json};
#[tokio::test] #[tokio::test]
@@ -1,3 +1,5 @@
mod common;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::CommsType; use tranquil_db_traits::CommsType;
@@ -5,10 +7,10 @@ use tranquil_types::Did;
#[tokio::test] #[tokio::test]
async fn test_send_email_success() { async fn test_send_email_success() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let (access_jwt, did) = crate::common::create_admin_account_and_login(&client).await; let (access_jwt, did) = common::create_admin_account_and_login(&client).await;
let res = client let res = client
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
.bearer_auth(&access_jwt) .bearer_auth(&access_jwt)
@@ -24,7 +26,7 @@ async fn test_send_email_success() {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
let body: Value = res.json().await.expect("Invalid JSON"); let body: Value = res.json().await.expect("Invalid JSON");
assert_eq!(body["sent"], true); assert_eq!(body["sent"], true);
let user_id = crate::common::user_id_of(repos, &Did::new(did).unwrap()).await; let user_id = common::user_id_of(repos, &Did::new(did).unwrap()).await;
let comms = repos let comms = repos
.infra .infra
.get_latest_comms_for_user(user_id, CommsType::AdminEmail, 1) .get_latest_comms_for_user(user_id, CommsType::AdminEmail, 1)
@@ -41,10 +43,10 @@ async fn test_send_email_success() {
#[tokio::test] #[tokio::test]
async fn test_send_email_default_subject() { async fn test_send_email_default_subject() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let (access_jwt, did) = crate::common::create_admin_account_and_login(&client).await; let (access_jwt, did) = common::create_admin_account_and_login(&client).await;
let res = client let res = client
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
.bearer_auth(&access_jwt) .bearer_auth(&access_jwt)
@@ -59,7 +61,7 @@ async fn test_send_email_default_subject() {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
let body: Value = res.json().await.expect("Invalid JSON"); let body: Value = res.json().await.expect("Invalid JSON");
assert_eq!(body["sent"], true); assert_eq!(body["sent"], true);
let user_id = crate::common::user_id_of(repos, &Did::new(did).unwrap()).await; let user_id = common::user_id_of(repos, &Did::new(did).unwrap()).await;
let comms = repos let comms = repos
.infra .infra
.get_latest_comms_for_user(user_id, CommsType::AdminEmail, 10) .get_latest_comms_for_user(user_id, CommsType::AdminEmail, 10)
@@ -81,9 +83,9 @@ async fn test_send_email_default_subject() {
#[tokio::test] #[tokio::test]
async fn test_send_email_recipient_not_found() { async fn test_send_email_recipient_not_found() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let (access_jwt, _) = crate::common::create_admin_account_and_login(&client).await; let (access_jwt, _) = common::create_admin_account_and_login(&client).await;
let res = client let res = client
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
.bearer_auth(&access_jwt) .bearer_auth(&access_jwt)
@@ -102,9 +104,9 @@ async fn test_send_email_recipient_not_found() {
#[tokio::test] #[tokio::test]
async fn test_send_email_missing_content() { async fn test_send_email_missing_content() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let (access_jwt, did) = crate::common::create_admin_account_and_login(&client).await; let (access_jwt, did) = common::create_admin_account_and_login(&client).await;
let res = client let res = client
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
.bearer_auth(&access_jwt) .bearer_auth(&access_jwt)
@@ -123,9 +125,9 @@ async fn test_send_email_missing_content() {
#[tokio::test] #[tokio::test]
async fn test_send_email_missing_recipient() { async fn test_send_email_missing_recipient() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let (access_jwt, _) = crate::common::create_admin_account_and_login(&client).await; let (access_jwt, _) = common::create_admin_account_and_login(&client).await;
let res = client let res = client
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
.bearer_auth(&access_jwt) .bearer_auth(&access_jwt)
@@ -142,8 +144,8 @@ async fn test_send_email_missing_recipient() {
#[tokio::test] #[tokio::test]
async fn test_send_email_requires_auth() { async fn test_send_email_requires_auth() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url)) .post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
.json(&json!({ .json(&json!({
@@ -159,11 +161,11 @@ async fn test_send_email_requires_auth() {
#[tokio::test] #[tokio::test]
async fn test_send_email_rejects_garbage_stored_email() { async fn test_send_email_rejects_garbage_stored_email() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let (access_jwt, did) = crate::common::create_admin_account_and_login(&client).await; let (access_jwt, did) = common::create_admin_account_and_login(&client).await;
let user_id = crate::common::user_id_of(repos, &Did::new(did.clone()).unwrap()).await; let user_id = common::user_id_of(repos, &Did::new(did.clone()).unwrap()).await;
repos repos
.user .user
.update_email(user_id, "not-an-email") .update_email(user_id, "not-an-email")
@@ -1,4 +1,6 @@
use crate::common::*; mod common;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,4 +1,6 @@
use crate::common::*; mod common;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::Value; use serde_json::Value;
@@ -1,4 +1,5 @@
use crate::common::{base_url, client, create_admin_account_and_login}; mod common;
use common::{base_url, client, create_admin_account_and_login};
use serde_json::Value; use serde_json::Value;
#[tokio::test] #[tokio::test]
@@ -1,7 +1,10 @@
use crate::common::{base_url, client, create_account_and_login, pds_endpoint}; mod common;
use crate::helpers::verify_new_account; mod helpers;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::Utc; use chrono::Utc;
use common::{base_url, client, create_account_and_login, pds_endpoint};
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -212,7 +215,7 @@ async fn test_oauth_admin_extractor_allows_oauth_tokens() {
let did = account["did"].as_str().unwrap().to_string(); let did = account["did"].as_str().unwrap().to_string();
verify_new_account(&http_client, &did).await; verify_new_account(&http_client, &did).await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
repos repos
.user .user
.set_admin_status(&tranquil_types::Did::new(did.clone()).unwrap(), true) .set_admin_status(&tranquil_types::Did::new(did.clone()).unwrap(), true)
@@ -13,8 +13,10 @@
* - we maintain this code precisely because we believe everyone deserves an experience on the web that is free from this kinda language * - we maintain this code precisely because we believe everyone deserves an experience on the web that is free from this kinda language
*/ */
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::json; use serde_json::json;
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
+37 -75
View File
@@ -1,3 +1,9 @@
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
use aws_config::BehaviorVersion;
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
use aws_sdk_s3::Client as S3Client;
#[cfg(all(not(feature = "external-infra"), feature = "s3"))]
use aws_sdk_s3::config::Credentials;
use chrono::Utc; use chrono::Utc;
use reqwest::{Client, StatusCode, header}; use reqwest::{Client, StatusCode, header};
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -53,7 +59,7 @@ pub struct ServerInstance {
#[cfg(all(not(feature = "external-infra"), feature = "s3"))] #[cfg(all(not(feature = "external-infra"), feature = "s3"))]
use testcontainers::GenericImage; use testcontainers::GenericImage;
#[cfg(all(not(feature = "external-infra"), feature = "s3"))] #[cfg(all(not(feature = "external-infra"), feature = "s3"))]
use testcontainers::core::{ContainerPort, WaitFor}; use testcontainers::core::ContainerPort;
#[cfg(not(feature = "external-infra"))] #[cfg(not(feature = "external-infra"))]
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}; use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
#[cfg(not(feature = "external-infra"))] #[cfg(not(feature = "external-infra"))]
@@ -77,41 +83,28 @@ fn has_external_infra() -> bool {
|| (std::env::var("DATABASE_URL").is_ok() || (std::env::var("DATABASE_URL").is_ok()
&& (std::env::var("S3_ENDPOINT").is_ok() || std::env::var("BLOB_STORAGE_PATH").is_ok())) && (std::env::var("S3_ENDPOINT").is_ok() || std::env::var("BLOB_STORAGE_PATH").is_ok()))
} }
#[cfg(not(feature = "external-infra"))]
fn started_container_ids() -> Vec<String> {
let db = DB_CONTAINER.get().map(|c| c.id().to_string());
#[cfg(feature = "s3")]
let s3 = S3_CONTAINER.get().map(|c| c.id().to_string());
#[cfg(not(feature = "s3"))]
let s3: Option<String> = None;
db.into_iter().chain(s3).collect()
}
#[cfg(feature = "external-infra")]
fn started_container_ids() -> Vec<String> {
Vec::new()
}
#[cfg(test)] #[cfg(test)]
#[ctor::dtor] #[ctor::dtor]
fn cleanup() { fn cleanup() {
if let Some(temp_dir) = TEST_TEMP_DIR.get() { if let Some(temp_dir) = TEST_TEMP_DIR.get() {
let _ = std::fs::remove_dir_all(temp_dir); let _ = std::fs::remove_dir_all(temp_dir);
} }
let ids = started_container_ids(); if has_external_infra() {
if ids.is_empty() {
return; return;
} }
if std::env::var("XDG_RUNTIME_DIR").is_ok() { if std::env::var("XDG_RUNTIME_DIR").is_ok() {
let _ = std::process::Command::new("podman") let _ = std::process::Command::new("podman")
.args(["rm", "-f"]) .args(["rm", "-f", "--filter", "label=tranquil_pds_test=true"])
.args(&ids)
.output(); .output();
} }
let _ = std::process::Command::new("docker") let _ = std::process::Command::new("docker")
.args(["rm", "-f"]) .args([
.args(&ids) "container",
"prune",
"-f",
"--filter",
"label=tranquil_pds_test=true",
])
.output(); .output();
} }
@@ -237,7 +230,6 @@ async fn setup_with_testcontainers() -> String {
#[cfg(all(not(feature = "external-infra"), feature = "s3"))] #[cfg(all(not(feature = "external-infra"), feature = "s3"))]
async fn setup_with_testcontainers() -> String { async fn setup_with_testcontainers() -> String {
let s3_container = GenericImage::new("cgr.dev/chainguard/minio", "latest") let s3_container = GenericImage::new("cgr.dev/chainguard/minio", "latest")
.with_wait_for(WaitFor::message_on_stderr("API: http"))
.with_exposed_port(ContainerPort::Tcp(9000)) .with_exposed_port(ContainerPort::Tcp(9000))
.with_env_var("MINIO_ROOT_USER", "minioadmin") .with_env_var("MINIO_ROOT_USER", "minioadmin")
.with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin")
@@ -263,25 +255,28 @@ async fn setup_with_testcontainers() -> String {
std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); std::env::set_var("SKIP_IMPORT_VERIFICATION", "true");
std::env::set_var("PLC_DIRECTORY_URL", &plc_url); std::env::set_var("PLC_DIRECTORY_URL", &plc_url);
} }
let s3_client = s3::Client::builder(&s3_endpoint) let sdk_config = aws_config::defaults(BehaviorVersion::latest())
.unwrap()
.region("us-east-1") .region("us-east-1")
.addressing_style(s3::AddressingStyle::Path) .endpoint_url(&s3_endpoint)
.auth(s3::Auth::from_env().unwrap()) .credentials_provider(Credentials::new(
.build() "minioadmin",
.unwrap(); "minioadmin",
s3_client None,
.buckets() None,
.create("test-bucket") "test",
))
.load()
.await;
let s3_config = aws_sdk_s3::config::Builder::from(&sdk_config)
.force_path_style(true)
.build();
let s3_client = S3Client::from_conf(s3_config);
let _ = s3_client.create_bucket().bucket("test-bucket").send().await;
let _ = s3_client
.create_bucket()
.bucket("test-backups")
.send() .send()
.await .await;
.unwrap();
s3_client
.buckets()
.create("test-backups")
.send()
.await
.unwrap();
register_mock_appview().await; register_mock_appview().await;
S3_CONTAINER.set(s3_container).ok(); S3_CONTAINER.set(s3_container).ok();
let container = Postgres::default() let container = Postgres::default()
@@ -739,40 +734,7 @@ pub async fn spawn_cluster(pool: Option<sqlx::PgPool>, node_count: usize) -> Vec
let first = &instances[0]; let first = &instances[0];
APP_PORT.set(first.port).ok(); APP_PORT.set(first.port).ok();
let caches: Vec<Arc<dyn Cache>> = instances tokio::time::sleep(Duration::from_millis(2000)).await;
.iter()
.map(|instance| instance.cache.clone().expect("cluster node has no cache"))
.collect();
futures::future::join_all(caches.iter().enumerate().map(|(i, cache)| async move {
cache
.set(
&format!("__cluster_ready_{i}"),
"1",
Duration::from_secs(60),
)
.await
.expect("cluster readiness probe failed");
}))
.await;
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
loop {
let ready = futures::future::join_all(caches.iter().flat_map(|cache| {
(0..caches.len()).map(move |i| async move {
cache.get(&format!("__cluster_ready_{i}")).await.is_some()
})
}))
.await
.into_iter()
.all(|seen| seen);
if ready {
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"cluster nodes did not converge within 10s"
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
instances instances
} }
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,6 +1,7 @@
use crate::common::*; mod common;
use base64::Engine; use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use common::*;
use k256::ecdsa::{SigningKey, signature::Signer}; use k256::ecdsa::{SigningKey, signature::Signer};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -99,7 +100,7 @@ async fn test_external_did_web_no_local_doc() {
let mock_addr = mock_uri.trim_start_matches("http://"); let mock_addr = mock_uri.trim_start_matches("http://");
let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
let handle = format!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let pds_endpoint = crate::common::pds_endpoint(); let pds_endpoint = common::pds_endpoint();
let reserve_res = client let reserve_res = client
.post(format!( .post(format!(
@@ -202,7 +203,7 @@ async fn assert_reserved_key_placement_rejected(
let mock_addr = mock_uri.trim_start_matches("http://"); let mock_addr = mock_uri.trim_start_matches("http://");
let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
let handle = format!("wm{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("wm{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let pds_endpoint = crate::common::pds_endpoint(); let pds_endpoint = common::pds_endpoint();
let signing_key = reserve_signing_key(&client, base, &did).await; let signing_key = reserve_signing_key(&client, base, &did).await;
let signing_multibase = signing_key let signing_multibase = signing_key
@@ -524,8 +525,8 @@ async fn test_did_web_byod_flow() {
unique_id unique_id
); );
let handle = format!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let pds_endpoint = crate::common::pds_endpoint(); let pds_endpoint = common::pds_endpoint();
let pds_hostname = crate::common::pds_hostname(); let pds_hostname = common::pds_hostname();
let pds_did = format!("did:web:{}", pds_hostname); let pds_did = format!("did:web:{}", pds_hostname);
let temp_key = SigningKey::random(&mut rand::thread_rng()); let temp_key = SigningKey::random(&mut rand::thread_rng());
@@ -578,7 +579,7 @@ async fn test_did_web_byod_flow() {
"BYOD accounts should require verification" "BYOD accounts should require verification"
); );
let access_jwt = crate::common::verify_new_account(&client, returned_did).await; let access_jwt = common::verify_new_account(&client, returned_did).await;
let res = client let res = client
.get(format!( .get(format!(
@@ -1,10 +1,11 @@
mod common;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::CommsType; use tranquil_db_traits::CommsType;
use tranquil_types::Did; use tranquil_types::Did;
async fn get_email_update_token(did: &str) -> String { async fn get_email_update_token(did: &str) -> String {
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let parsed_did = Did::new(did.to_string()).unwrap(); let parsed_did = Did::new(did.to_string()).unwrap();
let user = repos let user = repos
.user .user
@@ -59,14 +60,14 @@ async fn create_verified_account(
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
let body: Value = res.json().await.expect("Invalid JSON"); let body: Value = res.json().await.expect("Invalid JSON");
let did = body["did"].as_str().expect("No did").to_string(); let did = body["did"].as_str().expect("No did").to_string();
let jwt = crate::common::verify_new_account(client, &did).await; let jwt = common::verify_new_account(client, &did).await;
(jwt, did) (jwt, did)
} }
#[tokio::test] #[tokio::test]
async fn test_request_email_update_returns_token_required() { async fn test_request_email_update_returns_token_required() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("er{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("er{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await; let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
@@ -87,9 +88,9 @@ async fn test_request_email_update_returns_token_required() {
#[tokio::test] #[tokio::test]
async fn test_update_email_flow_success() { async fn test_update_email_flow_success() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
let (access_jwt, did) = create_verified_account(&client, base_url, &handle, &email).await; let (access_jwt, did) = create_verified_account(&client, base_url, &handle, &email).await;
@@ -135,8 +136,8 @@ async fn test_update_email_flow_success() {
#[tokio::test] #[tokio::test]
async fn test_update_email_requires_token_when_verified() { async fn test_update_email_requires_token_when_verified() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("ed{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("ed{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await; let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
@@ -156,8 +157,8 @@ async fn test_update_email_requires_token_when_verified() {
#[tokio::test] #[tokio::test]
async fn test_update_email_same_email_noop() { async fn test_update_email_same_email_noop() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("es{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("es{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await; let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
@@ -178,8 +179,8 @@ async fn test_update_email_same_email_noop() {
#[tokio::test] #[tokio::test]
async fn test_update_email_invalid_token() { async fn test_update_email_invalid_token() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("eb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("eb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await; let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
@@ -213,8 +214,8 @@ async fn test_update_email_invalid_token() {
#[tokio::test] #[tokio::test]
async fn test_update_email_no_auth() { async fn test_update_email_no_auth() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url)) .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
@@ -229,8 +230,8 @@ async fn test_update_email_no_auth() {
#[tokio::test] #[tokio::test]
async fn test_update_email_invalid_format() { async fn test_update_email_invalid_format() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("ef{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("ef{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await; let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
@@ -247,9 +248,9 @@ async fn test_update_email_invalid_format() {
#[tokio::test] #[tokio::test]
async fn test_confirm_email_confirms_existing_email() { async fn test_confirm_email_confirms_existing_email() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("ec{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("ec{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
@@ -322,9 +323,9 @@ async fn test_confirm_email_confirms_existing_email() {
#[tokio::test] #[tokio::test]
async fn test_confirm_email_rejects_wrong_email() { async fn test_confirm_email_rejects_wrong_email() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("ew{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("ew{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@jola.dev", handle); let email = format!("{}@jola.dev", handle);
@@ -390,8 +391,8 @@ async fn test_confirm_email_rejects_wrong_email() {
#[tokio::test] #[tokio::test]
async fn test_confirm_email_invalid_token() { async fn test_confirm_email_invalid_token() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("ei{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("ei{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@jola.dev", handle); let email = format!("{}@jola.dev", handle);
@@ -432,9 +433,9 @@ async fn test_confirm_email_invalid_token() {
#[tokio::test] #[tokio::test]
async fn test_unverified_account_can_update_email_without_token() { async fn test_unverified_account_can_update_email_without_token() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("ev{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("ev{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@nel.pet", handle); let email = format!("{}@nel.pet", handle);
@@ -502,9 +503,9 @@ async fn test_unverified_account_can_update_email_without_token() {
#[tokio::test] #[tokio::test]
async fn test_update_email_to_same_as_another_user_allowed() { async fn test_update_email_to_same_as_another_user_allowed() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email1 = format!("{}@jola.dev", handle1); let email1 = format!("{}@jola.dev", handle1);
@@ -556,8 +557,8 @@ async fn test_update_email_to_same_as_another_user_allowed() {
#[tokio::test] #[tokio::test]
async fn test_check_email_in_use_distinguishes_empty_from_invalid() { async fn test_check_email_in_use_distinguishes_empty_from_invalid() {
let client = crate::common::client(); let client = common::client();
let base = crate::common::base_url().await; let base = common::base_url().await;
let resp = client let resp = client
.post(format!("{}/xrpc/_account.checkEmailInUse", base)) .post(format!("{}/xrpc/_account.checkEmailInUse", base))
@@ -1,6 +1,8 @@
use crate::common::*; mod common;
use base64::Engine; use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -1,7 +1,11 @@
use crate::common::*; mod common;
use crate::firehose::FirehoseConsumer; mod firehose;
use crate::helpers::build_car_with_signature; mod helpers;
use cid::Cid; use cid::Cid;
use common::*;
use firehose::FirehoseConsumer;
use helpers::build_car_with_signature;
use iroh_car::CarReader; use iroh_car::CarReader;
use k256::ecdsa::SigningKey; use k256::ecdsa::SigningKey;
use multihash::Multihash; use multihash::Multihash;
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use cid::Cid; use cid::Cid;
use common::*;
use futures::{SinkExt, stream::StreamExt}; use futures::{SinkExt, stream::StreamExt};
use iroh_car::CarReader; use iroh_car::CarReader;
use reqwest::StatusCode; use reqwest::StatusCode;
@@ -1,6 +1,8 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_types::{Did, Nsid, Rkey}; use tranquil_types::{Did, Nsid, Rkey};
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use chrono::Utc; use chrono::Utc;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,4 +1,5 @@
use crate::common::*; mod common;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use wiremock::matchers::{method, path}; use wiremock::matchers::{method, path};
@@ -97,7 +98,7 @@ async fn test_create_did_web_account_and_resolve() {
let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
let handle = format!("wu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("wu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let base = base_url().await; let base = base_url().await;
let pds_endpoint = crate::common::pds_endpoint(); let pds_endpoint = common::pds_endpoint();
let reserve_res = client let reserve_res = client
.post(format!( .post(format!(
@@ -219,7 +220,7 @@ async fn test_did_web_lifecycle() {
let handle = format!("lc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("lc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle); let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle);
let email = format!("{}@test.com", handle); let email = format!("{}@test.com", handle);
let pds_endpoint = crate::common::pds_endpoint(); let pds_endpoint = common::pds_endpoint();
let reserve_res = client let reserve_res = client
.post(format!( .post(format!(
@@ -1,4 +1,5 @@
use crate::common::*; mod common;
use common::*;
use iroh_car::CarHeader; use iroh_car::CarHeader;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::json; use serde_json::json;
@@ -1,4 +1,5 @@
use crate::common::*; mod common;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,4 +1,5 @@
use crate::common::*; mod common;
use common::*;
use reqwest::{Client, StatusCode}; use reqwest::{Client, StatusCode};
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_pds::api::error::ApiError; use tranquil_pds::api::error::ApiError;
-85
View File
@@ -1,85 +0,0 @@
#[path = "../common/mod.rs"]
mod common;
#[path = "../firehose/mod.rs"]
mod firehose;
#[path = "../helpers/mod.rs"]
mod helpers;
#[path = "../mst_verify/mod.rs"]
mod mst_verify;
mod account_lifecycle;
mod account_notifications;
mod actor;
mod admin_email;
mod admin_invite;
mod admin_moderation;
mod admin_search;
mod admin_stats;
mod apple_sso_unit;
mod auth_extractor;
mod banned_words;
mod car_export_detects_corruption;
mod change_password;
mod commit_signing;
mod delete_account;
mod did_web;
mod dpop_unit;
mod email_update;
mod feed_proxy_aud;
mod firehose_inline_blocks;
mod firehose_validation;
mod gc_after_delete;
mod gc_compaction_restart;
mod identity;
mod image_processing;
mod import_verification;
mod invite;
mod invite_registration;
mod jwt_security;
mod legacy_2fa;
mod lifecycle_record;
mod lifecycle_session;
mod lifecycle_social;
mod moderation;
mod mst_diff_equivalence;
mod mst_firehose_e2e;
mod mst_inductive_firehose;
mod mst_integrity;
mod mst_property_fuzz;
mod mst_repair_audit;
mod mst_repair_durability;
mod mst_structural_repair;
mod notifications;
mod oauth;
mod oauth_lifecycle;
mod oauth_permission_sets;
mod oauth_scopes;
mod oauth_security;
mod oauth_token_eviction;
mod password_reset;
mod plc_operations;
mod plc_validation;
mod rate_limit;
mod record_validation;
mod repair_leaf_loss;
mod repo_batch;
mod repo_blob;
mod repo_conformance;
mod repo_lifecycle;
mod ripple_cluster;
mod scope_edge_cases;
mod security_fixes;
mod server;
mod session_management;
mod shutdown_unit;
mod signing_key;
mod sso;
mod store_parity;
mod sync_blob;
mod sync_conformance;
mod sync_deprecated;
mod sync_repo;
mod user_blocks_reachability;
mod validation_edge_cases;
mod verify_live_commit;
mod whole_story;
@@ -1,7 +1,8 @@
#![allow(unused_imports)] #![allow(unused_imports)]
use crate::common::{base_url, client, create_account_and_login, get_test_repos}; mod common;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use common::{base_url, client, create_account_and_login, get_test_repos};
use k256::SecretKey; use k256::SecretKey;
use k256::ecdsa::{Signature, SigningKey, signature::Signer}; use k256::ecdsa::{Signature, SigningKey, signature::Signer};
use rand::rngs::OsRng; use rand::rngs::OsRng;
@@ -1,4 +1,6 @@
use crate::common::{base_url, client, create_account_and_login, get_test_repos, user_id_of}; mod common;
use common::{base_url, client, create_account_and_login, get_test_repos, user_id_of};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::{CommsChannel, CommsType}; use tranquil_db_traits::{CommsChannel, CommsType};
@@ -1,6 +1,8 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use helpers::*;
use reqwest::{StatusCode, header}; use reqwest::{StatusCode, header};
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::time::Duration; use std::time::Duration;
@@ -1,6 +1,8 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,6 +1,8 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,13 +1,16 @@
mod common;
mod firehose;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::io::Cursor; use std::io::Cursor;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use crate::common::*;
use crate::firehose::{FirehoseConsumer, ParsedCommitFrame};
use bytes::Bytes; use bytes::Bytes;
use cid::Cid; use cid::Cid;
use common::*;
use firehose::{FirehoseConsumer, ParsedCommitFrame};
use iroh_car::CarReader; use iroh_car::CarReader;
use jacquard_common::smol_str::SmolStr; use jacquard_common::smol_str::SmolStr;
use jacquard_repo::commit::Commit; use jacquard_repo::commit::Commit;
@@ -28,7 +31,7 @@ async fn car_to_blocks(car_bytes: &[u8]) -> BTreeMap<Cid, Bytes> {
blocks blocks
} }
fn op_to_verified(op: &crate::firehose::ParsedRepoOp) -> Result<VerifiedWriteOp, String> { fn op_to_verified(op: &firehose::ParsedRepoOp) -> Result<VerifiedWriteOp, String> {
let key = SmolStr::new(&op.path); let key = SmolStr::new(&op.path);
match op.action { match op.action {
RepoAction::Create => { RepoAction::Create => {
@@ -1,14 +1,17 @@
mod common;
mod mst_verify;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use crate::common::*;
use crate::mst_verify::{extract_event_blocks, inline_to_store};
use cid::Cid; use cid::Cid;
use common::*;
use jacquard_common::smol_str::SmolStr; use jacquard_common::smol_str::SmolStr;
use jacquard_repo::commit::Commit; use jacquard_repo::commit::Commit;
use jacquard_repo::mst::{Mst, VerifiedWriteOp}; use jacquard_repo::mst::{Mst, VerifiedWriteOp};
use jacquard_repo::storage::{BlockStore, MemoryBlockStore}; use jacquard_repo::storage::{BlockStore, MemoryBlockStore};
use mst_verify::{extract_event_blocks, inline_to_store};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::{RepoEventType, SequenceNumber, SequencedEvent}; use tranquil_db_traits::{RepoEventType, SequenceNumber, SequencedEvent};
@@ -1,13 +1,17 @@
mod common;
mod firehose;
mod helpers;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::io::Cursor; use std::io::Cursor;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use crate::common::*;
use crate::firehose::FirehoseConsumer;
use crate::helpers::build_car_with_signature;
use bytes::Bytes; use bytes::Bytes;
use cid::Cid; use cid::Cid;
use common::*;
use firehose::FirehoseConsumer;
use helpers::build_car_with_signature;
use iroh_car::CarReader; use iroh_car::CarReader;
use jacquard_repo::commit::Commit; use jacquard_repo::commit::Commit;
use jacquard_repo::mst::Mst; use jacquard_repo::mst::Mst;
@@ -1,13 +1,16 @@
mod common;
mod mst_verify;
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use crate::common::*;
use crate::mst_verify::{extract_event_blocks, inline_to_store};
use cid::Cid; use cid::Cid;
use common::*;
use jacquard_common::smol_str::SmolStr; use jacquard_common::smol_str::SmolStr;
use jacquard_repo::commit::Commit; use jacquard_repo::commit::Commit;
use jacquard_repo::mst::{Mst, VerifiedWriteOp}; use jacquard_repo::mst::{Mst, VerifiedWriteOp};
use jacquard_repo::storage::BlockStore; use jacquard_repo::storage::BlockStore;
use mst_verify::{extract_event_blocks, inline_to_store};
use rand::rngs::StdRng; use rand::rngs::StdRng;
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use reqwest::StatusCode; use reqwest::StatusCode;
@@ -1,12 +1,13 @@
mod common;
use tranquil_db_traits::{CommsChannel, CommsStatus, CommsType}; use tranquil_db_traits::{CommsChannel, CommsStatus, CommsType};
use tranquil_types::{Did, Recipient}; use tranquil_types::{Did, Recipient};
#[tokio::test] #[tokio::test]
async fn test_enqueue_comms() { async fn test_enqueue_comms() {
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let (_, did) = crate::common::create_account_and_login(&crate::common::client()).await; let (_, did) = common::create_account_and_login(&common::client()).await;
let user_id = crate::common::user_id_of(repos, &Did::new(did).unwrap()).await; let user_id = common::user_id_of(repos, &Did::new(did).unwrap()).await;
let comms_id = repos repos
.infra .infra
.enqueue_comms( .enqueue_comms(
Some(user_id), Some(user_id),
@@ -20,13 +21,10 @@ async fn test_enqueue_comms() {
.expect("Failed to enqueue comms"); .expect("Failed to enqueue comms");
let comms = repos let comms = repos
.infra .infra
.get_latest_comms_for_user(user_id, CommsType::Welcome, 2) .get_latest_comms_for_user(user_id, CommsType::Welcome, 1)
.await .await
.expect("DB error"); .expect("DB error");
let row = comms let row = comms.first().expect("Comms not found");
.iter()
.find(|c| c.id == comms_id)
.expect("Comms not found");
assert_eq!(row.user_id, Some(user_id)); assert_eq!(row.user_id, Some(user_id));
assert_eq!(row.recipient, "test@nel.pet"); assert_eq!(row.recipient, "test@nel.pet");
assert_eq!(row.subject.as_deref(), Some("Test Subject")); assert_eq!(row.subject.as_deref(), Some("Test Subject"));
@@ -38,9 +36,9 @@ async fn test_enqueue_comms() {
#[tokio::test] #[tokio::test]
async fn test_comms_queue_status_index() { async fn test_comms_queue_status_index() {
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let (_, did) = crate::common::create_account_and_login(&crate::common::client()).await; let (_, did) = common::create_account_and_login(&common::client()).await;
let user_id = crate::common::user_id_of(repos, &Did::new(did).unwrap()).await; let user_id = common::user_id_of(repos, &Did::new(did).unwrap()).await;
let initial_count = repos let initial_count = repos
.infra .infra
.count_comms_by_type(user_id, CommsType::PasswordReset) .count_comms_by_type(user_id, CommsType::PasswordReset)
@@ -1,6 +1,8 @@
use crate::common::{base_url, client, get_test_repos}; mod common;
use crate::helpers::verify_new_account; mod helpers;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use common::{base_url, client, get_test_repos};
use helpers::verify_new_account;
use reqwest::{StatusCode, redirect}; use reqwest::{StatusCode, redirect};
use serde_json::{Value, json}; use serde_json::{Value, json};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -1,7 +1,10 @@
use crate::common::{base_url, client}; mod common;
use crate::helpers::verify_new_account; mod helpers;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::Utc; use chrono::Utc;
use common::{base_url, client};
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -1,7 +1,10 @@
use crate::common::{base_url, client, create_account_and_login}; mod common;
use crate::helpers::verify_new_account; mod helpers;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::Utc; use chrono::Utc;
use common::{base_url, client, create_account_and_login};
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -56,7 +59,7 @@ async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer {
} }
async fn seed_permission_set(nsid: &str, granular_scope: &str) { async fn seed_permission_set(nsid: &str, granular_scope: &str) {
let state = crate::common::get_test_app_state().await; let state = common::get_test_app_state().await;
let key = tranquil_pds::cache_keys::permission_set_key( let key = tranquil_pds::cache_keys::permission_set_key(
&tranquil_types::Nsid::new(nsid).unwrap(), &tranquil_types::Nsid::new(nsid).unwrap(),
None, None,
@@ -594,7 +597,7 @@ async fn test_grant_row_keeps_include_jwt_carries_expanded() {
); );
let token_id = token_id_from_jwt(&session.access_token); let token_id = token_id_from_jwt(&session.access_token);
let token_data = crate::common::get_test_repos() let token_data = common::get_test_repos()
.await .await
.oauth .oauth
.get_token_by_id(&token_id) .get_token_by_id(&token_id)
@@ -913,7 +916,7 @@ async fn test_consent_post_errors_when_set_unresolvable() {
"Consent GET should succeed" "Consent GET should succeed"
); );
let state = crate::common::get_test_app_state().await; let state = common::get_test_app_state().await;
let key = tranquil_pds::cache_keys::permission_set_key( let key = tranquil_pds::cache_keys::permission_set_key(
&tranquil_types::Nsid::new(UNRESOLVABLE_NSID).unwrap(), &tranquil_types::Nsid::new(UNRESOLVABLE_NSID).unwrap(),
None, None,
@@ -1208,7 +1211,7 @@ async fn test_consent_remember_persists_set_preference() {
let did: tranquil_types::Did = delegated_did.parse().expect("valid did"); let did: tranquil_types::Did = delegated_did.parse().expect("valid did");
let client_id_typed = tranquil_types::ClientId::new(client_id.clone()); let client_id_typed = tranquil_types::ClientId::new(client_id.clone());
let stored_prefs = crate::common::get_test_repos() let stored_prefs = common::get_test_repos()
.await .await
.oauth .oauth
.get_scope_preferences(&did, &client_id_typed) .get_scope_preferences(&did, &client_id_typed)
@@ -1592,7 +1595,7 @@ async fn test_consent_post_drops_unpresented_scope() {
let access_token = token_body["access_token"].as_str().unwrap().to_string(); let access_token = token_body["access_token"].as_str().unwrap().to_string();
let token_id = token_id_from_jwt(&access_token); let token_id = token_id_from_jwt(&access_token);
let token_data = crate::common::get_test_repos() let token_data = common::get_test_repos()
.await .await
.oauth .oauth
.get_token_by_id(&token_id) .get_token_by_id(&token_id)
@@ -1,7 +1,10 @@
use crate::common::{base_url, client, get_test_repos}; mod common;
use crate::helpers::verify_new_account; mod helpers;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::Utc; use chrono::Utc;
use common::{base_url, client, get_test_repos};
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -1,8 +1,10 @@
#![allow(unused_imports)] #![allow(unused_imports)]
use crate::common::{base_url, client, create_account_and_login}; mod common;
use crate::helpers::verify_new_account; mod helpers;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::Utc; use chrono::Utc;
use common::{base_url, client, create_account_and_login};
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -1,7 +1,10 @@
use crate::common::{base_url, client, get_test_repos}; mod common;
use crate::helpers::verify_new_account; mod helpers;
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use common::{base_url, client, get_test_repos};
use futures::StreamExt; use futures::StreamExt;
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_oauth::{ use tranquil_oauth::{
@@ -1,13 +1,15 @@
use crate::helpers::verify_new_account; mod common;
mod helpers;
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::CommsType; use tranquil_db_traits::CommsType;
#[tokio::test] #[tokio::test]
async fn test_request_password_reset_creates_code() { async fn test_request_password_reset_creates_code() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("pr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("pr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@example.com", handle); let email = format!("{}@example.com", handle);
let payload = json!({ let payload = json!({
@@ -53,8 +55,8 @@ async fn test_request_password_reset_creates_code() {
#[tokio::test] #[tokio::test]
async fn test_request_password_reset_unknown_email_returns_ok() { async fn test_request_password_reset_unknown_email_returns_ok() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.requestPasswordReset", "{}/xrpc/com.atproto.server.requestPasswordReset",
@@ -69,9 +71,9 @@ async fn test_request_password_reset_unknown_email_returns_ok() {
#[tokio::test] #[tokio::test]
async fn test_reset_password_with_valid_token() { async fn test_reset_password_with_valid_token() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("pr2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("pr2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@example.com", handle); let email = format!("{}@example.com", handle);
let old_password = "Oldpass123!"; let old_password = "Oldpass123!";
@@ -165,8 +167,8 @@ async fn test_reset_password_with_valid_token() {
#[tokio::test] #[tokio::test]
async fn test_reset_password_with_invalid_token() { async fn test_reset_password_with_invalid_token() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.resetPassword", "{}/xrpc/com.atproto.server.resetPassword",
@@ -186,9 +188,9 @@ async fn test_reset_password_with_invalid_token() {
#[tokio::test] #[tokio::test]
async fn test_reset_password_with_expired_token() { async fn test_reset_password_with_expired_token() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("pr3{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("pr3{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@example.com", handle); let email = format!("{}@example.com", handle);
let payload = json!({ let payload = json!({
@@ -247,9 +249,9 @@ async fn test_reset_password_with_expired_token() {
#[tokio::test] #[tokio::test]
async fn test_reset_password_invalidates_sessions() { async fn test_reset_password_invalidates_sessions() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let handle = format!("pr4{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("pr4{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@example.com", handle); let email = format!("{}@example.com", handle);
let payload = json!({ let payload = json!({
@@ -318,8 +320,8 @@ async fn test_reset_password_invalidates_sessions() {
#[tokio::test] #[tokio::test]
async fn test_request_password_reset_empty_email() { async fn test_request_password_reset_empty_email() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.requestPasswordReset", "{}/xrpc/com.atproto.server.requestPasswordReset",
@@ -336,9 +338,9 @@ async fn test_request_password_reset_empty_email() {
#[tokio::test] #[tokio::test]
async fn test_reset_password_creates_notification() { async fn test_reset_password_creates_notification() {
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("pr5{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("pr5{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let email = format!("{}@example.com", handle); let email = format!("{}@example.com", handle);
let payload = json!({ let payload = json!({
@@ -1,4 +1,5 @@
use crate::common::*; mod common;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::json; use serde_json::json;
use tranquil_types::Did; use tranquil_types::Did;
@@ -1,4 +1,5 @@
use crate::common::{base_url, client}; mod common;
use common::{base_url, client};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::json; use serde_json::json;
@@ -1,6 +1,9 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use cid::Cid; use cid::Cid;
use common::*;
use helpers::*;
use jacquard_repo::commit::Commit; use jacquard_repo::commit::Commit;
use jacquard_repo::storage::BlockStore; use jacquard_repo::storage::BlockStore;
use serde_json::json; use serde_json::json;
@@ -1,5 +1,6 @@
use crate::common::*; mod common;
use chrono::Utc; use chrono::Utc;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::{Backlink, BacklinkPath}; use tranquil_db_traits::{Backlink, BacklinkPath};
@@ -1,4 +1,5 @@
use crate::common::*; mod common;
use common::*;
use reqwest::{StatusCode, header}; use reqwest::{StatusCode, header};
use serde_json::Value; use serde_json::Value;
@@ -1,6 +1,8 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,6 +1,9 @@
use crate::common::*; mod common;
use crate::firehose::{FirehoseConsumer, ParsedCommitFrame}; mod firehose;
use cid::Cid; use cid::Cid;
use common::*;
use firehose::{FirehoseConsumer, ParsedCommitFrame};
use iroh_car::CarReader; use iroh_car::CarReader;
use jacquard_repo::commit::Commit; use jacquard_repo::commit::Commit;
use reqwest::StatusCode; use reqwest::StatusCode;
@@ -9,6 +12,8 @@ use std::io::Cursor;
use std::str::FromStr; use std::str::FromStr;
use tranquil_scopes::RepoAction; use tranquil_scopes::RepoAction;
mod helpers;
async fn create_post_record(client: &reqwest::Client, token: &str, did: &str, text: &str) -> Value { async fn create_post_record(client: &reqwest::Client, token: &str, did: &str, text: &str) -> Value {
let payload = json!({ let payload = json!({
"repo": did, "repo": did,
@@ -379,7 +384,7 @@ async fn test_firehose_commit_signature_verification() {
let client = client(); let client = client();
let (token, did) = create_account_and_login(&client).await; let (token, did) = create_account_and_login(&client).await;
let key_bytes = crate::helpers::get_user_signing_key(&did) let key_bytes = helpers::get_user_signing_key(&did)
.await .await
.expect("Failed to get signing key"); .expect("Failed to get signing key");
let signing_key = let signing_key =
@@ -1,3 +1,5 @@
mod common;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::json; use serde_json::json;
use std::sync::Arc; use std::sync::Arc;
@@ -23,14 +25,14 @@ where
} }
} }
fn cache_for(nodes: &[crate::common::ServerInstance], idx: usize) -> Arc<dyn Cache> { fn cache_for(nodes: &[common::ServerInstance], idx: usize) -> Arc<dyn Cache> {
nodes[idx] nodes[idx]
.cache .cache
.clone() .clone()
.unwrap_or_else(|| panic!("node {idx} should have a cache")) .unwrap_or_else(|| panic!("node {idx} should have a cache"))
} }
fn rl_for(nodes: &[crate::common::ServerInstance], idx: usize) -> Arc<dyn DistributedRateLimiter> { fn rl_for(nodes: &[common::ServerInstance], idx: usize) -> Arc<dyn DistributedRateLimiter> {
nodes[idx] nodes[idx]
.distributed_rate_limiter .distributed_rate_limiter
.clone() .clone()
@@ -39,10 +41,10 @@ fn rl_for(nodes: &[crate::common::ServerInstance], idx: usize) -> Arc<dyn Distri
#[tokio::test] #[tokio::test]
async fn cluster_formation() { async fn cluster_formation() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
assert!(nodes.len() >= 3, "expected at least 3 cluster nodes"); assert!(nodes.len() >= 3, "expected at least 3 cluster nodes");
let client = crate::common::client(); let client = common::client();
let results: Vec<_> = futures::future::join_all(nodes.iter().map(|node| { let results: Vec<_> = futures::future::join_all(nodes.iter().map(|node| {
let client = client.clone(); let client = client.clone();
let url = node.url.clone(); let url = node.url.clone();
@@ -69,8 +71,8 @@ async fn cluster_formation() {
#[tokio::test] #[tokio::test]
async fn cluster_any_node_access() { async fn cluster_any_node_access() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let client = crate::common::client(); let client = common::client();
let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let payload = serde_json::json!({ let payload = serde_json::json!({
@@ -95,7 +97,7 @@ async fn cluster_any_node_access() {
.expect("no accessJwt") .expect("no accessJwt")
.to_string(); .to_string();
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let user = repos let user = repos
.user .user
.get_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) .get_by_did(&tranquil_types::Did::new(did.clone()).unwrap())
@@ -174,7 +176,7 @@ async fn cluster_any_node_access() {
#[tokio::test] #[tokio::test]
async fn cache_convergence() { async fn cache_convergence() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let cache_a = nodes[0].cache.as_ref().expect("node 0 should have a cache"); let cache_a = nodes[0].cache.as_ref().expect("node 0 should have a cache");
let cache_b = nodes[1].cache.as_ref().expect("node 1 should have a cache"); let cache_b = nodes[1].cache.as_ref().expect("node 1 should have a cache");
@@ -216,7 +218,7 @@ async fn cache_convergence() {
#[tokio::test] #[tokio::test]
async fn rate_limit_convergence() { async fn rate_limit_convergence() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let rl_a = nodes[0] let rl_a = nodes[0]
.distributed_rate_limiter .distributed_rate_limiter
@@ -272,7 +274,7 @@ async fn rate_limit_convergence() {
#[tokio::test] #[tokio::test]
async fn delete_convergence() { async fn delete_convergence() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let cache_1 = cache_for(nodes, 1); let cache_1 = cache_for(nodes, 1);
@@ -306,7 +308,7 @@ async fn delete_convergence() {
#[tokio::test] #[tokio::test]
async fn three_node_transitive_convergence() { async fn three_node_transitive_convergence() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let cache_2 = cache_for(nodes, 2); let cache_2 = cache_for(nodes, 2);
@@ -329,7 +331,7 @@ async fn three_node_transitive_convergence() {
#[tokio::test] #[tokio::test]
async fn cluster_overwrite_conflict_resolution() { async fn cluster_overwrite_conflict_resolution() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let cache_1 = cache_for(nodes, 1); let cache_1 = cache_for(nodes, 1);
let cache_2 = cache_for(nodes, 2); let cache_2 = cache_for(nodes, 2);
@@ -372,7 +374,7 @@ async fn cluster_overwrite_conflict_resolution() {
#[tokio::test] #[tokio::test]
async fn cluster_bulk_key_convergence() { async fn cluster_bulk_key_convergence() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let cache_1 = cache_for(nodes, 1); let cache_1 = cache_for(nodes, 1);
let cache_2 = cache_for(nodes, 2); let cache_2 = cache_for(nodes, 2);
@@ -431,7 +433,7 @@ async fn cluster_bulk_key_convergence() {
#[tokio::test] #[tokio::test]
async fn cluster_concurrent_multi_node_writes() { async fn cluster_concurrent_multi_node_writes() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let cache_1 = cache_for(nodes, 1); let cache_1 = cache_for(nodes, 1);
let cache_2 = cache_for(nodes, 2); let cache_2 = cache_for(nodes, 2);
@@ -540,7 +542,7 @@ async fn cluster_concurrent_multi_node_writes() {
#[tokio::test] #[tokio::test]
async fn cluster_rate_limit_multi_node_convergence() { async fn cluster_rate_limit_multi_node_convergence() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let rl_0 = rl_for(nodes, 0); let rl_0 = rl_for(nodes, 0);
let rl_1 = rl_for(nodes, 1); let rl_1 = rl_for(nodes, 1);
let rl_2 = rl_for(nodes, 2); let rl_2 = rl_for(nodes, 2);
@@ -630,7 +632,7 @@ fn create_account_on_node<'a>(
.expect("no accessJwt") .expect("no accessJwt")
.to_string(); .to_string();
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let user = repos let user = repos
.user .user
.get_by_did(&tranquil_types::Did::new(did.clone()).unwrap()) .get_by_did(&tranquil_types::Did::new(did.clone()).unwrap())
@@ -687,9 +689,9 @@ fn create_account_on_node<'a>(
} }
#[tokio::test] #[tokio::test]
async fn jail_cross_node_rate_limit_via_login() { async fn cross_node_rate_limit_via_login() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let client = crate::common::client(); let client = common::client();
let now_ms = u64::try_from( let now_ms = u64::try_from(
std::time::SystemTime::now() std::time::SystemTime::now()
@@ -770,8 +772,8 @@ async fn jail_cross_node_rate_limit_via_login() {
#[tokio::test] #[tokio::test]
async fn cross_node_handle_resolution_from_cache() { async fn cross_node_handle_resolution_from_cache() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let client = crate::common::client(); let client = common::client();
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let fake_handle = format!("cached-{}.test", uuid::Uuid::new_v4().simple()); let fake_handle = format!("cached-{}.test", uuid::Uuid::new_v4().simple());
@@ -823,8 +825,8 @@ async fn cross_node_handle_resolution_from_cache() {
#[tokio::test] #[tokio::test]
async fn cross_node_cache_delete_observable_via_http() { async fn cross_node_cache_delete_observable_via_http() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let client = crate::common::client(); let client = common::client();
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let cache_1 = cache_for(nodes, 1); let cache_1 = cache_for(nodes, 1);
@@ -890,8 +892,8 @@ async fn cross_node_cache_delete_observable_via_http() {
#[tokio::test] #[tokio::test]
async fn cross_node_email_update_status() { async fn cross_node_email_update_status() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let client = crate::common::client(); let client = common::client();
let cache_0 = cache_for(nodes, 0); let cache_0 = cache_for(nodes, 0);
let cache_1 = cache_for(nodes, 1); let cache_1 = cache_for(nodes, 1);
@@ -965,8 +967,8 @@ async fn cross_node_email_update_status() {
#[tokio::test] #[tokio::test]
async fn cross_node_session_revocation() { async fn cross_node_session_revocation() {
let nodes = crate::common::cluster().await; let nodes = common::cluster().await;
let client = crate::common::client(); let client = common::client();
let (token, _did) = create_account_on_node(&client, &nodes[0].url).await; let (token, _did) = create_account_on_node(&client, &nodes[0].url).await;
@@ -1,3 +1,4 @@
mod common;
use tranquil_pds::comms::{SendError, is_valid_phone_number}; use tranquil_pds::comms::{SendError, is_valid_phone_number};
use tranquil_pds::image::{ImageError, ImageProcessor}; use tranquil_pds::image::{ImageError, ImageProcessor};
@@ -83,7 +84,7 @@ fn test_send_error_display() {
#[tokio::test] #[tokio::test]
async fn test_signup_queue_authentication() { async fn test_signup_queue_authentication() {
use crate::common::{base_url, client, create_account_and_login}; use common::{base_url, client, create_account_and_login};
let base = base_url().await; let base = base_url().await;
let http_client = client(); let http_client = client();
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use crate::helpers::verify_new_account; mod helpers;
use common::*;
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,11 +1,13 @@
use crate::helpers::verify_new_account; mod common;
mod helpers;
use helpers::verify_new_account;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
#[tokio::test] #[tokio::test]
async fn test_reserve_signing_key_without_did() { async fn test_reserve_signing_key_without_did() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.reserveSigningKey", "{}/xrpc/com.atproto.server.reserveSigningKey",
@@ -27,9 +29,9 @@ async fn test_reserve_signing_key_without_did() {
#[tokio::test] #[tokio::test]
async fn test_reserve_signing_key_with_did() { async fn test_reserve_signing_key_with_did() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let target_did = "did:plc:test123456"; let target_did = "did:plc:test123456";
let res = client let res = client
.post(format!( .post(format!(
@@ -58,9 +60,9 @@ async fn test_reserve_signing_key_with_did() {
#[tokio::test] #[tokio::test]
async fn test_reserve_signing_key_stores_private_key() { async fn test_reserve_signing_key_stores_private_key() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.reserveSigningKey", "{}/xrpc/com.atproto.server.reserveSigningKey",
@@ -98,8 +100,8 @@ async fn test_reserve_signing_key_stores_private_key() {
#[tokio::test] #[tokio::test]
async fn test_reserve_signing_key_unique_keys() { async fn test_reserve_signing_key_unique_keys() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res1 = client let res1 = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.reserveSigningKey", "{}/xrpc/com.atproto.server.reserveSigningKey",
@@ -129,8 +131,8 @@ async fn test_reserve_signing_key_unique_keys() {
#[tokio::test] #[tokio::test]
async fn test_reserve_signing_key_is_public() { async fn test_reserve_signing_key_is_public() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.reserveSigningKey", "{}/xrpc/com.atproto.server.reserveSigningKey",
@@ -149,9 +151,9 @@ async fn test_reserve_signing_key_is_public() {
#[tokio::test] #[tokio::test]
async fn test_create_account_with_reserved_signing_key() { async fn test_create_account_with_reserved_signing_key() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let repos = crate::common::get_test_repos().await; let repos = common::get_test_repos().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.reserveSigningKey", "{}/xrpc/com.atproto.server.reserveSigningKey",
@@ -201,8 +203,8 @@ async fn test_create_account_with_reserved_signing_key() {
#[tokio::test] #[tokio::test]
async fn test_create_account_with_invalid_signing_key() { async fn test_create_account_with_invalid_signing_key() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let handle = format!("bk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let handle = format!("bk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
let res = client let res = client
.post(format!( .post(format!(
@@ -225,8 +227,8 @@ async fn test_create_account_with_invalid_signing_key() {
#[tokio::test] #[tokio::test]
async fn test_create_account_cannot_reuse_signing_key() { async fn test_create_account_cannot_reuse_signing_key() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.reserveSigningKey", "{}/xrpc/com.atproto.server.reserveSigningKey",
@@ -278,8 +280,8 @@ async fn test_create_account_cannot_reuse_signing_key() {
#[tokio::test] #[tokio::test]
async fn test_reserved_key_tokens_work() { async fn test_reserved_key_tokens_work() {
let client = crate::common::client(); let client = common::client();
let base_url = crate::common::base_url().await; let base_url = common::base_url().await;
let res = client let res = client
.post(format!( .post(format!(
"{}/xrpc/com.atproto.server.reserveSigningKey", "{}/xrpc/com.atproto.server.reserveSigningKey",
@@ -1,4 +1,6 @@
use crate::common::{base_url, client, create_account_and_login, get_test_repos}; mod common;
use common::{base_url, client, create_account_and_login, get_test_repos};
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::{Value, json}; use serde_json::{Value, json};
use tranquil_db_traits::{CommsChannel, CommsType, SsoAction, SsoProviderType}; use tranquil_db_traits::{CommsChannel, CommsType, SsoAction, SsoProviderType};
@@ -1,3 +1,6 @@
mod common;
mod helpers;
use std::sync::Arc; use std::sync::Arc;
use tranquil_db::PostgresRepositories; use tranquil_db::PostgresRepositories;
use tranquil_db_traits::{Backlink, BacklinkPath, CommsChannel, CommsType}; use tranquil_db_traits::{Backlink, BacklinkPath, CommsChannel, CommsType};
@@ -82,7 +85,7 @@ async fn create_store_repos() -> Arc<PostgresRepositories> {
} }
async fn create_pg_repos() -> Arc<PostgresRepositories> { async fn create_pg_repos() -> Arc<PostgresRepositories> {
let db_url = crate::common::get_db_connection_string().await; let db_url = common::get_db_connection_string().await;
let pool = sqlx::postgres::PgPoolOptions::new() let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(&db_url) .connect(&db_url)
@@ -114,7 +117,7 @@ fn test_handle(suffix: &str) -> Handle {
} }
fn test_cid(seed: u8) -> CidLink { fn test_cid(seed: u8) -> CidLink {
CidLink::from_cid(&crate::helpers::make_cid(&[seed])) CidLink::from_cid(&helpers::make_cid(&[seed]))
} }
fn test_nsid(name: &str) -> Nsid { fn test_nsid(name: &str) -> Nsid {
@@ -136,7 +139,7 @@ fn test_at_uri(did: &Did, collection: &Nsid, rkey: &Rkey) -> AtUri {
} }
async fn seed_user(repos: &PostgresRepositories, did: &Did, handle: &Handle) -> Uuid { async fn seed_user(repos: &PostgresRepositories, did: &Did, handle: &Handle) -> Uuid {
let commit_cid = CidLink::from_cid(&crate::helpers::make_cid(did.as_str().as_bytes())); let commit_cid = CidLink::from_cid(&helpers::make_cid(did.as_str().as_bytes()));
let input = tranquil_db_traits::CreatePasswordAccountInput { let input = tranquil_db_traits::CreatePasswordAccountInput {
handle: handle.clone(), handle: handle.clone(),
email: None, email: None,
@@ -1777,7 +1780,7 @@ async fn parity_prune_events_older_than() {
let baseline = f.pg.repo.get_max_seq().await.unwrap(); let baseline = f.pg.repo.get_max_seq().await.unwrap();
f.pg.repo.insert_commit_event(&event).await.unwrap(); f.pg.repo.insert_commit_event(&event).await.unwrap();
f.store.repo.insert_commit_event(&event).await.unwrap(); f.store.repo.insert_commit_event(&event).await.unwrap();
let pg_seq = crate::common::sequenced_event_for_did(&f.pg, baseline, &did) let pg_seq = common::sequenced_event_for_did(&f.pg, baseline, &did)
.await .await
.seq; .seq;
@@ -1,4 +1,5 @@
use crate::common::*; mod common;
use common::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use reqwest::header; use reqwest::header;
use serde_json::Value; use serde_json::Value;
@@ -1,5 +1,8 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::Value; use serde_json::Value;
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::Value; use serde_json::Value;
@@ -1,5 +1,7 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use reqwest::header; use reqwest::header;
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1,6 +1,8 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use helpers::*;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde_json::json; use serde_json::json;
use std::sync::LazyLock; use std::sync::LazyLock;
@@ -1,6 +1,7 @@
use bytes::Bytes; use bytes::Bytes;
use cid::Cid; use cid::Cid;
use std::collections::HashMap; use std::collections::HashMap;
mod common;
#[tokio::test] #[tokio::test]
#[ignore = "depends on external live server state; run manually with --ignored"] #[ignore = "depends on external live server state; run manually with --ignored"]
@@ -1,7 +1,10 @@
use crate::common::*; mod common;
use crate::helpers::*; mod helpers;
use chrono::Utc; use chrono::Utc;
use common::*;
use futures::{StreamExt, future::join_all}; use futures::{StreamExt, future::join_all};
use helpers::*;
use k256::ecdsa::SigningKey; use k256::ecdsa::SigningKey;
use reqwest::{StatusCode, header}; use reqwest::{StatusCode, header};
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -1168,7 +1171,7 @@ async fn test_backup_restore_workflow() {
} }
#[tokio::test] #[tokio::test]
async fn jail_test_scale_1000_posts_with_pagination() { async fn test_scale_1000_posts_with_pagination() {
let client = client(); let client = client();
let base = base_url().await; let base = base_url().await;
let (did, jwt) = setup_new_user("scale-posts").await; let (did, jwt) = setup_new_user("scale-posts").await;
@@ -1279,7 +1282,7 @@ async fn jail_test_scale_1000_posts_with_pagination() {
} }
#[tokio::test] #[tokio::test]
async fn jail_test_scale_many_users_social_graph() { async fn test_scale_many_users_social_graph() {
let client = client(); let client = client();
let base = base_url().await; let base = base_url().await;
@@ -1690,7 +1693,7 @@ async fn test_scale_batch_operations() {
} }
#[tokio::test] #[tokio::test]
async fn jail_test_scale_reply_thread_depth() { async fn test_scale_reply_thread_depth() {
let client = client(); let client = client();
let base = base_url().await; let base = base_url().await;
let (did, jwt) = setup_new_user("deep-thread").await; let (did, jwt) = setup_new_user("deep-thread").await;
+2 -2
View File
@@ -1327,7 +1327,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn jail_timed_out_write_resets_stream_instead_of_truncating() { async fn timed_out_write_resets_stream_instead_of_truncating() {
let shutdown = CancellationToken::new(); let shutdown = CancellationToken::new();
let (sender, _rx_sender) = let (sender, _rx_sender) =
Transport::bind("127.0.0.1:0".parse().unwrap(), None, shutdown.clone()) Transport::bind("127.0.0.1:0".parse().unwrap(), None, shutdown.clone())
@@ -1368,7 +1368,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn jail_write_timeout_keeps_connection() { async fn write_timeout_keeps_connection() {
use futures::StreamExt; use futures::StreamExt;
let shutdown = CancellationToken::new(); let shutdown = CancellationToken::new();
@@ -36,22 +36,8 @@ async fn spawn_pair(
.await .await
.expect("node B failed to start"); .expect("node B failed to start");
cache_a tokio::time::sleep(Duration::from_millis(2000)).await;
.set("__ready_a", "1", Duration::from_secs(60))
.await
.expect("readiness probe on A failed");
cache_b
.set("__ready_b", "1", Duration::from_secs(60))
.await
.expect("readiness probe on B failed");
poll_until(10_000, 50, || {
let cache_a = cache_a.clone();
let cache_b = cache_b.clone();
async move {
cache_b.get("__ready_a").await.is_some() && cache_a.get("__ready_b").await.is_some()
}
})
.await;
((cache_a, rl_a), (cache_b, rl_b)) ((cache_a, rl_a), (cache_b, rl_b))
} }
@@ -794,5 +780,7 @@ async fn two_node_stress_concurrent_load() {
r.unwrap_or_else(|e| panic!("task {i} panicked: {e}")); r.unwrap_or_else(|e| panic!("task {i} panicked: {e}"));
}); });
tokio::time::sleep(Duration::from_secs(12)).await;
shutdown.cancel(); shutdown.cancel();
} }
+5 -1
View File
@@ -92,7 +92,11 @@ fn blob_covers(g: &BlobScope, r: &BlobScope) -> bool {
} }
fn rpc_covers(g: &RpcScope, r: &RpcScope) -> bool { fn rpc_covers(g: &RpcScope, r: &RpcScope) -> bool {
let lxm_ok = g.lxm == "*" || g.lxm == r.lxm; let lxm_ok = match &g.lxm {
None => true,
Some(gl) if gl == "*" => true,
Some(gl) => r.lxm.as_deref() == Some(gl.as_str()),
};
let aud_ok = match &g.aud { let aud_ok = match &g.aud {
None => true, None => true,
Some(ga) if ga == "*" => true, Some(ga) if ga == "*" => true,
+9 -13
View File
@@ -93,7 +93,7 @@ impl BlobScope {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct RpcScope { pub struct RpcScope {
pub lxm: String, pub lxm: Option<String>,
pub aud: Option<String>, pub aud: Option<String>,
} }
@@ -245,10 +245,9 @@ pub fn parse_scope(scope: &str) -> ParsedScope {
let lxm = lxm_positional.or_else(|| params.get("lxm").and_then(|v| v.first().cloned())); let lxm = lxm_positional.or_else(|| params.get("lxm").and_then(|v| v.first().cloned()));
let aud = params.get("aud").and_then(|v| v.first().cloned()); let aud = params.get("aud").and_then(|v| v.first().cloned());
let Some(lxm) = lxm else { let is_lxm_wildcard = lxm.as_deref() == Some("*") || lxm.is_none();
return ParsedScope::Unknown(scope.to_string()); let is_aud_wildcard = aud.as_deref() == Some("*");
}; if is_lxm_wildcard && is_aud_wildcard {
if lxm == "*" && aud.as_deref() == Some("*") {
return ParsedScope::Unknown(scope.to_string()); return ParsedScope::Unknown(scope.to_string());
} }
@@ -401,7 +400,7 @@ mod tests {
let scope = parse_scope("rpc:app.bsky.feed.getTimeline?aud=did:web:api.bsky.app"); let scope = parse_scope("rpc:app.bsky.feed.getTimeline?aud=did:web:api.bsky.app");
match scope { match scope {
ParsedScope::Rpc(r) => { ParsedScope::Rpc(r) => {
assert_eq!(r.lxm, "app.bsky.feed.getTimeline"); assert_eq!(r.lxm, Some("app.bsky.feed.getTimeline".to_string()));
assert_eq!(r.aud, Some("did:web:api.bsky.app".to_string())); assert_eq!(r.aud, Some("did:web:api.bsky.app".to_string()));
} }
_ => panic!("Expected Rpc scope"), _ => panic!("Expected Rpc scope"),
@@ -511,12 +510,6 @@ mod tests {
assert!(matches!(scope4, ParsedScope::Rpc(_))); assert!(matches!(scope4, ParsedScope::Rpc(_)));
} }
#[test]
fn test_rpc_lxm_required() {
let bare = parse_scope("rpc");
assert!(matches!(bare, ParsedScope::Unknown(_)));
}
#[test] #[test]
fn test_url_encoded_aud_with_fragment() { fn test_url_encoded_aud_with_fragment() {
let scope = let scope =
@@ -534,7 +527,10 @@ mod tests {
); );
match scope2 { match scope2 {
ParsedScope::Rpc(r) => { ParsedScope::Rpc(r) => {
assert_eq!(r.lxm, "com.atproto.moderation.createReport"); assert_eq!(
r.lxm,
Some("com.atproto.moderation.createReport".to_string())
);
assert_eq!(r.aud, Some("did:web:api.bsky.app#bsky_appview".to_string())); assert_eq!(r.aud, Some("did:web:api.bsky.app#bsky_appview".to_string()));
} }
_ => panic!("Expected Rpc scope"), _ => panic!("Expected Rpc scope"),
+8 -23
View File
@@ -49,7 +49,10 @@ impl ScopePermissions {
pub fn superseded_by_transition_generic(scope: &ParsedScope) -> bool { pub fn superseded_by_transition_generic(scope: &ParsedScope) -> bool {
match scope { match scope {
ParsedScope::Repo(_) | ParsedScope::Blob(_) => true, ParsedScope::Repo(_) | ParsedScope::Blob(_) => true,
ParsedScope::Rpc(rpc) => !(rpc.lxm == "*" || rpc.lxm.starts_with("chat.bsky.")), ParsedScope::Rpc(rpc) => !rpc
.lxm
.as_deref()
.is_some_and(|lxm| lxm == "*" || lxm.starts_with("chat.bsky.")),
ParsedScope::Account(_) ParsedScope::Account(_)
| ParsedScope::Identity(_) | ParsedScope::Identity(_)
| ParsedScope::TransitionEmail | ParsedScope::TransitionEmail
@@ -190,9 +193,9 @@ impl ScopePermissions {
let has_permission = self.find_rpc_scopes().any(|rpc_scope| { let has_permission = self.find_rpc_scopes().any(|rpc_scope| {
let lxm_matches = match &rpc_scope.lxm { let lxm_matches = match &rpc_scope.lxm {
scope_lxm if scope_lxm == "*" => true, None => true,
scope_lxm if scope_lxm == lxm => true, Some(scope_lxm) if scope_lxm == lxm => true,
scope_lxm if scope_lxm.ends_with(".*") => { Some(scope_lxm) if scope_lxm.ends_with(".*") => {
let prefix = scope_lxm.strip_suffix(".*").unwrap(); let prefix = scope_lxm.strip_suffix(".*").unwrap();
lxm.starts_with(prefix) && lxm.chars().nth(prefix.len()) == Some('.') lxm.starts_with(prefix) && lxm.chars().nth(prefix.len()) == Some('.')
} }
@@ -200,7 +203,7 @@ impl ScopePermissions {
}; };
let aud_matches = match &rpc_scope.aud { let aud_matches = match &rpc_scope.aud {
None => false, None => true,
Some(scope_aud) if scope_aud == "*" => true, Some(scope_aud) if scope_aud == "*" => true,
Some(scope_aud) => scope_aud == aud, Some(scope_aud) => scope_aud == aud,
}; };
@@ -471,24 +474,6 @@ mod tests {
assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed"))); assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed")));
} }
#[test]
fn test_rpc_wildcard_lxm() {
let perms = ScopePermissions::from_scope_string(Some(
"rpc:*?aud=did:web:api.bsky.app#bsky_appview",
));
let aud = "did:web:api.bsky.app#bsky_appview";
let other = "did:web:other.app#bsky_appview";
assert!(perms.allows_rpc(aud, &c("app.bsky.feed.getTimeline")));
assert!(!perms.allows_rpc(other, &c("app.bsky.feed.getTimeline")));
}
#[test]
fn test_rpc_wildcard_lxm_without_aud() {
let perms = ScopePermissions::from_scope_string(Some("rpc:*"));
let aud = "did:web:api.bsky.app#bsky_appview";
assert!(!perms.allows_rpc(aud, &c("app.bsky.feed.getTimeline")));
}
#[test] #[test]
fn test_granular_account() { fn test_granular_account() {
let perms = ScopePermissions::from_scope_string(Some("account:email?action=read")); let perms = ScopePermissions::from_scope_string(Some("account:email?action=read"));
+3 -9
View File
@@ -5,24 +5,18 @@ edition.workspace = true
license.workspace = true license.workspace = true
[features] [features]
s3 = ["dep:s3"] s3 = ["dep:aws-config", "dep:aws-sdk-s3"]
[dependencies] [dependencies]
tranquil-config = { workspace = true } tranquil-config = { workspace = true }
tranquil-infra = { workspace = true } tranquil-infra = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
aws-config = { workspace = true, optional = true }
aws-sdk-s3 = { workspace = true, optional = true }
bytes = { workspace = true } bytes = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
s3 = { workspace = true, optional = true }
sha2 = { workspace = true } sha2 = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
[dev-dependencies]
hyper-util = { workspace = true }
s3s = { workspace = true }
s3s-fs = { workspace = true }
tempfile = "3"
tokio = { workspace = true, features = ["net"] }
+126 -82
View File
@@ -102,8 +102,12 @@ fn map_io_not_found(key: &str) -> impl FnOnce(std::io::Error) -> StorageError +
#[cfg(feature = "s3")] #[cfg(feature = "s3")]
mod s3 { mod s3 {
use super::*; use super::*;
use ::s3::types::CompletedPart; use aws_config::BehaviorVersion;
use ::s3::{AddressingStyle, Auth, Client}; use aws_config::meta::region::RegionProviderChain;
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::CompletedMultipartUpload;
use aws_sdk_s3::types::CompletedPart;
const MIN_PART_SIZE: usize = 5 * 1024 * 1024; const MIN_PART_SIZE: usize = 5 * 1024 * 1024;
@@ -114,14 +118,24 @@ mod s3 {
} }
impl S3BlobStorage { impl S3BlobStorage {
pub async fn new(bucket: &str, endpoint: Option<&str>, path: &str) -> Self { pub async fn new() -> Self {
Self { let cfg = tranquil_config::get();
client: create_s3_client(endpoint), let bucket = cfg
bucket: bucket.to_string(), .storage
path: path .s3_bucket
.clone()
.expect("storage.s3_bucket (S3_BUCKET) must be set");
let client = create_s3_client().await;
let path = cfg
.storage
.s3_path
.trim_start_matches("/") .trim_start_matches("/")
.trim_end_matches("/") .trim_end_matches("/")
.to_string(), .to_string();
Self {
client,
bucket,
path,
} }
} }
@@ -134,26 +148,28 @@ mod s3 {
} }
} }
fn create_s3_client(endpoint: Option<&str>) -> Client { async fn create_s3_client() -> Client {
let region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string()); let region_provider = RegionProviderChain::default_provider().or_else("us-east-1");
let (url, style) = match endpoint {
Some(endpoint) => (endpoint.to_string(), AddressingStyle::Path), let config = aws_config::defaults(BehaviorVersion::latest())
None => ( .region(region_provider)
format!("https://s3.{region}.amazonaws.com"), .load()
AddressingStyle::Auto, .await;
),
}; tranquil_config::get()
let auth = .storage
Auth::from_env().expect("AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set"); .s3_endpoint
Client::builder(url) .as_deref()
.expect("storage.s3_endpoint (S3_ENDPOINT) must be a valid URL") .map_or_else(
.region(region) || Client::new(&config),
.addressing_style(style) |endpoint| {
.auth(auth) let s3_config = aws_sdk_s3::config::Builder::from(&config)
.base_retry_delay(std::time::Duration::from_secs(1)) .endpoint_url(endpoint)
.max_retry_delay(std::time::Duration::from_secs(20)) .force_path_style(true)
.build() .build();
.expect("failed to build S3 client") Client::from_conf(s3_config)
},
)
} }
#[async_trait] #[async_trait]
@@ -165,9 +181,10 @@ mod s3 {
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError> { async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError> {
let path = self.resolve_path(key); let path = self.resolve_path(key);
self.client self.client
.objects() .put_object()
.put(&self.bucket, path) .bucket(&self.bucket)
.body_bytes(data) .key(&path)
.body(ByteStream::from(data))
.send() .send()
.await .await
.map_err(|e| StorageError::Backend(e.to_string()))?; .map_err(|e| StorageError::Backend(e.to_string()))?;
@@ -183,39 +200,46 @@ mod s3 {
let path = self.resolve_path(key); let path = self.resolve_path(key);
let resp = self let resp = self
.client .client
.objects() .get_object()
.get(&self.bucket, &path) .bucket(&self.bucket)
.key(&path)
.send() .send()
.await .await
.map_err(|e| StorageError::Backend(e.to_string()))?; .map_err(|e| StorageError::Backend(e.to_string()))?;
resp.bytes() resp.body
.collect()
.await .await
.map(|agg| agg.into_bytes())
.map_err(|e| StorageError::Backend(e.to_string())) .map_err(|e| StorageError::Backend(e.to_string()))
} }
async fn get_head(&self, key: &str, size: usize) -> Result<Bytes, StorageError> { async fn get_head(&self, key: &str, size: usize) -> Result<Bytes, StorageError> {
let path = self.resolve_path(key); let path = self.resolve_path(key);
let range = format!("bytes=0-{}", size.saturating_sub(1));
let resp = self let resp = self
.client .client
.objects() .get_object()
.get(&self.bucket, &path) .bucket(&self.bucket)
.range_bytes(0, size.saturating_sub(1) as u64) .key(&path)
.map_err(|e| StorageError::Backend(e.to_string()))? .range(range)
.send() .send()
.await .await
.map_err(|e| StorageError::Backend(e.to_string()))?; .map_err(|e| StorageError::Backend(e.to_string()))?;
resp.bytes() resp.body
.collect()
.await .await
.map(|agg| agg.into_bytes())
.map_err(|e| StorageError::Backend(e.to_string())) .map_err(|e| StorageError::Backend(e.to_string()))
} }
async fn delete(&self, key: &str) -> Result<(), StorageError> { async fn delete(&self, key: &str) -> Result<(), StorageError> {
let path = self.resolve_path(key); let path = self.resolve_path(key);
self.client self.client
.objects() .delete_object()
.delete(&self.bucket, &path) .bucket(&self.bucket)
.key(&path)
.send() .send()
.await .await
.map_err(|e| StorageError::Backend(e.to_string()))?; .map_err(|e| StorageError::Backend(e.to_string()))?;
@@ -226,27 +250,32 @@ mod s3 {
async fn put_stream( async fn put_stream(
&self, &self,
key: &str, key: &str,
mut stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>, stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
) -> Result<StreamUploadResult, StorageError> { ) -> Result<StreamUploadResult, StorageError> {
use futures::StreamExt; use futures::StreamExt;
let path = self.resolve_path(key); let path = self.resolve_path(key);
let upload_id = self let create_resp = self
.client .client
.objects() .create_multipart_upload()
.create_multipart_upload(&self.bucket, &path) .bucket(&self.bucket)
.key(&path)
.send() .send()
.await .await
.map_err(|e| { .map_err(|e| {
StorageError::Backend(format!("Failed to create multipart upload: {}", e)) StorageError::Backend(format!("Failed to create multipart upload: {}", e))
})? })?;
.upload_id;
let upload_id = create_resp
.upload_id()
.ok_or_else(|| StorageError::Backend("No upload ID returned".to_string()))?
.to_string();
let upload_part = |client: &Client, let upload_part = |client: &Client,
bucket: &str, bucket: &str,
key: &str, key: &str,
upload_id: &str, upload_id: &str,
part_num: u32, part_num: i32,
data: Vec<u8>| data: Vec<u8>|
-> std::pin::Pin< -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<CompletedPart, StorageError>> + Send>, Box<dyn std::future::Future<Output = Result<CompletedPart, StorageError>> + Send>,
@@ -257,28 +286,36 @@ mod s3 {
let upload_id = upload_id.to_string(); let upload_id = upload_id.to_string();
Box::pin(async move { Box::pin(async move {
let resp = client let resp = client
.objects() .upload_part()
.upload_part(&bucket, &path, &upload_id, part_num) .bucket(&bucket)
.body_bytes(data) .key(&path)
.upload_id(&upload_id)
.part_number(part_num)
.body(ByteStream::from(data))
.send() .send()
.await .await
.map_err(|e| { .map_err(|e| {
StorageError::Backend(format!("Failed to upload part: {}", e)) StorageError::Backend(format!("Failed to upload part: {}", e))
})?; })?;
let etag = resp.etag.ok_or_else(|| { let etag = resp
.e_tag()
.ok_or_else(|| {
StorageError::Backend("No ETag returned for part".to_string()) StorageError::Backend("No ETag returned for part".to_string())
})?; })?
.to_string();
CompletedPart::new(part_num, etag) Ok(CompletedPart::builder()
.map_err(|e| StorageError::Backend(e.to_string())) .part_number(part_num)
.e_tag(etag)
.build())
}) })
}; };
struct UploadState { struct UploadState {
hasher: Sha256, hasher: Sha256,
total_size: u64, total_size: u64,
part_number: u32, part_number: i32,
completed_parts: Vec<CompletedPart>, completed_parts: Vec<CompletedPart>,
buffer: Vec<u8>, buffer: Vec<u8>,
} }
@@ -294,16 +331,20 @@ mod s3 {
let abort_upload = || async { let abort_upload = || async {
let _ = self let _ = self
.client .client
.objects() .abort_multipart_upload()
.abort_multipart_upload(&self.bucket, path, &upload_id) .bucket(&self.bucket)
.key(path)
.upload_id(&upload_id)
.send() .send()
.await; .await;
}; };
let result: Result<StreamUploadResult, StorageError> = async { let result: Result<UploadState, StorageError> = {
let mut state = initial_state; let mut state = initial_state;
while let Some(chunk_result) = stream.next().await { let chunk_results: Vec<Result<Bytes, std::io::Error>> = stream.collect().await;
for chunk_result in chunk_results {
match chunk_result { match chunk_result {
Ok(chunk) => { Ok(chunk) => {
state.hasher.update(&chunk); state.hasher.update(&chunk);
@@ -328,9 +369,17 @@ mod s3 {
state.part_number += 1; state.part_number += 1;
} }
} }
Err(e) => return Err(StorageError::Io(e)), Err(e) => {
abort_upload().await;
return Err(StorageError::Io(e));
} }
} }
}
Ok(state)
};
let mut state = result?;
if !state.buffer.is_empty() { if !state.buffer.is_empty() {
let part = upload_part( let part = upload_part(
@@ -346,15 +395,21 @@ mod s3 {
} }
if state.completed_parts.is_empty() { if state.completed_parts.is_empty() {
abort_upload().await;
return Err(StorageError::Other("Empty upload".to_string())); return Err(StorageError::Other("Empty upload".to_string()));
} }
let completed_upload = CompletedMultipartUpload::builder()
.set_parts(Some(state.completed_parts))
.build();
let path = self.resolve_path(key); let path = self.resolve_path(key);
self.client self.client
.objects() .complete_multipart_upload()
.complete_multipart_upload(&self.bucket, &path, &upload_id) .bucket(&self.bucket)
.parts(state.completed_parts) .key(&path)
.map_err(|e| StorageError::Backend(e.to_string()))? .upload_id(&upload_id)
.multipart_upload(completed_upload)
.send() .send()
.await .await
.map_err(|e| { .map_err(|e| {
@@ -367,21 +422,17 @@ mod s3 {
size: state.total_size, size: state.total_size,
}) })
} }
.await;
if result.is_err() {
abort_upload().await;
}
result
}
async fn copy(&self, src_key: &str, dst_key: &str) -> Result<(), StorageError> { async fn copy(&self, src_key: &str, dst_key: &str) -> Result<(), StorageError> {
let src_path = self.resolve_path(src_key); let src_path = self.resolve_path(src_key);
let copy_source = format!("{}/{}", self.bucket, &src_path);
let dst_path = self.resolve_path(dst_key); let dst_path = self.resolve_path(dst_key);
self.client self.client
.objects() .copy_object()
.copy(&self.bucket, &src_path, &self.bucket, &dst_path) .bucket(&self.bucket)
.copy_source(copy_source)
.key(&dst_path)
.send() .send()
.await .await
.map_err(|e| StorageError::Backend(format!("Failed to copy object: {}", e)))?; .map_err(|e| StorageError::Backend(format!("Failed to copy object: {}", e)))?;
@@ -546,14 +597,7 @@ pub async fn create_blob_storage() -> Arc<dyn BlobStorage> {
#[cfg(feature = "s3")] #[cfg(feature = "s3")]
"s3" => { "s3" => {
tracing::info!("Initializing S3 blob storage"); tracing::info!("Initializing S3 blob storage");
let storage = &cfg.storage; Arc::new(S3BlobStorage::new().await)
let bucket = storage
.s3_bucket
.as_deref()
.expect("storage.s3_bucket (S3_BUCKET) must be set");
Arc::new(
S3BlobStorage::new(bucket, storage.s3_endpoint.as_deref(), &storage.s3_path).await,
)
} }
#[cfg(not(feature = "s3"))] #[cfg(not(feature = "s3"))]
"s3" => { "s3" => {

Some files were not shown because too many files have changed in this diff Show More