147 lines
5.2 KiB
Python
147 lines
5.2 KiB
Python
"""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)
|