// Author: cirakg & claude ai // Summary: Tests for otpauth:// parsing: splitting, labels, parameters, Steam, separators and every validation error. import Toybox.Lang; import Toybox.Test; (:test) module OtpAuthFixtures { // RFC 4226 test key "12345678901234567890" in Base32. const SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // lint: allow-literal (RFC test key) function parse(uri as String) as ImportItem { return OtpAuth.parse(uri, 1); } function entry(logger as Test.Logger, uri as String) as Entry? { var item = parse(uri); if (item.entry == null) { logger.error("no entry for " + uri + ": " + item.problem + " / " + item.skipped); } return item.entry; } } (:test) function otpauthSplitsRunTogetherUris(logger as Test.Logger) as Boolean { var s = OtpAuthFixtures.SECRET; var uris = OtpAuth.splitUris("junk otpauth://totp/A?secret=" + s + " \n OTPAUTH://totp/B?secret=" + s + "otpauth://hotp/C?secret=" + s + "&counter=1\n"); return uris.size() == 3 && (uris[0] as String).find("totp/A") != null && (uris[1] as String).find("totp/B") != null && (uris[2] as String).find("counter=1") != null && (uris[2] as String).find("\n") == null; } (:test) function otpauthLabelsAndDefaults(logger as Test.Logger) as Boolean { var e = OtpAuthFixtures.entry(logger, "otpauth://totp/ACME%20Co:john%40example.com?secret=" + OtpAuthFixtures.SECRET); var bare = OtpAuthFixtures.entry(logger, "otpauth://totp/alice?secret=" + OtpAuthFixtures.SECRET.toLower()); if (e == null || bare == null) { return false; } return e.name.equals("ACME Co") && e.account.equals("john@example.com") && e.algorithm == Hashes.SHA1 && e.digits == 6 && e.period == 30 && e.type == Entry.OTP_TOTP && e.source == Entry.SOURCE_IMPORT && Bytes.equal(e.secret as ByteArray, Bytes.fromUtf8("12345678901234567890")) && bare.name.equals("") && bare.account.equals("alice"); } (:test) function otpauthParametersAndSeparators(logger as Test.Logger) as Boolean { var s = OtpAuthFixtures.SECRET; var e = OtpAuthFixtures.entry(logger, "otpauth://totp/Label:bob?SECRET=" + s + ";Algorithm=sha512;digits=8&period=60&image=https%3A%2F%2Fx&issuer=Big+Bank"); var item = OtpAuthFixtures.parse("otpauth://totp/Old:bob?secret=" + s + "&issuer=New"); if (e == null) { return false; } return e.name.equals("Big Bank") && e.algorithm == Hashes.SHA512 && e.digits == 8 && e.period == 60 && item.entry != null && (item.entry as Entry).name.equals("New") && item.note != null && e.account.equals("bob"); } (:test) function otpauthHotpAndSteam(logger as Test.Logger) as Boolean { var s = OtpAuthFixtures.SECRET; var h = OtpAuthFixtures.entry(logger, "otpauth://hotp/VPN:dave?secret=" + s + "&counter=42"); var noCounter = OtpAuthFixtures.parse("otpauth://hotp/VPN:dave?secret=" + s); var steam = OtpAuthFixtures.entry(logger, "otpauth://steam/Steam:erin?secret=" + s); var encoder = OtpAuthFixtures.entry(logger, "otpauth://totp/Steam:erin?secret=" + s + "&encoder=steam&digits=5"); return h != null && h.type == Entry.OTP_HOTP && h.counter == 42l && noCounter.problem != null && (noCounter.problem as String).find("counter") != null && steam != null && steam.type == Entry.OTP_STEAM && encoder != null && encoder.type == Entry.OTP_STEAM; } (:test) function otpauthReportsEveryProblem(logger as Test.Logger) as Boolean { var s = OtpAuthFixtures.SECRET; var cases = [ ["otpauth://totp/A:b", "secret missing"], ["otpauth://totp/A:b?secret=" + s + "&digits=9", "9 digits"], ["otpauth://totp/A:b?secret=" + s + "&digits=x", "not a whole number"], ["otpauth://totp/A:b?secret=" + s + "&period=0", "period 0"], ["otpauth://totp/A:b?secret=" + s + "&algorithm=MD5", "algorithm"], ["otpauth://totp/A:b?secret=ABC1DEFG", "position 4"], ["otpauth://totp/A%2:b?secret=" + s, "% escape"], ["otpauth://totp/?secret=" + s, "issuer or an account"], ["otpauth://totp", "no '/'"] ]; for (var i = 0; i < cases.size(); i++) { var c = cases[i] as Array; var item = OtpAuthFixtures.parse(c[0]); var problem = item.problem; if (item.entry != null || problem == null || problem.find(c[1]) == null) { logger.error("case " + i + ": problem " + problem); return false; } } var motp = OtpAuthFixtures.parse("otpauth://motp/A:b?secret=" + s); return motp.skipped != null && motp.entry == null; }