wikimedia/pywikibot

View on GitHub
tests/site_tests.py

Summary

Maintainability
B
5 hrs
Test Coverage
#!/usr/bin/env python3
"""Tests for the site module."""
#
# (C) Pywikibot team, 2008-2024
#
# Distributed under the terms of the MIT license.
#
from __future__ import annotations

import pickle
import random
import threading
import time
import unittest
from collections.abc import Iterable, Mapping
from contextlib import suppress

import pywikibot
from pywikibot import config
from pywikibot.exceptions import (
    APIError,
    Error,
    IsNotRedirectPageError,
    NoPageError,
    PageInUseError,
    UnknownExtensionError,
    UnknownSiteError,
)
from tests.aspects import (
    AlteredDefaultSiteTestCase,
    DefaultSiteTestCase,
    DeprecationTestCase,
    TestCase,
    WikimediaDefaultSiteTestCase,
)
from tests.basepage import BasePageLoadRevisionsCachingTestBase


class TestSiteObject(DefaultSiteTestCase):

    """Test cases for Site methods."""

    cached = True

    def test_pickle_ability(self):
        """Test pickle ability."""
        mysite = self.get_site()
        mysite_str = pickle.dumps(mysite, protocol=config.pickle_protocol)
        mysite_pickled = pickle.loads(mysite_str)
        self.assertEqual(mysite, mysite_pickled)

    def test_repr(self):
        """Test __repr__."""
        code = self.site.family.obsolete.get(self.code) or self.code
        expect = f"Site('{code}', '{self.family}')"
        reprs = repr(self.site)
        self.assertTrue(reprs.endswith(expect),
                        f'\n{reprs} does not end with {expect}')

    def test_constructors(self):
        """Test cases for site constructors."""
        test_list = [
            ['alswiki', 'wikipedia:als'],  # see T345036
            ['commonswiki', 'commons:commons'],
            ['dewikinews', 'wikinews:de'],
            ['enwiki', 'wikipedia:en'],
            ['eswikisource', 'wikisource:es'],
            ['metawiki', 'meta:meta'],
            ['roa_rupwiki', 'wikipedia:roa-rup'],  # see T345036
            ['sourceswiki', 'wikisource:mul'],  # see T226960
            ['test2wiki', 'wikipedia:test2'],  # see T225729
            ['testwiki', 'wikipedia:test'],  # see T225729, T228300
            ['testwikidatawiki', 'wikidata:test'],
            ['ukwikivoyage', 'wikivoyage:uk'],
            ['wikidatawiki', 'wikidata:wikidata'],
        ]
        if isinstance(self.site.family, pywikibot.family.WikimediaFamily):
            site = self.site
        else:
            site = None
        for dbname, sitename in test_list:
            with self.subTest(dbname=dbname):
                self.assertIs(
                    pywikibot.site.APISite.fromDBName(dbname, site),
                    pywikibot.Site(sitename))

    def test_codes_property(self):
        """Test cases for codes property and related methods."""
        mysite = self.get_site()
        codes = mysite.codes
        self.assertIsInstance(codes, set)
        self.assertIn(mysite.code, codes)
        self.assertIsInstance(mysite.obsolete, bool)
        ipf = mysite.interwiki_putfirst()
        if ipf:  # no languages use this anymore, keep it for foreign families
            self.assertIsInstance(ipf, list)  # pragma: no cover
        else:
            self.assertIsNone(ipf)

        for item in mysite.validLanguageLinks():
            self.assertIn(item, codes)
            self.assertIsNone(self.site.namespaces.lookup_name(item))

    def test_namespace_methods(self):
        """Test cases for methods manipulating namespace names."""
        mysite = self.get_site()
        ns = mysite.namespaces
        self.assertIsInstance(ns, Mapping)
        # built-in namespaces always present
        self.assertIsInstance(mysite.ns_normalize('project'), str)

        for ns_id in range(-2, 16):
            with self.subTest(namespace_id=ns_id):
                self.assertIn(ns_id, ns)

        for key in ns:
            all_ns = mysite.namespace(key, True)
            with self.subTest(namespace=key):
                self.assertIsInstance(key, int)
                self.assertIsInstance(mysite.namespace(key), str)
                self.assertNotIsInstance(all_ns, str)
                self.assertIsInstance(all_ns, Iterable)

            for item in all_ns:
                with self.subTest(namespace=key, item=item):
                    self.assertIsInstance(item, str)

        for val in ns.values():
            with self.subTest(value=val):
                self.assertIsInstance(val, Iterable)
            for name in val:
                with self.subTest(value=val, name=name):
                    self.assertIsInstance(name, str)

    def test_user_attributes_return_types(self):
        """Test returned types of user attributes."""
        mysite = self.get_site()
        self.assertIsInstance(mysite.logged_in(), bool)
        self.assertIsInstance(mysite.userinfo, dict)

    def test_messages(self):
        """Test MediaWiki: messages."""
        mysite = self.get_site()
        for msg in ('about', 'aboutpage', 'aboutsite', 'accesskey-n-portal'):
            with self.subTest(message=msg, lang=mysite.lang):
                self.assertTrue(mysite.has_mediawiki_message(msg))
                self.assertIsInstance(mysite.mediawiki_message(msg), str)
                self.assertEqual(
                    mysite.mediawiki_message(msg),
                    mysite.mediawiki_message(msg, lang=mysite.lang))

            with self.subTest(message=msg, lang='de'):
                self.assertTrue(mysite.has_mediawiki_message(msg, lang='de'))
                self.assertIsInstance(mysite.mediawiki_message(msg, lang='de'),
                                      str)

        with self.subTest(message='nosuchmessage'):
            self.assertFalse(mysite.has_mediawiki_message('nosuchmessage'))
            with self.assertRaises(KeyError):
                mysite.mediawiki_message('nosuchmessage')

        msg = ('about', 'aboutpage')
        with self.subTest(messages=msg):
            about_msgs = self.site.mediawiki_messages(msg)
            self.assertIsInstance(mysite.mediawiki_messages(msg), dict)
            self.assertTrue(mysite.mediawiki_messages(msg))
            self.assertLength(about_msgs, 2)
            self.assertIn(msg[0], about_msgs)

        months = ['january', 'february', 'march', 'april', 'may_long',
                  'june', 'july', 'august', 'september', 'october',
                  'november', 'december']
        codes = sorted(mysite.family.codes)
        lang1, lang2 = codes[0], codes[-1]
        with self.subTest(messages='months', lang1=lang1, lang2=lang2):
            self.assertLength(mysite.mediawiki_messages(months, lang1), 12)
            self.assertLength(mysite.mediawiki_messages(months, lang2), 12)
            familyname = mysite.family.name
            if lang1 not in (lang2, familyname) and lang2 != familyname:
                self.assertNotEqual(mysite.mediawiki_messages(months, lang1),
                                    mysite.mediawiki_messages(months, lang2))

        with self.subTest(messages='Test messages order'):
            msg = mysite.mediawiki_messages(months, 'en')
            self.assertIsInstance(msg, dict)
            self.assertLength(msg, 12)
            self.assertEqual([key.title() for key in msg][5:],
                             list(msg.values())[5:])
            self.assertEqual(list(msg), months)

        # mediawiki_messages must be given a list; using a string will split it
        with self.subTest(messages='about'), self.assertRaises(KeyError):
            self.site.mediawiki_messages('about')

        msg = ('nosuchmessage1', 'about', 'aboutpage', 'nosuchmessage')
        with self.subTest(messages=msg):
            self.assertFalse(mysite.has_all_mediawiki_messages(msg))
            with self.assertRaises(KeyError):
                mysite.mediawiki_messages(msg)

        with self.subTest(test='server_time'):
            self.assertIsInstance(mysite.server_time(), pywikibot.Timestamp)
            ts = mysite.getcurrenttimestamp()
            self.assertIsInstance(ts, str)
            self.assertRegex(
                ts, r'(19|20)\d\d[0-1]\d[0-3]\d[0-2]\d[0-5]\d[0-5]\d')

        with self.subTest(test='months_names'):
            self.assertIsInstance(mysite.months_names, list)
            self.assertLength(mysite.months_names, 12)
            for month in mysite.months_names:
                self.assertIsInstance(month, tuple)
                self.assertLength(month, 2)

        with self.subTest(test='list_to_text'):
            self.assertEqual(mysite.list_to_text(('pywikibot',)), 'pywikibot')

    def test_english_specific_methods(self):
        """Test Site methods using English specific inputs and outputs."""
        mysite = self.get_site()
        if mysite.lang != 'en':
            self.skipTest(
                f'English-specific tests not valid on {mysite}')

        self.assertEqual(mysite.months_names[4], ('May', 'May'))
        self.assertEqual(mysite.list_to_text(('Pride', 'Prejudice')),
                         'Pride and Prejudice')
        self.assertEqual(mysite.list_to_text(('This', 'that', 'the other')),
                         'This, that and the other')

    def test_page_methods(self):
        """Test ApiSite methods for getting page-specific info."""
        mysite = self.get_site()
        mainpage = self.get_mainpage()
        self.assertIsInstance(mysite.page_restrictions(mainpage), dict)
        self.assertIsInstance(mysite.page_can_be_edited(mainpage), bool)
        self.assertIsInstance(mysite.page_isredirect(mainpage), bool)
        if mysite.page_isredirect(mainpage):
            self.assertIsInstance(mysite.getredirtarget(mainpage),
                                  pywikibot.Page)
        else:
            with self.assertRaises(IsNotRedirectPageError):
                mysite.getredirtarget(mainpage)
        a = list(mysite.preloadpages([mainpage]))
        self.assertLength(a, int(mainpage.exists()))
        if a:
            self.assertEqual(a[0], mainpage)

    def test_maxlimit(self):
        """Test maxlimit property."""
        limit = self.site.maxlimit
        self.assertIsInstance(limit, int)
        self.assertIn(limit, [10, 50, 500, 5000])

    def test_ratelimit(self):
        """Test ratelimit method."""
        actions = ('edit', 'move', 'purge', 'invalid')
        if self.site.logged_in():
            groups = ['user', 'unknown', 'noratelimit']
        else:
            groups = ['ip', 'unknown']
            self.assertFalse(self.site.has_right('noratelimit'))
        for action in actions:
            with self.subTest(action=action):
                limit = self.site.ratelimit(action)
                self.assertIn(limit.group, groups)
                self.assertEqual(limit.seconds / limit.hits, limit.delay)
                self.assertEqual(
                    1 / limit.delay if limit.seconds else float('inf'),
                    limit.ratio)
                if limit.group == 'unknown':
                    self.assertEqual(limit.hits, self.site.maxlimit)
                    self.assertEqual(limit.seconds, config.put_throttle)


