Image rights / copyright detection system: SQLite store, HTTP app, search integrations (Naver, Google Custom Search, Google Cloud Vision web detection), image analysis (fingerprints, face/person detection, evidence enrichment, risk scoring), an admin/review layer, governance and retention policies, batch jobs, and a browser-based operator GUI. This baseline incorporates a full code-review remediation pass (46 fixes; 358 tests passing). Highlights: CRITICAL - Prevent evidence cascade-delete during the schema-constraint migration by disabling FK enforcement around the table rebuild. Security - Sandbox served media (neutralize stored XSS from uploaded/collected SVGs) via CSP + nosniff on the untrusted media routes. - Strip embedded EXIF/GPS from external image derivatives before they are sent to third-party APIs. - Return a clean 404 (not an uncaught StopIteration) for PATCH on an unknown provider. Correctness - LLM-summary failures no longer add +30 to the risk score. - Decode only explicit JS escapes so Korean image URLs are not mangled. - Consume search quota only after a successful request. - Naver/Google adapters map responses inside the failure boundary, so a malformed response degrades to evidence instead of crashing enrichment. - Domain-aware provider attribution; face-box IoU de-duplication; count searches (not result items); per-box crop isolation; clamp evidence confidence and Google CSE num; real submittedEpoch; and more. Robustness - Offline LLM connect fast-fails (short connect timeout) so seed/reload requests are not stalled; full read timeout preserved for generation. - Malformed numeric env vars fall back to defaults instead of crashing startup. Performance - Per-submission evidence reads (no full-table scan per rescore), audit-log LIMIT, lazy active-store lookup, hoisted timestamps. Tests - ~24 regression tests added pinning the above fixes. Runtime data (data/, outputs/, *.sqlite3, *.log), secrets (.env), and node_modules are gitignored.
191 lines
8 KiB
Python
191 lines
8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
from threading import Thread
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
APP_DIR = ROOT / "web" / "operator-gui"
|
|
|
|
from rights_filter.server.http_app import build_server
|
|
from rights_filter.server.image_store import LocalSubmissionImageStore
|
|
from rights_filter.server.sqlite_store import CopyrighterStore
|
|
|
|
|
|
def _start(server):
|
|
thread = Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
return thread
|
|
|
|
|
|
def _browser_or_skip(playwright):
|
|
try:
|
|
return playwright.chromium.launch(headless=True)
|
|
except Exception as exc:
|
|
pytest.skip(f"Playwright Chromium is not available: {exc}")
|
|
|
|
|
|
def _bootstrap_payload():
|
|
return {
|
|
"submissions": [
|
|
{
|
|
"id": "SUB-SMOKE1",
|
|
"title": "Smoke sample",
|
|
"asset": "/assets/case-portrait.svg",
|
|
"riskScore": 42,
|
|
"riskBand": "medium",
|
|
"submittedAt": "2026-06-03 10:00",
|
|
"submittedEpoch": 1780452000,
|
|
"lastAnalysis": "2026-06-03 10:01",
|
|
"applicantStatus": "검토 중",
|
|
"decisionStatus": "unreviewed",
|
|
"reasons": ["Naver search returned no results"],
|
|
"providerState": {"internal": "ok", "naver": "empty", "google": "disabled", "llm": "pending"},
|
|
"fileFacts": {"size": "320 x 240", "format": "SVG", "submitted": "2026-06-03 10:00", "analysis": "v1"},
|
|
"derivativeNote": "브라우저 smoke test submission",
|
|
"recommendation": {"label": "운영자 검토 필요", "detail": "검색 근거가 부족합니다."},
|
|
"derivedPreview": {"automatic": False, "entryName": "Smoke sample", "effect": "보강 검색 필요"},
|
|
"queryHistory": [
|
|
{
|
|
"provider": "naver",
|
|
"query": "Smoke sample official",
|
|
"status": "empty",
|
|
"timestamp": "2026-06-03 10:01",
|
|
"count": 0,
|
|
}
|
|
],
|
|
"similar": [{"asset": "/assets/case-portrait.svg", "label": "local submission"}],
|
|
"evidence": [
|
|
{
|
|
"id": "ev-smoke-empty",
|
|
"group": "naver",
|
|
"source": "naver",
|
|
"title": "Naver search returned no results",
|
|
"confidence": 0,
|
|
"query": "Smoke sample official",
|
|
"domain": "naver",
|
|
"url": "",
|
|
"imageUrl": "",
|
|
"thumbnailUrl": "",
|
|
"pageTitle": "",
|
|
"matchType": "empty",
|
|
"rank": "",
|
|
"providerScore": 0,
|
|
"retrievedAt": "2026-06-03 10:01",
|
|
"contributed": False,
|
|
"sourceEvidenceIds": [],
|
|
"status": "active",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"submissionQueue": {"label": "smoke", "folderPath": "smoke", "isActive": True},
|
|
"providers": [{"id": "naver", "name": "Naver", "enabled": True, "quota": 100, "usage": 0, "status": "ok"}],
|
|
"knowledgeEntries": [],
|
|
"collectionCandidates": [],
|
|
"corrections": [],
|
|
"auditEvents": [],
|
|
"coverageThresholds": {"coverageGoodRate": 70, "coverageWarnRate": 40, "queryGoodRate": 70, "queryWarnRate": 40},
|
|
"searchCoverage": {
|
|
"submissions": {"total": 1, "coverageSubmissions": 0},
|
|
"queries": {"failed": 0},
|
|
"providers": [{"id": "naver", "name": "Naver", "queryEntries": 1, "evidenceSubmissions": 0}],
|
|
},
|
|
}
|
|
|
|
|
|
def test_browser_smoke_suggested_query_fills_manual_query_without_running_search(tmp_path: Path):
|
|
playwright = pytest.importorskip("playwright.sync_api")
|
|
image_root = tmp_path / "submissions"
|
|
image_root.mkdir()
|
|
store = CopyrighterStore(tmp_path / "copyrighter.sqlite3")
|
|
store.initialize()
|
|
server = build_server(
|
|
host="127.0.0.1",
|
|
port=0,
|
|
store=store,
|
|
image_store=LocalSubmissionImageStore(image_root),
|
|
static_dir=APP_DIR,
|
|
)
|
|
_start(server)
|
|
base = f"http://127.0.0.1:{server.server_port}"
|
|
|
|
try:
|
|
with playwright.sync_playwright() as pw:
|
|
browser = _browser_or_skip(pw)
|
|
page = browser.new_page(viewport={"width": 1280, "height": 900})
|
|
browser_errors = []
|
|
page.on("console", lambda message: browser_errors.append(f"console:{message.type}:{message.text}"))
|
|
page.on("pageerror", lambda error: browser_errors.append(f"pageerror:{error}"))
|
|
page.route(
|
|
"**/api/bootstrap",
|
|
lambda route: route.fulfill(
|
|
status=200,
|
|
content_type="application/json",
|
|
body=json.dumps(_bootstrap_payload(), ensure_ascii=False),
|
|
),
|
|
)
|
|
page.goto(base + "/")
|
|
try:
|
|
page.wait_for_selector('#queue-body [data-select-case="SUB-SMOKE1"]', timeout=5000)
|
|
except Exception as exc:
|
|
pytest.fail(f"{exc}\nerrors={browser_errors}\nbody={page.locator('body').inner_text()[:1000]}")
|
|
page.get_by_role("button", name="SUB-SMOKE1").click()
|
|
page.get_by_role("button", name="Smoke sample 저작권").click()
|
|
|
|
assert page.locator('[data-workbench-panel="queries"]').is_visible()
|
|
assert page.locator("#manual-query").input_value() == "Smoke sample 저작권"
|
|
assert page.locator("#manual-query-status").inner_text() == "추천 쿼리를 입력했습니다. 실행 버튼을 눌러 검색하세요.", browser_errors
|
|
|
|
browser.close()
|
|
finally:
|
|
server.shutdown()
|
|
|
|
|
|
def test_browser_uploads_image_and_selects_new_submission(tmp_path: Path):
|
|
playwright = pytest.importorskip("playwright.sync_api")
|
|
image_root = tmp_path / "submissions"
|
|
image_root.mkdir()
|
|
upload_file = tmp_path / "smoke upload.svg"
|
|
upload_file.write_text("<svg xmlns='http://www.w3.org/2000/svg' width='80' height='60'></svg>", encoding="utf-8")
|
|
store = CopyrighterStore(tmp_path / "copyrighter.sqlite3")
|
|
store.initialize()
|
|
server = build_server(
|
|
host="127.0.0.1",
|
|
port=0,
|
|
store=store,
|
|
image_store=LocalSubmissionImageStore(image_root),
|
|
static_dir=APP_DIR,
|
|
)
|
|
_start(server)
|
|
base = f"http://127.0.0.1:{server.server_port}"
|
|
|
|
try:
|
|
with playwright.sync_playwright() as pw:
|
|
browser = _browser_or_skip(pw)
|
|
page = browser.new_page(viewport={"width": 1280, "height": 900})
|
|
browser_errors = []
|
|
page.on("console", lambda message: browser_errors.append(f"console:{message.type}:{message.text}"))
|
|
page.on("pageerror", lambda error: browser_errors.append(f"pageerror:{error}"))
|
|
|
|
page.goto(base + "/")
|
|
page.set_input_files("#submission-image", str(upload_file))
|
|
assert page.locator("#submission-image-name").inner_text() == "smoke upload.svg"
|
|
|
|
page.get_by_role("button", name="사진 넣기").click()
|
|
page.wait_for_selector('#queue-body [data-select-case="smoke-upload"]', state="attached", timeout=10000)
|
|
page.wait_for_selector("#workbench-view", state="visible", timeout=10000)
|
|
|
|
assert (image_root / "images" / "smoke-upload.svg").exists()
|
|
assert "smoke-upload 사진이 추가되었습니다. 새 심사 건으로 바로 선택했습니다." in page.locator("#submission-import-status").inner_text()
|
|
assert page.locator("#case-title").inner_text() == "smoke-upload · smoke-upload"
|
|
assert not browser_errors
|
|
|
|
browser.close()
|
|
finally:
|
|
server.shutdown()
|