`Question.is_shared` defaulted to 1 and was only ever set by a route nothing called, so in practice it divided the bank into "everything" and "everything, plus your own private ones" — a distinction that cost every recommendation denominator a join and never changed an answer. Who may reach the bank is the site's own access rules; who may manage a question is the category grant tree. So the two predicates the whole bank was built on are now the same thing, and say what they actually mean: a question is out of reach if it has been deleted or belongs to a course. Nothing else. The column is dropped, the route that set it is gone, the bulk "share" action with it, and the Private tile and pill go from the question manager. The tests that turned on it have been rewritten rather than deleted, because the rule they were really about survives: revoking a question still revokes every session carrying it — by deleting it, which is the only revocation left. Several others named a category holding exactly two reachable questions and then answered two particular ids; that category holds four now, so they name the pair instead. A session's own sharing flag is untouched — that is a different thing, and it is still how a session is handed to somebody. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
98 lines
4.4 KiB
Python
98 lines
4.4 KiB
Python
"""Questions, articles and flashcard decks all file under one category tree.
|
|
|
|
Run: DATABASE_URL=sqlite:///:memory: PYTHONPATH=backend python -m unittest discover -s backend/tests
|
|
"""
|
|
import os
|
|
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
|
|
|
|
import unittest
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from app.database import Base, get_db
|
|
from app.models.article import Article
|
|
from app.models.course import Course # noqa — Quiz.course_id FK needs the table in metadata.
|
|
from app.models.flashcard import FlashcardDeck
|
|
from app.models.question import Question
|
|
from app.models.question_category import QuestionCategory
|
|
from app.models.user import User
|
|
from app.routers import flashcards
|
|
from app.utils.auth import get_current_user
|
|
|
|
|
|
class SharedCategoryTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
|
Base.metadata.create_all(self.engine)
|
|
self.db = Session(self.engine)
|
|
|
|
self.user = User(id=1, name="Owner", email="owner@example.test", hashed_password="unused")
|
|
self.db.add(self.user)
|
|
self.db.add_all([
|
|
QuestionCategory(id=1, name="Cardiology", user_id=1),
|
|
QuestionCategory(id=2, name="Kawasaki disease", parent_id=1, user_id=1),
|
|
QuestionCategory(id=3, name="Neurology", user_id=1),
|
|
])
|
|
self.db.flush()
|
|
# One of each content type filed under the same tree.
|
|
self.db.add(Question(id=1, question_category_id=2, user_id=1,
|
|
question_text="A question", question_type="mcq",
|
|
options=["yes", "no"], correct_answer="yes"))
|
|
self.db.add(Article(id=1, slug="kawasaki", title="Kawasaki disease", sections=[],
|
|
category_id=2, user_id=1, status="published"))
|
|
self.db.add_all([
|
|
FlashcardDeck(id=1, title="Kawasaki cards", user_id=1, card_count=0, category_id=2),
|
|
FlashcardDeck(id=2, title="Neuro cards", user_id=1, card_count=0, category_id=3),
|
|
FlashcardDeck(id=3, title="Unfiled cards", user_id=1, card_count=0),
|
|
])
|
|
self.db.commit()
|
|
|
|
app = FastAPI()
|
|
app.include_router(flashcards.router, prefix="/flashcards")
|
|
app.dependency_overrides[get_db] = lambda: self.db
|
|
app.dependency_overrides[get_current_user] = lambda: self.user
|
|
self.client = TestClient(app)
|
|
|
|
def tearDown(self):
|
|
self.client.close()
|
|
self.db.close()
|
|
self.engine.dispose()
|
|
|
|
def test_all_three_content_types_use_the_same_category_ids(self):
|
|
question = self.db.get(Question, 1)
|
|
article = self.db.get(Article, 1)
|
|
deck = self.db.get(FlashcardDeck, 1)
|
|
self.assertEqual({question.question_category_id, article.category_id, deck.category_id}, {2})
|
|
|
|
def test_deck_list_filters_by_category_including_descendants(self):
|
|
titles = lambda params: {d["title"] for d in self.client.get("/flashcards/", params=params).json()}
|
|
self.assertEqual(titles({"category_id": 2}), {"Kawasaki cards"})
|
|
# A parent picks up everything filed beneath it.
|
|
self.assertEqual(titles({"category_id": 1}), {"Kawasaki cards"})
|
|
self.assertEqual(titles({"category_id": 3}), {"Neuro cards"})
|
|
self.assertEqual(titles({}), {"Kawasaki cards", "Neuro cards", "Unfiled cards"})
|
|
|
|
def test_a_deck_can_be_filed_and_unfiled(self):
|
|
response = self.client.patch("/flashcards/3", json={"category_id": 3})
|
|
self.assertEqual(response.status_code, 200, response.text)
|
|
self.assertEqual(response.json()["category_id"], 3)
|
|
self.assertEqual(self.db.get(FlashcardDeck, 3).category_id, 3)
|
|
|
|
self.assertEqual(self.client.patch("/flashcards/3", json={"category_id": None}).json()["category_id"], None)
|
|
|
|
def test_filing_under_a_category_that_does_not_exist_is_refused(self):
|
|
self.assertEqual(self.client.patch("/flashcards/1", json={"category_id": 999}).status_code, 404)
|
|
self.assertEqual(self.db.get(FlashcardDeck, 1).category_id, 2)
|
|
|
|
def test_renaming_a_deck_leaves_its_category_alone(self):
|
|
self.client.patch("/flashcards/1", json={"title": "Renamed"})
|
|
deck = self.db.get(FlashcardDeck, 1)
|
|
self.assertEqual((deck.title, deck.category_id), ("Renamed", 2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|