test_statements.py 17.7 KB
Newer Older
1
from django.test import TestCase
2
from django.urls import reverse
3
from django.utils.timezone import now
4
from django.core import mail
5
6
7
from backend import const
from backend import models as bmodels
import process.models as pmodels
8
import keyring.models as kmodels
9
from unittest.mock import patch
Enrico Zini's avatar
Enrico Zini committed
10
from process.unittest import (ProcessFixtureMixin,
11
                              test_fingerprint1, test_fpr1_signed_valid_text,
12
13
                              test_fingerprint2, test_fpr2_signed_valid_text,
                              test_fpr1_signed_valid_text_nonascii_htmlchars)
Enrico Zini's avatar
Enrico Zini committed
14
from process import ops as pops
15
16
17
18
19


class TestProcessStatementCreate(ProcessFixtureMixin, TestCase):
    @classmethod
    def setUpClass(cls):
20
        super().setUpClass()
21
        cls.create_person("app", status=const.STATUS_DC)
22
        cls.processes.create("app", person=cls.persons.app, applying_for=const.STATUS_DD_U, fd_comment="test")
23
        cls.create_person("am", status=const.STATUS_DD_NU)
24
        cls.ams.create("am", person=cls.persons.am)
Enrico Zini's avatar
Enrico Zini committed
25
26
        cls.amassignments.create(
                "am", process=cls.processes.app, am=cls.ams.am, assigned_by=cls.persons["fd"], assigned_time=now())
27
28
29
30

        cls.visitor = cls.persons.dc
        cls.fingerprints.create("visitor", person=cls.visitor, fpr=test_fingerprint1, is_active=True, audit_skip=True)

Enrico Zini's avatar
Enrico Zini committed
31
32
33
    def test_basic_op(self):
        req = self.processes.app.requirements.get(type="intent")
        o = pops.ProcessStatementAdd(audit_author=self.persons.fd, requirement=req, statement="test statement")
34

Enrico Zini's avatar
Enrico Zini committed
35
36
37
38
39
40
41
42
        @self.assertOperationSerializes(o)
        def _(o):
            self.assertEqual(o.audit_author, self.persons.fd)
            self.assertEqual(o.audit_notes, "Added a new statement")
            self.assertEqual(o.requirement, req)
            self.assertEqual(o.statement, "test statement")

    def test_op(self):
43
        mail.outbox[::] = []
Enrico Zini's avatar
Enrico Zini committed
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
        proc = self.processes.app
        req = self.processes.app.requirements.get(type="intent")
        o = pops.ProcessStatementAdd(audit_author=self.visitor, requirement=req, statement="test statement")
        o.execute()

        req.refresh_from_db()
        self.assertEqual(pmodels.Statement.objects.count(), 1)
        st = req.statements.get()
        self.assertEqual(st.fpr, self.fingerprints.visitor)
        self.assertEqual(st.statement, "test statement")
        self.assertEqual(st.uploaded_by, self.visitor)
        self.assertEqual(st.uploaded_time, o.audit_time)

        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].to, ["debian-newmaint@lists.debian.org"])
Enrico Zini's avatar
Enrico Zini committed
59
        self.assertCountEqual(
60
61
62
                mail.outbox[0].cc, ["{} <{}>".format(proc.person.fullname, proc.person.email),
                                    proc.archive_email,
                                    "{} <{}>".format(st.uploaded_by.fullname, st.uploaded_by.email)])
63
        self.assertEqual(mail.outbox[0].subject, "App: Declaration of intent to become a DD, upl.")
Enrico Zini's avatar
Enrico Zini committed
64
65
66
        self.assertIn("test statement", mail.outbox[0].body)
        self.assertIn(self.processes.app.get_absolute_url(), mail.outbox[0].body)

67
68
69
70
    @classmethod
    def __add_extra_tests__(cls):
        for req_type in ("intent", "sc_dmup", "advocate", "am_ok"):
            cls._add_method(cls._test_create_forbidden, req_type, set())
71
            cls._add_method(cls._test_create_success, req_type, {"edit_statements"})
72
73

    def _test_create_success(self, req_type, visit_perms):
74
        self.maxDiff = None
75
        url = reverse("process_statement_create", args=[self.processes.app.pk, req_type])
Enrico Zini's avatar
Enrico Zini committed
76
        client = self.make_test_client(self.visitor)
77
        with patch.object(pmodels.Requirement, "permissions_of", return_value=visit_perms):
