// Author: cirakg & claude ai // Summary: Tests for the UI's logic without a screen: the code cache, code grouping, text helpers and option defaults. import Toybox.Lang; import Toybox.Test; (:test) function codeCacheSplitsCodesForReading(logger as Test.Logger) as Boolean { var cases = [["123456", "123|456"], ["1234567", "123|4567"], ["12345678", "1234|5678"], ["R7VXK", "R7VXK"]]; for (var i = 0; i < cases.size(); i++) { var c = cases[i] as Array; var parts = CodeCache.split(c[0]); var joined = parts.size() == 2 ? parts[0] + "|" + parts[1] : parts[0]; if (!TestSupport.sameString(logger, "split " + c[0], joined, c[1])) { return false; } } return true; } // The cache gives the same codes as the OTP module, keeps them until the // period ends, and learns the period and cost from the encrypted part. (:test) function codeCacheComputesAndExpiresCodes(logger as Test.Logger) as Boolean { var vault = new Vault(new MemoryBackend(), new FakeTime()); var e = StorageFixtures.totp("Cache"); var secret = Bytes.copy(e.secret as ByteArray); var id = vault.add(e); var cache = new CodeCache(vault); var now = 1800000026l; // 34 s left in the fixture's 60 s period var expected = Otp.totp(Hashes.SHA256, secret, now, 60, 8); var next = Otp.totp(Hashes.SHA256, secret, now + 60, 60, 8); var unknownCost = cache.costOf(e); var code = cache.code(id, now, 0); var stillCached = cache.cached(id, now + 33) != null; var expired = cache.cached(id, now + 34) == null; return TestSupport.sameString(logger, "current", code, expected) && TestSupport.sameString(logger, "next", cache.code(id, now, 1), next) && stillCached && expired && cache.periodOf(id) == 60 && unknownCost == CodeCache.COST_SHA512 && cache.costOf(e) == CodeCache.COST_NATIVE; } (:test) function codeCacheHotpAndInvalidate(logger as Test.Logger) as Boolean { var vault = new Vault(new MemoryBackend(), new FakeTime()); var id = vault.add(StorageFixtures.hotp("Counter", 5l)); var cache = new CodeCache(vault); var first = cache.code(id, 0l, 0); vault.setCounter(id, 6l); var stale = cache.code(id, 0l, 0); cache.invalidate(id); var fresh = cache.code(id, 0l, 0); // RFC 4226 Appendix D: counters 5 and 6. return first.equals("254676") && stale.equals("254676") && fresh.equals("287922"); } (:test) function codeCacheRefusesLockedEntriesWhileLocked(logger as Test.Logger) as Boolean { var vault = new Vault(new MemoryBackend(), new FakeTime()); StorageFixtures.setPin(vault, StorageFixtures.PIN); var e = StorageFixtures.totp("Locked"); e.locked = true; var id = vault.add(e); var cache = new CodeCache(vault); var unlockedCode = cache.code(id, 1800000026l, 0); vault.lock(); cache.clear(); var code = 0; try { cache.code(id, 1800000026l, 0); } catch (err instanceof VaultError) { code = err.code; } return unlockedCode.length() == 8 && code == VaultError.LOCKED && cache.cached(id, 1800000026l) == null; } (:test) function uiTextHelpers(logger as Test.Logger) as Boolean { var parts = Ui.split("a b c", " "); return Ui.isDigits("123 456") && !Ui.isDigits("12a") && !Ui.isDigits("") && !Ui.isDigits("------") && parts.size() == 4 && parts[0].equals("a") && parts[2].equals("") && parts[3].equals("c") && Ui.minutesSeconds(90).equals("1:30") && Ui.minutesSeconds(3600).equals("60:00") && Ui.minutesSeconds(5).equals("0:05"); } // resources/settings/properties.xml defaults give README 10.2's defaults. (:test) function optionsDefaults(logger as Test.Logger) as Boolean { var o = new Options(); o.load(); return o.autoLockMs == 60000 && !o.eraseAfterFailures && o.timeOffset == 0 && o.codeSize == Options.CODE_LARGE && o.theme == Options.THEME_DARK && o.showGroups && !o.encryptNames && !o.glanceCode && o.fullBrightness; }