A MemoryStore meant every restart signed out every desk on the site being deployed. Bearable while a desk is three people, and not the kind of thing that gets better on its own. They live in NATS KV now: the JetStream already under the events, the projections, the content reload and the access questions. No new service, no new store, and the same bucket a second instance of a site would read if there ever is one. `create` draws a fresh id rather than writing over a session already at that key, the bucket's `max_age` collects what nobody comes back for, and a record's own expiry is what answers a load, whatever the bucket still holds. Proved the only way it can be: a store opened twice against one JetStream, which is what two runs of the binary are. Also fixed, while there: the integration test asserted a record's projected state was one that `aggregates::transition` never writes - it appends events, the desk handler updates the projection - so it failed whenever it was actually run. It now asserts what it says it does, that a mailed reply leaves the state alone. test.yml has been red since 0.3.47 while publish.yml stayed green, which is how v0.4.0 went out over it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d5de25e8c6
commit
214d4eb85c
+61
-1
@@ -68,6 +68,7 @@ async fn a_submission_is_created_moved_refused_and_read_back_through_the_real_en
|
||||
// through the same store the desks read - and moves nothing: the
|
||||
// record is still where the desk left it.
|
||||
let mut record: Answer = serde_json::from_slice(&store.get("rec-1").await.unwrap().expect("record")).unwrap();
|
||||
let state_before_the_note = record.state.clone();
|
||||
let reply = portal::mail::Incoming {
|
||||
v: portal::mail::CONTRACT,
|
||||
id: "mail-1".into(),
|
||||
@@ -86,10 +87,69 @@ async fn a_submission_is_created_moved_refused_and_read_back_through_the_real_en
|
||||
assert_eq!(read_back.notes.len(), 1);
|
||||
assert_eq!(read_back.notes[0].text, "Thursday suits us.");
|
||||
assert_eq!(read_back.notes[0].from, "a@b.example");
|
||||
assert_eq!(read_back.state, "quoted", "a reply is a note, never a decision");
|
||||
assert_eq!(read_back.state, state_before_the_note, "a reply is a note, never a decision");
|
||||
// The same reply read twice adds nothing.
|
||||
assert!(portal::mail::note_for(&reply, &read_back).is_err());
|
||||
// And the event log is untouched by it.
|
||||
let after = load_events(&js, &bucket, "rec-1").await.expect("events");
|
||||
assert_eq!(after.len(), 3, "a note writes no event");
|
||||
}
|
||||
|
||||
/// A session outlives the process that made it. The `MemoryStore` this
|
||||
/// replaced could not: every deploy signed out every desk, and the
|
||||
/// only proof that this one does not is a store opened twice against
|
||||
/// the same JetStream, as two runs of the binary would.
|
||||
#[tokio::test]
|
||||
async fn a_session_survives_the_process_that_made_it() {
|
||||
use tower_sessions::session::{Id, Record};
|
||||
use tower_sessions::SessionStore;
|
||||
|
||||
let Ok(url) = std::env::var("PORTAL_TEST_NATS_URL") else {
|
||||
eprintln!("skipped: PORTAL_TEST_NATS_URL is not set");
|
||||
return;
|
||||
};
|
||||
let js = async_nats::jetstream::new(async_nats::connect(&url).await.expect("connect"));
|
||||
let store = portal::sessions::NatsStore::open(&js).await.expect("open");
|
||||
|
||||
let mut record = Record {
|
||||
id: Id::default(),
|
||||
data: Default::default(),
|
||||
expiry_date: tower_sessions::cookie::time::OffsetDateTime::now_utc()
|
||||
+ tower_sessions::cookie::time::Duration::hours(1),
|
||||
};
|
||||
record.data.insert("who".into(), json!("ingrid"));
|
||||
store.create(&mut record).await.expect("create");
|
||||
|
||||
// A second store over the same JetStream is what the next boot of
|
||||
// the binary has: nothing carried over in memory.
|
||||
let next_boot = portal::sessions::NatsStore::open(&js).await.expect("reopen");
|
||||
let found = next_boot.load(&record.id).await.expect("load").expect("still there");
|
||||
assert_eq!(found.data.get("who"), Some(&json!("ingrid")));
|
||||
|
||||
// `create` never writes over somebody else's session: asked to
|
||||
// create one whose id is taken, it takes a new id instead.
|
||||
let mut collide = Record { id: record.id, data: Default::default(), expiry_date: record.expiry_date };
|
||||
next_boot.create(&mut collide).await.expect("create beside it");
|
||||
assert_ne!(collide.id, record.id, "an id already in use must not be reused");
|
||||
assert_eq!(
|
||||
next_boot.load(&record.id).await.expect("load").expect("original intact").data.get("who"),
|
||||
Some(&json!("ingrid")),
|
||||
"the original session must be untouched"
|
||||
);
|
||||
|
||||
// An expired record is gone to anyone asking, whatever the bucket
|
||||
// still holds.
|
||||
let stale = Record {
|
||||
id: Id::default(),
|
||||
data: Default::default(),
|
||||
expiry_date: tower_sessions::cookie::time::OffsetDateTime::now_utc()
|
||||
- tower_sessions::cookie::time::Duration::minutes(1),
|
||||
};
|
||||
next_boot.save(&stale).await.expect("save stale");
|
||||
assert!(next_boot.load(&stale.id).await.expect("load stale").is_none());
|
||||
|
||||
// Signing out leaves nothing to read back.
|
||||
next_boot.delete(&record.id).await.expect("delete");
|
||||
assert!(next_boot.load(&record.id).await.expect("load after delete").is_none());
|
||||
next_boot.delete(&collide.id).await.ok();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user