From c0ec4ac021826e19f1851a66e868a117bdfb6ed5 Mon Sep 17 00:00:00 2001 From: Andreas Kilde Lien Date: Tue, 30 Sep 2025 09:28:25 +0200 Subject: [PATCH] fix: Update mock imports to use unittest.mock for consistency --- test/test_article.py | 185 +++++++++++++++++++++++++++ test/test_contributions.py | 186 +++++++++++++++++++++++++++ test/test_more_filters.py | 256 +++++++++++++++++++++++++++++++++++++ test/test_revision.py | 153 ++++++++++++++++++++++ 4 files changed, 780 insertions(+) create mode 100644 test/test_article.py create mode 100644 test/test_contributions.py create mode 100644 test/test_more_filters.py create mode 100644 test/test_revision.py diff --git a/test/test_article.py b/test/test_article.py new file mode 100644 index 0000000..aa374fb --- /dev/null +++ b/test/test_article.py @@ -0,0 +1,185 @@ +# encoding=utf-8 +import unittest +from unittest.mock import Mock, patch, MagicMock +from datetime import datetime +import pytz +from collections import OrderedDict + +from ukbot.article import Article +from ukbot.revision import Revision + + +class TestArticle(unittest.TestCase): + + def setUp(self): + # Create a complete mock setup + self.site_mock = Mock() + self.site_mock.key = 'test.wikipedia.org' + self.site_mock.match_prefix = Mock(return_value=False) + self.site_mock.link_to = Mock(side_effect=lambda article: f'{article.site().key}:{article.name}') + + # Mock pages dict for MediaWiki API calls + page_mock = Mock() + page_mock.revisions = Mock(return_value=iter([{'timestamp': (2020, 1, 1, 0, 0, 0, 0, 0, 0)}])) + self.site_mock.pages = {'Test Article': page_mock} + + self.contest_mock = Mock() + self.contest_mock.sql = Mock() + self.contest_mock.sql.cursor = Mock() + self.contest_mock.sql.commit = Mock() + + self.user_mock = Mock() + self.user_mock.name = 'TestUser' + self.user_mock.point_deductions = [] + self.user_mock.revisions = OrderedDict() + self.user_mock.contest = Mock(return_value=self.contest_mock) + + self.article = Article(self.site_mock, self.user_mock, 'Test Article', ns='0') + + def test_init(self): + """Test Article initialization""" + self.assertEqual(self.article.name, 'Test Article') + self.assertEqual(self.article.ns, '0') + self.assertEqual(self.article.disqualified, False) + self.assertIsInstance(self.article.revisions, OrderedDict) + self.assertEqual(len(self.article.errors), 0) + + def test_key_property(self): + """Test article key generation""" + expected_key = 'test.wikipedia.org:Test Article' + self.assertEqual(self.article.key, expected_key) + + def test_site_method(self): + """Test site() method returns the site""" + self.assertEqual(self.article.site(), self.site_mock) + + def test_user_method(self): + """Test user() method returns the user""" + self.assertEqual(self.article.user(), self.user_mock) + + def test_add_revision(self): + """Test adding a revision to an article""" + rev = self.article.add_revision(123, timestamp=1234567890, username='TestUser') + + self.assertIsInstance(rev, Revision) + self.assertEqual(len(self.article.revisions), 1) + self.assertIn(123, self.article.revisions) + self.assertEqual(self.article.revisions[123], rev) + + def test_firstrev(self): + """Test firstrev property returns first revision""" + rev1 = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev2 = self.article.add_revision(200, timestamp=2000000000, username='TestUser') + + self.assertEqual(self.article.firstrev, rev1) + + def test_lastrev(self): + """Test lastrev property returns last revision""" + rev1 = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev2 = self.article.add_revision(200, timestamp=2000000000, username='TestUser') + + self.assertEqual(self.article.lastrev, rev2) + + def test_new_property_with_new_page(self): + """Test new property returns True for new pages""" + rev = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev.parentid = 0 + + self.assertTrue(self.article.new) + + def test_new_property_with_existing_page(self): + """Test new property returns False for existing pages""" + rev = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev.parentid = 50 + + self.assertFalse(self.article.new) + + def test_redirect_property(self): + """Test redirect property""" + rev = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev.text = '#REDIRECT [[Target]]' + + self.site_mock.redirect_regexp = Mock() + self.site_mock.redirect_regexp.search = Mock(return_value=True) + + self.assertTrue(self.article.redirect) + + def test_new_non_redirect(self): + """Test new_non_redirect property""" + rev = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev.parentid = 0 # Makes it a new page + rev.text = 'Normal content' + + # Mock redirect_regexp.match() to return None (no redirect) + redirect_mock = Mock() + redirect_mock.match = Mock(return_value=None) + self.site_mock.redirect_regexp = redirect_mock + + self.assertTrue(self.article.new_non_redirect) + + def test_bytes_property(self): + """Test bytes property calculation""" + rev1 = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev1.size = 200 + rev1.parentsize = 50 + rev2 = self.article.add_revision(200, timestamp=2000000000, username='TestUser') + rev2.size = 275 + rev2.parentsize = 200 + + # rev1.bytes = 200-50 = 150, rev2.bytes = 275-200 = 75, total = 225 + self.assertEqual(self.article.bytes, 225) + + def test_words_property(self): + """Test words property calculation""" + rev1 = self.article.add_revision(100, timestamp=1000000000, username='TestUser') + rev1.text = 'one two three four five six seven' # 7 words + rev1.parenttext = '' + rev2 = self.article.add_revision(200, timestamp=2000000000, username='TestUser') + rev2.text = 'one two three four five six seven eight nine ten' # 10 words + rev2.parenttext = 'one two three four five six seven' # 7 words, so 3 new words + + # rev1 adds 7 words, rev2 adds 3 words, total = 10 + self.assertEqual(self.article.words, 10) + + def test_link_method(self): + """Test link() method generates correct link""" + expected_link = 'test.wikipedia.org:Test Article' + self.assertEqual(self.article.link(), expected_link) + + def test_created_at_property(self): + """Test created_at property""" + # Test when _created_at is set + test_time = pytz.utc.localize(datetime(2020, 1, 1, 12, 0, 0)) + self.article._created_at = test_time + self.assertEqual(self.article.created_at, test_time) + + def test_created_at_from_firstrev(self): + """Test created_at falls back to firstrev timestamp""" + rev = self.article.add_revision(100, timestamp=1577836800.0, username='TestUser') # 2020-01-01 + + self.assertIsNotNone(self.article.created_at) + self.assertIsInstance(self.article.created_at, datetime) + + def test_eq_method(self): + """Test __eq__ method""" + article2 = Article(self.site_mock, self.user_mock, 'Test Article', ns='0') + article3 = Article(self.site_mock, self.user_mock, 'Different Article', ns='0') + + self.assertEqual(self.article, article2) + self.assertNotEqual(self.article, article3) + + def test_repr_and_str(self): + """Test __repr__ and __str__ methods""" + expected = 'Article(test.wikipedia.org, Test Article, TestUser)' + self.assertEqual(repr(self.article), expected) + self.assertEqual(str(self.article), expected) + + def test_hash(self): + """Test __hash__ method""" + article2 = Article(self.site_mock, self.user_mock, 'Test Article', ns='0') + + self.assertEqual(hash(self.article), hash(article2)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_contributions.py b/test/test_contributions.py new file mode 100644 index 0000000..ecf4350 --- /dev/null +++ b/test/test_contributions.py @@ -0,0 +1,186 @@ +# encoding=utf-8 +import unittest +from unittest.mock import Mock +from datetime import datetime +import pytz + +from ukbot.contributions import UserContribution, UserContributions + + +class TestUserContribution(unittest.TestCase): + + def setUp(self): + # Create mocks with specific return values + article_mock = Mock(name='Test Article') + site_mock = Mock(key='test.wiki') + user_mock = Mock(name='TestUser') + + article_mock.site = Mock(return_value=site_mock) + article_mock.user = Mock(return_value=user_mock) + + self.rev = Mock() + self.rev.revid = 12345 + self.rev.article = Mock(return_value=article_mock) + + self.rule = Mock() + self.rule.__class__.__name__ = 'TestRule' + + self.contribution = UserContribution( + rev=self.rev, + points=10.5, + rule=self.rule, + description='Test contribution' + ) + + def test_init(self): + """Test UserContribution initialization""" + self.assertEqual(self.contribution.rev, self.rev) + self.assertEqual(self.contribution.points, 10.5) + self.assertEqual(self.contribution.rule, self.rule) + self.assertEqual(self.contribution.description, 'Test contribution') + + def test_is_negative_false(self): + """Test is_negative returns False for positive points""" + self.assertFalse(self.contribution.is_negative()) + + def test_is_negative_true(self): + """Test is_negative returns True for negative points""" + negative_contrib = UserContribution( + rev=self.rev, + points=-5.0, + rule=self.rule, + description='Negative' + ) + self.assertTrue(negative_contrib.is_negative()) + + def test_article_property(self): + """Test article property returns article""" + self.assertEqual(self.contribution.article, self.rev.article()) + + def test_site_property(self): + """Test site property returns site""" + self.assertEqual(self.contribution.site, self.rev.article().site()) + + def test_user_property(self): + """Test user property returns user""" + self.assertEqual(self.contribution.user, self.rev.article().user()) + + +class TestUserContributions(unittest.TestCase): + + def setUp(self): + self.user = Mock() + self.user.name = 'TestUser' + self.user.point_deductions = [] + self.user.suspended_since = None + self.user.disqualified_articles = [] + + self.config = { + 'point_caps': {}, + 'wikidata_languages': ['en', 'es', 'fi'] + } + + self.contributions = UserContributions(self.user, self.config) + + def test_init(self): + """Test UserContributions initialization""" + # Note: self.contributions.user is a weakref, so we need to call it + self.assertEqual(self.contributions.user(), self.user) + self.assertEqual(self.contributions.wikidata_languages, ['en', 'es', 'fi']) + self.assertEqual(len(self.contributions.contributions), 0) + + def test_add_contribution(self): + """Test adding a contribution""" + article_mock = Mock(name='Article') + rev = Mock() + rev.revid = 100 + rev.article = Mock(return_value=article_mock) + + rule = Mock() + rule.maxpoints = None # No capping + + contrib = UserContribution( + rev=rev, + points=10, + rule=rule, + description='Test' + ) + + self.contributions.add(contrib) + + self.assertEqual(len(self.contributions.contributions), 1) + self.assertIn(contrib, self.contributions.contributions) + + def test_get_all_contributions(self): + """Test getting all contributions""" + article_mock = Mock(name='Article') + rev = Mock() + rev.revid = 100 + rev.article = Mock(return_value=article_mock) + + rule = Mock() + rule.maxpoints = None # No capping + + contrib1 = UserContribution(rev=rev, points=10, rule=rule, description='Test1') + contrib2 = UserContribution(rev=rev, points=5, rule=rule, description='Test2') + + self.contributions.add(contrib1) + self.contributions.add(contrib2) + + all_contribs = self.contributions.get() + self.assertEqual(len(all_contribs), 2) + + def test_get_contributions_by_revision(self): + """Test getting contributions by revision""" + article1_mock = Mock(name='Article1') + rev1 = Mock() + rev1.revid = 100 + rev1.article = Mock(return_value=article1_mock) + + article2_mock = Mock(name='Article2') + rev2 = Mock() + rev2.revid = 200 + rev2.article = Mock(return_value=article2_mock) + + rule = Mock() + rule.maxpoints = None # No capping + + contrib1 = UserContribution(rev=rev1, points=10, rule=rule, description='Test1') + contrib2 = UserContribution(rev=rev2, points=5, rule=rule, description='Test2') + + self.contributions.add(contrib1) + self.contributions.add(contrib2) + + rev1_contribs = self.contributions.get(revision=rev1) + self.assertEqual(len(rev1_contribs), 1) + self.assertEqual(rev1_contribs[0], contrib1) + + def test_sum_contributions(self): + """Test summing all contribution points""" + article_mock = Mock(name='Article') + article_mock.key = 'test:Article' + + rev = Mock() + rev.revid = 100 + rev.article = Mock(return_value=article_mock) + rev.utc = pytz.utc.localize(datetime(2020, 1, 1)) + rev.point_deductions = [] + + article_mock.revisions = {100: rev} + + rule = Mock() + rule.maxpoints = None # No capping + + contrib1 = UserContribution(rev=rev, points=10, rule=rule, description='Test1') + contrib2 = UserContribution(rev=rev, points=5.5, rule=rule, description='Test2') + + self.contributions.add(contrib1) + self.contributions.add(contrib2) + + # Sum using get_article_points + total = self.contributions.get_article_points(article_mock) + self.assertEqual(total, 15.5) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_more_filters.py b/test/test_more_filters.py new file mode 100644 index 0000000..69afd96 --- /dev/null +++ b/test/test_more_filters.py @@ -0,0 +1,256 @@ +# encoding=utf-8 +import unittest +from unittest.mock import Mock, MagicMock +from collections import OrderedDict + +from ukbot.filters import ( + ByteFilter, NewPageFilter, ExistingPageFilter, + NamespaceFilter, PageFilter, TemplateFilter +) + + +class TestByteFilter(unittest.TestCase): + + def test_byte_filter_keeps_articles_above_limit(self): + """Test ByteFilter keeps articles above byte limit""" + sites = Mock() + byte_filter = ByteFilter(sites, bytelimit=1000) + + article1 = Mock() + article1.bytes = 1500 + article1.key = 'site:Article1' + + article2 = Mock() + article2.bytes = 500 + article2.key = 'site:Article2' + + articles = OrderedDict([ + ('site:Article1', article1), + ('site:Article2', article2) + ]) + + result = byte_filter.filter(articles) + + self.assertEqual(len(result), 1) + self.assertIn('site:Article1', result) + self.assertNotIn('site:Article2', result) + + def test_test_page_method(self): + """Test test_page method""" + sites = Mock() + byte_filter = ByteFilter(sites, bytelimit=1000) + + page = Mock() + page.bytes = 1500 + + self.assertTrue(byte_filter.test_page(page)) + + page.bytes = 500 + self.assertFalse(byte_filter.test_page(page)) + + +class TestNewPageFilter(unittest.TestCase): + + def test_new_page_filter_keeps_new_pages(self): + """Test NewPageFilter keeps pages created within contest timeframe""" + from datetime import datetime + import pytz + + sites = Mock() + contest = Mock() + contest.start = pytz.utc.localize(datetime(2020, 1, 1)) + contest.end = pytz.utc.localize(datetime(2020, 12, 31)) + + npf = NewPageFilter(sites, contest, redirects=False) + + # Page created within contest + page1 = Mock() + page1.created_at = pytz.utc.localize(datetime(2020, 6, 15)) + page1.redirect = False + + # Page created before contest + page2 = Mock() + page2.created_at = pytz.utc.localize(datetime(2019, 6, 15)) + page2.redirect = False + + self.assertTrue(npf.test_page(page1)) + self.assertFalse(npf.test_page(page2)) + + def test_new_page_filter_excludes_redirects(self): + """Test NewPageFilter excludes redirects by default""" + from datetime import datetime + import pytz + + sites = Mock() + contest = Mock() + contest.start = pytz.utc.localize(datetime(2020, 1, 1)) + contest.end = pytz.utc.localize(datetime(2020, 12, 31)) + + npf = NewPageFilter(sites, contest, redirects=False) + + page = Mock() + page.created_at = pytz.utc.localize(datetime(2020, 6, 15)) + page.redirect = True + + self.assertFalse(npf.test_page(page)) + + def test_new_page_filter_includes_redirects_when_enabled(self): + """Test NewPageFilter includes redirects when enabled""" + from datetime import datetime + import pytz + + sites = Mock() + contest = Mock() + contest.start = pytz.utc.localize(datetime(2020, 1, 1)) + contest.end = pytz.utc.localize(datetime(2020, 12, 31)) + + npf = NewPageFilter(sites, contest, redirects=True) + + page = Mock() + page.created_at = pytz.utc.localize(datetime(2020, 6, 15)) + page.redirect = True + + self.assertTrue(npf.test_page(page)) + + +class TestExistingPageFilter(unittest.TestCase): + + def test_existing_page_filter_keeps_old_pages(self): + """Test ExistingPageFilter keeps pages created before contest""" + from datetime import datetime + import pytz + + sites = Mock() + contest = Mock() + contest.start = pytz.utc.localize(datetime(2020, 1, 1)) + + epf = ExistingPageFilter(sites, contest) + + # Page created before contest + page1 = Mock() + page1.created_at = pytz.utc.localize(datetime(2019, 6, 15)) + + # Page created during/after contest + page2 = Mock() + page2.created_at = pytz.utc.localize(datetime(2020, 6, 15)) + + self.assertTrue(epf.test_page(page1)) + self.assertFalse(epf.test_page(page2)) + + +class TestNamespaceFilter(unittest.TestCase): + + def test_namespace_filter_filters_by_namespace(self): + """Test NamespaceFilter filters by namespace""" + sites = Mock() + nsf = NamespaceFilter(sites, namespaces=['0', '14'], site=None) + + page1 = Mock() + page1.ns = '0' + page1.site = Mock(return_value=Mock(key='site1')) + + page2 = Mock() + page2.ns = '14' + page2.site = Mock(return_value=Mock(key='site1')) + + page3 = Mock() + page3.ns = '2' + page3.site = Mock(return_value=Mock(key='site1')) + + self.assertTrue(nsf.test_page(page1)) + self.assertTrue(nsf.test_page(page2)) + self.assertFalse(nsf.test_page(page3)) + + def test_namespace_filter_with_site_restriction(self): + """Test NamespaceFilter with site restriction""" + sites = Mock() + nsf = NamespaceFilter(sites, namespaces=['0'], site=['site1']) + + page1 = Mock() + page1.ns = '0' + page1.site = Mock(return_value=Mock(key='site1')) + + page2 = Mock() + page2.ns = '0' + page2.site = Mock(return_value=Mock(key='site2')) + + self.assertTrue(nsf.test_page(page1)) + self.assertFalse(nsf.test_page(page2)) + + +class TestPageFilter(unittest.TestCase): + + def test_page_filter_filters_specific_pages(self): + """Test PageFilter filters specific pages""" + sites = Mock() + + page_mock1 = Mock() + page_mock1.site = Mock(key='site1') + page_mock1.name = 'Page1' + + page_mock2 = Mock() + page_mock2.site = Mock(key='site1') + page_mock2.name = 'Page2' + + pf = PageFilter(sites, pages=[page_mock1, page_mock2]) + + # Test page that should match + test_page1 = Mock() + test_page1.key = 'site1:Page1' + + # Test page that should not match + test_page2 = Mock() + test_page2.key = 'site1:Page3' + + self.assertTrue(pf.test_page(test_page1)) + self.assertFalse(pf.test_page(test_page2)) + + +class TestTemplateFilter(unittest.TestCase): + + def test_text_contains_template(self): + """Test TemplateFilter detects templates in text""" + sites = Mock() + sites.homesite = Mock() + + # Mock template page + template_page = Mock() + template_page.exists = False + template_page.backlinks = Mock(return_value=[]) + + # Use MagicMock for pages to support __getitem__ + pages_mock = MagicMock() + pages_mock.__getitem__ = Mock(return_value=template_page) + sites.homesite.pages = pages_mock + + tf = TemplateFilter(sites, templates=['stub'], include_aliases=True) + + text_with_template = '{{stub}} This is stub text' + text_without_template = 'This is normal text' + + self.assertIsNotNone(tf.text_contains_template(text_with_template)) + self.assertIsNone(tf.text_contains_template(text_without_template)) + + def test_template_filter_with_parameters(self): + """Test TemplateFilter handles templates with parameters""" + sites = Mock() + sites.homesite = Mock() + + template_page = Mock() + template_page.exists = False + template_page.backlinks = Mock(return_value=[]) + + # Use MagicMock for pages to support __getitem__ + pages_mock = MagicMock() + pages_mock.__getitem__ = Mock(return_value=template_page) + sites.homesite.pages = pages_mock + + tf = TemplateFilter(sites, templates=['citation'], include_aliases=True) + + text = '{{citation|author=Smith|year=2020}}' + + self.assertIsNotNone(tf.text_contains_template(text)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_revision.py b/test/test_revision.py new file mode 100644 index 0000000..7ec6e7b --- /dev/null +++ b/test/test_revision.py @@ -0,0 +1,153 @@ +# encoding=utf-8 +import unittest +from unittest.mock import Mock +from datetime import datetime +import pytz +from collections import OrderedDict + +from ukbot.revision import Revision + + +class TestRevision(unittest.TestCase): + + def setUp(self): + # Create complete mock setup + self.site_mock = Mock() + self.site_mock.key = 'test.wikipedia.org' + self.site_mock.host = 'test.wikipedia.org' + self.site_mock.match_prefix = Mock(return_value=False) + + self.contest_mock = Mock() + self.contest_mock.wiki_tz = pytz.timezone('UTC') + + self.user_mock = Mock() + self.user_mock.name = 'TestUser' + self.user_mock.point_deductions = [] + self.user_mock.revisions = OrderedDict() + self.user_mock.contest = Mock(return_value=self.contest_mock) + + self.article = Mock() + self.article.name = 'Test Article' + self.article.site = Mock(return_value=self.site_mock) + self.article.user = Mock(return_value=self.user_mock) + + self.timestamp = 1577836800.0 # 2020-01-01 00:00:00 UTC + self.revision = Revision(self.article, 12345, timestamp=self.timestamp, username='TestUser') + + def test_init(self): + """Test Revision initialization""" + self.assertEqual(self.revision.revid, 12345) + self.assertEqual(self.revision.username, 'TestUser') + self.assertEqual(self.revision.text, '') + self.assertEqual(self.revision.parenttext, '') + self.assertFalse(self.revision.saved) + self.assertFalse(self.revision.dirty) + self.assertEqual(len(self.revision.errors), 0) + + def test_article_method(self): + """Test article() method returns article""" + self.assertEqual(self.revision.article(), self.article) + + def test_new_property_when_parentid_zero(self): + """Test new property returns True when parentid is 0""" + self.revision.parentid = 0 + self.assertTrue(self.revision.new) + + def test_new_property_when_has_parent(self): + """Test new property returns False when parentid is not 0""" + self.revision.parentid = 12344 + self.assertFalse(self.revision.new) + + def test_bytes_property(self): + """Test bytes property calculates diff correctly""" + self.revision.size = 500 + self.revision.parentsize = 300 + self.assertEqual(self.revision.bytes, 200) + + def test_bytes_property_negative(self): + """Test bytes property when content removed""" + self.revision.size = 300 + self.revision.parentsize = 500 + self.assertEqual(self.revision.bytes, -200) + + def test_words_property(self): + """Test words property calculates word diff""" + self.revision.text = 'one two three four five' + self.revision.parenttext = 'one two three' + + # Should be 2 new words (four, five) + self.assertEqual(self.revision.words, 2) + + def test_words_property_with_empty_parent(self): + """Test words on new page""" + self.revision.text = 'one two three' + self.revision.parenttext = '' + + self.assertEqual(self.revision.words, 3) + + def test_redirect_property(self): + """Test redirect property""" + self.revision.text = '#REDIRECT [[Target Page]]' + site_mock = Mock() + site_mock.redirect_regexp = Mock() + site_mock.redirect_regexp.search = Mock(return_value=True) + self.article.site = Mock(return_value=site_mock) + + self.assertTrue(self.revision.redirect) + + def test_parentredirect_property(self): + """Test parentredirect property""" + self.revision.parenttext = '#REDIRECT [[Old Target]]' + site_mock = Mock() + site_mock.redirect_regexp = Mock() + site_mock.redirect_regexp.search = Mock(return_value=True) + self.article.site = Mock(return_value=site_mock) + + self.assertTrue(self.revision.parentredirect) + + def test_get_link(self): + """Test get_link method""" + homesite = Mock() + homesite.host = 'test.wikipedia.org' + homesite.link_to = Mock(return_value=':test:Test Article') + + link = self.revision.get_link(homesite) + # Should contain revision ID in the link + self.assertIn('12345', link) + # When same host, should be just Special:Diff/revid + self.assertEqual(link, 'Special:Diff/12345') + + def test_utc_property(self): + """Test utc property returns UTC datetime""" + utc_time = self.revision.utc + self.assertIsInstance(utc_time, datetime) + self.assertEqual(utc_time.tzinfo, pytz.UTC) + + def test_wiki_tz_property(self): + """Test wiki_tz property returns datetime in wiki timezone""" + wiki_time = self.revision.wiki_tz + self.assertIsInstance(wiki_time, datetime) + + def test_repr_and_str(self): + """Test __repr__ and __str__ methods""" + expected = 'Revision(12345 of test.wikipedia.org:Test Article)' + self.assertEqual(repr(self.revision), expected) + self.assertEqual(str(self.revision), expected) + + def test_hash(self): + """Test __hash__ method""" + rev2 = Revision(self.article, 12345, timestamp=self.timestamp, username='TestUser') + + self.assertEqual(hash(self.revision), hash(rev2)) + + def test_add_point_deduction(self): + """Test add_point_deduction method""" + self.revision.add_point_deduction(10, 'Test reason') + + self.assertEqual(len(self.revision.point_deductions), 1) + self.assertEqual(self.revision.point_deductions[0][0], 10) + self.assertEqual(self.revision.point_deductions[0][1], 'Test reason') + + +if __name__ == '__main__': + unittest.main()