Skip to content

Commit b02b9a1

Browse files
committed
First pass of tests working with MySQL.
1 parent 1fed981 commit b02b9a1

26 files changed

+4338
-857
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,4 @@ test.db-journal
134134

135135
arxiv/db/.autogen_models.py
136136
/arxiv/db/autogen_models.py
137+
/arxiv/db/near_models.py

Makefile

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PROD_DB_PROXY_PORT := 2021
22

33

4-
.PHONY: db-models prod-proxy
4+
.PHONY: db-models prod-proxy test
55

66
default: venv/bin/poetry
77

@@ -22,16 +22,11 @@ venv:
2222

2323
db-models: arxiv/db/autogen_models.py
2424

25-
arxiv/db/autogen_models.py: arxiv/db/autogen_models_patch.diff ~/.arxiv/arxiv-db-prod-readonly
26-
@PROD_ARXIV_DB_URI=`cat ~/.arxiv/arxiv-db-prod-readonly`; . venv/bin/activate && \
27-
poetry run sqlacodegen "$$PROD_ARXIV_DB_URI" --outfile arxiv/db/autogen_models.py && \
28-
poetry run python3 development/patch_db_models.py arxiv/db/autogen_models.py arxiv/db/arxiv_db_metadata.json
29-
patch arxiv/db/autogen_models.py arxiv/db/autogen_models_patch.diff
30-
31-
arxiv/db/autogen_models_patch.diff:
32-
@PROD_ARXIV_DB_URI=`cat ~/.arxiv/arxiv-db-prod-readonly`; . venv/bin/activate && \
33-
poetry run sqlacodegen "$$PROD_ARXIV_DB_URI" --outfile arxiv/db/.autogen_models.py
34-
diff -c arxiv/db/.autogen_models.py arxiv/db/autogen_models.py > arxiv/db/autogen_models_patch.diff
3525

3626
prod-proxy:
3727
/usr/local/bin/cloud-sql-proxy --address 0.0.0.0 --port ${PROD_DB_PROXY_PORT} arxiv-production:us-central1:arxiv-production-rep9 > /dev/null 2>&1 &
28+
29+
test: venv/bin/poetry
30+
venv/bin/poetry run pytest --cov=arxiv.base fourohfour --cov-fail-under=67 arxiv/base fourohfour
31+
TEST_ARXIV_DB_URI=mysql://testuser:[email protected]:13306/testdb venv/bin/poetry run pytest --cov=arxiv --cov-fail-under=25 arxiv
32+
TEST_ARXIV_DB_URI=mysql://testuser:[email protected]:13306/testdb venv/bin/poetry run python tests/run_app_tests.py

arxiv/auth/conftest.py

Lines changed: 149 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import logging
2+
import shlex
13
import shutil
4+
import subprocess
25
import tempfile
3-
from copy import copy
46
from datetime import datetime, UTC
57

68
import pytest
79
import os
810

911
from flask import Flask
10-
from sqlalchemy import create_engine, select
11-
from sqlalchemy.orm import Session, make_transient_to_detached
12+
from sqlalchemy import create_engine, text, CursorResult
13+
from sqlalchemy.orm import Session
14+
from sqlalchemy.pool import NullPool
1215

1316
from .legacy import util
1417
from .legacy.passwords import hash_password
@@ -20,26 +23,106 @@
2023
from ..auth.auth import Auth
2124
from ..auth.auth.middleware import AuthMiddleware
2225

26+
logging.basicConfig(level=logging.INFO)
2327

24-
@pytest.fixture
25-
def classic_db_engine():
26-
db_path = tempfile.mkdtemp()
27-
uri = f'sqlite:///{db_path}/test.db'
28-
engine = create_engine(uri)
29-
util.create_all(engine)
30-
yield engine
31-
shutil.rmtree(db_path)
28+
DB_PORT = 25336
29+
DB_NAME = "testdb"
30+
ROOT_PASSWORD = "rootpassword"
3231

