"""Exercise Alembic's real offline path; never connect to an external database.""" import io import os from pathlib import Path import unittest from unittest.mock import patch os.environ["DATABASE_URL"] = "sqlite:///:memory:" import test_quiz_builder as fixtures from app.database import Base # Initialize the app engine with disposable SQLite before offline URL override. from app.models.question import Question from alembic import command from alembic.config import Config from alembic.script import ScriptDirectory class CategoryMigrationTests(unittest.TestCase): def test_graph_upgrade_and_downgrade(self): backend = Path(__file__).resolve().parents[1] output = io.StringIO() config = Config(output_buffer=output) config.set_main_option("script_location", str(backend / "alembic")) scripts = ScriptDirectory.from_config(config) self.assertEqual(len(scripts.get_heads()), 1) self.assertEqual(scripts.get_revision("c82d19e4a601").down_revision, "5f8c1c2a9d40") list(scripts.walk_revisions()) # sql=True uses no engine/connection; the URL selects PostgreSQL DDL only. with patch.dict(os.environ, {"DATABASE_URL": "postgresql://unused@127.0.0.1/offline_only"}): command.upgrade(config, "5f8c1c2a9d40:c82d19e4a601", sql=True) sql = output.getvalue() self.assertIn("ADD COLUMN IF NOT EXISTS parent_id INTEGER", sql) self.assertIn("WHERE conrelid = 'question_categories'::regclass", sql) self.assertIn("FOREIGN KEY (parent_id) REFERENCES question_categories(id) ON DELETE RESTRICT", sql) self.assertNotIn("UPDATE question_categories", sql) output.seek(0) output.truncate() command.downgrade(config, "c82d19e4a601:5f8c1c2a9d40", sql=True) self.assertIn("DROP CONSTRAINT IF EXISTS fk_question_categories_parent", output.getvalue()) self.assertIn("DROP COLUMN IF EXISTS parent_id", output.getvalue()) if __name__ == "__main__": unittest.main() class DeletingATopicTests(unittest.TestCase): """Where the questions go has to be said. Deleting a topic used to leave them filed nowhere — no discipline, no organ system, no relevance, no row on any tab of the analysis. In the bank and invisible to every page that counts them. """ def setUp(self): self.bank = fixtures.BuilderTests() self.bank.setUp() self.client = self.bank.client self.db = self.bank.db self.bank.user = self.bank.mod def tearDown(self): self.bank.tearDown() def test_a_topic_holding_questions_will_not_go_quietly(self): # Category 3 holds question 3 in the fixture bank. response = self.client.delete('/question-categories/3') self.assertEqual(response.status_code, 400, response.text) self.assertIn('Choose a topic to move them to', response.json()['detail']) self.assertIsNotNone(self.db.get(Question, 3).question_category_id) def test_naming_a_destination_moves_them_and_deletes_the_topic(self): from app.models.question_category import QuestionCategory response = self.client.delete('/question-categories/3', params={'move_to': 2}) self.assertEqual(response.status_code, 204, response.text) self.assertEqual(self.db.get(Question, 3).question_category_id, 2) self.assertIsNone(self.db.get(QuestionCategory, 3)) def test_an_empty_topic_goes_without_being_asked(self): from app.models.question_category import QuestionCategory # Category 4 holds nothing. self.assertEqual(self.client.delete('/question-categories/4').status_code, 204) self.assertIsNone(self.db.get(QuestionCategory, 4)) def test_an_extra_link_moves_too_and_does_not_duplicate(self): from app.models.question_category import QuestionCategoryLink # Question 1 is filed under category 1 and also linked to 3. self.db.add(QuestionCategoryLink(question_id=1, category_id=3)) self.db.add(QuestionCategoryLink(question_id=1, category_id=2)) self.db.commit() self.assertEqual( self.client.delete('/question-categories/3', params={'move_to': 2}).status_code, 204) pairs = self.db.query(QuestionCategoryLink).filter_by(question_id=1, category_id=2).count() self.assertEqual(pairs, 1)