aboutsummaryrefslogtreecommitdiff
path: root/test/ui
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-21 03:55:10 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-21 03:55:10 +0200
commit5e8ac1e2f5a660e55ea132996e1d5730cfa4fa64 (patch)
tree9bb99bdca1ef8f33c93c5100816448681329d810 /test/ui
parent88f1cac4fa530fc30fd6ef01970af11c7a959b8e (diff)
feat(glance): show the favourite code in the glance (opt-in)
With "Show the favourite code in the glance" on, the glance shows the code of the first favourite TOTP or Steam entry without the PIN, with a countdown bar. The glance can't open the vault, so the About scan seals that entry under "g" (device key, aad OTPW-GL-1) after every change, and removes it when the option is off or no entry qualifies; the glance also reads the option itself. HKDF moves to its own module so the glance loads only the crypto it needs (37 % of its 64 KB). Manage entry gains "Mark as favourite (glance)" and "Remove favourite", queued for locked entries like the other changes. Lines are inset on round screens so the Instinct 3's curved glance corner doesn't cut them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'test/ui')
-rw-r--r--test/ui/GlanceCodeTest.mc151
1 files changed, 151 insertions, 0 deletions
diff --git a/test/ui/GlanceCodeTest.mc b/test/ui/GlanceCodeTest.mc
new file mode 100644
index 0000000..25c7dd7
--- /dev/null
+++ b/test/ui/GlanceCodeTest.mc
@@ -0,0 +1,151 @@
+// Author: cirakg & claude ai
+// Summary: Tests for the glance code: the sealed item round trip and its codes, tampering, the scan's choice of entry, the option, and the Manage favourite actions.
+
+import Toybox.Lang;
+import Toybox.Test;
+
+(:test)
+module GlanceFixtures {
+
+ const NOW = 1800000026l;
+
+ // The item the glance would read from the store, or null.
+ function item(v as Vault) as GlanceCode.Item? {
+ var store = v.storage();
+ var dk = store.getValue(GlanceCode.DEVICE_KEY);
+ var sealed = store.getValue(GlanceCode.KEY);
+ return GlanceCode.open(dk instanceof ByteArray ? dk : null, sealed instanceof ByteArray ? sealed : null);
+ }
+
+ function favorite(e as Entry) as Entry {
+ e.favorite = true;
+ return e;
+ }
+}
+
+// The glance item opens to the entry's label, and its codes are the
+// entry's codes (TOTP with SHA-256, 8 digits, 60 s; Steam).
+(:test)
+function glanceItemRoundTripsWithTheSameCodes(logger as Test.Logger) as Boolean {
+ var vault = SettingsFixtures.vault();
+ vault.glanceCode = true;
+ var github = vault.add(StorageFixtures.totp("GitHub"));
+ var steam = vault.add(StorageFixtures.steam("Steam"));
+ GlanceStore.update(vault, github);
+ var item = GlanceFixtures.item(vault);
+ var secret = Bytes.fromUtf8("12345678901234567890");
+ var now = GlanceFixtures.NOW;
+ if (item == null) {
+ logger.error("no item");
+ return false;
+ }
+ var totpOk = item.label.equals("GitHub") && item.code(now).equals(Otp.totp(Hashes.SHA256, secret, now, 60, 8))
+ && item.code(now).length() == 8 && item.counter(now) == now / 60 && item.secondsLeft(now) == 60 - (now % 60).toNumber();
+ GlanceStore.update(vault, steam);
+ var s = GlanceFixtures.item(vault);
+ var steamOk = s != null && s.code(now).equals(Otp.steam(secret, now)) && s.periodSeconds() == 30;
+ return totpOk && steamOk && GlanceCode.TYPE_STEAM == Entry.OTP_STEAM && GlanceCode.DEVICE_KEY.equals(KeyManager.DEVICE_KEY);
+}
+
+// A changed byte, another device key or another format gives nothing.
+(:test)
+function glanceItemRefusesTamperingAndOtherKeys(logger as Test.Logger) as Boolean {
+ var vault = SettingsFixtures.vault();
+ vault.glanceCode = true;
+ GlanceStore.update(vault, vault.add(StorageFixtures.totp("GitHub")));
+ var store = vault.storage();
+ var sealed = store.getValue(GlanceCode.KEY) as ByteArray;
+ var dk = store.getValue(GlanceCode.DEVICE_KEY) as ByteArray;
+ var tampered = Bytes.copy(sealed);
+ tampered[20] = tampered[20] ^ 1;
+ var otherKey = Bytes.copy(dk);
+ otherKey[0] = otherKey[0] ^ 1;
+ var bad = GlanceCode.encode("x", 0, 9, 6, 30, [1]b); // algorithm 9 doesn't exist
+ return GlanceCode.open(dk, sealed) != null && GlanceCode.open(dk, tampered) == null
+ && GlanceCode.open(otherKey, sealed) == null && GlanceCode.open(null, sealed) == null
+ && GlanceCode.open(dk, null) == null && GlanceCode.decode(bad) == null
+ && GlanceCode.decode(GlanceCode.encode("x", 0, 0, 6, 0, [1]b)) == null;
+}
+
+// The About scan picks the first favourite TOTP or Steam entry without the
+// PIN (not a locked, HOTP or barcode favourite), and removes the item when
+// the option goes off or the entry is no longer a favourite.
+(:test)
+function glanceScanChoosesTheFirstQualifyingFavourite(logger as Test.Logger) as Boolean {
+ var vault = SettingsFixtures.vault();
+ StorageFixtures.setPin(vault, StorageFixtures.PIN);
+ vault.glanceCode = true;
+ var locked = GlanceFixtures.favorite(StorageFixtures.totp("Bank"));
+ locked.locked = true;
+ vault.add(locked);
+ vault.add(GlanceFixtures.favorite(StorageFixtures.hotp("VPN", 3l)));
+ vault.add(StorageFixtures.code("Gym")); // a favourite barcode
+ vault.add(StorageFixtures.totp("Plain"));
+ var mail = vault.add(GlanceFixtures.favorite(StorageFixtures.totp("Mail")));
+ var s = SettingsFixtures.store();
+ SettingsFixtures.run(vault, s);
+ var chosen = GlanceFixtures.item(vault);
+ var first = chosen != null && chosen.label.equals("Mail");
+ // "Remove favourite" from the phone: the item goes.
+ SettingsFixtures.manage(s, mail, ManageAction.UNFAVORITE);
+ var job = SettingsFixtures.run(vault, s);
+ var gone = vault.storage().getValue(GlanceCode.KEY) == null && SettingsFixtures.text(s, ManageForm.STATUS).find("no longer a favourite") != null;
+ // Marked again, then the option off: the item goes too.
+ SettingsFixtures.manage(s, mail.toLower(), ManageAction.FAVORITE);
+ SettingsFixtures.run(vault, s);
+ var back = GlanceFixtures.item(vault) != null;
+ vault.glanceCode = false;
+ var needed = AboutScan.needed(vault);
+ SettingsFixtures.run(vault, s);
+ var off = vault.storage().getValue(GlanceCode.KEY) == null;
+ if (!(first && gone && back && needed && off)) {
+ logger.error("first " + first + " gone " + gone + " (" + job.lines + ") back " + back + " needed " + needed + " off " + off);
+ return false;
+ }
+ return true;
+}
+
+// Favourite actions check the entry's current mark, and wait for the PIN
+// on a locked entry like the other changes.
+(:test)
+function manageFavouriteActions(logger as Test.Logger) as Boolean {
+ var vault = ManageFixtures.vault();
+ var github = vault.ids()[0];
+ var s = ManageFixtures.send(vault, github, ManageAction.UNFAVORITE);
+ SettingsFixtures.run(vault, s);
+ var notYet = SettingsFixtures.text(s, ManageForm.STATUS).find("is not a favourite") != null;
+ s = ManageFixtures.send(vault, github, ManageAction.FAVORITE);
+ SettingsFixtures.run(vault, s);
+ var marked = (vault.header(github) as Entry).favorite;
+ // (The same form again would be a repeat; mark another entry directly.)
+ var vpn = vault.ids()[1];
+ var e = vault.open(vpn);
+ e.favorite = true;
+ vault.update(e);
+ e.wipeSecrets();
+ s = ManageFixtures.send(vault, vpn, ManageAction.FAVORITE);
+ SettingsFixtures.run(vault, s);
+ var already = SettingsFixtures.text(s, ManageForm.STATUS).find("already a favourite") != null;
+ var bank = ManageFixtures.addLocked(vault, "Bank");
+ vault.lock();
+ s = ManageFixtures.send(vault, bank, ManageAction.FAVORITE);
+ SettingsFixtures.run(vault, s);
+ var waits = ChangeQueue.count(vault) == 1 && !(vault.anyHeader(bank) as Entry).favorite;
+ var a = new ManageAction();
+ a.id = bank;
+ a.action = ManageAction.UNFAVORITE;
+ var parsed = ManageAction.fromBytes(a.toBytes());
+ var ok = notYet && marked && already && waits && parsed != null && parsed.action == ManageAction.UNFAVORITE
+ && a.describe().equals("Remove favourite: " + bank) && ManageAction.NAMES.size() == ManageAction.UNFAVORITE + 1;
+ if (!ok) {
+ logger.error("notYet " + notYet + " marked " + marked + " already " + already + " waits " + waits + " queue "
+ + ChangeQueue.count(vault) + " describe '" + a.describe() + "'");
+ }
+ return ok;
+}
+
+(:test)
+function glanceGroupsDigits(logger as Test.Logger) as Boolean {
+ return GlanceCode.grouped("123456").equals("123 456") && GlanceCode.grouped("1234567").equals("1234 567")
+ && GlanceCode.grouped("12345678").equals("1234 5678") && GlanceCode.grouped("VK4T9").equals("VK4T9");
+}