Files
kvitt/server/crates/kvitt-server/tests/sync_api/serve.rs
T
jacobh2 15056dc7df M4: sync pull and push (E-31, E-32), audit writer and diff, FCM sender, push batching, scheduler, pruning, rotate-epoch
- src/sync/: pull with UNION ALL paging and cursor_ahead; push processing of SPEC §14.4.4 steps 1 to 12 in one
  BEGIN IMMEDIATE with a savepoint per mutation, LWW with delete-wins, tombstones, clock_skew, replay from
  sync_mutations, sync_batch for more than 3 notifiable changes; 90-day pruning.
- src/audit/: SqlAuditWriter and the §14.4.7 diff (now the default in AppState::new).
- src/push/: bounded PushHub (100, drop oldest), FCM HTTP v1 client with service-account JWT (RS256, jsonwebtoken
  on aws-lc-rs) and token cache, sender with 1/5/25 s retries and token clearing; disabled without credentials.
- src/tasks/: start installs push and spawns the 06:00 generation (and at start) and 04:00 pruning loops on the
  injectable clock.
- cli: rotate-epoch; serve uses into_make_service_with_connect_info; stale doc comments updated; /i/ empty code
  gets the HTML 404.
- tests: sync_api, push_fcm (local stub of Google's endpoints), scheduler_daily; skeleton routes test now asserts
  no route is 501.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-17 22:27:30 +02:00

91 lines
3.0 KiB
Rust

//! `serve` as deployed: the router with `ConnectInfo<SocketAddr>` (so the rate limiter sees each client's address,
//! SPEC §8.6), `/i/` with an empty code as the HTML 404 page, and `rotate-epoch` as the real binary (SPEC §10.4).
use std::net::SocketAddr;
use std::process::Command;
use axum::body::Body;
use axum::http::{Method, Request};
use kvitt_server::cli::make_service;
use tokio::net::TcpListener;
use crate::common::*;
#[tokio::test]
async fn two_client_addresses_get_separate_rate_limit_buckets_end_to_end() {
let app = TestApp::empty().await;
// One service, so one rate limiter, reached from two client addresses: 127.0.0.1 and ::1.
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let v4 = listener.local_addr().unwrap();
let listener6 = TcpListener::bind("[::1]:0").await.unwrap();
let v6 = listener6.local_addr().unwrap();
let service = make_service(app.state.clone());
let service6 = service.clone();
tokio::spawn(async move { axum::serve(listener, service).await.unwrap() });
tokio::spawn(async move { axum::serve(listener6, service6).await.unwrap() });
let client = reqwest::Client::new();
let get = |addr: SocketAddr| {
let client = client.clone();
async move {
client
.get(format!("http://{addr}/health"))
.send()
.await
.unwrap()
.status()
.as_u16()
}
};
for _ in 0..60 {
assert_eq!(get(v4).await, 200);
}
assert_eq!(
get(v4).await,
429,
"the 61st request from 127.0.0.1 in the minute"
);
assert_eq!(get(v6).await, 200, "::1 has its own bucket");
}
#[tokio::test]
async fn empty_invite_code_is_the_html_404_page() {
let app = TestApp::empty().await;
let reply = app
.send(
Request::builder()
.method(Method::GET)
.uri("/i/")
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(reply.status, 404);
assert_eq!(reply.headers["content-type"], "text/html; charset=utf-8");
assert!(reply.text().contains("<p>404</p>"));
}
#[tokio::test]
async fn rotate_epoch_sets_a_new_random_epoch_and_exits_0() {
let app = TestApp::empty().await;
let epoch = || async {
sqlx::query_scalar::<_, String>("SELECT value FROM meta WHERE key = 'sync_epoch'")
.fetch_one(&app.state.db)
.await
.unwrap()
};
let before = epoch().await;
let output = Command::new(env!("CARGO_BIN_EXE_kvitt-server"))
.arg("rotate-epoch")
.env("KVITT_DATA_DIR", app.data_dir())
.env("RUST_LOG", "warn")
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0), "{output:?}");
let after = epoch().await;
assert_ne!(before, after);
assert_eq!(after.len(), 32);
assert!(after.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')));
assert!(String::from_utf8_lossy(&output.stdout).contains(&after));
}