Skip to content

Commit 4409f2a

Browse files
authored
Merge pull request #275 from terrycain/docker_test_fixtures
Docker test fixtures
2 parents 5ad7e75 + fba6b35 commit 4409f2a

14 files changed

+305
-9
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ env:
99
- PYTHONASYNCIODEBUG=1
1010
- PYTHONASYNCIODEBUG=
1111

12+
services:
13+
- docker
14+
1215
matrix:
1316
include:
1417
- python: 3.6

requirements-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ ipython==6.2.1
55
pytest==3.4.2
66
pytest-cov==2.5.1
77
pytest-sugar==0.9.1
8+
PyMySQL>=0.7.5
9+
docker==3.1.4
810
sphinx==1.7.1
911
sphinxcontrib-asyncio==0.2.0
1012
sqlalchemy==1.2.5

tests/base.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def _connect_all(self):
3030
def setUp(self):
3131
super(AIOPyMySQLTestCase, self).setUp()
3232
self.host = os.environ.get('MYSQL_HOST', 'localhost')
33-
self.port = os.environ.get('MYSQL_PORT', 3306)
33+
self.port = int(os.environ.get('MYSQL_PORT', 3306))
3434
self.user = os.environ.get('MYSQL_USER', 'root')
3535
self.db = os.environ.get('MYSQL_DB', 'test_pymysql')
3636
self.other_db = os.environ.get('OTHER_MYSQL_DB', 'test_pymysql2')
@@ -47,7 +47,7 @@ def tearDown(self):
4747

4848
@asyncio.coroutine
4949
def connect(self, host=None, user=None, password=None,
50-
db=None, use_unicode=True, no_delay=None, **kwargs):
50+
db=None, use_unicode=True, no_delay=None, port=None, **kwargs):
5151
if host is None:
5252
host = self.host
5353
if user is None:
@@ -56,16 +56,20 @@ def connect(self, host=None, user=None, password=None,
5656
password = self.password
5757
if db is None:
5858
db = self.db
59+
if port is None:
60+
port = self.port
5961
conn = yield from aiomysql.connect(loop=self.loop, host=host,
6062
user=user, password=password,
6163
db=db, use_unicode=use_unicode,
62-
no_delay=no_delay, **kwargs)
64+
no_delay=no_delay, port=port,
65+
**kwargs)
6366
self.addCleanup(conn.close)
6467
return conn
6568

6669
@asyncio.coroutine
6770
def create_pool(self, host=None, user=None, password=None,
68-
db=None, use_unicode=True, no_delay=None, **kwargs):
71+
db=None, use_unicode=True, no_delay=None,
72+
port=None, **kwargs):
6973
if host is None:
7074
host = self.host
7175
if user is None:
@@ -74,9 +78,12 @@ def create_pool(self, host=None, user=None, password=None,
7478
password = self.password
7579
if db is None:
7680
db = self.db
81+
if port is None:
82+
port = self.port
7783
pool = yield from aiomysql.create_pool(loop=self.loop, host=host,
7884
user=user, password=password,
7985
db=db, use_unicode=use_unicode,
80-
no_delay=no_delay, **kwargs)
86+
no_delay=no_delay, port=port,
87+
**kwargs)
8188
self.addCleanup(pool.close)
8289
return pool

tests/conftest.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import asyncio
22
import gc
33
import os
4+
import ssl
5+
import socket
46
import sys
7+
import time
8+
import uuid
9+
10+
from docker import APIClient
511

612
import aiomysql
13+
import pymysql
714
import pytest
815

916

@@ -14,11 +21,36 @@
1421
uvloop = None
1522

1623

24+
@pytest.fixture(scope='session')
25+
def unused_port():
26+
def f():
27+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
28+
s.bind(('127.0.0.1', 0))
29+
return s.getsockname()[1]
30+
return f
31+
32+
1733
def pytest_generate_tests(metafunc):
1834
if 'loop_type' in metafunc.fixturenames:
1935
loop_type = ['asyncio', 'uvloop'] if uvloop else ['asyncio']
2036
metafunc.parametrize("loop_type", loop_type)
2137

