Skip to main content

coven_format/
test_utils.rs

1//! Deterministic examples for consumer tests, built from the production owners' types.
2
3/// The store shared by the deterministic storage and format fixtures.
4pub const STORE_ID: coven_foundation::id_source::StoreId =
5    coven_foundation::id_source::StoreId(uuid::Uuid::from_bytes([0x10; 16]));
6
7use crate::admin::*;
8use crate::codes::{InviteCode, InviteKind, KeychainRestoreCode, RestoreCode};
9use crate::key::encode_key;
10use crate::objects::*;
11use crate::snapshot::{SnapshotEncoder, SnapshotHeader, SnapshotRecord};
12use crate::snapshot_rows::*;
13use crate::value::*;
14use crate::write::*;
15use crate::Object;
16use coven_crypto::{InviteSecret, MemberIdentity, MemberKeys, SecretBytes};
17use coven_foundation::id_source::{CircleId, DeviceId, InviteId, KeyId, MemberId, StoreId};
18use coven_merge::slot;
19use coven_merge::{
20    Audience, Cell, Change, ColumnValue, LostKey, LostValue, MergeError, Operation, Parent, RowId,
21    RowState, Timestamp, WriteId, WriteOracle,
22};
23use std::collections::{BTreeMap, BTreeSet};
24use uuid::Uuid;
25use zeroize::Zeroizing;
26
27/// Decode a pinned hexadecimal fixture for consumer tests.
28pub fn fixture_bytes(text: &str) -> Vec<u8> {
29    hex::decode(text.trim()).expect("pinned hexadecimal fixture")
30}
31
32/// An original write by device one.
33pub fn position() -> WriteId {
34    WriteId { device: DeviceId(1), number: 3 }
35}
36/// The fixture write's timestamp.
37pub fn timestamp() -> Timestamp {
38    Timestamp::new(6, 3, DeviceId(1)).unwrap()
39}
40/// A row with a text key in the store.
41pub fn row() -> RowId {
42    RowId {
43        table: "t".into(),
44        key: encode_key(&[Value::Text("k".into())]).unwrap(),
45        audience: Audience::Store,
46    }
47}
48/// A value with no foreign-key metadata.
49pub fn column(value: Value) -> ColumnValue<Value> {
50    ColumnValue { value, parents: BTreeMap::new() }
51}
52/// A private pair whose signing and sealing seeds are both `seed`: public test data.
53pub fn keys_from_seed(seed: u8) -> MemberKeys {
54    MemberKeys::from_seed(seed)
55}
56/// The fixture member Ana's stable id, independent of every key she holds.
57pub fn member_id() -> MemberId {
58    MemberId(Uuid::from_bytes([0x4d; 16]))
59}
60/// Ana's original private pair, with fixed public test seeds 0x33.
61pub fn member_keys() -> MemberKeys {
62    keys_from_seed(0x33)
63}
64/// Ana's id with her original pair, as identity custody holds it.
65pub fn identity() -> MemberIdentity {
66    MemberIdentity::new(member_id(), member_keys())
67}
68/// The public halves of Ana's original pair.
69pub fn member() -> MemberPublicKeys {
70    member_keys().public_keys()
71}
72/// Ana's replacement private pair, with fixed public test seeds 0x34 (ยง10).
73pub fn replacement_keys() -> MemberKeys {
74    keys_from_seed(0x34)
75}
76/// The public halves of Ana's replacement pair, bound to the same id.
77pub fn replacement() -> MemberPublicKeys {
78    replacement_keys().public_keys()
79}
80/// Another member, Ben, whom the fixtures add and remove.
81pub fn other_member_id() -> MemberId {
82    MemberId(Uuid::from_bytes([0x42; 16]))
83}
84/// Ben's initial public pair, with fixed public test seeds 0x35.
85pub fn other_member() -> MemberPublicKeys {
86    keys_from_seed(0x35).public_keys()
87}
88/// A write updating one cell, with named foreign-key metadata on its new value.
89pub fn write() -> WriteRecord {
90    let mut new = column(Value::Integer(2));
91    new.parents.insert(
92        coven_merge::ForeignKey::new(["parent_fk"], "p", ["id"]),
93        Parent {
94            row: RowId {
95                table: "p".into(),
96                key: encode_key(&[Value::Text("q".into())]).unwrap(),
97                audience: Audience::Store,
98            },
99            generation: 1,
100        },
101    );
102    WriteRecord {
103        header: WriteHeader {
104            read_through: 5,
105            position: position(),
106            timestamp: timestamp(),
107            schema_version: 1,
108            disposition: WriteDisposition::Apply,
109        },
110        parts: vec![WritePart {
111            dismissals: Vec::new(),
112            audience: Audience::Store,
113            executions: vec![transient_execution(Audience::Store)],
114            rows: vec![RowChange {
115                row: row(),
116                creator: None,
117                execution: Execution {
118                    steps: vec![Step {
119                        sequence: 0,
120                        change: Change {
121                            generation: 1,
122                            operation: Operation::Update(BTreeMap::from([(
123                                "x".into(),
124                                new.clone(),
125                            )])),
126                        },
127                        half: Half::Whole,
128                    }],
129                    sources: Default::default(),
130                },
131                change: Change {
132                    generation: 1,
133                    operation: Operation::Update(BTreeMap::from([("x".into(), new)])),
134                },
135                old: BTreeMap::new(),
136                sources: crate::write_sources::WriteSources {
137                    generation: Some(crate::write_sources::ChangeId {
138                        write: WriteId { device: DeviceId(1), number: 1 },
139                        row: row(),
140                    }),
141                    setters: [(
142                        "x".into(),
143                        crate::write_sources::CellSource {
144                            change: crate::write_sources::ChangeId {
145                                write: WriteId { device: DeviceId(2), number: 1 },
146                                row: row(),
147                            },
148                            column: "x".into(),
149                        },
150                    )]
151                    .into(),
152                },
153            }],
154        }],
155    }
156}
157/// A row the fixture write inserts and deletes again: no net change, two
158/// operations, which still reach the write's authorship leaves (D5).
159pub fn transient_execution(audience: Audience) -> RowExecution {
160    let row = RowId { key: encode_key(&[Value::Text("l".into())]).unwrap(), audience, ..row() };
161    RowExecution {
162        row,
163        execution: Execution {
164            steps: vec![
165                Step {
166                    sequence: 1,
167                    change: Change {
168                        generation: 0,
169                        operation: Operation::Insert(BTreeMap::from([(
170                            "x".into(),
171                            column(Value::Integer(5)),
172                        )])),
173                    },
174                    half: Half::Whole,
175                },
176                Step {
177                    sequence: 2,
178                    change: Change { generation: 1, operation: Operation::Delete },
179                    half: Half::Whole,
180                },
181            ],
182            sources: Default::default(),
183        },
184    }
185}
186/// The public fixture's store key, whose bytes are committed by creation.
187pub fn store_key() -> coven_crypto::StoreKey {
188    coven_crypto::StoreKey::from_bytes(KeyId(Uuid::from_bytes([1; 16])), [17; 32])
189}
190/// The first admin change, carried by slot one.
191pub fn creation_change() -> StoreChange {
192    StoreChange::CreateStore {
193        access: crate::MemberAccess::S3AccessKey { access_key_id: "fixture-access-key".into() },
194        store: STORE_ID,
195        name: "S".into(),
196        admin: member(),
197        key: store_key().id(),
198        key_hash: store_key().hash(),
199        device_name: "D".into(),
200    }
201}
202/// A member-removal admin change.
203pub fn member_removal() -> StoreChange {
204    StoreChange::RemoveMember { member: other_member_id() }
205}
206/// A reset's slot used by loss examples.
207pub fn loss_entry() -> Slot {
208    slot(2)
209}
210/// A canonical path for one positive slot number.
211pub fn slot_path(number: u64) -> crate::path::ObjectPath {
212    crate::path::ObjectPath::slot(STORE_ID, number.try_into().unwrap())
213}
214/// The deterministic header of a device-one claim.
215pub fn slot_header(slot: u64, claim: u64) -> crate::sequence::SlotHeader {
216    crate::sequence::SlotHeader { device: DeviceId(1), claim, author: member_id(), after: slot - 1 }
217}
218/// Seal and sign a bounded slot with public test keys and deterministic nonces.
219pub fn bounded_slot(
220    slot: u64,
221    claim: u64,
222    kind: crate::sealed_slot::SlotKind,
223    frame: crate::sequence::SlotFrame,
224) -> Vec<u8> {
225    let path = slot_path(slot);
226    let prefix = crate::sealed_slot::SlotObjectPrefix {
227        format: crate::FormatVersion::CURRENT,
228        key: store_key().id(),
229        kind,
230    };
231    let plain = frame.encode(&slot_header(slot, claim), &path).unwrap();
232    let chunk = store_key().derive().reseal_object_chunk(
233        path.as_str(),
234        &prefix.encode().unwrap(),
235        0,
236        0,
237        &plain,
238    );
239    let mut bytes = prefix.encode_chunk(&chunk).unwrap();
240    let mut hash = coven_crypto::ObjectHasher::new();
241    hash.update(&bytes);
242    bytes.extend_from_slice(member_keys().sign_object(path.as_str(), &hash.finish()).as_bytes());
243    bytes
244}
245/// An earlier snapshot announcement preserved by the fixture snapshot.
246pub fn announcement() -> crate::sequence::SnapshotAnnouncement {
247    crate::sequence::SnapshotAnnouncement {
248        snapshot: snapshot_header().id,
249        covers: 6,
250        breaking_version: 0,
251        purpose: crate::sequence::Purpose::Ordinary,
252        digest: [0x99; 32],
253    }
254}
255/// An oracle of applied metadata that tests may populate from streamed records.
256#[derive(Clone)]
257pub struct TestOracle {
258    /// Metadata indexed by merge's write identity; no write bodies are reconstructed.
259    pub writes: BTreeMap<WriteId, AppliedWrite>,
260}
261impl WriteOracle for TestOracle {
262    type Error = crate::Error;
263    fn timestamp(&self, id: WriteId) -> Result<Option<Timestamp>, crate::Error> {
264        Ok(self.writes.get(&id).map(|w| w.timestamp))
265    }
266    fn had_read(&self, reader: WriteId, earlier: WriteId) -> Result<bool, crate::Error> {
267        let write = self.writes.get(&reader).ok_or(MergeError::MissingWrite(reader))?;
268        let slot = self.writes.get(&earlier).map(|earlier| earlier.slot);
269        Ok(coven_merge::had_read(reader, write.read_through, earlier, slot))
270    }
271}
272impl crate::snapshot::WriteSlots for TestOracle {
273    fn slot(&self, write: WriteId) -> Result<Option<Slot>, crate::Error> {
274        Ok(self.writes.get(&write).map(|write| write.slot))
275    }
276    fn landed(&self, write: WriteId) -> Result<bool, crate::Error> {
277        Ok(self.writes.keys().any(|id| id.device == write.device && id.number >= write.number))
278    }
279}
280/// Three applied writes, including two concurrent setters of the fixture row.
281pub fn oracle() -> TestOracle {
282    let first = WriteId { device: DeviceId(1), number: 1 };
283    let winner = WriteId { device: DeviceId(1), number: 2 };
284    let loser = WriteId { device: DeviceId(2), number: 1 };
285    TestOracle {
286        writes: [
287            AppliedWrite {
288                id: first,
289                slot: slot(3),
290                timestamp: Timestamp::new(1, 0, first.device).unwrap(),
291                read_through: 0,
292            },
293            AppliedWrite {
294                id: winner,
295                slot: slot(5),
296                timestamp: Timestamp::new(4, 0, winner.device).unwrap(),
297                read_through: 0,
298            },
299            AppliedWrite {
300                id: loser,
301                slot: slot(4),
302                timestamp: Timestamp::new(3, 0, loser.device).unwrap(),
303                read_through: 3,
304            },
305            AppliedWrite { id: position(), slot: slot(6), timestamp: timestamp(), read_through: 5 },
306        ]
307        .into_iter()
308        .map(|w| (w.id, w))
309        .collect(),
310    }
311}
312/// The fixture's merged row, with an actual concurrent loss validated by merge.
313pub fn merge_row() -> MergeRow {
314    merge_row_of(row(), Value::Integer(2))
315}
316/// A merge row of `row` whose cell `x` holds `value`.
317pub fn merge_row_of(row: RowId, value: Value) -> MergeRow {
318    let state = RowState::from_parts(
319        row,
320        BTreeMap::from([(1, WriteId { device: DeviceId(1), number: 1 })]),
321        BTreeMap::from([(
322            "x".into(),
323            Cell { write: WriteId { device: DeviceId(1), number: 2 }, value: column(value) },
324        )]),
325        BTreeMap::new(),
326        &oracle(),
327    )
328    .unwrap();
329    MergeRow { state }
330}
331/// The header for the fixture's eleven snapshot sections.
332pub fn snapshot_header() -> SnapshotHeader {
333    SnapshotHeader {
334        id: SnapshotId { device: DeviceId(1), number: 1, audience: Audience::Store },
335        schema_version: 2,
336        covers: 9,
337        counts: [4, 1, 1, 4, 5, 1, 1, 1, 1, 1, 2],
338    }
339}
340/// The sealed prefix carrying a snapshot's coverage, with a fixed test key.
341pub fn snapshot_prefix(header: &SnapshotHeader) -> crate::sealed_snapshot::SnapshotObjectPrefix {
342    crate::sealed_snapshot::SnapshotObjectPrefix {
343        audience: header.id.audience.clone(),
344        key: KeyId(Uuid::from_bytes([1; 16])),
345        covers: header.covers,
346        purpose: crate::sequence::Purpose::Raise(header.schema_version),
347    }
348}
349/// Records in their canonical section and identity order.
350pub fn snapshot_records() -> Vec<SnapshotRecord> {
351    let mut writes = oracle().writes;
352    let header = write().header;
353    writes.insert(
354        header.position,
355        AppliedWrite {
356            id: header.position,
357            timestamp: header.timestamp,
358            slot: slot(6),
359            read_through: header.read_through,
360        },
361    );
362    let mut records: Vec<_> = writes.into_values().map(SnapshotRecord::Write).collect();
363    records.push(SnapshotRecord::Column(SyncedColumn { table: "t".into(), column: "x".into() }));
364    records.push(SnapshotRecord::Merge(merge_row()));
365    records.extend(
366        retained_losses()
367            .into_iter()
368            .chain([concurrent_loss(), excluded_loss()])
369            .map(SnapshotRecord::Loss),
370    );
371    records.extend(authored_records().into_iter().map(SnapshotRecord::Authored));
372    records.push(SnapshotRecord::Creation(creation()));
373    records.push(SnapshotRecord::Evidence(write_evidence()));
374    records.push(SnapshotRecord::Admin(crate::floor::OriginalSlot {
375        slot: slot(1),
376        object: bounded_slot(
377            1,
378            1,
379            crate::sealed_slot::SlotKind::Admin,
380            crate::sequence::SlotFrame::Admin(creation_change()),
381        ),
382    }));
383    records.push(SnapshotRecord::KeyCopy(crate::floor::OriginalSlot {
384        slot: slot(2),
385        object: hex::decode(include_str!("../fixtures/sealed-store-key.hex").trim()).unwrap(),
386    }));
387    records.push(SnapshotRecord::Announcement(crate::floor::OriginalSlot {
388        slot: slot(7),
389        object: bounded_slot(
390            7,
391            6,
392            crate::sealed_slot::SlotKind::Announcement,
393            crate::sequence::SlotFrame::Announcement(announcement()),
394        ),
395    }));
396    records.push(SnapshotRecord::Claim(crate::snapshot::LastClaim {
397        device: DeviceId(1),
398        number: 8,
399        digest: Some([0x81; 32]),
400    }));
401    records.push(SnapshotRecord::Claim(crate::snapshot::LastClaim {
402        device: DeviceId(2),
403        number: 1,
404        digest: Some([0x21; 32]),
405    }));
406    records
407}
408/// The fixture row's selected creation, by device one's first write.
409pub fn creation() -> crate::authorship::Creation {
410    let write = WriteId { device: DeviceId(1), number: 1 };
411    crate::authorship::Creation {
412        row: row(),
413        write,
414        creator: crate::write_sources::ChangeId { write, row: row() },
415    }
416}
417/// Evidence for the fixture write's store part, opening its updated cell.
418pub fn write_evidence() -> crate::authorship::WriteEvidence {
419    let leaves = crate::authorship::part_leaves(&write().parts[0]);
420    let hasher = store_key().derive().authorship_hasher();
421    let proof = crate::authorship::Multiproof::prove(&hasher, &leaves, |leaf| {
422        matches!(leaf, crate::authorship::Leaf::Cell { .. })
423    })
424    .unwrap();
425    let mut header = 7u32.to_be_bytes().to_vec();
426    header.extend([0x23; 24 + 7 + 16]);
427    crate::authorship::WriteEvidence {
428        write: position(),
429        prefix: crate::sealed_write::WriteObjectPrefix {
430            format: crate::FormatVersion::CURRENT,
431            store_key: KeyId(Uuid::from_bytes([1; 16])),
432            part_keys: vec![KeyId(Uuid::from_bytes([1; 16]))],
433        }
434        .encode()
435        .unwrap(),
436        header,
437        roots: vec![crate::authorship::root(&hasher, &leaves).unwrap()],
438        signature: [0x22; 64],
439        proof,
440    }
441}
442/// The fixture's kept source evidence, each change applied by its author:
443/// the first write's insert of the row, its value kept as a digest, then the
444/// fixture write's store part, whose reference `x` is kept in full, and its
445/// execution-only row.
446pub fn authored_records() -> Vec<crate::snapshot_rows::AuthoredRecord> {
447    use crate::snapshot_rows::{
448        AuthoredChange, AuthoredPart, AuthoredRecord, ChangeVerdict, KeptChange, KeptRecord,
449        KeptValue,
450    };
451    let first = WriteId { device: DeviceId(1), number: 1 };
452    let created = KeptRecord::Change(KeptChange {
453        row: row(),
454        change: Change {
455            generation: 0,
456            operation: Operation::Insert(BTreeMap::from([(
457                "x".into(),
458                ColumnValue {
459                    value: KeptValue::Digest(KeptValue::digest(&Value::Integer(1)).unwrap()),
460                    parents: BTreeMap::new(),
461                },
462            )])),
463        },
464        creator: None,
465        steps: 1,
466    });
467    let write = write();
468    let part = &write.parts[0];
469    let applied = |record, stop| {
470        AuthoredRecord::Change(AuthoredChange { record, verdict: ChangeVerdict::Applied, stop })
471    };
472    vec![
473        AuthoredRecord::Part(AuthoredPart {
474            header: WriteHeader {
475                position: first,
476                timestamp: Timestamp::new(1, 0, first.device).unwrap(),
477                read_through: 0,
478                schema_version: 1,
479                disposition: WriteDisposition::Apply,
480            },
481        }),
482        applied(created, None),
483        AuthoredRecord::Part(AuthoredPart { header: write.header.clone() }),
484        applied(KeptRecord::keep(PartRecord::Change(&part.rows[0]), |_| true).unwrap(), None),
485        // An execution never becomes current: it stops at its own slot.
486        applied(
487            KeptRecord::keep(PartRecord::Execution(&part.executions[0]), |_| true).unwrap(),
488            Some(slot(6)),
489        ),
490    ]
491}
492/// A concurrent cell loss of the fixture row.
493pub fn concurrent_loss() -> crate::loss::Loss {
494    crate::loss::Loss::cell(
495        row(),
496        LostKey { column: "x".into(), write: WriteId { device: DeviceId(2), number: 1 } },
497        LostValue {
498            incarnation: 1,
499            value: column(Value::Integer(1)),
500            replaced_by: WriteId { device: DeviceId(1), number: 2 },
501        },
502    )
503}
504/// Both kinds of loss after their row's merge history has been discarded.
505pub fn retained_losses() -> Vec<crate::loss::Loss> {
506    use crate::loss::Loss;
507    let row = RowId { table: "removed".into(), ..row() };
508    let mut cell = concurrent_loss();
509    cell.row = row.clone();
510    cell.frozen = true;
511    let mut removed =
512        Loss::removed(&merge_row().state, [coven_merge::Rule::Check("valid".into())].into());
513    removed.row = row;
514    removed.frozen = true;
515    vec![cell, removed]
516}
517/// The row values of a write excluded by a schema change.
518pub fn excluded_loss() -> crate::loss::Loss {
519    use crate::loss::{Loss, LossCause, LossValues};
520    let change = write().parts.remove(0).rows.remove(0);
521    let Operation::Update(values) = change.change.operation else { unreachable!() };
522    Loss {
523        row: change.row,
524        generation: change.change.generation,
525        frozen: true,
526        values: LossValues::Row(
527            values
528                .into_iter()
529                .map(|(name, mut value)| {
530                    value.parents.clear();
531                    (name, Cell { write: position(), value })
532                })
533                .collect(),
534        ),
535        cause: LossCause::Excluded { write: write().header.position, version: 2 },
536    }
537}
538/// A plaintext queue value produced by the format's queue encoder.
539#[expect(clippy::map_err_ignore, reason = "a length past usize cannot be allocated")]
540pub fn write_plaintext(record: &WriteRecord) -> Result<Vec<u8>, crate::Error> {
541    let encoder = crate::write_stream::WriteEncoder::new(record)?;
542    let length =
543        usize::try_from(encoder.plaintext_length()).map_err(|_| crate::Error::Allocation)?;
544    let mut bytes = vec![0; length];
545    encoder.encode_plaintext(&mut bytes)?;
546    Ok(bytes)
547}
548/// A complete example snapshot emitted by the production streaming encoder.
549pub fn snapshot_frames() -> Vec<Vec<u8>> {
550    let (mut encoder, header) = SnapshotEncoder::start(snapshot_header()).unwrap();
551    let mut frames = vec![header];
552    for record in snapshot_records() {
553        frames.push(encoder.record(record).unwrap());
554    }
555    frames.push(encoder.finish().unwrap());
556    frames
557}
558/// The S3 location `coven-storage` packs into a code's storage part
559/// (D13): provider 0, bucket `bucket`, region `region`, no endpoint, no
560/// profile.
561const S3_LOCATION: &[u8] = b"\0\0\0\0\x06bucket\0\0\0\x06region\0\0";
562
563/// Restore contents with test seeds and that S3 location, without a key.
564pub fn restore() -> RestoreCode {
565    RestoreCode {
566        store: StoreId(Uuid::from_bytes([0x10; 16])),
567        member: member_id(),
568        member_keys: member_keys(),
569        storage: SecretBytes::new([S3_LOCATION, b"\0"].concat()),
570    }
571}
572/// The restore code as coven saves it for the person, named by a rename at slot 41.
573pub fn keychain_restore() -> KeychainRestoreCode {
574    KeychainRestoreCode { name: "Family".into(), named_at: slot(41), code: restore() }
575}
576/// Invite contents with a fixed test secret.
577pub fn invite() -> InviteCode {
578    InviteCode {
579        store: StoreId(Uuid::from_bytes([0x10; 16])),
580        name: "S".into(),
581        inviter: "A".into(),
582        // A re-binding invite recovers Ana, whose profile has her name.
583        kind: InviteKind::Recovery,
584        expires_at: 86_400,
585        invite: InviteId(Uuid::from_bytes([0x55; 16])),
586        // A re-binding invite names Ana's existing id (D13).
587        member: member_id(),
588        secret: InviteSecret::from_bytes([0x66; 32]),
589        initial_key: store_key().id(),
590        inviting_writer: DeviceId(1),
591        read_through: 12,
592        // The S3 location, then the key made for the invite.
593        storage: SecretBytes::new([S3_LOCATION, b"\0\0\0\x02id\0\0\0\x06secret"].concat()),
594    }
595}
596/// Each admin operation in its canonical tag order.
597pub fn store_changes() -> Vec<StoreChange> {
598    let c = CircleId(uuid::Uuid::from_bytes([2; 16]));
599    let m = member_id();
600    vec![
601        creation_change(),
602        StoreChange::AdmitMember {
603            member: other_member_id(),
604            keys: other_member(),
605            role: MemberRole::Member,
606            access: crate::MemberAccess::ProviderAccount("[email protected]".into()),
607            device: DeviceId(2),
608            device_name: "D".into(),
609        },
610        member_removal(),
611        StoreChange::ChangeRole { member: m, role: MemberRole::Admin },
612        StoreChange::AddDevice { name: "D".into(), replaces: None },
613        StoreChange::RemoveDevice { device: DeviceId(2) },
614        StoreChange::CreateCircle {
615            circle: c,
616            name: "C".into(),
617            key: coven_foundation::id_source::KeyId(uuid::Uuid::from_bytes([1; 16])),
618            key_hash: store_key().hash(),
619            breaking_version: 2,
620        },
621        StoreChange::RenameCircle { circle: c, name: "N".into() },
622        StoreChange::DeleteCircle { circle: c },
623        StoreChange::AddCircleMember { circle: c, member: m },
624        StoreChange::RemoveCircleMember { circle: c, member: m },
625        StoreChange::RaiseSchema { version: 2, announcement: slot(7) },
626        StoreChange::Reset { announcement: slot(8) },
627        StoreChange::SetAccess {
628            member: m,
629            access: crate::MemberAccess::S3AccessKey { access_key_id: "replacement-key".into() },
630        },
631        StoreChange::RotateKey {
632            audience: Audience::Store,
633            key: KeyId(Uuid::from_bytes([3; 16])),
634            key_hash: [0x91; 32],
635        },
636        StoreChange::RotateKey {
637            audience: Audience::Circle(c),
638            key: KeyId(Uuid::from_bytes([4; 16])),
639            key_hash: [0xa2; 32],
640        },
641        StoreChange::ReplaceMemberKeys { member: m, expected: member(), keys: replacement() },
642        StoreChange::RenameStore { name: "N".into() },
643        StoreChange::AddInviteShare {
644            invite: InviteId(Uuid::from_bytes([0x55; 16])),
645            account: "[email protected]".into(),
646        },
647        StoreChange::EndInviteShare { invite: InviteId(Uuid::from_bytes([0x55; 16])) },
648        StoreChange::RecoverMember {
649            member: m,
650            expected: member(),
651            keys: replacement(),
652            access: Some(crate::MemberAccess::S3AccessKey {
653                access_key_id: "recovered-key".into(),
654            }),
655            device: DeviceId(3),
656            device_name: "D".into(),
657        },
658    ]
659}
660/// Every independently decoded ordinary object kind.
661pub fn objects() -> Vec<Object> {
662    store_changes()
663        .into_iter()
664        .map(Object::Admin)
665        .chain([Object::JoinRequest(join_request())])
666        .collect()
667}
668/// Ana's request answering the re-binding invite with her replacement pair,
669/// which signs it (D8, D13).
670pub fn join_request() -> JoinRequest {
671    JoinRequest {
672        invite: InviteId(Uuid::from_bytes([0x55; 16])),
673        member: member_id(),
674        keys: replacement(),
675        device: DeviceId(3),
676        device_name: "D".into(),
677        joiner: Joiner::Recovery,
678    }
679}
680/// A two-audience write with a row frame spanning three chunks.
681pub fn chunked_write() -> WriteRecord {
682    let mut record = write();
683    let mut circle = record.parts[0].clone();
684    circle.audience = Audience::Circle(CircleId(Uuid::from_u128(1)));
685    circle.rows[0].row.audience = circle.audience.clone();
686    circle.executions = vec![transient_execution(circle.audience.clone())];
687    record.parts.push(circle);
688    let blob = Operation::Update(BTreeMap::from([(
689        "x".into(),
690        column(Value::Blob(vec![0x41; crate::chunks::CHUNK_SIZE * 2 + 29])),
691    )]));
692    record.parts[0].rows[0].change.operation = blob.clone();
693    record.parts[0].rows[0].execution.steps[0].change.operation = blob;
694    record
695}
696/// A snapshot with a merge row spanning chunk boundaries.
697pub fn chunked_snapshot_frames() -> Vec<Vec<u8>> {
698    let (mut encoder, header) = SnapshotEncoder::start(snapshot_header()).unwrap();
699    let mut records = snapshot_records();
700    let large = records.iter_mut().find(|record| matches!(record, SnapshotRecord::Merge(_)));
701    *large.expect("the fixture's merge row") = SnapshotRecord::Merge(merge_row_of(
702        row(),
703        Value::Blob(vec![0x42; crate::chunks::CHUNK_SIZE + 31]),
704    ));
705    let mut frames = vec![header];
706    for record in records {
707        frames.push(encoder.record(record).unwrap());
708    }
709    frames.push(encoder.finish().unwrap());
710    frames
711}
712/// Bounded canonical frames used to exercise every byte and truncation.
713pub fn frame_examples() -> Vec<Zeroizing<Vec<u8>>> {
714    let mut frames: Vec<_> =
715        objects().iter().map(|v| Zeroizing::new(v.encode().unwrap())).collect();
716    let record = write();
717    let encoder = crate::write_stream::WriteEncoder::new(&record).unwrap();
718    frames.push(Zeroizing::new(encoder.header_frame().to_vec()));
719    frames.push(Zeroizing::new(record.parts[0].rows[0].encode().unwrap()));
720    frames.push(Zeroizing::new(
721        crate::dismissal::Dismissal {
722            row: row(),
723            target: crate::dismissal::DismissalTarget::Cell("x".into()),
724            write: position(),
725        }
726        .encode()
727        .unwrap(),
728    ));
729    let mut migration = record.clone();
730    migration.header.disposition = WriteDisposition::Migration;
731    migration.parts.clear();
732    frames.push(Zeroizing::new(write_plaintext(&migration).unwrap()));
733    frames.extend(snapshot_frames().into_iter().map(Zeroizing::new));
734    frames
735}
736/// Pinned plaintext frames, then a write prefix/header/part chunks and a
737/// snapshot prefix/plaintext chunks. Crypto supplies sealed bytes separately.
738pub fn encoded_examples() -> Vec<Zeroizing<Vec<u8>>> {
739    let mut pieces = frame_examples();
740    let record = chunked_write();
741    let encoder = crate::write_stream::WriteEncoder::new(&record).unwrap();
742    pieces.push(Zeroizing::new(
743        crate::sealed_write::WriteObjectPrefix {
744            format: crate::FormatVersion::CURRENT,
745            store_key: KeyId(Uuid::from_bytes([1; 16])),
746            part_keys: vec![KeyId(Uuid::from_bytes([1; 16])), KeyId(Uuid::from_bytes([2; 16]))],
747        }
748        .encode()
749        .unwrap(),
750    ));
751    pieces.push(Zeroizing::new(encoder.header_frame().to_vec()));
752    for index in 0..record.parts.len() {
753        pieces.extend(encoder.part_chunks(index).unwrap().map(|v| Zeroizing::new(v.unwrap())));
754    }
755    pieces.push(Zeroizing::new(snapshot_prefix(&snapshot_header()).encode().unwrap()));
756    pieces.extend(
757        crate::chunks::PlaintextChunks::new(chunked_snapshot_frames().into_iter().map(Ok))
758            .map(|v| Zeroizing::new(v.unwrap())),
759    );
760    pieces
761}
762
763/// The fixture author has read these original writes.
764pub fn past() -> BTreeSet<WriteId> {
765    [(1, 1), (1, 2), (2, 1)]
766        .map(|(device, number)| WriteId { device: DeviceId(device), number })
767        .into()
768}
769/// Validate the fixture write against its explicitly supplied past.
770pub fn check_past(record: &WriteRecord) -> Result<(), crate::Error> {
771    let past = past();
772    for part in &record.parts {
773        for row in &part.rows {
774            row.validate_past(&past)?;
775        }
776        for execution in &part.executions {
777            execution.execution.sources.validate_past(&past)?;
778        }
779        for dismissal in &part.dismissals {
780            dismissal.validate_past(&past)?;
781        }
782    }
783    Ok(())
784}