Skip to main content

vcon_server/store/
legal.rs

1//! Legal access: reading a deleted or replaced vCon, and listing what is
2//! kept of a uuid, for a legal key. Access decides who holds a legal key;
3//! these calls trust their caller and record each answer.
4
5use rusqlite::{Connection, OptionalExtension, Transaction};
6use time::OffsetDateTime;
7use uuid::Uuid;
8
9use super::{Caller, Store, StoreError, Stored, commit, immediate, io, version_row};
10use crate::audit::{Action, Actor, Entry};
11use crate::clock::from_nanos;
12use crate::ids::VersionId;
13
14/// A version as a legal read returns it, and whether its uuid is deleted.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Legal {
17    pub stored: Stored,
18    /// The uuid is deleted; otherwise it is live.
19    pub deleted: bool,
20}
21
22/// One kept version of a uuid, as the legal version list shows it.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct VersionInfo {
25    pub version: VersionId,
26    pub stored_at: OffsetDateTime,
27    /// When it was replaced, or its uuid deleted; None while current.
28    pub hidden_at: Option<OffsetDateTime>,
29    /// The uuid's current version.
30    pub current: bool,
31}
32
33/// What a legal call finds of a uuid.
34enum Standing {
35    Purged,
36    Absent,
37    Live(VersionId),
38    Deleted,
39}
40
41fn standing(tx: &Connection, uuid: Uuid) -> Result<Standing, StoreError> {
42    let purged = tx
43        .query_row(
44            "SELECT 1 FROM tombstone WHERE uuid = ?1",
45            [uuid.as_bytes()],
46            |_| Ok(()),
47        )
48        .optional()
49        .map_err(io)?;
50    if purged.is_some() {
51        return Ok(Standing::Purged);
52    }
53    let row: Option<(Option<[u8; 16]>, Option<i64>)> = tx
54        .query_row(
55            "SELECT current, deleted_at FROM vcon WHERE uuid = ?1",
56            [uuid.as_bytes()],
57            |r| Ok((r.get(0)?, r.get(1)?)),
58        )
59        .optional()
60        .map_err(io)?;
61    match row {
62        None => Ok(Standing::Absent),
63        Some((_, Some(_))) => Ok(Standing::Deleted),
64        Some((Some(current), None)) => Ok(Standing::Live(VersionId(current))),
65        Some((None, None)) => Err(StoreError::Damaged),
66    }
67}
68
69/// The status a legal call records for `answer`, `ok` when it succeeded.
70fn status<T>(answer: &Result<T, StoreError>, ok: u16) -> u16 {
71    match answer {
72        Ok(_) => ok,
73        Err(StoreError::Gone) => 410,
74        Err(StoreError::NotFound) => 404,
75        Err(StoreError::NotDeleted) => 409,
76        Err(_) => 500,
77    }
78}
79
80/// The latest kept version of a deleted uuid. A deleted uuid left with no
81/// version is purged, with a tombstone, so a deleted row always keeps one.
82fn latest(tx: &Connection, uuid: Uuid) -> Result<VersionId, StoreError> {
83    let latest: Option<[u8; 16]> = tx
84        .query_row(
85            "SELECT version FROM version WHERE uuid = ?1
86             ORDER BY stored_at DESC, rowid DESC LIMIT 1",
87            [uuid.as_bytes()],
88            |r| r.get(0),
89        )
90        .optional()
91        .map_err(io)?;
92    Ok(VersionId(latest.ok_or(StoreError::Damaged)?))
93}
94
95impl Store {
96    /// Reads `version` of `uuid`, or with None its current version, or its
97    /// latest kept one when the uuid is deleted. A version purged, never
98    /// stored or of another uuid is NotFound; a purged uuid is Gone. The
99    /// whole read, file included, is one immediate transaction, so a purge
100    /// cannot remove what is being read. Each answer is recorded.
101    pub fn legal_get(
102        &self,
103        who: &Caller,
104        uuid: Uuid,
105        version: Option<VersionId>,
106    ) -> Result<Legal, StoreError> {
107        let mut conn = self.conn();
108        let tx = immediate(&mut conn)?;
109        let answer = self.legal_read(&tx, uuid, version);
110        let entry = Entry {
111            actor: Actor::Key(who.key),
112            action: Action::LegalRead,
113            target: Some(uuid.to_string()),
114            version: answer.as_ref().ok().map(|read| read.stored.version),
115            status: status(&answer, 200),
116        };
117        self.record(&tx, &entry)?;
118        commit(tx)?;
119        answer
120    }
121
122    fn legal_read(
123        &self,
124        tx: &Transaction,
125        uuid: Uuid,
126        asked: Option<VersionId>,
127    ) -> Result<Legal, StoreError> {
128        let (current, deleted) = match standing(tx, uuid)? {
129            Standing::Purged => return Err(StoreError::Gone),
130            Standing::Absent => return Err(StoreError::NotFound),
131            Standing::Live(current) => (Some(current), false),
132            Standing::Deleted => (None, true),
133        };
134        let version = match (asked, current) {
135            (Some(asked), _) => {
136                let kept = tx
137                    .query_row(
138                        "SELECT 1 FROM version WHERE version = ?1 AND uuid = ?2",
139                        (asked.0, uuid.as_bytes()),
140                        |_| Ok(()),
141                    )
142                    .optional()
143                    .map_err(io)?;
144                kept.map(|()| asked).ok_or(StoreError::NotFound)?
145            }
146            (None, Some(current)) => current,
147            (None, None) => latest(tx, uuid)?,
148        };
149        let row = version_row(tx, version)?;
150        let body = self.open_version(uuid, version, &row.wrapped)?;
151        Ok(Legal {
152            stored: Stored {
153                version,
154                form: row.form,
155                gzip: row.gzip,
156                body,
157            },
158            deleted,
159        })
160    }
161
162    /// Every kept version of `uuid`, oldest first, with its times. A purged
163    /// uuid is Gone and one never stored NotFound. Each answer is recorded.
164    pub fn legal_versions(&self, who: &Caller, uuid: Uuid) -> Result<Vec<VersionInfo>, StoreError> {
165        let mut conn = self.conn();
166        let tx = immediate(&mut conn)?;
167        let answer = versions(&tx, uuid);
168        let entry = Entry {
169            actor: Actor::Key(who.key),
170            action: Action::LegalVersions,
171            target: Some(uuid.to_string()),
172            version: None,
173            status: status(&answer, 200),
174        };
175        self.record(&tx, &entry)?;
176        commit(tx)?;
177        answer
178    }
179
180    /// Makes the latest kept version of a deleted `uuid` current again: its
181    /// hidden time and the uuid's deleted time are cleared, so no sweep
182    /// purges it. Older hidden versions stay hidden. A live uuid is
183    /// NotDeleted, a purged one Gone, one never stored NotFound. Recorded.
184    pub fn restore(&self, who: &Caller, uuid: Uuid) -> Result<VersionId, StoreError> {
185        let mut conn = self.conn();
186        let mut tx = immediate(&mut conn)?;
187        // The writes go in a savepoint, so a failure undoes them and still
188        // leaves the record.
189        let answer = {
190            let changes = tx.savepoint().map_err(io)?;
191            let answer = restored(&changes, uuid);
192            if answer.is_ok() {
193                changes.commit().map_err(io)?;
194            }
195            answer
196        };
197        let entry = Entry {
198            actor: Actor::Key(who.key),
199            action: Action::Restore,
200            target: Some(uuid.to_string()),
201            version: answer.as_ref().ok().copied(),
202            status: status(&answer, 200),
203        };
204        self.record(&tx, &entry)?;
205        commit(tx)?;
206        answer
207    }
208
209    /// Turns the legal hold on `uuid` on or off. A held uuid is never
210    /// purged; a delete still hides it. A purged uuid is Gone, one never
211    /// stored NotFound. Recorded.
212    pub fn set_hold(&self, who: &Caller, uuid: Uuid, on: bool) -> Result<(), StoreError> {
213        let mut conn = self.conn();
214        let mut tx = immediate(&mut conn)?;
215        let answer = {
216            let changes = tx.savepoint().map_err(io)?;
217            let answer = held(&changes, uuid, on);
218            if answer.is_ok() {
219                changes.commit().map_err(io)?;
220            }
221            answer
222        };
223        let entry = Entry {
224            actor: Actor::Key(who.key),
225            action: if on { Action::Hold } else { Action::Release },
226            target: Some(uuid.to_string()),
227            version: None,
228            status: status(&answer, 204),
229        };
230        self.record(&tx, &entry)?;
231        commit(tx)?;
232        answer
233    }
234}
235
236fn restored(tx: &Connection, uuid: Uuid) -> Result<VersionId, StoreError> {
237    match standing(tx, uuid)? {
238        Standing::Purged => return Err(StoreError::Gone),
239        Standing::Absent => return Err(StoreError::NotFound),
240        Standing::Live(_) => return Err(StoreError::NotDeleted),
241        Standing::Deleted => {}
242    }
243    let version = latest(tx, uuid)?;
244    tx.execute(
245        "UPDATE version SET hidden_at = NULL WHERE version = ?1",
246        [version.0],
247    )
248    .map_err(io)?;
249    tx.execute(
250        "UPDATE vcon SET current = ?1, deleted_at = NULL WHERE uuid = ?2",
251        (version.0, uuid.as_bytes()),
252    )
253    .map_err(io)?;
254    Ok(version)
255}
256
257fn held(tx: &Connection, uuid: Uuid, on: bool) -> Result<(), StoreError> {
258    match standing(tx, uuid)? {
259        Standing::Purged => Err(StoreError::Gone),
260        Standing::Absent => Err(StoreError::NotFound),
261        Standing::Live(_) | Standing::Deleted => {
262            tx.execute(
263                "UPDATE vcon SET hold = ?1 WHERE uuid = ?2",
264                (on, uuid.as_bytes()),
265            )
266            .map_err(io)?;
267            Ok(())
268        }
269    }
270}
271
272fn versions(tx: &Transaction, uuid: Uuid) -> Result<Vec<VersionInfo>, StoreError> {
273    let current = match standing(tx, uuid)? {
274        Standing::Purged => return Err(StoreError::Gone),
275        Standing::Absent => return Err(StoreError::NotFound),
276        Standing::Live(current) => Some(current),
277        Standing::Deleted => None,
278    };
279    let time = |nanos: i64| from_nanos(nanos).ok_or(StoreError::Damaged);
280    let mut rows = tx
281        .prepare(
282            "SELECT version, stored_at, hidden_at FROM version WHERE uuid = ?1
283             ORDER BY stored_at, rowid",
284        )
285        .map_err(io)?;
286    let found = rows
287        .query_map([uuid.as_bytes()], |r| {
288            Ok((
289                VersionId(r.get(0)?),
290                r.get::<_, i64>(1)?,
291                r.get::<_, Option<i64>>(2)?,
292            ))
293        })
294        .map_err(io)?;
295    found
296        .map(|row| {
297            let (version, stored_at, hidden_at) = row.map_err(io)?;
298            Ok(VersionInfo {
299                version,
300                stored_at: time(stored_at)?,
301                hidden_at: hidden_at.map(time).transpose()?,
302                current: Some(version) == current,
303            })
304        })
305        .collect()
306}
307
308#[cfg(test)]
309mod legal_tests {
310    use serde_json::Value;
311    use time::Duration;
312    use time::macros::datetime;
313    use uuid::Uuid;
314
315    use super::super::testing::vcon;
316    use super::super::tests::{Fixture, fixture};
317    use super::*;
318    use crate::clock::Clock;
319    use crate::fs::Fs;
320    use crate::ids::VersionId;
321    use crate::retention::Retention;
322    use crate::store::{Created, StoreError, Stored};
323
324    const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
325    const OTHER: &str = "0199e5c6-0d7a-7c11-8000-000000000001";
326
327    /// Stores a vCon of `uuid` and returns its version and body.
328    fn stored(f: &Fixture, uuid: &str, subject: &str) -> (VersionId, Vec<u8>) {
329        let (found, body) = vcon(uuid, subject);
330        let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
331            panic!("a new vCon")
332        };
333        (version, body)
334    }
335
336    /// Replaces the current version of `UUID` and returns the new one.
337    fn replaced(f: &Fixture, current: VersionId, subject: &str) -> (VersionId, Vec<u8>) {
338        let (found, body) = vcon(UUID, subject);
339        let version = f
340            .store
341            .replace(&f.caller, current, &found, &body)
342            .expect("replaced");
343        (version, body)
344    }
345
346    fn uuid() -> Uuid {
347        UUID.parse().expect("uuid")
348    }
349
350    fn later(f: &Fixture, by: Duration) {
351        f.clock.set(f.clock.now() + by);
352    }
353
354    /// The last record of `action`.
355    fn last(f: &Fixture, action: &str) -> Value {
356        f.store
357            .records()
358            .into_iter()
359            .rfind(|r| r["action"] == action)
360            .expect("a record")
361    }
362
363    #[test]
364    fn a_deleted_vcon_reads_back_byte_for_byte_and_says_it_is_deleted() {
365        let f = fixture();
366        let (version, body) = stored(&f, UUID, "first");
367        f.store
368            .delete(&f.caller, uuid(), Retention::Years(7))
369            .expect("deleted");
370
371        let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
372
373        assert_eq!(
374            read,
375            Legal {
376                stored: Stored {
377                    version,
378                    form: vcon::Form::Unsigned,
379                    gzip: false,
380                    body,
381                },
382                deleted: true,
383            }
384        );
385        let record = last(&f, "legal_read");
386        assert_eq!(record["status"], 200);
387        assert_eq!(record["target"], UUID);
388        assert_eq!(record["version"], version.to_string());
389    }
390
391    #[test]
392    fn a_deleted_vcon_reads_its_latest_kept_version() {
393        let f = fixture();
394        let (first, _) = stored(&f, UUID, "first");
395        later(&f, Duration::hours(1));
396        let (second, body) = replaced(&f, first, "second");
397        f.store
398            .delete(&f.caller, uuid(), Retention::Years(7))
399            .expect("deleted");
400
401        let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
402
403        assert_eq!((read.stored.version, read.stored.body), (second, body));
404    }
405
406    #[test]
407    fn a_live_vcon_reads_its_current_version() {
408        let f = fixture();
409        let (first, _) = stored(&f, UUID, "first");
410        let (second, body) = replaced(&f, first, "second");
411
412        let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
413
414        assert_eq!((read.stored.version, read.stored.body), (second, body));
415        assert!(!read.deleted);
416    }
417
418    #[test]
419    fn a_named_kept_version_reads_back_until_it_is_purged() {
420        let f = fixture();
421        let (first, body) = stored(&f, UUID, "first");
422        replaced(&f, first, "second");
423
424        let read = f
425            .store
426            .legal_get(&f.caller, uuid(), Some(first))
427            .expect("read");
428        assert_eq!((read.stored.version, read.stored.body), (first, body));
429        assert!(!read.deleted, "the uuid is live");
430
431        f.store.purge_due(Retention::Days(0)).expect("purged");
432        assert_eq!(
433            f.store.legal_get(&f.caller, uuid(), Some(first)),
434            Err(StoreError::NotFound)
435        );
436        assert_eq!(last(&f, "legal_read")["status"], 404);
437    }
438
439    #[test]
440    fn a_version_of_another_uuid_is_not_found() {
441        let f = fixture();
442        stored(&f, UUID, "first");
443        let (theirs, _) = stored(&f, OTHER, "other");
444
445        assert_eq!(
446            f.store.legal_get(&f.caller, uuid(), Some(theirs)),
447            Err(StoreError::NotFound)
448        );
449    }
450
451    #[test]
452    fn a_purged_uuid_is_gone_and_one_never_stored_is_not_found() {
453        let f = fixture();
454        stored(&f, UUID, "first");
455        f.store
456            .delete(&f.caller, uuid(), Retention::Days(0))
457            .expect("deleted and purged");
458
459        assert_eq!(
460            f.store.legal_get(&f.caller, uuid(), None),
461            Err(StoreError::Gone)
462        );
463        assert_eq!(last(&f, "legal_read")["status"], 410);
464        assert_eq!(
465            f.store.legal_versions(&f.caller, uuid()),
466            Err(StoreError::Gone)
467        );
468        assert_eq!(last(&f, "legal_versions")["status"], 410);
469
470        let never: Uuid = OTHER.parse().expect("uuid");
471        assert_eq!(
472            f.store.legal_get(&f.caller, never, None),
473            Err(StoreError::NotFound)
474        );
475        assert_eq!(
476            f.store.legal_versions(&f.caller, never),
477            Err(StoreError::NotFound)
478        );
479        assert_eq!(last(&f, "legal_versions")["status"], 404);
480    }
481
482    #[test]
483    fn the_version_list_gives_every_kept_version_with_its_times() {
484        let f = fixture();
485        let (first, _) = stored(&f, UUID, "first");
486        f.clock.set(datetime!(2026-10-23 13:00 UTC));
487        let (second, _) = replaced(&f, first, "second");
488
489        let live = f.store.legal_versions(&f.caller, uuid()).expect("list");
490        assert_eq!(
491            live,
492            [
493                VersionInfo {
494                    version: first,
495                    stored_at: datetime!(2026-10-23 12:00 UTC),
496                    hidden_at: Some(datetime!(2026-10-23 13:00 UTC)),
497                    current: false,
498                },
499                VersionInfo {
500                    version: second,
501                    stored_at: datetime!(2026-10-23 13:00 UTC),
502                    hidden_at: None,
503                    current: true,
504                },
505            ]
506        );
507        let record = last(&f, "legal_versions");
508        assert_eq!(
509            (record["status"].clone(), record["target"].clone()),
510            (200.into(), UUID.into())
511        );
512
513        f.clock.set(datetime!(2026-10-23 14:00 UTC));
514        f.store
515            .delete(&f.caller, uuid(), Retention::Years(7))
516            .expect("deleted");
517        let deleted = f.store.legal_versions(&f.caller, uuid()).expect("list");
518        assert_eq!(deleted.len(), 2);
519        assert_eq!(deleted[1].hidden_at, Some(datetime!(2026-10-23 14:00 UTC)));
520        assert!(deleted.iter().all(|v| !v.current), "nothing is current");
521    }
522
523    #[test]
524    fn a_damaged_kept_version_is_a_500_never_a_body() {
525        let f = fixture();
526        let (version, _) = stored(&f, UUID, "first");
527        f.store
528            .delete(&f.caller, uuid(), Retention::Years(7))
529            .expect("deleted");
530        let file = f.store.objects.join(version.to_string());
531        let mut blob = f.fs.read(&file).expect("file");
532        let last_byte = blob.len() - 1;
533        blob[last_byte] ^= 1;
534        f.fs.remove(&file).expect("remove");
535        f.fs.write_new(&file, &blob).expect("write");
536
537        assert_eq!(
538            f.store.legal_get(&f.caller, uuid(), None),
539            Err(StoreError::Damaged)
540        );
541        assert_eq!(last(&f, "legal_read")["status"], 500);
542    }
543
544    #[test]
545    fn a_restored_vcon_is_current_again_and_a_sweep_leaves_it() {
546        let f = fixture();
547        let (first, _) = stored(&f, UUID, "first");
548        later(&f, Duration::hours(1));
549        let (second, body) = replaced(&f, first, "second");
550        f.store
551            .delete(&f.caller, uuid(), Retention::Years(7))
552            .expect("deleted");
553        later(&f, Duration::days(2));
554
555        assert_eq!(f.store.restore(&f.caller, uuid()), Ok(second));
556        let record = last(&f, "restore");
557        assert_eq!(record["status"], 200);
558        assert_eq!(record["version"], second.to_string());
559
560        let read = f.store.get(&f.caller, uuid()).expect("live again");
561        assert_eq!((read.version, read.body), (second, body));
562        f.store.purge_due(Retention::Days(1)).expect("swept");
563        assert_eq!(
564            f.store.get(&f.caller, uuid()).map(|s| s.version),
565            Ok(second),
566            "the sweep leaves it current"
567        );
568        let kept = f.store.legal_versions(&f.caller, uuid()).expect("list");
569        assert_eq!(
570            kept.iter().map(|v| v.version).collect::<Vec<_>>(),
571            [second],
572            "the older hidden version was due and is gone"
573        );
574    }
575
576    #[test]
577    fn restore_refuses_a_live_a_purged_and_a_never_stored_uuid() {
578        let f = fixture();
579        stored(&f, UUID, "first");
580        assert_eq!(
581            f.store.restore(&f.caller, uuid()),
582            Err(StoreError::NotDeleted)
583        );
584        assert_eq!(last(&f, "restore")["status"], 409);
585
586        f.store
587            .delete(&f.caller, uuid(), Retention::Days(0))
588            .expect("deleted and purged");
589        assert_eq!(f.store.restore(&f.caller, uuid()), Err(StoreError::Gone));
590        assert_eq!(last(&f, "restore")["status"], 410);
591
592        let never: Uuid = OTHER.parse().expect("uuid");
593        assert_eq!(f.store.restore(&f.caller, never), Err(StoreError::NotFound));
594        assert_eq!(last(&f, "restore")["status"], 404);
595    }
596
597    #[test]
598    fn a_hold_blocks_the_purge_until_it_is_released() {
599        let f = fixture();
600        stored(&f, UUID, "first");
601        assert_eq!(f.store.set_hold(&f.caller, uuid(), true), Ok(()));
602        assert_eq!(last(&f, "hold")["status"], 204);
603        f.store
604            .delete(&f.caller, uuid(), Retention::Days(0))
605            .expect("deleted, but held");
606        f.store.purge_due(Retention::Days(0)).expect("swept");
607        assert!(
608            f.store.legal_get(&f.caller, uuid(), None).is_ok(),
609            "held, so kept"
610        );
611
612        assert_eq!(f.store.set_hold(&f.caller, uuid(), false), Ok(()));
613        f.store.purge_due(Retention::Days(0)).expect("swept");
614        assert_eq!(
615            f.store.legal_get(&f.caller, uuid(), None),
616            Err(StoreError::Gone),
617            "released, so purged"
618        );
619    }
620
621    /// A release is recorded as a release, so the log tells it from a hold.
622    #[test]
623    fn a_release_is_recorded_as_release_not_as_hold() {
624        let f = fixture();
625        stored(&f, UUID, "first");
626        assert_eq!(f.store.set_hold(&f.caller, uuid(), true), Ok(()));
627        assert_eq!(f.store.set_hold(&f.caller, uuid(), false), Ok(()));
628        let actions: Vec<_> = f
629            .store
630            .records()
631            .into_iter()
632            .filter(|r| r["target"] == UUID)
633            .map(|r| r["action"].as_str().expect("an action").to_owned())
634            .collect();
635        assert_eq!(actions, ["create", "hold", "release"]);
636        assert_eq!(last(&f, "release")["status"], 204);
637        let never: Uuid = OTHER.parse().expect("uuid");
638        assert_eq!(
639            f.store.set_hold(&f.caller, never, false),
640            Err(StoreError::NotFound)
641        );
642        assert_eq!(last(&f, "release")["status"], 404, "a refused release too");
643    }
644
645    #[test]
646    fn a_hold_refuses_a_purged_and_a_never_stored_uuid() {
647        let f = fixture();
648        stored(&f, UUID, "first");
649        f.store
650            .delete(&f.caller, uuid(), Retention::Days(0))
651            .expect("deleted and purged");
652        assert_eq!(
653            f.store.set_hold(&f.caller, uuid(), true),
654            Err(StoreError::Gone)
655        );
656        assert_eq!(last(&f, "hold")["status"], 410);
657        let never: Uuid = OTHER.parse().expect("uuid");
658        assert_eq!(
659            f.store.set_hold(&f.caller, never, true),
660            Err(StoreError::NotFound)
661        );
662        assert_eq!(last(&f, "hold")["status"], 404);
663    }
664
665    #[test]
666    fn a_restore_that_fails_partway_changes_nothing_and_is_recorded() {
667        let f = fixture();
668        stored(&f, UUID, "first");
669        f.store
670            .delete(&f.caller, uuid(), Retention::Years(7))
671            .expect("deleted");
672        f.store
673            .conn()
674            .execute_batch(
675                "CREATE TEMP TRIGGER no_restore BEFORE UPDATE OF current ON vcon
676                 BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
677            )
678            .expect("trigger");
679
680        assert!(f.store.restore(&f.caller, uuid()).is_err());
681
682        assert_eq!(last(&f, "restore")["status"], 500);
683        let kept = f.store.legal_versions(&f.caller, uuid()).expect("list");
684        assert!(
685            kept.iter().all(|v| v.hidden_at.is_some()),
686            "the version written first is hidden again"
687        );
688    }
689}