Enrico Zini's avatar
Enrico Zini committed
78
79
80
81
            with self.collect_operations() as ops:
                response = client.get(url)
                self.assertEqual(response.status_code, 200)
                self.assertEqual(len(ops), 0)
82

Enrico Zini's avatar
Enrico Zini committed
83
84
85
86
87
                # Post a signature done with the wrong key
                response = client.post(url, data={"statement": test_fpr2_signed_valid_text})
                self.assertEqual(response.status_code, 200)
                self.assertFormErrorMatches(response, "form", "statement", "NO_PUBKEY")
                self.assertEqual(len(ops), 0)
88

Enrico Zini's avatar
Enrico Zini committed
89
90
91
92
93
94
95
96
97
98
99
100
                # Post an invalid signature
                text = test_fpr1_signed_valid_text.replace("I agree", "I do not agree")
                response = client.post(url, data={"statement": text})
                self.assertEqual(response.status_code, 200)
                self.assertFormErrorMatches(response, "form", "statement", "BADSIG")
                self.assertEqual(len(ops), 0)

                # Post a valid signature
                response = client.post(url, data={"statement": test_fpr1_signed_valid_text})
                req = self.processes.app.requirements.get(type=req_type)
                self.assertRedirectMatches(response, req.get_absolute_url())
                self.assertEqual(len(ops), 1)
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
                self.assertEqual(ops[0].audit_author, self.visitor)
                self.assertEqual(ops[0].audit_notes, "Added a new statement")
                self.assertEqual(ops[0].requirement, self.processes.app.requirements.get(type=req_type))
                self.assertEqual(ops[0].statement, test_fpr1_signed_valid_text.strip())

                # Post a valid signature, with non-ascci and html chars
                ops[::] = []
                response = client.post(url, data={"statement": test_fpr1_signed_valid_text_nonascii_htmlchars})
                req = self.processes.app.requirements.get(type=req_type)
                self.assertRedirectMatches(response, req.get_absolute_url())
                self.assertEqual(len(ops), 1)
                self.assertEqual(ops[0].audit_author, self.persons[self.visitor])
                self.assertEqual(ops[0].audit_notes, "Added a new statement")
                self.assertEqual(ops[0].requirement, req)
                self.assertEqual(ops[0].statement, test_fpr1_signed_valid_text_nonascii_htmlchars.strip())
116
117

    def _test_create_forbidden(self, req_type, visit_perms=set()):
Enrico Zini's avatar
Enrico Zini committed
118
        client = self.make_test_client(self.visitor)
119
        with patch.object(pmodels.Requirement, "permissions_of", return_value=visit_perms):
Enrico Zini's avatar
Enrico Zini committed
120
121
122
123
            with self.collect_operations() as ops:
                response = client.get(reverse("process_statement_create", args=[self.processes.app.pk, req_type]))
                self.assertPermissionDenied(response)
                self.assertEqual(len(ops), 0)
124

125
126
                response = client.post(reverse("process_statement_create", args=[self.processes.app.pk, req_type]),
                                       data={"statement": test_fpr1_signed_valid_text})
Enrico Zini's avatar
Enrico Zini committed
127
128
                self.assertPermissionDenied(response)
                self.assertEqual(len(ops), 0)
129

130
131
    def test_encoding(self):
        # Test with Ondřej Nový's key which has non-ascii characters
Enrico Zini's avatar
Enrico Zini committed
132
133
134
        bmodels.Fingerprint.objects.create(
                person=self.persons.app, fpr="3D983C52EB85980C46A56090357312559D1E064B",
                is_active=True, audit_skip=True)
