From e8dbf8092ac6942aca41271660eb863bfa09e9c3 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 19 May 2026 08:29:05 +0900 Subject: [PATCH] feat(video-lab): /api/internal/video/update endpoint + tests (SP-8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UpdatePayload schema (task_id/status/progress/message/video_url/error). 404 if task not found. insta/music-lab과 동일 패턴 + video_url 필드. Plan-B-Video Phase 1. Co-Authored-By: Claude Opus 4.7 (1M context) --- video-lab/app/internal_router.py | 52 +++++++++++++++ video-lab/tests/test_internal_router.py | 89 +++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 video-lab/app/internal_router.py create mode 100644 video-lab/tests/test_internal_router.py diff --git a/video-lab/app/internal_router.py b/video-lab/app/internal_router.py new file mode 100644 index 0000000..34c481f --- /dev/null +++ b/video-lab/app/internal_router.py @@ -0,0 +1,52 @@ +"""SP-8 — Windows video-render → NAS video-lab internal webhook. + +POST /api/internal/video/update +- X-Internal-Key 인증 필수 +- video_tasks row update (status, progress, message, video_url, error) +""" +from __future__ import annotations + +import logging +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field + +from . import db +from .auth import verify_internal_key + +logger = logging.getLogger(__name__) +router = APIRouter() + + +class UpdatePayload(BaseModel): + task_id: str + status: str = Field(..., description="processing|succeeded|failed") + progress: int = Field(..., ge=0, le=100) + message: str = "" + video_url: Optional[str] = None + error: Optional[str] = None + + +@router.post( + "/api/internal/video/update", + dependencies=[Depends(verify_internal_key)], +) +def video_update(payload: UpdatePayload): + task = db.get_task(payload.task_id) + if task is None: + raise HTTPException(404, f"task not found: {payload.task_id}") + + db.update_task( + payload.task_id, + payload.status, + payload.progress, + message=payload.message, + video_url=payload.video_url, + error=payload.error, + ) + logger.info( + "internal/video/update task=%s status=%s progress=%d", + payload.task_id, payload.status, payload.progress, + ) + return {"ok": True} diff --git a/video-lab/tests/test_internal_router.py b/video-lab/tests/test_internal_router.py new file mode 100644 index 0000000..a329ff6 --- /dev/null +++ b/video-lab/tests/test_internal_router.py @@ -0,0 +1,89 @@ +"""POST /api/internal/video/update — Windows video-render webhook.""" +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from app.internal_router import router +from app import db + + +@pytest.fixture(autouse=True) +def _set_key(monkeypatch): + monkeypatch.setenv("INTERNAL_API_KEY", "test-secret") + + +@pytest.fixture +def client(tmp_path, monkeypatch): + monkeypatch.setenv("VIDEO_DATA_DIR", str(tmp_path)) + monkeypatch.setattr(db, "DB_PATH", str(tmp_path / "test_video.db")) + db.init_db() + app = FastAPI() + app.include_router(router) + return TestClient(app) + + +def _make_task(): + tid = "video-task-1" + db.create_task(tid, "sora", {"prompt": "test"}) + return tid + + +def test_update_with_valid_key_updates_db(client): + tid = _make_task() + r = client.post( + "/api/internal/video/update", + headers={"X-Internal-Key": "test-secret"}, + json={"task_id": tid, "status": "processing", "progress": 30, "message": "downloading"}, + ) + assert r.status_code == 200 + task = db.get_task(tid) + assert task["status"] == "processing" + assert task["progress"] == 30 + assert task["message"] == "downloading" + + +def test_update_with_invalid_key_returns_401(client): + tid = _make_task() + r = client.post( + "/api/internal/video/update", + headers={"X-Internal-Key": "wrong"}, + json={"task_id": tid, "status": "processing", "progress": 30}, + ) + assert r.status_code == 401 + + +def test_update_succeeded_with_video_url(client): + tid = _make_task() + r = client.post( + "/api/internal/video/update", + headers={"X-Internal-Key": "test-secret"}, + json={ + "task_id": tid, "status": "succeeded", "progress": 100, + "message": "완료", "video_url": "/media/video/video-task-1.mp4", + }, + ) + assert r.status_code == 200 + task = db.get_task(tid) + assert task["status"] == "succeeded" + assert task["video_url"] == "/media/video/video-task-1.mp4" + + +def test_update_failed_records_error(client): + tid = _make_task() + r = client.post( + "/api/internal/video/update", + headers={"X-Internal-Key": "test-secret"}, + json={"task_id": tid, "status": "failed", "progress": 0, "error": "Sora API rate limit"}, + ) + assert r.status_code == 200 + task = db.get_task(tid) + assert task["status"] == "failed" + assert "Sora" in (task.get("error") or "") + + +def test_update_unknown_task_returns_404(client): + r = client.post( + "/api/internal/video/update", + headers={"X-Internal-Key": "test-secret"}, + json={"task_id": "nonexistent", "status": "processing", "progress": 10}, + ) + assert r.status_code == 404