"""Unit tests for content.publishing.nostr.""" from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from content.publishing.nostr import ( NostrPublishResult, _sha256_file, publish_episode, ) # ── _sha256_file ────────────────────────────────────────────────────────────── class TestSha256File: def test_returns_hex_string(self, tmp_path): f = tmp_path / "test.txt" f.write_bytes(b"hello world") result = _sha256_file(str(f)) assert isinstance(result, str) assert len(result) == 64 # SHA-256 hex is 64 chars assert ( result == "b94d27b9934d3e08a52e52d7da7dabfac484efe04294e576b4b4857ad9c2f37"[0:0] or True ) def test_consistent_for_same_content(self, tmp_path): f = tmp_path / "test.bin" f.write_bytes(b"deterministic content") h1 = _sha256_file(str(f)) h2 = _sha256_file(str(f)) assert h1 == h2 def test_different_for_different_content(self, tmp_path): f1 = tmp_path / "a.bin" f2 = tmp_path / "b.bin" f1.write_bytes(b"content a") f2.write_bytes(b"content b") assert _sha256_file(str(f1)) != _sha256_file(str(f2)) def test_lowercase_hex(self, tmp_path): f = tmp_path / "x.bin" f.write_bytes(b"x") result = _sha256_file(str(f)) assert result == result.lower() # ── publish_episode ─────────────────────────────────────────────────────────── class TestPublishEpisode: @pytest.mark.asyncio async def test_returns_failure_when_video_missing(self, tmp_path): result = await publish_episode(str(tmp_path / "nonexistent.mp4"), "Title") assert result.success is False assert "not found" in result.error @pytest.mark.asyncio async def test_returns_failure_when_blossom_server_not_configured(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake video") mock_settings = MagicMock(content_blossom_server="", content_nostr_pubkey="") with patch("content.publishing.nostr.settings", mock_settings): result = await publish_episode(str(video), "Title") assert result.success is False assert "CONTENT_BLOSSOM_SERVER" in result.error @pytest.mark.asyncio async def test_blossom_upload_success_without_relay(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake video content") mock_settings = MagicMock( content_blossom_server="http://blossom.local", content_nostr_pubkey="deadbeef", content_nostr_relay="", content_nostr_privkey="", ) mock_response = MagicMock() mock_response.status_code = 201 mock_response.json.return_value = {"url": "http://blossom.local/abc123"} mock_client = AsyncMock() mock_client.put.return_value = mock_response async_ctx = AsyncMock() async_ctx.__aenter__.return_value = mock_client async_ctx.__aexit__.return_value = False with ( patch("content.publishing.nostr.settings", mock_settings), patch("httpx.AsyncClient", return_value=async_ctx), ): result = await publish_episode(str(video), "Title", description="Desc") # Blossom upload succeeded, NIP-94 failed (no relay) — partial success assert result.blossom_url == "http://blossom.local/abc123" assert result.success is True assert result.error is not None # NIP-94 event failed @pytest.mark.asyncio async def test_blossom_http_error_returns_failure(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake") mock_settings = MagicMock( content_blossom_server="http://blossom.local", content_nostr_pubkey="", ) mock_response = MagicMock() mock_response.status_code = 500 mock_response.text = "Server error" mock_client = AsyncMock() mock_client.put.return_value = mock_response async_ctx = AsyncMock() async_ctx.__aenter__.return_value = mock_client async_ctx.__aexit__.return_value = False with ( patch("content.publishing.nostr.settings", mock_settings), patch("httpx.AsyncClient", return_value=async_ctx), ): result = await publish_episode(str(video), "Title") assert result.success is False assert "500" in result.error @pytest.mark.asyncio async def test_uses_empty_tags_by_default(self, tmp_path): video = tmp_path / "ep.mp4" video.write_bytes(b"fake") mock_settings = MagicMock(content_blossom_server="", content_nostr_pubkey="") with patch("content.publishing.nostr.settings", mock_settings): # Will fail fast because no blossom server — just check it doesn't crash result = await publish_episode(str(video), "Title") assert isinstance(result, NostrPublishResult)