38+
# if 'mysql_tag' in metafunc.fixturenames:
39+
# tags = set(metafunc.config.option.mysql_tag)
40+
# if not tags:
41+
# tags = ['5.7']
42+
# elif 'all' in tags:
43+
# tags = ['5.6', '5.7', '8.0']
44+
# else:
45+
# tags = list(tags)
46+
# metafunc.parametrize("mysql_tag", tags, scope='session')
47+
48+
49+
# This is here unless someone fixes the generate_tests bit
50+
@pytest.yield_fixture(scope='session')
51+
def mysql_tag():
52+
return '5.6'
53+
2254

2355
@pytest.yield_fixture
2456
def loop(request, loop_type):
@@ -77,10 +109,19 @@ def pytest_ignore_collect(path, config):
77109
return True
78110

79111

112+
def pytest_addoption(parser):
113+
parser.addoption("--mysql_tag", action="append", default=[],
114+
help=("MySQL server versions. "
115+
"May be used several times. "
116+
"Available values: 5.6, 5.7, 8.0, all"))
117+
parser.addoption("--no-pull", action="store_true", default=False,
118+
help="Don't perform docker images pulling")
119+
120+
80121
@pytest.fixture
81122
def mysql_params():
82123
params = {"host": os.environ.get('MYSQL_HOST', 'localhost'),
83-
"port": os.environ.get('MYSQL_PORT', 3306),
124+
"port": int(os.environ.get('MYSQL_PORT', 3306)),
84125
"user": os.environ.get('MYSQL_USER', 'root'),
85126
"db": os.environ.get('MYSQL_DB', 'test_pymysql'),
86127
"password": os.environ.get('MYSQL_PASSWORD', ''),
@@ -164,3 +205,108 @@ def _register_table(table_name):
164205
# TODO: probably this is not safe code
165206
sql = "DROP TABLE IF EXISTS {};".format(t)
166207
loop.run_until_complete(cursor.execute(sql))
208+
209+
210+
@pytest.fixture(scope='session')
211+
def session_id():
212+
"""Unique session identifier, random string."""
213+
return str(uuid.uuid4())
214+
215+
216+
@pytest.fixture(scope='session')
217+
def docker():
218+
return APIClient(version='auto')
219+
220+
221+
@pytest.fixture(scope='session')
222+
def mysql_server(unused_port, docker, session_id, mysql_tag, request):
223+
if not request.config.option.no_pull:
224+
docker.pull('mysql:{}'.format(mysql_tag))
225+
226+
# bound IPs do not work on OSX
227+
host = "127.0.0.1"
228+
host_port = unused_port()
229+
230+
# As TLS is optional, might as well always configure it
231+
ssl_directory = os.path.join(os.path.dirname(__file__),
232+
'ssl_resources', 'ssl')
233+
ca_file = os.path.join(ssl_directory, 'ca.pem')
234+
tls_cnf = os.path.join(os.path.dirname(__file__),
235+
'ssl_resources', 'tls.cnf')
236+
237+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
238+
ctx.check_hostname = False
239+
ctx.load_verify_locations(cafile=ca_file)
240+
# ctx.verify_mode = ssl.CERT_NONE
241+
242+
container_args = dict(
243+
image='mysql:{}'.format(mysql_tag),
244+
name='aiomysql-test-server-{}-{}'.format(mysql_tag, session_id),
245+
ports=[3306],
246+
detach=True,
247+
host_config=docker.create_host_config(
248+
port_bindings={3306: (host, host_port)},
249+
binds={
250+
ssl_directory: {'bind': '/etc/mysql/ssl', 'mode': 'ro'},
251+
tls_cnf: {'bind': '/etc/mysql/conf.d/tls.cnf', 'mode': 'ro'},
252+
}
253+
),
254+
environment={'MYSQL_ROOT_PASSWORD': 'rootpw'}
255+
)
256+
257+
container = docker.create_container(**container_args)
258+
259+
try:
260+
docker.start(container=container['Id'])
261+
262+
# MySQL restarts at least 4 times in the container before its ready
263+
time.sleep(10)
264+
265+
server_params = {
266+
'host': host,
267+
'port': host_port,
268+
'user': 'root',
269+
'password': 'rootpw',
270+
'ssl': ctx
271+
}
272+
delay = 0.001
273+
for i in range(100):
274+
try:
275+
connection = pymysql.connect(
276+
db='mysql',
277+
charset='utf8mb4',
278+
cursorclass=pymysql.cursors.DictCursor,
279+
**server_params)
280+
281+
with connection.cursor() as cursor:
282+
cursor.execute("SHOW VARIABLES LIKE '%ssl%';")
283+
284+
result = cursor.fetchall()
285+
result = {item['Variable_name']:
286+
item['Value'] for item in result}
287+
288+
assert result['have_ssl'] == "YES", \
289+
"SSL Not Enabled on docker'd MySQL"
290+
291+
cursor.execute("SHOW STATUS LIKE '%Ssl_version%'")
292+
293+
result = cursor.fetchone()
294+
# As we connected with TLS, it should start with that :D
295+
assert result['Value'].startswith('TLS'), \
296+
"Not connected to the database with TLS"
297+
298+
break
299+
except Exception as err:
300+
time.sleep(delay)
301+
delay *= 2
302+
else:
303+
pytest.fail("Cannot start MySQL server")
304+
305+
container['host'] = host
306+
container['port'] = host_port
307+
container['conn_params'] = server_params
308+
309+
yield container
310+
finally:
311+
docker.kill(container=container['Id'])
312+
docker.remove_container(container['Id'])

tests/sa/test_sa_connection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def setUp(self):
2222
self.loop = asyncio.new_event_loop()
2323
asyncio.set_event_loop(None)
2424
self.host = os.environ.get('MYSQL_HOST', 'localhost')
25-
self.port = os.environ.get('MYSQL_PORT', 3306)
25+
self.port = int(os.environ.get('MYSQL_PORT', 3306))
2626
self.user = os.environ.get('MYSQL_USER', 'root')
2727
self.db = os.environ.get('MYSQL_DB', 'test_pymysql')
2828
self.password = os.environ.get('MYSQL_PASSWORD', '')
@@ -37,6 +37,7 @@ def connect(self, **kwargs):
3737
password=self.password,
3838
host=self.host,
3939
loop=self.loop,
40+
port=self.port,
4041
**kwargs)
4142
yield from conn.autocommit(True)
4243
cur = yield from conn.cursor()