135
        kmodels.Key.objects.test_preload("3D983C52EB85980C46A56090357312559D1E064B")
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
        statement = """
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Changed message, with wrong signature
-----BEGIN PGP SIGNATURE-----
Comment: GPGTools - https://gpgtools.org

iQIbBAEBCgAGBQJXU0eVAAoJEDVzElWdHgZLSsYP90ZNjvyIY63BTQv5tvRutF/V
sJ3eCIp2GCQ5AiQngv6dP0VMMN4f7bzTnjX4HpLOkgDftmtXm2LPyANkW2YXL1q0
A7WjkAUHlpnlGeGfkjfu58v++rVgVmORMTL1CRCuCWT/in4D3dBJJ6PUYR9W5S98
xnxmKfw+Gn9VJVl6I133k1wTEYfK2JY5o4aCQKcXiVZlFsE9CwatmoXhIJwIIV/Z
jDEMO74vURPqX1yesxIEBs7P97j6jiEgRqYkHmwqr+/FA524cYkjhA3vX7MG124N
nZt+8BREMJNzoCbSI2Nl/gQxTMEcyZtIOi8yKZzq5QmeT5ChdnPGVJhxXIFsNFUc
LR8goiONvIxATE2i5M+yPQvohiUlozwu/2s+zA9csjaya+IatcqMDTbkXxfUW7e7
wTiSv5IKwfuMb2JFhekElvJW5yRD3g+tEctB5eWGycTnUdtYBhdmkNZX2w2vnfly
Q+LDTtLH6MTPlkXb3+Vz0oy0sfZFhWou0p84L+SAMZf00083MaXJJbuUyRpbqJSm
lr1zsMVtcsW6vWCY2TIFV8krZk0v1A52CQFYm/9BC5sAFcFA6r5rtgQ56TVaW95b
wbMM0zN66Q7TlCJq4Wf34+9ZyQEs5IPk6QyyjnjQ6uPYExgfF3WrOsfjJSTupZLH
v85pPGXRppmFCX/Pk+U=
=eWfI
-----END PGP SIGNATURE-----
"""
        url = reverse("process_statement_create", args=[self.processes.app.pk, "intent"])
        client = self.make_test_client(self.persons.app)
        # Post an invalid signature
Enrico Zini's avatar
Enrico Zini committed
162
163
164
165
166
        with self.collect_operations() as ops:
            response = client.post(url, data={"statement": statement})
            self.assertEqual(response.status_code, 200)
            self.assertFormErrorMatches(response, "form", "statement", "Ondřej Nový <novy@ondrej.org>")
            self.assertEqual(len(ops), 0)
167

168
    def test_encoding1(self):
Enrico Zini's avatar
Enrico Zini committed
169
170
171
        bmodels.Fingerprint.objects.create(
                person=self.persons.app, fpr="3D983C52EB85980C46A56090357312559D1E064B",
                is_active=True, audit_skip=True)
172
        kmodels.Key.objects.test_preload("3D983C52EB85980C46A56090357312559D1E064B")
173
174
175
176
        statement = "\xe8"
        url = reverse("process_statement_create", args=[self.processes.app.pk, "intent"])
        client = self.make_test_client(self.persons.app)
        # Post an invalid signature
Enrico Zini's avatar
Enrico Zini committed
177
178
179
180
181
        with self.collect_operations() as ops:
            response = client.post(url, data={"statement": statement})
            self.assertEqual(response.status_code, 200)
            self.assertFormErrorMatches(response, "form", "statement", "OpenPGP MIME data not found")
            self.assertEqual(len(ops), 0)
182

183
184
    def test_description(self):
        self.fingerprints.create("dam", person=self.persons.dam, fpr=test_fingerprint2, is_active=True, audit_skip=True)
185
        kmodels.Key.objects.test_preload(test_fingerprint2)
186
187
        client = self.make_test_client("dam")
        url = reverse("process_statement_create", args=[self.processes.app.pk, "intent"])
Enrico Zini's avatar
Enrico Zini committed
188
189
        with self.collect_operations() as ops:
            response = client.get(url)
Enrico Zini's avatar
Enrico Zini committed
190
191
192
            self.assertContains(
                    response,
                    'The statement will be sent to'
Romain Porte's avatar
Romain Porte committed
193
                    ' <a href="https://lists.debian.org/debian-newmaint">debian-newmaint</a> as <code>Dam')
Enrico Zini's avatar
Enrico Zini committed
194
            self.assertEqual(len(ops), 0)
195

Enrico Zini's avatar
Enrico Zini committed
196
        with self.collect_operations() as ops:
Enrico Zini's avatar
Enrico Zini committed
197
198
199
            response = client.post(
                    reverse("process_statement_create", args=[self.processes.app.pk, "intent"]),
                    data={"statement": test_fpr2_signed_valid_text})
Enrico Zini's avatar
Enrico Zini committed
200
201
202
203
204
205
206
207
            self.assertRedirectMatches(response, self.processes.app.requirements.get(type="intent").get_absolute_url())
            self.assertEqual(len(ops), 1)

        op = ops[0]
        self.assertEqual(op.audit_author, self.persons.dam)
        self.assertEqual(op.audit_notes, "Added a new statement")
        self.assertEqual(op.requirement, self.processes.app.requirements.get(type="intent"))
        self.assertEqual(op.statement, test_fpr2_signed_valid_text.strip())
