Skip to content
Merged
Show file tree
Hide file tree
Changes from 102 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
9f009c7
feat(be): record that a session is still in use
sea-snake Aug 22, 2026
2c6ba71
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Aug 25, 2026
275084b
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Aug 25, 2026
3cd69a2
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Aug 25, 2026
c937d0c
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Aug 31, 2026
5d59340
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Aug 31, 2026
bcaa704
merge: carry the unit rename into the refresh stamps
sea-snake Aug 31, 2026
03c726b
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Aug 31, 2026
287a699
merge: one predicate, and a default idle bound
sea-snake Aug 31, 2026
06882bc
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 1, 2026
f1c4ace
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 1, 2026
0a98cf1
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 1, 2026
496a3d2
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 1, 2026
6cf9f86
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 1, 2026
a45eb62
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 2, 2026
290cc26
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
bcb1027
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
62aeef8
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
c399c08
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
ee1b045
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
6711920
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
238ed4f
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
77bef4a
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
5b674f1
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 4, 2026
52d1056
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
794659f
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
b48147c
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
94b77de
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
1f9e1ab
fix: stamp the session the id names
sea-snake Sep 5, 2026
1067517
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
1c5f5f3
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
8391124
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
ace6e61
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
71a5045
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
919a837
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
1614028
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
e6985ad
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
509e9f5
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
5ac9db5
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
4bae789
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
49e79e0
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
80eb7ea
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
d3ba629
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
c447dd9
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
c66868b
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
a6e8f59
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
969c018
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
96ce31e
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
7475c3d
refactor(be): name the account reference list rather than a row
sea-snake Sep 5, 2026
d3fbcdb
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 5, 2026
3e9c56c
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
3d2e53c
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
9f6a4c2
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
61d8df4
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
3353209
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
cabf76e
refactor(be): a browser, not a session device
sea-snake Sep 6, 2026
db6c3bf
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
ce9dde4
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
b3991db
fix(be): bring the tests onto the current create_session shape
sea-snake Sep 6, 2026
13537ef
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
2b483d9
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
03b466f
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
bb7e6ab
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
6bb77a6
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
5abbfe5
refactor(be): a refresh stores the record once
sea-snake Sep 6, 2026
f4b7764
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 6, 2026
92dba78
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
0f74868
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
b643c32
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
747cd22
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
2e20039
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
f85893b
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
749140f
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
a632cec
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
14fac04
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
2b79f2a
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
68ec252
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
5163d3d
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 7, 2026
bc88105
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 8, 2026
16ba374
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 8, 2026
55d23f4
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 8, 2026
28e3556
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 8, 2026
2a5fcac
chore: merge feat/app-delegation-from-session
sea-snake Sep 8, 2026
ed87fc1
chore: merge feat/app-delegation-from-session
sea-snake Sep 8, 2026
5189983
chore: merge feat/app-delegation-from-session
sea-snake Sep 8, 2026
3b5a84a
chore: merge feat/app-delegation-from-session
sea-snake Sep 8, 2026
681ae7e
chore: merge feat/app-delegation-from-session
sea-snake Sep 8, 2026
c8b4484
chore: merge feat/app-delegation-from-session
sea-snake Sep 8, 2026
07ddad8
chore: merge feat/app-delegation-from-session
sea-snake Sep 8, 2026
898ee3b
chore: merge feat/app-delegation-from-session
sea-snake Sep 9, 2026
ac16c02
chore: merge feat/app-delegation-from-session
sea-snake Sep 9, 2026
45d602d
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 9, 2026
ac2855c
fix(be): the refresh write passes the clock the gate now takes
sea-snake Sep 9, 2026
5e02f93
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 9, 2026
917486c
chore: rename the session record accessors at their call sites
sea-snake Sep 9, 2026
21f2acd
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 9, 2026
bf3625d
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 9, 2026
4af979e
fix(session): a stamp that matched nothing is a refusal, not an Ok
sea-snake Sep 9, 2026
a570c48
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 9, 2026
1ccedc5
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 9, 2026
2453bfc
fix(session): a session lost to a race is not an internal fault
sea-snake Sep 9, 2026
d064768
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 9, 2026
d0efa42
Merge branch 'feat/app-delegation-from-session' into feat/session-ref…
sea-snake Sep 10, 2026
5363e32
test(session): drop a refresh test for a state that cannot occur
sea-snake Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/internet_identity/src/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,16 +355,33 @@ pub fn app_prepare_delegation(
) -> Result<AppPrepareDelegationResponse, AppSessionError> {
let now = time();
let AuthorizedSession {
account, session, ..
locator,
account,
session,
} = authorize_session(now)?;

// Everything that can refuse, before the stamp. Returning `Err` on the IC commits
// whatever was written before it — only a trap rolls back — so a stamp above this
// would leave the session recorded as used while the caller is told the call failed.
// All three depend on values already in hand, so there is nothing to gain by
// computing them later.
let expiration = u64::min(
now.saturating_add(APP_DELEGATION_TTL_NS),
session.valid_till_ns,
);
let seed = account_seed(&account)?;
Comment thread
sea-snake marked this conversation as resolved.
let access = DelegationAccess::from_read_only(session.read_only);

// A session revoked between `authorize_session` above and this stamp is a race, not
// an internal fault: the answer is the one the caller would have got a moment
// earlier.
storage_borrow_mut(|storage| storage.record_session_use(&locator, now)).map_err(
|err| match err {
StorageError::SessionNotFound { .. } => AppSessionError::NoSuchSession,
other => AppSessionError::InternalCanisterError(other.to_string()),
},
)?;

state::signature_map_mut(|sigs| {
add_delegation_signature(
sigs,
Expand Down Expand Up @@ -440,8 +457,6 @@ pub fn app_get_delegation(
/// three values a caller gathered: the principal lookup and the liveness check have both
/// happened, and no field can be here without them.
struct AuthorizedSession {
// Read by the refresh stamp, which lands one PR up.
#[allow(dead_code)]
locator: SessionLocator,
account: Account,
session: Session,
Expand Down
81 changes: 81 additions & 0 deletions src/internet_identity/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3331,6 +3331,73 @@ impl<M: Memory + Clone> Storage<M> {
})
}

/// Records that a session was used: its own stamp, its account reference's, and the
/// browser's in the device registry.
///
/// A locator naming nothing is [`StorageError::SessionNotFound`] rather than a
/// quiet non-event. Nobody makes a decision from it, and the one caller that could
/// have ignored it went on to mint a delegation for a session no list holds.
pub fn record_session_use(
&mut self,
key: &SessionLocator,
now: Timestamp,
) -> Result<(), StorageError> {
let SessionLocator {
anchor_number,
origin,
account_number,
session_id,
} = key;
let (anchor_number, account_number, session_id) =
(*anchor_number, *account_number, *session_id);
let not_found = || StorageError::SessionNotFound {
anchor_number,
session_id,
};

if self.lookup_application_number_with_origin(origin).is_none() {
return Err(not_found());
}
let mut anchor = self.read(anchor_number)?;
let (mut account_references, config) = self.account_state_for_origin(anchor_number, origin);

let write = account_references
.iter_mut()
.find(|write| write.account_reference.account_number == account_number)
.ok_or_else(not_found)?;
let session = write
.account_reference
.sessions
.iter_mut()
.find(|session| session.session_id == session_id)
.ok_or_else(not_found)?;

session.last_refreshed_ns = Some(now);
let browser_id = session.browser_id;
write.account_reference.last_used = Some(now);

// This list is being rewritten anyway, so its dead sessions go now. It costs one
// pass over a list already in memory and no write of its own, and it means every
// list anyone still uses stays clean without anything having to sweep for it.
for write in account_references.iter_mut() {
write
.account_reference
.sessions
.retain(|session| !session.is_expired_or_idle(now));
}

// Stamped before the write rather than after, because the write is what stores the
// record. There is no second store: handing it over is handing over the storing of
// it, whatever was changed on it.
anchor.stamp_browser_use(browser_id, now);
self.write_account_state(
anchor,
now,
BTreeMap::from([(origin.clone(), Some((account_references, config)))]),
)?;
Ok(())
}

/// Retires an application no anchor references any more. The number is never
/// reissued.
fn remove_unreferenced_application(
Expand Down Expand Up @@ -4276,6 +4343,13 @@ pub enum StorageError {
SessionAlreadyOver {
anchor_number: AnchorNumber,
},
/// The session a caller named is not among the identity's — whether it never was, or
/// has since been revoked, replaced or pruned. Those are one observation rather than
/// three: a session that is not there cannot be told apart from one that never was.
SessionNotFound {
anchor_number: AnchorNumber,
session_id: SessionId,
},
AnchorNumberOutOfRange {
anchor_number: AnchorNumber,
range: (AnchorNumber, AnchorNumber),
Expand Down Expand Up @@ -4368,6 +4442,13 @@ impl fmt::Display for StorageError {
f,
"a session for Identity Anchor {anchor_number} would be over before it started"
),
Self::SessionNotFound {
anchor_number,
session_id,
} => write!(
f,
"Identity Anchor {anchor_number} holds no session {session_id}"
),
Self::DeserializationError(err) => {
write!(f, "failed to deserialize a Candid value: {err}")
}
Expand Down
15 changes: 15 additions & 0 deletions src/internet_identity/src/storage/anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,21 @@ impl Anchor {
}
}

/// Advances a browser's `last_used`, where the anchor holds that browser and the
/// stamp moves it forward. A browser no entry names, or a repeat inside one message,
/// leaves the registry as it is.
pub fn stamp_browser_use(&mut self, browser_id: BrowserId, now: Timestamp) {
if let Some(browser) = self
.browsers
.iter_mut()
.find(|browser| browser.id == browser_id)
{
if browser.last_used < now {
browser.last_used = now;
}
}
}

/// What a caller outside storage may know about this anchor's browsers: an
/// identifier, what the browser said it was, and when. The keys stay here — they
/// are how a sign-in proves which entry it is, so handing them out would let
Expand Down
211 changes: 211 additions & 0 deletions src/internet_identity/src/storage/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6380,6 +6380,217 @@ mod session_consent_change_tests {
}
}

mod session_refresh_stamp_tests {
use super::held_references;
use super::params;
use crate::storage::account::{AccountReference, Session, SessionLocator};
use crate::storage::{CreateSessionParams, StorageError};
use crate::Storage;
use ic_stable_structures::VectorMemory;
use internet_identity_interface::internet_identity::types::AnchorNumber;
use pretty_assertions::assert_eq;

const ORIGIN: &str = "https://example.com";

fn storage_with_session() -> (Storage<VectorMemory>, AnchorNumber, SessionLocator) {
let mut storage = Storage::new((10_000, 3_784_873), VectorMemory::default());
storage.update_salt([17u8; 32]);
let anchor = storage.allocate_anchor(0).unwrap();
let anchor_number = anchor.anchor_number();
storage.write(anchor).unwrap();
let (key, _) = storage
.create_session(CreateSessionParams {
valid_till_ns: u64::MAX,
..params(anchor_number, 1, 1_000)
})
.unwrap();
(storage, anchor_number, key)
}

fn reference(storage: &Storage<VectorMemory>, anchor_number: AnchorNumber) -> AccountReference {
let application_number = storage
.lookup_application_number_with_origin(&ORIGIN.to_string())
.unwrap();
held_references(storage, anchor_number, application_number)
.into_iter()
.find(|reference| reference.account_number.is_none())
.unwrap()
}

fn session_of(storage: &Storage<VectorMemory>, anchor_number: AnchorNumber) -> Session {
reference(storage, anchor_number).sessions.remove(0)
}

#[test]
fn a_refresh_stamps_the_session_and_the_reference() {
let (mut storage, anchor_number, key) = storage_with_session();

storage.record_session_use(&key, 2_000).unwrap();

assert_eq!(
session_of(&storage, anchor_number).last_refreshed_ns,
Some(2_000)
);
assert_eq!(reference(&storage, anchor_number).last_used, Some(2_000));
}

#[test]
fn every_refresh_advances_the_stamp() {
let (mut storage, anchor_number, key) = storage_with_session();

for now in [1_001, 1_002, 1_003] {
storage.record_session_use(&key, now).unwrap();
assert_eq!(
session_of(&storage, anchor_number).last_refreshed_ns,
Some(now)
);
}
}

/// The list is rewritten anyway, so the refresh is where a dead sibling is collected —
/// index entry and session count included, since nothing else will come for them.
#[test]
fn a_refresh_collects_the_dead_sessions_beside_it() {
let (mut storage, anchor_number, key) = storage_with_session();

let (_, dead) = storage
.create_session(CreateSessionParams {
valid_till_ns: 1_500,
..params(anchor_number, 9, 1_000)
})
.unwrap();
let dead_principal = storage
.lookup_session_with_principal_memory
.iter()
.find(|(_, handle)| handle.session_id == dead.session_id)
.map(|(principal, _)| principal)
.expect("the session should be indexed");
assert!(storage
.lookup_session_with_principal(dead_principal)
.is_some());
assert_eq!(storage.read(anchor_number).unwrap().session_count, 2);

storage.record_session_use(&key, 2_000).unwrap();

let sessions = reference(&storage, anchor_number).sessions;
assert_eq!(sessions.len(), 1, "the expired sibling was left behind");
// The first browser to sign in, so the first id the registry minted.
assert_eq!(sessions[0].browser_id, 0);
assert!(
storage
.lookup_session_with_principal(dead_principal)
.is_none(),
"the expired sibling's index entry outlived it"
);
assert_eq!(storage.read(anchor_number).unwrap().session_count, 1);
}

/// A locator naming nothing is refused rather than reported as a quiet non-event.
/// The caller that would have ignored a `false` here goes on to mint a delegation,
/// which is the one thing a session no list holds must not get.
#[test]
fn a_stamp_for_a_session_that_is_gone_is_refused() {
let (mut storage, anchor_number, _key) = storage_with_session();

let refused = storage.record_session_use(
&SessionLocator {
anchor_number,
origin: ORIGIN.to_string(),
account_number: None,
session_id: 9_999,
},
5_000,
);

assert!(
matches!(
refused,
Err(StorageError::SessionNotFound {
anchor_number: refused_anchor,
session_id: 9_999
}) if refused_anchor == anchor_number
),
"a locator naming no session should be refused, got {refused:?}"
);
}

#[test]
fn stamping_leaves_a_second_device_alone() {
let (mut storage, anchor_number, key) = storage_with_session();
storage
.create_session(CreateSessionParams {
valid_till_ns: u64::MAX,
..params(anchor_number, 2, 1_000)
})
.unwrap();
let now = 2_000;

storage.record_session_use(&key, now).unwrap();

let sessions = reference(&storage, anchor_number).sessions;
assert_eq!(sessions.len(), 2);
// The registry mints the ids, in the order the browsers first signed in.
let stamped = sessions.iter().find(|s| s.browser_id == 0).unwrap();
let untouched = sessions.iter().find(|s| s.browser_id == 1).unwrap();
assert_eq!(stamped.last_refreshed_ns, Some(now));
assert_eq!(untouched.last_refreshed_ns, None);
}

fn storage_with_registered_browser(
) -> (Storage<VectorMemory>, AnchorNumber, SessionLocator, u32) {
let mut storage = Storage::new((10_000, 3_784_873), VectorMemory::default());
storage.update_salt([17u8; 32]);
let anchor = storage.allocate_anchor(0).unwrap();
let anchor_number = anchor.anchor_number();
storage.write(anchor).unwrap();
// The sign-in registers the browser, so nothing here puts one on the record by
// hand and then claims its id.
let (key, session) = storage
.create_session(CreateSessionParams {
valid_till_ns: u64::MAX,
..params(anchor_number, 1, 1_000)
})
.unwrap();
(storage, anchor_number, key, session.browser_id)
}

fn browser_last_used(storage: &Storage<VectorMemory>, anchor_number: AnchorNumber) -> u64 {
storage.read(anchor_number).unwrap().browsers()[0].last_used
}

#[test]
fn a_refresh_advances_the_device_registry() {
let (mut storage, anchor_number, key, _browser_id) = storage_with_registered_browser();

storage.record_session_use(&key, 9_000).unwrap();

assert_eq!(browser_last_used(&storage, anchor_number), 9_000);
}

#[test]
fn a_refresh_leaves_the_device_enrolment_timestamp_alone() {
let (mut storage, anchor_number, key, _browser_id) = storage_with_registered_browser();

storage.record_session_use(&key, 9_000).unwrap();

let device = storage.read(anchor_number).unwrap().browsers()[0].clone();
assert_eq!(device.created_at, 1_000);
assert_eq!(device.last_used, 9_000);
}

#[test]
fn a_refresh_for_a_device_the_anchor_never_registered_still_stamps_the_session() {
let (mut storage, anchor_number, key) = storage_with_session();

storage.record_session_use(&key, 9_000).unwrap();

assert_eq!(
session_of(&storage, anchor_number).last_refreshed_ns,
Some(9_000)
);
}
}

mod browser_session_count_tests {
use super::TEST_NOW;
use super::{params, params_at};
Expand Down
Loading