tests/sa/test_sa_engine.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def setUp(self):
1919
self.loop = asyncio.new_event_loop()
2020
asyncio.set_event_loop(None)
2121
self.host = os.environ.get('MYSQL_HOST', 'localhost')
22-
self.port = os.environ.get('MYSQL_PORT', 3306)
22+
self.port = int(os.environ.get('MYSQL_PORT', 3306))
2323
self.user = os.environ.get('MYSQL_USER', 'root')
2424
self.db = os.environ.get('MYSQL_DB', 'test_pymysql')
2525
self.password = os.environ.get('MYSQL_PASSWORD', '')
@@ -38,6 +38,7 @@ def make_engine(self, use_loop=True, **kwargs):
3838
user=self.user,
3939
password=self.password,
4040
host=self.host,
41+
port=self.port,
4142
loop=self.loop,
4243
minsize=10,
4344
**kwargs))
@@ -46,6 +47,7 @@ def make_engine(self, use_loop=True, **kwargs):
4647
user=self.user,
4748
password=self.password,
4849
host=self.host,
50+
port=self.port,
4951
minsize=10,
5052
**kwargs))
5153

tests/sa/test_sa_transaction.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def setUp(self):
3333
self.loop = asyncio.new_event_loop()
3434
asyncio.set_event_loop(None)
3535
self.host = os.environ.get('MYSQL_HOST', 'localhost')
36-
self.port = os.environ.get('MYSQL_PORT', 3306)
36+
self.port = int(os.environ.get('MYSQL_PORT', 3306))
3737
self.user = os.environ.get('MYSQL_USER', 'root')
3838
self.db = os.environ.get('MYSQL_DB', 'test_pymysql')
3939
self.password = os.environ.get('MYSQL_PASSWORD', '')
@@ -58,6 +58,7 @@ def connect(self, **kwargs):
5858
user=self.user,
5959
password=self.password,
6060
host=self.host,
61+
port=self.port,
6162
loop=self.loop,
6263
**kwargs)
6364
# TODO: fix this, should autocommit be enabled by default?

