forked from LiveCarta/ContentAutomation
209 lines
6.1 KiB
Python
209 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
from content_automation.adapters.social.youtube import (
|
|
YouTubeAdapter,
|
|
YouTubeDataApiClient,
|
|
YouTubeSnippet,
|
|
YouTubeStatus,
|
|
YouTubeVideoInsertPayload,
|
|
)
|
|
|
|
|
|
class FakeInsertRequest:
|
|
def __init__(self, response: dict[str, object]) -> None:
|
|
self._response = response
|
|
|
|
def execute(self) -> dict[str, object]:
|
|
return self._response
|
|
|
|
|
|
class FakeResumableRequest:
|
|
def __init__(self) -> None:
|
|
self._calls = 0
|
|
|
|
def next_chunk(self):
|
|
self._calls += 1
|
|
if self._calls == 1:
|
|
return object(), None
|
|
return None, {"id": "video-123"}
|
|
|
|
|
|
class FakeVideosResource:
|
|
def __init__(self, request) -> None:
|
|
self._request = request
|
|
|
|
def insert(self, part: str, body: dict, media_body=None):
|
|
return self._request
|
|
|
|
|
|
class FakeService:
|
|
def __init__(self, request) -> None:
|
|
self._videos = FakeVideosResource(request)
|
|
|
|
def videos(self) -> FakeVideosResource:
|
|
return self._videos
|
|
|
|
|
|
def test_resumable_upload_happy_path(monkeypatch, tmp_path: Path) -> None:
|
|
media_file = tmp_path / "clip.mp4"
|
|
media_file.write_bytes(b"abcdef")
|
|
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.build",
|
|
lambda *args, **kwargs: FakeService(FakeResumableRequest()),
|
|
)
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.MediaFileUpload",
|
|
lambda *args, **kwargs: object(),
|
|
)
|
|
|
|
adapter = YouTubeAdapter(
|
|
access_token="token",
|
|
use_resumable_upload=True,
|
|
resumable_chunk_size=3,
|
|
)
|
|
|
|
post_id = adapter.post_media(media_url=media_file.as_uri(), caption="caption")
|
|
|
|
assert post_id == "video-123"
|
|
|
|
|
|
def test_regular_upload_happy_path(monkeypatch, tmp_path: Path) -> None:
|
|
media_file = tmp_path / "clip.mp4"
|
|
media_file.write_bytes(b"abcdef")
|
|
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.build",
|
|
lambda *args, **kwargs: FakeService(
|
|
FakeInsertRequest({"id": "video-regular-123"})
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.MediaFileUpload",
|
|
lambda *args, **kwargs: object(),
|
|
)
|
|
|
|
adapter = YouTubeAdapter(
|
|
access_token="token",
|
|
use_resumable_upload=False,
|
|
)
|
|
|
|
post_id = adapter.post_media(media_url=media_file.as_uri(), caption="caption")
|
|
|
|
assert post_id == "video-regular-123"
|
|
|
|
|
|
def test_insert_video_happy_path_for_non_local_url(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.build",
|
|
lambda *args, **kwargs: FakeService(
|
|
FakeInsertRequest({"id": "video-insert-123"})
|
|
),
|
|
)
|
|
|
|
adapter = YouTubeAdapter(access_token="token")
|
|
post_id = adapter.post_media(
|
|
media_url="https://cdn.example.com/path/to/video.mp4", caption="caption"
|
|
)
|
|
|
|
assert post_id == "video-insert-123"
|
|
|
|
|
|
def test_client_refreshes_expired_token_before_request(monkeypatch) -> None:
|
|
refreshed_tokens: list[str] = []
|
|
|
|
def fake_refresh(self, request) -> None:
|
|
self.token = "new-token"
|
|
self.expiry = datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=30)
|
|
refreshed_tokens.append(self.token)
|
|
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.Credentials.refresh",
|
|
fake_refresh,
|
|
)
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.build",
|
|
lambda *args, **kwargs: FakeService(
|
|
FakeInsertRequest({"id": "video-refreshed"})
|
|
),
|
|
)
|
|
|
|
client = YouTubeDataApiClient(
|
|
access_token="expired-token",
|
|
category_id="22",
|
|
privacy_status="public",
|
|
refresh_token="refresh-token",
|
|
client_id="client-id",
|
|
client_secret="client-secret",
|
|
expiry="2024-01-01T00:00:00Z",
|
|
)
|
|
|
|
payload = YouTubeVideoInsertPayload(
|
|
snippet=YouTubeSnippet(
|
|
title="title",
|
|
description="description",
|
|
categoryId="22",
|
|
),
|
|
status=YouTubeStatus(privacyStatus="public"),
|
|
sourceUrl="https://cdn.example.com/video.mp4",
|
|
)
|
|
response = client.insert_video(part="snippet,status", payload=payload)
|
|
|
|
assert response["id"] == "video-refreshed"
|
|
assert refreshed_tokens == ["new-token"]
|
|
|
|
|
|
def test_obtain_credentials_from_client_secret_file(
|
|
monkeypatch, tmp_path: Path
|
|
) -> None:
|
|
captured: dict[str, object] = {}
|
|
|
|
class FakeCredentials:
|
|
def to_json(self) -> str:
|
|
return json.dumps(
|
|
{
|
|
"token": "token-123",
|
|
"refresh_token": "refresh-123",
|
|
"token_uri": "https://oauth2.googleapis.com/token",
|
|
"client_id": "client-123",
|
|
"client_secret": "secret-123",
|
|
"scopes": ["https://www.googleapis.com/auth/youtube.upload"],
|
|
}
|
|
)
|
|
|
|
class FakeFlow:
|
|
def run_local_server(self):
|
|
return FakeCredentials()
|
|
|
|
def fake_from_client_secrets_file(client_secret_file: str, scopes: list[str]):
|
|
captured["client_secret_file"] = client_secret_file
|
|
captured["scopes"] = scopes
|
|
return FakeFlow()
|
|
|
|
monkeypatch.setattr(
|
|
"content_automation.adapters.social.youtube.InstalledAppFlow.from_client_secrets_file",
|
|
fake_from_client_secrets_file,
|
|
)
|
|
|
|
client_secret_path = tmp_path / "client_secret.json"
|
|
token_output_path = tmp_path / "youtube_credentials.json"
|
|
|
|
credentials_payload = YouTubeAdapter.obtain_credentials_from_client_secret_file(
|
|
client_secret_file_path=client_secret_path,
|
|
scopes=["https://www.googleapis.com/auth/youtube.upload"],
|
|
token_output_path=token_output_path,
|
|
)
|
|
|
|
assert captured["client_secret_file"] == str(client_secret_path)
|
|
assert captured["scopes"] == ["https://www.googleapis.com/auth/youtube.upload"]
|
|
assert credentials_payload["token"] == "token-123"
|
|
assert token_output_path.exists()
|
|
assert (
|
|
json.loads(token_output_path.read_text(encoding="utf-8"))["token"]
|
|
== "token-123"
|
|
)
|