summaryrefslogtreecommitdiff
path: root/test/settings/ChangeQueueTest.mc
blob: 8ad0feb03cad10ec102c7a6e767eb6a45a9002df (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// Author:  cirakg & claude ai
// Summary: Tests for the change queue's item format: codes readable without decrypting, material stored apart, 0.9.0 items, LockJob and stepwise promotion.

import Toybox.Application;
import Toybox.Lang;
import Toybox.Test;

(:test)
module QueueFixtures {

    // A queued action for `id`.
    function action(id as String, code as Number) as ManageAction {
        var a = new ManageAction();
        a.id = id;
        a.action = code;
        a.label = "Bank";
        return a;
    }

    // Queue an action the way 0.9.0 did: one bare box, material inside.
    function addOld(v as Vault, a as ManageAction) as Void {
        var stored = ChangeQueue.items(v);
        stored.add(SealedBox.seal(v.keyManager.deviceKeys(), Bytes.fromUtf8(ChangeQueue.AAD_V1), a.toBytes()));
        v.storage().setValue(ChangeQueue.KEY, stored);
        ChangeQueue.changed();
    }

    function backend(v as Vault) as MemoryBackend {
        return v.storage() as MemoryBackend;
    }
}

// Items are [code, box] or [code, box, material number]; a REPLACE's
// material is a sealed value of its own, not part of the item's box.
(:test)
function queueStoresMaterialApart(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var bank = ManageFixtures.addLocked(vault, "Bank");
    var r = QueueFixtures.action(bank, ManageAction.REPLACE);
    r.material = Bytes.fromUtf8("abcdefghijklmnopqrst");
    r.decoded = true;
    ChangeQueue.add(vault, QueueFixtures.action(bank, ManageAction.DELETE));
    ChangeQueue.add(vault, r);
    var raw = QueueFixtures.backend(vault).raw(ChangeQueue.KEY) as Array<Array<Application.Storage.ValueType>>;
    var items = ChangeQueue.items(vault);
    var shape = raw.size() == 2 && raw[0].size() == 2 && ChangeQueue.codeOf(items[0]) == ManageAction.DELETE && raw[1].size() == 3
        && ChangeQueue.codeOf(items[1]) == ManageAction.REPLACE && ChangeQueue.refOf(items[1]) == 0;
    var stored = QueueFixtures.backend(vault).raw(ChangeQueue.MATERIAL_KEY + "0");
    var opened = ChangeQueue.open(vault, raw[1] as Application.Storage.ValueType, new Jobs.Wiper(), true);
    var same = opened != null && opened.material != null
        && Bytes.equal(opened.material as ByteArray, Bytes.fromUtf8("abcdefghijklmnopqrst")) && opened.ref == 0;
    // The material opens only for its own number and entry.
    raw[1][2] = 1;
    var other = QueueFixtures.action(bank, ManageAction.REPLACE);
    other.material = Bytes.fromUtf8("other");
    ChangeQueue.add(vault, other); // takes number 0 again
    var swapped = ChangeQueue.open(vault, raw[1] as Application.Storage.ValueType, new Jobs.Wiper(), true) == null;
    if (!shape || !same || !swapped) {
        logger.error("shape " + shape + " same " + same + " swapped " + swapped);
    }
    return shape && same && swapped && stored instanceof ByteArray && r.material != null && r.ref == -1;
}

// Counting reads only the codes: a damaged box still counts, and no item
// is opened (a tampered code fails only when the item is opened).
(:test)
function queueCountsWithoutDecrypting(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var ids = vault.ids();
    vault.lock();
    ChangeQueue.add(vault, QueueFixtures.action(ids[0], ManageAction.LOCK));
    ChangeQueue.add(vault, QueueFixtures.action(ids[1], ManageAction.LOCK));
    ChangeQueue.add(vault, QueueFixtures.action(ids[2], ManageAction.DELETE));
    var raw = QueueFixtures.backend(vault).raw(ChangeQueue.KEY) as Array<Array<Application.Storage.ValueType>>;
    var box = raw[0][1] as ByteArray;
    box[5] = box[5] ^ 1;
    var counted = ChangeQueue.countLocks(vault) == 2 && ChangeQueue.countToConfirm(vault) == 1;
    // A LOCK relabelled DELETE doesn't open.
    raw[1][0] = ManageAction.DELETE;
    ChangeQueue.changed();
    var relabelled = ChangeQueue.countLocks(vault) == 1 && ChangeQueue.open(vault, raw[1] as Application.Storage.ValueType, new Jobs.Wiper(), true) == null
        && ChangeQueue.open(vault, raw[0] as Application.Storage.ValueType, new Jobs.Wiper(), true) == null
        && ChangeQueue.open(vault, raw[2] as Application.Storage.ValueType, new Jobs.Wiper(), true) != null;
    return counted && relabelled;
}

// Items queued by 0.9.0 still count (as "to confirm"), describe, apply at
// "Confirm all", and their locks apply at the next PIN.
(:test)
function queueReadsOldItems(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var ids = vault.ids();
    var bank = ManageFixtures.addLocked(vault, "Bank");
    vault.lock();
    QueueFixtures.addOld(vault, QueueFixtures.action(ids[0], ManageAction.LOCK));
    QueueFixtures.addOld(vault, QueueFixtures.action(bank, ManageAction.DELETE));
    var counted = ChangeQueue.count(vault) == 2 && ChangeQueue.countLocks(vault) == 0 && ChangeQueue.countToConfirm(vault) == 2;
    var described = ChangeQueue.descriptions(vault);
    var listed = described.size() == 2 && described[1].equals("Delete: " + bank + " Bank");
    StorageFixtures.unlock(vault, StorageFixtures.PIN);
    var locks = ManageFixtures.applyLocks(vault);
    var locked = ManageFixtures.isLocked(vault, ids[0]) && locks.lines.size() == 1 && ChangeQueue.count(vault) == 1;
    var job = ManageFixtures.confirm(vault);
    if (!counted || !listed || !locked) {
        logger.error("counted " + counted + " listed " + listed + " locked " + locked + " " + described.toString());
    }
    return counted && listed && locked && vault.ids().indexOf(bank) < 0 && ChangeQueue.count(vault) == 0
        && SettingsFixtures.hasLine(job.lines, "Deleted " + bank);
}

// A REPLACE is checked before it is queued: contents too long fail at once;
// valid ones are queued as checked contents and applied after the PIN.
(:test)
function queueChecksReplaceFirst(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var card = StorageFixtures.code("Card");
    card.type = Codes.QR;
    card.locked = true;
    var id = vault.add(card);
    var s = ManageFixtures.send(vault, id, ManageAction.REPLACE);
    var long = "";
    for (var i = 0; i < Entry.MAX_PAYLOAD_BYTES / 8 + 1; i++) {
        long += "abcdefgh";
    }
    s.set("mgSecret", long);
    SettingsFixtures.run(vault, s);
    var refused = SettingsFixtures.contains(logger, s, ManageForm.STATUS, "Replace: too long") && ChangeQueue.count(vault) == 0;
    s = ManageFixtures.send(vault, id, ManageAction.REPLACE);
    s.set("mgSecret", "new contents");
    SettingsFixtures.run(vault, s);
    var queued = ManageFixtures.load(vault);
    var checked = queued.size() == 1 && queued[0].decoded && queued[0].ref == 0
        && Bytes.equal(queued[0].material as ByteArray, Bytes.fromUtf8("new contents"));
    var job = ManageFixtures.confirm(vault);
    var applied = SettingsFixtures.secretOf(vault, id).equals("new contents");
    if (!applied) {
        logger.error(job.lines.toString());
    }
    return refused && checked && applied;
}

// A REPLACE's material record goes with its item: rejected, confirmed, or
// dropped as damaged by clear().
(:test)
function queueDeletesMaterialWithItem(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var bank = ManageFixtures.addLocked(vault, "Bank");
    var r = QueueFixtures.action(bank, ManageAction.REPLACE);
    r.material = Bytes.fromUtf8("abcdefghijklmnopqrst");
    r.decoded = true;
    var key = ChangeQueue.MATERIAL_KEY + "0";
    ChangeQueue.add(vault, r);
    var stored = vault.storage().getValue(key) != null;
    ChangeQueue.clear(vault);
    var rejected = vault.storage().getValue(key) == null;
    ChangeQueue.add(vault, r);
    ManageFixtures.confirm(vault);
    var confirmed = vault.storage().getValue(key) == null && ChangeQueue.count(vault) == 0
        && SettingsFixtures.secretOf(vault, bank).equals("abcdefghijklmnopqrst");
    // A missing material record makes its item fail to open (dropped).
    ChangeQueue.add(vault, r);
    vault.storage().deleteValue(key);
    var job = ManageFixtures.confirm(vault);
    return stored && rejected && confirmed && ChangeQueue.count(vault) == 0 && job.lines.size() == 1
        && job.lines[0].equals("Not applied: a change was damaged");
}

// LockJob drops a lock that can't apply (with a line), keeps the other
// items in order, and leaves its lines for a "Confirm all" once.
(:test)
function queueLockJobAppliesAndDrops(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var ids = vault.ids();
    var bank = ManageFixtures.addLocked(vault, "Bank");
    vault.lock();
    ChangeQueue.add(vault, QueueFixtures.action(ids[0], ManageAction.LOCK));
    ChangeQueue.add(vault, QueueFixtures.action(bank, ManageAction.DELETE));
    ChangeQueue.add(vault, QueueFixtures.action("ZZZZ", ManageAction.LOCK));
    ChangeQueue.add(vault, QueueFixtures.action(bank, ManageAction.FAVORITE));
    StorageFixtures.unlock(vault, StorageFixtures.PIN);
    var job = ManageFixtures.applyLocks(vault);
    var items = ChangeQueue.items(vault);
    var kept = items.size() == 2 && ChangeQueue.codeOf(items[0]) == ManageAction.DELETE
        && ChangeQueue.codeOf(items[1]) == ManageAction.FAVORITE;
    var lines = job.lines.size() == 2 && job.lines[1].find("Not applied: Require PIN: ZZZZ") == 0;
    var taken = ChangeQueue.takeLockLines().size() == 2 && ChangeQueue.takeLockLines().size() == 0;
    if (!kept || !lines) {
        logger.error(job.lines.toString());
    }
    return kept && lines && taken && ManageFixtures.isLocked(vault, ids[0]);
}

// Pending entries are promoted a few per step, each saved on its own.
(:test)
function queuePromotesInSteps(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    vault.lock();
    for (var i = 0; i < 8; i++) {
        var e = StorageFixtures.totp("Pending " + i);
        e.locked = true;
        vault.add(e);
    }
    StorageFixtures.unlock(vault, StorageFixtures.PIN);
    var job = new LockJob(vault, true);
    job.step(1);
    var partly = vault.pendingIds().size() > 0 && vault.pendingIds().size() < 8;
    var steps = 1;
    while (!job.step(1)) {
        steps++;
    }
    return partly && steps > 2 && vault.pendingIds().size() == 0 && vault.ids().size() == 11
        && ManageFixtures.isLocked(vault, vault.ids()[10]);
}

// "Confirm all" and "Reject all" act only on the items the menu read
// (DescribeJob.keys): changes that arrive meanwhile keep waiting.
(:test)
function queueActsOnWhatWasShown(logger as Test.Logger) as Boolean {
    var vault = ManageFixtures.vault();
    var bank = ManageFixtures.addLocked(vault, "Bank");
    var safe = ManageFixtures.addLocked(vault, "Safe");
    ChangeQueue.add(vault, QueueFixtures.action(bank, ManageAction.DELETE));
    var menu = new DescribeJob(vault);
    Jobs.runToCompletion(menu, 1);
    ChangeQueue.add(vault, QueueFixtures.action(safe, ManageAction.DELETE)); // while the PIN is typed
    var job = new ChangeQueueJob(vault, menu.keys);
    Jobs.runToCompletion(job, 1);
    var confirmed = vault.ids().indexOf(bank) < 0 && vault.ids().indexOf(safe) >= 0 && ChangeQueue.count(vault) == 1
        && SettingsFixtures.hasLine(job.lines, "1 change arrived meanwhile and still waits");
    menu = new DescribeJob(vault);
    Jobs.runToCompletion(menu, 1);
    ChangeQueue.add(vault, QueueFixtures.action(bank, ManageAction.FAVORITE));
    var left = ChangeQueue.remove(vault, menu.keys); // "Reject all"
    var items = ChangeQueue.items(vault);
    if (!confirmed) {
        logger.error(job.lines.toString());
    }
    return confirmed && left == 1 && ChangeQueue.codeOf(items[0]) == ManageAction.FAVORITE && menu.keys.size() == 1;
}