fix: make photo retention and exclusion mutually exclusive
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Successful in 6m10s

This commit is contained in:
渡口浪人
2026-10-20 19:19:30 +08:00
parent b770626a66
commit 95625c5857
9 changed files with 187 additions and 34 deletions
+121 -6
View File
@@ -26,10 +26,26 @@ def test_local_exclusion_star_and_restore(client, service):
== 200
)
result = client.put(f"/api/v1/assets/{excluded}/exclusion", json={"excluded": True})
assert result.status_code == 409
client.put(f"/api/v1/assets/{excluded}/star", json={"starred": False})
result = client.put(f"/api/v1/assets/{excluded}/exclusion", json={"excluded": True})
assert (
result.status_code == 200
and result.json()["excluded"]
and result.json()["starred"]
and not result.json()["starred"]
)
assert (
client.put(
f"/api/v1/assets/{excluded}/star", json={"starred": True}
).status_code
== 409
)
batch = client.post(
"/api/v1/assets/batch", json={"asset_ids": [excluded], "action": "star"}
).json()
assert (
batch["results"][0]["status"] == "failed"
and batch["results"][0]["code"] == "CONFLICT"
)
assert next(
x for x in client.get("/api/v1/assets").json() if x["asset_id"] == excluded
@@ -57,7 +73,13 @@ def test_local_exclusion_star_and_restore(client, service):
restored = client.put(
f"/api/v1/assets/{excluded}/exclusion", json={"excluded": False}
).json()
assert not restored["excluded"] and restored["starred"]
assert not restored["excluded"] and not restored["starred"]
assert (
client.put(
f"/api/v1/assets/{excluded}/star", json={"starred": True}
).status_code
== 200
)
assert service.content_asset(
AutoCollageContent(type="auto_collage", count=1, use_ai=False)
)
@@ -77,11 +99,21 @@ def test_source_preferences_are_scoped_and_do_not_mutate_cached_catalog(
client.put(url, json={"item_id": items[0]["id"], "starred": True}).status_code
== 200
)
assert (
client.put(url, json={"item_id": items[0]["id"], "excluded": True}).status_code
== 409
)
assert (
client.put(
url, json={"item_id": items[0]["id"], "starred": True, "excluded": True}
).status_code
== 409
)
assert client.put(
url, json={"item_id": items[0]["id"], "excluded": True}
).json() == {"starred": True, "excluded": True}
url, json={"item_id": items[0]["id"], "starred": False, "excluded": True}
).json() == {"starred": False, "excluded": True}
result = client.get("/api/v1/photo-sources/drive/images").json()["items"][0]
assert result["starred"] and result["excluded"]
assert not result["starred"] and result["excluded"]
assert "excluded" not in items[0]
assert eligible(service.db, "drive", items) == []
assert eligible(service.db, "another-drive", items) == items
@@ -104,7 +136,7 @@ def test_source_preferences_are_scoped_and_do_not_mutate_cached_catalog(
client.put(url, json={"item_id": items[0]["id"], "excluded": False})
assert eligible(service.db, "drive", items) == items
assert flags(service.db, "drive", items[0]["id"]) == {
"starred": True,
"starred": False,
"excluded": False,
}
@@ -120,3 +152,86 @@ def test_preference_persistence(settings):
reopened = Database(path)
assert flags(reopened, "drive", "item")["excluded"]
reopened.conn.close()
@pytest.mark.parametrize("value", ["true", "false", 1, 0, [], {}])
def test_marks_reject_non_boolean_payloads(client, service, value):
key = photo(service, "fixture.jpg", "red")["asset_id"]
assert (
client.put(f"/api/v1/assets/{key}/star", json={"starred": value}).status_code
== 422
)
assert (
client.put(
f"/api/v1/assets/{key}/exclusion", json={"excluded": value}
).status_code
== 422
)
assert (
client.put(
"/api/v1/photo-sources/drive/preferences",
json={"item_id": "fixture", "starred": value},
).status_code
== 422
)
def test_asset_marks_are_mutually_exclusive_under_concurrent_requests(client, service):
from concurrent.futures import ThreadPoolExecutor
key = photo(service, "fixture.jpg", "blue")["asset_id"]
def request_star():
return client.put(
f"/api/v1/assets/{key}/star", json={"starred": True}
).status_code
def request_exclusion():
return client.put(
f"/api/v1/assets/{key}/exclusion", json={"excluded": True}
).status_code
with ThreadPoolExecutor(max_workers=2) as pool:
first = pool.submit(request_star)
second = pool.submit(request_exclusion)
assert sorted([first.result(), second.result()]) == [200, 409]
final = service.assets.info(key)
assert final["starred"] != final["excluded"]
def test_source_marks_are_mutually_exclusive_under_concurrent_requests(service):
from concurrent.futures import ThreadPoolExecutor
def change(**mark):
try:
set_flags(service.db, "drive", "fixture", **mark)
return 200
except FlowError as error:
return error.status
with ThreadPoolExecutor(max_workers=2) as pool:
first = pool.submit(change, starred=True)
second = pool.submit(change, excluded=True)
assert sorted([first.result(), second.result()]) == [200, 409]
final = flags(service.db, "drive", "fixture")
assert final["starred"] != final["excluded"]
def test_batch_exclusion_rejects_starred_and_unknown_payload_fields(client, service):
key = photo(service, "fixture.jpg", "red")["asset_id"]
assert (
client.put(
f"/api/v1/assets/{key}/star", json={"starred": True, "excluded": False}
).status_code
== 422
)
client.put(f"/api/v1/assets/{key}/star", json={"starred": True})
result = client.post(
"/api/v1/assets/batch", json={"asset_ids": [key], "action": "exclude"}
).json()
assert (
result["results"][0]["status"] == "failed"
and result["results"][0]["code"] == "CONFLICT"
)
final = service.assets.info(key)
assert final["starred"] and not final["excluded"]