208
209
210

        self.processes.app.applying_for = const.STATUS_EMERITUS_DD
        self.processes.app.save()
Enrico Zini's avatar
Enrico Zini committed
211
212
213
214
        with self.collect_operations() as ops:
            response = client.get(url)
            self.assertPermissionDenied(response)
            self.assertEqual(len(ops), 0)
215

Enrico Zini's avatar
Enrico Zini committed
216
217
        self.processes.app.applying_for = const.STATUS_REMOVED_DD
        self.processes.app.save()
Enrico Zini's avatar
Enrico Zini committed
218
219
220
221
        with self.collect_operations() as ops:
            response = client.get(url)
            self.assertPermissionDenied(response)
            self.assertEqual(len(ops), 0)
222

223
224

class TestProcessStatementDelete(ProcessFixtureMixin, TestCase):
Enrico Zini's avatar
Enrico Zini committed
225
226
    def test_basic_op(self):
        req = self.processes.app.requirements.get(type="intent")
Enrico Zini's avatar
Enrico Zini committed
227
228
        st = pmodels.Statement.objects.create(
                requirement=req, statement="test", uploaded_by=self.persons.fd, uploaded_time=now())
Enrico Zini's avatar
Enrico Zini committed
229
230

        o = pops.ProcessStatementRemove(audit_author=self.persons.fd, statement=st)
231

Enrico Zini's avatar
Enrico Zini committed
232
233
234
235
236
237
        @self.assertOperationSerializes(o)
        def _(o):
            self.assertEqual(o.audit_author, self.persons.fd)
            self.assertEqual(o.audit_notes, "Removed a statement")
            self.assertEqual(o.statement, st)

238
239
    @classmethod
    def setUpClass(cls):
240
        super().setUpClass()
241
        cls.create_person("app", status=const.STATUS_DC)
242
        cls.processes.create("app", person=cls.persons.app, applying_for=const.STATUS_DD_U, fd_comment="test")
243
        cls.create_person("am", status=const.STATUS_DD_NU)
244
        cls.ams.create("am", person=cls.persons.am)
Enrico Zini's avatar
Enrico Zini committed
245
246
        cls.amassignments.create(
                "am", process=cls.processes.app, am=cls.ams.am, assigned_by=cls.persons["fd"], assigned_time=now())
247
248
249
250
251
252

        cls.visitor = cls.persons.dc
        cls.fingerprints.create("visitor", person=cls.visitor, fpr=test_fingerprint1, is_active=True, audit_skip=True)
        cls.statement_pks = {}
        for req_type in ("intent", "sc_dmup", "advocate", "am_ok"):
            req = cls.processes.app.requirements.get(type=req_type)
253
254
            cls.statements.create(req_type, requirement=req, fpr=cls.fingerprints.visitor,
                                  statement=test_fpr1_signed_valid_text, uploaded_by=cls.visitor, uploaded_time=now())
255

Enrico Zini's avatar
Enrico Zini committed
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
    def test_op(self):
        mail.outbox = []

        o = pops.ProcessStatementRemove(audit_author=self.persons.fd, statement=self.statements.intent)
        o.execute()
        self.assertFalse(pmodels.Statement.objects.filter(pk=self.statements.intent.pk).exists())

        o = pops.ProcessStatementRemove(audit_author=self.persons.fd, statement=self.statements.sc_dmup)
        o.execute()
        self.assertFalse(pmodels.Statement.objects.filter(pk=self.statements.sc_dmup.pk).exists())

        o = pops.ProcessStatementRemove(audit_author=self.persons.fd, statement=self.statements.advocate)
        o.execute()
        self.assertFalse(pmodels.Statement.objects.filter(pk=self.statements.advocate.pk).exists())

        o = pops.ProcessStatementRemove(audit_author=self.persons.fd, statement=self.statements.am_ok)
        o.execute()
        self.assertFalse(pmodels.Statement.objects.filter(pk=self.statements.am_ok.pk).exists())

        self.assertEqual(len(mail.outbox), 0)

        # Notice that the statements were removed, so tearDownClass does not fail trying to remove them again
        self.statements.refresh()

280
281
282
    @classmethod
    def __add_extra_tests__(cls):
        for req_type in ("intent", "sc_dmup", "advocate", "am_ok"):