tests/ssl_resources/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# MySQL TLS Stuff
2+
3+
This folder contains some resources to be mounted into a mysql container to support TLS
4+
5+
Most of the instructions were taken from here https://dev.mysql.com/doc/refman/5.7/en/creating-ssl-files-using-openssl.html
6+
7+
# Generating certificates
8+
```bash
9+
openssl genrsa 2048 > ca-key.pem
10+
openssl req -new -x509 -nodes -days 3600 -key ca-key.pem -out ca.pem
11+
12+
openssl req -newkey rsa:2048 -days 3600 -nodes -keyout server-key.pem -out server-req.pem
13+
openssl rsa -in server-key.pem -out server-key.pem
14+
openssl x509 -req -in server-req.pem -days 3600 -CA ca.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem
15+
```
16+
The current files under `ssl/` have the default values provided by openssl.
17+
18+
# MySQL Config
19+
MySQL imports all `.cnf` files under `/etc/mysql/conf.d` so a `tls.cnf` is placed in there referencing the SSL CA
20+
cert and server cert and key. The entire `ssl/` directory should be mounted on the container to `/etc/mysql/ssl/`

tests/ssl_resources/ssl/ca-key.pem

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEA4QGo8oVPe8HCIqfJXYVxZbHAZDVlMK/e6ypQM463B8lNg6Ce
3+
dd0QfAVIs3wuXtRTVD6ELVFDrmcg/eBjM00Idh2GcI0ojDAPcECBvvk0SWYrUBFn
4+
bou5R1tPwCb8yY601dNExteT61egVhTQd8Vz6NjN9tkMqrpnZ3pn86QqnFWuDOvJ
5+
5YkOAHOjXipkb8ba+Y4Jl50N6KZH2vrHwdWiO/DYQJLuvoOtYI6a+AXNH+ZSNvid
6+
Vz9nb3taUQcNxUlYtTByPfnU9wo/dzDFpNKvC5yScPvh2AmJUrEKNEYkFY/YbqLc
7+
iCXOoBfh1EHqmR2EASjanr+LgwTJfXoX3bQeQQIDAQABAoIBAQDLQ3igPhXjstHy
8+
BKlANwCN4dnvrNzQ8s/qmbsCGHb4Lb48nqkHyMDPiOZ4XkJ1oFH21NMLLVJ7Buci
9+
8cYr3fc63MlKe/qZSgFoYp3TK8U0WXvfRRmvH8Is2CxfZdkPLD/ouoZzKuSRwgMy
10+
QHNi/5kKTHEkAkgTI3muXUHzM+baeknisEqXqCib1yY4FfX2Vnip1dTbbj1gEbGu
11+
vgFM67uXsoKeQ8ykTF5ZUDIphP+tFWgLMZA2L3iTgRSZJztWwxUMa9NZ8erxy25h
12+
HPntC5OELndKEbO+su7wCMkw8w2cNA1V6yyQlwSoIRD1fhit/v4hOmvofTpoikXH
13+
DaCJjgwBAoGBAP84Wq8nrp+79BWRX/qsivERtkGnfjFIDbWBhj70az/CgCV8OWWu
14+
td6VEEl/Gk7IHsrmOySF0EMYYeMoc5WQKMZbb+1x2m7ovxEwS/UGwNGdtZswyOJw
15+
y0LKI6dJPQUiZm/O2m0w1zs6/CvvIfu9QOUTxyvwub4lM8UW/gDy79URAoGBAOGx
16+
q83cTMHKY32dNU/IVaMpw9OxojAWpWXYOEqyV7hM2+gw/lrdBKRyWwB3l3smwet1
17+
FvKINCZ0bTIRbz/UsNtXp8lISTvw5bQhGsQEYx6ncBeBeHN50zSVoR2xBfqu3pQ4
18+
G5V/UI82hba7QUDXkuMJ2T7dcZixLk1vp3y+LvYxAoGBANkGYc7J7qs0F6Xzfeta
19+
p7fA+PuxYxSjEc1DfBWyoDSSv4egr+owe8Tveu8UnxlZAR5GUwqGo4c6h5qzvj3z
20+
XUj3XiFKjJV9Y2RJbn3IpVRaSKDUBi7P/XgpDdJl6/aevv7aplDtlEhwqxjs+zfn
21+
QfTKMbbCuB/h4Lj7CTljW+ARAoGAchB7hgVK/b4t3jRv1yymq1nWUM077RXk7b4D
22+
ZS0RTGH72jO4uW9ugzYQbAIFGwaRh1CcEmNoB+9bqKxLD3WNFK4ObJoN+S9cyFba
23+
0iptdfalnhufJq1xYugkj38CSJnMgBiDSGEZ8+dYWOv2pLDO2dQGadE9MjCJ+DTv
24+
7wmnbmECgYBnkVNH0wOvdMuT0vovm18zqtH04PmQGg5JXgQjtpn/6iC9BjRaF2Nz
25+
Jj9arX00KxIO5vkzbyt5ht5fzk4dpXoG5ozOqnqbj6WNEfgJDeyCkqo1gZOMZSQ/
26+
YPjKGL7rhXbE/FsNEH90nG0NpIAk1ibtD9sYn6LBBYJcuauegOgE7g==
27+
-----END RSA PRIVATE KEY-----

