// 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; }