diff options
Diffstat (limited to 'test/settings')
| -rw-r--r-- | test/settings/ChangeQueueTest.mc | 239 | ||||
| -rw-r--r-- | test/settings/ManageTest.mc | 39 |
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") |
