"""Hardware-free tests for the QR->CSV utility (otpauth parsing + dedup).""" import os import tempfile import unittest from otpauth import parse_otpauth, canonical_key from qr_to_sheet import (existing_keys, dedup_new, append_csv, read_csv_rows, apply_new_candidates) class TestParseOtpauth(unittest.TestCase): def test_basic_totp(self): o = parse_otpauth("otpauth://totp/Google:amal@amal.net?secret=abcde&issuer=Google") self.assertEqual(o.type, "totp") self.assertEqual(o.issuer, "Google") self.assertEqual(o.account, "amal@amal.net") self.assertEqual(o.secret, "ABCDE") # uppercased self.assertEqual((o.algorithm, o.digits, o.period), ("SHA1", 6, 30)) def test_percent_decoding(self): o = parse_otpauth("otpauth://totp/Google%3Aamal%40amal.net?secret=AA&issuer=Google") self.assertEqual(o.account, "amal@amal.net") def test_issuer_from_label_when_no_param(self): o = parse_otpauth("otpauth://totp/SAW:michelle@d.com?secret=AA") self.assertEqual(o.issuer, "SAW") self.assertEqual(o.account, "michelle@d.com") def test_label_without_issuer_prefix(self): o = parse_otpauth("otpauth://totp/amalg?secret=AA&issuer=Porkbun") self.assertEqual(o.issuer, "Porkbun") self.assertEqual(o.account, "amalg") def test_explicit_params(self): o = parse_otpauth("otpauth://totp/X:y?secret=AA&algorithm=SHA256&digits=8&period=60") self.assertEqual((o.algorithm, o.digits, o.period), ("SHA256", 8, 60)) class TestCanonicalKey(unittest.TestCase): def test_label_issuer_equals_param_issuer(self): a = parse_otpauth("otpauth://totp/Google:amal@amal.net?secret=JBSWY3DP&issuer=Google") b = parse_otpauth("otpauth://totp/amal@amal.net?secret=JBSWY3DP&issuer=Google") self.assertEqual(canonical_key(a), canonical_key(b)) def test_secret_case_insensitive(self): a = parse_otpauth("otpauth://totp/X:y?secret=jbswy3dp&issuer=X") b = parse_otpauth("otpauth://totp/X:y?secret=JBSWY3DP&issuer=X") self.assertEqual(canonical_key(a), canonical_key(b)) def test_different_secret_differs(self): a = parse_otpauth("otpauth://totp/X:y?secret=AAAA&issuer=X") b = parse_otpauth("otpauth://totp/X:y?secret=BBBB&issuer=X") self.assertNotEqual(canonical_key(a), canonical_key(b)) A = "otpauth://totp/X:a?secret=AAAA&issuer=X" B = "otpauth://totp/X:b?secret=BBBB&issuer=X" class TestDedup(unittest.TestCase): def test_dedups_against_existing_and_within_batch(self): existing = [{"issuer": "X", "account": "a", "otpauth_uri": A, "source_file": "a.png"}] keys = existing_keys(existing) new_rows, dups = dedup_new([(A, "a2.png"), (B, "b.png"), (B, "b2.png")], keys) self.assertEqual([r["otpauth_uri"] for r in new_rows], [B]) self.assertEqual(new_rows[0]["issuer"], "X") self.assertEqual(new_rows[0]["account"], "b") self.assertEqual(dups, 2) class TestCsvIO(unittest.TestCase): def test_append_creates_then_appends_preserving_rows(self): d = tempfile.mkdtemp() p = os.path.join(d, "otp-keys.csv") append_csv(p, [{"issuer": "X", "account": "a", "otpauth_uri": "u1", "source_file": "a.png"}]) append_csv(p, [{"issuer": "Y", "account": "b", "otpauth_uri": "u2", "source_file": "b.png"}]) rows = read_csv_rows(p) self.assertEqual(len(rows), 2) self.assertEqual(rows[0]["issuer"], "X") self.assertEqual(rows[1]["account"], "b") self.assertEqual(rows[1]["otpauth_uri"], "u2") def test_read_missing_file_is_empty(self): self.assertEqual(read_csv_rows(os.path.join(tempfile.mkdtemp(), "nope.csv")), []) class TestApplyNewCandidates(unittest.TestCase): def _csv_with_A(self): p = os.path.join(tempfile.mkdtemp(), "otp-keys.csv") append_csv(p, [{"issuer": "X", "account": "a", "otpauth_uri": A, "source_file": "a.png"}]) return p def test_only_new_otpauth_is_appended(self): p = self._csv_with_A() summary = apply_new_candidates([(A, "a2.png"), (B, "b.png"), (B, "b2.png")], p) self.assertEqual(summary["added"], 1) self.assertEqual(summary["duplicates"], 2) rows = read_csv_rows(p) self.assertEqual(len(rows), 2) # existing A + new B only self.assertEqual(rows[1]["otpauth_uri"], B) def test_all_existing_appends_nothing(self): p = self._csv_with_A() summary = apply_new_candidates([(A, "a2.png")], p) self.assertEqual((summary["added"], summary["duplicates"]), (0, 1)) self.assertEqual(len(read_csv_rows(p)), 1) # unchanged def test_dry_run_reports_but_does_not_write(self): p = self._csv_with_A() summary = apply_new_candidates([(B, "b.png")], p, dry_run=True) self.assertEqual(summary["added"], 1) self.assertEqual(len(read_csv_rows(p)), 1) # not written if __name__ == "__main__": unittest.main()