diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py index f640de5c..354e0b14 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py @@ -418,9 +418,7 @@ def compile_table_exists(self, table, database=None, schema=None): return f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'" def compile_column_exists(self, table, column): - return ( - f"SELECT column_name FROM information_schema.columns WHERE table_name='{table}' and column_name='{column}'" - ) + return f"SELECT name FROM pragma_table_info('{table}') WHERE name='{column}'" def compile_get_all_tables(self, database, schema=None): return "SELECT name FROM sqlite_master WHERE type='table'" diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/database_seeder.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/database_seeder.py index 6460ecd7..916c8614 100644 --- a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/database_seeder.py +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/database_seeder.py @@ -1,8 +1,8 @@ from fastapi_startkit.masoniteorm.seeders import Seeder -from .recorder import CALLS +from ...models import SeededUser class DatabaseSeeder(Seeder): async def run(self): - CALLS.append(("database", self.connection)) + await SeededUser.create({"name": "database-seeder"}) diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/recorder.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/recorder.py deleted file mode 100644 index 0d32f16f..00000000 --- a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/recorder.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Shared call recorder used by the fixture seeder classes below.""" - -CALLS = [] diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/sample_seeder.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/sample_seeder.py index aa19a2b1..d6d4d255 100644 --- a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/sample_seeder.py +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/sample_seeder.py @@ -1,8 +1,8 @@ from fastapi_startkit.masoniteorm.seeders import Seeder -from .recorder import CALLS +from ...models import SeededUser class SampleSeeder(Seeder): async def run(self): - CALLS.append(("sample", self.connection)) + await SeededUser.create({"name": "sample-seeder"}) diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/special_seeder.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/special_seeder.py index d51a277d..7da78334 100644 --- a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/special_seeder.py +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/special_seeder.py @@ -1,8 +1,8 @@ from fastapi_startkit.masoniteorm.seeders import Seeder -from .recorder import CALLS +from ...models import SeededUser class SpecialSeeder(Seeder): async def run(self): - CALLS.append(("special", self.connection)) + await SeededUser.create({"name": "special-seeder"}) diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/user_table_seeder.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/user_table_seeder.py index 250278f6..81a9c7f5 100644 --- a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/user_table_seeder.py +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/seeders/user_table_seeder.py @@ -1,8 +1,8 @@ from fastapi_startkit.masoniteorm.seeders import Seeder -from .recorder import CALLS +from ...models import SeededUser class UserTableSeeder(Seeder): async def run(self): - CALLS.append(("user_table", self.connection)) + await SeededUser.create({"name": "user-table-seeder"}) diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/models.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/models.py new file mode 100644 index 00000000..e3fc9fdc --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/models.py @@ -0,0 +1,10 @@ +from fastapi_startkit.masoniteorm.models.model import Model + + +class SeededUser(Model): + """Real model backing the sqlite table the fixture seeders write into.""" + + __table__ = "seed_users" + __timestamps__ = False + + name: str diff --git a/fastapi_startkit/tests/masoniteorm/commands/test_db_seed_command.py b/fastapi_startkit/tests/masoniteorm/commands/test_db_seed_command.py new file mode 100644 index 00000000..76289703 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/commands/test_db_seed_command.py @@ -0,0 +1,109 @@ +import asyncio +import unittest + +from cleo.testers.command_tester import CommandTester + +from fastapi_startkit.masoniteorm.commands.DBSeedCommand import DBSeedCommand + +from .fixtures.app import create_app, DB_PATH +from .fixtures.models import SeededUser + +FIXTURE_SEED_PATH = "tests.masoniteorm.commands.fixtures.databases.seeders" + + +class TestDBSeedCommand(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.app = create_app() + + def setUp(self): + asyncio.run(self._reset_table()) + + def tearDown(self): + asyncio.run(self._drop_table()) + if DB_PATH.exists(): + DB_PATH.unlink() + + async def _reset_table(self): + db = self.app.make("db") + await db.clear() + schema = db.get_schema_builder() + await schema.drop_table_if_exists("seed_users") + async with await schema.create("seed_users") as table: + table.id() + table.string("name") + + async def _drop_table(self): + db = self.app.make("db") + schema = db.get_schema_builder() + await schema.drop_table_if_exists("seed_users") + await db.clear() + + def _run(self, args=""): + tester = CommandTester(DBSeedCommand()) + tester.execute(args) + return tester.io.fetch_output() + + def _seeded_names(self): + return asyncio.run(self._fetch_names()) + + @staticmethod + async def _fetch_names(): + rows = await SeededUser.all() + return sorted(row.name for row in rows) + + # -- behavior is proven by the rows the real Seeder + real fixture seeder + # classes write into a real sqlite table, not by console output -- + + def test_runs_database_seeder_by_default(self): + self._run(f"--directory {FIXTURE_SEED_PATH} --connection sqlite") + + self.assertEqual(self._seeded_names(), ["database-seeder"]) + + def test_seeds_specific_table_from_argument(self): + self._run(f"user --directory {FIXTURE_SEED_PATH} --connection sqlite") + + self.assertEqual(self._seeded_names(), ["user-table-seeder"]) + + def test_class_option_resolves_plain_class_name(self): + self._run(f"--directory {FIXTURE_SEED_PATH} --class SampleSeeder --connection sqlite") + + self.assertEqual(self._seeded_names(), ["sample-seeder"]) + + def test_class_option_resolves_table_seeder_suffix(self): + self._run(f"--directory {FIXTURE_SEED_PATH} --class UserTableSeeder --connection sqlite") + + self.assertEqual(self._seeded_names(), ["user-table-seeder"]) + + def test_class_option_accepts_dotted_path(self): + self._run(f"--directory {FIXTURE_SEED_PATH} --class special_seeder.SpecialSeeder --connection sqlite") + + self.assertEqual(self._seeded_names(), ["special-seeder"]) + + def test_class_option_takes_precedence_over_table_argument(self): + self._run(f"user --directory {FIXTURE_SEED_PATH} --class SampleSeeder --connection sqlite") + + self.assertEqual(self._seeded_names(), ["sample-seeder"]) + + def test_uses_default_connection_when_not_specified(self): + self._run(f"--directory {FIXTURE_SEED_PATH}") + + self.assertEqual(self._seeded_names(), ["database-seeder"]) + + def test_success_message_names_the_seeder(self): + # Minimal, secondary output check -- the user-facing message contract, + # not a substitute for the row assertions above. + output = self._run(f"--directory {FIXTURE_SEED_PATH} --connection sqlite") + + self.assertIn("Database Seeder seeded!", output) + + # -- error paths: driven directly through CommandTester, since the console + # application catches command exceptions instead of propagating them -- + + def test_raises_when_seeder_class_cannot_be_found(self): + with self.assertRaises(ValueError): + self._run(f"--directory {FIXTURE_SEED_PATH} --class does_not_exist.NopeSeeder --connection sqlite") + + def test_raises_when_database_seeder_missing_from_directory(self): + with self.assertRaises(ValueError): + self._run("--directory tests.masoniteorm.commands.fixtures.databases.migrations --connection sqlite") diff --git a/fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py b/fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py new file mode 100644 index 00000000..5ad20843 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py @@ -0,0 +1,273 @@ +import asyncio +import importlib.util +import os +import shutil +import tempfile +import unittest + +from cleo.testers.command_tester import CommandTester + +from fastapi_startkit.masoniteorm.commands.MakeMigrationCommand import MakeMigrationCommand +from fastapi_startkit.masoniteorm.commands.MakeModelCommand import MakeModelCommand +from fastapi_startkit.masoniteorm.commands.MakeObserverCommand import MakeObserverCommand +from fastapi_startkit.masoniteorm.commands.MakeSeedCommand import MakeSeedCommand +from fastapi_startkit.masoniteorm.migrations.Migration import Migration + +from .fixtures.app import create_app, DB_PATH + + +class _TempCwdTestCase(unittest.TestCase): + """Base case that runs each test inside an isolated temp working directory. + + The make/generator commands write files relative to ``os.getcwd()``, so the + working directory is swapped for a throwaway one to keep the repo clean. + """ + + def setUp(self): + self._original_cwd = os.getcwd() + self._tmp_dir = tempfile.mkdtemp() + os.chdir(self._tmp_dir) + + def tearDown(self): + os.chdir(self._original_cwd) + shutil.rmtree(self._tmp_dir, ignore_errors=True) + + def _tester(self, command_class): + return CommandTester(command_class()) + + def _generated_file(self, directory): + files = [f for f in os.listdir(directory) if f.endswith(".py")] + self.assertEqual(len(files), 1, f"expected one generated file in {directory}, got {files}") + return os.path.join(directory, files[0]) + + def _read_single_file(self, directory): + path = self._generated_file(directory) + with open(path) as fp: + return os.path.basename(path), fp.read() + + +class TestMakeMigrationCommand(_TempCwdTestCase): + """Real sqlite integration tests: a generated migration is actually executed + against a live sqlite database and the resulting schema is inspected, rather + than asserting on the generated file's text. + """ + + @classmethod + def setUpClass(cls): + cls.app = create_app() + + def setUp(self): + super().setUp() + asyncio.run(self._reset_db()) + + def tearDown(self): + super().tearDown() + if DB_PATH.exists(): + DB_PATH.unlink() + + async def _reset_db(self): + db = self.app.make("db") + await db.clear() + schema = db.get_schema_builder() + for table in await schema.get_all_tables(): + if table.startswith("sqlite_"): + continue + await schema.drop_table_if_exists(table) + + def _generate(self, args, directory="databases/migrations"): + os.makedirs(directory, exist_ok=True) + tester = self._tester(MakeMigrationCommand) + tester.execute(args) + return self._generated_file(directory), tester.io.fetch_output() + + @staticmethod + def _load_migration_class(file_path): + spec = importlib.util.spec_from_file_location( + f"generated_migration_{os.path.basename(file_path).replace('.py', '')}", + file_path, + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + for obj in vars(module).values(): + if isinstance(obj, type) and issubclass(obj, Migration) and obj is not Migration: + return obj + raise AssertionError(f"no Migration subclass found in {file_path}") + + def _run_generated_migration(self, file_path): + asyncio.run(self._execute_up(file_path)) + + async def _execute_up(self, file_path): + migration_class = self._load_migration_class(file_path) + db = self.app.make("db") + await db.clear() + schema = db.get_schema_builder() + await migration_class(connection="sqlite", schema=schema).up() + + async def _create_table(self, name): + db = self.app.make("db") + await db.clear() + schema = db.get_schema_builder() + async with await schema.create(name) as table: + table.increments("id") + + def _has_table(self, table): + return asyncio.run(self._async_has_table(table)) + + async def _async_has_table(self, table): + db = self.app.make("db") + await db.clear() + return await db.get_schema_builder().has_table(table) + + def _has_column(self, table, column): + return asyncio.run(self._async_has_column(table, column)) + + async def _async_has_column(self, table, column): + db = self.app.make("db") + await db.clear() + return await db.get_schema_builder().has_column(table, column) + + # -- behavior is proven by the real sqlite schema the generated migration + # produces when executed, not by the generated file's text -- + + def test_inferred_table_migration_creates_real_table(self): + file_path, output = self._generate("create_posts_table") + self.assertIn("Migration file created:", output) + + self.assertFalse(self._has_table("posts")) + self._run_generated_migration(file_path) + + self.assertTrue(self._has_table("posts")) + self.assertTrue(self._has_column("posts", "id")) + self.assertTrue(self._has_column("posts", "created_at")) + self.assertTrue(self._has_column("posts", "updated_at")) + + def test_create_option_creates_named_table(self): + file_path, _ = self._generate("add_users --create users") + + self._run_generated_migration(file_path) + + self.assertTrue(self._has_table("users")) + self.assertTrue(self._has_column("users", "id")) + + def test_table_option_migration_executes_against_existing_table(self): + # The table (alter) stub has an empty ``up`` body, so there is no new + # column to assert -- the meaningful real-DB check is that the generated + # alter migration executes cleanly against an existing table. + asyncio.run(self._create_table("widgets")) + file_path, _ = self._generate("modify_widgets --table widgets") + + self._run_generated_migration(file_path) + + self.assertTrue(self._has_table("widgets")) + + def test_custom_directory_migration_runs_from_custom_location(self): + file_path, output = self._generate( + "create_posts_table --directory custom/migrations", + directory="custom/migrations", + ) + self.assertIn("custom/migrations", output) + + self._run_generated_migration(file_path) + + self.assertTrue(self._has_table("posts")) + + +class TestMakeModelCommand(_TempCwdTestCase): + # make:model emits a plain Python class with no schema side effects, so + # there is nothing to run against a database -- the generated file content + # is the only meaningful contract to assert. + def test_creates_model_file(self): + os.makedirs("app") + tester = self._tester(MakeModelCommand) + tester.execute("Post") + + output = tester.io.fetch_output() + self.assertIn("Model created:", output) + + content = open(os.path.join("app", "Post.py")).read() + self.assertIn("class Post(Model)", content) + + def test_pep_option_uses_underscore_filename(self): + os.makedirs("app") + tester = self._tester(MakeModelCommand) + tester.execute("BlogPost --pep") + + self.assertTrue(os.path.exists(os.path.join("app", "blog_post.py"))) + + def test_reports_when_model_already_exists(self): + os.makedirs("app") + self._tester(MakeModelCommand).execute("Post") + + tester = self._tester(MakeModelCommand) + tester.execute("Post") + output = tester.io.fetch_output() + self.assertIn('Model "Post" Already Exists', output) + + def test_custom_directory_option(self): + os.makedirs("models") + tester = self._tester(MakeModelCommand) + tester.execute("Post --directory models") + + self.assertTrue(os.path.exists(os.path.join("models", "Post.py"))) + + +class TestMakeSeedCommand(_TempCwdTestCase): + # make:seed emits a Seeder subclass with an empty ``run`` body; it performs + # no database work on its own, so the generated file content is the only + # meaningful contract (seeders executing against a real DB are covered in + # test_db_seed_command.py). + def test_creates_seed_file(self): + os.makedirs("databases/seeders") + tester = self._tester(MakeSeedCommand) + tester.execute("Post") + + output = tester.io.fetch_output() + self.assertIn("Seed file created:", output) + + content = open(os.path.join("databases/seeders", "post_table_seeder.py")).read() + self.assertIn("class PostTableSeeder(Seeder)", content) + + def test_reports_when_seed_already_exists(self): + os.makedirs("databases/seeders") + self._tester(MakeSeedCommand).execute("Post") + + tester = self._tester(MakeSeedCommand) + tester.execute("Post") + output = tester.io.fetch_output() + self.assertIn("already exists.", output) + + +class TestMakeObserverCommand(_TempCwdTestCase): + # make:observer emits a plain observer class with no schema side effects, so + # the generated file content is the only meaningful contract to assert. + def test_creates_observer_file(self): + tester = self._tester(MakeObserverCommand) + tester.execute("Post") + + output = tester.io.fetch_output() + self.assertIn("Observer created:", output) + + content = open(os.path.join("app/observers", "PostObserver.py")).read() + self.assertIn("class PostObserver:", content) + self.assertIn("def created(self, post):", content) + + def test_model_option_controls_variable_and_type(self): + tester = self._tester(MakeObserverCommand) + tester.execute("Audit --model User") + + content = open(os.path.join("app/observers", "AuditObserver.py")).read() + self.assertIn("class AuditObserver:", content) + self.assertIn("def created(self, user):", content) + self.assertIn("User model.", content) + + def test_reports_when_observer_already_exists(self): + self._tester(MakeObserverCommand).execute("Post") + + tester = self._tester(MakeObserverCommand) + tester.execute("Post") + output = tester.io.fetch_output() + self.assertIn('Observer "Post" Already Exists', output) + + +if __name__ == "__main__": + unittest.main() diff --git a/fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py b/fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py index 64f7614e..792922c0 100644 --- a/fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py +++ b/fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py @@ -5,12 +5,18 @@ from fastapi_startkit.masoniteorm.commands.DBMigrateCommand import DBMigrateCommand from fastapi_startkit.masoniteorm.commands.MigrateFreshCommand import MigrateFreshCommand +from fastapi_startkit.masoniteorm.commands.MigrateRefreshCommand import MigrateRefreshCommand from fastapi_startkit.masoniteorm.commands.MigrateResetCommand import MigrateResetCommand from fastapi_startkit.masoniteorm.commands.MigrateRollbackCommand import MigrateRollbackCommand from fastapi_startkit.masoniteorm.commands.MigrateStatusCommand import MigrateStatusCommand from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator +from fastapi_startkit.masoniteorm.models.MigrationModel import MigrationModel + from .fixtures.app import create_app, DB_PATH +CREATE_POSTS = "2026_01_01_000000_create_posts_table" +ADD_BODY_TO_POSTS = "2026_01_01_000001_add_body_to_posts_table" + class TestMigrateCommands(unittest.TestCase): @classmethod @@ -18,6 +24,7 @@ def setUpClass(cls): cls.app = create_app() def setUp(self): + self.schema = self.app.make("db").get_schema_builder() asyncio.run(self._reset_db()) def tearDown(self): @@ -29,6 +36,8 @@ async def _reset_db(self): await db.clear() schema = db.get_schema_builder() for table in await schema.get_all_tables(): + if table.startswith("sqlite_"): + continue await schema.drop_table_if_exists(table) async def _migrate(self): @@ -47,69 +56,112 @@ def _make_command(self, command_class): cmd.set_container(self.app) return cmd - def test_migrate_runs_pending_migrations(self): + def _has_table(self, table): + return asyncio.run(self.schema.has_table(table)) + + def _has_column(self, table, column): + return asyncio.run(self.schema.has_column(table, column)) + + def _ran_migrations(self): + return asyncio.run(self._fetch_ran_migrations()) + + @staticmethod + async def _fetch_ran_migrations(): + rows = await MigrationModel.all() + return sorted((row.migration, row.batch) for row in rows) + + # -- behavior is proven by the real sqlite schema and the migrations + # tracking table, not by console output -- + + def test_migrate_creates_table_and_applies_pending_migrations(self): cmd = self._make_command(DBMigrateCommand) - tester = CommandTester(cmd) - tester.execute("--connection sqlite") - output = tester.io.fetch_output() - self.assertIn("Migrated:", output) - self.assertIn("create_posts_table", output) + CommandTester(cmd).execute("--connection sqlite") + + self.assertTrue(self._has_table("posts")) + self.assertTrue(self._has_column("posts", "title")) + self.assertTrue(self._has_column("posts", "body")) + self.assertEqual( + self._ran_migrations(), + [(CREATE_POSTS, 1), (ADD_BODY_TO_POSTS, 1)], + ) - def test_migrate_reports_nothing_to_migrate(self): + def test_migrate_is_idempotent_when_nothing_pending(self): cmd = self._make_command(DBMigrateCommand) - tester = CommandTester(cmd) - tester.execute("--connection sqlite") - tester.io.fetch_output() + CommandTester(cmd).execute("--connection sqlite") + ran_after_first_run = self._ran_migrations() - tester.execute("--connection sqlite") - output = tester.io.fetch_output() - self.assertIn("Nothing To Migrate!", output) + CommandTester(cmd).execute("--connection sqlite") + + self.assertEqual(self._ran_migrations(), ran_after_first_run) + self.assertTrue(self._has_table("posts")) - def test_status_shows_unran_migrations(self): + def test_status_command_creates_tracking_table_without_running_migrations(self): cmd = self._make_command(MigrateStatusCommand) - tester = CommandTester(cmd) - tester.execute("--connection sqlite") - output = tester.io.fetch_output() - self.assertIn("create_posts_table", output) - self.assertIn("N", output) + CommandTester(cmd).execute("--connection sqlite") - def test_status_shows_ran_migrations(self): + self.assertTrue(self._has_table("migrations")) + self.assertFalse(self._has_table("posts")) + self.assertEqual(self._ran_migrations(), []) + + def test_status_command_leaves_state_unchanged_after_migrate(self): asyncio.run(self._migrate()) + ran_before = self._ran_migrations() cmd = self._make_command(MigrateStatusCommand) - tester = CommandTester(cmd) - tester.execute("--connection sqlite") - output = tester.io.fetch_output() - self.assertIn("create_posts_table", output) - self.assertIn("Y", output) + CommandTester(cmd).execute("--connection sqlite") + + self.assertEqual(self._ran_migrations(), ran_before) + self.assertTrue(self._has_table("posts")) - def test_rollback_rolls_back_last_batch(self): + def test_rollback_drops_last_batch(self): asyncio.run(self._migrate()) cmd = self._make_command(MigrateRollbackCommand) - tester = CommandTester(cmd) - tester.execute("--connection sqlite") - output = tester.io.fetch_output() - self.assertIn("Rolled back:", output) - self.assertIn("create_posts_table", output) + CommandTester(cmd).execute("--connection sqlite") + + self.assertEqual(self._ran_migrations(), []) + self.assertFalse(self._has_table("posts")) def test_reset_rolls_back_all_migrations(self): asyncio.run(self._migrate()) cmd = self._make_command(MigrateResetCommand) - tester = CommandTester(cmd) - tester.execute("--connection sqlite") - output = tester.io.fetch_output() - self.assertIn("Rolled back:", output) - self.assertIn("create_posts_table", output) + CommandTester(cmd).execute("--connection sqlite") + + self.assertEqual(self._ran_migrations(), []) + self.assertFalse(self._has_table("posts")) + + def test_refresh_resets_and_remigrates(self): + asyncio.run(self._migrate()) + + cmd = self._make_command(MigrateRefreshCommand) + CommandTester(cmd).execute("--connection sqlite") + + self.assertTrue(self._has_table("posts")) + self.assertTrue(self._has_column("posts", "body")) + self.assertEqual( + self._ran_migrations(), + [(CREATE_POSTS, 1), (ADD_BODY_TO_POSTS, 1)], + ) def test_fresh_drops_all_tables_and_remigrates(self): asyncio.run(self._migrate()) cmd = self._make_command(MigrateFreshCommand) + CommandTester(cmd).execute("--connection sqlite") + + self.assertTrue(self._has_table("posts")) + self.assertTrue(self._has_column("posts", "body")) + self.assertEqual( + self._ran_migrations(), + [(CREATE_POSTS, 1), (ADD_BODY_TO_POSTS, 1)], + ) + + def test_migrate_command_reports_success_message(self): + # Minimal, secondary output check -- the user-facing message contract, + # not a substitute for the schema-state assertions above. + cmd = self._make_command(DBMigrateCommand) tester = CommandTester(cmd) tester.execute("--connection sqlite") - output = tester.io.fetch_output() - self.assertIn("Dropping all tables", output) - self.assertIn("Migrated:", output) - self.assertIn("create_posts_table", output) + + self.assertIn("Migrated:", tester.io.fetch_output()) diff --git a/fastapi_startkit/tests/masoniteorm/commands/test_seed_commands.py b/fastapi_startkit/tests/masoniteorm/commands/test_seed_commands.py deleted file mode 100644 index c1559d3b..00000000 --- a/fastapi_startkit/tests/masoniteorm/commands/test_seed_commands.py +++ /dev/null @@ -1,87 +0,0 @@ -import unittest - -from cleo.testers.command_tester import CommandTester - -from fastapi_startkit.masoniteorm.commands.DBSeedCommand import DBSeedCommand - -from .fixtures.databases.seeders.recorder import CALLS - -FIXTURE_SEED_PATH = "tests.masoniteorm.commands.fixtures.databases.seeders" - - -class TestDBSeedCommand(unittest.TestCase): - def setUp(self): - CALLS.clear() - - def tearDown(self): - CALLS.clear() - - def _make_tester(self): - command = DBSeedCommand() - return CommandTester(command) - - def test_runs_database_seeder_by_default(self): - tester = self._make_tester() - tester.execute(f"--directory {FIXTURE_SEED_PATH} --connection sqlite") - - output = tester.io.fetch_output() - self.assertIn("Database Seeder seeded!", output) - self.assertEqual(CALLS, [("database", "sqlite")]) - - def test_runs_seeder_for_table_argument(self): - tester = self._make_tester() - tester.execute(f"user --directory {FIXTURE_SEED_PATH} --connection sqlite") - - output = tester.io.fetch_output() - self.assertIn("UserTableSeeder seeded!", output) - self.assertEqual(CALLS, [("user_table", "sqlite")]) - - def test_runs_seeder_for_class_option_without_table_suffix(self): - tester = self._make_tester() - tester.execute(f"--directory {FIXTURE_SEED_PATH} --class SampleSeeder --connection sqlite") - - output = tester.io.fetch_output() - self.assertIn("SampleSeeder seeded!", output) - self.assertEqual(CALLS, [("sample", "sqlite")]) - - def test_runs_seeder_for_class_option_with_table_suffix(self): - tester = self._make_tester() - tester.execute(f"--directory {FIXTURE_SEED_PATH} --class UserTableSeeder --connection sqlite") - - output = tester.io.fetch_output() - self.assertIn("UserTableSeeder seeded!", output) - self.assertEqual(CALLS, [("user_table", "sqlite")]) - - def test_runs_seeder_for_fully_qualified_class_option(self): - tester = self._make_tester() - tester.execute(f"--directory {FIXTURE_SEED_PATH} --class special_seeder.SpecialSeeder --connection sqlite") - - output = tester.io.fetch_output() - self.assertIn("SpecialSeeder seeded!", output) - self.assertEqual(CALLS, [("special", "sqlite")]) - - def test_class_option_takes_precedence_over_table_argument(self): - tester = self._make_tester() - tester.execute(f"user --directory {FIXTURE_SEED_PATH} --class SampleSeeder --connection sqlite") - - output = tester.io.fetch_output() - self.assertIn("SampleSeeder seeded!", output) - self.assertEqual(CALLS, [("sample", "sqlite")]) - - def test_raises_when_seeder_class_cannot_be_found(self): - tester = self._make_tester() - - with self.assertRaises(ValueError): - tester.execute(f"--directory {FIXTURE_SEED_PATH} --class does_not_exist.NopeSeeder --connection sqlite") - - def test_raises_when_database_seeder_missing_from_directory(self): - tester = self._make_tester() - - with self.assertRaises(ValueError): - tester.execute("--directory tests.masoniteorm.commands.fixtures.databases.migrations --connection sqlite") - - def test_uses_default_connection_option(self): - tester = self._make_tester() - tester.execute(f"--directory {FIXTURE_SEED_PATH}") - - self.assertEqual(CALLS, [("database", "default")]) diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py b/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py index 2da6ef0b..b167c038 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py @@ -374,7 +374,7 @@ async def test_has_column(self): sql, _ = mock_select.call_args[0] self.assertEqual( sql, - "SELECT column_name FROM information_schema.columns WHERE table_name='users' and column_name='name'", + "SELECT name FROM pragma_table_info('users') WHERE name='name'", ) async def test_can_have_unsigned_columns(self):