32+
my_sql_cmd = ["mysql", f"--port={DB_PORT}", "-h", "127.0.0.1", "-u", "root", f"--password={ROOT_PASSWORD}",
33+
# "--ssl-mode=DISABLED",
34+
DB_NAME]
3335

36+
def arxiv_base_dir() -> str:
37+
"""
38+
Returns:
39+
"arxiv-base" directory abs path
40+
"""
41+
here = os.path.abspath(__file__)
42+
root_dir = here
43+
for _ in range(3):
44+
root_dir = os.path.dirname(root_dir)
45+
return root_dir
46+
47+
48+
@pytest.fixture(scope="session")
49+
def db_uri(request):
50+
db_type = request.config.getoption("--db")
51+
52+
if db_type == "sqlite":
53+
# db_path = tempfile.mkdtemp()
54+
# uri = f'sqlite:///{db_path}/test.db'
55+
uri = f'sqlite'
56+
elif db_type == "mysql":
57+
# load_arxiv_db_schema.py sets up the docker and load the db schema
58+
loader_py = os.path.join(arxiv_base_dir(), "development", "load_arxiv_db_schema.py")
59+
subprocess.run(["poetry", "run", "python", loader_py, f"--db_name={DB_NAME}", f"--db_port={DB_PORT}",
60+
f"--root_password={ROOT_PASSWORD}"], encoding="utf-8", check=True)
61+
uri = f"mysql://testuser:[email protected]:{DB_PORT}/{DB_NAME}"
62+
else:
63+
raise ValueError(f"Unsupported database dialect: {db_type}")
64+
65+
yield uri
66+
67+
68+
@pytest.fixture(scope="function")
69+
def classic_db_engine(db_uri):
70+
logger = logging.getLogger()
71+
db_path = None
72+
if db_uri.startswith("sqlite"):
73+
db_path = tempfile.mkdtemp()
74+
uri = f'sqlite:///{db_path}/test.db'
75+
db_engine = create_engine(uri)
76+
util.create_arxiv_db_schema(db_engine)
77+
else:
78+
conn_args = {}
79+
# conn_args["ssl"] = None
80+
db_engine = create_engine(db_uri, connect_args=conn_args, poolclass=NullPool)
81+
82+
# Clean up the tables to real fresh
83+
targets = []
84+
with db_engine.connect() as connection:
85+
tables = [row[0] for row in connection.execute(text("SHOW TABLES"))]
86+
for table_name in tables:
87+
counter: CursorResult = connection.execute(text(f"select count(*) from {table_name}"))
88+
count = counter.first()[0]
89+
if count and int(count):
90+
targets.append(table_name)
91+
connection.invalidate()
92+
93+
if targets:
94+
statements = [ "SET FOREIGN_KEY_CHECKS = 0;"] + [f"TRUNCATE TABLE {table_name};" for table_name in targets] + ["SET FOREIGN_KEY_CHECKS = 1;"]
95+
debug_sql = "SHOW PROCESSLIST;\nSELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;\n"
96+
sql = "\n".join(statements)
97+
mysql = subprocess.Popen(my_sql_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, encoding="utf-8")
98+
try:
99+
logger.info(debug_sql + sql)
100+
out, err = mysql.communicate(sql, timeout=9999)
101+
if out:
102+
logger.info(out)
103+
if err:
104+
logger.info(err)
105+
except Exception as exc:
106+
logger.error(f"BOO: {str(exc)}", exc_info=True)
107+
108+
util.bootstrap_arxiv_db(db_engine)
109+
110+
yield db_engine
111+
112+
if db_path:
113+
shutil.rmtree(db_path)
114+
else:
115+
with db_engine.connect() as connection:
116+
danglings: CursorResult = connection.execute(text("select id from information_schema.processlist where user = 'testuser';")).all()
117+
connection.invalidate()
118+
119+
if danglings:
120+
kill_conn = "\n".join([ f"kill {id[0]};" for id in danglings ])
121+
logger.info(kill_conn)
122+
mysql = subprocess.Popen(my_sql_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, encoding="utf-8")
123+
mysql.communicate(kill_conn)
124+
db_engine.dispose()
34125

