API Key 인증 완전 제거
사내 운영 전제로 X-API-Key 인증을 전면 제거. - app/core/auth.py 삭제 - routes.py에서 Depends(require_api_key) 모두 제거 - config.Settings에서 api_keys / api_key_set 제거 - .env / .env.example에서 API_KEYS 제거 - docker-compose.yml에서 API_KEYS 환경변수 제거 (KOSIMCSE_MODEL 추가) - UI(index.html)에서 DEFAULT_API_KEY 상수 + X-API-Key 헤더 모두 제거 - scripts/sample_curl.sh, sample_python.py에서 키 헤더 제거 - tests: test_detect_requires_api_key → test_detect_no_auth_required로 갱신 - README: 인증 컬럼 제거, curl 예시에서 헤더 제거main
parent
f31ef142e8
commit
4913bf3ecc
|
|
@ -4,7 +4,6 @@ PORT=8000
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
RELOAD=false
|
RELOAD=false
|
||||||
|
|
||||||
API_KEYS=combooks-key-change-me,baikal-key-change-me
|
|
||||||
ENGINE_VERSION=o2o-plagiarism-2.1.0-kosimcse
|
ENGINE_VERSION=o2o-plagiarism-2.1.0-kosimcse
|
||||||
REFERENCE_CORPUS_DIR=./data/reference
|
REFERENCE_CORPUS_DIR=./data/reference
|
||||||
TAXONOMY_DIR=./data/taxonomy
|
TAXONOMY_DIR=./data/taxonomy
|
||||||
|
|
|
||||||
34
README.md
34
README.md
|
|
@ -74,7 +74,6 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
# 탐지
|
# 탐지
|
||||||
curl -X POST http://localhost:8000/v1/plagiarism/detect \
|
curl -X POST http://localhost:8000/v1/plagiarism/detect \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "X-API-Key: combooks-key-change-me" \
|
|
||||||
-d '{
|
-d '{
|
||||||
"doc_id": "test-001",
|
"doc_id": "test-001",
|
||||||
"text": "검사할 본문 텍스트…",
|
"text": "검사할 본문 텍스트…",
|
||||||
|
|
@ -84,17 +83,15 @@ curl -X POST http://localhost:8000/v1/plagiarism/detect \
|
||||||
# 자서전 업로드
|
# 자서전 업로드
|
||||||
curl -X POST http://localhost:8000/v1/corpus \
|
curl -X POST http://localhost:8000/v1/corpus \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "X-API-Key: combooks-key-change-me" \
|
|
||||||
-d '{"title": "김OO 자서전", "text": "본문…"}'
|
-d '{"title": "김OO 자서전", "text": "본문…"}'
|
||||||
|
|
||||||
# .txt 파일 업로드
|
# .txt 파일 업로드
|
||||||
curl -X POST http://localhost:8000/v1/corpus/file \
|
curl -X POST http://localhost:8000/v1/corpus/file \
|
||||||
-H "X-API-Key: combooks-key-change-me" \
|
|
||||||
-F "title=김OO 자서전" \
|
-F "title=김OO 자서전" \
|
||||||
-F "file=@autobiography.txt"
|
-F "file=@autobiography.txt"
|
||||||
|
|
||||||
# 코퍼스 목록
|
# 코퍼스 목록
|
||||||
curl -H "X-API-Key: combooks-key-change-me" http://localhost:8000/v1/corpus
|
curl http://localhost:8000/v1/corpus
|
||||||
|
|
||||||
# 분류체계 조회 (10종 태그 + 39 케이스)
|
# 분류체계 조회 (10종 태그 + 39 케이스)
|
||||||
curl http://localhost:8000/v1/taxonomy
|
curl http://localhost:8000/v1/taxonomy
|
||||||
|
|
@ -102,21 +99,19 @@ curl http://localhost:8000/v1/taxonomy
|
||||||
|
|
||||||
## 엔드포인트 요약
|
## 엔드포인트 요약
|
||||||
|
|
||||||
| Method | Path | 용도 | 인증 |
|
| Method | Path | 용도 |
|
||||||
|---|---|---|---|
|
|---|---|---|
|
||||||
| GET | `/` | 검토 콘솔 (HTML) | - |
|
| GET | `/` | 검토 콘솔 (HTML) |
|
||||||
| GET | `/docs` | Swagger UI | - |
|
| GET | `/docs` | Swagger UI |
|
||||||
| GET | `/v1/health` | 헬스체크 + 엔진 정보 | - |
|
| GET | `/v1/health` | 헬스체크 + 엔진 정보 |
|
||||||
| GET | `/v1/taxonomy` | 분류체계 조회 | - |
|
| GET | `/v1/taxonomy` | 분류체계 조회 |
|
||||||
| POST | `/v1/plagiarism/detect` | 단건 동기 탐지 | ✅ |
|
| POST | `/v1/plagiarism/detect` | 단건 동기 탐지 |
|
||||||
| POST | `/v1/plagiarism/batch` | 배치 잡 등록 (≤500건) | ✅ |
|
| POST | `/v1/plagiarism/batch` | 배치 잡 등록 (≤500건) |
|
||||||
| GET | `/v1/plagiarism/batch/{job_id}` | 배치 결과 조회 | ✅ |
|
| GET | `/v1/plagiarism/batch/{job_id}` | 배치 결과 조회 |
|
||||||
| GET | `/v1/corpus` | 코퍼스 목록 | ✅ |
|
| GET | `/v1/corpus` | 코퍼스 목록 |
|
||||||
| POST | `/v1/corpus` | JSON 업로드 | ✅ |
|
| POST | `/v1/corpus` | JSON 업로드 |
|
||||||
| POST | `/v1/corpus/file` | .txt 파일 업로드 | ✅ |
|
| POST | `/v1/corpus/file` | .txt 파일 업로드 |
|
||||||
| DELETE | `/v1/corpus/{doc_id}` | 삭제 | ✅ |
|
| DELETE | `/v1/corpus/{doc_id}` | 삭제 |
|
||||||
|
|
||||||
인증: `X-API-Key` 헤더에 발급된 키 (기관별 분리 발급 가능, 콤마 구분).
|
|
||||||
|
|
||||||
## 응답 스키마 (탐지 결과)
|
## 응답 스키마 (탐지 결과)
|
||||||
|
|
||||||
|
|
@ -159,7 +154,6 @@ curl http://localhost:8000/v1/taxonomy
|
||||||
|
|
||||||
| 변수 | 기본 | 설명 |
|
| 변수 | 기본 | 설명 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `API_KEYS` | `combooks-key-…,baikal-key-…` | 콤마 구분 다중 키 |
|
|
||||||
| `SIMILARITY_THRESHOLD` | `0.85` | 침해 판정 임계값 (정밀도 우선) |
|
| `SIMILARITY_THRESHOLD` | `0.85` | 침해 판정 임계값 (정밀도 우선) |
|
||||||
| `AUTOBIOGRAPHY_MODE` | `true` | 자서전 특화 전처리 (공통표현+NER) |
|
| `AUTOBIOGRAPHY_MODE` | `true` | 자서전 특화 전처리 (공통표현+NER) |
|
||||||
| `USE_LSH_FILTER` | `true` | MinHash+LSH 1차 필터 |
|
| `USE_LSH_FILTER` | `true` | MinHash+LSH 1차 필터 |
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Request, UploadFile, status
|
from fastapi import APIRouter, BackgroundTasks, File, Form, HTTPException, Request, UploadFile, status
|
||||||
|
|
||||||
from app.api.schemas import (
|
from app.api.schemas import (
|
||||||
BatchCreatedResponse,
|
BatchCreatedResponse,
|
||||||
|
|
@ -17,7 +17,6 @@ from app.api.schemas import (
|
||||||
HealthResponse,
|
HealthResponse,
|
||||||
TaxonomyResponse,
|
TaxonomyResponse,
|
||||||
)
|
)
|
||||||
from app.core.auth import require_api_key
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.engine.corpus import add_document, delete_document, list_documents
|
from app.engine.corpus import add_document, delete_document, list_documents
|
||||||
from app.engine.detector import PlagiarismDetector
|
from app.engine.detector import PlagiarismDetector
|
||||||
|
|
@ -79,7 +78,6 @@ async def taxonomy(request: Request) -> TaxonomyResponse:
|
||||||
"/plagiarism/detect",
|
"/plagiarism/detect",
|
||||||
response_model=DetectResponse,
|
response_model=DetectResponse,
|
||||||
tags=["plagiarism"],
|
tags=["plagiarism"],
|
||||||
dependencies=[Depends(require_api_key)],
|
|
||||||
)
|
)
|
||||||
async def detect(req: DetectRequest, request: Request) -> DetectResponse:
|
async def detect(req: DetectRequest, request: Request) -> DetectResponse:
|
||||||
return _detector(request).detect_request(req)
|
return _detector(request).detect_request(req)
|
||||||
|
|
@ -90,7 +88,6 @@ async def detect(req: DetectRequest, request: Request) -> DetectResponse:
|
||||||
response_model=BatchCreatedResponse,
|
response_model=BatchCreatedResponse,
|
||||||
status_code=status.HTTP_202_ACCEPTED,
|
status_code=status.HTTP_202_ACCEPTED,
|
||||||
tags=["plagiarism"],
|
tags=["plagiarism"],
|
||||||
dependencies=[Depends(require_api_key)],
|
|
||||||
)
|
)
|
||||||
async def batch_create(
|
async def batch_create(
|
||||||
req: BatchRequest,
|
req: BatchRequest,
|
||||||
|
|
@ -113,7 +110,6 @@ async def batch_create(
|
||||||
"/plagiarism/batch/{job_id}",
|
"/plagiarism/batch/{job_id}",
|
||||||
response_model=BatchStatusResponse,
|
response_model=BatchStatusResponse,
|
||||||
tags=["plagiarism"],
|
tags=["plagiarism"],
|
||||||
dependencies=[Depends(require_api_key)],
|
|
||||||
)
|
)
|
||||||
async def batch_status(job_id: str, request: Request) -> BatchStatusResponse:
|
async def batch_status(job_id: str, request: Request) -> BatchStatusResponse:
|
||||||
job = _job_store(request).get(job_id)
|
job = _job_store(request).get(job_id)
|
||||||
|
|
@ -142,7 +138,6 @@ def _rebuild(request: Request) -> int:
|
||||||
"/corpus",
|
"/corpus",
|
||||||
response_model=CorpusListResponse,
|
response_model=CorpusListResponse,
|
||||||
tags=["corpus"],
|
tags=["corpus"],
|
||||||
dependencies=[Depends(require_api_key)],
|
|
||||||
)
|
)
|
||||||
async def corpus_list(request: Request) -> CorpusListResponse:
|
async def corpus_list(request: Request) -> CorpusListResponse:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
@ -158,7 +153,6 @@ async def corpus_list(request: Request) -> CorpusListResponse:
|
||||||
response_model=CorpusUploadResponse,
|
response_model=CorpusUploadResponse,
|
||||||
status_code=status.HTTP_201_CREATED,
|
status_code=status.HTTP_201_CREATED,
|
||||||
tags=["corpus"],
|
tags=["corpus"],
|
||||||
dependencies=[Depends(require_api_key)],
|
|
||||||
)
|
)
|
||||||
async def corpus_upload_json(req: CorpusUploadRequest, request: Request) -> CorpusUploadResponse:
|
async def corpus_upload_json(req: CorpusUploadRequest, request: Request) -> CorpusUploadResponse:
|
||||||
"""JSON으로 자서전 1건 업로드. 인덱스 자동 재빌드."""
|
"""JSON으로 자서전 1건 업로드. 인덱스 자동 재빌드."""
|
||||||
|
|
@ -183,7 +177,6 @@ async def corpus_upload_json(req: CorpusUploadRequest, request: Request) -> Corp
|
||||||
response_model=CorpusUploadResponse,
|
response_model=CorpusUploadResponse,
|
||||||
status_code=status.HTTP_201_CREATED,
|
status_code=status.HTTP_201_CREATED,
|
||||||
tags=["corpus"],
|
tags=["corpus"],
|
||||||
dependencies=[Depends(require_api_key)],
|
|
||||||
)
|
)
|
||||||
async def corpus_upload_file(
|
async def corpus_upload_file(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -218,7 +211,6 @@ async def corpus_upload_file(
|
||||||
"/corpus/{doc_id}",
|
"/corpus/{doc_id}",
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
tags=["corpus"],
|
tags=["corpus"],
|
||||||
dependencies=[Depends(require_api_key)],
|
|
||||||
)
|
)
|
||||||
async def corpus_delete(doc_id: str, request: Request) -> None:
|
async def corpus_delete(doc_id: str, request: Request) -> None:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
from fastapi import Header, HTTPException, status
|
|
||||||
|
|
||||||
from app.core.config import get_settings
|
|
||||||
|
|
||||||
|
|
||||||
async def require_api_key(x_api_key: str | None = Header(default=None)) -> str:
|
|
||||||
settings = get_settings()
|
|
||||||
if not x_api_key or x_api_key not in settings.api_key_set:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Invalid or missing X-API-Key header",
|
|
||||||
)
|
|
||||||
return x_api_key
|
|
||||||
|
|
@ -13,7 +13,6 @@ class Settings(BaseSettings):
|
||||||
log_level: str = "info" # debug / info / warning / error
|
log_level: str = "info" # debug / info / warning / error
|
||||||
reload: bool = False # 개발용 자동 재시작
|
reload: bool = False # 개발용 자동 재시작
|
||||||
|
|
||||||
api_keys: str = "combooks-key-change-me,baikal-key-change-me"
|
|
||||||
engine_version: str = "o2o-plagiarism-2.0.0-pdf-v1.2"
|
engine_version: str = "o2o-plagiarism-2.0.0-pdf-v1.2"
|
||||||
reference_corpus_dir: str = "./data/reference"
|
reference_corpus_dir: str = "./data/reference"
|
||||||
taxonomy_dir: str = "./data/taxonomy"
|
taxonomy_dir: str = "./data/taxonomy"
|
||||||
|
|
@ -49,10 +48,6 @@ class Settings(BaseSettings):
|
||||||
autobiography_mode: bool = True
|
autobiography_mode: bool = True
|
||||||
enable_entity_masking: bool = True
|
enable_entity_masking: bool = True
|
||||||
|
|
||||||
@property
|
|
||||||
def api_key_set(self) -> set[str]:
|
|
||||||
return {k.strip() for k in self.api_keys.split(",") if k.strip()}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def corpus_path(self) -> Path:
|
def corpus_path(self) -> Path:
|
||||||
return Path(self.reference_corpus_dir).resolve()
|
return Path(self.reference_corpus_dir).resolve()
|
||||||
|
|
|
||||||
|
|
@ -358,9 +358,6 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 검토 콘솔 내부 호출에 자동 첨부할 기본 키 (사내 운영용)
|
|
||||||
const DEFAULT_API_KEY = "combooks-key-change-me";
|
|
||||||
|
|
||||||
const SAMPLES = {
|
const SAMPLES = {
|
||||||
copy: {
|
copy: {
|
||||||
title: "어미만 변경한 표절 (홍길동전 기반)",
|
title: "어미만 변경한 표절 (홍길동전 기반)",
|
||||||
|
|
@ -423,7 +420,6 @@ async function runDetect() {
|
||||||
const docId = document.getElementById("doc-id").value.trim() || `web-${Date.now()}`;
|
const docId = document.getElementById("doc-id").value.trim() || `web-${Date.now()}`;
|
||||||
const title = document.getElementById("title").value.trim();
|
const title = document.getElementById("title").value.trim();
|
||||||
const author = document.getElementById("author").value.trim();
|
const author = document.getElementById("author").value.trim();
|
||||||
const apiKey = DEFAULT_API_KEY;
|
|
||||||
|
|
||||||
const thresholdVal = parseFloat(document.getElementById("threshold-slider").value);
|
const thresholdVal = parseFloat(document.getElementById("threshold-slider").value);
|
||||||
const autobioMode = document.querySelector('input[name="autobio-mode"]:checked').value;
|
const autobioMode = document.querySelector('input[name="autobio-mode"]:checked').value;
|
||||||
|
|
@ -451,7 +447,7 @@ async function runDetect() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/v1/plagiarism/detect", {
|
const resp = await fetch("/v1/plagiarism/detect", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
|
@ -614,11 +610,10 @@ document.querySelectorAll(".tab-btn").forEach((btn) => {
|
||||||
|
|
||||||
// ========== 코퍼스 관리 ==========
|
// ========== 코퍼스 관리 ==========
|
||||||
async function loadCorpus() {
|
async function loadCorpus() {
|
||||||
const apiKey = DEFAULT_API_KEY;
|
|
||||||
const tbody = document.getElementById("corpus-tbody");
|
const tbody = document.getElementById("corpus-tbody");
|
||||||
tbody.innerHTML = '<tr><td colspan="4" style="color: var(--muted); text-align: center; padding: 20px;">로딩 중…</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="4" style="color: var(--muted); text-align: center; padding: 20px;">로딩 중…</td></tr>';
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/v1/corpus", { headers: { "X-API-Key": apiKey } });
|
const resp = await fetch("/v1/corpus");
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
document.getElementById("corpus-count").textContent = data.total;
|
document.getElementById("corpus-count").textContent = data.total;
|
||||||
|
|
@ -647,7 +642,6 @@ async function uploadCorpus() {
|
||||||
const title = document.getElementById("corpus-title").value.trim();
|
const title = document.getElementById("corpus-title").value.trim();
|
||||||
const text = document.getElementById("corpus-text").value.trim();
|
const text = document.getElementById("corpus-text").value.trim();
|
||||||
const file = document.getElementById("corpus-file").files[0];
|
const file = document.getElementById("corpus-file").files[0];
|
||||||
const apiKey = DEFAULT_API_KEY;
|
|
||||||
const statusEl = document.getElementById("corpus-status");
|
const statusEl = document.getElementById("corpus-status");
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
|
|
@ -670,11 +664,11 @@ async function uploadCorpus() {
|
||||||
fd.append("title", title);
|
fd.append("title", title);
|
||||||
if (docId) fd.append("doc_id", docId);
|
if (docId) fd.append("doc_id", docId);
|
||||||
fd.append("file", file);
|
fd.append("file", file);
|
||||||
resp = await fetch("/v1/corpus/file", { method: "POST", headers: { "X-API-Key": apiKey }, body: fd });
|
resp = await fetch("/v1/corpus/file", { method: "POST", body: fd });
|
||||||
} else {
|
} else {
|
||||||
resp = await fetch("/v1/corpus", {
|
resp = await fetch("/v1/corpus", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ doc_id: docId || null, title, text }),
|
body: JSON.stringify({ doc_id: docId || null, title, text }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -700,11 +694,9 @@ async function uploadCorpus() {
|
||||||
|
|
||||||
async function deleteDoc(docId) {
|
async function deleteDoc(docId) {
|
||||||
if (!confirm(`'${docId}' 문서를 삭제하시겠습니까? 인덱스가 재빌드됩니다.`)) return;
|
if (!confirm(`'${docId}' 문서를 삭제하시겠습니까? 인덱스가 재빌드됩니다.`)) return;
|
||||||
const apiKey = DEFAULT_API_KEY;
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/v1/corpus/${encodeURIComponent(docId)}`, {
|
const resp = await fetch(`/v1/corpus/${encodeURIComponent(docId)}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { "X-API-Key": apiKey },
|
|
||||||
});
|
});
|
||||||
if (!resp.ok && resp.status !== 204) {
|
if (!resp.ok && resp.status !== 204) {
|
||||||
throw new Error(`HTTP ${resp.status}`);
|
throw new Error(`HTTP ${resp.status}`);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ services:
|
||||||
REFERENCE_CORPUS_DIR: /app/data/reference
|
REFERENCE_CORPUS_DIR: /app/data/reference
|
||||||
TAXONOMY_DIR: /app/data/taxonomy
|
TAXONOMY_DIR: /app/data/taxonomy
|
||||||
AUTOBIOGRAPHY_PATTERNS_PATH: /app/data/autobiography/common_patterns.txt
|
AUTOBIOGRAPHY_PATTERNS_PATH: /app/data/autobiography/common_patterns.txt
|
||||||
|
KOSIMCSE_MODEL: ${KOSIMCSE_MODEL:-BM-K/KoSimCSE-roberta-multitask}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
API_HOST="${API_HOST:-http://localhost:8000}"
|
API_HOST="${API_HOST:-http://localhost:8000}"
|
||||||
API_KEY="${API_KEY:-combooks-key-change-me}"
|
|
||||||
|
|
||||||
echo "--- 1) Health ---"
|
echo "--- 1) Health ---"
|
||||||
curl -sS "${API_HOST}/v1/health" | python3 -m json.tool
|
curl -sS "${API_HOST}/v1/health" | python3 -m json.tool
|
||||||
|
|
@ -12,7 +11,6 @@ echo
|
||||||
echo "--- 2) 단건 탐지: 어린왕자 유사 텍스트 ---"
|
echo "--- 2) 단건 탐지: 어린왕자 유사 텍스트 ---"
|
||||||
curl -sS -X POST "${API_HOST}/v1/plagiarism/detect" \
|
curl -sS -X POST "${API_HOST}/v1/plagiarism/detect" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "X-API-Key: ${API_KEY}" \
|
|
||||||
-d '{
|
-d '{
|
||||||
"doc_id": "test-001",
|
"doc_id": "test-001",
|
||||||
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 별을 떠나 여러 행성을 여행하며 다양한 어른들을 만난다. 마침내 지구에 도착해 여우를 만나고 길들임의 의미를 배운다.",
|
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 별을 떠나 여러 행성을 여행하며 다양한 어른들을 만난다. 마침내 지구에 도착해 여우를 만나고 길들임의 의미를 배운다.",
|
||||||
|
|
@ -23,7 +21,6 @@ echo
|
||||||
echo "--- 3) 배치 등록 ---"
|
echo "--- 3) 배치 등록 ---"
|
||||||
JOB_RESPONSE=$(curl -sS -X POST "${API_HOST}/v1/plagiarism/batch" \
|
JOB_RESPONSE=$(curl -sS -X POST "${API_HOST}/v1/plagiarism/batch" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "X-API-Key: ${API_KEY}" \
|
|
||||||
-d '{
|
-d '{
|
||||||
"items": [
|
"items": [
|
||||||
{"doc_id": "b-001", "text": "앤 셜리는 상상력이 풍부한 고아 소녀로 초록 지붕 집에 입양된다."},
|
{"doc_id": "b-001", "text": "앤 셜리는 상상력이 풍부한 고아 소녀로 초록 지붕 집에 입양된다."},
|
||||||
|
|
@ -36,5 +33,4 @@ JOB_ID=$(echo "${JOB_RESPONSE}" | python3 -c 'import json,sys; print(json.load(s
|
||||||
echo
|
echo
|
||||||
echo "--- 4) 배치 결과 조회 ---"
|
echo "--- 4) 배치 결과 조회 ---"
|
||||||
sleep 1
|
sleep 1
|
||||||
curl -sS "${API_HOST}/v1/plagiarism/batch/${JOB_ID}" \
|
curl -sS "${API_HOST}/v1/plagiarism/batch/${JOB_ID}" | python3 -m json.tool
|
||||||
-H "X-API-Key: ${API_KEY}" | python3 -m json.tool
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,13 @@ import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
API_HOST = os.environ.get("API_HOST", "http://localhost:8000")
|
API_HOST = os.environ.get("API_HOST", "http://localhost:8000")
|
||||||
API_KEY = os.environ.get("API_KEY", "combooks-key-change-me")
|
|
||||||
|
|
||||||
|
|
||||||
def _post(path: str, body: dict) -> dict:
|
def _post(path: str, body: dict) -> dict:
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{API_HOST}{path}",
|
f"{API_HOST}{path}",
|
||||||
data=json.dumps(body).encode("utf-8"),
|
data=json.dumps(body).encode("utf-8"),
|
||||||
headers={"Content-Type": "application/json", "X-API-Key": API_KEY},
|
headers={"Content-Type": "application/json"},
|
||||||
method="POST",
|
method="POST",
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req) as resp:
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
|
@ -25,7 +24,7 @@ def _post(path: str, body: dict) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def _get(path: str) -> dict:
|
def _get(path: str) -> dict:
|
||||||
req = urllib.request.Request(f"{API_HOST}{path}", headers={"X-API-Key": API_KEY})
|
req = urllib.request.Request(f"{API_HOST}{path}")
|
||||||
with urllib.request.urlopen(req) as resp:
|
with urllib.request.urlopen(req) as resp:
|
||||||
return json.loads(resp.read())
|
return json.loads(resp.read())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.main import app
|
from app.main import app
|
||||||
|
|
||||||
API_KEY = "combooks-key-change-me"
|
|
||||||
|
|
||||||
|
|
||||||
def test_health():
|
def test_health():
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
|
|
@ -17,13 +15,14 @@ def test_health():
|
||||||
assert body["corpus_size"] >= 0
|
assert body["corpus_size"] >= 0
|
||||||
|
|
||||||
|
|
||||||
def test_detect_requires_api_key():
|
def test_detect_no_auth_required():
|
||||||
|
"""인증 제거 - 키 없이도 200 응답."""
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
"/v1/plagiarism/detect",
|
"/v1/plagiarism/detect",
|
||||||
json={"doc_id": "x", "text": "테스트 본문"},
|
json={"doc_id": "x", "text": "테스트 본문"},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 401
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_detect_returns_schema():
|
def test_detect_returns_schema():
|
||||||
|
|
@ -34,7 +33,6 @@ def test_detect_returns_schema():
|
||||||
"doc_id": "t-1",
|
"doc_id": "t-1",
|
||||||
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 여우를 만나 길들임을 배운다.",
|
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 여우를 만나 길들임을 배운다.",
|
||||||
},
|
},
|
||||||
headers={"X-API-Key": API_KEY},
|
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
body = resp.json()
|
body = resp.json()
|
||||||
|
|
@ -53,12 +51,8 @@ def test_batch_flow():
|
||||||
{"doc_id": "b-1", "text": "앤 셜리는 초록 지붕 집에 입양된 소녀다."},
|
{"doc_id": "b-1", "text": "앤 셜리는 초록 지붕 집에 입양된 소녀다."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
headers={"X-API-Key": API_KEY},
|
|
||||||
)
|
)
|
||||||
assert resp.status_code == 202
|
assert resp.status_code == 202
|
||||||
job_id = resp.json()["job_id"]
|
job_id = resp.json()["job_id"]
|
||||||
status = client.get(
|
status = client.get(f"/v1/plagiarism/batch/{job_id}")
|
||||||
f"/v1/plagiarism/batch/{job_id}",
|
|
||||||
headers={"X-API-Key": API_KEY},
|
|
||||||
)
|
|
||||||
assert status.status_code == 200
|
assert status.status_code == 200
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue