Skip to main content

session_guard/
testing.rs

1//! Conformance suite for [`SessionStore`] implementations.
2//!
3//! Every built-in store runs this suite. Run it against your own store with
4//! [`store_conformance!`](crate::store_conformance), which needs `tokio` (with the `macros` and
5//! `rt-multi-thread` features) as a dev-dependency:
6//!
7//! ```no_run
8//! # #[cfg(feature = "valkey")]
9//! # mod suite {
10//! use session_guard::ValkeyStore;
11//!
12//! session_guard::store_conformance!(|| async {
13//!     ValkeyStore::connect("redis://localhost:6379").await.unwrap()
14//! });
15//! # }
16//! ```
17//!
18//! The tests use random ids and subjects and whole-second timestamps, so they can share one
19//! database with each other and with other data.
20//!
21//! Every check panics when the store breaks the contract.
22#![allow(
23    clippy::missing_panics_doc,
24    reason = "each check panics on a contract violation, documented above"
25)]
26
27use std::net::{IpAddr, Ipv4Addr};
28
29use chrono::{DateTime, TimeDelta, Utc};
30use uuid::Uuid;
31
32use crate::{
33    checks::Check,
34    event::{EventKind, SessionEvent},
35    fingerprint::{Baseline, Fingerprint},
36    store::{Rotation, SessionRecord, SessionStore, SessionUpdate, Supersede},
37    token::{SessionToken, TokenHash},
38};
39
40/// Generates one `#[tokio::test]` per conformance check. `$make` is a closure returning a
41/// future that resolves to a fresh store handle.
42#[macro_export]
43macro_rules! store_conformance {
44    ($make:expr) => {
45        $crate::store_conformance!(@tests $make;
46            insert_and_find,
47            unknown_token_is_none,
48            records_round_trip,
49            exact_rotation_happens_once,
50            concurrent_rotation_has_one_winner,
51            rotation_if_issued_before,
52            current_rotation,
53            newer_token_used_is_tracked,
54            mark_used_sets_once,
55            mark_used_reaches_the_rotated_token,
56            none_fields_are_preserved,
57            prune_superseded_tokens,
58            delete_session_removes_tokens,
59            delete_subject_sessions,
60            list_subject_sessions,
61            delete_expired_absolute_and_idle,
62            events_record_and_purge,
63            event_claims_dedupe_within_window,
64            apply_to_missing_session,
65        );
66    };
67    (@tests $make:expr; $($name:ident),* $(,)?) => {
68        $(
69            #[::tokio::test(flavor = "multi_thread")]
70            async fn $name() {
71                $crate::testing::$name(($make)().await).await;
72            }
73        )*
74    };
75}
76
77/// The current time truncated to whole seconds.
78pub fn now_secs() -> DateTime<Utc> {
79    let now = Utc::now();
80    DateTime::from_timestamp(now.timestamp(), 0).unwrap_or(now)
81}
82
83/// A fresh random token hash.
84///
85/// # Panics
86///
87/// If the OS random number generator is unavailable.
88pub fn random_hash() -> TokenHash {
89    SessionToken::generate().expect("OS RNG").hash()
90}
91
92/// A session for a random subject, created at `now`, expiring in a day.
93pub fn sample_session(now: DateTime<Utc>) -> SessionRecord {
94    sample_session_for(&format!("\"conformance-{}\"", Uuid::new_v4()), now)
95}
96
97/// A session for `subject` (canonical JSON), created at `now`, expiring in a day.
98pub fn sample_session_for(subject: &str, now: DateTime<Utc>) -> SessionRecord {
99    SessionRecord {
100        id: Uuid::new_v4(),
101        subject: subject.to_owned(),
102        created_at: now,
103        authenticated_at: now,
104        last_seen_at: now,
105        expires_at: now + TimeDelta::days(1),
106        step_up_required: false,
107        baseline: Baseline::new(Fingerprint {
108            ip_prefix: "203.0.113.0/24".into(),
109            ..Fingerprint::default()
110        }),
111    }
112}
113
114fn secs(n: i64) -> TimeDelta {
115    TimeDelta::seconds(n)
116}
117
118fn rotation(new_token: TokenHash, supersede: Supersede, prune_before: DateTime<Utc>) -> Rotation {
119    Rotation {
120        new_token,
121        supersede,
122        prune_superseded_before: prune_before,
123    }
124}
125
126fn rotate(id: Uuid, now: DateTime<Utc>, rotation: Rotation) -> SessionUpdate {
127    SessionUpdate {
128        rotation: Some(rotation),
129        ..SessionUpdate::touch(id, now)
130    }
131}
132
133/// An inserted session is found by its token, which starts current and unused.
134pub async fn insert_and_find<St: SessionStore>(store: St) {
135    let now = now_secs();
136    let session = sample_session(now);
137    let token = random_hash();
138    store.insert_session(&session, &token).await.unwrap();
139
140    let found = store
141        .find_by_token(&token)
142        .await
143        .unwrap()
144        .expect("inserted token is found");
145    assert_eq!(found.session, session);
146    assert_eq!(found.token.hash, token);
147    assert_eq!(found.token.issued_at, now);
148    assert_eq!(found.token.first_used_at, None);
149    assert_eq!(found.token.superseded_at, None);
150    assert!(!found.newer_token_used);
151}
152
153/// An unknown token finds nothing.
154pub async fn unknown_token_is_none<St: SessionStore>(store: St) {
155    assert!(store.find_by_token(&random_hash()).await.unwrap().is_none());
156}
157
158/// Unicode subjects and a fully populated baseline survive storage unchanged.
159pub async fn records_round_trip<St: SessionStore>(store: St) {
160    let now = now_secs();
161    let mut session = sample_session_for(
162        &format!("{{\"tenant\":\"zürich-🦀\",\"user\":\"{}\"}}", Uuid::new_v4()),
163        now,
164    );
165    session.step_up_required = true;
166    session.authenticated_at = now - secs(30);
167    session.last_seen_at = now - secs(5);
168    session.baseline = Baseline {
169        fingerprint: Fingerprint {
170            ip_prefix: "2001:db8:1::/48".into(),
171            country: Some("BE".into()),
172            asn: Some(64_496),
173            browser: Some("Firefox".into()),
174            browser_major: Some(131),
175            os: Some("Linux".into()),
176            ch_platform: Some("Linux".into()),
177            ch_mobile: Some(false),
178            accept_language: Some("nl-be,nl,en".into()),
179            tls: Some("t13d1516h2_8daaf6152771_02713d6af862".into()),
180        },
181        previous_ip_prefix: Some("198.51.100.0/24".into()),
182        ip_changed_at: Some(now - secs(60)),
183    };
184    let token = random_hash();
185    store.insert_session(&session, &token).await.unwrap();
186    let found = store.find_by_token(&token).await.unwrap().unwrap();
187    assert_eq!(found.session, session);
188}
189
190/// `Supersede::Exact` rotates only while that token is current.
191pub async fn exact_rotation_happens_once<St: SessionStore>(store: St) {
192    let now = now_secs();
193    let session = sample_session(now);
194    let first = random_hash();
195    store.insert_session(&session, &first).await.unwrap();
196
197    let later = now + secs(60);
198    let second = random_hash();
199    let rotated = store
200        .apply(&rotate(
201            session.id,
202            later,
203            rotation(second, Supersede::Exact(first), now),
204        ))
205        .await
206        .unwrap();
207    assert!(rotated);
208
209    let third = random_hash();
210    let again = store
211        .apply(&rotate(
212            session.id,
213            later,
214            rotation(third, Supersede::Exact(first), now),
215        ))
216        .await
217        .unwrap();
218    assert!(!again, "a superseded token must not rotate again");
219    assert!(store.find_by_token(&third).await.unwrap().is_none());
220
221    let old = store.find_by_token(&first).await.unwrap().unwrap();
222    assert_eq!(old.token.superseded_at, Some(later));
223    let new = store.find_by_token(&second).await.unwrap().unwrap();
224    assert_eq!(new.token.issued_at, later);
225    assert_eq!(new.token.superseded_at, None);
226    assert_eq!(new.session.last_seen_at, later);
227}
228
229/// Of many concurrent `Exact` rotations of one token, exactly one wins and the losers' tokens
230/// are never stored.
231pub async fn concurrent_rotation_has_one_winner<St: SessionStore>(store: St) {
232    let now = now_secs();
233    let session = sample_session(now);
234    let first = random_hash();
235    store.insert_session(&session, &first).await.unwrap();
236
237    let candidates: Vec<TokenHash> = (0..16).map(|_| random_hash()).collect();
238    let attempts = candidates.iter().map(|&candidate| {
239        let update = rotate(
240            session.id,
241            now + secs(1),
242            rotation(candidate, Supersede::Exact(first), now),
243        );
244        let store = &store;
245        async move { store.apply(&update).await.unwrap() }
246    });
247    let results = join_all(attempts).await;
248    assert_eq!(results.iter().filter(|r| **r).count(), 1, "exactly one rotation wins");
249
250    let mut current = 0;
251    for (candidate, won) in candidates.iter().zip(&results) {
252        let found = store.find_by_token(candidate).await.unwrap();
253        assert_eq!(found.is_some(), *won);
254        if found.is_some_and(|f| f.token.superseded_at.is_none()) {
255            current += 1;
256        }
257    }
258    assert_eq!(current, 1, "exactly one current token");
259}
260
261/// `Supersede::CurrentIssuedBefore` only rotates a current token older than the instant.
262pub async fn rotation_if_issued_before<St: SessionStore>(store: St) {
263    let now = now_secs();
264    let session = sample_session(now);
265    let first = random_hash();
266    store.insert_session(&session, &first).await.unwrap();
267
268    let too_new = random_hash();
269    let rotated = store
270        .apply(&rotate(
271            session.id,
272            now + secs(5),
273            rotation(too_new, Supersede::CurrentIssuedBefore(now), now),
274        ))
275        .await
276        .unwrap();
277    assert!(!rotated, "issued_at == cutoff is not strictly before");
278    assert!(store.find_by_token(&too_new).await.unwrap().is_none());
279
280    let second = random_hash();
281    let rotated = store
282        .apply(&rotate(
283            session.id,
284            now + secs(5),
285            rotation(second, Supersede::CurrentIssuedBefore(now + secs(1)), now),
286        ))
287        .await
288        .unwrap();
289    assert!(rotated);
290    let old = store.find_by_token(&first).await.unwrap().unwrap();
291    assert_eq!(old.token.superseded_at, Some(now + secs(5)));
292}
293
294/// `Supersede::Current` rotates whatever token is current.
295pub async fn current_rotation<St: SessionStore>(store: St) {
296    let now = now_secs();
297    let session = sample_session(now);
298    let first = random_hash();
299    store.insert_session(&session, &first).await.unwrap();
300    let second = random_hash();
301    store
302        .apply(&rotate(
303            session.id,
304            now + secs(1),
305            rotation(second, Supersede::Exact(first), now),
306        ))
307        .await
308        .unwrap();
309
310    let third = random_hash();
311    let rotated = store
312        .apply(&rotate(
313            session.id,
314            now + secs(2),
315            rotation(third, Supersede::Current, now),
316        ))
317        .await
318        .unwrap();
319    assert!(rotated);
320    let second_found = store.find_by_token(&second).await.unwrap().unwrap();
321    assert_eq!(second_found.token.superseded_at, Some(now + secs(2)));
322    let third_found = store.find_by_token(&third).await.unwrap().unwrap();
323    assert_eq!(third_found.token.superseded_at, None);
324}
325
326/// `newer_token_used` reports whether a later token of the session has been presented.
327pub async fn newer_token_used_is_tracked<St: SessionStore>(store: St) {
328    let now = now_secs();
329    let session = sample_session(now);
330    let first = random_hash();
331    store.insert_session(&session, &first).await.unwrap();
332    let second = random_hash();
333    store
334        .apply(&rotate(
335            session.id,
336            now + secs(1),
337            rotation(second, Supersede::Exact(first), now),
338        ))
339        .await
340        .unwrap();
341    assert!(!store.find_by_token(&first).await.unwrap().unwrap().newer_token_used);
342
343    store
344        .apply(&SessionUpdate {
345            mark_used: Some(second),
346            ..SessionUpdate::touch(session.id, now + secs(2))
347        })
348        .await
349        .unwrap();
350    assert!(store.find_by_token(&first).await.unwrap().unwrap().newer_token_used);
351    assert!(!store.find_by_token(&second).await.unwrap().unwrap().newer_token_used);
352}
353
354/// `mark_used` sets `first_used_at` once and never moves it.
355pub async fn mark_used_sets_once<St: SessionStore>(store: St) {
356    let now = now_secs();
357    let session = sample_session(now);
358    let token = random_hash();
359    store.insert_session(&session, &token).await.unwrap();
360
361    for offset in [3, 9] {
362        store
363            .apply(&SessionUpdate {
364                mark_used: Some(token),
365                ..SessionUpdate::touch(session.id, now + secs(offset))
366            })
367            .await
368            .unwrap();
369    }
370    let found = store.find_by_token(&token).await.unwrap().unwrap();
371    assert_eq!(found.token.first_used_at, Some(now + secs(3)));
372    assert_eq!(found.session.last_seen_at, now + secs(9));
373}
374
375/// `mark_used` may name the token the same update's rotation inserts.
376pub async fn mark_used_reaches_the_rotated_token<St: SessionStore>(store: St) {
377    let now = now_secs();
378    let session = sample_session(now);
379    let first = random_hash();
380    store.insert_session(&session, &first).await.unwrap();
381    let second = random_hash();
382    let rotated = store
383        .apply(&SessionUpdate {
384            mark_used: Some(second),
385            ..rotate(
386                session.id,
387                now + secs(1),
388                rotation(second, Supersede::Exact(first), now),
389            )
390        })
391        .await
392        .unwrap();
393    assert!(rotated);
394    let found = store.find_by_token(&second).await.unwrap().unwrap();
395    assert_eq!(found.token.first_used_at, Some(now + secs(1)));
396    assert!(store.find_by_token(&first).await.unwrap().unwrap().newer_token_used);
397}
398
399/// `None` fields leave stored values alone; `Some` fields replace them.
400pub async fn none_fields_are_preserved<St: SessionStore>(store: St) {
401    let now = now_secs();
402    let session = sample_session(now);
403    let token = random_hash();
404    store.insert_session(&session, &token).await.unwrap();
405
406    let mut baseline = session.baseline.clone();
407    baseline.fingerprint.country = Some("NL".into());
408    store
409        .apply(&SessionUpdate {
410            baseline: Some(baseline.clone()),
411            step_up_required: Some(true),
412            authenticated_at: Some(now + secs(4)),
413            ..SessionUpdate::touch(session.id, now + secs(4))
414        })
415        .await
416        .unwrap();
417    store
418        .apply(&SessionUpdate::touch(session.id, now + secs(8)))
419        .await
420        .unwrap();
421
422    let found = store.find_by_token(&token).await.unwrap().unwrap().session;
423    assert_eq!(found.baseline, baseline);
424    assert!(found.step_up_required);
425    assert_eq!(found.authenticated_at, now + secs(4));
426    assert_eq!(found.last_seen_at, now + secs(8));
427    assert_eq!(found.created_at, session.created_at);
428    assert_eq!(found.expires_at, session.expires_at);
429    assert_eq!(found.subject, session.subject);
430
431    store
432        .apply(&SessionUpdate {
433            step_up_required: Some(false),
434            ..SessionUpdate::touch(session.id, now + secs(9))
435        })
436        .await
437        .unwrap();
438    assert!(
439        !store
440            .find_by_token(&token)
441            .await
442            .unwrap()
443            .unwrap()
444            .session
445            .step_up_required
446    );
447}
448
449/// A rotation prunes the session's tokens superseded before its cutoff, and only those.
450pub async fn prune_superseded_tokens<St: SessionStore>(store: St) {
451    let now = now_secs();
452    let session = sample_session(now);
453    let t1 = random_hash();
454    store.insert_session(&session, &t1).await.unwrap();
455    let t2 = random_hash();
456    store
457        .apply(&rotate(
458            session.id,
459            now + secs(10),
460            rotation(t2, Supersede::Exact(t1), now),
461        ))
462        .await
463        .unwrap();
464    let t3 = random_hash();
465    store
466        .apply(&rotate(
467            session.id,
468            now + secs(20),
469            rotation(t3, Supersede::Exact(t2), now),
470        ))
471        .await
472        .unwrap();
473
474    // t1 was superseded at +10, t2 at +20 (the new rotation supersedes t3 at +30).
475    let t4 = random_hash();
476    store
477        .apply(&rotate(
478            session.id,
479            now + secs(30),
480            rotation(t4, Supersede::Exact(t3), now + secs(15)),
481        ))
482        .await
483        .unwrap();
484    assert!(
485        store.find_by_token(&t1).await.unwrap().is_none(),
486        "superseded before cutoff is pruned"
487    );
488    assert!(store.find_by_token(&t2).await.unwrap().is_some());
489    assert!(store.find_by_token(&t3).await.unwrap().is_some());
490    assert!(store.find_by_token(&t4).await.unwrap().is_some());
491}
492
493/// Deleting a session removes its tokens and reports whether it existed.
494pub async fn delete_session_removes_tokens<St: SessionStore>(store: St) {
495    let now = now_secs();
496    let session = sample_session(now);
497    let t1 = random_hash();
498    store.insert_session(&session, &t1).await.unwrap();
499    let t2 = random_hash();
500    store
501        .apply(&rotate(
502            session.id,
503            now + secs(1),
504            rotation(t2, Supersede::Exact(t1), now),
505        ))
506        .await
507        .unwrap();
508
509    assert!(store.delete_session(session.id).await.unwrap());
510    assert!(!store.delete_session(session.id).await.unwrap());
511    assert!(store.find_by_token(&t1).await.unwrap().is_none());
512    assert!(store.find_by_token(&t2).await.unwrap().is_none());
513}
514
515/// Deleting a subject's sessions leaves other subjects alone.
516pub async fn delete_subject_sessions<St: SessionStore>(store: St) {
517    let now = now_secs();
518    let subject = format!("\"conformance-{}\"", Uuid::new_v4());
519    let mut tokens = Vec::new();
520    for _ in 0..3 {
521        let token = random_hash();
522        store
523            .insert_session(&sample_session_for(&subject, now), &token)
524            .await
525            .unwrap();
526        tokens.push(token);
527    }
528    let other = random_hash();
529    store.insert_session(&sample_session(now), &other).await.unwrap();
530
531    assert_eq!(store.delete_subject_sessions(&subject).await.unwrap(), 3);
532    assert_eq!(store.delete_subject_sessions(&subject).await.unwrap(), 0);
533    for token in &tokens {
534        assert!(store.find_by_token(token).await.unwrap().is_none());
535    }
536    assert!(store.find_by_token(&other).await.unwrap().is_some());
537}
538
539/// Listing returns exactly the subject's sessions.
540pub async fn list_subject_sessions<St: SessionStore>(store: St) {
541    let now = now_secs();
542    let subject = format!("\"conformance-{}\"", Uuid::new_v4());
543    let a = sample_session_for(&subject, now);
544    let b = sample_session_for(&subject, now - secs(60));
545    store.insert_session(&a, &random_hash()).await.unwrap();
546    store.insert_session(&b, &random_hash()).await.unwrap();
547    store
548        .insert_session(&sample_session(now), &random_hash())
549        .await
550        .unwrap();
551
552    let mut listed = store.list_subject_sessions(&subject).await.unwrap();
553    listed.sort_by_key(|s| s.created_at);
554    assert_eq!(listed, vec![b, a]);
555    assert_eq!(store.list_subject_sessions("\"nobody\"").await.unwrap(), vec![]);
556}
557
558/// Expired (absolute) and idle sessions are purged; live ones stay.
559pub async fn delete_expired_absolute_and_idle<St: SessionStore>(store: St) {
560    // Far in the past, so these purges only reach rows at least that old: never the live rows
561    // of other tests or apps sharing the database. Stores whose keys expire on the wall clock
562    // drop `expired` on their own, so only the idle session is sure to be counted; the others
563    // expire in real time.
564    let base = far_past();
565    let later = Utc::now() + TimeDelta::days(1);
566    let mut expired = sample_session(base);
567    expired.expires_at = base;
568    let mut idle = sample_session(base);
569    idle.last_seen_at = base - secs(100);
570    idle.expires_at = later;
571    let mut live = sample_session(base);
572    live.expires_at = later;
573    live.last_seen_at = base - secs(99);
574
575    let (te, ti, tl) = (random_hash(), random_hash(), random_hash());
576    store.insert_session(&expired, &te).await.unwrap();
577    store.insert_session(&idle, &ti).await.unwrap();
578    store.insert_session(&live, &tl).await.unwrap();
579
580    let deleted = store.delete_expired(base, base - secs(100)).await.unwrap();
581    assert!(deleted >= 1);
582    assert!(store.find_by_token(&te).await.unwrap().is_none());
583    assert!(store.find_by_token(&ti).await.unwrap().is_none());
584    assert!(store.find_by_token(&tl).await.unwrap().is_some());
585    store.delete_session(live.id).await.unwrap();
586}
587
588/// Events are recorded and purged strictly before the cutoff.
589pub async fn events_record_and_purge<St: SessionStore>(store: St) {
590    let at = far_past();
591    let event = SessionEvent {
592        session_id: Uuid::new_v4(),
593        subject: "\"conformance\"".into(),
594        kind: EventKind::RiskRevoked,
595        enforced: false,
596        score: Some(95),
597        signals: vec![Check::OsChanged, Check::CountryChanged],
598        ip: IpAddr::V4(Ipv4Addr::new(203, 0, 113, 0)),
599        user_agent: Some("Mozilla/5.0 🦀".into()),
600        at,
601    };
602    store.record_event(&event).await.unwrap();
603    store
604        .record_event(&SessionEvent {
605            user_agent: None,
606            score: None,
607            signals: vec![],
608            ..event.clone()
609        })
610        .await
611        .unwrap();
612    store.delete_events_before(at).await.unwrap();
613    assert!(store.delete_events_before(at + secs(1)).await.unwrap() >= 2);
614}
615
616/// A claim succeeds once per session and kind per window, across store handles.
617pub async fn event_claims_dedupe_within_window<St: SessionStore>(store: St) {
618    let now = now_secs();
619    let window = secs(600);
620    let session = Uuid::new_v4();
621    let claim = |kind, at| store.claim_event(session, kind, at, window);
622    assert!(claim(EventKind::RiskRevoked, now).await.unwrap());
623    assert!(!claim(EventKind::RiskRevoked, now + secs(1)).await.unwrap());
624    assert!(!claim(EventKind::RiskRevoked, now + secs(599)).await.unwrap());
625    assert!(
626        claim(EventKind::StepUpRequired, now).await.unwrap(),
627        "kinds are independent"
628    );
629    assert!(
630        store
631            .claim_event(Uuid::new_v4(), EventKind::RiskRevoked, now, window)
632            .await
633            .unwrap(),
634        "sessions are independent"
635    );
636    assert!(
637        claim(EventKind::RiskRevoked, now + secs(600)).await.unwrap(),
638        "the window elapsed"
639    );
640    assert!(
641        !claim(EventKind::RiskRevoked, now + secs(601)).await.unwrap(),
642        "the new claim holds"
643    );
644}
645
646/// Updating a session that does not exist is not an error.
647pub async fn apply_to_missing_session<St: SessionStore>(store: St) {
648    let now = now_secs();
649    let update = rotate(Uuid::new_v4(), now, rotation(random_hash(), Supersede::Current, now));
650    assert!(!store.apply(&update).await.unwrap());
651}
652
653/// A random whole-minute instant between 1900 and 1931.
654fn far_past() -> DateTime<Utc> {
655    let bytes = Uuid::new_v4().into_bytes();
656    let minutes = i64::from(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], 0]));
657    DateTime::from_timestamp(-2_208_988_800 + minutes * 60, 0).unwrap_or_default()
658}
659
660/// Polls every future to completion concurrently, without a runtime dependency.
661async fn join_all<F: std::future::Future>(futures: impl IntoIterator<Item = F>) -> Vec<F::Output> {
662    use std::{
663        pin::Pin,
664        task::{Context, Poll},
665    };
666
667    struct JoinAll<F: std::future::Future> {
668        futures: Vec<Option<Pin<Box<F>>>>,
669        outputs: Vec<Option<F::Output>>,
670    }
671
672    // Every future is boxed and outputs are never pinned.
673    impl<F: std::future::Future> Unpin for JoinAll<F> {}
674
675    impl<F: std::future::Future> std::future::Future for JoinAll<F> {
676        type Output = Vec<F::Output>;
677
678        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
679            let this = self.get_mut();
680            let mut pending = false;
681            for (slot, out) in this.futures.iter_mut().zip(this.outputs.iter_mut()) {
682                if let Some(fut) = slot {
683                    match fut.as_mut().poll(cx) {
684                        Poll::Ready(v) => {
685                            *out = Some(v);
686                            *slot = None;
687                        }
688                        Poll::Pending => pending = true,
689                    }
690                }
691            }
692            if pending {
693                Poll::Pending
694            } else {
695                Poll::Ready(this.outputs.iter_mut().filter_map(Option::take).collect())
696            }
697        }
698    }
699
700    let futures: Vec<_> = futures.into_iter().map(|f| Some(Box::pin(f))).collect();
701    let outputs = futures.iter().map(|_| None).collect();
702    JoinAll { futures, outputs }.await
703}