fix(files): ending every session and making a token lock the user without holding up rows that refer to it
This commit is contained in:
@@ -12,9 +12,9 @@ from datetime import datetime, timedelta, timezone
|
||||
import pytest
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from app import apitokens
|
||||
from app import apitokens, audit
|
||||
from app.apitokens import files_caller
|
||||
from app.auth import current_user
|
||||
from app.downloads import folders, service
|
||||
@@ -24,6 +24,7 @@ from app.downloads.status import JobStatus
|
||||
from app.config import settings
|
||||
from app.db import SessionLocal, get_db
|
||||
from app.main import app
|
||||
from app.notifications import create_notification
|
||||
from app.models import (
|
||||
ApiToken,
|
||||
ApiTokenGrant,
|
||||
@@ -427,6 +428,74 @@ def test_a_token_asked_for_as_every_session_ends_is_refused(db, make_user, clien
|
||||
assert db.execute(select(ApiToken.id)).scalars().all() == []
|
||||
|
||||
|
||||
def _meanwhile(work):
|
||||
"""Run `work(other_session)` in another thread and session, committed; (thread, outcome)."""
|
||||
import threading
|
||||
|
||||
outcome: list[object] = []
|
||||
|
||||
def run():
|
||||
other = SessionLocal()
|
||||
try:
|
||||
outcome.append(work(other))
|
||||
other.commit()
|
||||
except Exception as e: # noqa: BLE001 — the assertion below names what went wrong
|
||||
outcome.append(e)
|
||||
finally:
|
||||
other.close()
|
||||
|
||||
t = threading.Thread(target=run)
|
||||
t.start()
|
||||
return t, outcome
|
||||
|
||||
|
||||
def test_ending_every_session_never_deadlocks_with_a_tokens_expiry_notice(db, make_user):
|
||||
"""The notice's sweep holds the token's row and then writes a row naming the user; the sessions'
|
||||
end holds the user and then deletes the token. A lock on the user that a mere reference to it
|
||||
waits on would make that a deadlock — one of the two failing."""
|
||||
alice = make_user()
|
||||
raw = apitokens.mint()
|
||||
soon = datetime.now(timezone.utc) + timedelta(days=1)
|
||||
tok = ApiToken(
|
||||
user_id=alice.id, name="t", prefix=raw[:12], token_hash=hash_token(raw), expires_at=soon
|
||||
)
|
||||
db.add(tok)
|
||||
db.commit()
|
||||
# `warn_expiring`, its two halves held apart: the token row taken…
|
||||
db.execute(update(ApiToken).where(ApiToken.id == tok.id).values(expiry_warned=True))
|
||||
t, outcome = _meanwhile(lambda other: bump_session_epoch(other, other.get(User, alice.id)))
|
||||
t.join(timeout=1)
|
||||
assert t.is_alive() # the sessions' end waits for the token row
|
||||
# …and then its notice, naming the user.
|
||||
create_notification(db, alice.id, "api_token_expiring", title="t", commit=False)
|
||||
db.commit()
|
||||
t.join(timeout=10)
|
||||
assert outcome == [None]
|
||||
db.expire_all()
|
||||
assert db.execute(select(ApiToken.id)).scalars().all() == []
|
||||
|
||||
|
||||
def test_a_token_granted_a_folder_being_deleted_never_deadlocks(db, make_user):
|
||||
"""The delete holds the folder and then writes its audit line naming the user; the token holds
|
||||
the user and then waits for the folder."""
|
||||
alice = make_user()
|
||||
fid = _own(db, alice.id, "Mine").id
|
||||
folders.delete_own(db, db.get(FileFolder, fid)) # the folder held, as the delete route does
|
||||
body = tokens_routes.TokenIn(
|
||||
name="x", grants=[tokens_routes.GrantIn(folder_id=fid, rights=["read"])]
|
||||
)
|
||||
t, outcome = _meanwhile(
|
||||
lambda other: tokens_routes.create_token(body, other.get(User, alice.id), other)
|
||||
)
|
||||
t.join(timeout=1)
|
||||
assert t.is_alive() # the token waits for the folder
|
||||
audit.record(db, alice.id, "file_folder_delete", target_type="file_folder", target_id=fid)
|
||||
db.commit()
|
||||
t.join(timeout=10)
|
||||
[refusal] = outcome
|
||||
assert getattr(refusal, "status_code", None) == 409, refusal # the folder went first
|
||||
|
||||
|
||||
def test_a_token_made_as_every_session_ends_goes_with_them(db, make_user):
|
||||
"""The other order: the token is on its way in when the sessions end."""
|
||||
import threading
|
||||
|
||||
Reference in New Issue
Block a user