datatracker/ietf/doc/tests_utils.py

431 lines
20 KiB
Python

# Copyright The IETF Trust 2020, All Rights Reserved
import datetime
import debug # pyflakes:ignore
from unittest.mock import patch
from django.db import IntegrityError
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.name.models import DocTagName
from ietf.person.factories import PersonFactory
from ietf.utils.test_utils import TestCase, name_of_file_containing
from ietf.person.models import Person
from ietf.doc.factories import DocumentFactory, WgRfcFactory, WgDraftFactory
from ietf.doc.models import State, DocumentActionHolder, DocumentAuthor, Document
from ietf.doc.utils import (update_action_holders, add_state_change_event, update_documentauthors,
fuzzy_find_documents, rebuild_reference_relations)
from ietf.utils.draft import Draft, PlaintextDraft
from ietf.utils.xmldraft import XMLDraft
class ActionHoldersTests(TestCase):
def setUp(self):
"""Set up helper for the update_action_holders tests"""
super().setUp()
self.authors = PersonFactory.create_batch(3)
self.ad = Person.objects.get(user__username='ad')
self.group = GroupFactory()
RoleFactory(name_id='ad', group=self.group, person=self.ad)
def doc_in_iesg_state(self, slug):
return DocumentFactory(authors=self.authors, group=self.group, ad=self.ad, states=[('draft-iesg', slug)])
def update_doc_state(self, doc, new_state, add_tags=None, remove_tags=None):
"""Update document state/tags, create change event, and save"""
prev_tags = list(doc.tags.all()) # list to make sure we retrieve now
# prev_action_holders = list(doc.action_holders.all())
prev_state = doc.get_state(new_state.type_id)
if new_state != prev_state:
doc.set_state(new_state)
if add_tags:
doc.tags.add(*DocTagName.objects.filter(slug__in=add_tags))
if remove_tags:
doc.tags.remove(*DocTagName.objects.filter(slug__in=remove_tags))
new_tags = list(doc.tags.all())
events = []
e = add_state_change_event(
doc,
Person.objects.get(name='(System)'),
prev_state, new_state,
prev_tags, new_tags)
self.assertIsNotNone(e, 'Test logic error')
events.append(e)
e = update_action_holders(doc, prev_state, new_state, prev_tags, new_tags)
if e:
events.append(e)
doc.save_with_history(events)
def test_update_action_holders_by_state(self):
"""Doc action holders should auto-update correctly on state change"""
# Test the transition from every state to each of its 'next_states'
for initial_state in State.objects.filter(type__slug='draft-iesg'):
for next_state in initial_state.next_states.all():
# Test with no action holders initially
doc = DocumentFactory(
authors=self.authors,
group=self.group,
ad=self.ad,
states=[('draft-iesg', initial_state.slug)],
)
docevents_before = set(doc.docevent_set.all())
self.update_doc_state(doc, next_state)
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
if next_state.slug in DocumentActionHolder.CLEAR_ACTION_HOLDERS_STATES:
self.assertCountEqual(doc.action_holders.all(), [])
self.assertEqual(len(new_docevents), 1)
else:
self.assertCountEqual(
doc.action_holders.all(), [doc.ad],
'AD should be only action holder after transition to %s' % next_state.slug)
self.assertEqual(len(new_docevents), 2)
change_event = doc.latest_event(type='changed_action_holders')
self.assertIn(change_event, new_docevents)
self.assertIn('Changed action holders', change_event.desc)
self.assertIn(doc.ad.name, change_event.desc)
doc.delete() # clean up for next iteration
# Test with action holders initially
doc = DocumentFactory(
authors=self.authors,
group=self.group,
ad=self.ad,
states=[('draft-iesg', initial_state.slug)],
)
doc.action_holders.add(*self.authors) # adds all authors
docevents_before = set(doc.docevent_set.all())
self.update_doc_state(doc, next_state)
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
self.assertEqual(len(new_docevents), 2)
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
change_event = doc.latest_event(type='changed_action_holders')
self.assertIn(change_event, new_docevents)
if next_state.slug in DocumentActionHolder.CLEAR_ACTION_HOLDERS_STATES:
self.assertCountEqual(doc.action_holders.all(), [])
self.assertIn('Removed all action holders', change_event.desc)
else:
self.assertCountEqual(
doc.action_holders.all(), [doc.ad],
'AD should be only action holder after transition to %s' % next_state.slug)
self.assertIn('Changed action holders', change_event.desc)
self.assertIn(doc.ad.name, change_event.desc)
doc.delete() # clean up for next iteration
def test_update_action_holders_with_no_ad(self):
"""A document with no AD should be handled gracefully"""
doc = self.doc_in_iesg_state('idexists')
doc.ad = None
doc.save()
docevents_before = set(doc.docevent_set.all())
self.update_doc_state(doc, State.objects.get(slug='pub-req'))
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
self.assertEqual(len(new_docevents), 1)
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
self.assertCountEqual(doc.action_holders.all(), [])
def test_update_action_holders_resets_age(self):
"""Action holder age should reset when document state changes"""
doc = self.doc_in_iesg_state('pub-req')
doc.action_holders.set([self.ad])
dah = doc.documentactionholder_set.get(person=self.ad)
dah.time_added = datetime.datetime(2020, 1, 1) # arbitrary date in the past
dah.save()
self.assertNotEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
self.update_doc_state(doc, State.objects.get(slug='ad-eval'))
self.assertEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
def test_update_action_holders_add_tag_need_rev(self):
"""Adding need-rev tag adds authors as action holders"""
doc = self.doc_in_iesg_state('pub-req')
first_author = self.authors[0]
doc.action_holders.add(first_author)
self.assertCountEqual(doc.action_holders.all(), [first_author])
self.update_doc_state(doc,
doc.get_state('draft-iesg'),
add_tags=['need-rev'],
remove_tags=None)
self.assertCountEqual(doc.action_holders.all(), self.authors)
def test_update_action_holders_add_tag_need_rev_no_dups(self):
"""Adding need-rev tag does not duplicate existing action holders"""
doc = self.doc_in_iesg_state('pub-req')
self.assertCountEqual(doc.action_holders.all(), [])
self.update_doc_state(doc,
doc.get_state('draft-iesg'),
add_tags=['need-rev'],
remove_tags=None)
self.assertCountEqual(doc.action_holders.all(), self.authors)
def test_update_action_holders_remove_tag_need_rev(self):
"""Removing need-rev tag drops authors as action holders"""
doc = self.doc_in_iesg_state('pub-req')
doc.tags.add(DocTagName.objects.get(slug='need-rev'))
self.assertEqual(doc.action_holders.count(), 0)
self.update_doc_state(doc,
doc.get_state('draft-iesg'),
add_tags=None,
remove_tags=['need-rev'])
self.assertEqual(doc.action_holders.count(), 0)
def test_update_action_holders_add_tag_need_rev_ignores_non_authors(self):
"""Adding need-rev tag does not affect existing action holders"""
doc = self.doc_in_iesg_state('pub-req')
doc.action_holders.add(self.ad)
self.assertCountEqual(doc.action_holders.all(),[self.ad])
self.update_doc_state(doc,
doc.get_state('draft-iesg'),
add_tags=['need-rev'],
remove_tags=None)
self.assertCountEqual(doc.action_holders.all(), [self.ad] + self.authors)
def test_update_action_holders_remove_tag_need_rev_ignores_non_authors(self):
"""Removing need-rev tag does not affect non-author action holders"""
doc = self.doc_in_iesg_state('pub-req')
doc.tags.add(DocTagName.objects.get(slug='need-rev'))
doc.action_holders.add(self.ad)
self.assertCountEqual(doc.action_holders.all(), [self.ad])
self.update_doc_state(doc,
doc.get_state('draft-iesg'),
add_tags=None,
remove_tags=['need-rev'])
self.assertCountEqual(doc.action_holders.all(), [self.ad])
def test_doc_action_holders_enabled(self):
"""Action holders should only be enabled in certain states"""
doc = self.doc_in_iesg_state('idexists')
self.assertFalse(doc.action_holders_enabled())
for state in State.objects.filter(type='draft-iesg').exclude(slug='idexists'):
doc.set_state(state)
self.assertTrue(doc.action_holders_enabled())
class MiscTests(TestCase):
def test_update_documentauthors_with_nulls(self):
"""A 'None' value in the affiliation/country should be handled correctly"""
author_person = PersonFactory()
doc = DocumentFactory(authors=[author_person])
doc.documentauthor_set.update(
affiliation='Some Affiliation', country='USA'
)
try:
events = update_documentauthors(
doc,
[
DocumentAuthor(
person=author_person,
email=author_person.email(),
affiliation=None,
country=None,
)
],
)
except IntegrityError as err:
self.fail('IntegrityError was raised: {}'.format(err))
self.assertEqual(len(events), 1)
self.assertEqual(events[0].type, 'edited_authors')
self.assertIn('cleared affiliation (was "Some Affiliation")', events[0].desc)
self.assertIn('cleared country (was "USA")', events[0].desc)
docauth = doc.documentauthor_set.first()
self.assertEqual(docauth.affiliation, '')
self.assertEqual(docauth.country, '')
def do_fuzzy_find_documents_rfc_test(self, name):
rfc = WgRfcFactory(name=name, create_revisions=(0, 1, 2))
rfc = Document.objects.get(pk=rfc.pk) # clear out any cached values
# by canonical name
found = fuzzy_find_documents(rfc.canonical_name(), None)
self.assertCountEqual(found.documents, [rfc])
self.assertEqual(found.matched_rev, None)
self.assertEqual(found.matched_name, rfc.canonical_name())
# by draft name, no rev
found = fuzzy_find_documents(rfc.name, None)
self.assertCountEqual(found.documents, [rfc])
self.assertEqual(found.matched_rev, None)
self.assertEqual(found.matched_name, rfc.name)
# by draft name, latest rev
found = fuzzy_find_documents(rfc.name, '02')
self.assertCountEqual(found.documents, [rfc])
self.assertEqual(found.matched_rev, '02')
self.assertEqual(found.matched_name, rfc.name)
# by draft name, earlier rev
found = fuzzy_find_documents(rfc.name, '01')
self.assertCountEqual(found.documents, [rfc])
self.assertEqual(found.matched_rev, '01')
self.assertEqual(found.matched_name, rfc.name)
# wrong name or revision
found = fuzzy_find_documents(rfc.name + '-incorrect')
self.assertCountEqual(found.documents, [], 'Should not find document that does not match')
found = fuzzy_find_documents(rfc.name + '-incorrect', '02')
self.assertCountEqual(found.documents, [], 'Still should not find document, even with a version')
found = fuzzy_find_documents(rfc.name, '22')
self.assertCountEqual(found.documents, [rfc],
'Should find document even if rev does not exist')
def test_fuzzy_find_documents(self):
# Should add additional tests/test cases for other document types/name formats
self.do_fuzzy_find_documents_rfc_test('draft-normal-name')
self.do_fuzzy_find_documents_rfc_test('draft-name-with-number-01')
self.do_fuzzy_find_documents_rfc_test('draft-name-that-has-two-02-04')
self.do_fuzzy_find_documents_rfc_test('draft-wild-01-numbers-0312')
class RebuildReferenceRelationsTests(TestCase):
def setUp(self):
super().setUp()
self.doc = WgDraftFactory() # document under test
# Other documents that should be found by rebuild_reference_relations
self.normative, self.informative, self.unknown = WgRfcFactory.create_batch(3)
for relationship in ['refnorm', 'refinfo', 'refunk', 'refold']:
self.doc.relateddocument_set.create(
target=WgRfcFactory().docalias.first(),
relationship_id=relationship,
)
self.updated = WgRfcFactory() # related document that should be left alone
self.doc.relateddocument_set.create(target=self.updated.docalias.first(), relationship_id='updates')
self.assertCountEqual(self.doc.relateddocument_set.values_list('relationship__slug', flat=True),
['refnorm', 'refinfo', 'refold', 'refunk', 'updates'],
'Test conditions set up incorrectly: wrong prior document relationships')
for other_doc in [self.normative, self.informative, self.unknown]:
self.assertEqual(
self.doc.relateddocument_set.filter(target__name=other_doc.canonical_name()).count(),
0,
'Test conditions set up incorrectly: new documents already related',
)
def _get_refs_return_value(self):
return {
self.normative.canonical_name(): Draft.REF_TYPE_NORMATIVE,
self.informative.canonical_name(): Draft.REF_TYPE_INFORMATIVE,
self.unknown.canonical_name(): Draft.REF_TYPE_UNKNOWN,
'draft-not-found': Draft.REF_TYPE_NORMATIVE,
}
def test_requires_txt_or_xml(self):
result = rebuild_reference_relations(self.doc, {})
self.assertCountEqual(result.keys(), ['errors'])
self.assertEqual(len(result['errors']), 1)
self.assertIn('No draft text available', result['errors'][0],
'Error should be reported if no draft file is given')
result = rebuild_reference_relations(self.doc, {'md': 'cant-do-this.md'})
self.assertCountEqual(result.keys(), ['errors'])
self.assertEqual(len(result['errors']), 1)
self.assertIn('No draft text available', result['errors'][0],
'Error should be reported if no XML or plaintext file is given')
@patch.object(XMLDraft, 'get_refs')
@patch.object(XMLDraft, '__init__', return_value=None)
def test_xml(self, mock_init, mock_get_refs):
"""Should build reference relations with only XML"""
mock_get_refs.return_value = self._get_refs_return_value()
result = rebuild_reference_relations(self.doc, {'xml': 'file.xml'})
# if the method of calling the XMLDraft() constructor changes, this will need to be updated
xmldraft_init_args, _ = mock_init.call_args
self.assertEqual(xmldraft_init_args, ('file.xml',), 'XMLDraft initialized with unexpected arguments')
self.assertEqual(
result,
{
'warnings': ['There were 1 references with no matching DocAlias'],
'unfound': ['draft-not-found'],
}
)
self.assertCountEqual(
self.doc.relateddocument_set.values_list('target__name', 'relationship__slug'),
[
(self.normative.canonical_name(), 'refnorm'),
(self.informative.canonical_name(), 'refinfo'),
(self.unknown.canonical_name(), 'refunk'),
(self.updated.docalias.first().name, 'updates'),
]
)
@patch.object(PlaintextDraft, 'get_refs')
@patch.object(PlaintextDraft, '__init__', return_value=None)
def test_plaintext(self, mock_init, mock_get_refs):
"""Should build reference relations with only plaintext"""
mock_get_refs.return_value = self._get_refs_return_value()
with name_of_file_containing('contents') as temp_file_name:
result = rebuild_reference_relations(self.doc, {'txt': temp_file_name})
# if the method of calling the PlaintextDraft() constructor changes, this test will need to be updated
_, mock_init_kwargs = mock_init.call_args
self.assertEqual(mock_init_kwargs, {'text': 'contents', 'source': temp_file_name},
'PlaintextDraft initialized with unexpected arguments')
self.assertEqual(
result,
{
'warnings': ['There were 1 references with no matching DocAlias'],
'unfound': ['draft-not-found'],
}
)
self.assertCountEqual(
self.doc.relateddocument_set.values_list('target__name', 'relationship__slug'),
[
(self.normative.canonical_name(), 'refnorm'),
(self.informative.canonical_name(), 'refinfo'),
(self.unknown.canonical_name(), 'refunk'),
(self.updated.docalias.first().name, 'updates'),
]
)
@patch.object(PlaintextDraft, '__init__')
@patch.object(XMLDraft, 'get_refs')
@patch.object(XMLDraft, '__init__', return_value=None)
def test_xml_and_plaintext(self, mock_init, mock_get_refs, mock_plaintext_init):
"""Should build reference relations with XML when plaintext also available"""
mock_get_refs.return_value = self._get_refs_return_value()
result = rebuild_reference_relations(self.doc, {'txt': 'file.txt', 'xml': 'file.xml'})
self.assertFalse(mock_plaintext_init.called, 'PlaintextDraft should not be used when XML is available')
# if the method of calling the XMLDraft() constructor changes, this will need to be updated
xmldraft_init_args, _ = mock_init.call_args
self.assertEqual(xmldraft_init_args, ('file.xml',), 'XMLDraft initialized with unexpected arguments')
self.assertEqual(
result,
{
'warnings': ['There were 1 references with no matching DocAlias'],
'unfound': ['draft-not-found'],
}
)
self.assertCountEqual(
self.doc.relateddocument_set.values_list('target__name', 'relationship__slug'),
[
(self.normative.canonical_name(), 'refnorm'),
(self.informative.canonical_name(), 'refinfo'),
(self.unknown.canonical_name(), 'refunk'),
(self.updated.docalias.first().name, 'updates'),
]
)