15056dc7df
- 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
50 lines
1.8 KiB
Rust
50 lines
1.8 KiB
Rust
//! End to end: device A creates through E-32, device B pulls through E-31 and sees the row and its audit entry
|
|
//! (AC-12.3); exactly one push is queued on the real queue, for the other user's devices.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use kvitt_server::push::{PushHub, PushType};
|
|
|
|
use crate::common::*;
|
|
use crate::m4::*;
|
|
|
|
#[tokio::test]
|
|
async fn create_on_device_a_pull_on_device_b_row_audit_and_one_push() {
|
|
let mut app = app().await;
|
|
let hub = Arc::new(PushHub::new());
|
|
with_push(&mut app, hub.clone());
|
|
let id = new_id();
|
|
let cursor_b = pull(&app, &member_token(), 0, Some(1000)).await.json["next_since"]
|
|
.as_i64()
|
|
.unwrap();
|
|
|
|
let result = push_one(
|
|
&app,
|
|
&admin_token(),
|
|
exp(expense(&id, "2026-10-17T11:59:00.000Z")),
|
|
)
|
|
.await;
|
|
let created = accepted(&result);
|
|
|
|
let page = pull(&app, &member_token(), cursor_b, None).await.json;
|
|
assert_eq!(page["expenses"].as_array().unwrap().len(), 1);
|
|
assert_eq!(page["expenses"][0], created);
|
|
let audit = page["audit_entries"].as_array().unwrap();
|
|
assert_eq!(audit.len(), 1);
|
|
assert_eq!(audit[0]["entity_id"], id);
|
|
assert_eq!(audit[0]["action"], "create");
|
|
assert_eq!(audit[0]["source"], "app");
|
|
assert_eq!(audit[0]["actor_user_id"], ADMIN_ID);
|
|
assert_eq!(audit[0]["client_at"], "2026-10-17T11:59:00.000Z");
|
|
assert_eq!(audit[0]["changes"].as_array().unwrap().len(), 9);
|
|
assert!(audit[0]["server_version"].as_i64() > created["server_version"].as_i64());
|
|
assert_eq!(page["next_since"], audit[0]["server_version"]);
|
|
|
|
assert_eq!(hub.len(), 1);
|
|
let message = hub.pop().unwrap();
|
|
assert_eq!(message.push_type, PushType::ExpenseCreated);
|
|
assert_eq!(message.entity_id.unwrap().to_string(), id);
|
|
assert_eq!(message.actor_user_id.to_string(), ADMIN_ID);
|
|
assert_eq!(message.count, 1);
|
|
}
|