class TestLockingPage(DefaultSiteTestCase):

    """Test cases for lock/unlock a page within threads."""

    cached = True

    def worker(self):
        """Lock a page, wait few seconds and unlock the page."""
        page = pywikibot.Page(self.site, 'Foo')
        page.site.lock_page(page=page, block=True)
        wait = random.randint(1, 25) / 10
        time.sleep(wait)
        page.site.unlock_page(page=page)

    def test_threads_locking_page(self):
        """Test lock_page and unlock_page methods for multiple threads."""
        # Start few threads
        threads = []
        for _ in range(5):
            thread = threading.Thread(target=self.worker, daemon=True)
            thread.start()
            threads.append(thread)

        for thread in threads:
            thread.join(15)  # maximum wait time for all threads

            with self.subTest(name=thread.name):
                # Check whether a timeout happened.
                # In that case is_alive() is True
                self.assertFalse(thread.is_alive(),
                                 'test page is still locked')

    def test_lock_page(self):
        """Test the site.lock_page() and site.unlock_page() method."""
        site = self.get_site()
        p1 = pywikibot.Page(site, 'Foo')

        site.lock_page(page=p1, block=True)
        with self.assertRaises(PageInUseError):
            site.lock_page(page=p1, block=False)
        site.unlock_page(page=p1)
        # verify it's unlocked
        site.lock_page(page=p1, block=False)
        site.unlock_page(page=p1)


