import base64 import json import pytest from unittest.mock import patch, MagicMock from cryptography.hazmat.primitives.ciphers.aead import AESGCM from openvpncertupdate import create_note, CryptgeonError def mock_response(body: bytes): resp = MagicMock() resp.read.return_value = body resp.__enter__ = lambda s: s resp.__exit__ = MagicMock(return_value=False) return resp @patch("openvpncertupdate.urllib.request.urlopen") def test_url_uses_path_based_note_route_with_hex_key_fragment(mock_open): mock_open.return_value = mock_response(b'{"id":"abc-123"}') url = create_note("secret", "https://cg.example.com") assert url.startswith("https://cg.example.com/note/abc-123#") fragment = url.split("#", 1)[1] assert len(fragment) == 64 bytes.fromhex(fragment) # must be valid hex, not base64url @patch("openvpncertupdate.urllib.request.urlopen") def test_posts_to_correct_endpoint(mock_open): mock_open.return_value = mock_response(b'{"id":"x"}') create_note("hello", "https://cg.example.com") req = mock_open.call_args[0][0] assert req.full_url == "https://cg.example.com/api/notes/" @patch("openvpncertupdate.urllib.request.urlopen") def test_meta_is_json_string_describing_text_note(mock_open): mock_open.return_value = mock_response(b'{"id":"x"}') create_note("hello", "https://cg.example.com") req = mock_open.call_args[0][0] body = json.loads(req.data) assert json.loads(body["meta"]) == {"type": "text"} assert body["views"] == 1 @patch("openvpncertupdate.urllib.request.urlopen") def test_contents_is_three_part_dash_delimited_base64(mock_open): mock_open.return_value = mock_response(b'{"id":"x"}') create_note("hello", "https://cg.example.com") req = mock_open.call_args[0][0] body = json.loads(req.data) parts = body["contents"].split("--") assert len(parts) == 3 alg, nonce, ciphertext = (base64.b64decode(p) for p in parts) assert alg == b"AES-GCM" assert len(nonce) == 12 @patch("openvpncertupdate.urllib.request.urlopen") def test_raises_on_http_error(mock_open): import urllib.error mock_open.side_effect = urllib.error.HTTPError("", 500, "Error", None, None) with pytest.raises(CryptgeonError, match="500"): create_note("secret", "https://cg.example.com") @patch("openvpncertupdate.urllib.request.urlopen") def test_different_contents_give_different_urls(mock_open): mock_open.return_value = mock_response(b'{"id":"same"}') url1 = create_note("pass1", "https://cg.example.com") url2 = create_note("pass2", "https://cg.example.com") assert url1 != url2 @patch("openvpncertupdate.urllib.request.urlopen") def test_decrypts_like_the_browser_client(mock_open): """Reproduces the note/[id] view: key from the URL fragment must be the exact key used to encrypt (no extra hashing step), or GCM auth fails.""" mock_open.return_value = mock_response(b'{"id":"abc"}') url = create_note("my secret password", "https://cg.example.com") req = mock_open.call_args[0][0] body = json.loads(req.data) key_hex = url.split("#", 1)[1] key = bytes.fromhex(key_hex) alg_b64, nonce_b64, ciphertext_b64 = body["contents"].split("--") nonce = base64.b64decode(nonce_b64) ciphertext = base64.b64decode(ciphertext_b64) plaintext = AESGCM(key).decrypt(nonce, ciphertext, None) assert plaintext == b"my secret password"