Files

846 lines
36 KiB
Python

import asyncio
import base64
import json
from pathlib import Path
import httpx
import imagefind.api as api_module
from imagefind import __version__
from imagefind.config import Settings
from imagefind.database import utcnow
from imagefind.main import create_app
from imagefind.speech import SPEECH_INDEX_REVISION
from imagefind.text import search_tokens
def test_setup_login_csrf_and_token(tmp_path: Path):
app = create_app(Settings(data_dir=tmp_path, embedding_backend="hash", scan_interval_seconds=86400))
async def scenario():
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
assert (await client.get("/api/v1/status")).json()["configured"] is False
response = await client.post("/api/v1/setup", json={"password": "a secure test password"})
assert response.status_code == 200
csrf = response.json()["csrf_token"]
assert (await client.get("/api/v1/auth/me")).status_code == 200
assert (await client.post("/api/v1/tokens", json={"name": "test"})).status_code == 403
token = await client.post(
"/api/v1/tokens",
json={"name": "test"},
headers={"X-CSRF-Token": csrf},
)
assert token.status_code == 201
api_token = token.json()["token"]
assert (
await client.get("/api/v1/sources", headers={"Authorization": f"Bearer {api_token}"})
).status_code == 200
asyncio.run(scenario())
def test_status_frontend_fallback_and_openapi(tmp_path: Path):
settings = Settings(
data_dir=tmp_path,
frontend_dir=tmp_path / "frontend-not-built",
embedding_backend="hash",
scan_interval_seconds=86400,
)
app = create_app(settings)
async def scenario():
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
status = await client.get("/api/v1/status")
assert status.status_code == 200
assert status.json() == {"configured": False, "version": __version__, "access_mode": "direct"}
root = await client.get("/")
assert root.status_code == 200
assert "ImageFind API 正在运行" in root.text
openapi = await client.get("/api/openapi.json")
assert openapi.status_code == 200
assert "/api/v1/search" in openapi.json()["paths"]
asyncio.run(scenario())
def test_security_headers_token_scope_payload_and_diagnostics(tmp_path: Path):
app = create_app(Settings(data_dir=tmp_path, embedding_backend="hash", scan_interval_seconds=86400))
async def scenario():
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
status = await client.get("/api/v1/status")
assert status.headers["x-content-type-options"] == "nosniff"
assert status.headers["referrer-policy"] == "same-origin"
assert "camera=()" in status.headers["permissions-policy"]
assert "default-src 'self'" in status.headers["content-security-policy"]
setup = await client.post("/api/v1/setup", json={"password": "a secure test password"})
csrf = setup.json()["csrf_token"]
created = await client.post(
"/api/v1/tokens",
headers={"X-CSRF-Token": csrf},
json={"name": "DAV automation", "scopes": ["webdav"]},
)
assert created.status_code == 201
assert created.json()["scopes"] == ["webdav"]
listed = await client.get("/api/v1/tokens")
assert listed.json()[0]["scopes"] == ["webdav"]
assert "token" not in listed.json()[0]
diagnostics = await client.get("/api/v1/system/diagnostics")
assert diagnostics.status_code == 200
payload = diagnostics.json()
assert payload["process_rss_bytes"] >= 0
assert payload["database"]["engine"] == "postgresql"
assert payload["database"]["pool_max"] >= 1
assert isinstance(payload["database"]["activity"]["states"], dict)
assert "running" in payload["inference"]
assert payload["events"]["subscribers"] == 0
asyncio.run(scenario())
def test_fnos_gateway_auth_is_bound_to_unix_socket_and_prefix(tmp_path: Path):
socket_path = tmp_path / "imagefind.sock"
settings = Settings(
data_dir=tmp_path / "data",
gateway_socket=socket_path,
gateway_prefix="/app/imagefind",
embedding_backend="hash",
scan_interval_seconds=86400,
)
app = create_app(settings)
app.state.services.auth.setup("gateway fallback password")
class UnixSocketScope:
def __init__(self, target):
self.target = target
async def __call__(self, scope, receive, send):
mounted = dict(scope)
mounted["server"] = (str(socket_path), None)
await self.target(mounted, receive, send)
async def scenario():
# Header spoofing over TCP must never enable gateway SSO.
direct_transport = httpx.ASGITransport(app=app)
spoofed = {
"X-Trim-Isadmin": "true",
"X-Trim-Userid": "nas-admin",
"X-Trim-Username": "Administrator",
}
async with httpx.AsyncClient(transport=direct_transport, base_url="http://test") as direct:
status = await direct.get("/api/v1/status", headers=spoofed)
assert status.json()["access_mode"] == "direct"
assert (await direct.post("/api/v1/auth/gateway", headers=spoofed)).status_code == 404
assert (await direct.get("/api/v1/sources", headers=spoofed)).status_code == 401
gateway_transport = httpx.ASGITransport(app=UnixSocketScope(app))
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers=spoofed,
follow_redirects=False,
) as gateway:
redirect = await gateway.get("/app/imagefind")
assert redirect.status_code == 307
assert redirect.headers["location"] == "/app/imagefind/"
assert (await gateway.get("/outside-prefix")).status_code == 404
status = await gateway.get("/app/imagefind/api/v1/status")
assert status.json()["access_mode"] == "gateway"
login = await gateway.post("/app/imagefind/api/v1/auth/gateway")
assert login.status_code == 200
csrf = login.json()["csrf_token"]
gateway_token = login.json()["gateway_session_token"]
assert gateway_token
assert login.json()["nas_username"] == "Administrator"
cookie = login.headers["set-cookie"]
assert "imagefind_gateway_session=" in cookie
assert "Path=/app/imagefind/" in cookie
me = await gateway.get("/app/imagefind/api/v1/auth/me")
assert me.status_code == 200
assert me.json()["kind"] == "gateway"
assert (await gateway.post("/app/imagefind/api/v1/tokens", json={"name": "blocked"})).status_code == 403
created = await gateway.post(
"/app/imagefind/api/v1/tokens",
json={"name": "gateway"},
headers={**spoofed, "X-CSRF-Token": csrf},
)
assert created.status_code == 201
changed_user = await gateway.get(
"/app/imagefind/api/v1/auth/me",
headers={**spoofed, "X-Trim-Userid": "another-admin"},
)
assert changed_user.status_code == 401
# The fnOS WebView may omit the scoped cookie. The short-lived session
# header keeps SSO working, but only inside the trusted Unix gateway.
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers={**spoofed, "X-ImageFind-Gateway-Session": gateway_token},
) as header_only:
me = await header_only.get("/app/imagefind/api/v1/auth/me")
assert me.status_code == 200
assert me.json()["nas_user_id"] == "nas-admin"
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers={
**spoofed,
"X-Trim-Userid": "another-admin",
"X-ImageFind-Gateway-Session": gateway_token,
},
) as wrong_identity:
assert (await wrong_identity.get("/app/imagefind/api/v1/auth/me")).status_code == 401
other_headers = {
"X-Trim-Isadmin": "true",
"X-Trim-Userid": "other-admin",
"X-Trim-Username": "Other administrator",
}
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers=other_headers,
) as other_gateway:
other_login = await other_gateway.post("/app/imagefind/api/v1/auth/gateway")
other_token = other_login.json()["gateway_session_token"]
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers={
**spoofed,
"Cookie": f"imagefind_gateway_session={other_token}",
"X-ImageFind-Gateway-Session": gateway_token,
},
) as stale_cookie:
me = await stale_cookie.get("/app/imagefind/api/v1/auth/me")
assert me.status_code == 200
assert me.json()["nas_user_id"] == "nas-admin"
async with httpx.AsyncClient(transport=direct_transport, base_url="http://test") as direct:
response = await direct.get(
"/api/v1/auth/me",
headers={**spoofed, "X-ImageFind-Gateway-Session": gateway_token},
)
assert response.status_code == 401
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers={"X-Trim-Isadmin": "false", "X-Trim-Userid": "ordinary-user"},
) as ordinary:
assert (await ordinary.post("/app/imagefind/api/v1/auth/gateway")).status_code == 403
async with httpx.AsyncClient(transport=gateway_transport, base_url="http://test") as anonymous:
assert (await anonymous.post("/app/imagefind/api/v1/auth/gateway")).status_code == 401
asyncio.run(scenario())
def test_frontend_runtime_base_is_injected_for_gateway_and_direct_access(tmp_path: Path):
frontend = tmp_path / "frontend"
frontend.mkdir()
(frontend / "index.html").write_text("<html><head></head><body>ImageFind</body></html>")
socket_path = tmp_path / "imagefind.sock"
settings = Settings(
data_dir=tmp_path / "data",
frontend_dir=frontend,
gateway_socket=socket_path,
embedding_backend="hash",
scan_interval_seconds=86400,
)
app = create_app(settings)
class UnixSocketScope:
async def __call__(self, scope, receive, send):
mounted = dict(scope)
mounted["server"] = (str(socket_path), None)
await app(mounted, receive, send)
async def scenario():
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as direct:
index = await direct.get("/")
assert 'window.__IMAGEFIND_BASE__="/"' in index.text
assert 'dataset.imagefindAccess="direct"' in index.text
assert index.headers["cache-control"] == "no-store"
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=UnixSocketScope()), base_url="http://test"
) as gateway:
index = await gateway.get("/app/imagefind/")
assert 'window.__IMAGEFIND_BASE__="/app/imagefind/"' in index.text
assert 'dataset.imagefindAccess="gateway"' in index.text
asyncio.run(scenario())
def _seed_local_search(app, tmp_path: Path) -> tuple[str, str, bytes]:
service = app.state.services
source_id = "local-source"
video_id = "local-video"
frame_id = "local-frame"
video_bytes = b"0123456789abcdef"
video_path = tmp_path / "sample.mp4"
video_path.write_bytes(video_bytes)
now = utcnow()
with service.db.transaction() as conn:
conn.execute(
"INSERT INTO sources(id,kind,name,config_json,created_at,updated_at) VALUES(?,?,?,?,?,?)",
(source_id, "local", "本地资料库", json.dumps({"path": str(tmp_path)}), now, now),
)
conn.execute(
"INSERT INTO videos(id,source_id,source_key,display_name,location,fingerprint,duration_ms,status,"
"available,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
(
video_id,
source_id,
"sample.mp4",
"海边假期.mp4",
str(video_path),
"fingerprint",
120_000,
"ready",
1,
now,
now,
),
)
conn.execute(
"INSERT INTO frames(id,video_id,timestamp_ms,segment_start_ms,segment_end_ms,thumbnail_path,created_at) "
"VALUES(?,?,?,?,?,?,?)",
(frame_id, video_id, 42_000, 40_000, 48_000, str(tmp_path / "frame.webp"), now),
)
tokens = " ".join(search_tokens("海边日落"))
for kind in ("subtitle", "filename"):
entry_id = f"text-{kind}"
conn.execute(
"INSERT INTO text_entries(id,video_id,frame_id,kind,start_ms,end_ms,raw_text,tokens,created_at) "
"VALUES(?,?,?,?,?,?,?,?,?)",
(entry_id, video_id, frame_id, kind, 40_000, 48_000, "海边日落", tokens, now),
)
conn.execute("INSERT INTO text_fts(entry_id,tokens) VALUES(?,?)", (entry_id, tokens))
audio_tokens = " ".join(search_tokens("海浪声音"))
conn.execute(
"INSERT INTO text_entries(id,video_id,frame_id,kind,start_ms,end_ms,raw_text,tokens,created_at) "
"VALUES('text-audio',?,?, 'audio',?,?,?,?,?)",
(video_id, frame_id, 65_000, 68_000, "远处传来海浪声音", audio_tokens, now),
)
conn.execute("INSERT INTO text_fts(entry_id,tokens) VALUES('text-audio',?)", (audio_tokens,))
_, token = service.auth.create_api_token("test")
return token, video_id, video_bytes
def test_gateway_media_token_reads_native_media_without_cookie_or_session_header(tmp_path: Path):
socket_path = tmp_path / "imagefind.sock"
settings = Settings(
data_dir=tmp_path / "data",
gateway_socket=socket_path,
gateway_prefix="/app/imagefind",
embedding_backend="hash",
scan_interval_seconds=86400,
)
settings.prepare()
app = create_app(settings)
service = app.state.services
service.auth.setup("gateway native media test password")
_, video_id, video_bytes = _seed_local_search(app, tmp_path)
thumbnail_path = tmp_path / "frame.webp"
thumbnail_path.write_bytes(b"webp-thumbnail")
preview = settings.preview_dir / "media123"
preview.mkdir(parents=True)
(preview / "index.m3u8").write_text("#EXTM3U\nsegment-00001.ts\n", encoding="utf-8")
(preview / "segment-00001.ts").write_bytes(b"gateway-segment")
class UnixSocketScope:
async def __call__(self, scope, receive, send):
mounted = dict(scope)
mounted["server"] = (str(socket_path), None)
await app(mounted, receive, send)
identity = {
"X-Trim-Isadmin": "true",
"X-Trim-Userid": "nas-admin",
"X-Trim-Username": "Administrator",
}
gateway_transport = httpx.ASGITransport(app=UnixSocketScope())
async def scenario():
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers=identity,
) as login_client:
login = await login_client.post("/app/imagefind/api/v1/auth/gateway")
assert login.status_code == 200
session_token = login.json()["gateway_session_token"]
media_token = login.json()["gateway_media_token"]
assert media_token and media_token != session_token
refreshed = await login_client.post(
"/app/imagefind/api/v1/auth/gateway/media-token",
headers={"X-CSRF-Token": login.json()["csrf_token"]},
)
assert refreshed.status_code == 200
refreshed_token = refreshed.json()["gateway_media_token"]
query = f"media_token={media_token}"
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers=identity,
) as native:
thumbnail = await native.get(f"/app/imagefind/api/v1/frames/local-frame/thumbnail?{query}")
assert thumbnail.status_code == 200
assert thumbnail.content == b"webp-thumbnail"
stream = await native.get(
f"/app/imagefind/api/v1/videos/{video_id}/stream?{query}",
headers={"Range": "bytes=2-5"},
)
assert stream.status_code == 206
assert stream.content == video_bytes[2:6]
download = await native.get(f"/app/imagefind/api/v1/videos/{video_id}/download?{query}")
assert download.status_code == 200
assert download.content == video_bytes
playlist = await native.get(f"/app/imagefind/api/v1/previews/media123/index.m3u8?{query}")
assert playlist.status_code == 200
assert f"segment-00001.ts?media_token={media_token}" in playlist.text
segment = await native.get(
f"/app/imagefind/api/v1/previews/media123/segment-00001.ts?{query}"
)
assert segment.status_code == 200
assert segment.content == b"gateway-segment"
assert (await native.get(f"/app/imagefind/api/v1/sources?{query}")).status_code == 401
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers={**identity, "X-Trim-Userid": "another-admin"},
) as wrong_user:
assert (
await wrong_user.get(f"/app/imagefind/api/v1/videos/{video_id}/stream?{query}")
).status_code == 401
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://test"
) as direct:
assert (await direct.get(f"/api/v1/videos/{video_id}/stream?{query}")).status_code == 401
with service.db.transaction() as conn:
conn.execute(
"UPDATE gateway_media_tokens SET expires_at='2000-01-01T00:00:00+00:00' WHERE token_hash=?",
(service.auth._digest(refreshed_token),),
)
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers=identity,
) as expired:
assert (
await expired.get(
f"/app/imagefind/api/v1/videos/{video_id}/stream?media_token={refreshed_token}"
)
).status_code == 401
service.auth.logout(session_token)
async with httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers=identity,
) as revoked:
assert (
await revoked.get(f"/app/imagefind/api/v1/videos/{video_id}/stream?{query}")
).status_code == 401
asyncio.run(scenario())
def test_search_match_sources_local_range_and_hls_paths(tmp_path: Path):
settings = Settings(data_dir=tmp_path, embedding_backend="hash", scan_interval_seconds=86400)
settings.prepare()
app = create_app(settings)
token, video_id, video_bytes = _seed_local_search(app, tmp_path)
headers = {"Authorization": f"Bearer {token}"}
preview = settings.preview_dir / "abc123"
preview.mkdir(parents=True)
(preview / "index.m3u8").write_bytes(b"#EXTM3U\n")
(preview / "segment-00001.ts").write_bytes(b"segment")
async def scenario():
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
search = await client.post("/api/v1/search", headers=headers, json={"text": "海边日落"})
assert search.status_code == 200
item = search.json()["items"][0]
assert item["video_id"] == video_id
assert item["timestamp_ms"] == 40_000
assert item["match_sources"] == ["filename", "subtitle"]
assert {detail["type"] for detail in item["match_details"]} == {"metadata", "subtitle"}
audio = await client.post(
"/api/v1/search",
headers=headers,
json={"text": "海浪声音", "recognition_types": ["audio"]},
)
audio_item = audio.json()["items"][0]
assert audio_item["match_sources"] == ["audio"]
assert audio_item["segment_start_ms"] == 65_000
assert audio_item["match_details"][0]["text"] == "远处传来海浪声音"
excluded = await client.post(
"/api/v1/search",
headers=headers,
json={"text": "海浪声音", "recognition_types": ["ocr"]},
)
assert excluded.json()["items"] == []
partial = await client.get(
f"/api/v1/videos/{video_id}/stream",
headers={**headers, "Range": "bytes=2-5"},
)
assert partial.status_code == 206
assert partial.content == video_bytes[2:6]
assert partial.headers["content-range"] == f"bytes 2-5/{len(video_bytes)}"
assert partial.headers["accept-ranges"] == "bytes"
suffix = await client.get(
f"/api/v1/videos/{video_id}/stream",
headers={**headers, "Range": "bytes=-3"},
)
assert suffix.status_code == 206
assert suffix.content == video_bytes[-3:]
invalid = await client.get(
f"/api/v1/videos/{video_id}/stream",
headers={**headers, "Range": "bytes=99-100"},
)
assert invalid.status_code == 416
playlist = await client.get("/api/v1/previews/abc123/index.m3u8", headers=headers)
assert playlist.status_code == 200
assert playlist.content == b"#EXTM3U\n"
segment = await client.get("/api/v1/previews/abc123/segment-00001.ts", headers=headers)
assert segment.status_code == 200
assert segment.content == b"segment"
assert (await client.get("/api/v1/previews/not-safe/index.m3u8", headers=headers)).status_code == 404
assert (await client.get("/api/v1/previews/abc123/metadata.json", headers=headers)).status_code == 404
asyncio.run(scenario())
def test_audio_search_has_an_independent_candidate_budget_and_simplified_traditional_variants(
tmp_path: Path,
):
settings = Settings(data_dir=tmp_path, embedding_backend="hash", scan_interval_seconds=86400)
settings.prepare()
app = create_app(settings)
token, video_id, _ = _seed_local_search(app, tmp_path)
service = app.state.services
now = utcnow()
simplified = "繁体关键词"
traditional = "繁體關鍵詞"
with service.db.transaction() as conn:
for index in range(520):
tokens = " ".join(search_tokens(traditional))
entry_id = f"crowding-ocr-{index:03d}"
conn.execute(
"INSERT INTO text_entries(id,video_id,frame_id,kind,start_ms,end_ms,raw_text,tokens,created_at) "
"VALUES(?,?,?,'ocr',0,1000,?,?,?)",
(entry_id, video_id, "local-frame", traditional, tokens, now),
)
tokens = " ".join(search_tokens(simplified))
conn.execute(
"INSERT INTO text_entries(id,video_id,frame_id,kind,start_ms,end_ms,raw_text,tokens,created_at) "
"VALUES('audio-simplified',?,?, 'audio',66000,69000,?,?,?)",
(video_id, "local-frame", f"这里说的是{simplified}", tokens, now),
)
async def scenario():
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
response = await client.post(
"/api/v1/search",
headers={"Authorization": f"Bearer {token}"},
json={"text": traditional},
)
assert response.status_code == 200
item = response.json()["items"][0]
assert "audio" in item["match_sources"]
assert any(detail["type"] == "audio" for detail in item["match_details"])
asyncio.run(scenario())
def test_transcript_coverage_pagination_and_manual_reindex(tmp_path: Path, monkeypatch):
settings = Settings(data_dir=tmp_path, embedding_backend="hash", scan_interval_seconds=86400)
settings.prepare()
app = create_app(settings)
token, video_id, _ = _seed_local_search(app, tmp_path)
service = app.state.services
monkeypatch.setattr(service.models, "runnable_component_versions", lambda: {"audio": "audio-v1"})
now = utcnow()
with service.db.transaction() as conn:
conn.execute(
"UPDATE videos SET audio_model_version='audio-v1',audio_index_revision=?,"
"audio_detected_language='zh',audio_quality_score=.94,audio_rejected_segments=1,"
"audio_quality_flags_json='not-json' WHERE id=?",
(SPEECH_INDEX_REVISION, video_id),
)
conn.execute("DELETE FROM text_entries WHERE video_id=? AND kind='audio'", (video_id,))
for index in range(35):
text = f"第{index + 1}个中文音频片段"
conn.execute(
"INSERT INTO text_entries(id,video_id,frame_id,kind,start_ms,end_ms,raw_text,tokens,created_at) "
"VALUES(?,?,NULL,'audio',?,?,?,?,?)",
(
f"transcript-{index:02d}",
video_id,
index * 1000,
index * 1000 + 900,
text,
" ".join(search_tokens(text)),
now,
),
)
# A trusted legacy transcript remains searchable while the current model reindex is queued.
conn.execute("UPDATE videos SET audio_model_version='audio-legacy' WHERE id=?", (video_id,))
async def scenario():
headers = {"Authorization": f"Bearer {token}"}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
coverage = await client.get("/api/v1/search/coverage", headers=headers)
assert coverage.status_code == 200
coverage_payload = coverage.json()
assert coverage_payload["indexed"] == 0
assert coverage_payload["searchable"] == 1
assert coverage_payload["empty"] == 0
assert coverage_payload["percent"] == 100.0
assert coverage_payload["current_model_percent"] == 0.0
with service.db.transaction() as conn:
conn.execute(
"UPDATE videos SET audio_model_version='audio-v1' WHERE id=?", (video_id,)
)
current_coverage = await client.get("/api/v1/search/coverage", headers=headers)
assert current_coverage.status_code == 200
assert current_coverage.json()["indexed"] == 1
assert current_coverage.json()["searchable"] == 1
assert current_coverage.json()["current_model_percent"] == 100.0
transcript = await client.get(
f"/api/v1/videos/{video_id}/transcript?page=2&page_size=10", headers=headers
)
assert transcript.status_code == 200
payload = transcript.json()
assert payload["status"] == "ready"
assert payload["page"] == 2 and payload["pages"] == 4 and payload["total"] == 35
assert payload["items"][0]["start_ms"] == 10_000
assert payload["detected_language"] == "zh"
assert payload["quality_state"] == "ready"
assert payload["aggregate_risk"] is False
assert payload["repeated_phrases"] == []
assert payload["quality_score_semantics"] == "rule_check"
assert payload["quality_flags"] == []
speech = await client.get("/api/v1/speech/config", headers=headers)
assert speech.status_code == 200
assert speech.json()["language_policy"] == "zh_priority"
configured = await client.patch(
"/api/v1/speech/config",
headers=headers,
json={"language_policy": "auto", "quality_profile": "balanced"},
)
assert configured.status_code == 200
assert configured.json()["quality_profile"] == "balanced"
queued = await client.post(
f"/api/v1/videos/{video_id}/transcript/reindex", headers=headers, json={"language": "zh"}
)
assert queued.status_code == 202
with service.db.read() as conn:
job = conn.execute(
"SELECT kind,priority,payload_json FROM jobs WHERE id=?", (queued.json()["job_id"],)
).fetchone()
assert {"kind": job["kind"], "priority": job["priority"]} == {
"kind": "transcribe_audio",
"priority": 0,
}
assert json.loads(job["payload_json"]) == {"video_id": video_id, "language": "zh"}
asyncio.run(scenario())
def test_webdav_range_proxy_forwards_headers_and_hides_credentials(tmp_path: Path, monkeypatch):
socket_path = tmp_path / "imagefind.sock"
settings = Settings(
data_dir=tmp_path,
gateway_socket=socket_path,
gateway_prefix="/app/imagefind",
embedding_backend="hash",
scan_interval_seconds=86400,
)
settings.prepare()
app = create_app(settings)
service = app.state.services
service.auth.setup("gateway WebDAV media test password")
now = utcnow()
secret_blob = service.secrets.encrypt_json({"password": "remote-password"})
with service.db.transaction() as conn:
conn.execute(
"INSERT INTO sources(id,kind,name,config_json,secret_blob,created_at,updated_at) "
"VALUES(?,?,?,?,?,?,?)",
(
"dav-source",
"webdav",
"远程资料库",
json.dumps(
{
"base_url": "https://dav.example/videos/",
"username": "remote-user",
"verify_tls": True,
}
),
secret_blob,
now,
now,
),
)
conn.execute(
"INSERT INTO videos(id,source_id,source_key,display_name,location,fingerprint,status,available,"
"created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)",
(
"dav-video",
"dav-source",
"movie.mp4",
"movie.mp4",
"https://dav.example/videos/movie.mp4",
"remote-fingerprint",
"ready",
1,
now,
now,
),
)
_, token = service.auth.create_api_token("test")
service.db.set_setting(
"webdav_server", {"enabled": True, "source_id": "dav-source", "relative_path": ""}
)
webdav_auth = {
"Authorization": "Basic " + base64.b64encode(f"imagefind:{token}".encode()).decode()
}
observed: dict[str, object] = {"requests": []}
class UnixSocketScope:
async def __call__(self, scope, receive, send):
mounted = dict(scope)
mounted["server"] = (str(socket_path), None)
await app(mounted, receive, send)
identity = {
"X-Trim-Isadmin": "true",
"X-Trim-Userid": "nas-admin",
"X-Trim-Username": "Administrator",
}
class FakeAsyncClient:
def __init__(self, **kwargs):
observed["client_kwargs"] = kwargs
def build_request(self, method, url, headers):
request = httpx.Request(method, url, headers=headers)
observed["requests"].append(request)
return request
async def send(self, request, stream=False):
assert stream is True
if request.method == "HEAD":
return httpx.Response(405, request=request)
if request.headers.get("range") == "bytes=0-0":
return httpx.Response(
206,
request=request,
content=b"0",
headers={
"Content-Length": "1",
"Content-Range": "bytes 0-0/10",
"Accept-Ranges": "bytes",
"Content-Type": "application/octet-stream",
},
)
return httpx.Response(
206,
request=request,
content=b"2345",
headers={
"Content-Length": "4",
"Content-Range": "bytes 2-5/10",
"Accept-Ranges": "bytes",
"Content-Type": "application/octet-stream",
},
)
async def aclose(self):
observed["closed"] = True
async def scenario():
transport = httpx.ASGITransport(app=app)
gateway_transport = httpx.ASGITransport(app=UnixSocketScope())
async with (
httpx.AsyncClient(transport=transport, base_url="http://test") as client,
httpx.AsyncClient(
transport=gateway_transport,
base_url="http://test",
headers=identity,
) as gateway,
):
login = await gateway.post("/app/imagefind/api/v1/auth/gateway")
assert login.status_code == 200
media_token = login.json()["gateway_media_token"]
monkeypatch.setattr(api_module.httpx, "AsyncClient", FakeAsyncClient)
response = await client.get(
"/api/v1/videos/dav-video/stream",
headers={"Authorization": f"Bearer {token}", "Range": "bytes=2-5", "If-Range": '"etag"'},
)
assert response.status_code == 206
assert response.content == b"2345"
assert response.headers["content-range"] == "bytes 2-5/10"
assert response.headers["content-type"] == "video/mp4"
gateway_response = await gateway.get(
f"/app/imagefind/api/v1/videos/dav-video/stream?media_token={media_token}",
headers={"Range": "bytes=2-5", "If-Range": '"etag"'},
)
assert gateway_response.status_code == 206
assert gateway_response.content == b"2345"
assert gateway_response.headers["content-type"] == "video/mp4"
head = await client.request(
"HEAD",
"/webdav/movie.mp4",
headers=webdav_auth,
)
assert head.status_code == 200
assert head.headers["content-length"] == "10"
assert "content-range" not in head.headers
assert head.headers["accept-ranges"] == "bytes"
assert head.headers["content-type"] == "video/mp4"
ranged_head = await client.request(
"HEAD",
"/webdav/movie.mp4",
headers={**webdav_auth, "Range": "bytes=2-5"},
)
assert ranged_head.status_code == 206
assert ranged_head.headers["content-length"] == "4"
assert ranged_head.headers["content-range"] == "bytes 2-5/10"
asyncio.run(scenario())
requests = observed["requests"]
assert isinstance(requests, list)
assert [request.method for request in requests] == ["GET", "GET", "HEAD", "GET", "HEAD", "GET"]
upstream = requests[0]
assert isinstance(upstream, httpx.Request)
assert upstream.url == "https://dav.example/videos/movie.mp4"
assert upstream.headers["range"] == "bytes=2-5"
assert upstream.headers["if-range"] == '"etag"'
assert "remote-password" not in str(upstream.url)
assert observed["closed"] is True