class SiteUserTestCase(DefaultSiteTestCase, DeprecationTestCase):

    """Test site method using a user."""

    login = True

    def test_methods(self):
        """Test user related methods."""
        mysite = self.get_site()
        self.assertIsInstance(mysite.is_blocked(), bool)
        self.assertIsInstance(mysite.has_right('edit'), bool)
        self.assertFalse(mysite.has_right('nonexistent_right'))
        self.assertIsInstance(mysite.has_group('bots'), bool)
        self.assertFalse(mysite.has_group('nonexistent_group'))
        for grp in ('user', 'autoconfirmed', 'bot', 'sysop', 'nosuchgroup'):
            self.assertIsInstance(mysite.has_group(grp), bool)
        for rgt in ('read', 'edit', 'move', 'delete', 'rollback', 'block',
                    'nosuchright'):
            self.assertIsInstance(mysite.has_right(rgt), bool)

    def test_deprecated_methods(self):
        """Test deprecated user related methods."""
        mysite = self.get_site()
        self.assertIsInstance(mysite.messages(), bool)
        self.assertOneDeprecation()

    def test_logevents(self):
        """Test the site.logevents() method."""
        mysite = self.get_site()
        for entry in mysite.logevents(user=mysite.user(), total=3):
            self.assertEqual(entry.user(), mysite.user())


class SiteSysopTestCase(DefaultSiteTestCase):

    """Test site method using a sysop account."""

    rights = 'delete'

    def test_methods(self):
        """Test sysop related methods."""
        mysite = self.get_site()
        self.assertIsInstance(mysite.is_blocked(), bool)
        self.assertIsInstance(mysite.has_right('edit'), bool)
        self.assertFalse(mysite.has_right('nonexistent_right'))
        self.assertIsInstance(mysite.has_group('bots'), bool)
        self.assertFalse(mysite.has_group('nonexistent_group'))
        self.assertTrue(mysite.has_right(self.rights))

    def test_deletedrevs(self):
        """Test the site.deletedrevs() method."""
        mysite = self.get_site()
        if not mysite.has_right('deletedhistory'):
            self.skipTest(
                "You don't have permission to view the deleted revisions "
                f'on {mysite}.')
        mainpage = self.get_mainpage()
        gen = mysite.deletedrevs(total=10, titles=mainpage)

        for dr in gen:
            break
        else:
            self.skipTest(
                f'{mainpage} contains no deleted revisions.')
        self.assertLessEqual(len(dr['revisions']), 10)
        for rev in dr['revisions']:
            self.assertIsInstance(rev, dict)

        with self.subTest(start='2008-10-11T01:02:03Z', reverse=False):
            for item in mysite.deletedrevs(start='2008-10-11T01:02:03Z',
                                           titles=mainpage, total=5):
                for rev in item['revisions']:
                    self.assertIsInstance(rev, dict)
                    self.assertLessEqual(rev['timestamp'],
                                         '2008-10-11T01:02:03Z')

        with self.subTest(end='2008-04-01T02:03:04Z', reverse=False):
            for item in mysite.deletedrevs(end='2008-04-01T02:03:04Z',
                                           titles=mainpage, total=5):
                for rev in item['revisions']:
                    self.assertIsInstance(rev, dict)
                    self.assertGreaterEqual(rev['timestamp'],
                                            '2008-10-11T02:03:04Z')

        with self.subTest(start='2008-10-11T03:05:07Z', reverse=True):
            for item in mysite.deletedrevs(start='2008-10-11T03:05:07Z',
                                           titles=mainpage, total=5,
                                           reverse=True):
                for rev in item['revisions']:
                    self.assertIsInstance(rev, dict)
                    self.assertGreaterEqual(rev['timestamp'],
                                            '2008-10-11T03:05:07Z')

        with self.subTest(end='2008-10-11T04:06:08Z', reverse=True):
            for item in mysite.deletedrevs(end='2008-10-11T04:06:08Z',
                                           titles=mainpage, total=5,
                                           reverse=True):
                for rev in item['revisions']:
                    self.assertIsInstance(rev, dict)
                    self.assertLessEqual(rev['timestamp'],
                                         '2008-10-11T04:06:08Z')

        with self.subTest(start='2008-10-13T11:59:59Z',
                          end='2008-10-13T00:00:01Z',
                          reverse=False):
            for item in mysite.deletedrevs(start='2008-10-13T11:59:59Z',
                                           end='2008-10-13T00:00:01Z',
                                           titles=mainpage, total=5):
                for rev in item['revisions']:
                    self.assertIsInstance(rev, dict)
                    self.assertLessEqual(rev['timestamp'],
                                         '2008-10-13T11:59:59Z')
                    self.assertGreaterEqual(rev['timestamp'],
                                            '2008-10-13T00:00:01Z')

        with self.subTest(start='2008-10-15T06:00:01Z',
                          end='2008-10-15T23:59:59Z',
                          reverse=True):
            for item in mysite.deletedrevs(start='2008-10-15T06:00:01Z',
                                           end='2008-10-15T23:59:59Z',
                                           titles=mainpage, total=5,
                                           reverse=True):
                for rev in item['revisions']:
                    self.assertIsInstance(rev, dict)
                    self.assertLessEqual(rev['timestamp'],
                                         '2008-10-15T23:59:59Z')
                    self.assertGreaterEqual(rev['timestamp'],
                                            '2008-10-15T06:00:01Z')

        # start earlier than end
        with self.subTest(start='2008-09-03T00:00:01Z',
                          end='2008-09-03T23:59:59Z',
                          reverse=False), self.assertRaises(AssertionError):
            gen = mysite.deletedrevs(titles=mainpage,
                                     start='2008-09-03T00:00:01Z',
                                     end='2008-09-03T23:59:59Z', total=5)
            next(gen)

        # reverse: end earlier than start
        with self.subTest(start='2008-09-03T23:59:59Z',
                          end='2008-09-03T00:00:01Z',
                          reverse=True), self.assertRaises(AssertionError):
            gen = mysite.deletedrevs(titles=mainpage,
                                     start='2008-09-03T23:59:59Z',
                                     end='2008-09-03T00:00:01Z', total=5,
                                     reverse=True)
            next(gen)

    def test_alldeletedrevisions(self):
        """Test the site.alldeletedrevisions() method."""
        mysite = self.get_site()
        myuser = mysite.user()
        if not mysite.has_right('deletedhistory'):
            self.skipTest(
                "You don't have permission to view the deleted revisions "
                f'on {mysite}.')
        prop = ['ids', 'timestamp', 'flags', 'user', 'comment']
        gen = mysite.alldeletedrevisions(total=10, prop=prop)

        for data in gen:
            break
        else:
            self.skipTest(f'{myuser} does not have deleted edits.')
        self.assertIn('revisions', data)
        for drev in data['revisions']:
            for key in ('revid', 'timestamp', 'user', 'comment'):
                self.assertIn(key, drev)

        with self.subTest(start='2008-10-11T01:02:03Z', reverse=False,
                          prop=prop):
            for item in mysite.alldeletedrevisions(
                start='2008-10-11T01:02:03Z',
                user=myuser,
                total=5
            ):
                for drev in item['revisions']:
                    self.assertIsInstance(drev, dict)
                    self.assertLessEqual(drev['timestamp'],
                                         '2008-10-11T01:02:03Z')

        with self.subTest(start='2008-10-11T01:02:03Z', reverse=True,
                          prop=prop):
            for item in mysite.alldeletedrevisions(
                start='2008-10-11T01:02:03Z',
                user=myuser,
                total=5
            ):
                for drev in item['revisions']:
                    self.assertIsInstance(drev, dict)
                    self.assertGreaterEqual(drev['timestamp'],
                                            '2008-10-11T01:02:03Z')

        # start earlier than end
        with self.subTest(start='2008-09-03T00:00:01Z',
                          end='2008-09-03T23:59:59Z',
                          reverse=False,
                          prop=prop), self.assertRaises(AssertionError):
            gen = mysite.alldeletedrevisions(start='2008-09-03T00:00:01Z',
                                             end='2008-09-03T23:59:59Z',
                                             total=5)
            next(gen)

        # reverse: end earlier than start
        with self.subTest(start='2008-09-03T23:59:59Z',
                          end='2008-09-03T00:00:01Z',
                          reverse=True,
                          prop=prop), self.assertRaises(AssertionError):
            gen = mysite.alldeletedrevisions(start='2008-09-03T23:59:59Z',
                                             end='2008-09-03T00:00:01Z',
                                             total=5, reverse=True)
            next(gen)


