diff --git a/datacenterlight/factories.py b/datacenterlight/factories.py new file mode 100644 index 00000000..3bbe6e17 --- /dev/null +++ b/datacenterlight/factories.py @@ -0,0 +1,21 @@ +import factory +from django.contrib.sites.models import Site + +from datacenterlight.cms_models import CMSIntegration + + +class SiteFactory(factory.django.DjangoModelFactory): + class Meta: + model = Site + django_get_or_create = ("domain",) + + domain = factory.Sequence(lambda n: f"example{n}.test") + name = factory.LazyAttribute(lambda obj: obj.domain) + + +class CMSIntegrationFactory(factory.django.DjangoModelFactory): + class Meta: + model = CMSIntegration + + name = factory.Sequence(lambda n: f"integration-{n}") + domain = factory.SubFactory(SiteFactory) \ No newline at end of file diff --git a/datacenterlight/test_views.py b/datacenterlight/test_views.py new file mode 100644 index 00000000..f0b1bbcf --- /dev/null +++ b/datacenterlight/test_views.py @@ -0,0 +1,591 @@ +# datacenterlight/tests/test_views.py + +import json +from unittest.mock import patch + +from django.contrib.messages import get_messages +from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.auth.models import AnonymousUser +from django.core.exceptions import ValidationError +from django.test import RequestFactory, TestCase +from django.urls import reverse + +from datacenterlight.forms import ContactForm +from datacenterlight.views import ( + ContactUsView, + IndexView, + OrderConfirmationView, + PaymentOrderView, + WhyDataCenterLightView, + create_incomplete_intent_request, + get_error_response_dict, + show_error, +) +from utils.forms import BillingAddressFormSignup +from .factories import CMSIntegrationFactory + + +class SessionRequestMixin: + """ + Adds a real Django session to RequestFactory requests. + """ + + def add_session(self, request): + middleware = SessionMiddleware(lambda request: None) + middleware.process_request(request) + request.session.save() + return request + + +class ContactUsViewTests(TestCase, SessionRequestMixin): + def setUp(self): + self.factory = RequestFactory() + + def test_get_redirects_to_index(self): + response = self.client.get( + reverse("datacenterlight:contact_us") + ) + + self.assertRedirects( + response, + reverse("datacenterlight:index"), + fetch_redirect_response=False, + ) + + def test_form_valid_renders_index_template(self): + request = self.factory.post( + reverse("datacenterlight:contact_us") + ) + + view = ContactUsView() + view.setup(request) + + form = ContactForm() + + response = view.form_valid(form) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Thank you") + + def test_form_invalid_renders_index_template(self): + request = self.factory.post( + reverse("datacenterlight:contact_us") + ) + + view = ContactUsView() + view.setup(request) + + # Bind the form with invalid/missing data. + form = ContactForm(data={}) + + self.assertFalse(form.is_valid()) + + response = view.form_invalid(form) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "index") + + +class IndexViewTests(TestCase, SessionRequestMixin): + + def setUp(self): + self.factory = RequestFactory() + + def test_get_redirects_to_cms_index(self): + response = self.client.get( + reverse("datacenterlight:whydatacenterlight") + ) + + self.assertRedirects( + response, + reverse("datacenterlight:cms_index"), + fetch_redirect_response=False, + ) + + def test_index_t_redirect_chain(self): + response = self.client.get( + reverse("datacenterlight:index_t"), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + + self.assertEqual( + response.redirect_chain[0][0], + reverse("datacenterlight:cms_index"), + ) + + def test_index_g_uses_index_view(self): + response = self.client.get( + reverse("datacenterlight:index_g"), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + + self.assertEqual( + response.redirect_chain[0][0], + reverse("datacenterlight:cms_index"), + ) + + def test_index_f_uses_index_view(self): + response = self.client.get( + reverse("datacenterlight:index_f"), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + + self.assertEqual( + response.redirect_chain[0][0], + reverse("datacenterlight:cms_index"), + ) + + def test_index_l_uses_index_view(self): + response = self.client.get( + reverse("datacenterlight:index_l"), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + + self.assertEqual( + response.redirect_chain[0][0], + reverse("datacenterlight:cms_index"), + ) + + def test_cms_index_redirects_to_cms(self): + response = self.client.get( + reverse("datacenterlight:cms_index"), + follow=True, + ) + + def test_get_success_url(self): + request = self.factory.get( + reverse("datacenterlight:index") + ) + + view = IndexView() + view.setup(request) + + self.assertEqual( + view.get_success_url(), + reverse("datacenterlight:index") + "#requestform", + ) + + def test_get_context_data_contains_base_url(self): + request = self.factory.get( + reverse("datacenterlight:index") + ) + + view = IndexView() + view.setup(request) + + with patch( + "datacenterlight.views.CreateView.get_context_data", + return_value={}, + ): + context = view.get_context_data() + + self.assertIn("base_url", context) + self.assertIn("contact_form", context) + self.assertIs(context["contact_form"], ContactForm) + + self.assertEqual( + context["base_url"], + f"{request.scheme}://{request.get_host()}", + ) + + # ------------------------------------------------------------------ + # validate_cores + # ------------------------------------------------------------------ + + def test_validate_cores_accepts_one(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + self.assertIsNone(view.validate_cores(1)) + + def test_validate_cores_accepts_48(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + self.assertIsNone(view.validate_cores(48)) + + def test_validate_cores_rejects_zero(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + with self.assertRaises(ValidationError): + view.validate_cores(0) + + def test_validate_cores_rejects_more_than_48(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + with self.assertRaises(ValidationError): + view.validate_cores(49) + + def test_validate_cores_rejects_negative_number(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + with self.assertRaises(ValidationError): + view.validate_cores(-1) + + # ------------------------------------------------------------------ + # validate_memory + # ------------------------------------------------------------------ + + def test_validate_memory_requires_pid(self): + request = self.factory.post( + "/", + { + "ram": "1", + }, + ) + + view = IndexView() + view.setup(request) + + with self.assertRaises(ValidationError): + view.validate_memory(1) + + def test_validate_memory_rejects_unknown_plugin(self): + request = self.factory.post( + "/", + { + "pid": "999999999", + }, + ) + + view = IndexView() + view.setup(request) + + with self.assertRaises(ValidationError): + view.validate_memory(1) + + # ------------------------------------------------------------------ + # validate_storage + # ------------------------------------------------------------------ + + def test_validate_storage_accepts_10(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + self.assertIsNone(view.validate_storage(10)) + + def test_validate_storage_accepts_2000(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + self.assertIsNone(view.validate_storage(2000)) + + def test_validate_storage_rejects_below_10(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + with self.assertRaises(ValidationError): + view.validate_storage(9) + + def test_validate_storage_rejects_above_2000(self): + request = self.factory.post("/") + view = IndexView() + view.setup(request) + + with self.assertRaises(ValidationError): + view.validate_storage(2001) + + # ------------------------------------------------------------------ + # POST without a valid pricing name + # ------------------------------------------------------------------ + + def test_post_with_unknown_pricing_name_redirects_to_referer(self): + response = self.client.post( + reverse("datacenterlight:index"), + { + "cpu": "2", + "ram": "4", + "storage": "20", + "config": "1", + "pricing_name": "definitely-does-not-exist", + }, + HTTP_REFERER="/some-page/", + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual( + response["Location"], + "/some-page/#order_form", + ) + + messages = list( + get_messages(response.wsgi_request) + ) + + self.assertTrue(messages) + self.assertIn( + "Incorrect pricing name", + str(messages[0]), + ) + + +class WhyDataCenterLightViewTests(TestCase): + + def test_template_name(self): + view = WhyDataCenterLightView() + + self.assertEqual( + view.template_name, + "datacenterlight/whydatacenterlight.html", + ) + + def test_get_redirects_to_cms_index(self): + response = self.client.get( + reverse("datacenterlight:whydatacenterlight") + ) + + self.assertRedirects( + response, + reverse("datacenterlight:cms_index"), + fetch_redirect_response=False, + ) + + +class PaymentOrderViewTests(TestCase, SessionRequestMixin): + + def setUp(self): + self.factory = RequestFactory() + CMSIntegrationFactory.create(name="default", domain=None) + + def set_payment_session(self, **extra): + session = self.client.session + + session["vat_validation_status"] = "not_needed" + session["card_id"] = None + session["token"] = None + session["id_payment_method"] = None + + session.update(extra) + session.save() + + def test_product_lookup_with_unknown_product_returns_empty_json(self): + self.set_payment_session() + + response = self.client.post( + reverse("datacenterlight:payment"), + { + "product": "1", + "generic_payment_form-product_name": "999999999", + }, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {}) + + def test_get_form_class_for_anonymous_user(self): + request = self.factory.get( + reverse("datacenterlight:payment") + ) + request.user = AnonymousUser() + + view = PaymentOrderView() + view.setup(request) + + form_class = view.get_form_class() + + self.assertIsNotNone(form_class) + self.assertIs(form_class, BillingAddressFormSignup) + + def test_generic_payment_page_renders(self): + self.set_payment_session() + + response = self.client.get( + reverse("datacenterlight:payment"), + { + "type": "generic", + }, + ) + + self.assertEqual(response.status_code, 200) + + self.assertTemplateUsed( + response, + "datacenterlight/landing_payment.html", + ) + + +class OrderConfirmationViewTests(TestCase, SessionRequestMixin): + + def setUp(self): + self.factory = RequestFactory() + CMSIntegrationFactory.create(name="default", domain=None) + + def test_get_without_payment_data_redirects_to_index(self): + response = self.client.get( + reverse("datacenterlight:order_confirmation") + ) + + self.assertRedirects( + response, + reverse("datacenterlight:index"), + fetch_redirect_response=False, + ) + + def test_post_without_payment_method_returns_error(self): + response = self.client.post( + reverse("datacenterlight:order_confirmation") + ) + + self.assertEqual(response.status_code, 200) + + data = response.json() + + self.assertIn("status", data) + self.assertFalse(data["status"]) + + def test_order_confirmation_url_exists(self): + response = self.client.get( + reverse("datacenterlight:order_confirmation") + ) + + self.assertIn( + response.status_code, + (200, 302, 404), + ) + + @patch( + "datacenterlight.views.StripeUtils." + "get_cards_details_from_payment_method" + ) + def test_generic_recurring_payment_renders_confirmation( + self, + mock_get_card_details, + ): + mock_get_card_details.return_value = { + "response_object": { + "brand": "visa", + "last4": "4242", + "exp_month": 12, + "exp_year": 2030, + } + } + + session = self.client.session + + session["generic_payment_type"] = "generic" + + session["generic_payment_details"] = { + "amount": 100, + "amount_before_vat": 100, + "vat_rate": 0, + "vat_amount": 0, + "product_name": "Test Product", + "product_id": 1, + "product_slug": "test-product", + "recurring": True, + "description": "Test product", + "recurring_interval": "month", + "exclude_vat_calculations": False, + } + + session["id_payment_method"] = "pm_test" + session["customer"] = "cus_test" + session["vat_validation_status"] = "not_needed" + + session.save() + + response = self.client.get( + reverse("datacenterlight:order_confirmation") + ) + + self.assertEqual(response.status_code, 200) + + self.assertTemplateUsed( + response, + "datacenterlight/order_detail.html", + ) + + self.assertIn( + "generic_payment_details", + response.context, + ) + + mock_get_card_details.assert_called_once_with( + "pm_test" + ) + + +class HelperFunctionTests(TestCase, SessionRequestMixin): + + def setUp(self): + self.factory = RequestFactory() + + def test_create_incomplete_intent_request(self): + request = self.factory.post( + "/payment/", + { + "amount": "100", + }, + ) + + self.add_session(request) + + request.session["specs"] = { + "cpu": 2, + "ram": 4, + "storage": 20, + } + + request.session["template"] = { + "id": 123, + } + + request.session.save() + + result = create_incomplete_intent_request(request) + + self.assertIsInstance(result, str) + + + def test_show_error_returns_json_response(self): + request = self.factory.get( + reverse("datacenterlight:payment") + ) + + self.add_session(request) + + # Add messages middleware because show_error() + # uses django.contrib.messages. + from django.contrib.messages.middleware import MessageMiddleware + + MessageMiddleware( + lambda request: None + ).process_request(request) + + response = show_error( + "Something went wrong", + request, + ) + + self.assertEqual( + response.status_code, + 200, + ) + + data = json.loads( + response.content.decode("utf-8") + ) + self.assertFalse(data["status"]) + self.assertEqual( + data["msg_title"], + "Error.", + ) diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 7128b669..2029f157 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -13,6 +13,7 @@ from django.http import ( ) from django.shortcuts import render from django.utils.translation import get_language, gettext_lazy as _ +from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from django.views.generic import FormView, CreateView, DetailView @@ -55,7 +56,7 @@ class ContactUsView(FormView): return HttpResponseRedirect(reverse('datacenterlight:index')) def form_invalid(self, form): - if self.request.is_ajax(): + if self.request.headers.get("x-requested-with") == "XMLHttpRequest": return self.render_to_response( self.get_context_data(contact_form=form)) else: @@ -82,7 +83,7 @@ class ContactUsView(FormView): # 'reply_to': [form.cleaned_data.get('email')], #} #send_plain_email_task.delay(email_data) - if self.request.is_ajax(): + if self.request.headers.get("x-requested-with") == "XMLHttpRequest": return self.render_to_response( self.get_context_data(success=True, contact_form=form)) else: @@ -91,7 +92,14 @@ class ContactUsView(FormView): self.get_context_data(success=True, contact_form=form) ) - +@method_decorator( + cache_control( + no_cache=True, + must_revalidate=True, + no_store=True, + ), + name="dispatch", +) class IndexView(CreateView): template_name = "datacenterlight/index.html" success_url = "/datacenterlight#requestform" @@ -131,7 +139,6 @@ class IndexView(CreateView): if (value > 2000) or (value < 10): raise ValidationError(_('Invalid storage size')) - @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): clear_all_session_vars(request) return HttpResponseRedirect(reverse('datacenterlight:cms_index')) @@ -238,11 +245,19 @@ class WhyDataCenterLightView(IndexView): template_name = "datacenterlight/whydatacenterlight.html" +@method_decorator( + cache_control( + no_cache=True, + must_revalidate=True, + no_store=True, + ), + name="dispatch", +) class PaymentOrderView(FormView): template_name = 'datacenterlight/landing_payment.html' def get_form_class(self): - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: return BillingAddressForm else: return BillingAddressFormSignup @@ -254,7 +269,7 @@ class PaymentOrderView(FormView): else: billing_address_data = {} - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: if billing_address_data: billing_address_form = BillingAddressForm( initial=billing_address_data @@ -315,7 +330,6 @@ class PaymentOrderView(FormView): return context - @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): request.session.pop('vat_validation_status') request.session.pop('card_id') @@ -403,7 +417,7 @@ class PaymentOrderView(FormView): context = self.get_context_data() context['login_form'] = login_form return self.render_to_response(context) - if request.user.is_authenticated(): + if request.user.is_authenticated: address_form = BillingAddressForm( data=request.POST, ) @@ -478,7 +492,7 @@ class PaymentOrderView(FormView): id_payment_method = address_form.cleaned_data.get('card') request.session["id_payment_method"] = id_payment_method logger.debug("id_payment_method is %s" % id_payment_method) - if request.user.is_authenticated(): + if request.user.is_authenticated: this_user = { 'email': request.user.email, 'name': request.user.name @@ -573,6 +587,14 @@ class PaymentOrderView(FormView): return self.render_to_response(context) +@method_decorator( + cache_control( + no_cache=True, + must_revalidate=True, + no_store=True, + ), + name="dispatch", +) class OrderConfirmationView(DetailView, FormView): form_class = UserHostingKeyForm template_name = "datacenterlight/order_detail.html" @@ -585,7 +607,6 @@ class OrderConfirmationView(DetailView, FormView): kwargs.update({'request': self.request}) return kwargs - @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): context = {} # this is amount to be charge/subscribed before VAT and discount @@ -762,7 +783,7 @@ class OrderConfirmationView(DetailView, FormView): 'msg_body': _("Your product will be provisioned as soon as " "we receive the payment."), 'redirect': reverse('hosting:invoices') if - request.user.is_authenticated() else + request.user.is_authenticated else reverse('datacenterlight:index') }, 'stripe_key': settings.STRIPE_API_PUBLIC_KEY, @@ -1046,7 +1067,7 @@ class OrderConfirmationView(DetailView, FormView): latest_invoice.payment_intent ) # TODO: requires_attention is probably wrong value to compare - if request.user.is_authenticated(): + if request.user.is_authenticated: if 'generic_payment_details' in request.session: redirect_url = reverse('hosting:invoices') else: @@ -1128,7 +1149,7 @@ class OrderConfirmationView(DetailView, FormView): 'status': True, 'redirect': ( reverse('hosting:virtual_machines') - if request.user.is_authenticated() + if request.user.is_authenticated else reverse('datacenterlight:index') ), 'msg_title': str(_('Thank you for the order.')), @@ -1592,7 +1613,7 @@ def do_provisioning(request, stripe_api_cus_id, card_details_response, logger.debug("Sent user/admin emails") if real_request: clear_all_session_vars(real_request) - if real_request.user.is_authenticated(): + if real_request.user.is_authenticated: redirect_url = reverse('hosting:invoices') logger.debug("redirect_url = %s " % redirect_url) response = { diff --git a/hosting/forms.py b/hosting/forms.py index aadf15b7..cd4a52ab 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -218,7 +218,7 @@ class UserHostingKeyForm(forms.ModelForm): return self.data.get('name') def clean_user(self): - return self.request.user if self.request.user.is_authenticated() else None + return self.request.user if self.request.user.is_authenticated else None def clean(self): cleaned_data = self.cleaned_data diff --git a/hosting/mixins.py b/hosting/mixins.py index dfb010cf..f90470b1 100644 --- a/hosting/mixins.py +++ b/hosting/mixins.py @@ -21,7 +21,7 @@ class ProcessVMSelectionMixin(object): request.session['template'] = data request.session['specs'] = configuration.serialize() - if not request.user.is_authenticated(): + if not request.user.is_authenticated: request.session['next'] = reverse('hosting:payment') return redirect(reverse('hosting:login')) return redirect(reverse('hosting:payment')) diff --git a/hosting/views.py b/hosting/views.py index 0f25f205..6b1828ec 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -278,7 +278,7 @@ class SignupView(HostingContextMixin, CreateView): @method_decorator(decorators) def get(self, request, *args, **kwargs): - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: return HttpResponseRedirect(self.get_success_url()) return super(SignupView, self).get(request, *args, **kwargs) @@ -355,7 +355,7 @@ class SignupValidatedView(SignupValidateView, HostingContextMixin): @method_decorator(decorators) def get(self, request, *args, **kwargs): - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: return HttpResponseRedirect(reverse_lazy('hosting:dashboard')) return super(SignupValidatedView, self).get(request, *args, **kwargs) @@ -1746,7 +1746,7 @@ class VirtualMachineView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): vm = self.get_object() if vm is None: - if self.request.is_ajax(): + if self.request.headers.get("x-requested-with") == "XMLHttpRequest": storage = messages.get_messages(request) for m in storage: pass @@ -1754,7 +1754,7 @@ class VirtualMachineView(LoginRequiredMixin, View): return JsonResponse({'text': gettext('Terminated')}) else: return redirect(reverse('hosting:virtual_machines')) - elif self.request.is_ajax(): + elif self.request.headers.get("x-requested-with") == "XMLHttpRequest": return HttpResponse() context = None try: diff --git a/membership/views.py b/membership/views.py index 6fea3edd..032cbc0e 100644 --- a/membership/views.py +++ b/membership/views.py @@ -83,7 +83,7 @@ class LoginRegistrationView(View): login_form = LoginForm() register_form = RegisterForm() request.session['next'] = None - if request.user.is_authenticated(): + if request.user.is_authenticated: return redirect("membership") else: return render(request, 'templates/login.html', diff --git a/nosystemd/views.py b/nosystemd/views.py index 7f6e0fc7..51cdcaa6 100644 --- a/nosystemd/views.py +++ b/nosystemd/views.py @@ -25,7 +25,7 @@ class LandingView(TemplateView): def get_context_data(self, *args, **kwargs): allow_donation = self.request.user.is_anonymous() or \ - (self.request.user.is_authenticated() and + (self.request.user.is_authenticated and not DonatorStatus.objects.filter(user=self.request.user).exists()) total_donations_amount = Donation.get_total_donations_amount() diff --git a/requirements.txt b/requirements.txt index 6caf33a0..95166ffe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,6 +60,8 @@ djangocms-text-ckeditor==5.1.7 djangocms-video==3.1.0 djangorestframework==3.18.1 easy-thumbnails==2.10.1 +factory_boy==3.3.3 +Faker==40.40.0 gunicorn==26.2.0 html5lib==1.1 idna==3.20 diff --git a/utils/views.py b/utils/views.py index c1fbed36..dc728ce2 100644 --- a/utils/views.py +++ b/utils/views.py @@ -69,7 +69,7 @@ class LoginViewMixin(FormView): @cache_control(no_cache=True, must_revalidate=True, no_store=True) def get(self, request, *args, **kwargs): - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: return HttpResponseRedirect(self.get_success_url()) return super(LoginViewMixin, self).get(request, *args, **kwargs) @@ -225,7 +225,7 @@ class SSHKeyCreateView(FormView): 'form': UserHostingKeyForm(request=self.request), }) - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: owner = self.request.user manager = OpenNebulaManager( email=owner.username,