summaryrefslogtreecommitdiff
path: root/test/settings/SettingsJobTest.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
commit479d7ba2e843d5ea93979f2eed68f4d38d146c25 (patch)
tree52b0bc58a7456e8675e977698db20e55515b7100 /test/settings/SettingsJobTest.mc
parent151bfda5d4c567ff909a61dc9c90bf8eece1db37 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'test/settings/SettingsJobTest.mc')
-rw-r--r--test/settings/SettingsJobTest.mc175
1 files changed, 175 insertions, 0 deletions
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<Dictionary>;
+ 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;
+}