class TestSiteSysopWrite(TestCase):

    """Test site methods that require writing rights."""

    family = 'wikipedia'
    code = 'test'

    write = True
    rights = 'delete,deleterevision,protect'

    def test_protect(self):
        """Test the site.protect() method."""
        site = self.get_site()
        p1 = pywikibot.Page(site, 'User:Unicodesnowman/ProtectTest')

        r = site.protect(protections={'edit': 'sysop',
                                      'move': 'autoconfirmed'},
                         page=p1,
                         reason='Pywikibot unit test')
        self.assertIsNone(r)
        self.assertEqual(site.page_restrictions(page=p1),
                         {'edit': ('sysop', 'infinite'),
                          'move': ('autoconfirmed', 'infinite')})

        expiry = pywikibot.Timestamp.fromISOformat('2050-01-01T00:00:00Z')
        site.protect(protections={'edit': 'sysop', 'move': 'autoconfirmed'},
                     page=p1,
                     expiry=expiry,
                     reason='Pywikibot unit test')

        self.assertEqual(site.page_restrictions(page=p1),
                         {'edit': ('sysop', '2050-01-01T00:00:00Z'),
                          'move': ('autoconfirmed', '2050-01-01T00:00:00Z')})

        site.protect(protections={'edit': '', 'move': ''},
                     page=p1,
                     reason='Pywikibot unit test')
        self.assertEqual(site.page_restrictions(page=p1), {})

    def test_protect_alt(self):
        """Test the site.protect() method, works around T78522."""
        site = self.get_site()
        p1 = pywikibot.Page(site, 'User:Unicodesnowman/ProtectTest')

        r = site.protect(protections={'edit': 'sysop',
                                      'move': 'autoconfirmed'},
                         page=p1,
                         reason='Pywikibot unit test')
        self.assertIsNone(r)
        self.assertEqual(site.page_restrictions(page=p1),
                         {'edit': ('sysop', 'infinite'),
                          'move': ('autoconfirmed', 'infinite')})

        p1 = pywikibot.Page(site, 'User:Unicodesnowman/ProtectTest')
        expiry = pywikibot.Timestamp.fromISOformat('2050-01-01T00:00:00Z')
        site.protect(protections={'edit': 'sysop', 'move': 'autoconfirmed'},
                     page=p1,
                     expiry=expiry,
                     reason='Pywikibot unit test')

        self.assertEqual(site.page_restrictions(page=p1),
                         {'edit': ('sysop', '2050-01-01T00:00:00Z'),
                          'move': ('autoconfirmed', '2050-01-01T00:00:00Z')})

        p1 = pywikibot.Page(site, 'User:Unicodesnowman/ProtectTest')
        site.protect(protections={'edit': '', 'move': ''},
                     page=p1,
                     reason='Pywikibot unit test')
        self.assertEqual(site.page_restrictions(page=p1), {})

    def test_protect_exception(self):
        """Test that site.protect() throws an exception for invalid args."""
        site = self.get_site()
        page = pywikibot.Page(site, 'User:Unicodesnowman/ProtectTest')

        with self.subTest(test='anInvalidType'), \
            self.assertRaisesRegex(APIError,
                                   'Invalid protection type "anInvalidType"'):
            site.protect(protections={'anInvalidType': 'sysop'},
                         page=page, reason='Pywikibot unit test')

        with self.subTest(test='anInvalidLevel'), \
                self.assertRaisesRegex(Error, 'Invalid protection level'):
            site.protect(protections={'edit': 'anInvalidLevel'},
                         page=page, reason='Pywikibot unit test')

    def test_delete(self):
        """Test the site.delete() and site.undelete() methods."""
        site = self.get_site()
        p = pywikibot.Page(site, 'User:Unicodesnowman/DeleteTestSite')
        # Verify state
        if not p.exists():
            site.undelete(p, 'pywikibot unit tests')

        site.delete(p, reason='pywikibot unit tests')
        with self.assertRaises(NoPageError):
            p.get(force=True)

        site.undelete(p, 'pywikibot unit tests',
                      revisions=['2014-12-21T06:07:47Z',
                                 '2014-12-21T06:07:31Z'])

        revs = list(p.revisions())
        self.assertLength(revs, 2)
        self.assertEqual(revs[0].revid, 219995)
        self.assertEqual(revs[1].revid, 219994)

        site.delete(p, reason='pywikibot unit tests')
        site.undelete(p, 'pywikibot unit tests')
        revs = list(p.revisions())
        self.assertGreater(len(revs), 2)

    def test_revdel_page(self):
        """Test deleting and undeleting page revisions."""
        site = self.get_site()
        # Verify state
        site.deleterevs('revision', ids=[219993, 219994], hide='',
                        show='content|comment|user',
                        reason='pywikibot unit tests')

        # Single revision
        site.deleterevs('revision', '219994', hide='user',
                        reason='pywikibot unit tests')

        p1 = pywikibot.Page(site, 'User:Unicodesnowman/DeleteTestSite')
        revs = list(p1.revisions())
        for rev in revs:
            if rev['revid'] != 219994:
                continue
            self.assertTrue(rev['userhidden'])

        # Multiple revisions
        site.deleterevs('revision', '219993|219994', hide='comment',
                        reason='pywikibot unit tests')

        p2 = pywikibot.Page(site, 'User:Unicodesnowman/DeleteTestSite')
        revs = list(p2.revisions())
        for rev in revs:
            if rev['revid'] != 219994:
                continue
            self.assertTrue(rev['userhidden'])
            self.assertTrue(rev['commenthidden'])

        # Concurrently show and hide
        site.deleterevs('revision', ['219993', '219994'], hide='user|content',
                        show='comment', reason='pywikibot unit tests')

        p3 = pywikibot.Page(site, 'User:Unicodesnowman/DeleteTestSite')
        revs = list(p3.revisions())
        for rev in revs:
            if rev['revid'] == 219993:
                self.assertTrue(rev['userhidden'])
            elif rev['revid'] == 219994:
                self.assertFalse(rev['commenthidden'])

        # Cleanup
        site.deleterevs('revision', [219993, 219994],
                        show='content|comment|user',
                        reason='pywikibot unit tests')

    @unittest.expectedFailure  # T367309
    def test_revdel_file(self):
        """Test deleting and undeleting file revisions."""
        site = pywikibot.Site('test')

        # Verify state
        site.deleterevs('oldimage', [20210314184415, 20210314184430],
                        show='content|comment|user',
                        reason='pywikibot unit tests',
                        target='File:T276726.png')

        # Single revision
        site.deleterevs('oldimage', '20210314184415', hide='user', show='',
                        reason='pywikibot unit tests',
                        target='File:T276726.png')

        ts1 = pywikibot.Timestamp(2021, 3, 14, 18, 43, 57)
        ts2 = pywikibot.Timestamp(2021, 3, 14, 18, 44, 17)

        fp1 = pywikibot.FilePage(site, 'File:T276726.png')
        site.loadimageinfo(fp1, history=True)
        for v in fp1._file_revisions.values():
            if v['timestamp'] == ts1:
                self.assertTrue(hasattr(v, 'userhidden'))

        # Multiple revisions
        site.deleterevs('oldimage', '20210314184415|20210314184430',
                        hide='comment', reason='pywikibot unit tests',
                        target='File:T276726.png')

        fp2 = pywikibot.FilePage(site, 'File:T276726.png')
        site.loadimageinfo(fp2, history=True)
        for v in fp2._file_revisions.values():
            if v['timestamp'] in (ts1, ts2):
                self.assertTrue(hasattr(v, 'commenthidden'))

        # Concurrently show and hide
        site.deleterevs('oldimage', ['20210314184415', '20210314184430'],
                        hide='user|content', show='comment',
                        reason='pywikibot unit tests',
                        target='File:T276726.png')

        fp3 = pywikibot.FilePage(site, 'File:T276726.png')
        site.loadimageinfo(fp3, history=True)
        for v in fp3._file_revisions.values():
            if v['timestamp'] in (ts1, ts2):
                self.assertFalse(hasattr(v, 'commenthidden'))
                self.assertFalse(hasattr(v, 'userhidden'))
                self.assertFalse(hasattr(v, 'filehidden'))

        # Cleanup
        site.deleterevs('oldimage', [20210314184415, 20210314184430],
                        show='content|comment|user',
                        reason='pywikibot unit tests',
                        target='File:T276726.png')

    def test_delete_oldimage(self):
        """Test deleting and undeleting specific versions of files."""
        site = self.get_site()
        fp = pywikibot.FilePage(site, 'File:T276725.png')

        # Verify state
        gen = site.filearchive(start='T276725.png', end='T276725.pngg')
        fileid = None

        for filearchive in gen:
            fileid = filearchive['id']
            break

        if fileid is not None:
            site.undelete(fp, 'pywikibot unit tests', fileids=[fileid])

        # Delete the older version of file
        hist = fp.get_file_history()
        ts = pywikibot.Timestamp(2021, 3, 8, 2, 38, 57)
        oldimageid = hist[ts]['archivename']

        site.delete(fp, 'pywikibot unit tests', oldimage=oldimageid)

        # Undelete the older revision of file
        gen = site.filearchive(start='T276725.png', end='T276725.pngg')
        fileid = None

        for filearchive in gen:
            fileid = filearchive['id']
            break

        self.assertIsNotNone(fileid)

        site.undelete(fp, 'pywikibot unit tests', fileids=[fileid])