tests/ssl_resources/ssl/ca.pem

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDXTCCAkWgAwIBAgIJAP2JtVGC0ZPKMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
3+
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
4+
aWRnaXRzIFB0eSBMdGQwHhcNMTgwMzI5MjEzMzA1WhcNMjgwMjA1MjEzMzA1WjBF
5+
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
6+
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
7+
CgKCAQEA4QGo8oVPe8HCIqfJXYVxZbHAZDVlMK/e6ypQM463B8lNg6Cedd0QfAVI
8+
s3wuXtRTVD6ELVFDrmcg/eBjM00Idh2GcI0ojDAPcECBvvk0SWYrUBFnbou5R1tP
9+
wCb8yY601dNExteT61egVhTQd8Vz6NjN9tkMqrpnZ3pn86QqnFWuDOvJ5YkOAHOj
10+
Xipkb8ba+Y4Jl50N6KZH2vrHwdWiO/DYQJLuvoOtYI6a+AXNH+ZSNvidVz9nb3ta
11+
UQcNxUlYtTByPfnU9wo/dzDFpNKvC5yScPvh2AmJUrEKNEYkFY/YbqLciCXOoBfh
12+
1EHqmR2EASjanr+LgwTJfXoX3bQeQQIDAQABo1AwTjAdBgNVHQ4EFgQUyNN4RMiv
13+
zhCNNvMta2kOoiw/SXswHwYDVR0jBBgwFoAUyNN4RMivzhCNNvMta2kOoiw/SXsw
14+
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAyAiIqfKoemjnJmoMqcrg
15+
N/7QhC9P79rzwKQOEFkCFWJFuGiOD6GhPPCu/9ssrl5vBEwDLdl0V4AeGq8DeKV+
16+
SNON1o6Y6uUAiX5uYK5Asv0avQUu7SS+uvE3YhTELjbvp4vqdLCUjBqq4KZoEA+F
17+
4hXCltPVdOItztzAd7hgktYrkJeDA1M7sZHTv26HaO6vJ0trUdb4tvqzShzMCvN/
18+
2s9ZJAVBZL77Px40yUPiK6cjpq5fGcUen0zBumymBRFOb8ykvq7azUdjk6sz65Vb
19+
Q3kgsKGBHjVOPfXf00YWvgG3NePX3FsBEHtYbBDSD6k+aXQ1WfOUB8mtdwjyEnzD
20+
7w==
21+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)