Archived
fix: make indexing review paths replay-safe
This commit is contained in:
1 parent
77fa48c7a2
commit
653c32db43
24 files changed
+759
-86
No files matched your search
@@ -1,5 +1,6 @@
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -45,4 +46,24 @@ def test_empty_create_and_rename_do_not_enqueue_jobs(
|
||||
client.patch(f"/collections/{collection_id}", json={"name": "Reading"}, headers=headers)
|
||||
with cast(FastAPI, client.app).state.database.connect() as connection:
|
||||
total = connection.execute("SELECT COUNT(*) FROM jobs").fetchone()[0]
|
||||
assert total == 0
|
||||
assert total == 0
|
||||
|
||||
|
||||
def test_delete_rolls_back_when_cleanup_job_cannot_be_written(
|
||||
authenticated: tuple[TestClient, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
client, csrf = authenticated
|
||||
collection_id = client.post(
|
||||
"/collections", json={"name": "Books"}, headers={"X-CSRF-Token": csrf}
|
||||
).json()["id"]
|
||||
|
||||
def fail_id(_size: int) -> str:
|
||||
raise RuntimeError("simulated job ID failure")
|
||||
|
||||
monkeypatch.setattr("recoll_modern.storage.jobs.secrets.token_hex", fail_id)
|
||||
with pytest.raises(RuntimeError, match="simulated job ID failure"):
|
||||
client.delete(
|
||||
f"/collections/{collection_id}", params={"confirmation": "Books"},
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
assert client.get(f"/collections/{collection_id}").status_code == 200
|
||||
@@ -34,6 +34,8 @@ def test_text_and_byte_range(authenticated: tuple[TestClient, str], tmp_path: Pa
|
||||
assert client.get(
|
||||
f"/documents/{digest}/file", headers={"Range": "bytes=99-100"}
|
||||
).status_code == 416
|
||||
suffix = client.get(f"/documents/{digest}/file", headers={"Range": "bytes=-2"})
|
||||
assert suffix.status_code == 206 and suffix.content == b"ef"
|
||||
|
||||
|
||||
def test_unknown_and_missing_documents(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -22,6 +25,7 @@ def test_retry_refuses_completed_job(authenticated: tuple[TestClient, str]) -> N
|
||||
if client.get(f"/jobs/{job_id}").json()["job"]["state"] == "completed":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
time.sleep(0.01)
|
||||
response = client.post(f"/jobs/{job_id}/retry", headers={"X-CSRF-Token": csrf})
|
||||
assert response.status_code == 409
|
||||
assert response.json()["error"]["code"] == "job_not_retryable"
|
||||
@@ -32,3 +36,37 @@ def test_job_list_validates_pagination(authenticated: tuple[TestClient, str]) ->
|
||||
response = client.get("/jobs", params={"limit": 201})
|
||||
assert response.status_code == 422
|
||||
assert response.json()["error"]["code"] == "validation_failed"
|
||||
|
||||
|
||||
def test_collection_reindex_includes_departed_rule_path(
|
||||
authenticated: tuple[TestClient, str], tmp_path: Path
|
||||
) -> None:
|
||||
client, csrf = authenticated
|
||||
books = tmp_path / "docs" / "books"
|
||||
books.mkdir()
|
||||
file = books / "gone.txt"
|
||||
file.write_text("gone")
|
||||
headers = {"X-CSRF-Token": csrf}
|
||||
collection_id = client.post(
|
||||
"/collections", headers=headers, json={"name": "Books"}
|
||||
).json()["id"]
|
||||
created = client.post(
|
||||
f"/collections/{collection_id}/memberships", headers=headers,
|
||||
json={"sources": [{"type": "directory", "path": "books", "mode": "persistent"}]},
|
||||
)
|
||||
job_id = created.json()["job"]["id"]
|
||||
for _ in range(100):
|
||||
if client.get(f"/jobs/{job_id}").json()["job"]["state"] == "completed":
|
||||
break
|
||||
engine = cast(FastAPI, client.app).state.engine
|
||||
engine.forced_paths.clear()
|
||||
file.unlink()
|
||||
refreshed = client.post(
|
||||
"/jobs/reindex", headers=headers, json={"collections": [collection_id]}
|
||||
)
|
||||
refresh_id = refreshed.json()["job"]["id"]
|
||||
for _ in range(100):
|
||||
if client.get(f"/jobs/{refresh_id}").json()["job"]["state"] == "completed":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
assert any(str(file) in batch for batch in engine.forced_paths)
|
||||
@@ -2,6 +2,8 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -177,3 +179,90 @@ def test_remove_document_creates_override_then_direct_add_restores_it(
|
||||
assert len(members) == 1
|
||||
assert members[0]["direct"] is True
|
||||
assert members[0]["override"] is False
|
||||
|
||||
|
||||
def test_rule_deletion_retry_reindexes_removed_membership(
|
||||
authenticated: tuple[TestClient, str], tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
client, csrf = authenticated
|
||||
books = tmp_path / "docs" / "books"
|
||||
books.mkdir()
|
||||
file = books / "a.txt"
|
||||
file.write_text("book")
|
||||
collection_id = _collection(client, csrf)
|
||||
created = client.post(
|
||||
f"/collections/{collection_id}/memberships",
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
json={"sources": [{"type": "directory", "path": "books", "mode": "persistent"}]},
|
||||
)
|
||||
assert _wait(client, created.json()["job"]["id"])["state"] == "completed"
|
||||
rule_id = client.get(f"/collections/{collection_id}/directory-rules").json()["items"][0]["id"]
|
||||
engine = cast(FastAPI, client.app).state.engine
|
||||
original = engine.force_reindex
|
||||
calls: list[tuple[str, ...]] = []
|
||||
|
||||
def fail_once(paths: tuple[str, ...]) -> None:
|
||||
calls.append(paths)
|
||||
if len(calls) == 1:
|
||||
raise RuntimeError("simulated indexing failure")
|
||||
original(paths)
|
||||
|
||||
monkeypatch.setattr(engine, "force_reindex", fail_once)
|
||||
deleted = client.delete(
|
||||
f"/collections/{collection_id}/directory-rules/{rule_id}",
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
job_id = deleted.json()["job"]["id"]
|
||||
assert _wait(client, job_id)["state"] == "failed"
|
||||
retried = client.post(f"/jobs/{job_id}/retry", headers={"X-CSRF-Token": csrf})
|
||||
assert retried.status_code == 202
|
||||
assert _wait(client, job_id)["state"] == "completed"
|
||||
assert calls == [(str(file),), (str(file),)]
|
||||
|
||||
|
||||
def test_rule_patch_retry_reindexes_excluded_member(
|
||||
authenticated: tuple[TestClient, str], tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
client, csrf = authenticated
|
||||
skipped = tmp_path / "docs" / "books" / "skip"
|
||||
skipped.mkdir(parents=True)
|
||||
file = skipped / "a.txt"
|
||||
file.write_text("book")
|
||||
collection_id = _collection(client, csrf)
|
||||
created = client.post(
|
||||
f"/collections/{collection_id}/memberships",
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
json={"sources": [{"type": "directory", "path": "books", "mode": "persistent"}]},
|
||||
)
|
||||
assert _wait(client, created.json()["job"]["id"])["state"] == "completed"
|
||||
rule_id = client.get(f"/collections/{collection_id}/directory-rules").json()["items"][0]["id"]
|
||||
engine = cast(FastAPI, client.app).state.engine
|
||||
original = engine.force_reindex
|
||||
calls: list[tuple[str, ...]] = []
|
||||
|
||||
def fail_once(paths: tuple[str, ...]) -> None:
|
||||
calls.append(paths)
|
||||
if len(calls) == 1:
|
||||
raise RuntimeError("simulated indexing failure")
|
||||
original(paths)
|
||||
|
||||
monkeypatch.setattr(engine, "force_reindex", fail_once)
|
||||
patched = client.patch(
|
||||
f"/collections/{collection_id}/directory-rules/{rule_id}",
|
||||
headers={"X-CSRF-Token": csrf}, json={"excluded_directories": ["books/skip"]},
|
||||
)
|
||||
job_id = patched.json()["job"]["id"]
|
||||
assert _wait(client, job_id)["state"] == "failed"
|
||||
client.post(f"/jobs/{job_id}/retry", headers={"X-CSRF-Token": csrf})
|
||||
assert _wait(client, job_id)["state"] == "completed"
|
||||
assert calls == [(str(file),), (str(file),)]
|
||||
|
||||
|
||||
def test_rule_list_validates_pagination(authenticated: tuple[TestClient, str]) -> None:
|
||||
client, csrf = authenticated
|
||||
collection_id = _collection(client, csrf)
|
||||
response = client.get(
|
||||
f"/collections/{collection_id}/directory-rules", params={"limit": 201}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
assert response.json()["error"]["code"] == "validation_failed"
|
||||
Reference in new issue
Block a user