class TestUsernameInUsers(DefaultSiteTestCase):

    """Test that the user account can be found in users list."""

    login = True
    cached = True

    def test_username_in_users(self):
        """Test the site.users() method with bot username."""
        mysite = self.get_site()
        us = list(mysite.users(mysite.user()))
        self.assertLength(us, 1)
        self.assertIsInstance(us[0], dict)


class TestSiteExtensions(WikimediaDefaultSiteTestCase):

    """Test cases for Site extensions."""

    cached = True

    def test_extensions(self):
        """Test Extensions."""
        mysite = self.get_site()
        # test automatically getting extensions cache
        if 'extensions' in mysite.siteinfo:
            del mysite.siteinfo._cache['extensions']
        self.assertTrue(mysite.has_extension('Disambiguator'))

        # test case-sensitivity
        self.assertFalse(mysite.has_extension('disambiguator'))

        self.assertFalse(mysite.has_extension('ThisExtensionDoesNotExist'))


class TestSiteLoadRevisionsCaching(BasePageLoadRevisionsCachingTestBase,
                                   DefaultSiteTestCase):

    """Test site.loadrevisions() caching."""

    def setUp(self):
        """Setup tests."""
        self._page = self.get_mainpage(force=True)
        super().setUp()

    def test_page_text(self):
        """Test site.loadrevisions() with Page.text."""
        self._test_page_text()