35-
@pytest.fixture
36-
def classic_db_engine():
37-
db_path = tempfile.mkdtemp()
38-
uri = f'sqlite:///{db_path}/test.db'
39-
engine = create_engine(uri)
40-
util.create_all(engine)
41-
yield engine
42-
shutil.rmtree(db_path)
43126

44127

45128
@pytest.fixture
@@ -90,7 +173,7 @@ def foouser(mocker):
90173
issued_when=n,
91174
issued_to='127.0.0.1',
92175
remote_host='foohost.foo.com',
93-
session_id=0
176+
session_id=1
94177
)
95178
user.tapir_nicknames = nick
96179
user.tapir_passwords = password
@@ -100,8 +183,17 @@ def foouser(mocker):
100183

101184
@pytest.fixture
102185
def db_with_user(classic_db_engine, foouser):
103-
# just combines classic_db_engine and foouser
104-
with Session(classic_db_engine, expire_on_commit=False) as session:
186+
try:
187+
_load_test_user(classic_db_engine, foouser)
188+
except Exception as e:
189+
pass
190+
yield classic_db_engine
191+
192+
193+
def _load_test_user(db_engine, foouser):
194+
# just combines db_engine and foouser
195+
with Session(db_engine) as session:
196+
105197
user = models.TapirUser(
106198
user_id=foouser.user_id,
107199
first_name=foouser.first_name,
@@ -117,6 +209,15 @@ def db_with_user(classic_db_engine, foouser):
117209
flag_banned=foouser.flag_banned,
118210
tracking_cookie=foouser.tracking_cookie,
119211
)
212+
session.add(user)
213+
session.commit()
214+
215+
# Make sure the ID is correct. If you are using mysql with different auto-increment. you may get an different id
216+
# However, domain.User's user_id is str, and the db/models User model user_id is int.
217+
# wish they match but since tapir's user id came from auto-increment id which has to be int, I guess
218+
# "it is what it is".
219+
assert str(foouser.user_id) == str(user.user_id)
220+
120221
nick = models.TapirNickname(
121222
nickname=foouser.tapir_nicknames.nickname,
122223
user_id=foouser.tapir_nicknames.user_id,
@@ -126,11 +227,30 @@ def db_with_user(classic_db_engine, foouser):
126227
policy=foouser.tapir_nicknames.policy,
127228
flag_primary=foouser.tapir_nicknames.flag_primary,
128229
)
230+
session.add(nick)
231+
session.commit()
232+
129233
password = models.TapirUsersPassword(
130234
user_id=foouser.user_id,
131235
password_storage=foouser.tapir_passwords.password_storage,
132236
password_enc=foouser.tapir_passwords.password_enc,
133237
)
238+
session.add(password)
239+
session.commit()
240+
241+
with Session(db_engine) as session:
242+
tapir_session_1 = models.TapirSession(
243+
session_id = foouser.tapir_tokens.session_id,
244+
user_id = foouser.user_id,
245+
last_reissue = 0,
246+
start_time = 0,
247+
end_time = 0
248+
)
249+
session.add(tapir_session_1)
250+
session.commit()
251+
assert foouser.tapir_tokens.session_id == tapir_session_1.session_id
252+
253+
with Session(db_engine) as session:
134254
token = models.TapirPermanentToken(
135255
user_id=foouser.user_id,
136256
secret=foouser.tapir_tokens.secret,
@@ -140,20 +260,14 @@ def db_with_user(classic_db_engine, foouser):
140260
remote_host=foouser.tapir_tokens.remote_host,
141261
session_id=foouser.tapir_tokens.session_id,
142262
)
143-
session.add(user)
144263
session.add(token)
145-
session.add(password)
146-
session.add(nick)
147264
session.commit()
148-
session.close()
149265

150-
foouser.tapir_nicknames.nickname
151-
yield classic_db_engine
152266

153267
@pytest.fixture
154268
def db_configed(db_with_user):
155-
configure_db_engine(db_with_user,None)
156-
269+
db_engine, _ = configure_db_engine(db_with_user,None)
270+
yield None
157271

