summaryrefslogtreecommitdiff
path: root/test/settings
diff options
context:
space:
mode:
Diffstat (limited to 'test/settings')
-rw-r--r--test/settings/ChangeQueueTest.mc239
-rw-r--r--test/settings/ManageTest.mc39
2 files changed, 269 insertions, 9 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;
+}
diff --git a/test/settings/ManageTest.mc b/test/settings/ManageTest.mc
index 1f050c5..4c93f60 100644
--- a/test/settings/ManageTest.mc
+++ b/test/settings/ManageTest.mc
@@ -1,6 +1,7 @@
// Author: cirakg & claude ai
// Summary: Tests for the Manage entry form: every action on unlocked entries, the PIN queue for locked ones, IDs, repeats and damage.
+import Toybox.Application;
import Toybox.Lang;
import Toybox.Test;
@@ -43,9 +44,29 @@ module ManageFixtures {
return s;
}
+ // What PinView runs after a correct PIN: promotion and waiting locks.
+ function applyLocks(v as Vault) as LockJob {
+ var job = new LockJob(v, true);
+ Jobs.runToCompletion(job, 1);
+ return job;
+ }
+
+ // Every queued item, opened (the ones that verify).
+ function load(v as Vault) as Array<ManageAction> {
+ var items = ChangeQueue.items(v);
+ var out = [] as Array<ManageAction>;
+ for (var i = 0; i < items.size(); i++) {
+ var a = ChangeQueue.open(v, items[i], new Jobs.Wiper(), true);
+ if (a != null) {
+ out.add(a);
+ }
+ }
+ return out;
+ }
+
// Confirm the waiting changes, as after a correct PIN.
function confirm(v as Vault) as ChangeQueueJob {
- var job = new ChangeQueueJob(v);
+ var job = new ChangeQueueJob(v, null);
Jobs.runToCompletion(job, 1);
return job;
}
@@ -256,7 +277,7 @@ function manageLocksEntries(logger as Test.Logger) as Boolean {
}
// Waiting "Require PIN" changes apply at the next PIN without "Confirm
-// all" (the PIN screen calls applyLocks); other changes keep waiting, and
+// all" (the PIN screen runs LockJob); other changes keep waiting, and
// the results of a following "Confirm all" list the locks too.
(:test)
function manageQueuedLocksApplyAtNextPin(logger as Test.Logger) as Boolean {
@@ -271,14 +292,14 @@ function manageQueuedLocksApplyAtNextPin(logger as Test.Logger) as Boolean {
s = ManageFixtures.send(vault, ids[1], ManageAction.LOCK);
SettingsFixtures.run(vault, s);
var counted = ChangeQueue.count(vault) == 3 && ChangeQueue.countLocks(vault) == 2;
- ChangeQueue.applyLocks(vault); // still locked: nothing happens
+ ManageFixtures.applyLocks(vault); // still locked: nothing happens
var untouched = ChangeQueue.count(vault) == 3 && !ManageFixtures.isLocked(vault, ids[0]);
StorageFixtures.unlock(vault, StorageFixtures.PIN);
- ChangeQueue.applyLocks(vault);
+ var locks = ManageFixtures.applyLocks(vault);
var left = ChangeQueue.descriptions(vault);
var applied = ManageFixtures.isLocked(vault, ids[0]) && ManageFixtures.isLocked(vault, ids[1])
&& left.size() == 1 && left[0].equals("Delete: " + bank + " Bank (alice@example.com)")
- && ChangeQueue.countLocks(vault) == 0 && vault.ids().indexOf(bank) >= 0;
+ && ChangeQueue.countLocks(vault) == 0 && vault.ids().indexOf(bank) >= 0 && locks.lines.size() == 2;
var job = ManageFixtures.confirm(vault);
var reported = job.lines.size() == 3 && job.lines[0].find(ids[0] + " ") == 0
&& job.lines[0].find("now requires the PIN") != null && SettingsFixtures.hasLine(job.lines, "Deleted " + bank);
@@ -299,7 +320,7 @@ function manageQueueKeepsLateArrivals(logger as Test.Logger) as Boolean {
var safe = ManageFixtures.addLocked(vault, "Safe");
var s = ManageFixtures.send(vault, bank, ManageAction.DELETE);
SettingsFixtures.run(vault, s);
- var job = new ChangeQueueJob(vault);
+ var job = new ChangeQueueJob(vault, null);
job.step(1);
s = ManageFixtures.send(vault, safe, ManageAction.DELETE);
SettingsFixtures.run(vault, s);
@@ -337,10 +358,10 @@ function manageQueueIsSealedAndLimited(logger as Test.Logger) as Boolean {
b.id = "M3P9";
b.action = ManageAction.DELETE;
ChangeQueue.add(vault, b);
- var raw = (vault.storage() as MemoryBackend).raw(ChangeQueue.KEY) as Array<ByteArray>;
- var box = raw[0];
+ var raw = (vault.storage() as MemoryBackend).raw(ChangeQueue.KEY) as Array<Array<Application.Storage.ValueType>>;
+ var box = raw[0][1] as ByteArray;
box[20] = box[20] ^ 1;
- var loaded = ChangeQueue.load(vault);
+ var loaded = ManageFixtures.load(vault);
var sealed = loaded.size() == 1 && loaded[0].id.equals("M3P9") && loaded[0].action == ManageAction.DELETE;
var roundTrip = ManageAction.fromBytes(a.toBytes());
var same = roundTrip != null && roundTrip.decoded && roundTrip.label.equals("Bank")