Skip to content

Commit 01eec0e

Browse files
committed
tests: [WIP] Test that foreign keys are enabled in sqlite
1 parent 8aeb783 commit 01eec0e

File tree

5 files changed

+86
-2
lines changed

5 files changed

+86
-2
lines changed

grader_service/autograding/git_manager.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ def _run_git(self, command: list[str], cwd: Optional[str]) -> None:
159159
self.log.error(e)
160160
raise
161161

162-
# TODO: Can I decorate executable_validator with both "git_executable" and "convert_executable"?
163162
@validate("git_executable")
164163
def _validate_executable(self, proposal):
165164
return executable_validator(proposal)

grader_service/orm/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
2828
dbapi_connection.autocommit = True
2929

3030
cursor = dbapi_connection.cursor()
31+
# Note: this is a SQLite-specific pragma
3132
cursor.execute("PRAGMA foreign_keys=ON")
3233
cursor.close()
3334

grader_service/tests/conftest.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import pytest
1111
from alembic import config
1212
from alembic.command import upgrade
13+
from sqlalchemy import event
14+
from sqlalchemy.engine import Engine
1315
from sqlalchemy.orm import Session, scoped_session, sessionmaker
1416

1517
from grader_service import GraderService, handlers
@@ -18,7 +20,51 @@
1820
from grader_service.orm import User
1921
from grader_service.registry import HandlerPathRegistry
2022
from grader_service.server import GraderServer
21-
from grader_service.tests.handlers.db_util import insert_assignments, insert_lectures
23+
from grader_service.tests.handlers.db_util import (
24+
insert_assignments,
25+
insert_default_user,
26+
insert_lectures,
27+
)
28+
29+
30+
@pytest.fixture(scope="function")
31+
def set_database_type_to_sqlite():
32+
"""
33+
Set the DATABASE_TYPE env var to "sqlite" and register a listener
34+
which enables foreign keys support for SQLite database.
35+
Unset the var after the test runs.
36+
37+
This is a hack, because normally `DATABASE_TYPE` is not set when tests run.
38+
It also cannot be set to "sqlite" by default, because we have some tests
39+
running on PostgreSQL, where executing the sqlite pragma would cause an error.
40+
41+
Outside the test setting, the DATABASE_TYPE environment variable is set, and
42+
the event listener is registered in grader_service/orm/base.py.
43+
"""
44+
os.environ["DATABASE_TYPE"] = "sqlite"
45+
46+
# Original code taken from the SQLAlchemy documentation, here slightly adjusted:
47+
# https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#foreign-key-support
48+
@event.listens_for(Engine, "connect")
49+
def set_sqlite_pragma(dbapi_connection, connection_record):
50+
database_type = os.getenv("DATABASE_TYPE")
51+
if database_type == "sqlite":
52+
# the sqlite3 driver will not set PRAGMA foreign_keys
53+
# if autocommit=False; set to True temporarily
54+
ac = dbapi_connection.autocommit
55+
dbapi_connection.autocommit = True
56+
57+
cursor = dbapi_connection.cursor()
58+
# Note: this is a SQLite-specific pragma
59+
cursor.execute("PRAGMA foreign_keys=ON")
60+
cursor.close()
61+
62+
# restore previous autocommit setting
63+
dbapi_connection.autocommit = ac
64+
65+
yield
66+
# Unset the variable for other tests, which may use a different database type
67+
os.environ.pop("DATABASE_TYPE")
2268

2369

2470
@pytest.fixture(scope="function")
@@ -68,6 +114,7 @@ def sql_alchemy_sessionmaker(db_test_config):
68114
upgrade(db_test_config, "head")
69115
insert_lectures(engine)
70116
insert_assignments(engine)
117+
insert_default_user(engine)
71118
yield session_maker
72119

73120

grader_service/tests/handlers/db_util.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ def insert_student(ex: Engine, username: str, lecture_id: int) -> User:
148148
return user
149149

150150

151+
def insert_default_user(ex: Engine) -> None:
152+
session: Session = sessionmaker(ex)()
153+
session.add(User(name="ubuntu", display_name="ubuntu"))
154+
session.commit()
155+
156+
151157
def create_user_submission_with_repo(
152158
engine: Engine, gitbase_dir: Path, student: User, assignment_id: int, lecture_code: str
153159
) -> Submission:

grader_service/tests/handlers/test_assignment_handler.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,46 @@
77
from http import HTTPStatus
88

99
import pytest
10+
from sqlalchemy.exc import IntegrityError
1011
from tornado.httpclient import HTTPClientError
1112

1213
from grader_service.api.models.assignment import Assignment
1314
from grader_service.api.models.assignment_settings import AssignmentSettings
15+
from grader_service.orm import Assignment as AssignmentORM
16+
from grader_service.orm import Submission
1417
from grader_service.server import GraderServer
1518

1619
from .db_util import insert_assignments, insert_submission
1720

1821

22+
def test_foreign_key_constraints_in_sqlite(
23+
set_database_type_to_sqlite, sql_alchemy_engine, sql_alchemy_sessionmaker, default_user
24+
):
25+
"""Make sure the FK constraints are enabled in SQLite on engine connection."""
26+
27+
# Note: tests use an sqlite db by default (see the `alembic_test.ini` file),
28+
# but `DATABASE_TYPE` environment variable is not set. We use the fixture
29+
# `set_database_type_to_sqlite` to set it to "sqlite" for this test.
30+
31+
a_id = 1 # This assignment exists in the test database
32+
session = sql_alchemy_sessionmaker()
33+
engine = session.get_bind()
34+
sub = insert_submission(engine, a_id, "ubuntu", 1)
35+
36+
session = sql_alchemy_sessionmaker()
37+
38+
with pytest.raises(IntegrityError, match="FOREIGN KEY constraint failed"):
39+
# Try to delete an existing assignment with a submission
40+
session.query(AssignmentORM).filter(AssignmentORM.id == a_id).delete()
41+
session.commit()
42+
43+
assign = session.query(AssignmentORM).filter(AssignmentORM.id == a_id).one_or_none()
44+
sub_2 = session.query(Submission).filter(Submission.id == sub.id).one_or_none()
45+
assert assign is not None
46+
assert sub_2.id == assign.submissions[0].id
47+
session.close()
48+
49+
1950
async def test_get_assignments(
2051
app: GraderServer,
2152
service_base_url,

0 commit comments

Comments
 (0)