class TestCommonsSite(TestCase):

    """Test cases for Site methods on Commons."""

    family = 'commons'
    code = 'commons'

    cached = True

    def test_interwiki_forward(self):
        """Test interwiki forward."""
        self.site = self.get_site()
        self.mainpage = pywikibot.Page(pywikibot.Link('Main Page', self.site))
        # test pagelanglinks on commons,
        # which forwards interwikis to wikipedia
        ll = next(self.site.pagelanglinks(self.mainpage))
        self.assertIsInstance(ll, pywikibot.Link)
        self.assertEqual(ll.site.family.name, 'wikipedia')


class TestWiktionarySite(TestCase):

    """Test Site Object on English Wiktionary."""

    family = 'wiktionary'
    code = 'en'

    cached = True

    def test_namespace_case(self):
        """Test namespace case."""
        site = self.get_site()

        main_namespace = site.namespaces[0]
        self.assertEqual(main_namespace.case, 'case-sensitive')
        user_namespace = site.namespaces[2]
        self.assertEqual(user_namespace.case, 'first-letter')


class TestNonEnglishWikipediaSite(TestCase):

    """Test Site Object on Nynorsk Wikipedia."""

    family = 'wikipedia'
    code = 'nn'

    cached = True

    def test_namespace_aliases(self):
        """Test namespace aliases."""
        site = self.get_site()

        namespaces = site.namespaces
        image_namespace = namespaces[6]
        self.assertEqual(image_namespace.custom_name, 'Fil')
        self.assertEqual(image_namespace.canonical_name, 'File')
        self.assertEqual(str(image_namespace), ':File:')
        self.assertEqual(image_namespace.custom_prefix(), ':Fil:')
        self.assertEqual(image_namespace.canonical_prefix(), ':File:')
        self.assertEqual(sorted(image_namespace.aliases), ['Bilde', 'Image'])
        self.assertLength(image_namespace, 4)

        self.assertIsEmpty(namespaces[1].aliases)
        self.assertLength(namespaces[4].aliases, 1)
        self.assertEqual(namespaces[4].aliases[0], 'WP')
        self.assertIn('WP', namespaces[4])


class TestUploadEnabledSite(TestCase):

    """Test Site.is_uploaddisabled."""

    sites = {
        'wikidatatest': {
            'family': 'wikidata',
            'code': 'test',
            'disabled': True,
        },
        'wikipediatest': {
            'family': 'wikipedia',
            'code': 'test',
            'disabled': False,
        }
    }

    login = True

    def test_is_uploaddisabled(self, key):
        """Test is_uploaddisabled()."""
        site = self.get_site(key)
        self.assertEqual(site.is_uploaddisabled(), self.sites[key]['disabled'])


class TestSametitleSite(TestCase):

    """Test APISite.sametitle on sites with known behaviour."""

    sites = {
        'enwp': {
            'family': 'wikipedia',
            'code': 'en',
        },
        'dewp': {
            'family': 'wikipedia',
            'code': 'de',
        },
        'enwt': {
            'family': 'wiktionary',
            'code': 'en',
        }
    }

    def test_enwp(self):
        """Test sametitle for enwp."""
        self.assertTrue(self.get_site('enwp').sametitle('Foo', 'foo'))
        self.assertFalse(self.get_site('enwp').sametitle(
            'Template:Test template', 'Template:Test Template'))

    def test_dewp(self):
        """Test sametitle for dewp."""
        site = self.get_site('dewp')
        self.assertTrue(site.sametitle('Foo', 'foo'))
        self.assertTrue(site.sametitle('Benutzer:Foo', 'User:Foo'))
        self.assertTrue(site.sametitle('Benutzerin:Foo', 'User:Foo'))
        self.assertTrue(site.sametitle('Benutzerin:Foo', 'Benutzer:Foo'))

    def test_enwt(self):
        """Test sametitle for enwt."""
        self.assertFalse(self.get_site('enwt').sametitle('Foo', 'foo'))

    def test_general(self, code):
        """Test sametitle."""
        site = self.get_site(code)
        self.assertTrue(site.sametitle('File:Foo', 'Image:Foo'))
        self.assertTrue(site.sametitle(':Foo', 'Foo'))
        self.assertFalse(site.sametitle('User:Foo', 'Foo'))
        self.assertFalse(site.sametitle('User:Foo', 'Project:Foo'))

        self.assertTrue(site.sametitle('Namespace:', 'Namespace:'))

        self.assertFalse(site.sametitle('Invalid:Foo', 'Foo'))
        self.assertFalse(site.sametitle('Invalid1:Foo', 'Invalid2:Foo'))
        self.assertFalse(site.sametitle('Invalid:Foo', ':Foo'))
        self.assertFalse(site.sametitle('Invalid:Foo', 'Invalid:foo'))