Enrico Zini's avatar
Enrico Zini committed
283
            cls._add_method(cls._test_delete_forbidden, req_type, visit_perms=set())
284
            cls._add_method(cls._test_delete_success, req_type, visit_perms={"fd_comments"})
285
286

    def _test_delete_success(self, req_type, visit_perms):
Enrico Zini's avatar
Enrico Zini committed
287
288
289
        client = self.make_test_client(self.visitor)
        statement = self.statements[req_type]
        url = reverse("process_statement_delete", args=[self.processes.app.pk, req_type, statement.pk])
290
        with patch.object(pmodels.Requirement, "permissions_of", return_value=visit_perms):
Enrico Zini's avatar
Enrico Zini committed
291
292
293
294
295
296
297
298
            with self.collect_operations() as ops:
                response = client.get(url)
                self.assertEqual(response.status_code, 200)
                self.assertEqual(len(ops), 0)

                response = client.post(url)
                self.assertRedirectMatches(response, statement.requirement.get_absolute_url())
                self.assertEqual(len(ops), 1)
299

Enrico Zini's avatar
Enrico Zini committed
300
301
302
303
        op = ops[0]
        self.assertEqual(op.audit_author, client.visitor)
        self.assertEqual(op.audit_notes, "Removed a statement")
        self.assertEqual(op.statement, statement)
304
305

    def _test_delete_forbidden(self, req_type, visit_perms=set()):
Enrico Zini's avatar
Enrico Zini committed
306
307
308
        client = self.make_test_client(self.visitor)
        statement = self.statements[req_type]
        url = reverse("process_statement_delete", args=[self.processes.app.pk, req_type, statement.pk])
309
        with patch.object(pmodels.Requirement, "permissions_of", return_value=visit_perms):
Enrico Zini's avatar
Enrico Zini committed
310
311
312
313
            with self.collect_operations() as ops:
                response = client.get(url)
                self.assertPermissionDenied(response)
                self.assertEqual(len(ops), 0)
314

Enrico Zini's avatar
Enrico Zini committed
315
316
317
                response = client.post(url)
                self.assertPermissionDenied(response)
                self.assertEqual(len(ops), 0)
318
319
320
321
322


class TestProcessStatementRaw(ProcessFixtureMixin, TestCase):
    @classmethod
    def setUpClass(cls):
323
        super().setUpClass()
324
        cls.create_person("app", status=const.STATUS_DC)
325
        cls.processes.create("app", person=cls.persons.app, applying_for=const.STATUS_DD_U, fd_comment="test")
326
        cls.create_person("am", status=const.STATUS_DD_NU)
327
        cls.ams.create("am", person=cls.persons.am)
Enrico Zini's avatar
Enrico Zini committed
328
329
        cls.amassignments.create(
                "am", process=cls.processes.app, am=cls.ams.am, assigned_by=cls.persons["fd"], assigned_time=now())
330
331
332
333

        cls.signer = cls.persons.dc
        cls.fingerprints.create("signer", person=cls.signer, fpr=test_fingerprint1, is_active=True, audit_skip=True)
        cls.statement_pks = {}
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
334
        for req_type in ("intent", "sc_dmup", "advocate", "am_ok", "approval"):
335
            req = cls.processes.app.requirements.get(type=req_type)
336
337
            cls.statements.create(req_type, requirement=req, fpr=cls.fingerprints.signer,
                                  statement=test_fpr1_signed_valid_text, uploaded_by=cls.signer, uploaded_time=now())
338
339
340

    @classmethod
    def __add_extra_tests__(cls):
Enrico Zini's avatar
Enrico Zini committed
341
342
        for visitor in (None, "pending", "dc", "dc_ga", "dm", "dm_ga", "dd_nu",
                        "dd_u", "dd_e", "dd_r", "app", "am", "activeam", "fd", "dam"):
343
344
345
346
347
348
349
350
            for req_type in ("intent", "sc_dmup", "advocate", "am_ok"):
                cls._add_method(cls._test_access, visitor, req_type)

    def _test_access(self, visitor, req_type):
        client = self.make_test_client(visitor)
        statement = self.statements[req_type]
        response = client.get(reverse("process_statement_raw", args=[self.processes.app.pk, req_type, statement.pk]))
        self.assertEqual(response.status_code, 200)
Enrico Zini's avatar
Enrico Zini committed
351
        self.assertEqual(response.content.decode("utf8"), test_fpr1_signed_valid_text)