diff --git a/lms/envs/pact.py b/lms/envs/pact.py index df3e3ee51a..cc8fd1fb33 100644 --- a/lms/envs/pact.py +++ b/lms/envs/pact.py @@ -11,4 +11,7 @@ PROVIDER_STATES_URL = True MOCK_USERNAME = 'Mock User' ######################### Add Authentication Middleware for Pact Verification Calls ######################### -MIDDLEWARE = MIDDLEWARE + ['common.test.pacts.middleware.AuthenticationMiddleware', ] +MIDDLEWARE = MIDDLEWARE + [ + 'common.test.pacts.middleware.AuthenticationMiddleware', + 'openedx.core.djangoapps.user_api.accounts.tests.pact.user-middleware', +] diff --git a/openedx/core/djangoapps/user_api/accounts/tests/pact/__init__.py b/openedx/core/djangoapps/user_api/accounts/tests/pact/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/user_api/accounts/tests/pact/frontend-app-profile-edx-platform.json b/openedx/core/djangoapps/user_api/accounts/tests/pact/frontend-app-profile-edx-platform.json new file mode 100644 index 0000000000..7d64c788a7 --- /dev/null +++ b/openedx/core/djangoapps/user_api/accounts/tests/pact/frontend-app-profile-edx-platform.json @@ -0,0 +1,78 @@ +{ + "consumer": { + "name": "frontend-app-profile" + }, + "provider": { + "name": "edx-platform" + }, + "interactions": [ + { + "description": "A request for user's basic information", + "providerState": "I have a user's basic information", + "request": { + "method": "GET", + "path": "/api/user/v1/accounts/staff" + }, + "response": { + "body": { + "bio": "This is my bio", + "country": "ME", + "name": "Lemon Seltzer", + "username": "staff", + "is_active": true, + "gender": "m", + "mailing_address": "Park Ave", + "goals": "Learn and Grow!", + "year_of_birth": 1901, + "phone_number": "+11234567890" + }, + "headers": {"Content-Type": "application/json"}, + "matchingRules": { + "$.body.bio": { + "match": "type" + }, + "$.body.country": { + "match": "type" + }, + "$.body.name": { + "match": "type" + }, + "$.body.username": { + "match": "type" + }, + "$.body.is_active": { + "match": "type" + }, + "$.body.gender": { + "match": "type" + }, + "$.body.mailing_address": { + "match": "type" + }, + "$.body.goals": { + "match": "type" + }, + "$.body.year_of_birth": { + "match": "type" + }, + "$.body.phone_number": { + "match": "type" + }, + "status": 200 + } + } + } + ], + "metadata": { + "pact-js": { + "version": "11.0.2" + }, + "pactRust": { + "ffi": "0.4.0", + "models": "1.0.4" + }, + "pactSpecification": { + "version": "2.0.0" + } + } + } diff --git a/openedx/core/djangoapps/user_api/accounts/tests/pact/user-middleware.py b/openedx/core/djangoapps/user_api/accounts/tests/pact/user-middleware.py new file mode 100644 index 0000000000..4042db69f7 --- /dev/null +++ b/openedx/core/djangoapps/user_api/accounts/tests/pact/user-middleware.py @@ -0,0 +1,32 @@ +""" +Contain the middleware logic needed during pact verification +""" +from django.contrib import auth +from django.utils.deprecation import MiddlewareMixin + +User = auth.get_user_model() + + +class AuthenticationMiddleware(MiddlewareMixin): + """ + Middleware to add default authentication into the requests for pact verification. + + This middleware is required to add a default authenticated user and bypass CSRF validation + into the requests during the pact verification workflow. Without the authentication, the pact verification + process will not work as the apis. + See https://docs.pact.io/faq#how-do-i-test-oauth-or-other-security-headers + """ + + def __init__(self, get_response): + super().__init__(get_response) + self.auth_user = User.objects.get_or_create(username='staff', is_staff=True)[0] + self.get_response = get_response + + def process_view(self, request, view_func, view_args, view_kwargs): # pylint: disable=unused-argument + """ + Add a default authenticated user and remove CSRF checks for a request + in a subset of views. + """ + if request.user.is_anonymous and 'Pact-Authentication' in request.headers: + request.user = self.auth_user + request._dont_enforce_csrf_checks = True # pylint: disable=protected-access diff --git a/openedx/core/djangoapps/user_api/accounts/tests/pact/verify_user.py b/openedx/core/djangoapps/user_api/accounts/tests/pact/verify_user.py new file mode 100644 index 0000000000..feffcbbc82 --- /dev/null +++ b/openedx/core/djangoapps/user_api/accounts/tests/pact/verify_user.py @@ -0,0 +1,79 @@ +""" +User Verification Server for Profile Information +""" +import os +import logging +from django.test import LiveServerTestCase +from django.urls import reverse +from pact import Verifier + +from common.djangoapps.student.tests.factories import UserFactory +from common.djangoapps.student.models import User +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST +import json + +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) +PACT_DIR = os.path.dirname(os.path.realpath(__file__)) +PACT_FILE = "frontend-app-profile-edx-platform.json" + + +class ProviderState(): + """ Provider State for the testing profile """ + + def account_setup(self, request): + """ Sets up the Profile that we want to mock in accordance to our contract """ + User.objects.filter(username="staff").delete() + user_acc = UserFactory.create(username="staff") + user_acc.profile.name = "Lemon Seltzer" + user_acc.profile.bio = "This is my bio" + user_acc.profile.country = "ME" + user_acc.profile.is_active = True + user_acc.profile.goals = "Learn and Grow!" + user_acc.profile.year_of_birth = 1901 + user_acc.profile.phone_number = "+11234567890" + user_acc.profile.mailing_address = "Park Ave" + user_acc.profile.save() + return user_acc + + +@csrf_exempt +@require_POST +def provider_state(request): + """ Provider State view for our verifier""" + state_setup = {"I have a user's basic information": ProviderState().account_setup} + request_body = json.loads(request.body) + state = request_body.get('state') + User.objects.filter(username="staff").delete() + print('Setting up provider state for state value: {}'.format(state)) + state_setup["I have a user's basic information"](request) + return JsonResponse({'result': state}) + + +class ProviderVerificationServer(LiveServerTestCase): + """ Live Server for Pact Account Verification """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.PACT_URL = cls.live_server_url + + cls.verifier = Verifier( + provider='edx-platform', + provider_base_url=cls.PACT_URL, + ) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_pact(self): + output, _ = self.verifier.verify_pacts( + os.path.join(PACT_DIR, PACT_FILE), + headers=['Pact-Authentication: Allow', ], + provider_states_setup_url=f"{self.PACT_URL}{reverse('acc-provider-state-view')}", + ) + assert output == 0 diff --git a/openedx/core/djangoapps/user_api/urls.py b/openedx/core/djangoapps/user_api/urls.py index e93e7119ec..1539ca18d6 100644 --- a/openedx/core/djangoapps/user_api/urls.py +++ b/openedx/core/djangoapps/user_api/urls.py @@ -5,6 +5,7 @@ Defines the URL routes for this app. from django.conf import settings from django.urls import include, path, re_path +from django.conf.urls import url from rest_framework import routers from ..profile_images.views import ProfileImageView @@ -226,3 +227,14 @@ urlpatterns = [ path('v1/preferences/time_zones/', user_api_views.CountryTimeZoneListView.as_view(), ), ] + +# Provider States url for Account +if getattr(settings, 'PROVIDER_STATES_URL', None): + from openedx.core.djangoapps.user_api.accounts.tests.pact.verify_user import provider_state as acc_provider_state + urlpatterns += [ + url( + r'^pact/provider_states/$', + acc_provider_state, + name='acc-provider-state-view', + ) + ]