158272
@pytest.fixture
159273
def app(db_with_user):
@@ -169,3 +283,8 @@ def app(db_with_user):
169283
@pytest.fixture
170284
def request_context(app):
171285
yield app.test_request_context()
286+
287+
288+
def pytest_addoption(parser):
289+
parser.addoption("--db", action="store", default="sqlite",
290+
help="Database type to test against (sqlite/mysql)")

arxiv/auth/legacy/tests/test_accounts.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,22 @@ def get_user(session, user_id):
3131
def test_with_nonexistant_user_w_app(app):
3232
"""There is no user with the passed username."""
3333
with app.app_context():
34-
assert not accounts.does_username_exist('baruser')
34+
assert not accounts.does_username_exist('baruser')
3535

36-
def test_with_nonexistant_user_wo_app(db_configed):
36+
def test_with_nonexistant_user_wo_app(app):
3737
"""There is no user with the passed username."""
38-
assert not accounts.does_username_exist('baruser')
38+
with app.app_context():
39+
assert not accounts.does_username_exist('baruser')
3940

4041
def test_with_existant_user_w_app(app):
4142
"""There is a user with the passed username."""
4243
with app.app_context():
4344
assert accounts.does_username_exist('foouser')
4445

45-
def test_with_existant_user_wo_app(db_configed):
46+
def test_with_existant_user_wo_app(app):
4647
"""There is a user with the passed username."""
47-
assert accounts.does_username_exist('foouser')
48+
with app.app_context():
49+
assert accounts.does_username_exist('foouser')
4850

4951
def test_email(app):
5052
"""There is no user with the passed email."""

arxiv/auth/legacy/tests/test_bootstrap.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
"""Test the legacy integration with synthetic data."""
22

3-
import os
43
import shutil
5-
import sys
64
import tempfile
75
from typing import Tuple
86
from unittest import TestCase
97
from flask import Flask
10-
import locale
8+
import pytest
119

12-
from typing import List
1310
import random
14-
from datetime import datetime
15-
from pytz import timezone, UTC
11+
from pytz import timezone
1612
from mimesis import Person, Internet, Datetime, locales
1713

18-
from sqlalchemy import select, func
14+
from sqlalchemy import select
1915

2016
import arxiv.db
2117
from arxiv.db import transaction
@@ -82,21 +78,22 @@ def setUp(self):
8278
papers_to_endorse=3
8379
))
8480

85-
for category in definitions.CATEGORIES_ACTIVE.keys():
86-
if '.' in category:
87-
archive, subject_class = category.split('.', 1)
88-
else:
89-
archive, subject_class = category, ''
90-
91-
with transaction() as session:
92-
#print(f"arch: {archive} sc: {subject_class}")
93-
session.add(models.Category(
94-
archive=archive,
95-
subject_class=subject_class,
96-
definitive=1,
97-
active=1,
98-
endorsement_domain='test_domain_bootstrap'
99-
))
81+
# categories are loaded already
82+
# for category in definitions.CATEGORIES_ACTIVE.keys():
83+
# if '.' in category:
84+
# archive, subject_class = category.split('.', 1)
85+
# else:
86+
# archive, subject_class = category, ''
87+
#
88+
# with transaction() as session:
89+
# #print(f"arch: {archive} sc: {subject_class}")
90+
# session.add(models.Category(
91+
# archive=archive,
92+
# subject_class=subject_class,
93+
# definitive=1,
94+
# active=1,
95+
# endorsement_domain='test_domain_bootstrap'
96+
# ))
10097

10198
COUNT = 100
10299

@@ -250,6 +247,8 @@ def setUp(self):
250247
def tearDown(self):
251248
shutil.rmtree(self.db_path)
252249

250+
@pytest.mark.skipif(lambda request: request.config.getoption("--db") == "mysql",
251+
reason="is set up for sqlite3 only")
253252
def test_authenticate_and_use_session(self):
254253
"""Attempt to authenticate users and create/load auth sessions."""
255254
with self.app.app_context():

0 commit comments

Comments
 (0)