class TestLinktrails(TestCase):

    """Test linktrail method."""

    family = 'wikipedia'
    code = 'test'

    def test_has_linktrail(self):
        """Verify that every code has a linktrail.

        Test all smallest wikis and the others randomly.
        """
        size = 20
        small_wikis = self.site.family.languages_by_size[-size:]
        great_wikis = self.site.family.languages_by_size[:-size]
        great_wikis = random.sample(great_wikis, size)
        # Also test for 'hr' which failed due to T378787
        for code in {'hr', *small_wikis, *great_wikis}:
            site = pywikibot.Site(code, self.family)
            with self.subTest(site=site):
                self.assertIsInstance(site.linktrail(), str)

    def test_linktrails(self):
        """Test special linktrails.

        This is a subset of the old `family.linktrails` dict.
        """
        linktrails = {
            'ami': '',
            'bug': '[a-z]*',
            'ca': "(?:[a-zàèéíòóúç·ïü]|'(?!'))*",
            'da': '[a-zæøå]*',
            'ext': '[a-záéíóúñ]*',
            'fa': '[ابپتثجچحخدذرزژسشصضطظعغفقکگلمنوهیآأئؤة‌]*',
            'gu': '[઀-૿]*',
            'he': '[a-zא-ת]*',
            'ii': '',
            'jv': '[a-z]*',
            'kaa': "(?:[a-zıʼ’“»]|'(?!'))*",
            'lez': '[a-zабвгдеёжзийклмнопрстуфхцчшщъыьэюяӀ]*',
            'mai': '[a-zऀ-ॣ०-꣠-ꣿ]*',
            'nds-nl': '[a-zäöüïëéèà]*',
            'or': '[a-z଀-୿]*',
            'pt': '[áâãàéêẽçíòóôõq̃úüűũa-z]*',
            'qu': '[a-záéíóúñ]*',
            'roa-rup': '[a-zăâîşţșțĂÂÎŞŢȘȚ]*',
            'sa': '[a-zऀ-ॣ०-꣠-ꣿ]*',
            'te': '[ఁ-౯]*',
            'uz': '[a-zʻʼ“»]*',
            'vec': '[a-zàéèíîìóòúù]*',
            'wuu': '',
            'xmf': '[a-zაბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰ“»]*',
            'yi': '[a-zא-ת]*',
            'zh': ''
        }
        for code, linktrail in linktrails.items():
            site = pywikibot.Site(code, self.family)
            with self.subTest(site=site):
                self.assertEqual(site.linktrail(), linktrail)


class TestSingleCodeFamilySite(AlteredDefaultSiteTestCase):

    """Test single code family sites."""

    sites = {
        'i18n': {
            'family': 'i18n',
            'code': 'i18n',
        },
    }

    def test_twn(self):
        """Test translatewiki.net."""
        url = 'translatewiki.net'
        site = self.get_site('i18n')
        self.assertEqual(site.hostname(), url)
        self.assertEqual(site.code, 'i18n')
        self.assertIsInstance(site.namespaces, Mapping)
        self.assertFalse(site.obsolete)
        self.assertEqual(site.family.hostname('en'), url)
        self.assertEqual(site.family.hostname('i18n'), url)
        self.assertEqual(site.family.hostname('translatewiki'), url)


class TestSubdomainFamilySite(TestCase):

    """Test subdomain family site."""

    code = 'en'
    family = 'wowwiki'

    def test_wow(self):
        """Test wowwiki.fandom.com."""
        url = 'wowwiki-archive.fandom.com'
        site = self.site
        self.assertEqual(site.hostname(), url)
        self.assertEqual(site.code, 'en')
        self.assertIsInstance(site.namespaces, Mapping)
        self.assertFalse(site.obsolete)
        self.assertEqual(site.family.hostname('en'), url)

        with self.assertRaises(KeyError):
            site.family.hostname('wow')
        with self.assertRaises(KeyError):
            site.family.hostname('wowwiki')
        with self.assertRaises(UnknownSiteError):
            pywikibot.Site('wowwiki')
        with self.assertRaises(UnknownSiteError):
            pywikibot.Site('ceb', 'wowwiki')


