f60d6e52fc
worker-pool-policy §B 1B 영역 완료. 1A scaffold (mig 270~274 + 503 stub) 위에:
- mig 275/276: worker_jobs (status CHECK + user_id=owner) + pending partial index
- create_laptop_worker_bot_token + require_worker_user dependency (voice-memo 동형)
- /internal/worker/{register,heartbeat,claim,result,drain} 5 endpoint 실 구현
- /claim FOR UPDATE SKIP LOCKED + 204 body 0
- /result 소유권 검증 (worker_id 매칭, 404) + failed 재시도 (attempts/max)
- explicit failure 시 request.result 무시 (DB result NULL 유지)
- 테스트 22 항목 7 파일
policy §B.2 5 invariant 보존: voice-memo wrapper 변경 0, drain advisory,
result raw JSONB, ProcessingQueue 무변경, 운영 자동 분기 변경 0.
활용처 (recap context + /jobs/recap + payload 100KB guard) = Registry-1C 영역.
stale recovery / 노트북 client / canonical promote = Notebook-Pilot-1 영역.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
"""PR-Worker-Pool-Registry-1B — /internal/worker/* 권한 분리 (정정 #2 보조).
|
|
|
|
worker user 외 모든 사용자 = 403.
|
|
1. voice-memo-bot JWT → 403
|
|
2. 일반 user JWT → 403
|
|
|
|
(401 = token 자체 invalid 시. 본 테스트는 토큰 자체는 유효 + 권한만 부족.)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))
|
|
|
|
from httpx import ASGITransport, AsyncClient
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
from _worker_pool_helpers import ensure_user, get_database_url, mint_access_token
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def setup(monkeypatch):
|
|
monkeypatch.setenv("LAPTOP_WORKER_BOT_USERNAME", "laptop-worker-bot")
|
|
engine = create_async_engine(get_database_url())
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with sm() as session:
|
|
await ensure_user(session, "voice-memo-bot")
|
|
await ensure_user(session, "test-regular-user-1b")
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def client():
|
|
from main import app
|
|
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=app),
|
|
base_url="http://test",
|
|
) as ac:
|
|
yield ac
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_voice_memo_bot_jwt_rejected(client, setup):
|
|
"""voice-memo-bot JWT 로 /internal/worker/register 호출 → 403."""
|
|
token = mint_access_token("voice-memo-bot")
|
|
r = await client.post(
|
|
"/internal/worker/register",
|
|
json={
|
|
"worker_id": "x",
|
|
"device_label": "x",
|
|
"worker_class": "x",
|
|
"tier": "x",
|
|
},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert r.status_code == 403, r.text
|
|
assert "worker user" in r.json().get("detail", "").lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_regular_user_jwt_rejected(client, setup):
|
|
"""일반 user JWT 로 /internal/worker/heartbeat 호출 → 403."""
|
|
token = mint_access_token("test-regular-user-1b")
|
|
r = await client.post(
|
|
"/internal/worker/heartbeat",
|
|
json={"worker_id": "x", "status": "available"},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert r.status_code == 403, r.text
|