Server gap pass: invite-partner subcommand, CLI and drain tests, push info log (D-113, D-114)
- kvitt-server invite-partner: a partner invite by the admin as E-12 does (cancels the pending one, 72 h, link with ?from=<admin>), exit 2 household full, 3 no admin; README lines in server/ and infra/. - Tests: invite-partner, migrate (schema at SCHEMA_VERSION, idempotent), healthcheck (200, 503, nothing listening), serve SIGTERM drain (in-flight request finishes, new connections refused, exit 0; gives up after SHUTDOWN_DRAIN). - Push sender: one info line "push sent" per message with push_type, devices_sent, devices_targeted and 8-character device-id tails (SPEC §8.7 aggregates); per-device line stays at debug. - tools/contracts-check.py: stale TODO(spec) replaced by the §9.11 CS-5 citation; shapes updated for invite_kind (D-086), app_version_name (D-090) and fid (D-096), 134 checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -465,3 +465,59 @@ async fn sync_push_reaches_fcm_through_the_queue_and_the_sender_task() {
|
||||
assert_eq!(body["message"]["data"]["entity_id"], id);
|
||||
assert_eq!(body["message"]["data"]["actor_user_id"], MEMBER_ID);
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Captured(Arc<std::sync::Mutex<Vec<u8>>>);
|
||||
|
||||
impl std::io::Write for Captured {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.0.lock().unwrap().extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The only test of this binary that installs a subscriber. Global, not thread-local, because tracing caches callsite
|
||||
/// interest process-wide; the other tests' lines are captured too, so only lines of this test's push type are read.
|
||||
#[tokio::test]
|
||||
async fn a_successful_send_logs_one_info_line_with_aggregates_only() {
|
||||
let captured = Captured::default();
|
||||
let writer = captured.clone();
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.json()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_writer(move || writer.clone())
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
|
||||
let s = Setup::new().await;
|
||||
let deliveries = s
|
||||
.sender()
|
||||
.deliver(&message(PushType::SettlementDeleted, MEMBER_ID))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(deliveries, [Delivery::Sent { attempts: 1 }]);
|
||||
|
||||
let logs = String::from_utf8(captured.0.lock().unwrap().clone()).unwrap();
|
||||
let lines: Vec<Value> = logs
|
||||
.lines()
|
||||
.map(|l| serde_json::from_str::<Value>(l).unwrap())
|
||||
.filter(|l| {
|
||||
l["fields"]["message"] == "push sent"
|
||||
&& l["fields"]["push_type"] == "settlement_deleted"
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(lines.len(), 1, "{logs}");
|
||||
let line = &lines[0];
|
||||
assert_eq!(line["level"], "INFO");
|
||||
assert_eq!(line["fields"]["devices_sent"], 1);
|
||||
assert_eq!(line["fields"]["devices_targeted"], 1);
|
||||
// The member acted, so the admin's one live device got it: its id's last 8 characters only.
|
||||
assert_eq!(line["fields"]["devices"], "00000001");
|
||||
let text = line.to_string();
|
||||
for secret in ["tok-admin", ADMIN_DEVICE_ID, MEMBER_ID, EXPENSE] {
|
||||
assert!(!text.contains(secret), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user