class TestProductionAndTestSite(AlteredDefaultSiteTestCase):

    """Test site without other production sites in its family."""

    sites = {
        'commons': {
            'family': 'commons',
            'code': 'commons',
        },
        'beta': {
            'family': 'commons',
            'code': 'beta',
        },
        'wikidata': {
            'family': 'wikidata',
            'code': 'wikidata',
        },
        'wikidatatest': {
            'family': 'wikidata',
            'code': 'test',
        },
    }

    def test_commons(self):
        """Test Wikimedia Commons."""
        site = self.get_site('commons')
        self.assertEqual(site.hostname(), 'commons.wikimedia.org')
        self.assertEqual(site.code, 'commons')
        self.assertIsInstance(site.namespaces, Mapping)
        self.assertFalse(site.obsolete)

        with self.assertRaises(KeyError):
            site.family.hostname('en')

        pywikibot.config.family = 'commons'
        pywikibot.config.mylang = 'de'

        site2 = pywikibot.Site('beta')
        self.assertEqual(site2.hostname(),
                         'commons.wikimedia.beta.wmflabs.org')
        self.assertEqual(site2.code, 'beta')
        self.assertFalse(site2.obsolete)

        with self.assertRaises(UnknownSiteError):
            pywikibot.Site()

    def test_wikidata(self):
        """Test Wikidata family, with sites for test and production."""
        site = self.get_site('wikidata')
        self.assertEqual(site.hostname(), 'www.wikidata.org')
        self.assertEqual(site.code, 'wikidata')
        self.assertIsInstance(site.namespaces, Mapping)
        self.assertFalse(site.obsolete)

        with self.assertRaises(KeyError):
            site.family.hostname('en')

        pywikibot.config.family = 'wikidata'
        pywikibot.config.mylang = 'en'

        site2 = pywikibot.Site('test')
        self.assertEqual(site2.hostname(), 'test.wikidata.org')
        self.assertEqual(site2.code, 'test')

        # Languages can't be used due to T71255
        with self.assertRaises(UnknownSiteError):
            pywikibot.Site('en', 'wikidata')


class TestSiteProofreadinfo(DefaultSiteTestCase):

    """Test proofreadinfo information."""

    sites = {
        'en-ws': {
            'family': 'wikisource',
            'code': 'en',
        },
        'en-wp': {
            'family': 'wikipedia',
            'code': 'en',
        },
    }

    cached = True

    def test_cache_proofreadinfo_on_site_with_proofreadpage(self):
        """Test Site._cache_proofreadinfo()."""
        site = self.get_site('en-ws')
        ql_res = {0: 'Without text', 1: 'Not proofread', 2: 'Problematic',
                  3: 'Proofread', 4: 'Validated'}

        site._cache_proofreadinfo()
        self.assertEqual(site.namespaces[106], site.proofread_index_ns)
        self.assertEqual(site.namespaces[104], site.proofread_page_ns)
        self.assertEqual(site.proofread_levels, ql_res)
        self.assertEqual(site.namespaces[106], site.proofread_index_ns)
        del site._proofread_page_ns  # Check that property reloads.
        self.assertEqual(site.namespaces[104], site.proofread_page_ns)

    def test_cache_proofreadinfo_on_site_without_proofreadpage(self):
        """Test Site._cache_proofreadinfo()."""
        site = self.get_site('en-wp')
        with self.assertRaises(UnknownExtensionError):
            site._cache_proofreadinfo()
        with self.assertRaises(UnknownExtensionError):
            site.proofread_index_ns
        with self.assertRaises(UnknownExtensionError):
            site.proofread_page_ns
        with self.assertRaises(UnknownExtensionError):
            site.proofread_levels


class TestPropertyNames(DefaultSiteTestCase):

    """Test Special:PagesWithProp method."""

    sites = {
        'en-ws': {
            'family': 'wikisource',
            'code': 'en',
        },
        'de-wp': {
            'family': 'wikipedia',
            'code': 'de',
        },
    }

    cached = True

    def test_get_property_names(self, key):
        """Test get_property_names method."""
        mysite = self.get_site(key)
        pnames = mysite.get_property_names()
        self.assertIsInstance(pnames, list)
        for item in ('defaultsort', 'disambiguation', 'displaytitle',
                     'expectunusedcategory', 'forcetoc', 'hiddencat', 'index',
                     'jsonconfig_getdata', 'newsectionlink', 'noeditsection',
                     'nogallery', 'noindex', 'nonewsectionlink', 'notoc',
                     'score', 'templatedata', 'unexpectedUnconnectedPage',
                     'wikibase-badge-Q17437796', 'wikibase-badge-Q17437798',
                     'wikibase-badge-Q70894304', 'wikibase_item'):
            with self.subTest(item=item):
                self.assertIn(item, pnames)


class TestPageFromWikibase(DefaultSiteTestCase):

    """Test page_from_repository method."""

    sites = {
        'it-wb': {
            'family': 'wikibooks',
            'code': 'it',
            'result': 'Hello world',
        },
        'de-wp': {
            'family': 'wikipedia',
            'code': 'de',
            'result': 'Hallo-Welt-Programm',
        },
        'en-wp': {
            'family': 'wikipedia',
            'code': 'en',
            'result': '"Hello, World!" program',
        },
    }

    ITEM = 'Q131303'

    def test_page_from_repository(self, key):
        """Validate page_from_repository."""
        site = self.get_site(key)
        page = site.page_from_repository(self.ITEM)
        self.assertIsInstance(page, pywikibot.Page)
        self.assertEqual(page.title(), self.sites[key]['result'])

    def test_page_from_repository_none(self):
        """Validate page_from_repository return NoneType."""
        site = pywikibot.Site('pdc', 'wikipedia')
        page = site.page_from_repository(self.ITEM)
        self.assertIsNone(page)


class TestCategoryFromWikibase(DefaultSiteTestCase):

    """Test page_from_repository method."""

    sites = {
        'it-ws': {
            'family': 'wikisource',
            'code': 'it',
            'result': 'Categoria:2016',
        },
        'de-wp': {
            'family': 'wikipedia',
            'code': 'de',
            'result': 'Kategorie:2016',
        },
        'en-wp': {
            'family': 'wikipedia',
            'code': 'en',
            'result': 'Category:2016',
        },
    }

    ITEM = 'Q6939656'

    def test_page_from_repository(self, key):
        """Validate page_from_repository."""
        site = self.get_site(key)
        page = site.page_from_repository(self.ITEM)
        self.assertIsInstance(page, pywikibot.Category)
        self.assertEqual(page.title(), self.sites[key]['result'])

    def test_page_from_repository_none(self):
        """Validate page_from_repository return NoneType."""
        site = pywikibot.Site('pdc', 'wikipedia')
        page = site.page_from_repository(self.ITEM)
        self.assertIsNone(page)


if __name__ == '__main__':
    with suppress(SystemExit):
        unittest.main()