From 479d7ba2e843d5ea93979f2eed68f4d38d146c25 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 18:36:54 +0200 Subject: test(settings): cover every form, symbology and Manage action; send the forms end to end in the UI flow 34 Monkey C tests (154 per device): validation messages, idempotency, the queue, the About scan, "Encrypt names" and the properties contract. The UI-test build fills in Add OTP, Add code and Manage like the phone; the flow checks the new TOTP (309 848 at the frozen time), decodes the Code 128 card and confirms the queued change to the locked entry with the PIN. Co-Authored-By: Claude Opus 5.5 --- test/settings/SettingsJobTest.mc | 175 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 test/settings/SettingsJobTest.mc (limited to 'test/settings/SettingsJobTest.mc') diff --git a/test/settings/SettingsJobTest.mc b/test/settings/SettingsJobTest.mc new file mode 100644 index 0000000..07dbc64 --- /dev/null +++ b/test/settings/SettingsJobTest.mc @@ -0,0 +1,175 @@ +// Author: cirakg & claude ai +// Summary: Tests for the whole settings run: README order, the import through its form, the About fields and "Encrypt names". + +import Toybox.Application; +import Toybox.Lang; +import Toybox.Test; + +// All forms at once are applied in README 10.3 order: Import, Add OTP, +// Add code, Manage. +(:test) +function settingsAppliesFormsInOrder(logger as Test.Logger) as Boolean { + var vault = ManageFixtures.vault(); + var first = vault.ids()[0]; + var s = SettingsFixtures.store(); + SettingsFixtures.manage(s, first, ManageAction.DELETE); + SettingsFixtures.code(s, Codes.QR, "Member", "https://example.com/m/42"); + SettingsFixtures.otp(s, Entry.OTP_TOTP, "Mail", "bob", AegisFixtures.SECRET_2); + s.set("impText1", ImporterFixtures.uri("Proton", "zoe", AegisFixtures.SECRET_3)); + s.set("impApply", true); + var job = SettingsFixtures.run(vault, s); + var lines = job.lines; + var order = ["Import: Imported 1", "Add OTP: Added", "Add code: Added", "Manage: Deleted " + first]; + var at = 0; + for (var i = 0; i < lines.size() && at < order.size(); i++) { + if (lines[i].find(order[at] as String) == 0) { + at++; + } + } + if (at != order.size() || job.forms != 4) { + logger.error(lines.toString()); + return false; + } + // 3 fixtures + 3 added - 1 deleted. + return vault.ids().size() == 5 && SettingsFixtures.text(s, "impText1").equals("") && !Forms.flag(s, "impApply") + && SettingsFixtures.text(s, "impStatus").equals("Imported 1"); +} + +// The import form keeps its M5 behaviour: a repeat is ignored, a cancel +// leaves the text. +(:test) +function settingsImportFormIsAppliedOnce(logger as Test.Logger) as Boolean { + var vault = SettingsFixtures.vault(); + var s = SettingsFixtures.store(); + var uri = ImporterFixtures.uri("Proton", "zoe", AegisFixtures.SECRET_3); + s.set("impText1", uri); + s.set("impApply", true); + SettingsFixtures.run(vault, s); + s.set("impText1", uri); + s.set("impApply", true); + SettingsFixtures.run(vault, s); + var repeated = SettingsFixtures.text(s, "impStatus").equals("Already imported") && vault.ids().size() == 1; + s.set("impText1", "partial"); + s.set("impMode", Importer.MODE_CANCEL); + s.set("impApply", true); + SettingsFixtures.run(vault, s); + return repeated && SettingsFixtures.text(s, "impStatus").equals("Waiting import cancelled") + && SettingsFixtures.text(s, "impText1").equals("partial"); +} + +// The About fields list IDs and names, the version and the storage use; +// they are rewritten only when something changed. +(:test) +function settingsWritesTheAboutFields(logger as Test.Logger) as Boolean { + var vault = ManageFixtures.vault(); + var ids = vault.ids(); + var s = SettingsFixtures.store(); + var job = SettingsFixtures.run(vault, s); + var catalog = SettingsFixtures.text(s, "catalog"); + var expected = ids[0] + " GitHub (alice@example.com) · " + ids[1] + " VPN (alice@example.com) · " + ids[2] + " Gym"; + if (!catalog.equals(expected)) { + logger.error("catalog: " + catalog); + return false; + } + var fresh = !AboutScan.needed(vault) && job.forms == 0 && job.lines.size() == 0; + vault.add(StorageFixtures.totp("New")); + return fresh && AboutScan.needed(vault) && SettingsFixtures.text(s, "version").equals(AppInfo.VERSION) + && SettingsFixtures.contains(logger, s, "storageUse", "3 of 150 entries, 1 KB of about 100 KB"); +} + +// A long catalog is cut with a count of what's left out. +(:test) +function settingsShortensTheCatalog(logger as Test.Logger) as Boolean { + var vault = SettingsFixtures.vault(); + for (var i = 0; i < 60; i++) { + var e = StorageFixtures.totp("Service number " + i); + vault.add(e); + } + var s = SettingsFixtures.store(); + SettingsFixtures.run(vault, s); + var catalog = SettingsFixtures.text(s, "catalog"); + return catalog.length() <= AboutScan.CATALOG_MAX_CHARS + 60 && catalog.find("more (see Entry IDs on the watch)") != null; +} + +// "Encrypt names" moves names into the encrypted part of every entry it +// can reach, keeps them off the phone's catalog, and is undone when off. +// Locked entries wait for the session. +(:test) +function settingsAppliesEncryptNames(logger as Test.Logger) as Boolean { + var vault = ManageFixtures.vault(); + var bank = ManageFixtures.addLocked(vault, "Bank"); + vault.lock(); + vault.encryptNames = true; + var s = SettingsFixtures.store(); + SettingsFixtures.run(vault, s); + var ids = vault.ids(); + var unlockedDone = (vault.anyHeader(ids[0]) as Entry).namesEncrypted && (vault.anyHeader(ids[0]) as Entry).name.equals("") + && (vault.header(ids[0]) as Entry).name.equals("GitHub"); + var lockedWaits = !(vault.anyHeader(bank) as Entry).namesEncrypted && AboutScan.needed(vault); + var catalog = SettingsFixtures.text(s, "catalog"); + var namesHidden = catalog.find("GitHub") == null && catalog.find(ids[0] + " TOTP") != null && catalog.find(ids[2] + " EAN-13") != null; + StorageFixtures.unlock(vault, StorageFixtures.PIN); + SettingsFixtures.run(vault, s); + var lockedDone = (vault.anyHeader(bank) as Entry).namesEncrypted && !AboutScan.needed(vault); + vault.encryptNames = false; + SettingsFixtures.run(vault, s); + var back = !(vault.anyHeader(ids[0]) as Entry).namesEncrypted && (vault.anyHeader(ids[0]) as Entry).name.equals("GitHub") + && SettingsFixtures.text(s, "catalog").find("GitHub") != null; + // A new entry follows the option. + vault.encryptNames = true; + var id = vault.add(StorageFixtures.totp("Later")); + var follows = (vault.anyHeader(id) as Entry).namesEncrypted; + if (!(unlockedDone && lockedWaits && namesHidden && lockedDone && back && follows)) { + logger.error([unlockedDone, lockedWaits, namesHidden, lockedDone, back, follows].toString() + " " + catalog); + return false; + } + return true; +} + +// hasWork: nothing to do once the About fields are current and every +// switch is off. +(:test) +function settingsHasWorkOnlyWhenNeeded(logger as Test.Logger) as Boolean { + var vault = SettingsFixtures.vault(); + var s = SettingsFixtures.store(); + var before = SettingsJob.hasWork(vault, s); + SettingsFixtures.run(vault, s); + var after = SettingsJob.hasWork(vault, s); + s.set(CodeForm.SWITCH, true); + return before && !after && SettingsJob.hasWork(vault, s); +} + +// The watch's side of the properties contract (tools/check_resources.py +// checks the phone's side): every field the forms read exists in +// properties.xml (reading an undefined property crashes the app) and the +// values a cleared form goes back to are the properties' defaults. +(:test) +function settingsDefaultsMatchProperties(logger as Test.Logger) as Boolean { + var forms = [OtpForm.defaults(), CodeForm.defaults(), ManageForm.defaults()] as Array; + var ok = true; + for (var f = 0; f < forms.size(); f++) { + var d = forms[f]; + var keys = d.keys(); + for (var i = 0; i < keys.size(); i++) { + var key = keys[i] as String; + var actual = Application.Properties.getValue(key); + var expected = (d[key] as Object).toString(); + if (actual == null || !actual.toString().equals(expected)) { + logger.error(key + ": property " + actual + ", form default " + expected); + ok = false; + } + } + } + var others = [OtpForm.SWITCH, OtpForm.STATUS, CodeForm.SWITCH, CodeForm.STATUS, ManageForm.SWITCH, ManageForm.STATUS, + "impApply", "impStatus", "impMode", "impLocked", "catalog", "version", "storageUse"]; + for (var i = 0; i < others.size(); i++) { + if (Application.Properties.getValue(others[i] as String) == null) { + logger.error((others[i] as String) + " is missing"); + ok = false; + } + } + for (var i = 1; i <= SettingsJob.IMPORT_TEXT_FIELDS; i++) { + ok = ok && Application.Properties.getValue("impText" + i) != null; + } + return ok; +} -- cgit v1.2.3