def test_get_client_credentials_token(request_ctx): """Test retrieval of the webhook tokens.""" with request_ctx("/"), patch("orcid_hub.utils.requests.post") as mockpost: admin = User.get(email="*****@*****.**") org = admin.organisation login_user(admin) mockresp = MagicMock(status_code=201) mockresp.json.return_value = { "access_token": "ACCESS-TOKEN-123", "token_type": "bearer", "refresh_token": "REFRESH-TOKEN-123", "expires_in": 99999, "scope": "/webhook", "orcid": None, } mockpost.return_value = mockresp OrcidToken.create( org=org, access_token="access_token", refresh_token="refresh_token", scopes="/webhook" ) token = utils.get_client_credentials_token(org, "/webhook") assert ( OrcidToken.select() .where(OrcidToken.org == org, OrcidToken.scopes == "/webhook") .count() == 1 ) assert token.access_token == "ACCESS-TOKEN-123" assert token.refresh_token == "REFRESH-TOKEN-123" assert token.expires_in == 99999 assert token.scopes == "/webhook"
def test_test_database(models): """Test of the consitency of the test database.""" assert Organisation.select().count() == 10 assert User.select().count() == 63 assert OrcidToken.select().count() == 60 assert AffiliationRecord.select().count() == 10 assert FundingRecord.select().count() == 10 assert FundingContributor.select().count() == 10 assert FundingInvitee.select().count() == 10 assert ExternalId.select().count() == 10 assert WorkRecord.select().count() == 10 assert WorkContributor.select().count() == 10 assert WorkExternalId.select().count() == 10 assert WorkInvitee.select().count() == 10 assert PeerReviewRecord.select().count() == 10 assert PeerReviewExternalId.select().count() == 10 assert PeerReviewInvitee.select().count() == 10 assert ResearcherUrlRecord.select().count() == 10 assert OtherNameRecord.select().count() == 10 assert KeywordRecord.select().count() == 10 assert Task.select().count() == 30 assert UserOrgAffiliation.select().count() == 30 assert User.get(id=43).admin_for.count() == 10 assert User.get(id=1).admin_for.count() == 0 assert User.get(id=42).admin_for.count() > 0 assert User.get(id=2).organisations.count() > 0 assert Organisation.get(id=1).admins.count() == 1 assert Organisation.get(id=5).users.count() > 0 assert Organisation.get(id=5).admins.count() > 0 assert User.select().where(User.orcid == User.get( email="*****@*****.**").orcid).count() == 3 assert len(User.get(email="*****@*****.**").org_links) == 3 user = User.get(email="*****@*****.**") available_organisations = user.available_organisations assert available_organisations.count() == 10 admin = User.create(email="*****@*****.**", organisation=user.organisation, confirmed=True, first_name="TEST", last_name="ADMIN", roles=Role.ADMIN) ui = UserInvitation.create(email=user.email, invitee=user, inviter=admin, token="TOKEN-123") admin.delete_instance() ui = UserInvitation.get(ui.id) assert ui.inviter_id is None user.delete_instance() assert not UserInvitation.select().where(UserInvitation.id == ui.id).exists() org = Organisation.select().limit(1).first() user = User.select().limit(1).first() ot = OrcidToken.create(user=user, org=org, scope="S1,S2,S3") assert len(ot.scopes) == 3 ot.scopes = ["A", "B", "C", "D"] assert ot.scope == "A,B,C,D"
def test_link_orcid_auth_callback_with_affiliation(name, mocker, client): """Test ORCID callback - the user authorized the organisation access to the ORCID profile.""" mocker.patch("requests_oauthlib.OAuth2Session.fetch_token", lambda self, *args, **kwargs: dict( name="NEW TEST", access_token="ABC123", orcid="ABC-123-456-789", scope=['/read-limited,/activities/update'], expires_in="1212", refresh_token="ABC1235")) m = mocker.patch("orcid_hub.orcid_client.MemberAPI") mocker.patch("orcid_hub.orcid_client.SourceClientId") org = Organisation.get(name="THE ORGANISATION") test_user = User.create( name=name, email="*****@*****.**", organisation=org, orcid="ABC123", confirmed=True) UserOrg.create(user=test_user, org=org, affiliations=Affiliation.EMP | Affiliation.EDU) client.login(test_user) resp = client.get("/link") state = session['oauth_state'] resp = client.get(f"/auth?state={state}") api_mock = m.return_value test_user = User.get(test_user.id) assert test_user.orcid == "ABC-123-456-789" orcid_token = OrcidToken.get(user=test_user, org=org) assert orcid_token.access_token == "ABC123" api_mock.create_or_update_affiliation.assert_has_calls([ call(affiliation=Affiliation.EDU, initial=True), call(affiliation=Affiliation.EMP, initial=True), ]) # User with no Affiliation, should get flash warning. user_org = UserOrg.get(user=test_user, org=org) user_org.affiliations = Affiliation.NONE user_org.save() orcid_token.delete_instance() assert OrcidToken.select().where(OrcidToken.user == test_user, OrcidToken.org == org).count() == 0 resp = client.get(f"/auth?state={state}") assert resp.status_code == 302 assert b"<!DOCTYPE HTML" in resp.data, "Expected HTML content" assert "profile" in resp.location, "redirection to 'profile' showing the ORCID" assert OrcidToken.select().where(OrcidToken.user == test_user, OrcidToken.org == org).count() == 1 get_person = mocker.patch("requests_oauthlib.OAuth2Session.get", return_value=Mock(status_code=200)) resp = client.get(f"/profile", follow_redirects=True) assert b"can create and update research activities" in resp.data get_person.assert_called_once() get_person = mocker.patch("requests_oauthlib.OAuth2Session.get", return_value=Mock(status_code=401)) resp = client.get(f"/profile", follow_redirects=True) assert b"you'll be taken to ORCID to create or sign into your ORCID record" in resp.data get_person.assert_called_once()
def test_orcidtoken_count(test_models): assert OrcidToken.select().count() == 60
def test_webhook_registration(client): """Test webhook registration.""" test_client = client user = User.get(email="*****@*****.**") test_client.login(user) org = user.organisation orcid_id = "0000-0000-0000-00X3" client = Client.get(org=org) resp = test_client.post( "/oauth/token", data=dict( grant_type="client_credentials", client_id=client.client_id, client_secret=client.client_secret, scope="/webhook", ), ) assert resp.status_code == 200 data = json.loads(resp.data) client = Client.get(client_id="CLIENT_ID") token = Token.select().where(Token.user == user, Token._scopes == "/webhook").first() assert data["access_token"] == token.access_token assert data["expires_in"] == test_client.application.config["OAUTH2_PROVIDER_TOKEN_EXPIRES_IN"] assert data["token_type"] == token.token_type # prevously created access token should be removed resp = test_client.put( "/api/v1/INCORRECT/webhook/http%3A%2F%2FCALL-BACK", headers=dict(authorization=f"Bearer {token.access_token}"), ) assert resp.status_code == 415 assert json.loads(resp.data)["error"] == "Missing or invalid ORCID iD." resp = test_client.put( "/api/v1/0000-0001-8228-7153/webhook/http%3A%2F%2FCALL-BACK", headers=dict(authorization=f"Bearer {token.access_token}"), ) assert resp.status_code == 404 assert json.loads(resp.data)["error"] == "Invalid ORCID iD." resp = test_client.put( f"/api/v1/{orcid_id}/webhook/INCORRECT-WEBHOOK-URL", headers=dict(authorization=f"Bearer {token.access_token}"), ) assert resp.status_code == 415 assert json.loads(resp.data) == { "error": "Invalid call-back URL", "message": "Invalid call-back URL: INCORRECT-WEBHOOK-URL", } with patch("orcid_hub.utils.requests.post") as mockpost, patch( "orcid_hub.utils.requests.put" ) as mockput: # Access toke request resp: mockresp = MagicMock(status_code=201) mockresp.json.return_value = { "access_token": "ACCESS-TOKEN-123", "token_type": "bearer", "refresh_token": "REFRESH-TOKEN-123", "expires_in": 99999, "scope": "/webhook", "orcid": None, } mockpost.return_value = mockresp # Webhook registration response: mockresp = MagicMock(status_code=201, data=b"") mockresp.headers = { "Server": "TEST123", "Connection": "keep-alive", "Pragma": "no-cache", "Expires": "0", } mockput.return_value = mockresp resp = test_client.put( f"/api/v1/{orcid_id}/webhook/http%3A%2F%2FCALL-BACK", headers=dict(authorization=f"Bearer {token.access_token}"), ) assert resp.status_code == 201 args, kwargs = mockpost.call_args assert args[0] == "https://sandbox.orcid.org/oauth/token" assert kwargs["data"] == { "client_id": "APP-12345678", "client_secret": "CLIENT-SECRET", "scope": "/webhook", "grant_type": "client_credentials", } assert kwargs["headers"] == {"Accept": "application/json"} args, kwargs = mockput.call_args assert ( args[0] == "https://api.sandbox.orcid.org/0000-0000-0000-00X3/webhook/http%3A%2F%2FCALL-BACK" ) assert kwargs["headers"] == { "Accept": "application/json", "Authorization": "Bearer ACCESS-TOKEN-123", "Content-Length": "0", } q = OrcidToken.select().where(OrcidToken.org == org, OrcidToken.scopes == "/webhook") assert q.count() == 1 orcid_token = q.first() assert orcid_token.access_token == "ACCESS-TOKEN-123" assert orcid_token.refresh_token == "REFRESH-TOKEN-123" assert orcid_token.expires_in == 99999 assert orcid_token.scopes == "/webhook" with patch("orcid_hub.utils.requests.delete") as mockdelete: # Webhook deletion response: mockresp = MagicMock(status_code=204, data=b"") mockresp.headers = { "Seresper": "TEST123", "Connection": "keep-alive", "Location": "TEST-LOCATION", "Pragma": "no-cache", "Expires": "0", } mockdelete.return_value = mockresp resp = test_client.delete( f"/api/v1/{orcid_id}/webhook/http%3A%2F%2FCALL-BACK", headers=dict(authorization=f"Bearer {token.access_token}"), ) assert resp.status_code == 204 assert urlparse(resp.location).path == f"/api/v1/{orcid_id}/webhook/http://TEST-LOCATION" args, kwargs = mockput.call_args assert ( args[0] == "https://api.sandbox.orcid.org/0000-0000-0000-00X3/webhook/http%3A%2F%2FCALL-BACK" ) assert kwargs["headers"] == { "Accept": "application/json", "Authorization": "Bearer ACCESS-TOKEN-123", "Content-Length": "0", } q = OrcidToken.select().where(OrcidToken.org == org, OrcidToken.scopes == "/webhook") assert q.count() == 1 token = q.first() assert token.access_token == "ACCESS-TOKEN-123" assert token.refresh_token == "REFRESH-TOKEN-123" assert token.expires_in == 99999 assert token.scopes == "/webhook"
def test_org_webhook_api(client, mocker): """Test Organisation webhooks.""" mocker.patch.object( utils.requests, "post", lambda *args, **kwargs: Mock( status_code=201, json=lambda: dict( access_token="ABC123", refresh_token="REFRESH_ME", expires_in=123456789 ), ), ) mockput = mocker.patch.object(utils.requests, "put") mockdelete = mocker.patch.object(utils.requests, "delete") org = client.data["org"] admin = org.tech_contact send_email = mocker.patch("orcid_hub.utils.send_email") api_client = Client.get(org=org) resp = client.post( "/oauth/token", data=dict( grant_type="client_credentials", client_id=api_client.client_id, client_secret=api_client.client_secret, scope="/webhook", ), ) assert resp.status_code == 200 data = json.loads(resp.data) api_client = Client.get(client_id="CLIENT_ID") token = Token.select().where(Token.user == admin, Token._scopes == "/webhook").first() assert data["access_token"] == token.access_token assert data["expires_in"] == client.application.config["OAUTH2_PROVIDER_TOKEN_EXPIRES_IN"] assert data["token_type"] == token.token_type client.access_token = token.access_token resp = client.put("/api/v1/webhook/INCORRECT-WEBHOOK-URL") assert resp.status_code == 415 assert json.loads(resp.data) == { "error": "Invalid call-back URL", "message": "Invalid call-back URL: INCORRECT-WEBHOOK-URL", } # Webhook registration response: mockresp = MagicMock(status_code=201, data=b"") mockresp.headers = { "Server": "TEST123", "Connection": "keep-alive", "Pragma": "no-cache", "Expires": "0", } mockput.return_value = mockresp # Webhook deletion response: mockresp = MagicMock(status_code=204, data=b"") mockresp.headers = { "Seresper": "TEST123", "Connection": "keep-alive", "Location": "TEST-LOCATION", "Pragma": "no-cache", "Expires": "0", } mockdelete.return_value = mockresp resp = client.put("/api/v1/webhook/http%3A%2F%2FCALL-BACK") assert resp.status_code == 200 resp = client.put( "/api/v1/webhook/http%3A%2F%2FCALL-BACK", data=dict(enabled=True, url="https://CALL-BACK.edu/callback"), ) assert resp.status_code == 201 server_name = client.application.config["SERVER_NAME"] mockput.assert_has_calls( [ call( "https://api.sandbox.orcid.org/1001-0001-0001-0001/webhook/" f"https%3A%2F%2F{server_name}%2Fservices%2F21%2Fupdated", headers={ "Accept": "application/json", "Authorization": "Bearer ABC123", "Content-Length": "0", }, ), call( "https://api.sandbox.orcid.org/0000-0000-0000-00X3/webhook/" f"https%3A%2F%2F{server_name}%2Fservices%2F22%2Fupdated", headers={ "Accept": "application/json", "Authorization": "Bearer ABC123", "Content-Length": "0", }, ), call( "https://api.sandbox.orcid.org/0000-0000-0000-11X2/webhook/" f"https%3A%2F%2F{server_name}%2Fservices%2F30%2Fupdated", headers={ "Accept": "application/json", "Authorization": "Bearer ABC123", "Content-Length": "0", }, ), ] ) q = OrcidToken.select().where(OrcidToken.org == org, OrcidToken.scopes == "/webhook") assert q.exists() assert q.count() == 1 orcid_token = q.first() assert orcid_token.access_token == "ABC123" assert orcid_token.refresh_token == "REFRESH_ME" assert orcid_token.expires_in == 123456789 assert orcid_token.scopes == "/webhook" # deactivate: resp = client.delete("/api/v1/webhook/http%3A%2F%2FCALL-BACK") assert resp.status_code == 200 assert "job-id" in resp.json # activate with all options: mockput.reset_mock() resp = client.put( "/api/v1/webhook", data={ "enabled": True, "append-orcid": True, "apikey": "APIKEY123", "email-notifications-enabled": True, "notification-email": "*****@*****.**", }, ) server_name = client.application.config["SERVER_NAME"] mockput.assert_has_calls( [ call( "https://api.sandbox.orcid.org/1001-0001-0001-0001/webhook/" f"https%3A%2F%2F{server_name}%2Fservices%2F21%2Fupdated", headers={ "Accept": "application/json", "Authorization": "Bearer ABC123", "Content-Length": "0", }, ), call( "https://api.sandbox.orcid.org/0000-0000-0000-00X3/webhook/" f"https%3A%2F%2F{server_name}%2Fservices%2F22%2Fupdated", headers={ "Accept": "application/json", "Authorization": "Bearer ABC123", "Content-Length": "0", }, ), call( "https://api.sandbox.orcid.org/0000-0000-0000-11X2/webhook/" f"https%3A%2F%2F{server_name}%2Fservices%2F30%2Fupdated", headers={ "Accept": "application/json", "Authorization": "Bearer ABC123", "Content-Length": "0", }, ), ] ) # Link other org to the users org2 = Organisation.select().where(Organisation.id != org.id).first() UserOrg.insert_many([dict(user_id=u.id, org_id=org2.id) for u in org.users]).execute() org2.webhook_enabled = True org2.save() resp = client.delete("/api/v1/webhook") mockput.reset_mock() resp = client.put( "/api/v1/webhook", data={ "enabled": False, "url": "https://CALL-BACK.edu/callback", "append-orcid": False, "email-notifications-enabled": True, "notification-email": "*****@*****.**", }, ) mockput.assert_not_called() # Test update summary: User.update( orcid_updated_at=datetime.date.today().replace(day=1) - datetime.timedelta(days=15) ).execute() send_email.reset_mock() utils.send_orcid_update_summary() send_email.assert_called_once() client.logout()
def test_orcid_login_callback_researcher_flow(client, mocker): """Test login from orcid callback function for researcher and display profile.""" fetch_token = mocker.patch("orcid_hub.OAuth2Session.fetch_token", side_effect=fetch_token_mock) mocker.patch( "orcid_hub.orcid_client.MemberAPI.create_or_update_affiliation", side_effect=affiliation_mock) org = Organisation.create( name="THE ORGANISATION:test_orcid_login_callback_researcher_flow", tuakiri_name= "THE ORGANISATION:test_orcid_login_callback_researcher_flow", confirmed=True, orcid_client_id="CLIENT ID", orcid_secret="Client Secret", city="CITY", country="COUNTRY", disambiguated_id="ID", disambiguation_source="RINGGOLD", is_email_sent=True) u = User.create(email="*****@*****.**", name="TEST USER", roles=Role.RESEARCHER, orcid="123", confirmed=True, organisation=org) UserOrg.create(user=u, org=org, is_admin=False) token = utils.new_invitation_token() UserInvitation.create(email=u.email, token=token, affiliations=Affiliation.EMP, org=org, invitee=u) resp = client.get(f"/orcid/login/{token}") assert resp.status_code == 200 assert token.encode() in resp.data state = session['oauth_state'] resp = client.get(f"/auth/?state={state}&login=1") assert resp.status_code == 302 assert resp.location.endswith("/link") fetch_token.assert_called_once() resp = client.get(f"/auth/?invitation_token={token}&login=1") assert resp.status_code == 302 assert urlparse(resp.location).path == "/" fetch_token.assert_called_once() resp = client.get(f"/auth/?invitation_token={token}&login=1", follow_redirects=True) assert b"Danger" in resp.data assert b"Something went wrong, Please retry giving permissions " in resp.data fetch_token.reset_mock() resp = client.get(f"/auth/?invitation_token={token}&state={state}", follow_redirects=True) assert b"Warning" in resp.data assert b"The ORCID Hub was not able to automatically write an affiliation" in resp.data OrcidToken.delete().where(OrcidToken.user == u, OrcidToken.org == org).execute() fetch_token.reset_mock() resp = client.get(f"/auth/?invitation_token={token}&state={state}&login=1") assert resp.status_code == 302 assert resp.location.endswith("/profile") fetch_token.assert_called_once() assert OrcidToken.select().where(OrcidToken.user == u).count() == 1 resp = client.get(f"/orcid/login/{token}", follow_redirects=True) assert b"You have already given permission" in resp.data
def test_webhook_registration(app_req_ctx): """Test webhook registration.""" user = User.get(email="*****@*****.**") org = user.organisation orcid_id = "0000-0000-0000-00X3" client = Client.get(org=org) with app_req_ctx("/oauth/token", method="POST", data=dict(grant_type="client_credentials", client_id=client.client_id, client_secret=client.client_secret, scope="/webhook")) as ctx: login_user(user) rv = ctx.app.full_dispatch_request() assert rv.status_code == 200 data = json.loads(rv.data) token = Token.get(user=user, _scopes="/webhook") client = Client.get(client_id="CLIENT_ID") token = Token.get(client=client) assert data["access_token"] == token.access_token assert data["expires_in"] == ctx.app.config[ "OAUTH2_PROVIDER_TOKEN_EXPIRES_IN"] assert data["token_type"] == token.token_type # prevously created access token should be removed with app_req_ctx( f"/api/v1.0/{orcid_id}/webhook/http%3A%2F%2FCALL-BACK", method="PUT", headers=dict(authorization=f"Bearer {token.access_token}") ) as ctx, patch("orcid_hub.utils.requests.post") as mockpost, patch( "orcid_hub.utils.requests.put") as mockput: # Access toke request resp: mockresp = MagicMock(status_code=201) mockresp.json.return_value = { "access_token": "ACCESS-TOKEN-123", "token_type": "bearer", "refresh_token": "REFRESH-TOKEN-123", "expires_in": 99999, "scope": "/webhook", "orcid": None } mockpost.return_value = mockresp # Webhook registration response: mockresp = MagicMock(status_code=201, data=b'') # mockresp.raw.stream = lambda *args, **kwargs: iter([b"""{"data": "TEST"}"""]) mockresp.raw.headers = { "Server": "TEST123", "Connection": "keep-alive", "Location": "LOCATION", "Pragma": "no-cache", "Expires": "0", } mockput.return_value = mockresp resp = ctx.app.full_dispatch_request() assert resp.status_code == 201 args, kwargs = mockpost.call_args assert args[0] == "https://sandbox.orcid.org/oauth/token" assert kwargs["data"] == { "client_id": "APP-12345678", "client_secret": "CLIENT-SECRET", "scope": "/webhook", "grant_type": "client_credentials" } assert kwargs["headers"] == {"Accept": "application/json"} args, kwargs = mockput.call_args assert args[ 0] == "https://api.sandbox.orcid.org/0000-0000-0000-00X3/webhook/http%3A%2F%2FCALL-BACK" assert kwargs["headers"] == { "Accept": "application/json", "Authorization": "Bearer ACCESS-TOKEN-123", "Content-Length": "0" } q = OrcidToken.select().where(OrcidToken.org == org, OrcidToken.scope == "/webhook") assert q.count() == 1 orcid_token = q.first() assert orcid_token.access_token == "ACCESS-TOKEN-123" assert orcid_token.refresh_token == "REFRESH-TOKEN-123" assert orcid_token.expires_in == 99999 assert orcid_token.scope == "/webhook" with app_req_ctx( f"/api/v1.0/{orcid_id}/webhook/http%3A%2F%2FCALL-BACK", method="DELETE", headers=dict(authorization=f"Bearer {token.access_token}") ) as ctx, patch("orcid_hub.utils.requests.delete") as mockdelete: # Webhook deletion response: mockresp = MagicMock(status_code=204, data=b'') # mockresp.raw.stream = lambda *args, **kwargs: iter([b"""{"data": "TEST"}"""]) mockresp.raw.headers = { "Server": "TEST123", "Connection": "keep-alive", "Location": "LOCATION", "Pragma": "no-cache", "Expires": "0", } mockdelete.return_value = mockresp resp = ctx.app.full_dispatch_request() assert resp.status_code == 204 args, kwargs = mockput.call_args assert args[ 0] == "https://api.sandbox.orcid.org/0000-0000-0000-00X3/webhook/http%3A%2F%2FCALL-BACK" assert kwargs["headers"] == { "Accept": "application/json", "Authorization": "Bearer ACCESS-TOKEN-123", "Content-Length": "0" } q = OrcidToken.select().where(OrcidToken.org == org, OrcidToken.scope == "/webhook") assert q.count() == 1 token = q.first() assert token.access_token == "ACCESS-TOKEN-123" assert token.refresh_token == "REFRESH-TOKEN-123" assert token.expires_in == 99999 assert token.scope == "/webhook"