Repository: django-oscar/django-oscar

Issue:
/oscar/checkout/thank-you/` returns 404 when checkout_order_id not in session
### Issue Summary

Oscar 2.0 smoke tests, `GET /oscar/checkout/thank-you/` returns 404.

The smoke test is obviously requesting this without going through the checkout process, so 403 could be appropriate.  It is a very long stretch of the meaning of 404 to use it here - the *resource* was found - it was the resource is responding that it doesnt like the *request* context.

`ThankYouView` has `raise http.Http404(_("No order found"))`

More interesting, it only does this if a session field `checkout_order_id` wasnt found, which that error message does not convey accurately.

If it is about a missing field, it could be a `SuspiciousOperation`.

However as it is a GET, that page URL could be in a users browser history, in which case it could be inadvertently loaded in a new session, which isnt really suspicious, and the user shouldnt see a http error page.

It should be possible to check if there is a successful order by the user, and thank them again! ;-)
Failing that, a "there is no checkout in progress" error message?

### Steps to Reproduce

c.f. setup in https://github.com/django-oscar/django-oscar/issues/3421

### Technical details

* Python version: 3.8
* Django version: 3.0
* Oscar version: 2.0


Discussion:
A similar issue I'll dump here instead of creating lots of issues.

410 Gone is described in Wikipedia as

>  The 410 (Gone) status code indicates that access to the target resource is no longer available at the origin server and that this condition is likely to be permanent.  If the origin server does not know, or has no facility to determine, whether or not the condition is permanent, the status code 404 (Not Found) ought to be used instead.

> The 410 response is primarily intended to assist the task of web maintenance by notifying the recipient that the resource is intentionally unavailable and that the server owners desire that remote links to that resource be removed.  Such an event is common for limited-time, promotional services and for resources belonging to individuals no longer associated with the origin server's site.  It is not necessary to mark all permanently unavailable resources as "gone" or to keep the mark for any length of time -- that is left to the discretion of the server owner.

> A 410 response is cacheable by default; i.e., unless otherwise indicated by the method definition or explicit cache controls (see Section 4.2.2 of [RFC7234]).

That is clearly inappropriate here, and even 404 is inappropriate here.  Again, 403 or 400 would be more appropriate.

```py
FAIL: test_smoke_GET_oscar/^accounts/^notifications/update/$ (django_smoke_tests.tests.SmokeTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/django_smoke_tests/generator.py", line 95, in test
    self_of_test.fail_test(url, method, response=response)
  File "/usr/lib/python3.8/site-packages/django_smoke_tests/tests.py", line 44, in fail_test
    self.fail(fail_msg)
AssertionError: 
SMOKE TEST FAILED
URL: /oscar/accounts/notifications/update/
HTTP METHOD: GET
STATUS CODE: 410
```
> It should be possible to check if there is a successful order by the user, and thank them again! ;-)
Failing that, a "there is no checkout in progress" error message?

Sounds logical to me.
WDYT @django-oscar/core ^ ?

Resolving patch:
diff --git a/src/oscar/apps/checkout/views.py b/src/oscar/apps/checkout/views.py
--- a/src/oscar/apps/checkout/views.py
+++ b/src/oscar/apps/checkout/views.py
@@ -2,6 +2,7 @@
 from urllib.parse import quote
 
 from django import http
+from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth import login
 from django.shortcuts import redirect
@@ -659,7 +660,14 @@ class ThankYouView(generic.DetailView):
     template_name = 'oscar/checkout/thank_you.html'
     context_object_name = 'order'
 
-    def get_object(self):
+    def get(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        if self.object is None:
+            return redirect(settings.OSCAR_HOMEPAGE)
+        context = self.get_context_data(object=self.object)
+        return self.render_to_response(context)
+
+    def get_object(self, queryset=None):
         # We allow superusers to force an order thank-you page for testing
         order = None
         if self.request.user.is_superuser:
@@ -674,9 +682,6 @@ def get_object(self):
             if 'checkout_order_id' in self.request.session:
                 order = Order._default_manager.get(
                     pk=self.request.session['checkout_order_id'])
-            else:
-                raise http.Http404(_("No order found"))
-
         return order
 
     def get_context_data(self, *args, **kwargs):


Test patch:
diff --git a/tests/functional/checkout/test_customer_checkout.py b/tests/functional/checkout/test_customer_checkout.py
--- a/tests/functional/checkout/test_customer_checkout.py
+++ b/tests/functional/checkout/test_customer_checkout.py
@@ -1,5 +1,3 @@
-from http import client as http_client
-
 from django.urls import reverse
 
 from oscar.core.loading import get_class, get_model
@@ -201,10 +199,11 @@ def test_records_use(self):
 
 class TestThankYouView(CheckoutMixin, WebTestCase):
 
-    def tests_gets_a_404_when_there_is_no_order(self):
+    def tests_gets_a_302_when_there_is_no_order(self):
         response = self.get(
             reverse('checkout:thank-you'), user=self.user, status="*")
-        self.assertEqual(http_client.NOT_FOUND, response.status_code)
+        self.assertIsRedirect(response)
+        self.assertRedirectsTo(response, 'catalogue:index')
 
     def tests_custumers_can_reach_the_thank_you_page(self):
         self.add_product_to_basket()
@@ -230,7 +229,7 @@ def test_superusers_can_force_an_order(self):
         response = self.get(test_url, status='*', user=user)
         self.assertIsOk(response)
 
-    def test_users_cannot_force_an_other_custumer_order(self):
+    def test_users_cannot_force_an_other_customer_order(self):
         self.add_product_to_basket()
         self.enter_shipping_address()
         self.place_order()
@@ -241,8 +240,10 @@ def test_users_cannot_force_an_other_custumer_order(self):
         test_url = '%s?order_number=%s' % (
             reverse('checkout:thank-you'), order.number)
         response = self.get(test_url, status='*', user=user)
-        self.assertEqual(response.status_code, http_client.NOT_FOUND)
+        self.assertIsRedirect(response)
+        self.assertRedirectsTo(response, 'catalogue:index')
 
         test_url = '%s?order_id=%s' % (reverse('checkout:thank-you'), order.pk)
         response = self.get(test_url, status='*', user=user)
-        self.assertEqual(response.status_code, http_client.NOT_FOUND)
+        self.assertIsRedirect(response)
+        self.assertRedirectsTo(response, 'catalogue:index')
diff --git a/tests/integration/checkout/test_views.py b/tests/integration/checkout/test_views.py
--- a/tests/integration/checkout/test_views.py
+++ b/tests/integration/checkout/test_views.py
@@ -1,24 +1,32 @@
-from django.test import TestCase
-from django.test.utils import override_settings
 from django.urls import reverse
 
 from oscar.test.factories import OrderFactory
+from oscar.test.testcases import WebTestCase
 
 
-class ThankYouViewTestCase(TestCase):
+class ThankYouViewTestCase(WebTestCase):
+    is_anonymous = False
 
-    @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
     def test_analytics_event_triggered_only_on_first_view(self):
-        order = OrderFactory()
-        session = self.client.session
-        # Put the order ID in the session, mimicking a completed order,
-        # so that we can reach the thank you page.
-        session['checkout_order_id'] = order.pk
-        session.save()
+        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
+            url = reverse('checkout:thank-you')
+            order = OrderFactory()
+            session = self.client.session
+            # Put the order ID in the session, mimicking a completed order,
+            # so that we can reach the thank you page.
+            session['checkout_order_id'] = order.pk
+            session.save()
 
-        r1 = self.client.get(reverse('checkout:thank-you'), follow=True)
-        self.assertTrue(r1.context['send_analytics_event'])
+            r1 = self.client.get(url, follow=True)
+            self.assertTrue(r1.context['send_analytics_event'])
 
-        # Request the view a second time
-        r2 = self.client.get(reverse('checkout:thank-you'), follow=True)
-        self.assertFalse(r2.context['send_analytics_event'])
+            # Request the view a second time
+            r2 = self.client.get(url, follow=True)
+            self.assertFalse(r2.context['send_analytics_event'])
+
+    def test_missing_order_id_in_the_session(self):
+        with self.settings(OSCAR_ALLOW_ANON_CHECKOUT=True):
+            url = reverse('checkout:thank-you')
+            response = self.app.get(url)
+            self.assertIsRedirect(response)
+            self.assertRedirectsTo(response, 'catalogue:index')