Add a simple test mode (#1181)

- [x] Provide data model and configuration toggle
- [x] Allow to delete individual test orders
- [x] Add tests
- [x] Add a prominent warning message to the backend if test mode orders exist (even though test mode is off), as this leads to wrong statistics
- [x] Decide if and how to generate invoices for test orders as invoice numbers cannot be repeated or should not have gaps.
- [x] Decide if and how we expose test orders through the API, since our difference pull mechanism relies on the fact that orders cannot be deleted.
- [x] Decide if and how we want to couple test modes of payment providers?
- [ ] pretix.eu: Ignore test orders for billing
- [ ] Adjust payment providers: Mollie, bitpay, cash, fakepayment, sepadebit

![download](https://user-images.githubusercontent.com/64280/53009081-fe420d80-343a-11e9-8361-b8511c988598.png)
This commit is contained in:
Raphael Michel
2019-02-20 17:51:26 +01:00
committed by GitHub
parent 8ffc96bf31
commit 67059fe323
49 changed files with 759 additions and 91 deletions
+28
View File
@@ -77,6 +77,7 @@ def cart_position(event, item, variations):
TEST_EVENT_RES = {
"name": {"en": "Dummy"},
"live": False,
"testmode": False,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": None,
@@ -180,6 +181,7 @@ def test_event_create(token_client, organizer, event, meta_prop):
format='json'
)
assert resp.status_code == 201
assert not organizer.events.get(slug="2030").testmode
assert organizer.events.get(slug="2030").meta_values.filter(
property__name=meta_prop.name, value="Conference"
).exists()
@@ -275,6 +277,7 @@ def test_event_create_with_clone(token_client, organizer, event, meta_prop):
"en": "Demo Conference 2020 Test"
},
"live": False,
"testmode": True,
"currency": "EUR",
"date_from": "2018-12-27T10:00:00Z",
"date_to": "2018-12-28T10:00:00Z",
@@ -298,6 +301,7 @@ def test_event_create_with_clone(token_client, organizer, event, meta_prop):
cloned_event = Event.objects.get(organizer=organizer.pk, slug='2030')
assert cloned_event.plugins == 'pretix.plugins.ticketoutputpdf'
assert cloned_event.is_public is False
assert cloned_event.testmode
assert organizer.events.get(slug="2030").meta_values.filter(
property__name=meta_prop.name, value="Conference"
).exists()
@@ -493,6 +497,30 @@ def test_event_update(token_client, organizer, event, item, meta_prop):
assert resp.content.decode() == '{"meta_data":["Meta data property \'test\' does not exist."]}'
@pytest.mark.django_db
def test_event_test_mode(token_client, organizer, event):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
{
"testmode": True
},
format='json'
)
assert resp.status_code == 200
event.refresh_from_db()
assert event.testmode
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
{
"testmode": False
},
format='json'
)
assert resp.status_code == 200
event.refresh_from_db()
assert not event.testmode
@pytest.mark.django_db
def test_event_update_live_no_product(token_client, organizer, event):
resp = token_client.patch(
+46
View File
@@ -181,6 +181,7 @@ TEST_REFUNDS_RES = [
TEST_ORDER_RES = {
"code": "FOO",
"status": "n",
"testmode": False,
"secret": "k24fiuwvu8kxz3y1",
"email": "dummy@dummy.test",
"locale": "en",
@@ -242,6 +243,11 @@ def test_order_list(token_client, organizer, event, order, item, taxrule, questi
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?code=BAR'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?testmode=false'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?testmode=true'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?status=n'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?status=p'.format(organizer.slug, event.slug))
@@ -1352,6 +1358,7 @@ def test_order_create(token_client, organizer, event, item, quota, question):
assert o.total == Decimal('23.25')
assert o.status == Order.STATUS_PENDING
assert o.sales_channel == "web"
assert not o.testmode
p = o.payments.first()
assert p.provider == "banktransfer"
@@ -1423,6 +1430,22 @@ def test_order_create_sales_channel_invalid(token_client, organizer, event, item
assert resp.data == {'sales_channel': ['Unknown sales channel.']}
@pytest.mark.django_db
def test_order_create_in_test_mode(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res['testmode'] = True
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert o.testmode
@pytest.mark.django_db
def test_order_create_attendee_name_optional(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
@@ -2502,3 +2525,26 @@ def test_refund_create_invalid_payment(token_client, organizer, event, order):
), format='json', data=res
)
assert resp.status_code == 400
@pytest.mark.django_db
def test_order_delete(token_client, organizer, event, order):
resp = token_client.delete(
'/api/v1/organizers/{}/events/{}/orders/{}/'.format(
organizer.slug, event.slug, order.code
)
)
assert resp.status_code == 403
@pytest.mark.django_db
def test_order_delete_test_mode(token_client, organizer, event, order):
order.testmode = True
order.save()
resp = token_client.delete(
'/api/v1/organizers/{}/events/{}/orders/{}/'.format(
organizer.slug, event.slug, order.code
)
)
assert resp.status_code == 204
assert not Order.objects.filter(code=order.code).exists()
+14
View File
@@ -291,18 +291,28 @@ def test_invoice_numbers(env):
)
order2.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'))
testorder = Order.objects.create(
code='BAR', event=event, email='dummy2@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, testmode=True,
locale='en'
)
inv1 = generate_invoice(order)
inv2 = generate_invoice(order)
invt1 = generate_invoice(testorder)
event.settings.set('invoice_numbers_consecutive', False)
inv3 = generate_invoice(order)
inv4 = generate_invoice(order)
inv21 = generate_invoice(order2)
invt2 = generate_invoice(testorder)
inv22 = generate_invoice(order2)
event.settings.set('invoice_numbers_consecutive', True)
inv5 = generate_invoice(order)
inv6 = generate_invoice(order)
invt3 = generate_invoice(testorder)
inv7 = generate_invoice(order)
Invoice.objects.filter(pk=inv6.pk).delete() # This should never ever happen, but what if it happens anyway?
inv8 = generate_invoice(order)
@@ -328,6 +338,10 @@ def test_invoice_numbers(env):
assert inv1.number == '{}-00001'.format(event.slug.upper())
assert inv3.number == '{}-{}-3'.format(event.slug.upper(), order.code)
assert invt1.number == '{}-TEST-00001'.format(event.slug.upper())
assert invt2.number == '{}-TEST-{}-2'.format(event.slug.upper(), testorder.code)
assert invt3.number == '{}-TEST-00002'.format(event.slug.upper())
@pytest.mark.django_db
def test_invoice_number_prefixes(env):
+57 -5
View File
@@ -248,8 +248,60 @@ class EventsTest(SoupTest):
{'plugin:pretix.plugins.paypal': 'disable'})
self.assertIn("Enable", doc.select("[name=\"plugin:pretix.plugins.paypal\"]")[0].text)
def test_testmode_enable(self):
self.event1.testmode = False
self.event1.save()
self.post_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug),
{'testmode': 'true'})
self.event1.refresh_from_db()
assert self.event1.testmode
def test_testmode_disable(self):
o = Order.objects.create(
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en', testmode=True
)
o2 = Order.objects.create(
code='FOO2', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en'
)
self.event1.testmode = True
self.event1.save()
self.post_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug),
{'testmode': 'false'})
self.event1.refresh_from_db()
assert not self.event1.testmode
assert Order.objects.filter(pk=o.pk).exists()
assert Order.objects.filter(pk=o2.pk).exists()
def test_testmode_disable_delete(self):
o = Order.objects.create(
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en', testmode=True
)
o2 = Order.objects.create(
code='FOO2', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en'
)
self.event1.testmode = True
self.event1.save()
self.post_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug),
{'testmode': 'false', 'delete': 'yes'})
self.event1.refresh_from_db()
assert not self.event1.testmode
assert not Order.objects.filter(pk=o.pk).exists()
assert Order.objects.filter(pk=o2.pk).exists()
def test_live_disable(self):
self.event1.live = False
self.event1.live = True
self.event1.save()
self.post_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug),
{'live': 'false'})
@@ -261,7 +313,7 @@ class EventsTest(SoupTest):
self.event1.settings.set('payment_banktransfer__enabled', True)
self.event1.quotas.create(name='Test quota')
doc = self.get_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug))
assert len(doc.select(".btn-primary"))
assert len(doc.select("input[name=live]"))
self.post_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug),
{'live': 'true'})
self.event1.refresh_from_db()
@@ -272,19 +324,19 @@ class EventsTest(SoupTest):
self.event1.settings.set('payment_banktransfer__enabled', False)
self.event1.quotas.create(name='Test quota')
doc = self.get_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug))
assert len(doc.select(".btn-primary"))
assert len(doc.select("input[name=live]"))
def test_live_require_payment_method(self):
self.event1.items.create(name='Test', default_price=5)
self.event1.settings.set('payment_banktransfer__enabled', False)
self.event1.quotas.create(name='Test quota')
doc = self.get_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug))
assert len(doc.select(".btn-primary")) == 0
assert len(doc.select("input[name=live]")) == 0
def test_live_require_a_quota(self):
self.event1.settings.set('payment_banktransfer__enabled', True)
doc = self.get_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug))
assert len(doc.select(".btn-primary")) == 0
assert len(doc.select("input[name=live]")) == 0
def test_payment_settings_provider(self):
self.get_doc('/control/event/%s/%s/settings/payment/banktransfer' % (self.orga1.slug, self.event1.slug))
+39
View File
@@ -126,6 +126,15 @@ def test_order_list(client, env):
response = client.get('/control/event/dummy/dummy/orders/?question=%d&answer=%d' % (q.pk, qo2.pk))
assert 'FOO' not in response.rendered_content
response = client.get('/control/event/dummy/dummy/orders/?status=testmode')
assert 'FOO' not in response.rendered_content
assert 'TEST MODE' not in response.rendered_content
env[2].testmode = True
env[2].save()
response = client.get('/control/event/dummy/dummy/orders/?status=testmode')
assert 'FOO' in response.rendered_content
assert 'TEST MODE' in response.rendered_content
@pytest.mark.django_db
def test_order_detail(client, env):
@@ -134,6 +143,16 @@ def test_order_detail(client, env):
assert 'Early-bird' in response.rendered_content
assert 'Peter' in response.rendered_content
assert 'Lukas Gelöscht' in response.rendered_content
assert 'TEST MODE' not in response.rendered_content
@pytest.mark.django_db
def test_order_detail_show_test_mode(client, env):
env[2].testmode = True
env[2].save()
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/orders/FOO/')
assert 'TEST MODE' in response.rendered_content
@pytest.mark.django_db
@@ -260,6 +279,26 @@ def test_order_deny(client, env):
assert o.require_approval
@pytest.mark.django_db
def test_order_delete_require_testmode(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
res = client.get('/control/event/dummy/dummy/orders/FOO/delete', {}, follow=True)
assert 'alert-danger' in res.rendered_content
assert 'Only orders created in test mode can be deleted' in res.rendered_content
client.post('/control/event/dummy/dummy/orders/FOO/delete', {}, follow=True)
assert Order.objects.get(id=env[2].id)
@pytest.mark.django_db
def test_order_delete(client, env):
o = Order.objects.get(id=env[2].id)
o.testmode = True
o.save()
client.login(email='dummy@dummy.dummy', password='dummy')
client.post('/control/event/dummy/dummy/orders/FOO/delete', {}, follow=True)
assert not Order.objects.filter(id=env[2].id).exists()
@pytest.mark.django_db
@pytest.mark.parametrize("process", [
# (Old status, new status, success expected)
+32
View File
@@ -1799,3 +1799,35 @@ class CheckoutTestCase(TestCase):
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertEqual(len(doc.select(".thank-you")), 1)
def test_create_testmode_order_in_testmode(self):
self.event.testmode = True
self.event.save()
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
self._set_session('payment', 'banktransfer')
response = self.client.get('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
assert "test mode" in response.rendered_content
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertEqual(len(doc.select(".thank-you")), 1)
assert Order.objects.last().testmode
assert Order.objects.last().code[1] == "0"
def test_do_not_create_testmode_order_without_testmode(self):
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
self._set_session('payment', 'banktransfer')
response = self.client.get('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
assert "test mode" not in response.rendered_content
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertEqual(len(doc.select(".thank-you")), 1)
assert not Order.objects.last().testmode
assert "0" not in Order.objects.last().code