aboutsummaryrefslogtreecommitdiff
path: root/test/settings/ChangeQueueTest.mc
diff options
context:
space:
mode:
Diffstat (limited to 'test/settings/ChangeQueueTest.mc')
-rw-r--r--test/settings/ChangeQueueTest.mc239
1 files changed, 239 insertions, 0 deletions
diff --git a/test/settings/ChangeQueueTest.mc b/test/settings/ChangeQueueTest.mc
new file mode 100644
index 0000000..8ad0feb
--- /dev/null
+++ b/test/settings/ChangeQueueTest.mc
@@ -0,0 +1,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;
+}