diff options
Diffstat (limited to 'scripts/google-keep/test_keep_bridge.py')
| -rw-r--r-- | scripts/google-keep/test_keep_bridge.py | 152 |
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() |
