From e9571eaa4ff59d2c11cefe89191daa19312eb33c Mon Sep 17 00:00:00 2001 From: heartsucker Date: Tue, 4 Dec 2018 15:34:23 +0100 Subject: [PATCH 1/9] added test_alembic.py (taken from SecureDrop core) --- tests/conftest.py | 29 +++++++- tests/test_alembic.py | 169 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 tests/test_alembic.py diff --git a/tests/conftest.py b/tests/conftest.py index c2f861e97..cc3b439aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import tempfile import pytest +from configparser import ConfigParser from datetime import datetime from securedrop_client.config import Config from securedrop_client.db import Base, make_engine, Source @@ -48,13 +49,37 @@ def config(homedir) -> str: return full_path +@pytest.fixture(scope='function') +def alembic_config(homedir): + return _alembic_config(homedir) + + +def _alembic_config(homedir): + base_dir = os.path.join(os.path.dirname(__file__), '..') + migrations_dir = os.path.join(base_dir, 'alembic') + ini = ConfigParser() + ini.read(os.path.join(base_dir, 'alembic.ini')) + + ini.set('alembic', 'script_location', os.path.join(migrations_dir)) + ini.set('alembic', 'sqlalchemy.url', + 'sqlite:///{}'.format(os.path.join(homedir, 'svs.sqlite'))) + + alembic_path = os.path.join(homedir, 'alembic.ini') + + with open(alembic_path, 'w') as f: + ini.write(f) + + return alembic_path + + @pytest.fixture(scope='function') def session(homedir): engine = make_engine(homedir) - Base.metadata.create_all(bind=engine) + Base.metadata.create_all(bind=engine, checkfirst=False) Session = sessionmaker(bind=engine) session = Session() - return session + yield session + session.close() @pytest.fixture(scope='function') diff --git a/tests/test_alembic.py b/tests/test_alembic.py new file mode 100644 index 000000000..f37851c51 --- /dev/null +++ b/tests/test_alembic.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- + +import os +import pytest +import re +import subprocess + +from alembic.config import Config as AlembicConfig +from alembic.script import ScriptDirectory +from os import path +from sqlalchemy import text +from sqlalchemy.orm import sessionmaker + +from . import conftest +from securedrop_client.db import make_engine, Base + +MIGRATION_PATH = path.join(path.dirname(__file__), '..', 'alembic', 'versions') + +ALL_MIGRATIONS = [x.split('.')[0].split('_')[0] + for x in os.listdir(MIGRATION_PATH) + if x.endswith('.py')] + +WHITESPACE_REGEX = re.compile(r'\s+') + + +def list_migrations(cfg_path, head): + cfg = AlembicConfig(cfg_path) + script = ScriptDirectory.from_config(cfg) + migrations = [x.revision + for x in script.walk_revisions(base='base', head=head)] + migrations.reverse() + return migrations + + +def upgrade(alembic_config, migration): + subprocess.check_call(['alembic', 'upgrade', migration], cwd=path.dirname(alembic_config)) + + +def downgrade(alembic_config, migration): + subprocess.check_call(['alembic', 'downgrade', migration], cwd=path.dirname(alembic_config)) + + +def get_schema(session): + result = list(session.execute(text(''' + SELECT type, name, tbl_name, sql + FROM sqlite_master + ORDER BY type, name, tbl_name + '''))) + + return {(x[0], x[1], x[2]): x[3] for x in result} + + +def assert_schemas_equal(left, right): + for (k, v) in left.items(): + if k not in right: + raise AssertionError('Left contained {} but right did not'.format(k)) + if not ddl_equal(v, right[k]): + raise AssertionError( + 'Schema for {} did not match:\nLeft:\n{}\nRight:\n{}' + .format(k, v, right[k])) + right.pop(k) + + if right: + raise AssertionError( + 'Right had additional tables: {}'.format(right.keys())) + + +def ddl_equal(left, right): + ''' + Check the "tokenized" DDL is equivalent because, because sometimes Alembic schemas append + columns on the same line to the DDL comes out like: + + column1 TEXT NOT NULL, column2 TEXT NOT NULL + + and SQLAlchemy comes out: + + column1 TEXT NOT NULL, + column2 TEXT NOT NULL + ''' + # ignore the autoindex cases + if left is None and right is None: + return True + + left = [x for x in WHITESPACE_REGEX.split(left) if x] + right = [x for x in WHITESPACE_REGEX.split(right) if x] + + # Strip commas and quotes + left = [x.replace("\"", "").replace(",", "") for x in left] + right = [x.replace("\"", "").replace(",", "") for x in right] + + return sorted(left) == sorted(right) + + +def test_alembic_head_matches_db_models(tmpdir): + ''' + This test is to make sure that our database models in `db.py` are always in sync with the schema + generated by `alembic upgrade head`. + ''' + models_homedir = str(tmpdir.mkdir('models')) + subprocess.check_call(['sqlite3', os.path.join(models_homedir, 'svs.sqlite'), '.databases']) + engine = make_engine(models_homedir) + Base.metadata.create_all(bind=engine, checkfirst=False) + session = sessionmaker(engine)() + models_schema = get_schema(session) + + alembic_homedir = str(tmpdir.mkdir('alembic')) + subprocess.check_call(['sqlite3', os.path.join(alembic_homedir, 'svs.sqlite'), '.databases']) + session = sessionmaker(make_engine(alembic_homedir))() + alembic_config = conftest._alembic_config(alembic_homedir) + upgrade(alembic_config, 'head') + alembic_schema = get_schema(session) + + # The initial migration creates the table 'alembic_version', but this is + # not present in the schema created by `Base.metadata.create_all()`. + alembic_schema = {k: v for k, v in alembic_schema.items() + if k[2] != 'alembic_version'} + + assert_schemas_equal(alembic_schema, models_schema) + + +@pytest.mark.parametrize('migration', ALL_MIGRATIONS) +def test_alembic_migration_upgrade(alembic_config, config, migration): + # run migrations in sequence from base -> head + for mig in list_migrations(alembic_config, migration): + upgrade(alembic_config, mig) + + +@pytest.mark.parametrize('migration', ALL_MIGRATIONS) +def test_alembic_migration_downgrade(alembic_config, config, migration): + # upgrade to the parameterized test case ("head") + upgrade(alembic_config, migration) + + # run migrations in sequence from "head" -> base + migrations = list_migrations(alembic_config, migration) + migrations.reverse() + + for mig in migrations: + downgrade(alembic_config, mig) + + +@pytest.mark.parametrize('migration', ALL_MIGRATIONS) +def test_schema_unchanged_after_up_then_downgrade(alembic_config, + tmpdir, + migration): + migrations = list_migrations(alembic_config, migration) + + if len(migrations) > 1: + target = migrations[-2] + upgrade(alembic_config, target) + else: + # The first migration is the degenerate case where we don't need to + # get the database to some base state. + pass + + session = sessionmaker(make_engine(str(tmpdir.mkdir('original'))))() + original_schema = get_schema(session) + + upgrade(alembic_config, '+1') + downgrade(alembic_config, '-1') + + session = sessionmaker(make_engine(str(tmpdir.mkdir('reverted'))))() + reverted_schema = get_schema(session) + + # The initial migration is a degenerate case because it creates the table + # 'alembic_version', but rolling back the migration doesn't clear it. + if len(migrations) == 1: + reverted_schema = {k: v for k, v in reverted_schema.items() if k[2] != 'alembic_version'} + + assert_schemas_equal(reverted_schema, original_schema) From 337d91f5fd13f51ef2cd334bd124ebc1caf95548 Mon Sep 17 00:00:00 2001 From: heartsucker Date: Wed, 5 Dec 2018 17:41:05 +0100 Subject: [PATCH 2/9] remove unique constraint on users.username --- securedrop_client/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/securedrop_client/db.py b/securedrop_client/db.py index f9982a237..0c5f5e19d 100644 --- a/securedrop_client/db.py +++ b/securedrop_client/db.py @@ -136,7 +136,7 @@ class User(Base): id = Column(Integer, primary_key=True) uuid = Column(String(36), unique=True, nullable=False) - username = Column(String(255), nullable=False, unique=True) + username = Column(String(255), nullable=False) def __init__(self, username): self.username = username From 012dcd4a227068f46b3a66479014cde7b3066581 Mon Sep 17 00:00:00 2001 From: heartsucker Date: Thu, 6 Dec 2018 15:30:45 +0100 Subject: [PATCH 3/9] correctly name check constraints --- securedrop_client/db.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/securedrop_client/db.py b/securedrop_client/db.py index 0c5f5e19d..022485eac 100644 --- a/securedrop_client/db.py +++ b/securedrop_client/db.py @@ -28,17 +28,16 @@ class Source(Base): __tablename__ = 'sources' - # TODO - add number_of_docs id = Column(Integer, primary_key=True) uuid = Column(String(36), unique=True, nullable=False) journalist_designation = Column(String(255), nullable=False) document_count = Column(Integer, server_default="0", nullable=False) - is_flagged = Column(Boolean(name='ck_sources_is_flagged'), + is_flagged = Column(Boolean(name='is_flagged'), server_default="false") public_key = Column(Text, nullable=True) fingerprint = Column(String(64)) interaction_count = Column(Integer, server_default="0", nullable=False) - is_starred = Column(Boolean(name='ck_sources_is_starred'), + is_starred = Column(Boolean(name='is_starred'), server_default="false") last_updated = Column(DateTime) @@ -78,11 +77,11 @@ class Submission(Base): download_url = Column(String(255), nullable=False) # This is whether the submission has been downloaded in the local database. - is_downloaded = Column(Boolean(name='ck_submissions_is_downloaded'), + is_downloaded = Column(Boolean(name='is_downloaded'), default=False) # This reflects read status stored on the server. - is_read = Column(Boolean(name='ck_submissions_is_read'), + is_read = Column(Boolean(name='is_read'), default=False) source_id = Column(Integer, ForeignKey('sources.id')) @@ -123,7 +122,7 @@ class Reply(Base): size = Column(Integer) # This is whether the reply has been downloaded in the local database. - is_downloaded = Column(Boolean(name='ck_replies_is_downloaded'), + is_downloaded = Column(Boolean(name='is_downloaded'), default=False) def __repr__(self): From ba5677aa69006026d2e5da4691641b3a6d6e23a5 Mon Sep 17 00:00:00 2001 From: heartsucker Date: Tue, 5 Feb 2019 16:07:02 +0100 Subject: [PATCH 4/9] run alembic and application tests as separate processes --- .circleci/config.yml | 2 +- Makefile | 8 ++++++-- tests/gui/test_main.py | 1 - 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 35fba3cd1..b75b7e341 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ jobs: command: | pipenv install --keep-outdated --dev export PYTHONPATH=$PYTHONPATH:. # so alembic can get to Base metadata - pipenv run make check + pipenv run make check --keep-going - run: name: Check Python dependencies for known vulnerabilities diff --git a/Makefile b/Makefile index 475657a69..4451d8eba 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,21 @@ TESTS ?= tests TESTOPTS ?= -v .PHONY: test test: ## Run the application tests - @TEST_CMD="python -m pytest -v --random-order-bucket=global --cov-config .coveragerc --cov-report html --cov-report term-missing --cov=securedrop_client --cov-fail-under 100 $(TESTOPTS) $(TESTS)" ; \ + @TEST_CMD="python -m pytest -v --random-order-bucket=global --cov-config .coveragerc --cov-report html --cov-report term-missing --cov=securedrop_client --cov-fail-under 100 -k-test_alembic.py $(TESTOPTS) $(TESTS)" ; \ if command -v xvfb-run > /dev/null; then \ xvfb-run $$TEST_CMD ; else \ $$TEST_CMD ; fi +.PHONY: test-alembic +test-alembic: ## Run the alembic migration tests + @python -m pytest -v $(TESTOPTS) tests/test_alembic.py + .PHONY: lint lint: ## Run the linters @flake8 . .PHONY: check -check: clean lint test ## Run the full CI test suite +check: clean lint test test-alembic ## Run the full CI test suite # Explaination of the below shell command should it ever break. # 1. Set the field separator to ": ##" and any make targets that might appear between : and ## diff --git a/tests/gui/test_main.py b/tests/gui/test_main.py index b0804d62b..5ae6ab7a8 100644 --- a/tests/gui/test_main.py +++ b/tests/gui/test_main.py @@ -264,7 +264,6 @@ def test_conversation_pending_message(mocker): def test_set_status(mocker): - """ Ensure the status bar's text is updated. """ From e35f549b79f3e3f7e0bbf84aae8c20d1bc8cd495 Mon Sep 17 00:00:00 2001 From: heartsucker Date: Tue, 5 Feb 2019 16:13:28 +0100 Subject: [PATCH 5/9] install sqlite3 in CI --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index b75b7e341..ed61f0d23 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,8 @@ jobs: steps: - checkout + - run: sudo apt-get install -y sqlite3 + - run: name: Install requirements and run tests command: | From 480aa58123aa5aa96df015e9c79e56392544870a Mon Sep 17 00:00:00 2001 From: heartsucker Date: Tue, 5 Feb 2019 20:07:14 +0100 Subject: [PATCH 6/9] added notes about migrations to the readme --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 80121b06d..4ed8e57aa 100644 --- a/README.md +++ b/README.md @@ -85,21 +85,26 @@ After you run the server container, the journalist interface API will be running To ensure that file decryption works, please import [this test private key](https://raw.githubusercontent.com/freedomofpress/securedrop/0a901362b84a5378fba80e9cd0ffe4542bdcd598/securedrop/tests/files/test_journalist_key.sec) into your GnuPG keyring. Submissions in the SecureDrop server dev environment can be decrypted with this test key. -## Run linter +## Run the tests and checks -``` -make lint -``` - -## Run tests +To run everything, run: -``` -make test +```bash +make check ``` ## Generate and run database migrations -``` -alembic revision --autogenerate -m "describe your revision here" +```bash +rm -f svs.sqlite +sqlite3 svs.sqlite .databases > /dev/null alembic upgrade head +alembic revision --autogenerate -m "describe your revision here" +make test-alembic ``` + +### Merging Migrations + +This project aims to have at most one migration per release. There may be cases where this is not feasible, +but developers should merge their migration into the latest migration that has been generated since the last +release. The above mentioned autogenerate command will not do this for you. From 08193f8b2e9b842e9aad6351ecdee1bbf52cafb7 Mon Sep 17 00:00:00 2001 From: heartsucker Date: Thu, 6 Dec 2018 13:21:21 +0100 Subject: [PATCH 7/9] regenerate initial migration --- .../0ca2cc448074_added_fingerprint.py | 24 ------ ...tial_migration.py => 2f363b3d680e_init.py} | 78 +++++++++---------- .../cd0fbb73bf8e_capture_document_count.py | 25 ------ 3 files changed, 37 insertions(+), 90 deletions(-) delete mode 100644 alembic/versions/0ca2cc448074_added_fingerprint.py rename alembic/versions/{5344fc3a3d3f_initial_migration.py => 2f363b3d680e_init.py} (51%) delete mode 100644 alembic/versions/cd0fbb73bf8e_capture_document_count.py diff --git a/alembic/versions/0ca2cc448074_added_fingerprint.py b/alembic/versions/0ca2cc448074_added_fingerprint.py deleted file mode 100644 index 5f52e4707..000000000 --- a/alembic/versions/0ca2cc448074_added_fingerprint.py +++ /dev/null @@ -1,24 +0,0 @@ -"""added fingerprint - -Revision ID: 0ca2cc448074 -Revises: cd0fbb73bf8e -Create Date: 2018-11-30 18:55:23.007169 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0ca2cc448074' -down_revision = 'cd0fbb73bf8e' -branch_labels = None -depends_on = None - - -def upgrade(): - op.add_column('sources', sa.Column('fingerprint', sa.String(length=64), nullable=True)) - - -def downgrade(): - op.drop_column('sources', 'fingerprint') diff --git a/alembic/versions/5344fc3a3d3f_initial_migration.py b/alembic/versions/2f363b3d680e_init.py similarity index 51% rename from alembic/versions/5344fc3a3d3f_initial_migration.py rename to alembic/versions/2f363b3d680e_init.py index 679d017ab..6189dbec5 100644 --- a/alembic/versions/5344fc3a3d3f_initial_migration.py +++ b/alembic/versions/2f363b3d680e_init.py @@ -1,8 +1,8 @@ -"""Initial migration +"""init -Revision ID: 5344fc3a3d3f +Revision ID: 2f363b3d680e Revises: -Create Date: 2018-07-18 18:11:06.781732 +Create Date: 2019-02-08 12:07:47.062397 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '5344fc3a3d3f' +revision = '2f363b3d680e' down_revision = None branch_labels = None depends_on = None @@ -18,31 +18,29 @@ def upgrade(): op.create_table( - 'users', + 'sources', sa.Column('id', sa.Integer(), nullable=False), sa.Column('uuid', sa.String(length=36), nullable=False), - sa.Column('username', sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('username') + sa.Column('journalist_designation', sa.String(length=255), nullable=False), + sa.Column('document_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('is_flagged', sa.Boolean(name='is_flagged'), server_default='false', + nullable=True), + sa.Column('public_key', sa.Text(), nullable=True), + sa.Column('fingerprint', sa.String(length=64), nullable=True), + sa.Column('interaction_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('is_starred', sa.Boolean(name='is_starred'), server_default='false', + nullable=True), + sa.Column('last_updated', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_sources')), + sa.UniqueConstraint('uuid', name=op.f('uq_sources_uuid')) ) op.create_table( - 'sources', + 'users', sa.Column('id', sa.Integer(), nullable=False), sa.Column('uuid', sa.String(length=36), nullable=False), - sa.Column('journalist_designation', sa.String(length=255), - nullable=False), - sa.Column('is_flagged', sa.Boolean(name='ck_sources_is_flagged'), - nullable=True, - server_default="false"), - sa.Column('public_key', sa.Text(), nullable=True), - sa.Column('interaction_count', sa.Integer(), nullable=False, - server_default="0"), - sa.Column('is_starred', sa.Boolean(name='ck_sources_is_starred'), - nullable=True, - server_default="false"), - sa.Column('last_updated', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('uuid') + sa.Column('username', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('uuid', name=op.f('uq_users_uuid')) ) op.create_table( 'replies', @@ -51,13 +49,14 @@ def upgrade(): sa.Column('source_id', sa.Integer(), nullable=True), sa.Column('journalist_id', sa.Integer(), nullable=True), sa.Column('filename', sa.String(length=255), nullable=False), - sa.Column('size', sa.Integer(), nullable=False), - sa.Column('is_downloaded', sa.Boolean( - name='ck_replies_is_downloaded'), nullable=True), - sa.ForeignKeyConstraint(['journalist_id'], ['users.id'], ), - sa.ForeignKeyConstraint(['source_id'], ['sources.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('uuid') + sa.Column('size', sa.Integer(), nullable=True), + sa.Column('is_downloaded', sa.Boolean(name='is_downloaded'), nullable=True), + sa.ForeignKeyConstraint(['journalist_id'], ['users.id'], + name=op.f('fk_replies_journalist_id_users')), + sa.ForeignKeyConstraint(['source_id'], ['sources.id'], + name=op.f('fk_replies_source_id_sources')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_replies')), + sa.UniqueConstraint('uuid', name=op.f('uq_replies_uuid')) ) op.create_table( 'submissions', @@ -65,22 +64,19 @@ def upgrade(): sa.Column('uuid', sa.String(length=36), nullable=False), sa.Column('filename', sa.String(length=255), nullable=False), sa.Column('size', sa.Integer(), nullable=False), - sa.Column('is_downloaded', - sa.Boolean(name='ck_submissions_is_downloaded'), - nullable=True), - sa.Column('is_read', - sa.Boolean(name='ck_submissions_is_read'), - nullable=True), - sa.Column('source_id', sa.Integer(), nullable=True), sa.Column('download_url', sa.String(length=255), nullable=False), - sa.ForeignKeyConstraint(['source_id'], ['sources.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('uuid') + sa.Column('is_downloaded', sa.Boolean(name='is_downloaded'), nullable=True), + sa.Column('is_read', sa.Boolean(name='is_read'), nullable=True), + sa.Column('source_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['source_id'], ['sources.id'], + name=op.f('fk_submissions_source_id_sources')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_submissions')), + sa.UniqueConstraint('uuid', name=op.f('uq_submissions_uuid')) ) def downgrade(): - op.drop_table('users') op.drop_table('submissions') op.drop_table('replies') + op.drop_table('users') op.drop_table('sources') diff --git a/alembic/versions/cd0fbb73bf8e_capture_document_count.py b/alembic/versions/cd0fbb73bf8e_capture_document_count.py deleted file mode 100644 index 1b425a5e7..000000000 --- a/alembic/versions/cd0fbb73bf8e_capture_document_count.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Capture document count - -Revision ID: cd0fbb73bf8e -Revises: 5344fc3a3d3f -Create Date: 2018-11-10 18:10:23.847838 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'cd0fbb73bf8e' -down_revision = '5344fc3a3d3f' -branch_labels = None -depends_on = None - - -def upgrade(): - op.add_column('sources', sa.Column( - 'document_count', sa.Integer(), nullable=False, server_default='0')) - - -def downgrade(): - op.drop_column('sources', 'document_count') From 5899b6d2147d0422f754d877f7dd455902113fc6 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Wed, 27 Feb 2019 12:32:21 -0800 Subject: [PATCH 8/9] modify naming convention for check constraint https://github.com/sqlalchemy/sqlalchemy/issues/3345 --- Makefile | 8 ++------ securedrop_client/db.py | 2 +- tests/test_alembic.py | 12 ++++++++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 4451d8eba..475657a69 100644 --- a/Makefile +++ b/Makefile @@ -13,21 +13,17 @@ TESTS ?= tests TESTOPTS ?= -v .PHONY: test test: ## Run the application tests - @TEST_CMD="python -m pytest -v --random-order-bucket=global --cov-config .coveragerc --cov-report html --cov-report term-missing --cov=securedrop_client --cov-fail-under 100 -k-test_alembic.py $(TESTOPTS) $(TESTS)" ; \ + @TEST_CMD="python -m pytest -v --random-order-bucket=global --cov-config .coveragerc --cov-report html --cov-report term-missing --cov=securedrop_client --cov-fail-under 100 $(TESTOPTS) $(TESTS)" ; \ if command -v xvfb-run > /dev/null; then \ xvfb-run $$TEST_CMD ; else \ $$TEST_CMD ; fi -.PHONY: test-alembic -test-alembic: ## Run the alembic migration tests - @python -m pytest -v $(TESTOPTS) tests/test_alembic.py - .PHONY: lint lint: ## Run the linters @flake8 . .PHONY: check -check: clean lint test test-alembic ## Run the full CI test suite +check: clean lint test ## Run the full CI test suite # Explaination of the below shell command should it ever break. # 1. Set the field separator to ": ##" and any make targets that might appear between : and ## diff --git a/securedrop_client/db.py b/securedrop_client/db.py index 022485eac..40d82070e 100644 --- a/securedrop_client/db.py +++ b/securedrop_client/db.py @@ -9,7 +9,7 @@ convention = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", + "ck": "ck_%(table_name)s_%(column_0_name)s", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "pk": "pk_%(table_name)s" } diff --git a/tests/test_alembic.py b/tests/test_alembic.py index f37851c51..21e66cf14 100644 --- a/tests/test_alembic.py +++ b/tests/test_alembic.py @@ -12,7 +12,7 @@ from sqlalchemy.orm import sessionmaker from . import conftest -from securedrop_client.db import make_engine, Base +from securedrop_client.db import make_engine, Base, convention MIGRATION_PATH = path.join(path.dirname(__file__), '..', 'alembic', 'versions') @@ -100,15 +100,23 @@ def test_alembic_head_matches_db_models(tmpdir): subprocess.check_call(['sqlite3', os.path.join(models_homedir, 'svs.sqlite'), '.databases']) engine = make_engine(models_homedir) Base.metadata.create_all(bind=engine, checkfirst=False) + assert Base.metadata.naming_convention == convention session = sessionmaker(engine)() models_schema = get_schema(session) + Base.metadata.drop_all(bind=engine) + session.close() + engine.dispose() alembic_homedir = str(tmpdir.mkdir('alembic')) subprocess.check_call(['sqlite3', os.path.join(alembic_homedir, 'svs.sqlite'), '.databases']) - session = sessionmaker(make_engine(alembic_homedir))() + engine = make_engine(alembic_homedir) + session = sessionmaker(engine)() alembic_config = conftest._alembic_config(alembic_homedir) upgrade(alembic_config, 'head') alembic_schema = get_schema(session) + Base.metadata.drop_all(bind=engine) + session.close() + engine.dispose() # The initial migration creates the table 'alembic_version', but this is # not present in the schema created by `Base.metadata.create_all()`. From 3b8530ebe3d6551fd3e94f92603eda3025420198 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Wed, 27 Feb 2019 12:38:23 -0800 Subject: [PATCH 9/9] init: add no cover --- securedrop_client/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/securedrop_client/__init__.py b/securedrop_client/__init__.py index 51d490197..578e3fc87 100644 --- a/securedrop_client/__init__.py +++ b/securedrop_client/__init__.py @@ -11,8 +11,8 @@ current_locale, encoding = locale.getdefaultlocale() # Get the language code. language_code = current_locale[:2] -except (TypeError, ValueError): - language_code = 'en' +except (TypeError, ValueError): # pragma: no cover + language_code = 'en' # pragma: no cover # DEBUG/TRANSLATE: override the language code here (e.g. to Chinese). # language_code = 'zh' gettext.translation('securedrop_client', localedir=localedir,