aboutsummaryrefslogtreecommitdiff
path: root/scripts/google-keep/test_keep_bridge.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/google-keep/test_keep_bridge.py')
-rw-r--r--scripts/google-keep/test_keep_bridge.py152
1 files changed, 152 insertions, 0 deletions
diff --git a/scripts/google-keep/test_keep_bridge.py b/scripts/google-keep/test_keep_bridge.py
new file mode 100644
index 000000000..a24132744
--- /dev/null
+++ b/scripts/google-keep/test_keep_bridge.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+"""Tests for keep-bridge's pure shaping helpers + its failure degradation.
+
+The gkeepapi auth/fetch path is the IO boundary and is exercised live once the
+token is configured; here we test the JSON-shaping logic (the round-trip
+contract the elisp side reads) with duck-typed stand-ins, plus a subprocess
+smoke test that the script degrades with a reason token rather than crashing.
+
+Run: python3 -m unittest test_keep_bridge (from scripts/google-keep/)
+"""
+
+import importlib.util
+import os
+import subprocess
+import sys
+import unittest
+from datetime import datetime, timezone, timedelta
+
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_BRIDGE = os.path.join(_HERE, "keep-bridge.py")
+
+_spec = importlib.util.spec_from_file_location("keep_bridge", _BRIDGE)
+assert _spec and _spec.loader
+kb = importlib.util.module_from_spec(_spec)
+_spec.loader.exec_module(kb)
+
+
+# --- duck-typed stand-ins for a gkeepapi note ---------------------------------
+
+class FakeLabel:
+ def __init__(self, name):
+ self.name = name
+
+
+class FakeLabels:
+ def __init__(self, names):
+ self._labels = [FakeLabel(n) for n in names]
+
+ def all(self):
+ return self._labels
+
+
+class FakeTimestamps:
+ def __init__(self, updated):
+ self.updated = updated
+
+
+class FakeColor:
+ def __init__(self, value):
+ self.value = value
+
+
+class FakeNote:
+ def __init__(self, id="n1", title: object = "T", text: object = "B", labels=(),
+ pinned=False, archived=False, color: object = "WHITE", updated=None):
+ self.id = id
+ self.title = title
+ self.text = text
+ self.labels = FakeLabels(labels)
+ self.pinned = pinned
+ self.archived = archived
+ self.color = color
+ self.timestamps = FakeTimestamps(updated)
+
+
+class TestIso8601Utc(unittest.TestCase):
+ def test_normal_naive_datetime_treated_as_utc(self):
+ self.assertEqual(kb.iso8601_utc(datetime(2026, 6, 25, 4, 12, 0)),
+ "2026-06-25T04:12:00Z")
+
+ def test_normal_aware_datetime_converted_to_utc(self):
+ est = timezone(timedelta(hours=-5))
+ self.assertEqual(kb.iso8601_utc(datetime(2026, 6, 24, 23, 12, 0, tzinfo=est)),
+ "2026-06-25T04:12:00Z")
+
+ def test_boundary_none_returns_none(self):
+ self.assertIsNone(kb.iso8601_utc(None))
+
+
+class TestColorName(unittest.TestCase):
+ def test_normal_enum_with_value(self):
+ self.assertEqual(kb.color_name(FakeColor("RED")), "RED")
+
+ def test_normal_plain_string(self):
+ self.assertEqual(kb.color_name("WHITE"), "WHITE")
+
+ def test_boundary_name_only_object(self):
+ class C:
+ name = "BLUE"
+ self.assertEqual(kb.color_name(C()), "BLUE")
+
+
+class TestNoteToDict(unittest.TestCase):
+ def test_normal_full_note(self):
+ note = FakeNote(id="abc", title="Groceries", text="milk\neggs",
+ labels=("shopping", "home"), pinned=True, archived=False,
+ color=FakeColor("YELLOW"),
+ updated=datetime(2026, 6, 25, 4, 0, 0, tzinfo=timezone.utc))
+ self.assertEqual(kb.note_to_dict(note), {
+ "id": "abc",
+ "title": "Groceries",
+ "text": "milk\neggs",
+ "labels": ["shopping", "home"],
+ "pinned": True,
+ "archived": False,
+ "color": "YELLOW",
+ "updated": "2026-06-25T04:00:00Z",
+ })
+
+ def test_boundary_empty_title_and_no_labels(self):
+ note = FakeNote(title="", labels=(), color="WHITE",
+ updated=datetime(2026, 1, 1, tzinfo=timezone.utc))
+ d = kb.note_to_dict(note)
+ self.assertEqual(d["title"], "")
+ self.assertEqual(d["labels"], [])
+
+ def test_boundary_none_title_text_coerced_to_empty(self):
+ note = FakeNote(title=None, text=None, color="WHITE",
+ updated=datetime(2026, 1, 1, tzinfo=timezone.utc))
+ d = kb.note_to_dict(note)
+ self.assertEqual(d["title"], "")
+ self.assertEqual(d["text"], "")
+
+
+class TestNotesToJson(unittest.TestCase):
+ def test_normal_array_of_notes(self):
+ import json
+ notes = [FakeNote(id="a", updated=datetime(2026, 1, 1, tzinfo=timezone.utc)),
+ FakeNote(id="b", updated=datetime(2026, 1, 2, tzinfo=timezone.utc))]
+ parsed = json.loads(kb.notes_to_json(notes))
+ self.assertEqual([n["id"] for n in parsed], ["a", "b"])
+
+ def test_boundary_empty_keep_is_empty_array(self):
+ self.assertEqual(kb.notes_to_json([]), "[]")
+
+
+class TestDegradation(unittest.TestCase):
+ def test_error_no_env_exits_nonzero_with_reason_token(self):
+ # With no KEEP_EMAIL/KEEP_MASTER_TOKEN the script must exit non-zero
+ # with a single reason token, never crash. The exact token depends on
+ # whether gkeepapi is installed in this environment.
+ env = {k: v for k, v in os.environ.items()
+ if k not in ("KEEP_EMAIL", "KEEP_MASTER_TOKEN")}
+ proc = subprocess.run([sys.executable, _BRIDGE], env=env,
+ capture_output=True, text=True)
+ self.assertNotEqual(proc.returncode, 0)
+ self.assertIn(proc.stderr.strip(), ("no-gkeepapi", "no-token"))
+ self.assertEqual(proc.stdout, "")
+
+
+if __name__ == "__main__":
+ unittest.main()