diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..060283a --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,39 @@ +name: Test +on: + push: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - python-version: '3.7' + job-number: 1 + # the job number is appended to the page title to form + # QuickCategories CI Test/1, QuickCategories CI Test/2, etc.; + # only subpages 1 and 2 exist so far, so if you add a third job, + # you need to manually create the extra page first + # (the bot password does not have permission to create pages) + services: + mariadb: + image: mariadb:10.1 + env: + MYSQL_ROOT_PASSWORD: 'mariadb-root-password' + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - run: pip install --upgrade pip + - run: pip install -r requirements.txt + - run: make check + env: + MARIADB_ROOT_PASSWORD: 'mariadb-root-password' + MARIADB_PORT: ${{ job.services.mariadb.ports['3306'] }} + MW_USERNAME: ${{ secrets.MW_USERNAME }} + MW_PASSWORD: ${{ secrets.MW_PASSWORD }} + CI_JOB_NUMBER: ${{ matrix.job-number }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 769beea..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: python -dist: trusty # MariaDB 10.1 doesn’t work in xenial -python: - # Note: each extra job will cause test_runner.py to try to edit another page – - # QuickCategories CI Test/1, QuickCategories CI Test/2, etc. – - # and since the bot password does not have permission to create pages, - # when adding a new job you’ll have to create that page manually first. - # (This may not apply when jobs were removed in the meantime.) - - "3.7-dev" -addons: - mariadb: "10.1" -env: - - MARIADB_ROOT_PASSWORD="" -script: - - make check diff --git a/conftest.py b/conftest.py index 1fe01cc..e0e316e 100644 --- a/conftest.py +++ b/conftest.py @@ -1,72 +1,75 @@ import freezegun # type: ignore import mwapi # type: ignore import os import pymysql import pytest # type: ignore import random import re import string from typing import Any, Iterator @pytest.fixture def frozen_time() -> Iterator[Any]: with freezegun.freeze_time() as frozen_time: yield frozen_time @pytest.fixture def internet_connection() -> Iterator[None]: """No-value fixture to skip tests if no internet connection is available.""" try: yield except mwapi.errors.ConnectionError: pytest.skip('no internet connection') @pytest.fixture(scope="module") def fresh_database_connection_params() -> Iterator[dict]: if 'MARIADB_ROOT_PASSWORD' not in os.environ: pytest.skip('MariaDB credentials not provided') - connection = pymysql.connect(host='localhost', + host = 'localhost' + port = int(os.environ.get('MARIADB_PORT', 0)) + connection = pymysql.connect(host=host, + port=port, user='root', password=os.environ['MARIADB_ROOT_PASSWORD']) database_name = 'quickcategories_test_' + ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(16)) user_name = 'quickcategories_test_user_' + ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(16)) user_password = 'quickcategories_test_password_' + ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(16)) try: with connection.cursor() as cursor: cursor.execute('CREATE DATABASE `%s`' % database_name) cursor.execute('GRANT ALL PRIVILEGES ON `%s`.* TO `%s` IDENTIFIED BY %%s' % (database_name, user_name), (user_password,)) cursor.execute('USE `%s`' % database_name) with open('tables.sql') as tables: queries = tables.read() # PyMySQL does not support multiple queries in execute(), so we have to split for query in queries.split(';'): query = query.strip() if query: cursor.execute(query) connection.commit() - yield {'host': 'localhost', 'user': user_name, 'password': user_password, 'db': database_name} + yield {'host': host, 'port': port, 'user': user_name, 'password': user_password, 'db': database_name} finally: with connection.cursor() as cursor: cursor.execute('DROP DATABASE IF EXISTS `%s`' % database_name) cursor.execute('DROP USER IF EXISTS `%s`' % user_name) connection.commit() connection.close() @pytest.fixture def database_connection_params(fresh_database_connection_params: dict) -> Iterator[dict]: connection = pymysql.connect(**fresh_database_connection_params) try: with open('tables.sql') as tables: queries = tables.read() with connection.cursor() as cursor: for table in re.findall(r'CREATE TABLE ([^ ]+) ', queries): cursor.execute('DELETE FROM `%s`' % table) # more efficient than TRUNCATE TABLE on my system :/ # cursor.execute('ALTER TABLE `%s` AUTO_INCREMENT = 1' % table) # currently not necessary connection.commit() finally: connection.close() yield fresh_database_connection_params diff --git a/test_runner.py b/test_runner.py index 4ad11f4..86f6414 100644 --- a/test_runner.py +++ b/test_runner.py @@ -1,988 +1,987 @@ import datetime import mwapi # type: ignore import os import pytest # type: ignore from typing import List, Optional from action import Action, AddCategoryAction, RemoveCategoryAction from command import Command, CommandPending, CommandEdit, CommandNoop, CommandPageMissing, CommandTitleInvalid, CommandPageProtected, CommandEditConflict, CommandMaxlagExceeded, CommandBlocked, CommandWikiReadOnly from page import Page from runner import Runner from test_utils import FakeSession def test_resolve_pages_and_run_commands() -> None: if 'MW_USERNAME' not in os.environ or 'MW_PASSWORD' not in os.environ: pytest.skip('MediaWiki credentials not provided') session = mwapi.Session('https://test.wikipedia.org', user_agent='QuickCategories test (mail@lucaswerkmeister.de)') lgtoken = session.get(action='query', meta='tokens', type=['login'])['query']['tokens']['logintoken'] session.post(action='login', lgname=os.environ['MW_USERNAME'], lgpassword=os.environ['MW_PASSWORD'], lgtoken=lgtoken) suffix = '' - if 'TRAVIS_JOB_NUMBER' in os.environ: - job_number = os.environ['TRAVIS_JOB_NUMBER'] - suffix = '/' + job_number[job_number.index('.')+1:] + if 'CI_JOB_NUMBER' in os.environ: + suffix = '/' + os.environ['CI_JOB_NUMBER'] title_A = 'QuickCategories CI Test' + suffix title_B = 'QuickCategories CI Test Redirect' + suffix title_B2 = 'QuickCategories CI Test Redirect Target' + suffix title_C = 'QuickCategories CI Test Other Redirect' + suffix title_C2 = 'QuickCategories CI Test Other Redirect Target' + suffix actions: List[Action] = [AddCategoryAction('Added cat'), AddCategoryAction('Already present cat'), RemoveCategoryAction('Removed cat'), RemoveCategoryAction('Not present cat')] command_A = Command(Page(title_A, True), actions) command_B = Command(Page(title_B, True), actions) command_C = Command(Page(title_C, False), actions) runner = Runner(session, summary_batch_title='QuickCategories CI test') base_A = set_page_wikitext('setup', title_A, 'Test page for the QuickCategories tool.\n[[Category:Already present cat]]\n[[Category:Removed cat]]\nBottom text', runner) base_B = set_page_wikitext('setup', # NOQA: F841 (unused) title_B, '#REDIRECT [[' + title_B2 + ']]\n\n[[Category:Unchanged cat]]', runner) base_B2 = set_page_wikitext('setup', title_B2, 'Test page for the QuickCategories tool.\n[[Category:Already present cat]]\n[[Category:Removed cat]]\nBottom text', runner) base_C = set_page_wikitext('setup', title_C, '#REDIRECT [[' + title_C2 + ']]\n\n[[Category:Already present cat]]\n[[Category:Removed cat]]', runner) base_C2 = set_page_wikitext('setup', # NOQA: F841 (unused) title_C2, 'Test page for the QuickCategories tool.\n[[Category:Unchanged cat]]\nBottom text', runner) runner.resolve_pages([command_A.page, command_B.page, command_C.page]) edit_A = runner.run_command(CommandPending(0, command_A)) edit_B = runner.run_command(CommandPending(0, command_B)) edit_C = runner.run_command(CommandPending(0, command_C)) assert isinstance(edit_A, CommandEdit) assert edit_A.base_revision == base_A assert command_A.page.resolution is None assert isinstance(edit_B, CommandEdit) assert edit_B.base_revision == base_B2 assert command_B.page.resolution is None assert isinstance(edit_C, CommandEdit) assert edit_C.base_revision == base_C assert command_C.page.resolution is None revision_A = get_page_revision(title_A, runner) revision_B = get_page_revision(title_B, runner) revision_B2 = get_page_revision(title_B2, runner) revision_C = get_page_revision(title_C, runner) revision_C2 = get_page_revision(title_C2, runner) expected_comment = '+[[Category:Added cat]], (+[[Category:Already present cat]]), -[[Category:Removed cat]], (-[[Category:Not present cat]]); QuickCategories CI test' for revision in [revision_A, revision_B2, revision_C]: assert revision['comment'] == expected_comment assert not revision['minor'] expected_page_content = 'Test page for the QuickCategories tool.\n[[Category:Already present cat]]\n[[Category:Added cat]]\nBottom text' for revision in [revision_A, revision_B2]: assert revision['slots']['main']['content'] == expected_page_content expected_redirect_content = '#REDIRECT [[' + title_C2 + ']]\n\n[[Category:Already present cat]]\n[[Category:Added cat]]' assert revision_C['slots']['main']['content'] == expected_redirect_content assert revision_B['slots']['main']['content'] == '#REDIRECT [[' + title_B2 + ']]\n\n[[Category:Unchanged cat]]' assert revision_C2['slots']['main']['content'] == 'Test page for the QuickCategories tool.\n[[Category:Unchanged cat]]\nBottom text' set_page_wikitext('teardown', title_A, 'Test page for the QuickCategories tool.', runner) set_page_wikitext('teardown', title_B, '#REDIRECT [[' + title_B2 + ']]', runner) set_page_wikitext('teardown', title_B2, 'Test page for the QuickCategories tool.', runner) set_page_wikitext('teardown', title_C, '#REDIRECT [[' + title_C2 + ']]', runner) set_page_wikitext('teardown', title_C2, 'Test page for the QuickCategories tool.', runner) def set_page_wikitext(summary: str, title: str, wikitext: str, runner: Runner) -> int: response = runner.session.post(**{'action': 'edit', 'title': title, 'text': wikitext, 'summary': summary, 'token': runner.csrf_token, 'assert': 'user'}) if 'nochange' in response['edit']: return get_page_revision(title, runner)['revid'] else: return response['edit']['newrevid'] def get_page_revision(title: str, runner: Runner) -> dict: response = runner.session.get(action='query', titles=[title], prop=['revisions'], rvprop=['content', 'flags', 'comment', 'ids'], rvslots=['main'], formatversion=2) return response['query']['pages'][0]['revisions'][0] def test_with_nochange() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession( { 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'pageid': 58692, 'ns': 0, 'title': 'Main page', 'revisions': [ { 'revid': 195259, 'parentid': 114947, 'timestamp': '2014-02-23T15:14:40Z', 'slots': { 'main': { 'contentmodel': 'wikitext', 'contentformat': 'text/x-wiki', 'content': 'Unit Testing 1, 2, 3... External link: http://some-fake-site.com/?p=1774943982 Hit me with a captcha...', }, }, }, ], }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }, { 'edit': { 'result': 'Success', 'pageid': 58692, 'title': 'Main page', 'contentmodel': 'wikitext', 'nochange': '' } } ) session.host = 'test.wikidata.org' runner = Runner(session) command = Command(Page('Main page', True), [AddCategoryAction('Added cat')]) command_pending = CommandPending(0, command) command_record = runner.run_command(command_pending) assert isinstance(command_record, CommandNoop) assert command_record.revision == 195259 assert command.page.resolution is None def test_with_missing_page() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession({ 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'ns': 0, 'title': 'Missing page', 'missing': True, }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Missing page', True) runner.resolve_pages([page]) assert page.resolution == { 'missing': True, 'curtimestamp': curtimestamp, } command = Command(page, [AddCategoryAction('Added cat')]) command_pending = CommandPending(0, command) command_record = runner.run_command(command_pending) assert command_record == CommandPageMissing(command_pending.id, command_pending.command, curtimestamp) def test_with_missing_page_unnormalized() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession({ 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'ns': 0, 'title': 'Missing page', 'missing': True, }, ], 'normalized': [ { 'from': 'missing page', 'to': 'Missing page', }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('missing page', True) runner.resolve_pages([page]) assert page.resolution == { 'missing': True, 'curtimestamp': curtimestamp, } command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert command_record == CommandPageMissing(command_pending.id, command_pending.command, curtimestamp) def test_with_missing_page_redirect_resolve() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession({ 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'ns': 0, 'title': 'Redirect to missing page', 'invalid': True, # if Runner doesn’t resolve redirect, it’ll return CommandTitleInvalid instead of CommandPageMissing }, { 'ns': 0, 'title': 'Missing page', 'missing': True, }, ], 'redirects': [ { 'from': 'Redirect to missing page', 'to': 'Missing page', }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Redirect to missing page', True) runner.resolve_pages([page]) assert page.resolution == { 'missing': True, 'curtimestamp': curtimestamp, } command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert command_record == CommandPageMissing(command_pending.id, command_pending.command, curtimestamp) @pytest.mark.parametrize('resolve_redirects', [False, None]) def test_with_missing_page_redirect_without_resolve(resolve_redirects: Optional[bool]) -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession({ 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'ns': 0, 'title': 'Redirect to missing page', 'missing': True, }, # no entry for Missing page, if Runner resolves redirect it should crash ], 'redirects': [ { 'from': 'Redirect to missing page', 'to': 'Missing page', }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Redirect to missing page', resolve_redirects) runner.resolve_pages([page]) assert page.resolution == { 'missing': True, 'curtimestamp': curtimestamp, } command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert command_record == CommandPageMissing(command_pending.id, command_pending.command, curtimestamp) def test_with_missing_page_unnormalized_redirect() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession({ 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'ns': 0, 'title': 'Missing page', 'missing': True, }, ], 'normalized': [ { 'from': 'redirect to missing page', 'to': 'Redirect to missing page', }, ], 'redirects': [ { 'from': 'Redirect to missing page', 'to': 'Missing page', }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('redirect to missing page', True) runner.resolve_pages([page]) assert page.resolution == { 'missing': True, 'curtimestamp': curtimestamp, } command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert command_record == CommandPageMissing(command_pending.id, command_pending.command, curtimestamp) def test_with_invalid_title() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession({ 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'title': 'Invalid%20title', 'invalidreason': 'The requested page title contains invalid characters: "%20".', 'invalid': True, }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Invalid%20title', True) runner.resolve_pages([page]) assert page.resolution == { 'invalid': True, 'curtimestamp': curtimestamp, } command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert command_record == CommandTitleInvalid(command_pending.id, command_pending.command, curtimestamp) def test_with_protected_page() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession( { 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'pageid': 58692, 'ns': 0, 'title': 'Main page', 'revisions': [ { 'revid': 195259, 'parentid': 114947, 'timestamp': '2014-02-23T15:14:40Z', 'slots': { 'main': { 'contentmodel': 'wikitext', 'contentformat': 'text/x-wiki', 'content': 'Unit Testing 1, 2, 3... External link: http://some-fake-site.com/?p=1774943982 Hit me with a captcha...', }, }, }, ], }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }, mwapi.errors.APIError('protectedpage', 'This page has been protected to prevent editing or other actions.', None) ) session.host = 'test.wikidata.org' runner = Runner(session) command_pending = CommandPending(0, Command(Page('Main page', True), [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert command_record == CommandPageProtected(command_pending.id, command_pending.command, curtimestamp) def test_with_edit_conflict() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession( { 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'pageid': 58692, 'ns': 0, 'title': 'Main page', 'revisions': [ { 'revid': 195259, 'parentid': 114947, 'timestamp': '2014-02-23T15:14:40Z', 'slots': { 'main': { 'contentmodel': 'wikitext', 'contentformat': 'text/x-wiki', 'content': 'Unit Testing 1, 2, 3... External link: http://some-fake-site.com/?p=1774943982 Hit me with a captcha...', }, }, }, ], }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }, mwapi.errors.APIError('editconflict', 'Edit conflict: $1', None) ) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Main page', True) runner.resolve_pages([page]) assert page.resolution is not None command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert command_record == CommandEditConflict(command_pending.id, command_pending.command) assert page.resolution is None def test_with_maxlag_exceeded() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession( { 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'pageid': 58692, 'ns': 0, 'title': 'Main page', 'revisions': [ { 'revid': 195259, 'parentid': 114947, 'timestamp': '2014-02-23T15:14:40Z', 'slots': { 'main': { 'contentmodel': 'wikitext', 'contentformat': 'text/x-wiki', 'content': 'Unit Testing 1, 2, 3... External link: http://some-fake-site.com/?p=1774943982 Hit me with a captcha...', }, }, }, ], }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }, mwapi.errors.APIError('maxlag', 'Waiting for 10.64.48.35: 0.36570191383362 seconds lagged.', None) ) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Main page', True) runner.resolve_pages([page]) assert page.resolution is not None command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert isinstance(command_record, CommandMaxlagExceeded) assert command_record.retry_after.tzinfo == datetime.timezone.utc assert page.resolution is not None def test_with_blocked() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession( { 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'pageid': 58692, 'ns': 0, 'title': 'Main page', 'revisions': [ { 'revid': 195259, 'parentid': 114947, 'timestamp': '2014-02-23T15:14:40Z', 'slots': { 'main': { 'contentmodel': 'wikitext', 'contentformat': 'text/x-wiki', 'content': 'Unit Testing 1, 2, 3... External link: http://some-fake-site.com/?p=1774943982 Hit me with a captcha...', }, }, }, ], }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }, mwapi.errors.APIError('blocked', 'You have been blocked from editing.', None) ) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Main page', True) runner.resolve_pages([page]) assert page.resolution is not None command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert isinstance(command_record, CommandBlocked) assert not command_record.auto # would be nice to assert command_record.blockinfo once Runner can record it def test_with_autoblocked() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession( { 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'pageid': 58692, 'ns': 0, 'title': 'Main page', 'revisions': [ { 'revid': 195259, 'parentid': 114947, 'timestamp': '2014-02-23T15:14:40Z', 'slots': { 'main': { 'contentmodel': 'wikitext', 'contentformat': 'text/x-wiki', 'content': 'Unit Testing 1, 2, 3... External link: http://some-fake-site.com/?p=1774943982 Hit me with a captcha...', }, }, }, ], }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }, mwapi.errors.APIError('autoblocked', 'Your IP address has been blocked automatically, because it was used by a blocked user.', None) ) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Main page', True) runner.resolve_pages([page]) assert page.resolution is not None command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert isinstance(command_record, CommandBlocked) assert command_record.auto # would be nice to assert command_record.blockinfo once Runner can record it def test_with_readonly() -> None: curtimestamp = '2019-03-11T23:33:30Z' session = FakeSession( { 'curtimestamp': curtimestamp, 'query': { 'tokens': {'csrftoken': '+\\'}, 'pages': [ { 'pageid': 58692, 'ns': 0, 'title': 'Main page', 'revisions': [ { 'revid': 195259, 'parentid': 114947, 'timestamp': '2014-02-23T15:14:40Z', 'slots': { 'main': { 'contentmodel': 'wikitext', 'contentformat': 'text/x-wiki', 'content': 'Unit Testing 1, 2, 3... External link: http://some-fake-site.com/?p=1774943982 Hit me with a captcha...', }, }, }, ], }, ], 'namespaces': { '14': { 'id': 14, 'name': 'Category', 'canonical': 'Category', 'case': 'first-letter', }, }, 'namespacealiases': [], 'allmessages': [ { 'name': 'comma-separator', 'content': ', ', }, { 'name': 'semicolon-separator', 'content': '; ', }, { 'name': 'parentheses', 'content': '($1)', }, ], }, }, mwapi.errors.APIError('readonly', 'The wiki is currently in read-only mode.', None) ) session.host = 'test.wikidata.org' runner = Runner(session) page = Page('Main page', True) runner.resolve_pages([page]) assert page.resolution is not None command_pending = CommandPending(0, Command(page, [AddCategoryAction('Added cat')])) command_record = runner.run_command(command_pending) assert isinstance(command_record, CommandWikiReadOnly) # would be nice to assert command_record.reason once Runner can record it