diff --git a/README.md b/README.md index 4263889533..604810e6d8 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,9 @@ For more information on the web component refer to: # License [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) + +# Testing Thanks + +Thanks to BrowserStack for Testing Tool support via OpenSource Licensing + +[![BrowserStack](browserstack-logo-white-small.png)](http://browserstack.com/) diff --git a/auth-api/Makefile b/auth-api/Makefile index f1788eafc5..a93634ef33 100644 --- a/auth-api/Makefile +++ b/auth-api/Makefile @@ -46,7 +46,7 @@ venv/bin/activate: requirements/prod.txt requirements/dev.txt pip freeze | sort > requirements.txt ;\ cat requirements/repo-libraries.txt >> requirements.txt ;\ pip install -Ur requirements/repo-libraries.txt ;\ - pip install -Ur requirements/dev.txt + pip install -Ur requirements/dev.txt ;\ touch venv/bin/activate # update so it's as new as requirements/prod.txt .PHONY: install-dev @@ -56,7 +56,7 @@ install-dev: venv/bin/activate .PHONY: activate activate: venv/bin/activate - . venv/bin/activate + . venv/bin/activate .PHONY: local-test local-test: venv/bin/activate @@ -72,10 +72,10 @@ local-coverage: venv/bin/activate coverage-report: local-coverage . venv/bin/activate ; \ coverage report ; \ - coverage html + coverage html ## Run the coverage report and display in a browser window -mac-cov: install-dev coverage-report +mac-cov: install-dev coverage-report open -a "Google Chrome" htmlcov/index.html ## run pylint on the package and tests diff --git a/auth-api/config.py b/auth-api/config.py index f45435d98d..a0b19ebe40 100644 --- a/auth-api/config.py +++ b/auth-api/config.py @@ -106,18 +106,21 @@ class _Config(object): # pylint: disable=too-few-public-methods # email server MAIL_SERVER = os.getenv('MAIL_SERVER') MAIL_PORT = os.getenv('MAIL_PORT') - MAIL_USE_TLS = os.getenv('MAIL_USE_TLS') - MAIL_USE_SSL = os.getenv('MAIL_USE_SSL') + MAIL_USE_TLS = bool(os.getenv('MAIL_USE_TLS') == 'True') + MAIL_USE_SSL = bool(os.getenv('MAIL_USE_SSL') == 'True') MAIL_USERNAME = os.getenv('MAIL_USERNAME') MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') MAIL_FROM_ID = os.getenv('MAIL_FROM_ID') # mail token configuration - AUTH_WEB_TOKEN_CONFIRM_URL = os.getenv('AUTH_WEB_TOKEN_CONFIRM_URL') + AUTH_WEB_TOKEN_CONFIRM_PATH = os.getenv('AUTH_WEB_TOKEN_CONFIRM_PATH') EMAIL_SECURITY_PASSWORD_SALT = os.getenv('EMAIL_SECURITY_PASSWORD_SALT') EMAIL_TOKEN_SECRET_KEY = os.getenv('EMAIL_TOKEN_SECRET_KEY') TOKEN_EXPIRY_PERIOD = os.getenv('TOKEN_EXPIRY_PERIOD') + # Legal-API URL + LEGAL_API_URL = os.getenv('LEGAL_API_URL') + # Sentry Config SENTRY_DSN = os.getenv('SENTRY_DSN', None) @@ -202,6 +205,9 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods 4H8UZcVFN95vEKxJiLRjAmj6g273pu9kK4ymXNEjWWJn -----END RSA PRIVATE KEY-----""" + # Legal-API URL + LEGAL_API_URL = 'https://mock-lear-tools.pathfinder.gov.bc.ca/rest/legal-api/0.82/api/v1' + class ProdConfig(_Config): # pylint: disable=too-few-public-methods """Production environment configuration.""" diff --git a/auth-api/jenkins/dev-post.groovy b/auth-api/jenkins/dev-post.groovy deleted file mode 100644 index a28f26d1db..0000000000 --- a/auth-api/jenkins/dev-post.groovy +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env groovy -// Copyright © 2018 Province of British Columbia -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//JENKINS DEPLOY ENVIRONMENT VARIABLES: -// - JENKINS_JAVA_OVERRIDES -Dhudson.model.DirectoryBrowserSupport.CSP= -Duser.timezone=America/Vancouver -// -> user.timezone : set the local timezone so logfiles report correxct time -// -> hudson.model.DirectoryBrowserSupport.CSP : removes restrictions on CSS file load, thus html pages of test reports are displayed pretty -// See: https://docs.openshift.com/container-platform/3.9/using_images/other_images/jenkins.html for a complete list of JENKINS env vars - -import groovy.json.* - -// define constants - values sent in as env vars from whatever calls this pipeline -def APP_NAME = 'auth-api-post' -def DESTINATION_TAG = 'dev' -def TOOLS_TAG = 'tools' -def NAMESPACE_APP = '1rdehl' -def NAMESPACE_SHARED = 'd7eovc' -def NAMESPACE_BUILD = "${NAMESPACE_APP}" + '-' + "${TOOLS_TAG}" -def NAMESPACE_DEPLOY = "${NAMESPACE_APP}" + '-' + "${DESTINATION_TAG}" -def NAMESPACE_UNITTEST = "${NAMESPACE_SHARED}" + '-'+ "${TOOLS_TAG}" - -def ROCKETCHAT_DEVELOPER_CHANNEL='#relationship-developers' - -// post a notification to rocketchat -def rocketChatNotificaiton(token, channel, comments) { - def payload = JsonOutput.toJson([text: comments, channel: channel]) - def rocketChatUrl = "https://chat.pathfinder.gov.bc.ca/hooks/" + "${token}" - - sh(returnStdout: true, - script: "curl -X POST -H 'Content-Type: application/json' --data \'${payload}\' ${rocketChatUrl}") -} - -@NonCPS -boolean triggerBuild(String contextDirectory) { - // Determine if code has changed within the source context directory. - def changeLogSets = currentBuild.changeSets - def filesChangeCnt = 0 - for (int i = 0; i < changeLogSets.size(); i++) { - def entries = changeLogSets[i].items - for (int j = 0; j < entries.length; j++) { - def entry = entries[j] - //echo "${entry.commitId} by ${entry.author} on ${new Date(entry.timestamp)}: ${entry.msg}" - def files = new ArrayList(entry.affectedFiles) - for (int k = 0; k < files.size(); k++) { - def file = files[k] - def filePath = file.path - //echo ">> ${file.path}" - if (filePath.contains(contextDirectory)) { - filesChangeCnt = 1 - k = files.size() - j = entries.length - } - } - } - } - - if ( filesChangeCnt < 1 ) { - echo('The changes do not require a build.') - return false - } else { - echo('The changes require a build.') - return true - } -} - -// Get an image's hash tag -String getImageTagHash(String imageName, String tag = "") { - - if(!tag?.trim()) { - tag = "latest" - } - - def istag = openshift.raw("get istag ${imageName}:${tag} -o template --template='{{.image.dockerImageReference}}'") - return istag.out.tokenize('@')[1].trim() -} - -// define job properties - keep 10 builds only -properties([[$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '', numToKeepStr: '10']]]) - -def run_pipeline = true - -// build wasn't triggered by changes so check with user -if( !triggerBuild(APP_NAME) ) { - stage('No changes. Run pipeline?') { - try { - timeout(time: 1, unit: 'DAYS') { - input message: "Run pipeline?", id: "1234"//, submitter: 'admin' - } - } catch (Exception e) { - run_pipeline = false; - } - } -} - -if( run_pipeline ) { - node { - def build_ok = true - def old_version - - stage("Build ${APP_NAME}") { - script { - openshift.withCluster() { - openshift.withProject("${NAMESPACE_BUILD}") { - try { - echo "Building ${APP_NAME} ..." - def build = openshift.selector("bc", "${APP_NAME}").startBuild() - build.untilEach { - return it.object().status.phase == "Running" - } - build.logs('-f') - } catch (Exception e) { - echo e.getMessage() - build_ok = false - } - } - } - } - } - - if (build_ok) { - stage("Tag ${APP_NAME}:${DESTINATION_TAG}") { - script { - openshift.withCluster() { - openshift.withProject("${NAMESPACE_DEPLOY}") { - old_version = openshift.selector('dc', "${APP_NAME}-${DESTINATION_TAG}").object().status.latestVersion - } - } - openshift.withCluster() { - openshift.withProject("${NAMESPACE_BUILD}") { - try { - echo "Tagging ${APP_NAME} for deployment to ${DESTINATION_TAG} ..." - - // Don't tag with BUILD_ID so the pruner can do it's job; it won't delete tagged images. - // Tag the images for deployment based on the image's hash - def IMAGE_HASH = getImageTagHash("${APP_NAME}") - echo "IMAGE_HASH: ${IMAGE_HASH}" - openshift.tag("${APP_NAME}@${IMAGE_HASH}", "${APP_NAME}:${DESTINATION_TAG}") - } catch (Exception e) { - echo e.getMessage() - build_ok = false - } - } - } - } - } - } - - if (build_ok) { - stage("Deploy ${APP_NAME}-${DESTINATION_TAG}") { - sleep 10 - script { - openshift.withCluster() { - openshift.withProject("${NAMESPACE_DEPLOY}") { - try { - def new_version = openshift.selector('dc', "${APP_NAME}-${DESTINATION_TAG}").object().status.latestVersion - if (new_version == old_version) { - echo "New deployment was not triggered." - } - - def pod_selector = openshift.selector('pod', [ app:"${APP_NAME}-${DESTINATION_TAG}" ]) - pod_selector.untilEach { - deployment = it.objects()[0].metadata.labels.deployment - echo deployment - if (deployment == "${APP_NAME}-${DESTINATION_TAG}-${new_version}" && it.objects()[0].status.phase == 'Running' && it.objects()[0].status.containerStatuses[0].ready) { - return true - } else { - echo "Pod for new deployment not ready" - sleep 5 - return false - } - } - } catch (Exception e) { - echo e.getMessage() - build_ok = false - } - } - } - } - } - } - - if (build_ok) { - try { - stage("Run tests on ${APP_NAME}:${DESTINATION_TAG}") { - script { - openshift.withCluster() { - openshift.withProject("${NAMESPACE_UNITTEST}") { - def test_pipeline = openshift.selector('bc', 'pytest-pipeline') - test_pipeline.startBuild('--wait=true', "-e=component=${APP_NAME}", "-e=component_tag=${DESTINATION_TAG}", "-e=tag=${DESTINATION_TAG}", "-e=namespace=${NAMESPACE_APP}", "-e=db_type=PG").logs('-f') - echo "All tests passed" - } - } - } - } - } catch (Exception e) { - echo e.getMessage() - echo "Not all tests passed." - build_ok = false - } - } - - if (build_ok) { - stage("Run E2E API tests") { - - } - } - - stage("Notify on RocketChat") { - if(build_ok) { - currentBuild.result = "SUCCESS" - } else { - currentBuild.result = "FAILURE" - } - - ROCKETCHAT_TOKEN = sh ( - script: """oc get secret/apitest-secrets -n ${NAMESPACE_BUILD} -o template --template="{{.data.ROCKETCHAT_TOKEN}}" | base64 --decode""", - returnStdout: true).trim() - - rocketChatNotificaiton("${ROCKETCHAT_TOKEN}", "${ROCKETCHAT_DEVELOPER_CHANNEL}", "${APP_NAME} build and deploy to ${DESTINATION_TAG} ${currentBuild.result}!") - } - } -} - diff --git a/auth-api/jenkins/dev.groovy b/auth-api/jenkins/dev.groovy index ad2df1e05c..83621d1d26 100644 --- a/auth-api/jenkins/dev.groovy +++ b/auth-api/jenkins/dev.groovy @@ -21,9 +21,10 @@ import groovy.json.* -// define constants - values sent in as env vars from whatever calls this pipeline +// define constants - values sent in as env vars from whatever calls this pipeline def APP_NAME = 'auth-api' def DESTINATION_TAG = 'dev' +def E2E_TAG = 'e2e' def TOOLS_TAG = 'tools' def NAMESPACE_APP = '1rdehl' def NAMESPACE_SHARED = 'd7eovc' @@ -155,6 +156,23 @@ if( run_pipeline ) { } } } + + stage("Tag ${APP_NAME}:${E2E_TAG}") { + script { + openshift.withCluster() { + openshift.withProject("${NAMESPACE_BUILD}") { + try { + echo "Tagging ${APP_NAME} for deployment to ${E2E_TAG} ..." + openshift.tag("${APP_NAME}:${DESTINATION_TAG}", "${APP_NAME}:${E2E_TAG}") + } catch (Exception e) { + echo e.getMessage() + build_ok = false + } + } + } + } + } + } if (build_ok) { diff --git a/auth-api/jenkins/prod.groovy b/auth-api/jenkins/prod.groovy index 5f9a00a7c6..3a9c8e231e 100644 --- a/auth-api/jenkins/prod.groovy +++ b/auth-api/jenkins/prod.groovy @@ -58,7 +58,12 @@ node { } openshift.withCluster() { openshift.withProject("${NAMESPACE_BUILD}") { - echo "Tagging ${APP_NAME} for deployment to ${DESTINATION_TAG} ..." + // echo "Tagging ${APP_NAME}:${DESTINATION_TAG}-prev ..." + // def IMAGE_HASH = getImageTagHash("${APP_NAME}", "${DESTINATION_TAG}") + // echo "IMAGE_HASH: ${IMAGE_HASH}" + // openshift.tag("${APP_NAME}@${IMAGE_HASH}", "${APP_NAME}:${DESTINATION_TAG}-prev") + + echo "Tagging ${APP_NAME} for deployment to ${DESTINATION_TAG} ..." openshift.tag("${APP_NAME}:${SOURCE_TAG}", "${APP_NAME}:${DESTINATION_TAG}") } } diff --git a/auth-api/logging.conf b/auth-api/logging.conf index ffc1a01e36..8a936c32e4 100644 --- a/auth-api/logging.conf +++ b/auth-api/logging.conf @@ -1,5 +1,5 @@ [loggers] -keys=root,api +keys=root,api,tracing [handlers] keys=console @@ -17,6 +17,12 @@ handlers=console qualname=api propagate=0 +[logger_tracing] +level=ERROR +handlers=console +qualname=jaeger_tracing +propagate=0 + [handler_console] class=StreamHandler level=DEBUG diff --git a/auth-api/migrations/versions/d0cd84858921_documents_and_add_terms_of_service_to_.py b/auth-api/migrations/versions/d0cd84858921_documents_and_add_terms_of_service_to_.py new file mode 100644 index 0000000000..31a51d92e8 --- /dev/null +++ b/auth-api/migrations/versions/d0cd84858921_documents_and_add_terms_of_service_to_.py @@ -0,0 +1,209 @@ +"""documents and add terms of service to user + +Revision ID: d0cd84858921 +Revises: b2749f31f268 +Create Date: 2019-10-27 06:01:54.359356 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd0cd84858921' +down_revision = 'b2749f31f268' +branch_labels = None +depends_on = None + + +def upgrade(): + documents = op.create_table('documents', + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('modified', sa.DateTime(), nullable=True), + sa.Column('version_id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=20), nullable=False), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('modified_by_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['modified_by_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('version_id') + ) + op.add_column('user', sa.Column('is_terms_of_use_accepted', sa.Boolean(), nullable=True)) + op.add_column('user', sa.Column('terms_of_use_accepted_version', sa.Integer(), nullable=True)) + op.create_foreign_key('user_documents_fk', 'user', 'documents', ['terms_of_use_accepted_version'], ['version_id']) + + html_content = """ +
+

The parties to this “Cooperatives Registry Terms and Conditions of Agreement” (the “Agreement”) are Her Majesty the Queen in Right of the Province of British Columbia, as represented by the Minister of Citizens’ Services (the “Province”) and the Subscriber (as defined below).

+ +
+
1. Definitions
+
+

a. “Access” means the non-exclusive right to electronically access and use the Service;

+

b. “Basic Account Subscriber” means a Subscriber with Access to up to ten Entities that pays Fees for Transactions using a credit card;

+

c. “BC Online Terms and Conditions” means the BC Online Terms and Conditions of Agreement found at https://www.bconline.gov.bc.ca/terms_conditions.html

+

d. "Content" means the Service’s Data Base, and all associated information and documentation, including any print copy or electronic display of any information retrieved from the Data Base and associated with the Service;

+

e. "Data Base" means any data base or information stored in electronic format for which Access is made available through the Service;

+

f. "Deposit Account" has the meaning given to it in the BC Online Terms and Conditions;

+

g. "Entity" means any BC co-operative for which a User has Access through the Service;

+

h. "Fees" means all fees and charges for the Service, as described in the Business Corporations Act - Schedule (Section 431) Fees, Cooperative Association Act - Cooperative Association Regulation, Schedule A;

+

i. "Incorporation Number" means the unique numerical identifier for a Subscriber’s cooperative association, and when entered in conjunction with the Passcode, permits a User to access the Service;

+

j. "Passcode" means the unique identifier issued by the Province to a Subscriber with regard to an Entity, which enables a User to have Access with regard to that Entity within the Service;

+

k. "Premium Account Subscriber" means a Subscriber with Access to unlimited Entities that has a Deposit Account with the Province and is charged Fees in accordance with the BC Online Terms and Conditions;

+

l. "Service" means the service operated by the Province that allows a Subscriber to completeTransactions relating to BC Entities.

+

m. "Services Card Number" means the User’s BC Services Card number, which authenticates the identity of the User to the Service;

+

n. "Subscriber" means a person that accesses the Service and that has accepted the terms of this Agreement, and includes Premium Account Subscribers and Basic Account Subscribers;

+

o. "Transaction" means any action performed by the Subscriber or any of its Users to the Service to display, print, transfer, or obtain a copy of information contained on the Service, or where permitted by the Province, to add to or delete information from the Service.

+

p. "User" means an individual that is granted Access on the individual’s behalf, if the individual is also the Subscriber, or on behalf of the Subscriber, if the individual is an employee or is otherwise authorized to act on behalf of the Subscriber, as applicable;

+

q. "Website" means the BC Cooperatives Website at bcregistry.ca/cooperatives and includes all web pages and associated materials, with the exception of the Content.

+
+
+ +
+
2. Acceptance of Agreement
+
+

1.1The Subscriber acknowledges that a duly authorized representative of the Subscriber has accepted the terms of this Agreement on behalf of the Subscriber and its Users.

+

1.2The Subscriber acknowledges and agrees that:

+
+

(a) by creating a profile and/or by clicking the button acknowledging acceptance of this Agreement, each User using the Services on behalf of the Subscriber also accepts, and will be conclusively deemed to have accepted, the terms of this Agreement as they pertain to the User’s use of the Services; and

+

(b)the Subscriber will be solely responsible for its Users’ use of the Services, including without limitation any Fees incurred by its Users in connection with such Services.

+
+

1.3The Subscriber acknowledges that the terms of the BC Services Card Login Service found at (https://www2.gov.bc.ca/gov/content/governments/government-id/bc-services-card/terms-of-use) continue to apply in respect of use of the Services Card.

+

1.4Premium Account Subscribers acknowledge that in addition to this Agreement, the terms of the BC Online Terms and Conditions will continue to apply in respect of the use of the Subscriber’s Deposit Account for payment of Fees for the Service.

+

1.5The Subscriber will ensure that each of its Users are aware of and comply with the terms of this Agreement as they pertain to the User’s use of the Services.

+

1.6The Province reserves the right to make changes to the terms of this Agreement at any time without direct notice to either the Subscriber or its Users, as applicable. The Subscriber acknowledges and agrees that it is the sole responsibility of the Subscriber to review, and, as applicable, to ensure that its Users review, the terms of this Agreement on a regular basis.

+

1.7Following the date of any such changes, the Subscriber will be conclusively deemed to have accepted any such changes on its own behalf and on behalf of its Users, as applicable. The Subscriber acknowledges and agrees that each of its Users must also accept any such changes as they pertain to the User’s use of the Services.

+
+
+ +
+
2. PROPRIETARY RIGHTS
+
+

2.1The Website and the Content is owned by the Province and/or its licensors and is protected by copyright, trademark and other laws. Except as expressly permitted in this Agreement, the Subscriber may not use, reproduce, modify or distribute, or allow any other person to use, reproduce, modify or distribute, any part of the Website in any form whatsoever without the prior written consent of the Province.

+
+
+ +
+
3. SERVICES
+
+

3.1The Province will provide the Subscriber and its Users with Access on the terms and conditions set out in this Agreement.

+

3.2Subject to section 3.3, Access will be available during the hours published on the Website, as may be determined by the Province in its sole discretion from time to time.

+

3.3The Province reserves the right to limit or withdraw Access at any time in order to perform maintenance of the Service or in the event that the integrity or security of the Service is compromised.

+

3.4The Province further reserves the right to discontinue the Service at any time.

+

3.5The Province will provide helpdesk support to assist Users with Access during the hours published on the Website, as may be determined by the Province in its sole discretion from time to time.

+

3.6The Subscriber acknowledges and agrees that, for the purpose of Access:

+
+

(a)it is the Subscriber’s sole responsibility, at the Subscriber’s own expense, to provide, operate and maintain computer hardware and communications software or web browser software that is compatible with the Services; and

+

(b)that any failure to do so may impact the Subscriber’s and/or User’s ability to access the Service.

+
+
+
+ +
+
4. SUBSCRIBER OBLIGATIONS
+
+

4.1The Subscriber will comply, and will ensure that all of its Users comply, with:

+
+

(a)the requirements regarding the integrity and/or security of the Service set out in this Article 4; and

+

(b)all applicable laws

+
+

in connection with the Subscriber’s and/or Users’ use of the Services.

+

4.2The Subscriber will ensure that each User:

+
+

(a)is duly authorized by the Subscriber to perform any Transaction and utilize the Service on behalf of the Subscriber;

+

(b)maintains in confidence Services Card Numbers, Incorporation Numbers and Passcodes;

+

(c)is competent to perform a Transaction and utilize the Service;

+

(d)has been adequately trained and instructed to perform a Transaction and utilize the Service; and

+

(e)does not use the Service for any inappropriate or unlawful purpose.

+
+
+
+ +
+
5. FEES
+
+

5.1The Subscriber will pay to the Province all applicable Fees for the Services.

+

5.2Fees payable for Transactions processed by Premium Account Subscribers will be charged to the applicable Deposit Account and in accordance with the BC Online Terms and Conditions.

+

5.3Fees payable for Transactions processed by Basic Account Subscribers will be payable by credit card before the Transaction is processed.

+
+
+ +
+
6. RELATIONSHIP
+
+

6.1This Agreement will not in any way make the Subscriber or any User an employee, agent or independent contractor of the Province and the Subscriber will not, and will ensure that its Users do not, in any way indicate or hold out to any person that the Subscriber or any User is an employee, agent or independent contractor of the Province.

+
+
+ +
+
7. SUSPENSION OF SERVICE
+
+

7.1The Province may, in its sole discretion, immediately suspend Access upon written notice to the Subscriber if:

+
+

(a)the Subscriber or any of its Users has, in the reasonable opinion of the Province, in any way jeopardized the integrity or security of the Service; or

+

(b)the Subscriber or any of its Users has violated any other provision of this Agreement.

+
+
+
+ +
+
8. TERMINATION
+
+

8.1The Province may immediately terminate this Agreement upon written notice to the Subscriber if the Subscriber’s Access has been suspended pursuant to section 7.1.

+

8.2Upon termination:

+
+

(a)the Subscriber will immediately cease, and will ensure that all of its Users immediately cease, all use of the Service and all Passcodes; and

+

(b)Premium Account Subscribers will pay to the Province all unpaid Fees incurred by the Subscriber up to the date of termination.

+
+

8.3In the event that a Subscriber’s Agreement is terminated, the Province reserves the right to refuse future Access to that Subscriber or to downgrade a Premium Account Subscriber to a Basic Account Subscriber, in which case the Subscriber acknowledges and agrees that it is only entitled to Access up to ten Entities and will release any Entities in excess of that number.

+
+
+ +
+
9. WARRANTY DISCLAIMER, LIMITATION OF LIABILITY AND INDEMNITY
+
+

9.1THE SUBSCRIBER ACKNOWLEDGES AND CONFIRMS THAT THE SUBSCRIBER UNDERSTANDS THAT THIS ARTICLE 10 REQUIRES THE SUBSCRIBER TO ASSUME THE FULL RISK IN RESPECT OF ANY USE OF THE SERVICES BY THE SUBSCRIBER AND/OR ITS USERS

+

9.2Except as expressly set out in this Agreement, and in addition to the Province’s general Warranty Disclaimer and Limitation of Liabilities, the Province assumes no responsibility or liability to any person using the Service or any Content. In particular, without limiting the general nature of the foregoing:

+
+

(a)in no event will the Province, its respective servants, agents, contractors or employees be liable for any direct, indirect, special or consequential damages or other loss, claim or injury, whether foreseeable or unforeseeable (including without limitation claims for damages for personal injury, lost profits, lost savings or business opportunities) arising out of or in any way connected with the use of, or inability to use the Service or any Content;

+

(b)the entire risk as to the quality and performance of the Service or any Content, is assumed by the Subscriber;

+

(c)the Service and all Content are provided “as is”, and the Province disclaims all representations, warranties, conditions, obligations and liabilities of any kind, whether express or implied, in relation to the Service or any Content, including without limitation implied warranties with respect to merchantability, fitness for a particular purpose, error-free or uninterrupted use and non-infringement; and

+

(d)in no event will the Province, its respective servants, agents, contractors or employees be liable for any loss or damage in connection with the Service or any Content, including without limitation any loss or damage caused by any alteration of the format or content of a print copy or electronic display of any information retrieved from the Service, the quality of any print display, the information contained in any screen dump, any system failure, hardware malfunction, manipulation of data, inadequate or faulty Transaction and/or Service, or delay or failure to provide Access to any User or any person using a User's Incorporation Numbers or Passcodes or using any information provided by a Subscriber or any User from the Service.

+
+

9.3The Subscriber must indemnify and save harmless the Province and its respective servants, agents, contractor and employees from any losses, claims, damages, actions, causes of action, costs and expenses that the Province or any of its respective servants, agents, contractors or employees may sustain, incur, suffer or be put to at any time, either before or after this Agreement ends, including any claim of infringement of third-party intellectual property rights, where the same or any of them are based upon, arise out of or occur, directly or indirectly, by reason of any act or omission by the Subscriber or by any of the Subscriber’s agents, employees, officers or directors in connection with this Agreement.

+
+
+ +
+
10. GENERAL
+
+

10.1In this Agreement,

+
+

(a)unless the context otherwise requires, references to sections by number are to sections of the Agreement;

+

(b)unless otherwise specified, a reference to a statute by name means the statute of British Columbia by that name, as amended or replaced from time to time;

+

(c)“person” includes an individual, partnership, corporation or legal entity of any nature; and

+

(d)unless the context otherwise requires, words expressed in the singular includes the plural and vice versa.

+
+

10.2This Agreement is the entire agreement between the Subscriber and the Province with respect to the subject matter of this Agreement, and supercedes and replaces any prior and/or written agreements.

+

10.3The headings in this Agreement are inserted for convenience only, and will not be used in interpreting or construing any provision of this Agreement.

+

10.4All provisions in this Agreement in favour or either party and all rights and remedies of either party, either at law or in equity, will survive the expiration or sooner termination of this Agreement.

+

10.5If any provision of this Agreement is invalid, illegal or unenforceable, that provision will be severed from this Agreement and all other provisions will remain in full force and effect.

+

10.6This Agreement will be governed by and construed in accordance with the laws of British Columbia and the laws of Canada applicable therein. By using the Service, the Subcriber consents to the exclusive jurisdiction and venue of the courts of the province of British Columbia for the hearing of any dispute arising from or related to this Agreement and/or the Subscriber’s use of the Service.

+
+
+ +
+ """ + op.bulk_insert( + documents, + [ + {'version_id': 1, 'type': 'termsofuse', 'content': html_content} + ] + ) + +def downgrade(): + op.drop_constraint('user_documents_fk', 'user', type_='foreignkey') + op.drop_column('user', 'terms_of_use_accepted_version') + op.drop_column('user', 'is_terms_of_use_accepted') + op.drop_table('documents') diff --git a/auth-api/openshift/templates/auth-api-deploy.e2e.json b/auth-api/openshift/templates/auth-api-deploy.e2e.json index b1853aa832..0782e19b24 100644 --- a/auth-api/openshift/templates/auth-api-deploy.e2e.json +++ b/auth-api/openshift/templates/auth-api-deploy.e2e.json @@ -32,7 +32,9 @@ "pre": { "failurePolicy": "Abort", "execNewPod": { - "command": ["/opt/app-root/src/pre-hook-update-db.sh"], + "command": [ + "/opt/app-root/src/pre-hook-update-db.sh" + ], "env": [ { "name": "DATABASE_ADMIN_PASSWORD", @@ -47,7 +49,7 @@ "name": "DATABASE_USERNAME", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_USER" } } @@ -65,7 +67,7 @@ "name": "DATABASE_NAME", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_NAME" } } @@ -74,7 +76,7 @@ "name": "DATABASE_HOST", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_HOST" } } @@ -83,7 +85,7 @@ "name": "DATABASE_PORT", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_PORT" } } @@ -99,7 +101,9 @@ "type": "ImageChange", "imageChangeParams": { "automatic": true, - "containerNames": ["${NAME}-${TAG_NAME}"], + "containerNames": [ + "${NAME}-${TAG_NAME}" + ], "from": { "kind": "ImageStreamTag", "namespace": "${IMAGE_NAMESPACE}", @@ -137,12 +141,19 @@ "protocol": "TCP" } ], + "envFrom": [ + { + "configMapRef": { + "name": "${NAME}-${TAG_NAME}-config" + } + } + ], "env": [ { "name": "DATABASE_USERNAME", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_USER" } } @@ -160,7 +171,7 @@ "name": "DATABASE_NAME", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_NAME" } } @@ -169,7 +180,7 @@ "name": "DATABASE_HOST", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_HOST" } } @@ -178,7 +189,7 @@ "name": "DATABASE_PORT", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_PORT" } } @@ -187,7 +198,7 @@ "name": "DATABASE_TEST_USERNAME", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_TEST_USER" } } @@ -205,7 +216,7 @@ "name": "DATABASE_TEST_NAME", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_TEST_NAME" } } @@ -214,7 +225,7 @@ "name": "DATABASE_TEST_HOST", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_TEST_HOST" } } @@ -223,61 +234,16 @@ "name": "DATABASE_TEST_PORT", "valueFrom": { "configMapKeyRef": { - "name": "${DATABASE_NAME}-auth-${TAG_NAME}-config", + "name": "${NAME}-${TAG_NAME}-config", "key": "DATABASE_TEST_PORT" } } }, - { - "name": "JWT_OIDC_ALGORITHMS", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_ALGORITHMS" - } - } - }, - { - "name": "JWT_OIDC_AUDIENCE", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_AUDIENCE" - } - } - }, - { - "name": "JWT_OIDC_CLIENT_SECRET", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_CLIENT_SECRET" - } - } - }, - { - "name": "JWT_OIDC_WELL_KNOWN_CONFIG", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_WELL_KNOWN_CONFIG" - } - } - }, - { - "name": "JWT_OIDC_JWKS_CACHE_TIMEOUT", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_JWKS_CACHE_TIMEOUT" - } - } - }, { "name": "KEYCLOAK_BASE_URL", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "${NAME}-${TAG_NAME}-config", "key": "KEYCLOAK_BASE_URL" } } @@ -285,8 +251,8 @@ { "name": "KEYCLOAK_REALMNAME", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "${NAME}-${TAG_NAME}-config", "key": "KEYCLOAK_REALMNAME" } } @@ -294,8 +260,8 @@ { "name": "KEYCLOAK_ADMIN_CLIENTID", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "${NAME}-${TAG_NAME}-config", "key": "KEYCLOAK_ADMIN_CLIENTID" } } @@ -303,8 +269,8 @@ { "name": "KEYCLOAK_ADMIN_SECRET", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "${NAME}-${TAG_NAME}-config", "key": "KEYCLOAK_ADMIN_SECRET" } } @@ -312,27 +278,18 @@ { "name": "KEYCLOAK_AUTH_AUDIENCE", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "${NAME}-${TAG_NAME}-config", "key": "KEYCLOAK_AUTH_AUDIENCE" } } }, { "name": "KEYCLOAK_AUTH_CLIENT_SECRET", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_AUTH_CLIENT_SECRET" - } - } - }, - { - "name": "POD_TESTING", "valueFrom": { "configMapKeyRef": { "name": "${NAME}-${TAG_NAME}-config", - "key": "POD_TESTING" + "key": "KEYCLOAK_AUTH_CLIENT_SECRET" } } } @@ -394,7 +351,9 @@ "protocol": "UDP" } ], - "args": ["${JAEGER_COLLECTOR}"] + "args": [ + "${JAEGER_COLLECTOR}" + ] } ], "restartPolicy": "Always", @@ -563,7 +522,7 @@ "displayName": "Jaeger Tracing collector address", "description": "Jaeger Tracing collector address.", "required": true, - "value": "--collector.host-port=jaeger-collector.d7eovc-${TAG_NAME}.svc:14267" + "value": "--collector.host-port=jaeger-collector.d7eovc-dev.svc:14267" } ] -} +} \ No newline at end of file diff --git a/auth-api/openshift/templates/auth-api-deploy.json b/auth-api/openshift/templates/auth-api-deploy.json index 4be797aeff..4f72b4fc40 100644 --- a/auth-api/openshift/templates/auth-api-deploy.json +++ b/auth-api/openshift/templates/auth-api-deploy.json @@ -32,7 +32,9 @@ "pre": { "failurePolicy": "Abort", "execNewPod": { - "command": ["/opt/app-root/src/pre-hook-update-db.sh"], + "command": [ + "/opt/app-root/src/pre-hook-update-db.sh" + ], "env": [ { "name": "DATABASE_ADMIN_PASSWORD", @@ -99,7 +101,9 @@ "type": "ImageChange", "imageChangeParams": { "automatic": true, - "containerNames": ["${NAME}-${TAG_NAME}"], + "containerNames": [ + "${NAME}-${TAG_NAME}" + ], "from": { "kind": "ImageStreamTag", "namespace": "${IMAGE_NAMESPACE}", @@ -137,6 +141,13 @@ "protocol": "TCP" } ], + "envFrom": [ + { + "configMapRef": { + "name": "api-${TAG_NAME}-config" + } + } + ], "env": [ { "name": "DATABASE_USERNAME", @@ -228,56 +239,11 @@ } } }, - { - "name": "JWT_OIDC_ALGORITHMS", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_ALGORITHMS" - } - } - }, - { - "name": "JWT_OIDC_AUDIENCE", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_AUDIENCE" - } - } - }, - { - "name": "JWT_OIDC_CLIENT_SECRET", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_CLIENT_SECRET" - } - } - }, - { - "name": "JWT_OIDC_WELL_KNOWN_CONFIG", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_WELL_KNOWN_CONFIG" - } - } - }, - { - "name": "JWT_OIDC_JWKS_CACHE_TIMEOUT", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_JWKS_CACHE_TIMEOUT" - } - } - }, { "name": "KEYCLOAK_BASE_URL", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "api-${TAG_NAME}-config", "key": "KEYCLOAK_BASE_URL" } } @@ -285,8 +251,8 @@ { "name": "KEYCLOAK_REALMNAME", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "api-${TAG_NAME}-config", "key": "KEYCLOAK_REALMNAME" } } @@ -294,8 +260,8 @@ { "name": "KEYCLOAK_ADMIN_CLIENTID", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "api-${TAG_NAME}-config", "key": "KEYCLOAK_ADMIN_CLIENTID" } } @@ -303,8 +269,8 @@ { "name": "KEYCLOAK_ADMIN_SECRET", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "api-${TAG_NAME}-config", "key": "KEYCLOAK_ADMIN_SECRET" } } @@ -312,137 +278,20 @@ { "name": "KEYCLOAK_AUTH_AUDIENCE", "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", + "configMapKeyRef": { + "name": "api-${TAG_NAME}-config", "key": "KEYCLOAK_AUTH_AUDIENCE" } } }, { "name": "KEYCLOAK_AUTH_CLIENT_SECRET", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_AUTH_CLIENT_SECRET" - } - } - }, - { - "name": "POD_TESTING", "valueFrom": { "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "POD_TESTING" - } - } - }, - { - "name": "MAIL_SERVER", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "MAIL_SERVER" - } - } - }, - { - "name": "MAIL_PORT", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "MAIL_PORT" - } - } - }, - { - "name": "MAIL_USE_TLS", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "MAIL_USE_TLS" - } - } - }, - { - "name": "MAIL_USE_SSL", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "MAIL_USE_SSL" - } - } - }, - { - "name": "MAIL_USERNAME", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "MAIL_USERNAME" - } - } - }, - { - "name": "MAIL_PASSWORD", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "MAIL_PASSWORD" - } - } - }, - { - "name": "MAIL_FROM_ID", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "MAIL_FROM_ID" - } - } - }, - { - "name": "AUTH_WEB_TOKEN_CONFIRM_URL", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "AUTH_WEB_TOKEN_CONFIRM_URL" - } - } - }, - { - "name": "EMAIL_SECURITY_PASSWORD_SALT", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "EMAIL_SECURITY_PASSWORD_SALT" - } - } - }, - { - "name": "EMAIL_TOKEN_SECRET_KEY", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "EMAIL_TOKEN_SECRET_KEY" - } - } - }, - { - "name": "TOKEN_EXPIRY_PERIOD", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "TOKEN_EXPIRY_PERIOD" + "name": "api-${TAG_NAME}-config", + "key": "KEYCLOAK_AUTH_CLIENT_SECRET" } } - }, - { - "name": "SENTRY_DSN", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "SENTRY_DSN" - } - } } ], "resources": { @@ -502,7 +351,9 @@ "protocol": "UDP" } ], - "args": ["${JAEGER_COLLECTOR}"] + "args": [ + "${JAEGER_COLLECTOR}" + ] } ], "restartPolicy": "Always", @@ -674,4 +525,4 @@ "value": "--collector.host-port=jaeger-collector.d7eovc-${TAG_NAME}.svc:14267" } ] -} +} \ No newline at end of file diff --git a/auth-api/requirements.txt b/auth-api/requirements.txt index c78101c571..294f824f38 100644 --- a/auth-api/requirements.txt +++ b/auth-api/requirements.txt @@ -5,41 +5,47 @@ Flask-Moment==0.9.0 Flask-SQLAlchemy==2.4.1 Flask-Script==2.0.6 Flask==1.1.1 -Jinja2==2.10.1 +Jinja2==2.10.3 Mako==1.1.0 MarkupSafe==1.1.1 -SQLAlchemy==1.3.8 +SQLAlchemy==1.3.10 Werkzeug==0.16.0 alembic==1.2.1 aniso8601==8.0.0 -attrs==19.1.0 +attrs==19.3.0 +bcrypt==3.1.7 blinker==1.4 certifi==2019.9.11 +cffi==1.13.1 chardet==3.0.4 -ecdsa==0.13.2 +ecdsa==0.13.3 flask-jwt-oidc==0.1.5 flask-marshmallow==0.10.1 flask-restplus==0.13.0 -future==0.17.1 +future==0.18.1 gunicorn==19.9.0 idna==2.8 +importlib-metadata==0.23 itsdangerous==1.1.0 -jsonschema==3.0.2 +jsonschema==3.1.1 marshmallow-sqlalchemy==0.19.0 marshmallow==3.0.0rc7 -psycopg2-binary==2.8.3 +more-itertools==7.2.0 +psycopg2-binary==2.8.4 pyasn1==0.4.7 -pyrsistent==0.15.4 +pycparser==2.19 +pyrsistent==0.15.5 python-dateutil==2.8.0 python-dotenv==0.10.3 python-editor==1.0.4 python-jose==3.0.1 -python-keycloak==0.17.5 -pytz==2019.2 +python-keycloak==0.17.6 +pytz==2019.3 requests==2.22.0 rsa==4.0 -sentry-sdk==0.12.2 +sentry-sdk==0.13.1 six==1.12.0 urllib3==1.25.6 +zipp==0.6.0 -e git://github.com/pwei1018/jaeger-client-python.git@186f14e14758273ed108508c0d388a4f4de5c75b#egg=jaeger-client -e git+https://github.com/bcgov/sbc-common-components.git@48f454595b5b2a7d3f2f86ec9b8e944bef2bcfb2#egg=sbc-common-components-1.0.0&subdirectory=python diff --git a/auth-api/requirements/dev.txt b/auth-api/requirements/dev.txt index 4e02665abb..18b1c0757a 100644 --- a/auth-api/requirements/dev.txt +++ b/auth-api/requirements/dev.txt @@ -3,6 +3,7 @@ # Testing pytest<4.1 +attrs==19.1.0 pytest-mock requests pyhamcrest diff --git a/auth-api/requirements/prod.txt b/auth-api/requirements/prod.txt index 3e90714e11..b3e28e3e58 100644 --- a/auth-api/requirements/prod.txt +++ b/auth-api/requirements/prod.txt @@ -16,4 +16,5 @@ jsonschema requests python-keycloak itsdangerous -sentry-sdk[flask] \ No newline at end of file +sentry-sdk[flask] +bcrypt \ No newline at end of file diff --git a/auth-api/src/auth_api/__init__.py b/auth-api/src/auth_api/__init__.py index d96fb7e0e3..02a0f71b23 100644 --- a/auth-api/src/auth_api/__init__.py +++ b/auth-api/src/auth_api/__init__.py @@ -11,14 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""The Legal API service. +"""The Authroization API service. -This module is the API for the Legal Entity system. +This module is the API for the Authroization system. """ + import os from flask import Flask -from sbc_common_components.exception_handling.exception_handler import ExceptionHandler +from sbc_common_components.exception_handling.exception_handler import ExceptionHandler # noqa: I001 from sentry_sdk.integrations.flask import FlaskIntegration # noqa: I001 from auth_api import models @@ -30,7 +31,7 @@ from config import CONFIGURATION, _Config -import sentry_sdk # noqa: I001; pylint: disable=ungrouped-imports; conflicts with Flake8 +import sentry_sdk # noqa: I001; pylint: disable=ungrouped-imports,wrong-import-order; conflicts with Flake8 setup_logging(os.path.join(_Config.PROJECT_ROOT, 'logging.conf')) # important to do this first @@ -50,7 +51,7 @@ def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): integrations=[FlaskIntegration()] ) - from auth_api.resources import API_BLUEPRINT, OPS_BLUEPRINT + from auth_api.resources import API_BLUEPRINT, OPS_BLUEPRINT # pylint: disable=import-outside-toplevel db.init_app(app) ma.init_app(app) diff --git a/auth-api/src/auth_api/exceptions/__init__.py b/auth-api/src/auth_api/exceptions/__init__.py index 68a0186a27..49c332a012 100644 --- a/auth-api/src/auth_api/exceptions/__init__.py +++ b/auth-api/src/auth_api/exceptions/__init__.py @@ -8,9 +8,9 @@ """ import traceback -from functools import wraps -from sbc_common_components.tracing.exception_tracing import ExceptionTracing +from functools import wraps # noqa: I001 +from sbc_common_components.tracing.exception_tracing import ExceptionTracing # noqa: I001 from auth_api.exceptions.errors import Error @@ -32,38 +32,11 @@ def __init__(self, error, exception, *args, **kwargs): ExceptionTracing.trace(self, traceback.format_exc()) -class UserException(Exception): - """Exception that adds error code and error name, that can be used for i18n support.""" +class ServiceUnavailableException(Exception): + """Exception to be raised if third party service is unavailable.""" - def __init__(self, error, status_code, trace_back, *args, **kwargs): - """Return a valid UserException.""" - super(UserException, self).__init__(*args, **kwargs) + def __init__(self, error, *args, **kwargs): + """Return a valid BusinessException.""" + super(ServiceUnavailableException, self).__init__(*args, **kwargs) self.error = error - self.status_code = status_code - self.trace_back = trace_back - - -def catch_custom_exception(func): - """TODO just a demo function.""" - @wraps(func) - def decorated_function(*args, **kwargs): - try: - return func(*args, **kwargs) - except BusinessException as e: - trace_back = traceback.format_exc() - ExceptionTracing.trace(e, trace_back) - raise UserException(e.with_traceback(None), e.status_code, trace_back) - - return decorated_function - - -def catch_business_exception(func): - """Catch and raise exception.""" - @wraps(func) - def decorated_function(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception as e: - raise BusinessException(Error.UNDEFINED_ERROR, e) - - return decorated_function + self.status_code = Error.SERVICE_UNAVAILABLE.name diff --git a/auth-api/src/auth_api/jwt_wrapper.py b/auth-api/src/auth_api/jwt_wrapper.py index 9c06e49c2e..6f98466955 100644 --- a/auth-api/src/auth_api/jwt_wrapper.py +++ b/auth-api/src/auth_api/jwt_wrapper.py @@ -20,7 +20,7 @@ class JWTWrapper: # pylint: disable=too-few-public-methods """Singleton wrapper for Flask JwtManager.""" - from flask_jwt_oidc import JwtManager + from flask_jwt_oidc import JwtManager # pylint: disable=import-outside-toplevel __instance = None @staticmethod diff --git a/auth-api/src/auth_api/models/__init__.py b/auth-api/src/auth_api/models/__init__.py index 5b92231f6e..dc8a9ee83f 100644 --- a/auth-api/src/auth_api/models/__init__.py +++ b/auth-api/src/auth_api/models/__init__.py @@ -14,7 +14,7 @@ """This exports all of the models and schemas used by the application.""" -from sbc_common_components.tracing.db_tracing import DBTracing +from sbc_common_components.tracing.db_tracing import DBTracing # noqa: I001 from sqlalchemy import event from sqlalchemy.engine import Engine @@ -22,6 +22,7 @@ from .contact import Contact from .contact_link import ContactLink from .db import db, ma +from .documents import Documents from .entity import Entity from .invitation import Invitation from .invitation_membership import InvitationMembership diff --git a/auth-api/src/auth_api/models/base_model.py b/auth-api/src/auth_api/models/base_model.py index 53998c18f9..512e0b0f96 100644 --- a/auth-api/src/auth_api/models/base_model.py +++ b/auth-api/src/auth_api/models/base_model.py @@ -55,7 +55,7 @@ def _get_current_user(): Used to populate the created_by and modified_by relationships on all models. """ try: - from .user import User as UserModel # pylint:disable=cyclic-import + from .user import User as UserModel # pylint:disable=cyclic-import, import-outside-toplevel token = g.jwt_oidc_token_info user = UserModel.find_by_jwt_token(token) if not user: diff --git a/auth-api/src/auth_api/models/documents.py b/auth-api/src/auth_api/models/documents.py new file mode 100644 index 0000000000..7b58168915 --- /dev/null +++ b/auth-api/src/auth_api/models/documents.py @@ -0,0 +1,39 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Table for storing all static documents. + +Documents which are static in nature are stored in this table ie.terms of use +""" + +from sqlalchemy import Column, Integer, String, Text, desc + +from .base_model import BaseModel +from .db import db + + +class Documents(BaseModel): + """This is the model for a documents.""" + + __tablename__ = 'documents' + + # TODO version concept is not well refined..this is the first version..refine it + version_id = Column(Integer, primary_key=True, autoincrement=False) + type = Column('type', String(20), nullable=False) + content = Column('content', Text) + + @classmethod + def fetch_latest_document_by_type(cls, file_type): + """Fetch latest document of any time.""" + return db.session.query(Documents).filter( + Documents.type == file_type).order_by(desc(Documents.version_id)).limit(1).one_or_none() diff --git a/auth-api/src/auth_api/models/entity.py b/auth-api/src/auth_api/models/entity.py index 8b4b157449..055b9944cc 100644 --- a/auth-api/src/auth_api/models/entity.py +++ b/auth-api/src/auth_api/models/entity.py @@ -20,6 +20,7 @@ from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy.orm import relationship +from auth_api.utils.passcode import passcode_hash from auth_api.utils.util import camelback2snake from .base_model import BaseModel @@ -49,6 +50,7 @@ def create_from_dict(cls, entity_info: dict): """Create a new Entity from the provided dictionary.""" if entity_info: entity = Entity(**camelback2snake(entity_info)) + entity.pass_code = passcode_hash(entity.pass_code) current_app.logger.debug( 'Creating entity from dictionary {}'.format(entity_info) ) diff --git a/auth-api/src/auth_api/models/invitation_membership.py b/auth-api/src/auth_api/models/invitation_membership.py index 0ed4e08ae3..378a4cffac 100644 --- a/auth-api/src/auth_api/models/invitation_membership.py +++ b/auth-api/src/auth_api/models/invitation_membership.py @@ -34,3 +34,4 @@ class InvitationMembership(BaseModel): # pylint: disable=too-few-public-methods membership_type = relationship('MembershipType', foreign_keys=[membership_type_code]) org = relationship('Org', foreign_keys=[org_id]) + invitation = relationship('Invitation', foreign_keys=[invitation_id]) diff --git a/auth-api/src/auth_api/models/invite_status.py b/auth-api/src/auth_api/models/invite_status.py index c70860b3b7..b76f09adb3 100644 --- a/auth-api/src/auth_api/models/invite_status.py +++ b/auth-api/src/auth_api/models/invite_status.py @@ -34,3 +34,8 @@ class InvitationStatus(BaseModel): # pylint: disable=too-few-public-methods # T def get_default_status(cls): """Return the default status code for an Invitation.""" return cls.query.filter_by(default=True).first() + + @classmethod + def get_status_by_code(cls, code: str): + """Return the status object corresponding to the given code.""" + return cls.query.filter_by(code=code).first() diff --git a/auth-api/src/auth_api/models/membership.py b/auth-api/src/auth_api/models/membership.py index d75116e22e..490544a019 100644 --- a/auth-api/src/auth_api/models/membership.py +++ b/auth-api/src/auth_api/models/membership.py @@ -47,3 +47,8 @@ def __init__(self, **kwargs): self.membership_type_code = kwargs.get('membership_type_code') if self.membership_type_code is None: self.membership_type = MembershipType.get_default_type() + + @classmethod + def find_membership_by_id(cls, membership_id): + """Find the first membership with the given id and return it.""" + return cls.query.filter_by(id=membership_id).first() diff --git a/auth-api/src/auth_api/models/membership_type.py b/auth-api/src/auth_api/models/membership_type.py index 62dc426fc7..02bb2678d8 100644 --- a/auth-api/src/auth_api/models/membership_type.py +++ b/auth-api/src/auth_api/models/membership_type.py @@ -34,3 +34,8 @@ class MembershipType(BaseModel): # pylint: disable=too-few-public-methods # Tem def get_default_type(cls): """Return the default type code for Membership.""" return cls.query.filter_by(default=True).first() + + @classmethod + def get_membership_type_by_code(cls, type_code): + """Return the membership type object that corresponds to given code.""" + return cls.query.filter_by(code=type_code).first() diff --git a/auth-api/src/auth_api/models/user.py b/auth-api/src/auth_api/models/user.py index b4d31ba11c..8f62bcfac5 100644 --- a/auth-api/src/auth_api/models/user.py +++ b/auth-api/src/auth_api/models/user.py @@ -19,7 +19,7 @@ import datetime from flask import current_app -from sqlalchemy import Column, Integer, String, or_ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, or_ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship @@ -44,6 +44,13 @@ class User(BaseModel): contacts = relationship('ContactLink', back_populates='user', primaryjoin='User.id == ContactLink.user_id') orgs = relationship('Membership', back_populates='user', primaryjoin='User.id == Membership.user_id') + is_terms_of_use_accepted = Column(Boolean(), default=False, nullable=True) + terms_of_use_accepted_version = Column( + ForeignKey('documents.version_id'), nullable=True + ) + terms_of_use_version = relationship('Documents', foreign_keys=[terms_of_use_accepted_version], uselist=False, + lazy='select') + @classmethod def find_by_username(cls, username): """Return the first user with the provided username.""" @@ -103,6 +110,23 @@ def find_users(cls, first_name, last_name, email): return cls.query.all() return cls.query.filter(or_(cls.firstname == first_name, cls.lastname == last_name, cls.email == email)).all() + @classmethod + def update_terms_of_use(cls, token: dict, is_terms_accepted, terms_of_use_version): + """Update the terms of service for the user.""" + if token: + user = cls.find_by_jwt_token(token) + user.is_terms_of_use_accepted = is_terms_accepted + user.terms_of_use_accepted_version = terms_of_use_version + + current_app.logger.debug( + 'Updating users Terms of use is_terms_accepted:{}; terms_of_use_version:{}'.format( + is_terms_accepted, terms_of_use_version) + ) + + cls.commit() + return user + return None + def delete(self): """Users cannot be deleted so intercept the ORM by just returning.""" return self diff --git a/auth-api/src/auth_api/resources/__init__.py b/auth-api/src/auth_api/resources/__init__.py index 027b1dd0d4..b9aa06fdaf 100644 --- a/auth-api/src/auth_api/resources/__init__.py +++ b/auth-api/src/auth_api/resources/__init__.py @@ -25,6 +25,7 @@ from sbc_common_components.exception_handling.exception_handler import ExceptionHandler from .apihelper import Api +from .documents import API as DOCUMENTS_API from .entity import API as ENTITY_API from .invitation import API as INVITATION_API from .logout import API as LOGOUT_API @@ -74,3 +75,4 @@ API.add_namespace(ENTITY_API, path='/entities') API.add_namespace(ORG_API, path='/orgs') API.add_namespace(INVITATION_API, path='/invitations') +API.add_namespace(DOCUMENTS_API, path='/documents') diff --git a/auth-api/src/auth_api/resources/documents.py b/auth-api/src/auth_api/resources/documents.py new file mode 100644 index 0000000000..94b8170e11 --- /dev/null +++ b/auth-api/src/auth_api/resources/documents.py @@ -0,0 +1,50 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""API endpoints for managing an Invitation resource.""" + +from flask_restplus import Namespace, Resource, cors + +from auth_api import status as http_status +from auth_api.exceptions import BusinessException +from auth_api.jwt_wrapper import JWTWrapper +from auth_api.services import Documents as DocumentService +from auth_api.tracer import Tracer +from auth_api.utils.util import cors_preflight + + +API = Namespace('invitations', description='Endpoints for invitations management') +TRACER = Tracer.get_instance() +_JWT = JWTWrapper.get_instance() + + +@cors_preflight('GET,OPTIONS') +@API.route('/', methods=['GET', 'OPTIONS']) +class Documents(Resource): + """Resource for managing Terms Of Use.""" + + @staticmethod + @TRACER.trace() + @cors.crossdomain(origin='*') + def get(document_type): + """Return the latest terms of use.""" + try: + doc = DocumentService.fetch_latest_document(document_type) + if doc is not None: + response, status = doc.as_dict(), http_status.HTTP_200_OK + else: + response, status = {'message': 'The requested invitation could not be found.'}, \ + http_status.HTTP_404_NOT_FOUND + except BusinessException as exception: + response, status = {'code': exception.code, 'message': exception.message}, exception.status_code + return response, status diff --git a/auth-api/src/auth_api/resources/invitation.py b/auth-api/src/auth_api/resources/invitation.py index daaa3ea237..330d3eda6f 100644 --- a/auth-api/src/auth_api/resources/invitation.py +++ b/auth-api/src/auth_api/resources/invitation.py @@ -13,7 +13,7 @@ # limitations under the License. """API endpoints for managing an Invitation resource.""" -from flask import g, jsonify, request +from flask import g, request from flask_restplus import Namespace, Resource, cors from auth_api import status as http_status @@ -43,6 +43,7 @@ class Invitations(Resource): def post(): """Send a new invitation using the details in request and saves the invitation.""" token = g.jwt_oidc_token_info + origin = request.environ.get('HTTP_ORIGIN', 'localhost') request_json = request.get_json() valid_format, errors = schema_utils.validate(request_json, 'invitation') if not valid_format: @@ -53,35 +54,12 @@ def post(): response, status = {'message': 'Not authorized to perform this action'}, \ http_status.HTTP_401_UNAUTHORIZED else: - response, status = InvitationService.create_invitation(request_json, user).as_dict(), \ + response, status = InvitationService.create_invitation(request_json, user, token, origin).as_dict(), \ http_status.HTTP_201_CREATED except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status - @staticmethod - @TRACER.trace() - @cors.crossdomain(origin='*') - @_JWT.requires_auth - def get(): - """Return a set of invitations sent by the user specified by the token.""" - token = g.jwt_oidc_token_info - try: - user = UserService.find_by_jwt_token(token) - if user is None: - response, status = {'message': 'Not authorized to perform this action'}, \ - http_status.HTTP_401_UNAUTHORIZED - else: - invitation_status = request.args.get('status').upper() if request.args.get('status') else 'ALL' - invitations = InvitationService.get_invitations(user.identifier, invitation_status) - if not invitations: - response, status = {'message': 'No invitations found.'}, http_status.HTTP_404_NOT_FOUND - else: - response, status = jsonify(invitations=invitations), http_status.HTTP_200_OK - except BusinessException as exception: - response, status = {'code': exception.code, 'message': exception.message}, exception.status_code - return response, status - @cors_preflight('GET,PATCH,DELETE,OPTIONS') @API.route('/', methods=['GET', 'PATCH', 'DELETE', 'OPTIONS']) @@ -94,7 +72,8 @@ class Invitation(Resource): @_JWT.requires_auth def get(invitation_id): """Get the invitation specified by the provided id.""" - invitation = InvitationService.find_invitation_by_id(invitation_id) + token = g.jwt_oidc_token_info + invitation = InvitationService.find_invitation_by_id(invitation_id, token) if invitation is None: response, status = {'message': 'The requested invitation could not be found.'}, \ http_status.HTTP_404_NOT_FOUND @@ -109,18 +88,20 @@ def get(invitation_id): def patch(invitation_id): """Update the invitation specified by the provided id as retried.""" token = g.jwt_oidc_token_info + origin = request.environ.get('HTTP_ORIGIN', 'localhost') try: user = UserService.find_by_jwt_token(token) if user is None: response, status = {'message': 'Not authorized to perform this action'}, \ http_status.HTTP_401_UNAUTHORIZED else: - invitation = InvitationService.find_invitation_by_id(invitation_id) + invitation = InvitationService.find_invitation_by_id(invitation_id, token) if invitation is None: response, status = {'message': 'The requested invitation could not be found.'}, \ http_status.HTTP_404_NOT_FOUND else: - response, status = invitation.update_invitation(user).as_dict(), http_status.HTTP_200_OK + response, status = invitation.update_invitation(user, token, origin).as_dict(), \ + http_status.HTTP_200_OK except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status @@ -131,8 +112,9 @@ def patch(invitation_id): @_JWT.requires_auth def delete(invitation_id): """Delete the specified invitation.""" + token = g.jwt_oidc_token_info try: - InvitationService.delete_invitation(invitation_id) + InvitationService.delete_invitation(invitation_id, token) response, status = {}, http_status.HTTP_200_OK except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code diff --git a/auth-api/src/auth_api/resources/logout.py b/auth-api/src/auth_api/resources/logout.py index 9c232c4ad7..4670cf02fd 100644 --- a/auth-api/src/auth_api/resources/logout.py +++ b/auth-api/src/auth_api/resources/logout.py @@ -19,7 +19,7 @@ from flask_restplus import Namespace, Resource, cors from auth_api import status as http_status -from auth_api.exceptions import BusinessException, catch_custom_exception +from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error from auth_api.services.keycloak import KeycloakService from auth_api.tracer import Tracer @@ -39,7 +39,6 @@ class Logout(Resource): @staticmethod @TRACER.trace() @cors.crossdomain(origin='*') - @catch_custom_exception def post(): """Return a JSON object that includes user detail information.""" data = request.get_json() diff --git a/auth-api/src/auth_api/resources/org.py b/auth-api/src/auth_api/resources/org.py index 715faa3ba4..a560f0a762 100644 --- a/auth-api/src/auth_api/resources/org.py +++ b/auth-api/src/auth_api/resources/org.py @@ -21,6 +21,7 @@ from auth_api.jwt_wrapper import JWTWrapper from auth_api.schemas import utils as schema_utils from auth_api.services import Affiliation as AffiliationService +from auth_api.services import Membership as MembershipService from auth_api.services import Org as OrgService from auth_api.services import User as UserService from auth_api.tracer import Tracer @@ -57,10 +58,10 @@ def post(): user = UserService.find_by_jwt_token(token) if user is None: response, status = {'message': 'Not authorized to perform this action'}, \ - http_status.HTTP_401_UNAUTHORIZED + http_status.HTTP_401_UNAUTHORIZED else: response, status = OrgService.create_org(request_json, user.identifier).as_dict(), \ - http_status.HTTP_201_CREATED + http_status.HTTP_201_CREATED except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status @@ -80,7 +81,7 @@ def get(org_id): org = OrgService.find_by_org_id(org_id, g.jwt_oidc_token_info, allowed_roles=ALL_ALLOWED_ROLES) if org is None: response, status = {'message': 'The requested organization could not be found.'}, \ - http_status.HTTP_404_NOT_FOUND + http_status.HTTP_404_NOT_FOUND else: response, status = org.as_dict(), http_status.HTTP_200_OK return response, status @@ -91,14 +92,18 @@ def get(org_id): @_JWT.requires_auth def put(org_id): """Update the org specified by the provided id with the request body.""" - org = OrgService.find_by_org_id(org_id, g.jwt_oidc_token_info, allowed_roles=CLIENT_ADMIN_ROLES) request_json = request.get_json() valid_format, errors = schema_utils.validate(request_json, 'org') if not valid_format: return {'message': schema_utils.serialize(errors)}, http_status.HTTP_400_BAD_REQUEST try: - response, status = org.update_org(request_json).as_dict(), http_status.HTTP_200_OK + org = OrgService.find_by_org_id(org_id, g.jwt_oidc_token_info, allowed_roles=CLIENT_ADMIN_ROLES) + if org: + response, status = org.update_org(request_json).as_dict(), http_status.HTTP_200_OK + else: + response, status = {'message': 'The requested organization could not be found.'}, \ + http_status.HTTP_404_NOT_FOUND except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status @@ -126,7 +131,7 @@ def post(org_id): response, status = org.add_contact(request_json).as_dict(), http_status.HTTP_201_CREATED else: response, status = {'message': 'The requested organization could not be found.'}, \ - http_status.HTTP_404_NOT_FOUND + http_status.HTTP_404_NOT_FOUND except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status @@ -147,7 +152,7 @@ def put(org_id): response, status = org.update_contact(request_json).as_dict(), http_status.HTTP_200_OK else: response, status = {'message': 'The requested organization could not be found.'}, \ - http_status.HTTP_404_NOT_FOUND + http_status.HTTP_404_NOT_FOUND except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status @@ -164,7 +169,7 @@ def delete(org_id): response, status = org.delete_contact().as_dict(), http_status.HTTP_200_OK else: response, status = {'message': 'The requested organization could not be found.'}, \ - http_status.HTTP_404_NOT_FOUND + http_status.HTTP_404_NOT_FOUND except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status @@ -204,7 +209,7 @@ def get(org_id): try: response, status = jsonify( AffiliationService.find_affiliated_entities_by_org_id(org_id, g.jwt_oidc_token_info)), \ - http_status.HTTP_200_OK + http_status.HTTP_200_OK except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, exception.status_code @@ -228,7 +233,7 @@ def delete(org_id, business_identifier): except BusinessException as exception: response, status = {'code': exception.code, 'message': exception.message}, \ - exception.status_code + exception.status_code return response, status @@ -258,8 +263,8 @@ def get(org_id): return response, status - @cors_preflight('DELETE,OPTIONS') - @API.route('//members/', methods=['DELETE', 'OPTIONS']) + @cors_preflight('DELETE,PATCH,OPTIONS') + @API.route('//members/', methods=['DELETE', 'PATCH', 'OPTIONS']) class OrgMember(Resource): """Resource for managing a single membership record between an org and a user.""" @@ -267,13 +272,40 @@ class OrgMember(Resource): @_JWT.requires_auth @TRACER.trace() @cors.crossdomain(origin='*') - def delete(org_id, member_id): + def patch(org_id, membership_id): # pylint:disable=unused-argument + """Update a membership record with new member role.""" + token = g.jwt_oidc_token_info + request_json = request.get_json() + role = request_json['role'] + try: + if not role: + response, status = {'message': 'Invalid role provided.'}, http_status.HTTP_400_BAD_REQUEST + return response, status + updated_role = MembershipService.get_membership_type_by_code(role) + membership = MembershipService.find_membership_by_id(membership_id, token) + if not membership: + response, status = {'message': 'The requested membership record could not be found.'}, \ + http_status.HTTP_404_NOT_FOUND + return response, status + + return membership.update_membership_role(updated_role=updated_role, token_info=token).as_dict(), \ + http_status.HTTP_200_OK + + except BusinessException as exception: + response, status = {'code': exception.code, 'message': exception.message}, exception.status_code + return response, status + + @staticmethod + @_JWT.requires_auth + @TRACER.trace() + @cors.crossdomain(origin='*') + def delete(org_id, membership_id): """Delete a membership record for the given org and user.""" try: org = OrgService.find_by_org_id(org_id, g.jwt_oidc_token_info, allowed_roles=(*CLIENT_ADMIN_ROLES, STAFF)) if org: - response, status = org.remove_member(member_id).as_dict(), \ + response, status = org.remove_member(membership_id).as_dict(), \ http_status.HTTP_200_OK else: response, status = {'message': 'The requested organization could not be found.'}, \ @@ -295,11 +327,12 @@ class OrgInvitations(Resource): def get(org_id): """Retrieve the set of invitations for the given org.""" try: + invitation_status = request.args.get('status').upper() if request.args.get('status') else 'ALL' org = OrgService.find_by_org_id(org_id, g.jwt_oidc_token_info, allowed_roles=(*CLIENT_ADMIN_ROLES, STAFF)) if org: - response, status = jsonify(org.get_invitations(invitation_status)), \ + response, status = jsonify(org.get_invitations(invitation_status, g.jwt_oidc_token_info)), \ http_status.HTTP_200_OK else: response, status = {'message': 'The requested organization could not be found.'}, \ diff --git a/auth-api/src/auth_api/resources/token.py b/auth-api/src/auth_api/resources/token.py index bcb8bbc97d..5665b6c8d1 100644 --- a/auth-api/src/auth_api/resources/token.py +++ b/auth-api/src/auth_api/resources/token.py @@ -17,7 +17,7 @@ from flask import request from flask_restplus import Namespace, Resource, cors -from sbc_common_components.tracing.trace_tags import TraceTags +from sbc_common_components.tracing.trace_tags import TraceTags # noqa: I001 from auth_api import status as http_status from auth_api.exceptions import BusinessException diff --git a/auth-api/src/auth_api/resources/user.py b/auth-api/src/auth_api/resources/user.py index 6bc559a5f3..2b9a9adedc 100644 --- a/auth-api/src/auth_api/resources/user.py +++ b/auth-api/src/auth_api/resources/user.py @@ -97,8 +97,8 @@ def get(username): return response, status -@cors_preflight('GET,OPTIONS') -@API.route('/@me', methods=['GET', 'OPTIONS']) +@cors_preflight('GET,OPTIONS,PATCH') +@API.route('/@me', methods=['GET', 'OPTIONS','PATCH']) class User(Resource): """Resource for managing an individual user.""" @@ -118,6 +118,30 @@ def get(): response, status = {'code': exception.code, 'message': exception.message}, exception.status_code return response, status + @staticmethod + @TRACER.trace() + @cors.crossdomain(origin='*') + @_JWT.requires_auth + def patch(): + """Update terms of service for the user.""" + token = g.jwt_oidc_token_info + request_json = request.get_json() + + valid_format, errors = schema_utils.validate(request_json, 'termsofuse') + if not token: + return {'message': 'Authorization required.'}, http_status.HTTP_401_UNAUTHORIZED + if not valid_format: + return {'message': schema_utils.serialize(errors)}, http_status.HTTP_400_BAD_REQUEST + + version = request_json['termsversion'] + is_terms_accepted = request_json['istermsaccepted'] + try: + response, status = UserService.update_terms_of_use(token, is_terms_accepted, version).as_dict(), \ + http_status.HTTP_200_OK + except BusinessException as exception: + response, status = {'code': exception.code, 'message': exception.message}, exception.status_code + return response, status + @cors_preflight('DELETE, POST, PUT, OPTIONS') @API.route('/contacts', methods=['DELETE', 'POST', 'PUT', 'OPTIONS']) @@ -218,3 +242,6 @@ def get(): """Add a new contact for the Entity identified by the provided id.""" sub = g.jwt_oidc_token_info.get('sub', None) return AuthorizationService.get_user_authorizations(sub), http_status.HTTP_200_OK + + + diff --git a/auth-api/src/auth_api/schemas/__init__.py b/auth-api/src/auth_api/schemas/__init__.py index 65de00374d..f6c2e4fa62 100644 --- a/auth-api/src/auth_api/schemas/__init__.py +++ b/auth-api/src/auth_api/schemas/__init__.py @@ -17,6 +17,7 @@ from .affiliation import AffiliationSchema from .contact import ContactSchema from .contact_link import ContactLinkSchema +from .documents import DocumentSchema from .entity import EntitySchema from .invitation import InvitationSchema from .invitation_membership import InvitationMembershipSchema diff --git a/auth-api/src/auth_api/schemas/documents.py b/auth-api/src/auth_api/schemas/documents.py new file mode 100644 index 0000000000..67a4e5aeb0 --- /dev/null +++ b/auth-api/src/auth_api/schemas/documents.py @@ -0,0 +1,27 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Manager for user schema and export.""" + +from auth_api.models import Documents as DocumentsModel + +from .base_schema import BaseSchema + + +class DocumentSchema(BaseSchema): # pylint: disable=too-many-ancestors, too-few-public-methods + """This is the schema for the Documents model.""" + + class Meta: # pylint: disable=too-few-public-methods + """Maps all of the documents fields to a default schema.""" + + model = DocumentsModel diff --git a/auth-api/src/auth_api/schemas/entity.py b/auth-api/src/auth_api/schemas/entity.py index 8c15f86a4a..672e3c52c9 100644 --- a/auth-api/src/auth_api/schemas/entity.py +++ b/auth-api/src/auth_api/schemas/entity.py @@ -17,10 +17,10 @@ from auth_api.models import Entity as EntityModel -from .base_schema import BaseSchema +from .camel_case_schema import CamelCaseSchema -class EntitySchema(BaseSchema): # pylint: disable=too-many-ancestors, too-few-public-methods +class EntitySchema(CamelCaseSchema): # pylint: disable=too-many-ancestors, too-few-public-methods """Used to manage the default mapping between JSON and the Entity model.""" class Meta: # pylint: disable=too-few-public-methods diff --git a/auth-api/src/auth_api/schemas/invitation.py b/auth-api/src/auth_api/schemas/invitation.py index b8671125aa..a575aca560 100644 --- a/auth-api/src/auth_api/schemas/invitation.py +++ b/auth-api/src/auth_api/schemas/invitation.py @@ -31,4 +31,3 @@ class Meta: # pylint: disable=too-few-public-methods fields = ('id', 'recipient_email', 'sent_date', 'expires_on', 'accepted_date', 'status', 'membership') membership = fields.Nested(InvitationMembershipSchema, many=True) - expires_on = fields.Str(data_key='expiresOn') diff --git a/auth-api/src/auth_api/schemas/invitation_membership.py b/auth-api/src/auth_api/schemas/invitation_membership.py index 77a6a2bca5..def254ccbb 100644 --- a/auth-api/src/auth_api/schemas/invitation_membership.py +++ b/auth-api/src/auth_api/schemas/invitation_membership.py @@ -27,8 +27,9 @@ class Meta: # pylint: disable=too-few-public-methods """Maps all of the Membership fields to a default schema.""" model = InvitationMembershipModel - fields = ('org', 'membership_type') - org = fields.Nested('OrgSchema', exclude=['contacts', 'created', 'created_by', 'id', + org = fields.Nested('OrgSchema', exclude=['contacts', 'created', 'created_by', 'affiliated_entities', 'invitations', 'members', 'modified', 'preferred_payment', 'org_status', 'org_type']) + + invitation = fields.Nested('InvitationSchema', only=('id', 'recipient_email', 'sent_date', 'expires_on', 'status')) diff --git a/auth-api/src/auth_api/schemas/membership.py b/auth-api/src/auth_api/schemas/membership.py index f5b7ec360c..9c8c4f1633 100644 --- a/auth-api/src/auth_api/schemas/membership.py +++ b/auth-api/src/auth_api/schemas/membership.py @@ -30,5 +30,5 @@ class Meta: # pylint: disable=too-few-public-methods fields = ('id', 'membership_type_code', 'user', 'org') user = fields.Nested('UserSchema', only=('firstname', 'lastname', 'username', 'modified')) - org = fields.Nested('OrgSchema', only=('id', 'name', 'affiliated_entities', 'org_type')) + org = fields.Nested('OrgSchema', only=('id', 'name', 'affiliated_entities', 'org_type', 'members', 'invitations')) membership_type_code = fields.Str(data_key='membershipTypeCode') diff --git a/auth-api/src/auth_api/schemas/org.py b/auth-api/src/auth_api/schemas/org.py index e757e9ccf5..c3b7290027 100644 --- a/auth-api/src/auth_api/schemas/org.py +++ b/auth-api/src/auth_api/schemas/org.py @@ -30,5 +30,6 @@ class Meta: # pylint: disable=too-few-public-methods contacts = fields.Pluck('ContactLinkSchema', 'contact', many=True) members = fields.Nested('MembershipSchema', only=('id', 'user', 'membership_type_code'), many=True) + invitations = fields.Pluck('InvitationMembershipSchema', 'invitation', many=True) affiliated_entities = fields.Pluck('AffiliationSchema', 'entity', many=True, data_key='affiliatedEntities') org_type = fields.Pluck('OrgTypeSchema', 'code', data_key='orgType') diff --git a/auth-api/src/auth_api/schemas/schemas/termsofuse.json b/auth-api/src/auth_api/schemas/schemas/termsofuse.json new file mode 100644 index 0000000000..07e6e9cc9b --- /dev/null +++ b/auth-api/src/auth_api/schemas/schemas/termsofuse.json @@ -0,0 +1,32 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://bcrs.gov.bc.ca/.well_known/schemas/termsofuse", + "type": "object", + "title": "TermsOfUse", + "required": [ + "termsversion","istermsaccepted" + ], + "properties": { + "termsversion": { + "$id": "#/properties/termsversion", + "type": "integer", + "title": "terms version", + "default": "", + "examples": [ + 1 + ], + "pattern": "^(.*)$" + }, + "istermsaccepted": { + "$id": "#/properties/istermsaccepted", + "type": "boolean", + "title": "istermsaccepted", + "default": "", + "examples": [ + true + ] + } + } + } + \ No newline at end of file diff --git a/auth-api/src/auth_api/services/__init__.py b/auth-api/src/auth_api/services/__init__.py index ece8b335ba..6c8684fb54 100644 --- a/auth-api/src/auth_api/services/__init__.py +++ b/auth-api/src/auth_api/services/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. """Exposes all of the Services used in the API.""" from .affiliation import Affiliation +from .documents import Documents from .entity import Entity from .invitation import Invitation from .membership import Membership diff --git a/auth-api/src/auth_api/services/affiliation.py b/auth-api/src/auth_api/services/affiliation.py index 6582b50a02..adbab26510 100644 --- a/auth-api/src/auth_api/services/affiliation.py +++ b/auth-api/src/auth_api/services/affiliation.py @@ -16,7 +16,7 @@ from typing import Dict from flask import current_app -from sbc_common_components.tracing.service_tracing import ServiceTracing +from sbc_common_components.tracing.service_tracing import ServiceTracing # noqa: I001 from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error @@ -24,6 +24,7 @@ from auth_api.schemas import AffiliationSchema from auth_api.services.entity import Entity as EntityService from auth_api.services.org import Org as OrgService +from auth_api.utils.passcode import validate_passcode from auth_api.utils.roles import ALL_ALLOWED_ROLES, CLIENT_ADMIN_ROLES, STAFF @@ -75,8 +76,7 @@ def find_affiliated_entities_by_org_id(org_id, token_info: Dict = None): raise BusinessException(Error.DATA_NOT_FOUND, None) for affiliation_model in affiliation_models: - if affiliation_model: - data.append(EntityService(affiliation_model.entity).as_dict()) + data.append(EntityService(affiliation_model.entity).as_dict()) current_app.logger.debug('>find_affiliations_by_org_id') return data @@ -102,10 +102,9 @@ def create_affiliation(org_id, business_identifier, pass_code=None, token_info: authorized = False # If a passcode was provided... - if pass_code: + elif pass_code: # ... and the entity has a passcode on it, check that they match - if entity.pass_code != pass_code: - authorized = False + authorized = validate_passcode(pass_code, entity.pass_code) # If a passcode was not provided... else: # ... check that the entity does not have a passcode protecting it @@ -123,6 +122,10 @@ def create_affiliation(org_id, business_identifier, pass_code=None, token_info: if affiliation is not None: raise BusinessException(Error.DATA_ALREADY_EXISTS, None) + # Retrieve entity name from Legal-API and update the entity with current name + # TODO: Create subscription to listen for future name updates + entity.sync_name() + affiliation = AffiliationModel(org_id=org_id, entity_id=entity_id) affiliation.save() entity.set_pass_code_claimed(True) diff --git a/auth-api/src/auth_api/services/authorization.py b/auth-api/src/auth_api/services/authorization.py index 1942f2b238..2ff3acf616 100644 --- a/auth-api/src/auth_api/services/authorization.py +++ b/auth-api/src/auth_api/services/authorization.py @@ -49,10 +49,6 @@ def get_user_authorizations_for_entity(token_info: Dict, business_identifier: st auth_response = { 'roles': ['edit', 'view'] } - elif 'staff' in token_info.get('realm_access').get('roles'): - auth_response = { - 'roles': ['edit', 'view'] - } else: keycloak_guid = token_info.get('sub', None) auth = AuthorizationView.find_user_authorization_by_business_number(keycloak_guid, business_identifier) diff --git a/auth-api/src/auth_api/services/documents.py b/auth-api/src/auth_api/services/documents.py new file mode 100644 index 0000000000..877da33a3a --- /dev/null +++ b/auth-api/src/auth_api/services/documents.py @@ -0,0 +1,55 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service for managing the documents.""" + +from jinja2 import Environment, FileSystemLoader +from sbc_common_components.tracing.service_tracing import ServiceTracing + +from auth_api.models import Documents as DocumentsModel +from auth_api.schemas import DocumentSchema +from config import get_named_config + + +ENV = Environment(loader=FileSystemLoader('.'), autoescape=True) +CONFIG = get_named_config() + + +@ServiceTracing.trace(ServiceTracing.enable_tracing, ServiceTracing.should_be_tracing) +class Documents: + """Manages the documents in DB. + + This service manages retrieving the documents. + """ + + def __init__(self, model): + """Return an invitation service instance.""" + self._model = model + + @ServiceTracing.disable_tracing + def as_dict(self): + """Return the User as a python dict. + + None fields are not included in the dict. + """ + document_schema = DocumentSchema() + obj = document_schema.dump(self._model, many=False) + return obj + + @classmethod + def fetch_latest_document(cls, document_type): + """Get a membership type by the given code.""" + doc = DocumentsModel.fetch_latest_document_by_type(file_type=document_type) + if doc: + return Documents(doc) + return None diff --git a/auth-api/src/auth_api/services/entity.py b/auth-api/src/auth_api/services/entity.py index cd3267e8f6..a6028d30ac 100644 --- a/auth-api/src/auth_api/services/entity.py +++ b/auth-api/src/auth_api/services/entity.py @@ -15,7 +15,8 @@ from typing import Dict, Tuple -from sbc_common_components.tracing.service_tracing import ServiceTracing +from flask import current_app +from sbc_common_components.tracing.service_tracing import ServiceTracing # noqa: I001 from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error @@ -23,8 +24,11 @@ from auth_api.models import ContactLink as ContactLinkModel from auth_api.models.entity import Entity as EntityModel from auth_api.schemas import EntitySchema +from auth_api.utils.passcode import passcode_hash from auth_api.utils.util import camelback2snake + from .authorization import check_auth +from .rest_service import RestService @ServiceTracing.trace(ServiceTracing.enable_tracing, ServiceTracing.should_be_tracing) @@ -102,11 +106,11 @@ def save_entity(entity_info: dict): if existing_entity is None: entity_model = EntityModel.create_from_dict(entity_info) else: - existing_entity.update_from_dict(**entity_info) + # TODO temporary allow update passcode, should replace with reset passcode endpoint. + entity_info['passCode'] = passcode_hash(entity_info['passCode']) + existing_entity.update_from_dict(**camelback2snake(entity_info)) entity_model = existing_entity - - if not entity_model: - return None + entity_model.commit() entity = Entity(entity_model) return entity @@ -169,3 +173,14 @@ def validate_pass_code(self, pass_code): if pass_code == self._model.pass_code: return True return False + + def sync_name(self): + """Sync this entity's name with the name used in the LEAR database.""" + legal_url = current_app.config.get('LEGAL_API_URL') + f'/businesses/{self._model.business_identifier}' + legal_response = RestService.get(legal_url) + + if legal_response: + entity_json = legal_response.json() + entity_name = entity_json.get('business').get('legalName') + self._model.name = entity_name + self._model.save() diff --git a/auth-api/src/auth_api/services/invitation.py b/auth-api/src/auth_api/services/invitation.py index 102cec0ec6..0cb81ecb5b 100644 --- a/auth-api/src/auth_api/services/invitation.py +++ b/auth-api/src/auth_api/services/invitation.py @@ -15,19 +15,23 @@ from datetime import datetime from threading import Thread +from typing import Dict from flask import copy_current_request_context from itsdangerous import URLSafeTimedSerializer from jinja2 import Environment, FileSystemLoader -from sbc_common_components.tracing.service_tracing import ServiceTracing +from sbc_common_components.tracing.service_tracing import ServiceTracing # noqa: I001 from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error from auth_api.models import Invitation as InvitationModel +from auth_api.models import InvitationStatus as InvitationStatusModel from auth_api.models import Membership as MembershipModel from auth_api.schemas import InvitationSchema +from auth_api.utils.roles import ADMIN, OWNER from config import get_named_config +from .authorization import check_auth from .notification import send_email @@ -53,44 +57,45 @@ def as_dict(self): return obj @staticmethod - def create_invitation(invitation_info: dict, user): + def create_invitation(invitation_info: Dict, user, token_info: Dict, invitation_origin): """Create a new invitation.""" + # Ensure that the current user is OWNER or ADMIN on each org being invited to + context_path = CONFIG.AUTH_WEB_TOKEN_CONFIRM_PATH + for membership in invitation_info['membership']: + org_id = membership['orgId'] + check_auth(token_info, org_id=org_id, one_of_roles=(OWNER, ADMIN)) invitation = InvitationModel.create_from_dict(invitation_info, user.identifier) invitation.save() - Invitation.send_invitation(invitation, user.as_dict()) + Invitation.send_invitation(invitation, user.as_dict(), '{}/{}'.format(invitation_origin, context_path)) return Invitation(invitation) - def update_invitation(self, user): + def update_invitation(self, user, token_info: Dict, invitation_origin): """Update the specified invitation with new data.""" + # Ensure that the current user is OWNER or ADMIN on each org being re-invited to + context_path = CONFIG.AUTH_WEB_TOKEN_CONFIRM_PATH + for membership in self._model.membership: + org_id = membership.org_id + check_auth(token_info, org_id=org_id, one_of_roles=(OWNER, ADMIN)) updated_invitation = self._model.update_invitation_as_retried() - Invitation.send_invitation(updated_invitation, user.as_dict()) + Invitation.send_invitation(updated_invitation, user.as_dict(), '{}/{}'.format(invitation_origin, context_path)) return Invitation(updated_invitation) @staticmethod - def delete_invitation(invitation_id): + def delete_invitation(invitation_id, token_info: Dict = None): """Delete the specified invitation.""" + # Ensure that the current user is OWNER or ADMIN for each org in the invitation invitation = InvitationModel.find_invitation_by_id(invitation_id) if invitation is None: raise BusinessException(Error.DATA_NOT_FOUND, None) + for membership in invitation.membership: + org_id = membership.org_id + check_auth(token_info, org_id=org_id, one_of_roles=(OWNER, ADMIN)) invitation.delete() @staticmethod - def get_invitations(user_id, status): - """Get invitations sent by a user.""" - collection = [] - if status == 'ALL': - invitations = InvitationModel.find_invitations_by_user(user_id) - elif status == 'PENDING': - invitations = InvitationModel.find_pending_invitations_by_user(user_id) - else: - invitations = InvitationModel.find_invitations_by_status(user_id, status) - for invitation in invitations: - collection.append(Invitation(invitation).as_dict()) - return collection - - @staticmethod - def get_invitations_by_org_id(org_id, status): + def get_invitations_by_org_id(org_id, status, token_info: Dict = None): """Get invitations for an org.""" + check_auth(token_info, org_id=org_id, one_of_roles=(OWNER, ADMIN)) collection = [] if status == 'ALL': invitations = InvitationModel.find_invitations_by_org(org_id) @@ -101,7 +106,7 @@ def get_invitations_by_org_id(org_id, status): return collection @staticmethod - def find_invitation_by_id(invitation_id): + def find_invitation_by_id(invitation_id, token_info: Dict = None): """Find an existing invitation with the provided id.""" if invitation_id is None: return None @@ -110,17 +115,22 @@ def find_invitation_by_id(invitation_id): if not invitation: return None + # Ensure that the current user is an OWNER or ADMIN on each org in the invite being retrieved + for membership in invitation.membership: + org_id = membership.org_id + check_auth(token_info, org_id=org_id, one_of_roles=(OWNER, ADMIN)) + return Invitation(invitation) @staticmethod - def send_invitation(invitation: InvitationModel, user): + def send_invitation(invitation: InvitationModel, user, confirm_url): """Send the email notification.""" subject = '[BC Registries & Online Services] {} {} has invited you to join a team'.format(user['firstname'], user['lastname']) sender = CONFIG.MAIL_FROM_ID recipient = invitation.recipient_email confirmation_token = Invitation.generate_confirmation_token(invitation.id) - token_confirm_url = '{}/validatetoken/{}'.format(CONFIG.AUTH_WEB_TOKEN_CONFIRM_URL, confirmation_token) + token_confirm_url = '{}/validatetoken/{}'.format(confirm_url, confirmation_token) template = ENV.get_template('email_templates/business_invitation_email.html') try: @copy_current_request_context @@ -145,7 +155,7 @@ def generate_confirmation_token(invitation_id): def validate_token(token): """Check whether the passed token is valid.""" serializer = URLSafeTimedSerializer(CONFIG.EMAIL_TOKEN_SECRET_KEY) - token_valid_for = int(CONFIG.TOKEN_EXPIRY_PERIOD)*3600*24 if CONFIG.TOKEN_EXPIRY_PERIOD else 3600*24*7 + token_valid_for = int(CONFIG.TOKEN_EXPIRY_PERIOD) * 3600 * 24 if CONFIG.TOKEN_EXPIRY_PERIOD else 3600 * 24 * 7 try: invitation_id = serializer.loads(token, salt=CONFIG.EMAIL_SECURITY_PASSWORD_SALT, max_age=token_valid_for) except: # noqa: E722 @@ -166,10 +176,9 @@ def accept_invitation(invitation_id, user_id): membership_model = MembershipModel() membership_model.org_id = membership.org_id membership_model.user_id = user_id - membership_model.membership_type_code = membership.membership_type_code - membership_model.flush() + membership_model.membership_type = membership.membership_type + membership_model.save() invitation.accepted_date = datetime.now() - invitation.invitation_status_code = 'ACCEPTED' - invitation.flush() - MembershipModel.commit() + invitation.invitation_status = InvitationStatusModel.get_status_by_code('ACCEPTED') + invitation.save() return Invitation(invitation) diff --git a/auth-api/src/auth_api/services/membership.py b/auth-api/src/auth_api/services/membership.py index b1c14de943..338cf9d643 100644 --- a/auth-api/src/auth_api/services/membership.py +++ b/auth-api/src/auth_api/services/membership.py @@ -15,10 +15,16 @@ This module manages the Membership Information between an org and a user. """ +from typing import Dict -from sbc_common_components.tracing.service_tracing import ServiceTracing +from sbc_common_components.tracing.service_tracing import ServiceTracing # noqa: I001 +from auth_api.models import Membership as MembershipModel +from auth_api.models import MembershipType as MembershipTypeModel from auth_api.schemas import MembershipSchema +from auth_api.utils.roles import ADMIN, OWNER + +from .authorization import check_auth @ServiceTracing.trace(ServiceTracing.enable_tracing, ServiceTracing.should_be_tracing) @@ -42,3 +48,27 @@ def as_dict(self): membership_schema = MembershipSchema() obj = membership_schema.dump(self._model, many=False) return obj + + @staticmethod + def get_membership_type_by_code(type_code): + """Get a membership type by the given code.""" + return MembershipTypeModel.get_membership_type_by_code(type_code=type_code) + + @classmethod + def find_membership_by_id(cls, membership_id, token_info: Dict = None): + """Retrieve a membership record by id.""" + membership = MembershipModel.find_membership_by_id(membership_id) + + if membership: + # Ensure that this user is an ADMIN or OWNER on the org associated with this membership + check_auth(org_id=membership.org_id, token_info=token_info, one_of_roles=(ADMIN, OWNER)) + return Membership(membership) + return None + + def update_membership_role(self, updated_role: MembershipTypeModel, token_info: Dict = None): + """Update an existing membership with the given role.""" + # Ensure that this user is an ADMIN or OWNER on the org associated with this membership + check_auth(org_id=self._model.org_id, token_info=token_info, one_of_roles=(ADMIN, OWNER)) + self._model.membership_type = updated_role + self._model.save() + return self diff --git a/auth-api/src/auth_api/services/org.py b/auth-api/src/auth_api/services/org.py index 118b8d528d..3118c632e1 100644 --- a/auth-api/src/auth_api/services/org.py +++ b/auth-api/src/auth_api/services/org.py @@ -15,7 +15,7 @@ from typing import Dict, Tuple -from sbc_common_components.tracing.service_tracing import ServiceTracing +from sbc_common_components.tracing.service_tracing import ServiceTracing # noqa: I001 from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error @@ -25,8 +25,8 @@ from auth_api.models import Org as OrgModel from auth_api.schemas import OrgSchema from auth_api.utils.util import camelback2snake -from .authorization import check_auth +from .authorization import check_auth from .invitation import Invitation as InvitationService from .membership import Membership as MembershipService @@ -136,9 +136,9 @@ def get_members(self): """Return the set of members for this org.""" return {'members': self.as_dict()['members']} - def get_invitations(self, status='ALL'): + def get_invitations(self, status='ALL', token_info: Dict = None): """Return the unresolved (pending or failed) invitations for this org.""" - return {'invitations': InvitationService.get_invitations_by_org_id(self._model.id, status)} + return {'invitations': InvitationService.get_invitations_by_org_id(self._model.id, status, token_info)} def remove_member(self, member_id): """Remove the user with specified username from this org.""" @@ -147,4 +147,5 @@ def remove_member(self, member_id): self._model.members.remove(member) self._model.commit() return MembershipService(member) - return None + # If we get to this point, member with that id could not be found, so raise exception + raise BusinessException(Error.DATA_NOT_FOUND, None) diff --git a/auth-api/src/auth_api/services/rest_service.py b/auth-api/src/auth_api/services/rest_service.py new file mode 100644 index 0000000000..2d4240ce58 --- /dev/null +++ b/auth-api/src/auth_api/services/rest_service.py @@ -0,0 +1,109 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to invoke Rest services.""" +import json + +import requests +from flask import current_app +from requests.adapters import HTTPAdapter # pylint:disable=ungrouped-imports +# pylint:disable=ungrouped-imports +from requests.exceptions import ConnectionError as ReqConnectionError +from requests.exceptions import ConnectTimeout, HTTPError +from urllib3.util.retry import Retry + +from auth_api.exceptions import ServiceUnavailableException +from auth_api.utils.enums import AuthHeaderType, ContentType + + +RETRY_ADAPTER = HTTPAdapter(max_retries=Retry(total=5, backoff_factor=1, status_forcelist=[404])) + + +class RestService: + """Service to invoke Rest services which uses OAuth 2.0 implementation.""" + + @staticmethod + def post(endpoint, token, auth_header_type: AuthHeaderType, content_type: ContentType, data): + """POST service.""" + current_app.logger.debug('= 500: + raise ServiceUnavailableException(exc) + raise exc + finally: + current_app.logger.debug(response.headers if response else 'Empty Response Headers') + current_app.logger.info('response : {}'.format(response.text if response else '')) + + current_app.logger.debug('>post') + return response + + @staticmethod + def get(endpoint, token=None, auth_header_type: AuthHeaderType = AuthHeaderType.BEARER, + content_type: ContentType = ContentType.JSON, retry_on_failure: bool = False): + """GET service.""" + current_app.logger.debug('= 500: + raise ServiceUnavailableException(exc) + raise exc + finally: + current_app.logger.debug(response.headers if response else 'Empty Response Headers') + current_app.logger.info('response : {}'.format(response.text if response else '')) + + current_app.logger.debug('>GET') + return response diff --git a/auth-api/src/auth_api/services/user.py b/auth-api/src/auth_api/services/user.py index ab2f435f24..fdaf9389d8 100644 --- a/auth-api/src/auth_api/services/user.py +++ b/auth-api/src/auth_api/services/user.py @@ -16,7 +16,7 @@ This module manages the User Information. """ -from sbc_common_components.tracing.service_tracing import ServiceTracing +from sbc_common_components.tracing.service_tracing import ServiceTracing # noqa: I001 from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error @@ -117,6 +117,14 @@ def update_contact(token, contact_info: dict): # return the user with the updated contact return User(user) + @staticmethod + def update_terms_of_use(token, is_terms_accepted, terms_of_use_version): + """Update terms of use for an existing user.""" + if token is None: + raise BusinessException(Error.DATA_NOT_FOUND, None) + user = UserModel.update_terms_of_use(token, is_terms_accepted, terms_of_use_version) + return User(user) + @staticmethod def delete_contact(token): """Delete the contact for an existing user.""" diff --git a/auth-api/src/auth_api/utils/enums.py b/auth-api/src/auth_api/utils/enums.py new file mode 100644 index 0000000000..6760016821 --- /dev/null +++ b/auth-api/src/auth_api/utils/enums.py @@ -0,0 +1,29 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Enum definitions.""" +from enum import Enum + + +class AuthHeaderType(Enum): + """Authorization header types.""" + + BASIC = 'Basic {}' + BEARER = 'Bearer {}' + + +class ContentType(Enum): + """Http Content Types.""" + + JSON = 'application/json' + FORM_URL_ENCODED = 'application/x-www-form-urlencoded' diff --git a/auth-api/src/auth_api/utils/passcode.py b/auth-api/src/auth_api/utils/passcode.py new file mode 100644 index 0000000000..bec15017e2 --- /dev/null +++ b/auth-api/src/auth_api/utils/passcode.py @@ -0,0 +1,32 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Using the bcrypt library to securely hash and check hashed passcode.""" +import bcrypt + + +def passcode_hash(passcode: str): + """Return hashed passcode.""" + if passcode: + hashed_passcode: bytes = bcrypt.hashpw(passcode.encode(), bcrypt.gensalt()) + return hashed_passcode.decode() + return None + + +def validate_passcode(passcode: str, hashed_passcode: str): + """Validate passcode and hashed passcode.""" + if passcode and hashed_passcode: + passcode_bytes: str = passcode.encode() + hashed_passcod_bytes: bytes = hashed_passcode.encode() + return bcrypt.checkpw(passcode_bytes, hashed_passcod_bytes) + return False diff --git a/auth-api/tests/postman/auth-api-bcsc.postman_collection.json b/auth-api/tests/postman/auth-api-bcsc.postman_collection.json deleted file mode 100644 index 6653e33da0..0000000000 --- a/auth-api/tests/postman/auth-api-bcsc.postman_collection.json +++ /dev/null @@ -1,1594 +0,0 @@ -{ - "info": { - "_postman_id": "6c870592-a59a-432f-93fe-a3f6c4f00f75", - "name": "auth-api-bcsc", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Refresh admin token", - "event": [ - { - "listen": "test", - "script": { - "id": "cbeb8178-cd2f-447e-acac-8eb69efc0031", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"adminToken\", jsonData.access_token);", - "pm.environment.set(\"adminRefreshToken\", jsonData.refresh_token);" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "id": "776fb4fd-4f46-4f00-91eb-fd796e49fcc8", - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "grant_type=password&client_id={{webClientId}}&username={{test_staff_username}}&password={{test_staff_username}}&client_secret={{webClientSecret}}" - }, - "url": { - "raw": "https://{{base_url}}/auth/realms/{{realm_name}}/protocol/openid-connect/token", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "realms", - "{{realm_name}}", - "protocol", - "openid-connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Login basic user", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var auth_token = pm.request.getHeaders()['Authorization'];", - "auth_token = auth_token.replace('Bearer ', '');", - "postman.setEnvironmentVariable('userToken', auth_token);", - "", - "var jsonData = pm.response.json();", - "pm.environment.set(\"username\", jsonData.username);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "oauth2", - "oauth2": [ - { - "key": "accessToken", - "value": "", - "type": "string" - }, - { - "key": "tokenType", - "value": "bearer", - "type": "string" - }, - { - "key": "addTokenTo", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api_url}}/api/v1/users", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users" - ] - } - }, - "response": [] - }, - { - "name": "Get users (Staff Only)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://{{api_url}}/api/v1/users", - "protocol": "http", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users" - ] - } - }, - "response": [] - }, - { - "name": "Search on orgs (Staff Only)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{api_url}}/api/v1/orgs", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs" - ] - } - }, - "response": [] - }, - { - "name": "Get a specific user (Staff Only)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{adminToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://{{api_url}}/api/v1/users/{{username}}", - "protocol": "http", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users", - "{{username}}" - ] - } - }, - "response": [] - }, - { - "name": "Get user profile (me)", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "http://{{api_url}}/api/v1/users/@me", - "protocol": "http", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users", - "@me" - ] - } - }, - "response": [] - }, - { - "name": "Add contact to user profile (me)", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"username\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"email\": \"{{sample_email}}\",\r\n \"phone\": \"{{sample_phone}}\",\r\n \"phoneExtension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/users/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Update contact on user profile (me)", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"email\": \"{{sample_updated_email}}\",\r\n \"phone\": \"{{sample_phone}}\",\r\n \"phoneExtension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/users/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Delete contact on user profile (me)", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api_url}}/api/v1/users/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Add entity", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - "});", - "", - "var jsonData = pm.response.json();", - "pm.environment.set(\"entity_identifier\", jsonData.id);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"businessIdentifier\": \"{{business_identifier}}\",\r\n \"name\": \"{{business_name}}\",\r\n \"businessNumber\": \"{{business_number}}\",\r\n \"passCode\": \"{{entity_passcode}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities" - ] - } - }, - "response": [] - }, - { - "name": "Get entity", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userId\", jsonData.id);", - "", - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities/{{business_identifier}}", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities", - "{{business_identifier}}" - ] - } - }, - "response": [] - }, - { - "name": "Add contact to entity", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"email\": \"{{sample_email}}\",\r\n \"phone\": \"{{sample_phone}}\",\r\n \"phoneExtension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities/{{business_identifier}}/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities", - "{{business_identifier}}", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Update contact for entity", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"email\": \"{{sample_updated_email}}\",\r\n \"phone\": \"{{sample_phone}}\",\r\n \"phoneExtension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities/{{business_identifier}}/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities", - "{{business_identifier}}", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Delete contact for entity", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{api_url}}/api/v1/entities/{{business_identifier}}/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities", - "{{business_identifier}}", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Create an org", - "event": [ - { - "listen": "test", - "script": { - "id": "58bd542c-3498-4e98-8d28-9fe2eb62ea3b", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"org_identifier\", jsonData.id);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"{{test_org_name}}\"\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/orgs", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs" - ] - } - }, - "response": [] - }, - { - "name": "Get a specific org", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userId\", jsonData.id);", - "", - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api_url}}/api/v1/orgs/{{org_identifier}}", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}" - ] - } - }, - "response": [] - }, - { - "name": "Update a specific org", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"{{test_org_name_updated}}\"\n}" - }, - "url": { - "raw": "http://{{api_url}}/api/v1/orgs/{{org_identifier}}", - "protocol": "http", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}" - ] - } - }, - "response": [] - }, - { - "name": "Add contact to org", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"username\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"email\": \"{{sample_email}}\",\r\n \"phone\": \"{{sample_phone}}\",\r\n \"phoneExtension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/orgs/{{org_identifier}}/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Update contact for org", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"username\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"email\": \"{{sample_updated_email}}\",\r\n \"phone\": \"{{sample_phone}}\",\r\n \"phoneExtension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/orgs/{{org_identifier}}/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Delete contact for org", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"username\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{api_url}}/api/v1/orgs/{{org_identifier}}/contacts", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}", - "contacts" - ] - } - }, - "response": [] - }, - { - "name": "Get members for org", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userId\", jsonData.id);", - "", - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"membershipTypeCode\");", - " pm.expect(pm.response.text()).to.include(\"user\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api_url}}/api/v1/orgs/{{org_identifier}}/members", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}", - "members" - ] - } - }, - "response": [] - }, - { - "name": "Create an affiliation", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var auth_token = pm.request.getHeaders()['Authorization'];", - "auth_token = auth_token.replace('Bearer ', '');", - "postman.setEnvironmentVariable('userToken', auth_token);", - "", - "var jsonData = pm.response.json();", - "pm.environment.set(\"username\", jsonData.username);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"businessIdentifier\": \"{{business_identifier}}\",\r\n \"passCode\": \"{{entity_passcode}}\"\r\n}" - }, - "url": { - "raw": "http://{{api_url}}/api/v1/orgs/{{org_identifier}}/affiliations", - "protocol": "http", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}", - "affiliations" - ] - } - }, - "response": [] - }, - { - "name": "Delete an affiliation", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var auth_token = pm.request.getHeaders()['Authorization'];", - "auth_token = auth_token.replace('Bearer ', '');", - "postman.setEnvironmentVariable('userToken', auth_token);", - "", - "var jsonData = pm.response.json();", - "pm.environment.set(\"username\", jsonData.username);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "http://{{api_url}}/api/v1/orgs/{{org_identifier}}/affiliations/{{business_identifier}}", - "protocol": "http", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}", - "affiliations", - "{{business_identifier}}" - ] - } - }, - "response": [] - }, - { - "name": "Get entities affiliated with an org", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var auth_token = pm.request.getHeaders()['Authorization'];", - "auth_token = auth_token.replace('Bearer ', '');", - "postman.setEnvironmentVariable('userToken', auth_token);", - "", - "var jsonData = pm.response.json();", - "pm.environment.set(\"username\", jsonData.username);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "http://{{api_url}}/api/v1/orgs/{{org_identifier}}/affiliations", - "protocol": "http", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "orgs", - "{{org_identifier}}", - "affiliations" - ] - } - }, - "response": [] - }, - { - "name": "Get orgs for user (me)", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"username\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{api_url}}/api/v1/users/orgs", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users", - "orgs" - ] - } - }, - "response": [] - } - ] -} \ No newline at end of file diff --git a/auth-api/tests/postman/auth-api.passcode.postman_data.json b/auth-api/tests/postman/auth-api.passcode.postman_data.json deleted file mode 100644 index ef547ac580..0000000000 --- a/auth-api/tests/postman/auth-api.passcode.postman_data.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "corp_num": "CP0011322", - "temp_password": "340847540" - } -] diff --git a/auth-api/tests/postman/auth-api.postman_collection.json b/auth-api/tests/postman/auth-api.postman_collection.json deleted file mode 100644 index a4c36d7070..0000000000 --- a/auth-api/tests/postman/auth-api.postman_collection.json +++ /dev/null @@ -1,1861 +0,0 @@ -{ - "info": { - "_postman_id": "237af49f-7ccd-469e-89db-b77470b65c78", - "name": "auth-api", - "description": "Endpoint samples to create an user using KeyCloak REST Admin API", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Refresh admin token", - "event": [ - { - "listen": "test", - "script": { - "id": "cbeb8178-cd2f-447e-acac-8eb69efc0031", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"adminToken\", jsonData.access_token);", - "pm.environment.set(\"adminRefreshToken\", jsonData.refresh_token);" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "id": "776fb4fd-4f46-4f00-91eb-fd796e49fcc8", - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "grant_type=refresh_token&client_id={{clientId}}&refresh_token={{adminRefreshToken}}&client_secret={{clientSecret}}" - }, - "url": { - "raw": "https://{{base_url}}/auth/realms/{{realm_name}}/protocol/openid-connect/token", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "realms", - "{{realm_name}}", - "protocol", - "openid-connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Create passcode user", - "event": [ - { - "listen": "test", - "script": { - "id": "cbeb8178-cd2f-447e-acac-8eb69efc0031", - "exec": [ - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "id": "776fb4fd-4f46-4f00-91eb-fd796e49fcc8", - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"username\": \"{{corp_num}}\",\n \"enabled\": true,\n \"attributes\": {\n \"incNumber\": [\n \"{{corp_num}}\"\n ],\n \"source\": [\n \"PASSCODE\"\n ]\n }\n}" - }, - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/users", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "users" - ] - }, - "description": "Create user records with the input from the csv" - }, - "response": [] - }, - { - "name": "Get user id", - "event": [ - { - "listen": "test", - "script": { - "id": "cfabd047-d9a1-431d-86c5-b54bc921debe", - "exec": [ - "pm.test(\"Set User Id \", function () {", - " var jsonData = pm.response.json();", - " pm.environment.set(\"userId\", jsonData[0].id);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/users?username={{corp_num}}", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "users" - ], - "query": [ - { - "key": "username", - "value": "{{corp_num}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Get user id by email", - "event": [ - { - "listen": "test", - "script": { - "id": "cfabd047-d9a1-431d-86c5-b54bc921debe", - "exec": [ - "pm.test(\"Set User Id \", function () {", - " var jsonData = pm.response.json();", - " pm.environment.set(\"userId\", jsonData[0].id);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/users?username={{corp_num}}", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "users" - ], - "query": [ - { - "key": "username", - "value": "{{corp_num}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Update password", - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"type\": \"password\",\n \"value\": \"{{temp_password}}\",\n \"temporary\": false\n}" - }, - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/users/{{userId}}/reset-password", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "users", - "{{userId}}", - "reset-password" - ] - } - }, - "response": [] - }, - { - "name": "Get group id", - "event": [ - { - "listen": "test", - "script": { - "id": "510198a3-47d6-475a-bd1c-daef0b9eb180", - "exec": [ - "pm.test(\"Get group by path\", function () {", - " var jsonData = pm.response.json();", - " pm.environment.set(\"groupId\", jsonData.id);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/group-by-path/{{defaultGroup}}", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "group-by-path", - "{{defaultGroup}}" - ] - } - }, - "response": [] - }, - { - "name": "Assign group to user", - "request": { - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/users/{{userId}}/groups/{{groupId}}", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "users", - "{{userId}}", - "groups", - "{{groupId}}" - ] - } - }, - "response": [] - }, - { - "name": "Get user group", - "event": [ - { - "listen": "test", - "script": { - "id": "510198a3-47d6-475a-bd1c-daef0b9eb180", - "exec": [ - "pm.test(\"Get group by path\", function () {", - " var jsonData = pm.response.json();", - " pm.environment.set(\"groupId\", jsonData.id);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/users/{{userId}}/groups", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "users", - "{{userId}}", - "groups" - ] - } - }, - "response": [] - }, - { - "name": "Login user", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userToken\", jsonData.access_token);", - "pm.environment.set(\"refreshUserToken\", jsonData.refresh_token);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "grant_type=password&client_id={{clientId}}&username={{corp_num}}&password={{temp_password}}&client_secret={{clientSecret}}" - }, - "url": { - "raw": "https://{{base_url}}/auth/realms/{{realm_name}}/protocol/openid-connect/token", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "realms", - "{{realm_name}}", - "protocol", - "openid-connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Refresh user token", - "event": [ - { - "listen": "test", - "script": { - "id": "3ef30827-b3d6-4518-8dc5-afdc26a7681e", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userToken\", jsonData.access_token);", - "pm.environment.set(\"refreshUserToken\", jsonData.refresh_token);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "grant_type=refresh_token&client_id={{clientId}}&refresh_token={{refreshUserToken}}&client_secret={{clientSecret}}" - }, - "url": { - "raw": "https://{{base_url}}/auth/realms/{{realm_name}}/protocol/openid-connect/token", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "realms", - "{{realm_name}}", - "protocol", - "openid-connect", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Logout user", - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{userToken}}" - }, - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/x-www-form-urlencoded", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "client_id={{webClientId}}&refresh_token={{refreshUserToken}}&client_secret={{webClientSecret}}" - }, - "url": { - "raw": "https://{{base_url}}/auth/realms/{{realm_name}}/protocol/openid-connect/logout", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "realms", - "{{realm_name}}", - "protocol", - "openid-connect", - "logout" - ] - } - }, - "response": [] - }, - { - "name": "Delete user", - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "https://{{base_url}}/auth/admin/realms/{{realm_name}}/users/{{userId}}", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "admin", - "realms", - "{{realm_name}}", - "users", - "{{userId}}" - ] - } - }, - "response": [] - }, - { - "name": "Logout admin", - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminToken}}" - } - ], - "url": { - "raw": "https://{{base_url}}/auth/realms/{{realm_name}}/protocol/openid-connect/userinfo", - "protocol": "https", - "host": [ - "{{base_url}}" - ], - "path": [ - "auth", - "realms", - "{{realm_name}}", - "protocol", - "openid-connect", - "userinfo" - ] - } - }, - "response": [] - }, - { - "name": "Add user to keycloak-api", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"id\");", - " pm.expect(pm.response.text()).to.include(\"username\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"username\":\"test11\",\r\n \"password\":\"1111\",\r\n \"firstname\":\"111\",\r\n \"lastname\":\"test\",\r\n \"email\":\"test11@gov.bc.ca\",\r\n \"enabled\":true,\r\n \"user_type\":[ \r\n \"/test\",\r\n \"/basic/editor\"\r\n ],\r\n \"corp_type\":\"CP\",\r\n \"source\":\"PASSCODE\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/admin/users", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "admin", - "users" - ] - } - }, - "response": [] - }, - { - "name": "Add business to API", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"businessIdentifier\": \"{{business_identifier}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities" - ] - } - }, - "response": [] - }, - { - "name": "Get business by business id", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userId\", jsonData.id);", - "", - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities/{{business_identifier}}", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities", - "{{business_identifier}}" - ] - } - }, - "response": [] - }, - { - "name": "Add to contact to business API", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"emailAddress\": \"{{sample_email}}\",\r\n \"phoneNumber\": \"{{sample_phone}}\",\r\n \"extension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities/{{business_identifier}}/contact", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities", - "{{business_identifier}}", - "contact" - ] - } - }, - "response": [] - }, - { - "name": "Update contact for business API", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"businessIdentifier\");", - " pm.expect(pm.response.text()).to.include(\"contacts\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"emailAddress\": \"{{sample_updated_email}}\",\r\n \"phoneNumber\": \"{{sample_phone}}\",\r\n \"extension\": \"{{sample_extension}}\"\r\n}" - }, - "url": { - "raw": "{{api_url}}/api/v1/entities/{{business_identifier}}/contact", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "entities", - "{{business_identifier}}", - "contact" - ] - } - }, - "response": [] - }, - { - "name": "get user by username-api", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userId\", jsonData.id);", - "", - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"id\");", - " pm.expect(pm.response.text()).to.include(\"username\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "username=test11" - }, - "url": { - "raw": "{{api_url}}/api/v1/admin/users", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "admin", - "users" - ] - } - }, - "response": [] - }, - { - "name": "Login by userid/passwd-api", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userToken\", jsonData.access_token);", - "pm.environment.set(\"refreshUserToken\", jsonData.refresh_token);", - "", - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"access_token\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "username=test11&password=1111" - }, - "url": { - "raw": "{{api_url}}/api/v1/token", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Refresh token-api", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "var jsonData = pm.response.json();", - "pm.environment.set(\"userToken\", jsonData.access_token);", - "pm.environment.set(\"refreshUserToken\", jsonData.refresh_token);", - "", - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"access_token\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "refresh_token={{refreshUserToken}}" - }, - "url": { - "raw": "{{api_url}}/api/v1/token", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "token" - ] - } - }, - "response": [] - }, - { - "name": "logout-api", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(204);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "refresh_token={{refreshUserToken}}" - }, - "url": { - "raw": "{{api_url}}/api/v1/logout", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "logout" - ] - } - }, - "response": [] - }, - { - "name": "Get user info-api", - "event": [ - { - "listen": "test", - "script": { - "id": "0f28387b-085a-457e-8b66-f05626602b59", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the status code is 200", - " pm.response.to.be.ok; // info, success, redirection, clientError, serverError, are other variants", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"roles\");", - " pm.expect(pm.response.text()).to.include(\"username\");", - " pm.expect(pm.response.text()).to.not.include(\"auth-api\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{userToken}}" - } - ], - "url": { - "raw": "{{api_url}}/api/v1/users/info", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "users", - "info" - ] - } - }, - "response": [] - }, - { - "name": "delete user by username-api", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(204);", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "username=test11" - }, - "url": { - "raw": "{{api_url}}/api/v1/admin/users", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "admin", - "users" - ] - } - }, - "response": [] - }, - { - "name": "Add invitation", - "event": [ - { - "listen": "test", - "script": { - "id": "50064553-46fd-4d90-a5a9-35690fa4386d", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"status\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"recipientEmail\": \"{{invitation_recipient_email}}\",\n \"membership\": [\n {\n \"membershipType\": \"{{invitation_membership_type}}\",\n \"orgId\": \"{{invitation_membership_org_id}}\"\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{api_url}}/api/v1/invitations", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "invitations" - ] - } - }, - "response": [] - }, - { - "name": "Get Invitations by user", - "event": [ - { - "listen": "test", - "script": { - "id": "113a6b3d-d9d3-42f4-9a09-4a0d689c0a1b", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the status code is 200", - " pm.response.to.be.ok; // info, success, redirection, clientError, serverError, are other variants", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"invitations\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{api_url}}/api/v1/invitations?status=pending", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "invitations" - ], - "query": [ - { - "key": "status", - "value": "pending" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Invitation By Id", - "event": [ - { - "listen": "test", - "script": { - "id": "5c71c6c0-9789-4092-8b2f-33dac29cc26a", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"status\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{api_url}}/api/v1/invitations/{{invitation_id}}", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "invitations", - "{{invitation_id}}" - ] - } - }, - "response": [] - }, - { - "name": "Resend Invitation", - "event": [ - { - "listen": "test", - "script": { - "id": "d1a6bcaf-6909-43b3-8836-1c742855b4e2", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"status\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{api_url}}/api/v1/invitations/{{invitation_id}}", - "host": [ - "{api_url}}" - ], - "path": [ - "api", - "v1", - "invitations", - "{{invitation_id}}" - ] - } - }, - "response": [] - }, - { - "name": "Delete Invitation", - "event": [ - { - "listen": "test", - "script": { - "id": "a27660f6-b1e5-473c-8149-e5309b672fa7", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "{{api_url}}/api/v1/invitations/{{invitation_id}}", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "invitations", - "{{invitation_id}}" - ] - } - }, - "response": [] - }, - { - "name": "Validate Invitation Token", - "event": [ - { - "listen": "test", - "script": { - "id": "50064553-46fd-4d90-a5a9-35690fa4386d", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{api_url}}/api/v1/invitations/tokens/{{token}}", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "invitations", - "tokens", - "{{token}}" - ] - } - }, - "response": [] - }, - { - "name": "Accept Invitation", - "event": [ - { - "listen": "test", - "script": { - "id": "50064553-46fd-4d90-a5a9-35690fa4386d", - "exec": [ - "pm.test(\"Response time is less than 5000ms\", function () {", - " pm.expect(pm.response.responseTime).to.be.below(5000);", - "});", - "", - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"response should be okay to process\", function () { ", - " pm.response.to.not.be.error; ", - " //pm.response.to.have.jsonBody(\"\"); ", - " pm.response.to.not.have.jsonBody(\"error\"); ", - "});", - "", - "pm.test(\"response must be valid and have a body\", function () {", - " // assert that the response has a valid JSON body", - " pm.response.to.be.withBody;", - " pm.response.to.be.json; // this assertion also checks if a body exists, so the above check is not needed", - "});", - "", - "pm.test(\"Verify payload\", () => {", - " pm.expect(pm.response.text()).to.include(\"status\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{userToken}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{api_url}}/api/v1/invitations/tokens/{{token}}", - "host": [ - "{{api_url}}" - ], - "path": [ - "api", - "v1", - "invitations", - "tokens", - "{{token}}" - ] - } - }, - "response": [] - } - ], - "auth": { - "type": "oauth2", - "oauth2": [ - { - "key": "accessToken", - "value": "{{adminToken}}", - "type": "string" - }, - { - "key": "addTokenTo", - "value": "header", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "id": "09ab863a-9b1b-4ac8-82c9-0c1d2178cb02", - "type": "text/javascript", - "exec": [ - "function getvar(variableName) {", - " let value = pm.variables.get(variableName);", - " if (!value) throw new Error(", - " `Variable '${variableName}' is not defined. Do you forget to select an environment?`);", - " return value;", - "}", - "", - "let tokenUrl = getvar('tokenUrl');", - "let clientId = getvar('clientId');", - "let clientSecret = getvar('clientSecret');", - "let scope = ''", - "", - "let getTokenRequest = {", - " method: 'POST',", - " url: tokenUrl,", - " auth: {", - " type: \"basic\",", - " basic: [", - " { key: \"username\", value: clientId },", - " { key: \"password\", value: clientSecret }", - " ]", - " },", - " body: {", - " mode: 'urlencoded',", - " urlencoded: [", - " { key: 'grant_type', value: 'client_credentials' }", - " ] ", - " ", - " }", - "};", - "", - "pm.sendRequest(getTokenRequest, (err, response) => {", - " ", - " let jsonResponse = response.json()", - " let newAdminToken = jsonResponse.access_token", - " let newRefreshToken = jsonResponse.refresh_token", - "", - " console.log({ err, jsonResponse, newAdminToken })", - "", - " pm.environment.set('adminToken', newAdminToken);", - " pm.variables.set('adminToken', newAdminToken);", - " pm.environment.set('adminRefreshToken', newRefreshToken);", - " pm.variables.set('adminRefreshToken', newRefreshToken);", - "});", - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "20ab2fef-3029-46e0-8b81-8cbb467e56a5", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "protocolProfileBehavior": {} -} \ No newline at end of file diff --git a/auth-api/tests/postman/auth-api.postman_environment.json b/auth-api/tests/postman/auth-api.postman_environment.json deleted file mode 100644 index 4186f68ccd..0000000000 --- a/auth-api/tests/postman/auth-api.postman_environment.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "id": "a13b521d-3dd4-4042-8087-a9f4df47f3b3", - "name": "Dev - Keycloak/Local APIs", - "values": [ - { - "key": "base_url", - "value": "sso-dev.pathfinder.gov.bc.ca", - "enabled": true - }, - { - "key": "realm_name", - "value": "fcf0kpqr", - "enabled": true - }, - { - "key": "tokenUrl", - "value": "https://sso-dev.pathfinder.gov.bc.ca/auth/realms/fcf0kpqr/protocol/openid-connect/token", - "enabled": true - }, - { - "key": "clientId", - "value": "sbc-auth-cron-job", - "enabled": true - }, - { - "key": "clientSecret", - "value": "", - "enabled": true - }, - { - "key": "defaultGroup", - "value": "basic/editor", - "enabled": true - }, - { - "key": "accessToken", - "value": "", - "enabled": true - }, - { - "key": "refreshToken", - "value": "", - "enabled": true - }, - { - "key": "userId", - "value": null, - "enabled": true - }, - { - "key": "groupId", - "value": null, - "enabled": true - }, - { - "key": "api_url", - "value": "localhost:5000", - "enabled": true - }, - { - "key": "adminRefreshToken", - "value": "", - "enabled": true - }, - { - "key": "adminToken", - "value": "", - "enabled": true - }, - { - "key": "refreshUserToken", - "value": null, - "enabled": true - }, - { - "key": "userToken", - "value": "", - "enabled": true - }, - { - "key": "webClientId", - "value": "sbc-auth-web", - "enabled": true - }, - { - "key": "webClientSecret", - "value": "", - "enabled": true - }, - { - "key": "user_id", - "value": null, - "enabled": true - }, - { - "key": "corp_num", - "value": "CP0001245", - "enabled": true - }, - { - "key": "user_email", - "value": "test111@gov.bc.ca", - "enabled": true - }, - { - "key": "temp_password", - "value": "foobar", - "enabled": true - }, - { - "key": "business_identifier", - "value": "CP0001245", - "enabled": true - }, - { - "key": "sample_email", - "value": "foo@bar.com", - "enabled": true - }, - { - "key": "sample_phone", - "value": "(555)-555-5555", - "enabled": true - }, - { - "key": "sample_extension", - "value": "123", - "enabled": true - }, - { - "key": "sample_updated_email", - "value": "test@bar.com", - "enabled": true - }, - { - "key": "test_staff_username", - "value": "test_staff", - "enabled": true - }, - { - "key": "test_staff_password", - "value": "test_staff", - "enabled": true - }, - { - "key": "username", - "value": null, - "enabled": true - }, - { - "key": "test_org_name", - "value": "Test Org", - "enabled": true - }, - { - "key": "org_identifier", - "value": 30, - "enabled": true - }, - { - "key": "test_org_name_updated", - "value": "Test Org Updated", - "enabled": true - }, - { - "key": "entity_identifier", - "value": null, - "enabled": true - }, - { - "key": "affiliation_identifier", - "value": null, - "enabled": true - }, - { - "key": "business_number", - "value": "791861073BC0001", - "enabled": true - }, - { - "key": "business_name", - "value": "Foobar, Inc.", - "enabled": true - }, - { - "key": "entity_passcode", - "value": "12345", - "enabled": true - }, - { - "key": "invitation_recipient_email", - "value": "test111@gov.bc.ca", - "enabled": true - }, - { - "key": "invitation_membership_type", - "value": "MEMBER", - "enabled": true - }, - { - "key": "invitation_membership_org_id", - "value": "1", - "enabled": true - }, - { - "key": "invitation_id", - "value": "1", - "enabled": true - }, - { - "key": "token", - "value": "MjA.XY1BKw.jlljqGk9m1driamPQpw67cvK74o", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2019-08-26T22:27:02.718Z", - "_postman_exported_using": "Postman/7.5.0" -} \ No newline at end of file diff --git a/auth-api/tests/unit/api/test_documents.py b/auth-api/tests/unit/api/test_documents.py new file mode 100644 index 0000000000..0a48ceaac1 --- /dev/null +++ b/auth-api/tests/unit/api/test_documents.py @@ -0,0 +1,67 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests to assure the documents API end-point. + +Test-Suite to ensure that the /documents endpoint is working as expected. +""" + +from auth_api import status as http_status +from tests.utilities.factory_utils import factory_document_model + + +def test_documents_returns_200(client, jwt, session): # pylint:disable=unused-argument + """Assert authorizations for affiliated users returns 200.""" + rv = client.get(f'/api/v1/documents/termsofuse', content_type='application/json') + + assert rv.status_code == http_status.HTTP_200_OK + assert rv.json.get('version_id') == 1 + + +def test_invalid_documents_returns_404(client, jwt, session): # pylint:disable=unused-argument + """Assert authorizations for affiliated users returns 200.""" + rv = client.get(f'/api/v1/documents/junk', content_type='application/json') + + assert rv.status_code == http_status.HTTP_404_NOT_FOUND + assert rv.json.get('message') == 'The requested invitation could not be found.' + + +def test_documents_returns_200_for_some_type(client, jwt, session): # pylint:disable=unused-argument + """Assert authorizations for affiliated users returns 200.""" + html_content = '' + version_id = 10 + factory_document_model(version_id, 'sometype', html_content) + + rv = client.get(f'/api/v1/documents/sometype', content_type='application/json') + + assert rv.status_code == http_status.HTTP_200_OK + assert rv.json.get('content') == html_content + assert rv.json.get('version_id') == version_id + + +def test_documents_returns_latest_always(client, jwt, session): # pylint:disable=unused-argument + """Assert authorizations for affiliated users returns 200.""" + html_content_1 = '1' + version_id_1 = 2 + factory_document_model(version_id_1, 'termsofuse', html_content_1) + + html_content_2 = '2' + version_id_2 = 3 + factory_document_model(version_id_2, 'termsofuse', html_content_2) + + rv = client.get(f'/api/v1/documents/termsofuse', content_type='application/json') + + assert rv.status_code == http_status.HTTP_200_OK + assert rv.json.get('content') == html_content_2 + assert rv.json.get('version_id') == version_id_2 diff --git a/auth-api/tests/unit/api/test_entity.py b/auth-api/tests/unit/api/test_entity.py index d43ecb0b0a..9bb89e8cb2 100644 --- a/auth-api/tests/unit/api/test_entity.py +++ b/auth-api/tests/unit/api/test_entity.py @@ -19,140 +19,75 @@ import copy import json -import os +from unittest.mock import patch from auth_api import status as http_status +from auth_api.exceptions import BusinessException +from auth_api.exceptions.errors import Error +from auth_api.services import Entity as EntityService +from tests.utilities.factory_scenarios import TestContactInfo, TestEntityInfo, TestJwtClaims from tests.utilities.factory_utils import ( - factory_affiliation_model, factory_entity_model, factory_membership_model, factory_org_model, factory_user_model) - - -TEST_ENTITY_INFO = { - 'businessIdentifier': 'CP1234567', - 'businessNumber': '791861073BC0001', - 'name': 'Foobar, Inc.' -} - -TEST_INVALID_ENTITY_INFO = { - 'foo': 'bar' -} - -TEST_CONTACT_INFO = { - 'email': 'foo@bar.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -TEST_UPDATED_CONTACT_INFO = { - 'email': 'bar@foo.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -TEST_INVALID_CONTACT_INFO = { - 'email': 'bar' -} - -TEST_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Test', - 'lastname': 'User', - 'preferred_username': 'testuser', - 'realm_access': { - 'roles': [ - 'system' - ] - } -} - -TEST_STAFF_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Test', - 'lastname': 'User', - 'preferred_username': 'testuser', - 'realm_access': { - 'roles': [ - 'staff' - ] - } -} - -TEST_PASSCODE_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Test', - 'lastname': 'User', - 'preferred_username': 'CP1234567', - 'username': 'CP1234567', - 'realm_access': { - 'roles': [ - 'system' - ] - }, - 'loginSource': 'PASSCODE' -} - -TEST_JWT_HEADER = { - 'alg': os.getenv('JWT_OIDC_ALGORITHMS'), - 'typ': 'JWT', - 'kid': os.getenv('JWT_OIDC_AUDIENCE') -} - - -def factory_auth_header(jwt, claims): - """Produce JWT tokens for use in tests.""" - return {'Authorization': 'Bearer ' + jwt.create_jwt(claims=claims, header=TEST_JWT_HEADER)} + factory_affiliation_model, factory_auth_header, factory_entity_model, factory_membership_model, factory_org_model, + factory_user_model) def test_add_entity(client, jwt, session): # pylint:disable=unused-argument """Assert that an entity can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.system_role) + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED def test_add_entity_invalid_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that POSTing an invalid entity returns a 400.""" - headers = factory_auth_header(jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/entities', data=json.dumps(TEST_INVALID_ENTITY_INFO), + headers = factory_auth_header(jwt, claims=TestJwtClaims.system_role) + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.invalid), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST def test_add_entity_no_auth_returns_401(client, session): # pylint:disable=unused-argument """Assert that POSTing an entity without an auth header returns a 401.""" - rv = client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=None, content_type='application/json') assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED +def test_add_entity_invalid_returns_exception(client, jwt, session): # pylint:disable=unused-argument + """Assert that POSTing an invalid entity returns an exception.""" + headers = factory_auth_header(jwt, claims=TestJwtClaims.system_role) + with patch.object(EntityService, 'save_entity', side_effect=BusinessException(Error.DATA_ALREADY_EXISTS, None)): + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), + headers=headers, content_type='application/json') + assert rv.status_code == 400 + + def test_get_entity(client, jwt, session): # pylint:disable=unused-argument """Assert that an entity can be retrieved via GET.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.system_role) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) - rv = client.get('/api/v1/entities/{}'.format(TEST_ENTITY_INFO['businessIdentifier']), + rv = client.get('/api/v1/entities/{}'.format(TestEntityInfo.entity1['businessIdentifier']), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK dictionary = json.loads(rv.data) - assert dictionary['businessIdentifier'] == TEST_ENTITY_INFO['businessIdentifier'] + assert dictionary['businessIdentifier'] == TestEntityInfo.entity1['businessIdentifier'] def test_get_entity_unauthorized_user_returns_403(client, jwt, session): # pylint:disable=unused-argument """Assert that an entity can be retrieved via GET.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.system_role) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) - rv = client.get('/api/v1/entities/{}'.format(TEST_ENTITY_INFO['businessIdentifier']), + rv = client.get('/api/v1/entities/{}'.format(TestEntityInfo.entity1['businessIdentifier']), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_403_FORBIDDEN @@ -160,122 +95,120 @@ def test_get_entity_unauthorized_user_returns_403(client, jwt, session): # pyli def test_get_entity_no_auth_returns_401(client, jwt, session): # pylint:disable=unused-argument """Assert that an entity cannot be retrieved without an authorization header.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.system_role) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - rv = client.get('/api/v1/entities/{}'.format(TEST_ENTITY_INFO['businessIdentifier']), + rv = client.get('/api/v1/entities/{}'.format(TestEntityInfo.entity1['businessIdentifier']), headers=None, content_type='application/json') assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED def test_get_entity_no_entity_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that attempting to retrieve a non-existent entity returns a 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) - rv = client.get('/api/v1/entities/{}'.format(TEST_ENTITY_INFO['businessIdentifier']), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + rv = client.get('/api/v1/entities/{}'.format(TestEntityInfo.entity1['businessIdentifier']), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND def test_add_contact(client, jwt, session): # pylint:disable=unused-argument """Assert that a contact can be added to an entity.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) - rv = client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED - rv = client.post('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + rv = client.post('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED dictionary = json.loads(rv.data) assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact1['email'] def test_add_contact_invalid_format_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that adding an invalidly formatted contact returns a 400.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - rv = client.post('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_INVALID_CONTACT_INFO), content_type='application/json') + rv = client.post('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.invalid), content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST def test_add_contact_no_entity_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that adding a contact to a non-existant Entity returns 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) - rv = client.post('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + rv = client.post('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND def test_add_contact_duplicate_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that adding a duplicate contact to an Entity returns 400.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - client.post('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') - rv = client.post('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + client.post('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') + rv = client.post('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST def test_update_contact(client, jwt, session): # pylint:disable=unused-argument """Assert that a contact can be updated on an entity.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - rv = client.post('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + rv = client.post('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED - rv = client.put('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_UPDATED_CONTACT_INFO), content_type='application/json') + rv = client.put('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact2), content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK dictionary = json.loads(rv.data) assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_UPDATED_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact2['email'] def test_update_contact_invalid_format_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that updating with an invalidly formatted contact returns a 400.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - client.post('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') - rv = client.put('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_INVALID_CONTACT_INFO), content_type='application/json') + client.post('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') + rv = client.put('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.invalid), content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST def test_update_contact_no_entity_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that updating a contact on a non-existant entity returns 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) - rv = client.put('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + rv = client.put('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND def test_update_contact_missing_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that updating a non-existant contact returns 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_PASSCODE_JWT_CLAIMS) - client.post('/api/v1/entities', data=json.dumps(TEST_ENTITY_INFO), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), headers=headers, content_type='application/json') - rv = client.put('/api/v1/entities/{}/contacts'.format(TEST_ENTITY_INFO['businessIdentifier']), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + rv = client.put('/api/v1/entities/{}/contacts'.format(TestEntityInfo.entity1['businessIdentifier']), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND def test_authorizations_passcode_returns_200(client, jwt, session): # pylint:disable=unused-argument """Assert authorizations for passcode user returns 200.""" - claims = copy.deepcopy(TEST_JWT_CLAIMS) - claims['username'] = inc_number = 'CP123456789' - claims['loginSource'] = 'PASSCODE' + inc_number = 'CP1234567' - headers = factory_auth_header(jwt=jwt, claims=claims) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) rv = client.get(f'/api/v1/entities/{inc_number}/authorizations', headers=headers, content_type='application/json') @@ -283,7 +216,7 @@ def test_authorizations_passcode_returns_200(client, jwt, session): # pylint:di assert rv.json.get('orgMembership') == 'OWNER' # Test with invalid number - headers = factory_auth_header(jwt=jwt, claims=claims) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) rv = client.get('/api/v1/entities/INVALID/authorizations', headers=headers, content_type='application/json') @@ -293,12 +226,9 @@ def test_authorizations_passcode_returns_200(client, jwt, session): # pylint:di def test_authorizations_for_staff_returns_200(client, jwt, session): # pylint:disable=unused-argument """Assert authorizations for staff user returns 200.""" - claims = copy.deepcopy(TEST_JWT_CLAIMS) - claims['username'] = inc_number = 'tester' - claims['loginSource'] = '' - claims['realm_access']['roles'].append('staff') + inc_number = 'tester' - headers = factory_auth_header(jwt=jwt, claims=claims) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_role) rv = client.get(f'/api/v1/entities/{inc_number}/authorizations', headers=headers, content_type='application/json') @@ -308,12 +238,12 @@ def test_authorizations_for_staff_returns_200(client, jwt, session): # pylint:d def test_authorizations_for_affiliated_users_returns_200(client, jwt, session): # pylint:disable=unused-argument """Assert authorizations for affiliated users returns 200.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) - claims = copy.deepcopy(TEST_JWT_CLAIMS) + claims = copy.deepcopy(TestJwtClaims.passcode.value) claims['sub'] = str(user.keycloak_guid) headers = factory_auth_header(jwt=jwt, claims=claims) diff --git a/auth-api/tests/unit/api/test_invitation.py b/auth-api/tests/unit/api/test_invitation.py index fbda6ebdaf..5d95d67bdf 100644 --- a/auth-api/tests/unit/api/test_invitation.py +++ b/auth-api/tests/unit/api/test_invitation.py @@ -16,173 +16,44 @@ Test-Suite to ensure that the /invitations endpoint is working as expected. """ - import json -import os from auth_api import status as http_status from auth_api.services import Invitation as InvitationService - - -TEST_ORG_INFO = { - 'name': 'My Test Org' -} - -TEST_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Test', - 'lastname': 'User', - 'preferred_username': 'testuser', - 'realm_access': { - 'roles': [ - 'edit' - ] - } -} - -TEST_JWT_HEADER = { - 'alg': os.getenv('JWT_OIDC_ALGORITHMS'), - 'typ': 'JWT', - 'kid': os.getenv('JWT_OIDC_AUDIENCE') -} - - -def factory_auth_header(jwt, claims): - """Produce JWT tokens for use in tests.""" - return {'Authorization': 'Bearer ' + jwt.create_jwt(claims=claims, header=TEST_JWT_HEADER)} +from tests.utilities.factory_scenarios import TestJwtClaims, TestOrgInfo +from tests.utilities.factory_utils import factory_auth_header, factory_invitation def test_add_invitation(client, jwt, session): # pylint:disable=unused-argument """Assert that an invitation can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id=org_id)), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED def test_add_invitation_invalid(client, jwt, session): # pylint:disable=unused-argument """Assert that POSTing an invalid invitation returns a 400.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), - headers=headers, content_type='application/json') - - new_invitation = { - 'recipientEmail': 'test@abc.com' - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id=None)), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST -def test_get_invitations_by_user(client, jwt, session): # pylint:disable=unused-argument - """Assert that an invitation by a user can be retrieved.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), - headers=headers, content_type='application/json') - dictionary = json.loads(rv.data) - org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), - headers=headers, content_type='application/json') - rv = client.get('/api/v1/invitations', headers=headers, content_type='application/json') - invitation_dict = json.loads(rv.data) - assert rv.status_code == http_status.HTTP_200_OK - assert invitation_dict['invitations'] - assert len(invitation_dict['invitations']) == 1 - - -def test_get_invitations_by_valid_status(client, jwt, session): # pylint:disable=unused-argument - """Assert that an invitation by a user can be retrieved.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), - headers=headers, content_type='application/json') - dictionary = json.loads(rv.data) - org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), - headers=headers, content_type='application/json') - rv = client.get('/api/v1/invitations?status=PENDING', headers=headers, content_type='application/json') - invitation_dict = json.loads(rv.data) - assert rv.status_code == http_status.HTTP_200_OK - assert invitation_dict['invitations'] - assert len(invitation_dict['invitations']) == 1 - - -def test_get_invitations_by_invalid_status(client, jwt, session): # pylint:disable=unused-argument - """Assert that an invitation by a user can be retrieved.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), - headers=headers, content_type='application/json') - dictionary = json.loads(rv.data) - org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), - headers=headers, content_type='application/json') - rv = client.get('/api/v1/invitations?status=TEST', headers=headers, content_type='application/json') - assert rv.status_code == http_status.HTTP_404_NOT_FOUND - - def test_get_invitations_by_id(client, jwt, session): # pylint:disable=unused-argument """Assert that an invitation can be retrieved.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id=org_id)), headers=headers, content_type='application/json') invitation_dictionary = json.loads(rv.data) invitation_id = invitation_dictionary['id'] @@ -192,22 +63,13 @@ def test_get_invitations_by_id(client, jwt, session): # pylint:disable=unused-a def test_delete_invitation(client, jwt, session): # pylint:disable=unused-argument """Assert that an invitation can be deleted.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id=org_id)), headers=headers, content_type='application/json') invitation_dictionary = json.loads(rv.data) invitation_id = invitation_dictionary['id'] @@ -221,22 +83,13 @@ def test_delete_invitation(client, jwt, session): # pylint:disable=unused-argum def test_update_invitation(client, jwt, session): # pylint:disable=unused-argument """Assert that an invitation can be updated.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id=org_id)), headers=headers, content_type='application/json') invitation_dictionary = json.loads(rv.data) invitation_id = invitation_dictionary['id'] @@ -250,22 +103,13 @@ def test_update_invitation(client, jwt, session): # pylint:disable=unused-argum def test_validate_token(client, jwt, session): # pylint:disable=unused-argument """Assert that a token is valid.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id=org_id)), headers=headers, content_type='application/json') invitation_dictionary = json.loads(rv.data) invitation_id = invitation_dictionary['id'] @@ -277,22 +121,13 @@ def test_validate_token(client, jwt, session): # pylint:disable=unused-argument def test_accept_invitation(client, jwt, session): # pylint:disable=unused-argument """Assert that an invitation can be accepted.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] - new_invitation = { - 'recipientEmail': 'test@abc.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } - rv = client.post('/api/v1/invitations', data=json.dumps(new_invitation), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id=org_id)), headers=headers, content_type='application/json') invitation_dictionary = json.loads(rv.data) invitation_id = invitation_dictionary['id'] diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index 293ff91e91..54112bc8ce 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -18,108 +18,71 @@ """ import json -import os +from unittest.mock import patch from auth_api import status as http_status - - -TEST_ORG_INFO = { - 'name': 'My Test Org' -} - -TEST_INVALID_ORG_INFO = { - 'foo': 'bar' -} - -TEST_CONTACT_INFO = { - 'email': 'foo@bar.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -TEST_UPDATED_CONTACT_INFO = { - 'email': 'bar@foo.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -TEST_INVALID_CONTACT_INFO = { - 'email': 'bar' -} - -TEST_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Test', - 'lastname': 'User', - 'preferred_username': 'testuser', - 'realm_access': { - 'roles': [ - 'edit' - ] - } -} -TEST_STAFF_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Test', - 'lastname': 'User', - 'preferred_username': 'testuser', - 'realm_access': { - 'roles': [ - 'staff' - ] - } -} - -TEST_JWT_HEADER = { - 'alg': os.getenv('JWT_OIDC_ALGORITHMS'), - 'typ': 'JWT', - 'kid': os.getenv('JWT_OIDC_AUDIENCE') -} - - -def factory_auth_header(jwt, claims): - """Produce JWT tokens for use in tests.""" - return {'Authorization': 'Bearer ' + jwt.create_jwt(claims=claims, header=TEST_JWT_HEADER)} - - -def factory_invite(org_id, email): - """Produce an invite for the given org and email.""" - return { - 'recipientEmail': email, - 'sentDate': '2019-09-09', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_id - } - ] - } +from auth_api.exceptions import BusinessException +from auth_api.exceptions.errors import Error +from auth_api.services import Affiliation as AffiliationService +from auth_api.services import Invitation as InvitationService +from auth_api.services import Org as OrgService +from auth_api.services import User as UserService +from tests.utilities.factory_scenarios import ( + TestAffliationInfo, TestContactInfo, TestEntityInfo, TestJwtClaims, TestOrgInfo) +from tests.utilities.factory_utils import factory_auth_header, factory_invitation def test_add_org(client, jwt, session): # pylint:disable=unused-argument """Assert that an org can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED def test_add_org_invalid_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that POSTing an invalid org returns a 400.""" - headers = factory_auth_header(jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_INVALID_ORG_INFO), + headers = factory_auth_header(jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.invalid), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST +def test_add_org_invalid_returns_401(client, jwt, session): # pylint:disable=unused-argument + """Assert that POSTing an invalid org returns a 401.""" + headers = factory_auth_header(jwt, claims=TestJwtClaims.view_role) + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED + + +def test_add_org_invalid_user_returns_401(client, jwt, session): # pylint:disable=unused-argument + """Assert that POSTing an org with invalid user returns a 401.""" + headers = factory_auth_header(jwt, claims=TestJwtClaims.edit_role) + + with patch.object(UserService, 'find_by_jwt_token', return_value=None): + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED + + +def test_add_org_invalid_returns_exception(client, jwt, session): # pylint:disable=unused-argument + """Assert that POSTing an invalid org returns an exception.""" + headers = factory_auth_header(jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + + with patch.object(OrgService, 'create_org', side_effect=BusinessException(Error.DATA_ALREADY_EXISTS, None)): + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + assert rv.status_code == 400 + + def test_get_org(client, jwt, session): # pylint:disable=unused-argument """Assert that an org can be retrieved via GET.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] @@ -133,9 +96,9 @@ def test_get_org(client, jwt, session): # pylint:disable=unused-argument def test_get_org_no_auth_returns_401(client, jwt, session): # pylint:disable=unused-argument """Assert that an org cannot be retrieved without an authorization header.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] @@ -146,133 +109,228 @@ def test_get_org_no_auth_returns_401(client, jwt, session): # pylint:disable=un def test_get_org_no_org_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that attempting to retrieve a non-existent org returns a 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) - rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.get('/api/v1/orgs/{}'.format(999), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND +def test_update_org(client, jwt, session): # pylint:disable=unused-argument + """Assert that an org can be updated via PUT.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + rv = client.put('/api/v1/orgs/{}'.format(org_id), data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_200_OK + dictionary = json.loads(rv.data) + assert dictionary['id'] == org_id + + +def test_update_org_returns_400(client, jwt, session): # pylint:disable=unused-argument + """Assert that an org can not be updated and return 400 error via PUT.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + rv = client.put('/api/v1/orgs/{}'.format(org_id), data=json.dumps(TestOrgInfo.invalid), + headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_400_BAD_REQUEST + + +def test_update_org_no_org_returns_404(client, jwt, session): # pylint:disable=unused-argument + """Assert that attempting to update a non-existent org returns a 404.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.put('/api/v1/orgs/{}'.format(999), data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_404_NOT_FOUND + + +def test_update_org_returns_exception(client, jwt, session): # pylint:disable=unused-argument + """Assert that attempting to update a non-existent org returns an exception.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + with patch.object(OrgService, 'update_org', side_effect=BusinessException(Error.DATA_ALREADY_EXISTS, None)): + rv = client.put('/api/v1/orgs/{}'.format(org_id), data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + assert rv.status_code == 400 + + def test_add_contact(client, jwt, session): # pylint:disable=unused-argument """Assert that a contact can be added to an org.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] rv = client.post('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED dictionary = json.loads(rv.data) assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact1['email'] def test_add_contact_invalid_format_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that adding an invalidly formatted contact returns a 400.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] rv = client.post('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_INVALID_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.invalid), content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST def test_add_contact_no_org_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that adding a contact to a non-existant org returns 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/orgs/{}/contacts'.format(99), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND def test_add_contact_duplicate_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that adding a duplicate contact to an org returns 400.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] client.post('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') rv = client.post('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST def test_update_contact(client, jwt, session): # pylint:disable=unused-argument """Assert that a contact can be updated on an org.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] rv = client.post('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED rv = client.put('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_UPDATED_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact2), content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK dictionary = json.loads(rv.data) assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_UPDATED_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact2['email'] def test_update_contact_invalid_format_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that updating with an invalidly formatted contact returns a 400.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] client.post('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') rv = client.put('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_INVALID_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.invalid), content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST def test_update_contact_no_org_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that updating a contact on a non-existant entity returns 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.put('/api/v1/orgs/{}/contacts'.format(99), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND def test_update_contact_missing_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that updating a non-existant contact returns 404.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] rv = client.put('/api/v1/orgs/{}/contacts'.format(org_id), - headers=headers, data=json.dumps(TEST_CONTACT_INFO), content_type='application/json') + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') + assert rv.status_code == http_status.HTTP_404_NOT_FOUND + + +def test_delete_contact(client, jwt, session): # pylint:disable=unused-argument + """Assert that a contact can be deleted on an org.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + rv = client.post('/api/v1/orgs/{}/contacts'.format(org_id), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') + assert rv.status_code == http_status.HTTP_201_CREATED + + rv = client.delete('/api/v1/orgs/{}/contacts'.format(org_id), + headers=headers, data=json.dumps(TestContactInfo.contact2), content_type='application/json') + + assert rv.status_code == http_status.HTTP_200_OK + dictionary = json.loads(rv.data) + assert len(dictionary['contacts']) == 0 + + +def test_delete_contact_no_org_returns_404(client, jwt, session): # pylint:disable=unused-argument + """Assert that deleting a contact on a non-existant entity returns 404.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.delete('/api/v1/orgs/{}/contacts'.format(99), + headers=headers, data=json.dumps(TestContactInfo.contact1), content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND +def test_delete_contact_returns_exception(client, jwt, session): # pylint:disable=unused-argument + """Assert that attempting to delete an org returns an exception.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + with patch.object(OrgService, 'delete_contact', side_effect=BusinessException(Error.DATA_ALREADY_EXISTS, None)): + rv = client.delete('/api/v1/orgs/{}/contacts'.format(org_id), headers=headers, content_type='application/json') + assert rv.status_code == 400 + + def test_get_members(client, jwt, session): # pylint:disable=unused-argument """Assert that a list of members for an org can be retrieved.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] @@ -289,17 +347,17 @@ def test_get_members(client, jwt, session): # pylint:disable=unused-argument def test_get_invitations(client, jwt, session): # pylint:disable=unused-argument """Assert that a list of invitations for an org can be retrieved.""" - headers = factory_auth_header(jwt=jwt, claims=TEST_JWT_CLAIMS) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/orgs', data=json.dumps(TEST_ORG_INFO), + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), headers=headers, content_type='application/json') dictionary = json.loads(rv.data) org_id = dictionary['id'] - rv = client.post('/api/v1/invitations', data=json.dumps(factory_invite(org_id, 'abc123@email.com')), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id, 'abc123@email.com')), headers=headers, content_type='application/json') - rv = client.post('/api/v1/invitations', data=json.dumps(factory_invite(org_id, 'xyz456@email.com')), + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id, 'xyz456@email.com')), headers=headers, content_type='application/json') rv = client.get('/api/v1/orgs/{}/invitations'.format(org_id), @@ -311,3 +369,146 @@ def test_get_invitations(client, jwt, session): # pylint:disable=unused-argumen assert len(dictionary['invitations']) == 2 assert dictionary['invitations'][0]['recipientEmail'] == 'abc123@email.com' assert dictionary['invitations'][1]['recipientEmail'] == 'xyz456@email.com' + + +def test_update_member(client, jwt, session, auth_mock): # pylint:disable=unused-argument + """Assert that a member of an org can have their role updated.""" + # Set up: create/login user, create org + headers_invitee = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers_invitee, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers_invitee, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + # Invite a user to the org + rv = client.post('/api/v1/invitations', data=json.dumps(factory_invitation(org_id, 'abc123@email.com')), + headers=headers_invitee, content_type='application/json') + dictionary = json.loads(rv.data) + invitation_id = dictionary['id'] + invitation_id_token = InvitationService.generate_confirmation_token(invitation_id) + + # Create/login as invited user + headers_invited = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role_2) + rv = client.post('/api/v1/users', headers=headers_invited, content_type='application/json') + + # Accept invite as invited user + rv = client.put('/api/v1/invitations/tokens/{}'.format(invitation_id_token), + headers=headers_invited, content_type='application/json') + + assert rv.status_code == http_status.HTTP_200_OK + dictionary = json.loads(rv.data) + assert dictionary['status'] == 'ACCEPTED' + + # Get members for the org as invitee and assert length of 2 + rv = client.get('/api/v1/orgs/{}/members'.format(org_id), headers=headers_invitee) + assert rv.status_code == http_status.HTTP_200_OK + dictionary = json.loads(rv.data) + assert dictionary['members'] + assert len(dictionary['members']) == 2 + + # Find the newly added member + new_member = list(filter(lambda x: x['user']['username'] == TestJwtClaims.edit_role_2['preferred_username'], + dictionary['members'])) + assert len(new_member) == 1 + assert new_member[0]['membershipTypeCode'] == 'MEMBER' + member_id = new_member[0]['id'] + + # Update the new member + rv = client.patch('/api/v1/orgs/{}/members/{}'.format(org_id, member_id), headers=headers_invitee, + data=json.dumps({'role': 'ADMIN'}), content_type='application/json') + assert rv.status_code == http_status.HTTP_200_OK + dictionary = json.loads(rv.data) + assert dictionary['membershipTypeCode'] == 'ADMIN' + + +def test_add_affiliation(client, jwt, session): # pylint:disable=unused-argument + """Assert that a contact can be added to an org.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity_lear_mock), + headers=headers, content_type='application/json') + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + rv = client.post('/api/v1/orgs/{}/affiliations'.format(org_id), headers=headers, + data=json.dumps(TestAffliationInfo.affiliation3), content_type='application/json') + assert rv.status_code == http_status.HTTP_201_CREATED + dictionary = json.loads(rv.data) + assert dictionary['org']['id'] == org_id + + +def test_add_affiliation_invalid_format_returns_400(client, jwt, session): # pylint:disable=unused-argument + """Assert that adding an invalidly formatted affiliations returns a 400.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + rv = client.post('/api/v1/orgs/{}/affiliations'.format(org_id), + headers=headers, data=json.dumps(TestAffliationInfo.invalid), content_type='application/json') + assert rv.status_code == http_status.HTTP_400_BAD_REQUEST + + +def test_add_affiliation_no_org_returns_404(client, jwt, session): # pylint:disable=unused-argument + """Assert that adding a contact to a non-existant org returns 404.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/orgs/{}/affiliations'.format(99), headers=headers, + data=json.dumps(TestAffliationInfo.affliation1), content_type='application/json') + assert rv.status_code == http_status.HTTP_404_NOT_FOUND + + +def test_add_affiliation_returns_exception(client, jwt, session): # pylint:disable=unused-argument + """Assert that attempting to delete an affiliation returns an exception.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity1), + headers=headers, content_type='application/json') + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + with patch.object(AffiliationService, 'create_affiliation', + side_effect=BusinessException(Error.DATA_ALREADY_EXISTS, None)): + rv = client.post('/api/v1/orgs/{}/affiliations'.format(org_id), + data=json.dumps(TestAffliationInfo.affliation1), + headers=headers, + content_type='application/json') + assert rv.status_code == 400 + + +def test_get_affiliations(client, jwt, session): # pylint:disable=unused-argument + """Assert that a list of affiliation for an org can be retrieved.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity_lear_mock), + headers=headers, content_type='application/json') + rv = client.post('/api/v1/entities', data=json.dumps(TestEntityInfo.entity_lear_mock2), + headers=headers, content_type='application/json') + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + rv = client.post('/api/v1/orgs', data=json.dumps(TestOrgInfo.org1), + headers=headers, content_type='application/json') + dictionary = json.loads(rv.data) + org_id = dictionary['id'] + + rv = client.post('/api/v1/orgs/{}/affiliations'.format(org_id), + data=json.dumps(TestAffliationInfo.affiliation3), + headers=headers, + content_type='application/json') + rv = client.post('/api/v1/orgs/{}/affiliations'.format(org_id), + data=json.dumps(TestAffliationInfo.affiliation4), + headers=headers, + content_type='application/json') + + rv = client.get('/api/v1/orgs/{}/affiliations'.format(org_id), headers=headers) + assert rv.status_code == http_status.HTTP_200_OK + affiliations = json.loads(rv.data) + assert affiliations[0]['businessIdentifier'] == TestEntityInfo.entity_lear_mock['businessIdentifier'] + assert affiliations[1]['businessIdentifier'] == TestEntityInfo.entity_lear_mock2['businessIdentifier'] diff --git a/auth-api/tests/unit/api/test_user.py b/auth-api/tests/unit/api/test_user.py index f46932588d..87a1e1cc57 100644 --- a/auth-api/tests/unit/api/test_user.py +++ b/auth-api/tests/unit/api/test_user.py @@ -16,107 +16,22 @@ Test-Suite to ensure that the /users endpoint is working as expected. """ - import copy import json -import os +import uuid from auth_api import status as http_status from auth_api.exceptions.errors import Error from tests import skip_in_pod +from tests.utilities.factory_scenarios import TestContactInfo, TestJwtClaims, TestOrgInfo from tests.utilities.factory_utils import ( - factory_affiliation_model, factory_entity_model, factory_membership_model, factory_org_model, factory_user_model, - uuid) - - -TEST_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Test', - 'lastname': 'User', - 'preferred_username': 'testuser', - 'realm_access': { - 'roles': [ - 'edit' - ] - } -} - -TEST_JWT_CLAIMS_2 = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302065', - 'firstname': 'Test', - 'lastname': 'User 2', - 'preferred_username': 'testuser2', - 'realm_access': { - 'roles': [ - ] - } -} - -TEST_JWT_INVALID_CLAIMS = { - 'sub': 'barfoo', - 'firstname': 'Trouble', - 'lastname': 'Maker', - 'preferred_username': 'troublemaker' -} - -TEST_STAFF_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302066', - 'firstname': 'Test_Staff', - 'lastname': 'User', - 'preferred_username': 'testuser', - 'realm_access': { - 'roles': [ - 'staff' - ] - } -} - -UPDATED_TEST_JWT_CLAIMS = { - 'iss': os.getenv('JWT_OIDC_ISSUER'), - 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', - 'firstname': 'Updated_Test', - 'lastname': 'User', - 'username': 'testuser', - 'realm_access': { - 'roles': [ - ] - } -} - -TEST_JWT_HEADER = { - 'alg': os.getenv('JWT_OIDC_ALGORITHMS'), - 'typ': 'JWT', - 'kid': os.getenv('JWT_OIDC_AUDIENCE') -} - -TEST_CONTACT = { - 'email': 'foo@bar.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -UPDATED_TEST_CONTACT = { - 'email': 'bar@foo.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -INVALID_TEST_CONTACT = { - 'email': 'bar' -} - -TEST_ORG_INFO = { - 'name': 'My Test Org' -} + factory_affiliation_model, factory_auth_header, factory_entity_model, factory_membership_model, factory_org_model, + factory_user_model) def test_add_user(client, jwt, session): # pylint:disable=unused-argument """Assert that a user can be POSTed.""" - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED @@ -130,42 +45,86 @@ def test_add_user_no_token_returns_401(client, session): # pylint:disable=unuse @skip_in_pod def test_add_user_invalid_token_returns_401(client, jwt, session): # pylint:disable=unused-argument """Assert that POSTing a user with an invalid token returns a 401.""" - token = jwt.create_jwt(claims=TEST_JWT_INVALID_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.invalid) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED def test_update_user(client, jwt, session): # pylint:disable=unused-argument """Assert that a POST to an existing user updates that user.""" - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED user = json.loads(rv.data) assert user['firstname'] == 'Test' # post token with updated claims - token = jwt.create_jwt(claims=UPDATED_TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.updated_test) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED user = json.loads(rv.data) assert user['firstname'] == 'Updated_Test' +def test_update_user_terms_of_use(client, jwt, session): # pylint:disable=unused-argument + """Assert that a PATCH to an existing user updates that user.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_201_CREATED + user = json.loads(rv.data) + assert user['firstname'] == 'Test' + + # post token with updated claims + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.updated_test) + input_data = json.dumps({'termsversion': 1, 'istermsaccepted': True}) + rv = client.patch('/api/v1/users/@me', headers=headers, + data=input_data, content_type='application/json') + assert rv.status_code == http_status.HTTP_200_OK + user = json.loads(rv.data) + assert user['terms_of_use_version'] == 1 + + +def test_update_user_terms_of_use_invalid_input(client, jwt, session): # pylint:disable=unused-argument + """Assert that a PATCH to an existing user updates that user.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_201_CREATED + user = json.loads(rv.data) + assert user['firstname'] == 'Test' + + # post token with updated claims + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.updated_test) + input_data = json.dumps({'invalid': True}) + rv = client.patch('/api/v1/users/@me', headers=headers, + data=input_data, content_type='application/json') + assert rv.status_code == http_status.HTTP_400_BAD_REQUEST + + +def test_update_user_terms_of_use_no_jwt(client, jwt, session): # pylint:disable=unused-argument + """Assert that a PATCH to an existing user updates that user.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) + rv = client.post('/api/v1/users', headers=headers, content_type='application/json') + assert rv.status_code == http_status.HTTP_201_CREATED + user = json.loads(rv.data) + assert user['firstname'] == 'Test' + + # post token with updated claims + input_data = json.dumps({'invalid': True}) + rv = client.patch('/api/v1/users/@me', + data=input_data, content_type='application/json') + assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED + + def test_staff_get_user(client, jwt, session): # pylint:disable=unused-argument """Assert that a staff user can GET a user by id.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED # GET the test user as a staff user - token = jwt.create_jwt(claims=TEST_STAFF_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} - rv = client.get('/api/v1/users/{}'.format(TEST_JWT_CLAIMS['preferred_username']), + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_role) + rv = client.get('/api/v1/users/{}'.format(TestJwtClaims.edit_role['preferred_username']), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK user = json.loads(rv.data) @@ -174,8 +133,7 @@ def test_staff_get_user(client, jwt, session): # pylint:disable=unused-argument def test_staff_get_user_invalid_id_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that a staff user can GET a user by id.""" - token = jwt.create_jwt(claims=TEST_STAFF_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_role) rv = client.get('/api/v1/users/{}'.format('SOME_USER'), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND @@ -183,27 +141,24 @@ def test_staff_get_user_invalid_id_returns_404(client, jwt, session): # pylint: def test_staff_search_users(client, jwt, session): # pylint:disable=unused-argument """Assert that a staff user can GET a list of users with search parameters.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED # POST a second test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS_2, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.no_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED # Search on all users as a staff user - token = jwt.create_jwt(claims=TEST_STAFF_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_role) rv = client.get('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK users = json.loads(rv.data) assert len(users) == 2 # Search on users with a search parameter - rv = client.get('/api/v1/users?lastname={}'.format(TEST_JWT_CLAIMS_2['lastname']), + rv = client.get('/api/v1/users?lastname={}'.format(TestJwtClaims.no_role['lastname']), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK users = json.loads(rv.data) @@ -213,8 +168,7 @@ def test_staff_search_users(client, jwt, session): # pylint:disable=unused-argu def test_get_user(client, jwt, session): # pylint:disable=unused-argument """Assert that a user can retrieve their own profile.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED rv = client.get('/api/v1/users/@me', headers=headers, content_type='application/json') @@ -231,8 +185,7 @@ def test_get_user_returns_401(client, session): # pylint:disable=unused-argumen def test_get_user_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that the endpoint returns 404 when user is not found.""" - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.get('/api/v1/users/@me', headers=headers, content_type='application/json') assert rv.status_code == Error.DATA_NOT_FOUND.status_code @@ -240,12 +193,11 @@ def test_get_user_returns_404(client, jwt, session): # pylint:disable=unused-ar def test_add_contact(client, jwt, session): # pylint:disable=unused-argument """Assert that a contact can be added (POST) to an existing user.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') # POST a contact to test user - rv = client.post('/api/v1/users/contacts', data=json.dumps(TEST_CONTACT), + rv = client.post('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED user = json.loads(rv.data) @@ -255,7 +207,7 @@ def test_add_contact(client, jwt, session): # pylint:disable=unused-argument def test_add_contact_no_token_returns_401(client, session): # pylint:disable=unused-argument """Assert that adding a contact without providing a token returns a 401.""" - rv = client.post('/api/v1/users/contacts', data=json.dumps(TEST_CONTACT), + rv = client.post('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact1), headers=None, content_type='application/json') assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED @@ -263,11 +215,10 @@ def test_add_contact_no_token_returns_401(client, session): # pylint:disable=un def test_add_contact_invalid_format_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that adding a contact in an invalid format returns a 400.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.post('/api/v1/users/contacts', data=json.dumps(INVALID_TEST_CONTACT), + rv = client.post('/api/v1/users/contacts', data=json.dumps(TestContactInfo.invalid), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST @@ -275,16 +226,15 @@ def test_add_contact_invalid_format_returns_400(client, jwt, session): # pylint def test_add_contact_duplicate_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that adding a contact for a user who already has a contact returns a 400.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') # POST a contact to test user - rv = client.post('/api/v1/users/contacts', data=json.dumps(TEST_CONTACT), + rv = client.post('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED - rv = client.post('/api/v1/users/contacts', data=json.dumps(UPDATED_TEST_CONTACT), + rv = client.post('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact2), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST @@ -292,17 +242,16 @@ def test_add_contact_duplicate_returns_400(client, jwt, session): # pylint:disa def test_update_contact(client, jwt, session): # pylint:disable=unused-argument, invalid-name """Assert that a contact can be updated (PUT) on an existing user.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') # POST a contact to test user - rv = client.post('/api/v1/users/contacts', data=json.dumps(TEST_CONTACT), + rv = client.post('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED # PUT a contact on the same user - rv = client.put('/api/v1/users/contacts', data=json.dumps(UPDATED_TEST_CONTACT), + rv = client.put('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact2), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK user = json.loads(rv.data) @@ -312,7 +261,7 @@ def test_update_contact(client, jwt, session): # pylint:disable=unused-argument def test_update_contact_no_token_returns_401(client, session): # pylint:disable=unused-argument """Assert that updating a contact without providing a token returns a 401.""" - rv = client.put('/api/v1/users/contacts', data=json.dumps(UPDATED_TEST_CONTACT), + rv = client.put('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact2), headers=None, content_type='application/json') assert rv.status_code == http_status.HTTP_401_UNAUTHORIZED @@ -320,11 +269,10 @@ def test_update_contact_no_token_returns_401(client, session): # pylint:disable def test_update_contact_invalid_format_returns_400(client, jwt, session): # pylint:disable=unused-argument """Assert that adding a contact in an invalid format returns a 400.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') - rv = client.put('/api/v1/users/contacts', data=json.dumps(INVALID_TEST_CONTACT), + rv = client.put('/api/v1/users/contacts', data=json.dumps(TestContactInfo.invalid), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_400_BAD_REQUEST @@ -332,12 +280,11 @@ def test_update_contact_invalid_format_returns_400(client, jwt, session): # pyl def test_update_contact_missing_contact_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert that updating a contact for a non-existent user returns a 404.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') # PUT a contact to test user - rv = client.put('/api/v1/users/contacts', data=json.dumps(TEST_CONTACT), + rv = client.put('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_404_NOT_FOUND @@ -345,12 +292,11 @@ def test_update_contact_missing_contact_returns_404(client, jwt, session): # py def test_delete_contact(client, jwt, session): # pylint:disable=unused-argument, invalid-name """Assert that a contact can be deleted on an existing user.""" # POST a test user - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') # POST a contact to test user - rv = client.post('/api/v1/users/contacts', data=json.dumps(TEST_CONTACT), + rv = client.post('/api/v1/users/contacts', data=json.dumps(TestContactInfo.contact1), headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_201_CREATED @@ -369,8 +315,7 @@ def test_delete_contact_no_token_returns_401(client, session): # pylint:disable def test_delete_contact_no_contact_returns_404(client, jwt, session): # pylint:disable=unused-argument, invalid-name """Assert that deleting a contact that doesn't exist returns a 404.""" - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') rv = client.delete('/api/v1/users/contacts', headers=headers, content_type='application/json') @@ -379,12 +324,12 @@ def test_delete_contact_no_contact_returns_404(client, jwt, session): # pylint: def test_get_orgs_for_user(client, jwt, session): # pylint:disable=unused-argument """Assert that retrieving a list of orgs for a user functions.""" - token = jwt.create_jwt(claims=TEST_JWT_CLAIMS, header=TEST_JWT_HEADER) - headers = {'Authorization': 'Bearer ' + token} + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.edit_role) rv = client.post('/api/v1/users', headers=headers, content_type='application/json') # Add an org - the current user should be auto-added as an OWNER - rv = client.post('/api/v1/orgs', headers=headers, data=json.dumps(TEST_ORG_INFO), content_type='application/json') + rv = client.post('/api/v1/orgs', headers=headers, data=json.dumps(TestOrgInfo.org1), + content_type='application/json') rv = client.get('/api/v1/users/orgs', headers=headers) @@ -393,22 +338,21 @@ def test_get_orgs_for_user(client, jwt, session): # pylint:disable=unused-argum response = json.loads(rv.data) assert response['orgs'] assert len(response['orgs']) == 1 - assert response['orgs'][0]['name'] == TEST_ORG_INFO['name'] + assert response['orgs'][0]['name'] == TestOrgInfo.org1['name'] def test_user_authorizations_returns_200(client, jwt, session): # pylint:disable=unused-argument """Assert authorizations for users returns 200.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) - claims = copy.deepcopy(TEST_JWT_CLAIMS) + claims = copy.deepcopy(TestJwtClaims.edit_role.value) claims['sub'] = str(user.keycloak_guid) - token = jwt.create_jwt(header=TEST_JWT_HEADER, claims=claims) - headers = {'Authorization': f'Bearer {token}'} + headers = factory_auth_header(jwt=jwt, claims=claims) rv = client.get('/api/v1/users/authorizations', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK @@ -416,9 +360,7 @@ def test_user_authorizations_returns_200(client, jwt, session): # pylint:disabl # Test with invalid user claims['sub'] = str(uuid.uuid4()) - token = jwt.create_jwt(header=TEST_JWT_HEADER, claims=claims) - headers = {'Authorization': f'Bearer {token}'} - + headers = factory_auth_header(jwt=jwt, claims=claims) rv = client.get('/api/v1/users/authorizations', headers=headers, content_type='application/json') assert rv.status_code == http_status.HTTP_200_OK diff --git a/auth-api/tests/unit/conftest.py b/auth-api/tests/unit/conftest.py index 9f849efc87..6e44494317 100644 --- a/auth-api/tests/unit/conftest.py +++ b/auth-api/tests/unit/conftest.py @@ -158,3 +158,4 @@ def auth_mock(monkeypatch): """Mock check_auth.""" monkeypatch.setattr('auth_api.services.entity.check_auth', lambda *args, **kwargs: None) monkeypatch.setattr('auth_api.services.org.check_auth', lambda *args, **kwargs: None) + monkeypatch.setattr('auth_api.services.invitation.check_auth', lambda *args, **kwargs: None) diff --git a/auth-api/tests/unit/models/test_documents.py b/auth-api/tests/unit/models/test_documents.py new file mode 100644 index 0000000000..3339d0a370 --- /dev/null +++ b/auth-api/tests/unit/models/test_documents.py @@ -0,0 +1,43 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests to assure the Documents Class. + +Test-Suite to ensure that the Documents Class is working as expected. +""" + +from auth_api.models import Documents + + +def test_documents_with_insert(session): + """Assert that a Documents can be stored in the service. + + Start with a blank document. + """ + doc_latest = Documents.fetch_latest_document_by_type('termsofuse') + assert doc_latest.version_id == 1 + + +def test_documents_with_insert_some_type(session): + """Assert that a Documents can be stored in the service. + + Start with a blank document. + """ + html_content = '' + doc = Documents(version_id=2, type='sometype', content=html_content) + session.add(doc) + session.commit() + + doc_latest = Documents.fetch_latest_document_by_type('sometype') + assert doc_latest.content == html_content diff --git a/auth-api/tests/unit/models/test_entity.py b/auth-api/tests/unit/models/test_entity.py index eeb20b5d7c..d6134cf79b 100644 --- a/auth-api/tests/unit/models/test_entity.py +++ b/auth-api/tests/unit/models/test_entity.py @@ -38,3 +38,24 @@ def test_entity_find_by_business_id(session): result_entity = EntityModel.find_by_business_identifier(business_identifier=business_id) assert result_entity.id is not None + + +def test_create_from_dict(session): # pylint:disable=unused-argument + """Assert that an Entity can be created from schema.""" + updated_entity_info = { + 'businessIdentifier': 'CP1234567', + 'businessNumber': '791861073BC0001', + 'passCode': '9898989', + 'name': 'Barfoo, Inc.' + } + + result_entity = EntityModel.create_from_dict(updated_entity_info) + + assert result_entity.id is not None + + +def test_create_from_dict_no_schema(session): # pylint:disable=unused-argument + """Assert that an Entity can not be created without schema.""" + result_entity = EntityModel.create_from_dict(None) + + assert result_entity is None diff --git a/auth-api/tests/unit/models/test_invitation.py b/auth-api/tests/unit/models/test_invitation.py index 3a8314fa76..42b9122457 100644 --- a/auth-api/tests/unit/models/test_invitation.py +++ b/auth-api/tests/unit/models/test_invitation.py @@ -15,8 +15,7 @@ Test suite to ensure that the model routines are working as expected. """ - -from _datetime import datetime +from _datetime import datetime, timedelta from auth_api.models import Invitation as InvitationModel from auth_api.models import InvitationMembership as InvitationMembershipModel @@ -25,9 +24,10 @@ from auth_api.models import OrgType as OrgTypeModel from auth_api.models import PaymentType as PaymentTypeModel from auth_api.models import User +from config import get_named_config -def factory_invitation_model(session, status): +def factory_invitation_model(session, status, sent_date=datetime.now()): """Produce a templated invitation model.""" user = User(username='CP1234567', roles='{edit, uma_authorization, staff}', @@ -58,7 +58,7 @@ def factory_invitation_model(session, status): invitation = InvitationModel() invitation.recipient_email = 'abc@test.com' invitation.sender = user - invitation.sent_date = datetime.now() + invitation.sent_date = sent_date invitation.invitation_status_code = status invitation_membership = InvitationMembershipModel() @@ -153,3 +153,73 @@ def test_invitations_by_status(session): # pylint:disable=unused-argument retrieved_invitation = InvitationModel.find_invitations_by_status(invitation.sender_id, 'FAILED') assert len(retrieved_invitation) == 0 + + +def test_create_from_dict(session): # pylint:disable=unused-argument + """Assert that an Entity can be created from schema.""" + user = User(username='CP1234567', + roles='{edit, uma_authorization, staff}', + keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + + session.add(user) + session.commit() + + org_type = OrgTypeModel(code='TEST', desc='Test') + session.add(org_type) + session.commit() + + org_status = OrgStatusModel(code='TEST', desc='Test') + session.add(org_status) + session.commit() + + preferred_payment = PaymentTypeModel(code='TEST', desc='Test') + session.add(preferred_payment) + session.commit() + + org = OrgModel() + org.name = 'Test Org' + org.org_type = org_type + org.org_status = org_status + org.preferred_payment = preferred_payment + org.save() + + invitation_info = { + 'recipientEmail': 'abc.test@gmail.com', + 'membership': [ + { + 'membershipType': 'MEMBER', + 'orgId': org.id + } + ] + } + result_invitation = InvitationModel.create_from_dict(invitation_info, user.id) + + assert result_invitation.id is not None + + +def test_create_from_dict_no_schema(session): # pylint:disable=unused-argument + """Assert that an Entity can not be created without schema.""" + user = User(username='CP1234567', + roles='{edit, uma_authorization, staff}', + keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + + session.add(user) + session.commit() + + result_invitation = InvitationModel.create_from_dict(None, user.id) + + assert result_invitation is None + + +def test_invitations_status_expiry(session): # pylint:disable=unused-argument + """Assert can set the status from PENDING to EXPIRED.""" + sent_date = datetime.now() - timedelta(days=int(get_named_config().TOKEN_EXPIRY_PERIOD) + 1) + invitation = factory_invitation_model(session=session, + status='PENDING', + sent_date=sent_date) + session.add(invitation) + session.commit() + + result: str = invitation.status + + assert result == 'EXPIRED' diff --git a/auth-api/tests/unit/models/test_org.py b/auth-api/tests/unit/models/test_org.py index 059c17596e..8529035522 100644 --- a/auth-api/tests/unit/models/test_org.py +++ b/auth-api/tests/unit/models/test_org.py @@ -88,3 +88,21 @@ def test_update_org_from_dict(session): # pylint:disable=unused-argument org.update_org_from_dict(update_dictionary) assert org assert org.name == update_dictionary['name'] + + +def test_create_from_dict(session): # pylint:disable=unused-argument + """Assert that an Org can be created from schema.""" + org_info = { + 'name': 'My Test Org' + } + + result_org = OrgModel.create_from_dict(org_info) + + assert result_org.id is not None + + +def test_create_from_dict_no_schema(session): # pylint:disable=unused-argument + """Assert that an Org can not be created without schema.""" + result_org = OrgModel.create_from_dict(None) + + assert result_org is None diff --git a/auth-api/tests/unit/models/test_user.py b/auth-api/tests/unit/models/test_user.py index 373ad92e83..290fc457e7 100644 --- a/auth-api/tests/unit/models/test_user.py +++ b/auth-api/tests/unit/models/test_user.py @@ -71,8 +71,8 @@ def test_create_from_jwt_token(session): # pylint: disable=unused-argument 'edit', 'uma_authorization', 'basic' - ] - }, + ] + }, 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' } u = User.create_from_jwt_token(token) @@ -97,8 +97,8 @@ def test_update_from_jwt_token(session): # pylint: disable=unused-argument 'edit', 'uma_authorization', 'basic' - ] - }, + ] + }, 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' } user = User.create_from_jwt_token(token) @@ -112,8 +112,8 @@ def test_update_from_jwt_token(session): # pylint: disable=unused-argument 'edit', 'uma_authorization', 'basic' - ] - }, + ] + }, 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' } user = User.update_from_jwt_token(updated_token, user) @@ -121,6 +121,54 @@ def test_update_from_jwt_token(session): # pylint: disable=unused-argument assert user.firstname == 'Bob' +def test_update_terms_of_user_success(session): # pylint:disable=unused-argument + """Assert User is updated from a JWT with new terms of use.""" + token = { + 'preferred_username': 'CP1234567', + 'firstname': 'Bobby', + 'lasname': 'Joe', + 'realm_access': { + 'roles': [ + 'edit', + 'uma_authorization', + 'basic' + ] + }, + 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' + } + user = User.create_from_jwt_token(token) + assert user.is_terms_of_use_accepted is False + assert user.terms_of_use_accepted_version is None + + user = User.update_terms_of_use(token, True, 1) + assert user.is_terms_of_use_accepted is True + assert user.terms_of_use_accepted_version == 1 + + +def test_update_terms_of_user_success_with_string(session): # pylint:disable=unused-argument + """Assert User is updated from a JWT with new terms of use.""" + token = { + 'preferred_username': 'CP1234567', + 'firstname': 'Bobby', + 'lasname': 'Joe', + 'realm_access': { + 'roles': [ + 'edit', + 'uma_authorization', + 'basic' + ] + }, + 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' + } + user = User.create_from_jwt_token(token) + assert user.is_terms_of_use_accepted is False + assert user.terms_of_use_accepted_version is None + + user = User.update_terms_of_use(token, True, '1') + assert user.is_terms_of_use_accepted is True + assert user.terms_of_use_accepted_version == 1 + + def test_update_from_jwt_token_no_token(session): # pylint:disable=unused-argument """Assert that a user is not updateable without a token (should return None).""" token = { @@ -132,8 +180,8 @@ def test_update_from_jwt_token_no_token(session): # pylint:disable=unused-argum 'edit', 'uma_authorization', 'basic' - ] - }, + ] + }, 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' } existing_user = User.create_from_jwt_token(token) @@ -143,6 +191,26 @@ def test_update_from_jwt_token_no_token(session): # pylint:disable=unused-argum assert user is None +def test_update_from_jwt_token_no_user(session): # pylint:disable=unused-argument + """Assert that a user is not updateable without a user (should return None).""" + token = { + 'preferred_username': 'CP1234567', + 'firstname': 'Bobby', + 'lasname': 'Joe', + 'realm_access': { + 'roles': [ + 'edit', + 'uma_authorization', + 'basic' + ] + }, + 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' + } + + user = User.update_from_jwt_token(token, None) + assert user is None + + def test_find_by_username(session): """Assert User can be found by the most current username.""" user = User(username='CP1234567', diff --git a/auth-api/tests/unit/models/views/test_authorization.py b/auth-api/tests/unit/models/views/test_authorization.py index 995a0e9d53..043560d622 100644 --- a/auth-api/tests/unit/models/views/test_authorization.py +++ b/auth-api/tests/unit/models/views/test_authorization.py @@ -25,7 +25,7 @@ def test_find_user_authorization_by_business_number(session): # pylint:disable=unused-argument """Assert that authorization view is returning result.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() membership = factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) @@ -39,7 +39,7 @@ def test_find_user_authorization_by_business_number(session): # pylint:disable= def test_find_invalid_user_authorization_by_business_number(session): # pylint:disable=unused-argument """Test with invalid user id and assert that auth is None.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) @@ -55,7 +55,7 @@ def test_find_invalid_user_authorization_by_business_number(session): # pylint: def test_find_all_user_authorizations(session): # pylint:disable=unused-argument """Test find all user authoirzations.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() membership = factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) @@ -68,7 +68,7 @@ def test_find_all_user_authorizations(session): # pylint:disable=unused-argumen def test_find_all_user_authorizations_for_empty(session): # pylint:disable=unused-argument """Test with invalid user id and assert that auth is None.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() factory_membership_model(user.id, org.id) authorizations = Authorization.find_all_authorizations_for_user(str(user.keycloak_guid)) diff --git a/auth-api/tests/unit/services/test_affiliation.py b/auth-api/tests/unit/services/test_affiliation.py index 87770bc0e6..dc48edd791 100644 --- a/auth-api/tests/unit/services/test_affiliation.py +++ b/auth-api/tests/unit/services/test_affiliation.py @@ -15,93 +15,159 @@ Test suite to ensure that the Affiliation service routines are working as expected. """ +from unittest.mock import patch + +import pytest + +from auth_api.exceptions import BusinessException +from auth_api.exceptions.errors import Error from auth_api.models.affiliation import Affiliation as AffiliationModel -from auth_api.models.entity import Entity as EntityModel from auth_api.models.org import Org as OrgModel -from auth_api.models.org_status import OrgStatus as OrgStatusModel -from auth_api.models.org_type import OrgType as OrgTypeModel -from auth_api.models.payment_type import PaymentType as PaymentTypeModel from auth_api.services import Affiliation as AffiliationService -from auth_api.services import Entity as EntityService -from auth_api.services import Org as OrgService +from tests.utilities.factory_scenarios import TestEntityInfo, TestOrgTypeInfo +from tests.utilities.factory_utils import factory_entity_service, factory_org_service + + +def test_create_affiliation(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can be created.""" + entity_service = factory_entity_service(entity_info=TestEntityInfo.entity_lear_mock) + entity_dictionary = entity_service.as_dict() + business_identifier = entity_dictionary['businessIdentifier'] + + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + affiliation = AffiliationService.create_affiliation(org_id, business_identifier, + TestEntityInfo.entity_lear_mock['passCode'], + {}) + assert affiliation + assert affiliation.entity.identifier == entity_service.identifier + assert affiliation.as_dict()['org']['id'] == org_dictionary['id'] -def factory_entity_service(business_identifier='CP1234567', business_number='791861073BC0001', name='Foobar, Inc.'): - """Produce a templated entity model.""" - entity = EntityModel.create_from_dict({ - 'business_identifier': business_identifier, - 'business_number': business_number, - 'name': name - }) - entity.save() - entity_service = EntityService(entity) - return entity_service +def test_create_affiliation_no_org(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can not be created without org.""" + entity_service = factory_entity_service() + entity_dictionary = entity_service.as_dict() + business_identifier = entity_dictionary['businessIdentifier'] + with pytest.raises(BusinessException) as exception: + AffiliationService.create_affiliation(None, business_identifier, {}) + assert exception.value.code == Error.DATA_NOT_FOUND.name -def factory_org_service(name): - """Produce a templated org model.""" - org_type = OrgTypeModel(code='TEST', desc='Test') - org_type.save() - org_status = OrgStatusModel(code='TEST', desc='Test') - org_status.save() +def test_create_affiliation_no_entity(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can not be created without entity.""" + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] - preferred_payment = PaymentTypeModel(code='TEST', desc='Test') - preferred_payment.save() + with pytest.raises(BusinessException) as exception: + AffiliationService.create_affiliation(org_id, None, {}) + assert exception.value.code == Error.DATA_NOT_FOUND.name - org = OrgModel(name=name) - org.org_type = org_type - org.org_status = org_status - org.preferred_payment = preferred_payment - org.save() - org_service = OrgService(org) +def test_create_affiliation_implicit(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can not be created when org is IMPLICIT.""" + entity_service1 = factory_entity_service() + entity_dictionary1 = entity_service1.as_dict() + business_identifier1 = entity_dictionary1['businessIdentifier'] + + org_service = factory_org_service(org_type_info=TestOrgTypeInfo.implicit) + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] - return org_service + pass_code = '111111111' + with pytest.raises(BusinessException) as exception: + AffiliationService.create_affiliation(org_id, business_identifier1, pass_code, {}) -def factory_affiliation_service(entity_id, org_id): - """Produce a templated affiliation service.""" - affiliation = AffiliationModel(entity=entity_id, org=org_id) - affiliation.save() - affiliation_service = AffiliationService(affiliation) - return affiliation_service + found_org = OrgModel.query.filter_by(id=org_id).first() + assert found_org is None + assert exception.value.code == Error.INVALID_USER_CREDENTIALS.name -def test_create_affiliation(session, auth_mock): # pylint:disable=unused-argument + +def test_create_affiliation_with_passcode(session, auth_mock): # pylint:disable=unused-argument """Assert that an Affiliation can be created.""" - entity_service = factory_entity_service() + entity_service = factory_entity_service(entity_info=TestEntityInfo.entity_lear_mock) entity_dictionary = entity_service.as_dict() business_identifier = entity_dictionary['businessIdentifier'] - org_service = factory_org_service(name='My Test Org') + org_service = factory_org_service() org_dictionary = org_service.as_dict() org_id = org_dictionary['id'] - affiliation = AffiliationService.create_affiliation(org_id, business_identifier, {}) + affiliation = AffiliationService.create_affiliation(org_id, + business_identifier, + TestEntityInfo.entity_lear_mock['passCode'], + {}) assert affiliation assert affiliation.entity.identifier == entity_service.identifier assert affiliation.as_dict()['org']['id'] == org_dictionary['id'] +def test_create_affiliation_with_passcode_no_passcode_input(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can not be created with a passcode entity and no passcode input parameter.""" + entity_service = factory_entity_service(entity_info=TestEntityInfo.entity_passcode) + entity_dictionary = entity_service.as_dict() + business_identifier = entity_dictionary['businessIdentifier'] + + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + with pytest.raises(BusinessException) as exception: + AffiliationService.create_affiliation(org_id, business_identifier) + + assert exception.value.code == Error.INVALID_USER_CREDENTIALS.name + + +def test_create_affiliation_exists(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can not be created affiliation exists.""" + entity_service1 = factory_entity_service(entity_info=TestEntityInfo.entity_lear_mock) + entity_dictionary1 = entity_service1.as_dict() + business_identifier1 = entity_dictionary1['businessIdentifier'] + + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + pass_code = TestEntityInfo.entity_lear_mock['passCode'] + + # create first row in affiliation table + AffiliationService.create_affiliation(org_id, business_identifier1, pass_code, {}) + + with pytest.raises(BusinessException) as exception: + AffiliationService.create_affiliation(org_id, business_identifier1, pass_code, {}) + assert exception.value.code == Error.INVALID_USER_CREDENTIALS.name + + def test_find_affiliated_entities_by_org_id(session, auth_mock): # pylint:disable=unused-argument """Assert that an Affiliation can be created.""" - entity_service1 = factory_entity_service(business_identifier='CP555') + entity_service1 = factory_entity_service(entity_info=TestEntityInfo.entity_lear_mock) entity_dictionary1 = entity_service1.as_dict() business_identifier1 = entity_dictionary1['businessIdentifier'] - entity_service2 = factory_entity_service(business_identifier='CP556') + entity_service2 = factory_entity_service(entity_info=TestEntityInfo.entity_lear_mock2) entity_dictionary2 = entity_service2.as_dict() business_identifier2 = entity_dictionary2['businessIdentifier'] - org_service = factory_org_service(name='My Test Org') + org_service = factory_org_service() org_dictionary = org_service.as_dict() org_id = org_dictionary['id'] # create first row in affiliation table - AffiliationService.create_affiliation(org_id, business_identifier1) + AffiliationService.create_affiliation(org_id, + business_identifier1, + TestEntityInfo.entity_lear_mock['passCode'], + {}) # create second row in affiliation table - AffiliationService.create_affiliation(org_id, business_identifier2) + AffiliationService.create_affiliation(org_id, + business_identifier2, + TestEntityInfo.entity_lear_mock2['passCode'], + {}) affiliated_entities = AffiliationService.find_affiliated_entities_by_org_id(org_id) @@ -110,19 +176,134 @@ def test_find_affiliated_entities_by_org_id(session, auth_mock): # pylint:disab assert affiliated_entities[0]['businessIdentifier'] == entity_dictionary1['businessIdentifier'] +def test_find_affiliated_entities_by_org_id_no_org(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can not be find without org id or org id not exists.""" + with pytest.raises(BusinessException) as exception: + AffiliationService.find_affiliated_entities_by_org_id(None) + assert exception.value.code == Error.DATA_NOT_FOUND.name + + with pytest.raises(BusinessException) as exception: + AffiliationService.find_affiliated_entities_by_org_id(999999) + assert exception.value.code == Error.DATA_NOT_FOUND.name + + +def test_find_affiliated_entities_by_org_id_no_affiliation(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Affiliation can not be find without affiliation.""" + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + with patch.object(AffiliationModel, 'find_affiliations_by_org_id', return_value=None): + with pytest.raises(BusinessException) as exception: + AffiliationService.find_affiliated_entities_by_org_id(org_id) + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + def test_delete_affiliation(session, auth_mock): # pylint:disable=unused-argument """Assert that an affiliation can be deleted.""" - entity_service = factory_entity_service() + entity_service = factory_entity_service(TestEntityInfo.entity_lear_mock) + entity_dictionary = entity_service.as_dict() + business_identifier = entity_dictionary['businessIdentifier'] + + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + affiliation = AffiliationService.create_affiliation(org_id, + business_identifier, + TestEntityInfo.entity_lear_mock['passCode'], + {}) + + AffiliationService.delete_affiliation(org_id=org_id, business_identifier=business_identifier) + + found_affiliation = AffiliationModel.query.filter_by(id=affiliation.identifier).first() + assert found_affiliation is None + + +def test_delete_affiliation_no_org(session, auth_mock): # pylint:disable=unused-argument + """Assert that an affiliation can not be deleted without org.""" + entity_service = factory_entity_service(TestEntityInfo.entity_lear_mock) + entity_dictionary = entity_service.as_dict() + business_identifier = entity_dictionary['businessIdentifier'] + + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + AffiliationService.create_affiliation(org_id, + business_identifier, + TestEntityInfo.entity_lear_mock['passCode'], + {}) + + with pytest.raises(BusinessException) as exception: + AffiliationService.delete_affiliation(org_id=None, business_identifier=business_identifier) + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + +def test_delete_affiliation_no_entity(session, auth_mock): # pylint:disable=unused-argument + """Assert that an affiliation can not be deleted without entity.""" + entity_service = factory_entity_service(TestEntityInfo.entity_lear_mock) + entity_dictionary = entity_service.as_dict() + business_identifier = entity_dictionary['businessIdentifier'] + + org_service = factory_org_service() + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + AffiliationService.create_affiliation(org_id, + business_identifier, + TestEntityInfo.entity_lear_mock['passCode'], + {}) + + with pytest.raises(BusinessException) as exception: + AffiliationService.delete_affiliation(org_id=org_id, business_identifier=None) + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + +def test_delete_affiliation_no_affiliation(session, auth_mock): # pylint:disable=unused-argument + """Assert that an affiliation can not be deleted without affiliation.""" + entity_service = factory_entity_service(TestEntityInfo.entity_lear_mock) entity_dictionary = entity_service.as_dict() business_identifier = entity_dictionary['businessIdentifier'] - org_service = factory_org_service(name='My Test Org') + org_service = factory_org_service() org_dictionary = org_service.as_dict() org_id = org_dictionary['id'] - affiliation = AffiliationService.create_affiliation(org_id, business_identifier) + AffiliationService.create_affiliation(org_id, + business_identifier, + TestEntityInfo.entity_lear_mock['passCode'], + {}) + AffiliationService.delete_affiliation(org_id=org_id, business_identifier=business_identifier) + + with pytest.raises(BusinessException) as exception: + AffiliationService.delete_affiliation(org_id=org_id, business_identifier=business_identifier) + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + +def test_delete_affiliation_implicit(session, auth_mock): # pylint:disable=unused-argument + """Assert that an affiliation can be deleted.""" + entity_service = factory_entity_service(TestEntityInfo.entity_lear_mock) + entity_dictionary = entity_service.as_dict() + business_identifier = entity_dictionary['businessIdentifier'] + + org_service = factory_org_service(org_type_info=TestOrgTypeInfo.implicit) + org_dictionary = org_service.as_dict() + org_id = org_dictionary['id'] + + affiliation = AffiliationService.create_affiliation(org_id, + business_identifier, + TestEntityInfo.entity_lear_mock['passCode'], + {}) AffiliationService.delete_affiliation(org_id=org_id, business_identifier=business_identifier) found_affiliation = AffiliationModel.query.filter_by(id=affiliation.identifier).first() assert found_affiliation is None + + found_org = OrgModel.query.filter_by(id=org_id).first() + assert found_org is None diff --git a/auth-api/tests/unit/services/test_authorization.py b/auth-api/tests/unit/services/test_authorization.py index 9d46643cb2..9b96f1a796 100644 --- a/auth-api/tests/unit/services/test_authorization.py +++ b/auth-api/tests/unit/services/test_authorization.py @@ -30,7 +30,7 @@ def test_get_user_authorizations_for_entity(session): # pylint:disable=unused-argument """Assert that user authorizations for entity is working.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() membership = factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) @@ -82,7 +82,7 @@ def test_get_user_authorizations_for_entity(session): # pylint:disable=unused-a def test_get_user_authorizations(session): # pylint:disable=unused-argument """Assert that listing all user authorizations is working.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() membership = factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) @@ -100,7 +100,7 @@ def test_get_user_authorizations(session): # pylint:disable=unused-argument def test_check_auth(session): # pylint:disable=unused-argument """Assert that check_auth is working as expected.""" user = factory_user_model() - org = factory_org_model('TEST') + org = factory_org_model() factory_membership_model(user.id, org.id) entity = factory_entity_model() factory_affiliation_model(entity.id, org.id) @@ -110,6 +110,9 @@ def test_check_auth(session): # pylint:disable=unused-argument # Test for owner role check_auth({'realm_access': {'roles': ['public']}, 'sub': str(user.keycloak_guid)}, one_of_roles=OWNER, business_identifier=entity.business_identifier) + # Test for owner role with org id + check_auth({'realm_access': {'roles': ['public']}, 'sub': str(user.keycloak_guid)}, one_of_roles=OWNER, + org_id=org.id) # Test for exception, check for auth if resource is available for STAFF users with pytest.raises(HTTPException) as excinfo: @@ -128,3 +131,9 @@ def test_check_auth(session): # pylint:disable=unused-argument check_auth({'realm_access': {'roles': ['public']}, 'sub': str(user.keycloak_guid)}, equals_role=MEMBER, business_identifier=entity.business_identifier) assert excinfo.exception.code == 403 + + # Test auth where STAFF role is exact match + with pytest.raises(HTTPException) as excinfo: + check_auth({'realm_access': {'roles': ['public']}, 'sub': str(user.keycloak_guid)}, equals_role=MEMBER, + org_id=org.id) + assert excinfo.exception.code == 403 diff --git a/auth-api/tests/unit/services/test_documents.py b/auth-api/tests/unit/services/test_documents.py new file mode 100644 index 0000000000..1b12864b56 --- /dev/null +++ b/auth-api/tests/unit/services/test_documents.py @@ -0,0 +1,41 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests to verify the User Service. + +Test-Suite to ensure that the Document Service is working as expected. +""" + +from auth_api.models import Documents as DocumentsModel +from auth_api.services import Documents as DocumentService + + +def test_as_dict(session): # pylint: disable=unused-argument + """Assert that a document is rendered correctly as a dictionary.""" + _model = DocumentsModel.fetch_latest_document_by_type('termsofuse') + termsofuse = DocumentService(_model) + dictionary = termsofuse.as_dict() + assert dictionary['type'] == 'termsofuse' + + +def test_with_valid_type(session): # pylint: disable=unused-argument + """Assert that a document is rendered correctly as a dictionary.""" + terms_of_use = DocumentService.fetch_latest_document('termsofuse') + assert terms_of_use is not None + + +def test_with_no_valid_type(session): # pylint: disable=unused-argument + """Assert that a document is rendered correctly as a dictionary.""" + terms_of_use = DocumentService.fetch_latest_document('sometype') + assert terms_of_use is None diff --git a/auth-api/tests/unit/services/test_entity.py b/auth-api/tests/unit/services/test_entity.py index 8884b08e21..6742a7b450 100644 --- a/auth-api/tests/unit/services/test_entity.py +++ b/auth-api/tests/unit/services/test_entity.py @@ -15,175 +15,241 @@ Test suite to ensure that the Entity service routines are working as expected. """ - import pytest from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error -from auth_api.models.entity import Entity as EntityModel +from auth_api.models import ContactLink as ContactLinkModel from auth_api.services.entity import Entity as EntityService - - -TEST_CONTACT_INFO = { - 'email': 'foo@bar.com' -} - -TEST_UPDATED_CONTACT_INFO = { - 'email': 'bar@foo.com' -} - - -def factory_entity_model(business_identifier='CP1234567', - business_number='791861073BC0001', - name='Foobar, Inc.', pass_code=None): - """Return a valid entity object with the provided fields.""" - entity = EntityModel(business_identifier=business_identifier, - business_number=business_number, - name=name, - pass_code=pass_code) - entity.save() - return entity +from tests.utilities.factory_scenarios import TestContactInfo, TestEntityInfo +from tests.utilities.factory_utils import factory_contact_model, factory_entity_model, factory_org_service def test_as_dict(session): # pylint:disable=unused-argument """Assert that the Entity is exported correctly as a dictionary.""" - entity_model = factory_entity_model(business_identifier='CP1234567') + entity_model = factory_entity_model() entity = EntityService(entity_model) dictionary = entity.as_dict() - assert dictionary['businessIdentifier'] == 'CP1234567' + assert dictionary['businessIdentifier'] == TestEntityInfo.entity1['businessIdentifier'] def test_save_entity_new(session): # pylint:disable=unused-argument """Assert that an Entity can be created from a dictionary.""" entity = EntityService.save_entity({ - 'businessIdentifier': 'CP1234567', - 'businessNumber': '791861073BC0001', - 'passCode': '1234567', - 'name': 'Foobar, Inc.' + 'businessIdentifier': TestEntityInfo.entity_passcode['businessIdentifier'], + 'businessNumber': TestEntityInfo.entity_passcode['businessNumber'], + 'passCode': TestEntityInfo.entity_passcode['passCode'], + 'name': TestEntityInfo.entity_passcode['name'] }) assert entity is not None dictionary = entity.as_dict() - assert dictionary['businessIdentifier'] == 'CP1234567' + assert dictionary['businessIdentifier'] == TestEntityInfo.entity_passcode['businessIdentifier'] def test_save_entity_existing(session): # pylint:disable=unused-argument """Assert that an Entity can be updated from a dictionary.""" entity = EntityService.save_entity({ - 'businessIdentifier': 'CP1234567', - 'businessNumber': '791861073BC0001', - 'passCode': '1234567', - 'name': 'Foobar, Inc.' + 'businessIdentifier': TestEntityInfo.entity_passcode['businessIdentifier'], + 'businessNumber': TestEntityInfo.entity_passcode['businessNumber'], + 'passCode': TestEntityInfo.entity_passcode['passCode'], + 'name': TestEntityInfo.entity_passcode['name'] }) assert entity updated_entity_info = { - 'businessIdentifier': 'CP1234567', - 'businessNumber': '791861073BC0001', - 'passCode': '9898989', - 'name': 'Barfoo, Inc.' + 'businessIdentifier': TestEntityInfo.entity_passcode2['businessIdentifier'], + 'businessNumber': TestEntityInfo.entity_passcode2['businessNumber'], + 'passCode': TestEntityInfo.entity_passcode['passCode'], + 'name': TestEntityInfo.entity_passcode['name'] } updated_entity = EntityService.save_entity(updated_entity_info) assert updated_entity assert updated_entity.as_dict()['name'] == updated_entity_info['name'] + assert updated_entity.as_dict()['businessNumber'] == updated_entity_info['businessNumber'] + + +def test_save_entity_no_input(session): # pylint:disable=unused-argument + """Assert that an Entity can not be updated with no input.""" + updated_entity = EntityService.save_entity(None) + + assert updated_entity is None def test_entity_find_by_business_id(session, auth_mock): # pylint:disable=unused-argument """Assert that an Entity can be retrieved by business identifier.""" - factory_entity_model(business_identifier='CP1234567') - entity = EntityService.find_by_business_identifier('CP1234567') + factory_entity_model() + entity = EntityService.find_by_business_identifier(TestEntityInfo.entity1['businessIdentifier']) assert entity is not None dictionary = entity.as_dict() - assert dictionary['businessIdentifier'] == 'CP1234567' + assert dictionary['businessIdentifier'] == TestEntityInfo.entity1['businessIdentifier'] def test_entity_find_by_business_id_no_model(session, auth_mock): # pylint:disable=unused-argument """Assert that an Entity which does not exist cannot be retrieved.""" - entity = EntityService.find_by_business_identifier('CP1234567') + entity = EntityService.find_by_business_identifier(TestEntityInfo.entity1['businessIdentifier']) + + assert entity is None + + +def test_entity_find_by_entity_id(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Entity can be retrieved by entity identifier.""" + entity_model = factory_entity_model() + entity = EntityService(entity_model) + + entity = EntityService.find_by_entity_id(entity.identifier) + + assert entity is not None + dictionary = entity.as_dict() + assert dictionary['businessIdentifier'] == TestEntityInfo.entity1['businessIdentifier'] + + +def test_entity_find_by_entity_id_no_id(session, auth_mock): # pylint:disable=unused-argument + """Assert that an Entity can not be retrieved when no id input or entity not exists.""" + entity = EntityService.find_by_entity_id(None) + + assert entity is None + + entity = EntityService.find_by_entity_id(9999) assert entity is None def test_add_contact(session): # pylint:disable=unused-argument """Assert that a contact can be added to an Entity.""" - entity_model = factory_entity_model(business_identifier='CP1234567') + entity_model = factory_entity_model() entity = EntityService(entity_model) - entity.add_contact(TEST_CONTACT_INFO) + entity.add_contact(TestContactInfo.contact1) dictionary = entity.as_dict() assert dictionary['contacts'] assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact1['email'] def test_add_contact_duplicate(session): # pylint:disable=unused-argument """Assert that a contact cannot be added to an Entity if that Entity already has a contact.""" - entity_model = factory_entity_model(business_identifier='CP1234567') + entity_model = factory_entity_model() entity = EntityService(entity_model) - entity.add_contact(TEST_CONTACT_INFO) + entity.add_contact(TestContactInfo.contact1) with pytest.raises(BusinessException) as exception: - entity.add_contact(TEST_UPDATED_CONTACT_INFO) + entity.add_contact(TestContactInfo.contact2) assert exception.value.code == Error.DATA_ALREADY_EXISTS.name def test_update_contact(session): # pylint:disable=unused-argument """Assert that a contact for an existing Entity can be updated.""" - entity_model = factory_entity_model(business_identifier='CP1234567') + entity_model = factory_entity_model() entity = EntityService(entity_model) - entity.add_contact(TEST_CONTACT_INFO) + entity.add_contact(TestContactInfo.contact1) dictionary = entity.as_dict() assert len(dictionary['contacts']) == 1 assert dictionary['contacts'][0]['email'] == \ - TEST_CONTACT_INFO['email'] + TestContactInfo.contact1['email'] - entity.update_contact(TEST_UPDATED_CONTACT_INFO) + entity.update_contact(TestContactInfo.contact2) dictionary = None dictionary = entity.as_dict() assert len(dictionary['contacts']) == 1 assert dictionary['contacts'][0]['email'] == \ - TEST_UPDATED_CONTACT_INFO['email'] + TestContactInfo.contact2['email'] def test_update_contact_no_contact(session): # pylint:disable=unused-argument """Assert that a contact for a non-existent contact cannot be updated.""" - entity_model = factory_entity_model(business_identifier='CP1234567') + entity_model = factory_entity_model() entity = EntityService(entity_model) with pytest.raises(BusinessException) as exception: - entity.update_contact(TEST_UPDATED_CONTACT_INFO) + entity.update_contact(TestContactInfo.contact2) assert exception.value.code == Error.DATA_NOT_FOUND.name def test_get_contact_by_business_identifier(session): # pylint:disable=unused-argument """Assert that a contact can be retrieved by the associated business id.""" - entity_model = factory_entity_model(business_identifier='CP1234567') + entity_model = factory_entity_model() entity = EntityService(entity_model) - entity.add_contact(TEST_CONTACT_INFO) + entity.add_contact(TestContactInfo.contact1) contact = entity.get_contact() assert contact is not None - assert contact.email == TEST_CONTACT_INFO['email'] + assert contact.email == TestContactInfo.contact1['email'] def test_get_contact_by_business_identifier_no_contact(session): # pylint:disable=unused-argument """Assert that a contact cannot be retrieved from an entity with no contact.""" - entity_model = factory_entity_model(business_identifier='CP1234567') + entity_model = factory_entity_model() entity = EntityService(entity_model) contact = entity.get_contact() assert contact is None +def test_delete_contact(session): # pylint:disable=unused-argument + """Assert that a contact can be deleted to an Entity.""" + entity_model = factory_entity_model() + entity = EntityService(entity_model) + entity.add_contact(TestContactInfo.contact1) + + updated_entity = entity.delete_contact() + dictionary = updated_entity.as_dict() + assert not dictionary['contacts'] + + +def test_delete_contact_no_entity(session, auth_mock): # pylint:disable=unused-argument + """Assert that a contact can not be deleted without entity.""" + entity_model = factory_entity_model() + entity = EntityService(entity_model) + entity.add_contact(TestContactInfo.contact1) + + updated_entity = entity.delete_contact() + + with pytest.raises(BusinessException) as exception: + updated_entity.delete_contact() + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + +def test_delete_contact_entity_link(session, auth_mock): # pylint:disable=unused-argument + """Assert that a contact can not be deleted without entity.""" + entity_model = factory_entity_model() + entity = EntityService(entity_model) + + org = factory_org_service() + org_dictionary = org.as_dict() + org_id = org_dictionary['id'] + + contact = factory_contact_model() + + contact_link = ContactLinkModel() + contact_link.contact = contact + contact_link.entity = entity._model # pylint:disable=protected-access + contact_link.org = org._model # pylint:disable=protected-access + contact_link.commit() + + updated_entity = entity.delete_contact() + + dictionary = None + dictionary = updated_entity.as_dict() + assert len(dictionary['contacts']) == 0 + + delete_contact_link = ContactLinkModel.find_by_entity_id(entity.identifier) + assert not delete_contact_link + + exist_contact_link = ContactLinkModel.find_by_org_id(org_id) + assert exist_contact_link + + def test_validate_pass_code(app, session): # pylint:disable=unused-argument """Assert that a valid passcode can be correctly validated.""" - entity_model = factory_entity_model(business_identifier='CP1234567', pass_code='12345678') + entity_model = factory_entity_model(entity_info=TestEntityInfo.entity_passcode) entity = EntityService(entity_model) validated = entity.validate_pass_code(entity_model.pass_code) @@ -192,8 +258,18 @@ def test_validate_pass_code(app, session): # pylint:disable=unused-argument def test_validate_invalid_pass_code(app, session): # pylint:disable=unused-argument """Assert that an invalid passcode in not validated.""" - entity_model = factory_entity_model(business_identifier='CP1234567', pass_code='12345678') + entity_model = factory_entity_model(entity_info=TestEntityInfo.entity_passcode) entity = EntityService(entity_model) - validated = entity.validate_pass_code('1234') + validated = entity.validate_pass_code('222222222') assert not validated + + +def test_entity_name_sync(app, session): # pylint:disable=unused-argument + """Assert that the name syncing for entity affiliation is working correctly.""" + entity_model = factory_entity_model(entity_info=TestEntityInfo.entity_lear_mock) + entity = EntityService(entity_model) + entity.sync_name() + + dictionary = entity.as_dict() + assert dictionary['name'] == 'Legal Name CP0002103' diff --git a/auth-api/tests/unit/services/test_invitation.py b/auth-api/tests/unit/services/test_invitation.py index d7ea09b9b5..c41c1ca19d 100644 --- a/auth-api/tests/unit/services/test_invitation.py +++ b/auth-api/tests/unit/services/test_invitation.py @@ -17,173 +17,95 @@ """ from unittest.mock import patch +import pytest + import auth_api.services.authorization as auth -from auth_api.models import User as UserModel +from auth_api.exceptions import BusinessException +from auth_api.exceptions.errors import Error +from auth_api.models import Invitation as InvitationModel +from auth_api.models import InvitationStatus as InvitationStatusModel from auth_api.services import Invitation as InvitationService from auth_api.services import Org as OrgService from auth_api.services import User +from tests.utilities.factory_scenarios import TestOrgInfo, TestUserInfo +from tests.utilities.factory_utils import factory_invitation, factory_user_model -TEST_ORG_INFO = { - 'name': 'My Test Org' -} - -TEST_UPDATED_INVITATION_INFO = { - 'status': 'ACCEPTED' -} - - -def factory_user_model(username, - firstname=None, - lastname=None, - roles=None, - keycloak_guid=None): - """Return a valid user object stamped with the supplied designation.""" - user = UserModel(username=username, - firstname=firstname, - lastname=lastname, - roles=roles, - keycloak_guid=keycloak_guid) - user.save() - return user - - -def test_as_dict(session): # pylint:disable=unused-argument +def test_as_dict(session, auth_mock): # pylint:disable=unused-argument """Assert that the Invitation is exported correctly as a dictionary.""" with patch.object(InvitationService, 'send_invitation', return_value=None): - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) + user = factory_user_model() + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) org_dictionary = org.as_dict() - print(org_dictionary) - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'sentDate': '2019-09-09', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_dictionary['id'] - } - ] - } - invitation = InvitationService.create_invitation(invitation_info, User(user)) + invitation_info = factory_invitation(org_dictionary['id']) + invitation = InvitationService.create_invitation(invitation_info, User(user), {}, '') invitation_dictionary = invitation.as_dict() assert invitation_dictionary['recipientEmail'] == invitation_info['recipientEmail'] -def test_create_invitation(session): # pylint:disable=unused-argument +def test_create_invitation(session, auth_mock): # pylint:disable=unused-argument """Assert that an Invitation can be created.""" with patch.object(InvitationService, 'send_invitation', return_value=None) as mock_notify: - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) + user = factory_user_model(TestUserInfo.user_test) + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) org_dictionary = org.as_dict() - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_dictionary['id'] - } - ] - } - invitation = InvitationService.create_invitation(invitation_info, User(user)) + invitation_info = factory_invitation(org_dictionary['id']) + invitation = InvitationService.create_invitation(invitation_info, User(user), {}, '') invitation_dictionary = invitation.as_dict() assert invitation_dictionary['recipientEmail'] == invitation_info['recipientEmail'] assert invitation_dictionary['id'] mock_notify.assert_called() -def test_get_invitations(session): # pylint:disable=unused-argument - """Assert that invitations can be retrieved.""" - with patch.object(InvitationService, 'send_invitation', return_value=None): - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) - org_dictionary = org.as_dict() - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_dictionary['id'] - } - ] - } - InvitationService.create_invitation(invitation_info, User(user)) - invitation = InvitationService.get_invitations(user.id, 'ALL') - invitation_dictionary = invitation[0] - assert invitation_dictionary['recipientEmail'] == invitation_info['recipientEmail'] - - -def test_find_invitation_by_id(session): # pylint:disable=unused-argument +def test_find_invitation_by_id(session, auth_mock): # pylint:disable=unused-argument """Find an existing invitation with the provided id.""" with patch.object(InvitationService, 'send_invitation', return_value=None): - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) + user = factory_user_model(TestUserInfo.user_test) + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) org_dictionary = org.as_dict() - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_dictionary['id'] - } - ] - } - new_invitation = InvitationService.create_invitation(invitation_info, User(user)).as_dict() + invitation_info = factory_invitation(org_dictionary['id']) + new_invitation = InvitationService.create_invitation(invitation_info, User(user), {}, '').as_dict() invitation = InvitationService.find_invitation_by_id(new_invitation['id']).as_dict() assert invitation assert invitation['recipientEmail'] == invitation_info['recipientEmail'] -def test_delete_invitation(session): # pylint:disable=unused-argument +def test_find_invitation_by_id_exception(session, auth_mock): # pylint:disable=unused-argument + """Find an existing invitation with the provided id with exception.""" + invitation = InvitationService.find_invitation_by_id(None) + assert invitation is None + + +def test_delete_invitation(session, auth_mock): # pylint:disable=unused-argument """Delete the specified invitation.""" with patch.object(InvitationService, 'send_invitation', return_value=None): - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) + user = factory_user_model(TestUserInfo.user_test) + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) org_dictionary = org.as_dict() - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_dictionary['id'] - } - ] - } - new_invitation = InvitationService.create_invitation(invitation_info, User(user)).as_dict() + invitation_info = factory_invitation(org_dictionary['id']) + new_invitation = InvitationService.create_invitation(invitation_info, User(user), {}, '').as_dict() InvitationService.delete_invitation(new_invitation['id']) invitation = InvitationService.find_invitation_by_id(new_invitation['id']) assert invitation is None -def test_update_invitation(session): # pylint:disable=unused-argument +def test_delete_invitation_exception(session, auth_mock): # pylint:disable=unused-argument + """Delete the specified invitation with exception.""" + with pytest.raises(BusinessException) as exception: + InvitationService.delete_invitation(None) + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + +def test_update_invitation(session, auth_mock): # pylint:disable=unused-argument """Update the specified invitation with new data.""" with patch.object(InvitationService, 'send_invitation', return_value=None): - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) + user = factory_user_model(TestUserInfo.user_test) + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) org_dictionary = org.as_dict() - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_dictionary['id'] - } - ] - } - new_invitation = InvitationService.create_invitation(invitation_info, User(user)) - updated_invitation = new_invitation.update_invitation(User(user)).as_dict() + invitation_info = factory_invitation(org_dictionary['id']) + new_invitation = InvitationService.create_invitation(invitation_info, User(user), {}, '') + updated_invitation = new_invitation.update_invitation(User(user), {}, '').as_dict() assert updated_invitation['status'] == 'PENDING' @@ -200,26 +122,90 @@ def test_validate_token_valid(session): # pylint:disable=unused-argument assert invitation_id == 1 +def test_validate_token_exception(session): # pylint:disable=unused-argument + """Validate the invitation token with exception.""" + with pytest.raises(BusinessException) as exception: + InvitationService.validate_token(None) + + assert exception.value.code == Error.EXPIRED_INVITATION.name + + def test_accept_invitation(session, auth_mock): # pylint:disable=unused-argument """Accept the invitation and add membership from the invitation to the org.""" with patch.object(InvitationService, 'send_invitation', return_value=None): with patch.object(auth, 'check_auth', return_value=True): - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) + user = factory_user_model(TestUserInfo.user_test) + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) org_dictionary = org.as_dict() - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org_dictionary['id'] - } - ] - } - new_invitation = InvitationService.create_invitation(invitation_info, User(user)) + invitation_info = factory_invitation(org_dictionary['id']) + new_invitation = InvitationService.create_invitation(invitation_info, User(user), {}, '') new_invitation_dict = new_invitation.as_dict() InvitationService.accept_invitation(new_invitation_dict['id'], user.id) org_dict = OrgService.find_by_org_id(org_dictionary['id'], allowed_roles={'basic'}).as_dict() assert len(org_dict['members']) == 2 # Member count will be 2 only if the invite accept is successful. + + +def test_accept_invitation_exceptions(session, auth_mock): # pylint:disable=unused-argument + """Accept the invitation and add membership from the invitation to the org.""" + with patch.object(InvitationService, 'send_invitation', return_value=None): + with patch.object(auth, 'check_auth', return_value=True): + user = factory_user_model(TestUserInfo.user_test) + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) + org_dictionary = org.as_dict() + invitation_info = factory_invitation(org_dictionary['id']) + + with pytest.raises(BusinessException) as exception: + InvitationService.accept_invitation(None, user.id) + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + new_invitation = InvitationService.create_invitation(invitation_info, User(user), {}, '') + new_invitation_dict = new_invitation.as_dict() + InvitationService.accept_invitation(new_invitation_dict['id'], user.id) + + with pytest.raises(BusinessException) as exception: + InvitationService.accept_invitation(new_invitation_dict['id'], user.id) + + assert exception.value.code == Error.ACTIONED_INVITATION.name + + with pytest.raises(BusinessException) as exception: + expired_invitation: InvitationModel = InvitationModel.find_invitation_by_id(new_invitation_dict['id']) + expired_invitation.invitation_status = InvitationStatusModel.get_status_by_code('EXPIRED') + expired_invitation.save() + InvitationService.accept_invitation(expired_invitation.id, user.id) + + assert exception.value.code == Error.EXPIRED_INVITATION.name + + +def test_get_invitations_by_org_id(session, auth_mock): # pylint:disable=unused-argument + """Find an existing invitation with the provided org id.""" + with patch.object(InvitationService, 'send_invitation', return_value=None): + user = factory_user_model(TestUserInfo.user_test) + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) + org_dictionary = org.as_dict() + org_id = org_dictionary['id'] + invitation_info = factory_invitation(org_dictionary['id']) + InvitationService.create_invitation(invitation_info, User(user), {}, '').as_dict() + invitations: list = InvitationService.get_invitations_by_org_id(org_id, 'ALL') + assert invitations + assert len(invitations) == 1 + + invitations: list = InvitationService.get_invitations_by_org_id(org_id, 'PENDING') + assert len(invitations) == 1 + + +def test_send_invitation_exception(session, auth_mock): # pylint:disable=unused-argument + """Send an existing invitation with exception.""" + user = factory_user_model(TestUserInfo.user_test) + user_dictionary = User(user).as_dict() + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) + org_dictionary = org.as_dict() + + invitation_info = factory_invitation(org_dictionary['id']) + + invitation = InvitationModel.create_from_dict(invitation_info, user.id) + + with pytest.raises(BusinessException) as exception: + InvitationService.send_invitation(invitation, user_dictionary, '') + + assert exception.value.code == Error.FAILED_INVITATION.name diff --git a/auth-api/tests/unit/services/test_org.py b/auth-api/tests/unit/services/test_org.py index 62ca1bc669..a0260319c5 100644 --- a/auth-api/tests/unit/services/test_org.py +++ b/auth-api/tests/unit/services/test_org.py @@ -15,120 +15,60 @@ Test suite to ensure that the Org service routines are working as expected. """ - from unittest.mock import patch import pytest from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error -from auth_api.models import Org as OrgModel -from auth_api.models import OrgStatus as OrgStatusModel -from auth_api.models import OrgType as OrgTypeModel -from auth_api.models import PaymentType as PaymentTypeModel -from auth_api.models import User as UserModel +from auth_api.models import ContactLink as ContactLinkModel from auth_api.services import Invitation as InvitationService from auth_api.services import Org as OrgService from auth_api.services import User as UserService - - -TEST_ORG_INFO = { - 'name': 'My Test Org' -} - -TEST_UPDATED_ORG_INFO = { - 'name': 'My Updated Test Org' -} - -TEST_CONTACT_INFO = { - 'email': 'foo@bar.com' -} - -TEST_UPDATED_CONTACT_INFO = { - 'email': 'bar@foo.com' -} - - -def factory_user_model(username, - firstname=None, - lastname=None, - roles=None, - keycloak_guid=None): - """Return a valid user object stamped with the supplied designation.""" - user = UserModel(username=username, - firstname=firstname, - lastname=lastname, - roles=roles, - keycloak_guid=keycloak_guid) - user.save() - return user - - -def factory_org_service(session, name): - """Produce a templated org service.""" - org_type = OrgTypeModel(code='TEST', desc='Test') - session.add(org_type) - session.commit() - - org_status = OrgStatusModel(code='TEST', desc='Test') - session.add(org_status) - session.commit() - - preferred_payment = PaymentTypeModel(code='TEST', desc='Test') - session.add(preferred_payment) - session.commit() - - org_model = OrgModel(name=name) - org_model.org_type = org_type - org_model.org_status = org_status - org_model.preferred_payment = preferred_payment - org_model.save() - - org = OrgService(org_model) - - return org +from auth_api.services.entity import Entity as EntityService +from tests.utilities.factory_scenarios import TestContactInfo, TestOrgInfo +from tests.utilities.factory_utils import ( + factory_contact_model, factory_entity_model, factory_invitation, factory_org_service, factory_user_model) def test_as_dict(session): # pylint:disable=unused-argument """Assert that the Org is exported correctly as a dictinoary.""" - org = factory_org_service(session, **TEST_ORG_INFO) + org = factory_org_service() dictionary = org.as_dict() assert dictionary - assert dictionary['name'] == TEST_ORG_INFO['name'] + assert dictionary['name'] == TestOrgInfo.org1['name'] def test_create_org(session): # pylint:disable=unused-argument """Assert that an Org can be created.""" - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user_id=user.id) + user = factory_user_model() + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) assert org dictionary = org.as_dict() - assert dictionary['name'] == TEST_ORG_INFO['name'] + assert dictionary['name'] == TestOrgInfo.org1['name'] def test_update_org(session): # pylint:disable=unused-argument """Assert that an Org can be updated.""" - org = factory_org_service(session, **TEST_ORG_INFO) + org = factory_org_service() - org.update_org(TEST_UPDATED_ORG_INFO) + org.update_org(TestOrgInfo.org2) dictionary = org.as_dict() - assert dictionary['name'] == TEST_UPDATED_ORG_INFO['name'] + assert dictionary['name'] == TestOrgInfo.org2['name'] def test_find_org_by_id(session, auth_mock): # pylint:disable=unused-argument """Assert that an org can be retrieved by its id.""" - org = factory_org_service(session, **TEST_ORG_INFO) + org = factory_org_service() dictionary = org.as_dict() org_id = dictionary['id'] found_org = OrgService.find_by_org_id(org_id) assert found_org dictionary = found_org.as_dict() - assert dictionary['name'] == TEST_ORG_INFO['name'] + assert dictionary['name'] == TestOrgInfo.org1['name'] def test_find_org_by_id_no_org(session, auth_mock): # pylint:disable=unused-argument @@ -139,55 +79,53 @@ def test_find_org_by_id_no_org(session, auth_mock): # pylint:disable=unused-arg def test_add_contact(session): # pylint:disable=unused-argument """Assert that a contact can be added to an org.""" - org = factory_org_service(session, **TEST_ORG_INFO) - org.add_contact(TEST_CONTACT_INFO) + org = factory_org_service() + org.add_contact(TestContactInfo.contact1) dictionary = org.as_dict() assert dictionary['contacts'] assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact1['email'] def test_add_contact_duplicate(session): # pylint:disable=unused-argument """Assert that a contact cannot be added to an Org if that Org already has a contact.""" - org = factory_org_service(session, **TEST_ORG_INFO) - org.add_contact(TEST_CONTACT_INFO) + org = factory_org_service() + org.add_contact(TestContactInfo.contact1) with pytest.raises(BusinessException) as exception: - org.add_contact(TEST_UPDATED_CONTACT_INFO) + org.add_contact(TestContactInfo.contact2) assert exception.value.code == Error.DATA_ALREADY_EXISTS.name def test_update_contact(session): # pylint:disable=unused-argument """Assert that a contact for an existing Org can be updated.""" - org = factory_org_service(session, **TEST_ORG_INFO) - org.add_contact(TEST_CONTACT_INFO) + org = factory_org_service() + org.add_contact(TestContactInfo.contact1) dictionary = org.as_dict() assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact1['email'] - org.update_contact(TEST_UPDATED_CONTACT_INFO) + org.update_contact(TestContactInfo.contact2) dictionary = org.as_dict() assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_UPDATED_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact2['email'] def test_update_contact_no_contact(session): # pylint:disable=unused-argument """Assert that a contact for a non-existent contact cannot be updated.""" - org = factory_org_service(session, **TEST_ORG_INFO) + org = factory_org_service() with pytest.raises(BusinessException) as exception: - org.update_contact(TEST_UPDATED_CONTACT_INFO) + org.update_contact(TestContactInfo.contact2) assert exception.value.code == Error.DATA_NOT_FOUND.name def test_get_members(session): # pylint:disable=unused-argument """Assert that members for an org can be retrieved.""" - user = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user.id) + user = factory_user_model() + org = OrgService.create_org(TestOrgInfo.org1, user.id) response = org.get_members() assert response @@ -195,29 +133,85 @@ def test_get_members(session): # pylint:disable=unused-argument assert response['members'][0]['membershipTypeCode'] == 'OWNER' -def test_get_invitations(session): # pylint:disable=unused-argument +def test_get_invitations(session, auth_mock): # pylint:disable=unused-argument """Assert that invitations for an org can be retrieved.""" with patch.object(InvitationService, 'send_invitation', return_value=None): - user = factory_user_model(username='testuser', - firstname='Test', - lastname='User', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - org = OrgService.create_org(TEST_ORG_INFO, user.id) - - invitation_info = { - 'recipientEmail': 'abc.test@gmail.com', - 'membership': [ - { - 'membershipType': 'MEMBER', - 'orgId': org.as_dict()['id'] - } - ] - } - - invitation = InvitationService.create_invitation(invitation_info, UserService(user)) + user = factory_user_model() + org = OrgService.create_org(TestOrgInfo.org1, user.id) + + invitation_info = factory_invitation(org.as_dict()['id']) + + invitation = InvitationService.create_invitation(invitation_info, UserService(user), {}, '') response = org.get_invitations() assert response assert len(response['invitations']) == 1 assert response['invitations'][0]['recipientEmail'] == invitation.as_dict()['recipientEmail'] + + +def test_delete_contact_no_org(session, auth_mock): # pylint:disable=unused-argument + """Assert that a contact can not be deleted without org.""" + org = factory_org_service() + org.add_contact(TestContactInfo.contact1) + + updated_org = org.delete_contact() + + with pytest.raises(BusinessException) as exception: + updated_org.delete_contact() + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + +def test_delete_contact_org_link(session, auth_mock): # pylint:disable=unused-argument + """Assert that a contact can not be deleted without entity.""" + entity_model = factory_entity_model() + entity = EntityService(entity_model) + + org = factory_org_service() + org_dictionary = org.as_dict() + org_id = org_dictionary['id'] + + contact = factory_contact_model() + + contact_link = ContactLinkModel() + contact_link.contact = contact + contact_link.entity = entity._model # pylint:disable=protected-access + contact_link.org = org._model # pylint:disable=protected-access + contact_link.commit() + + updated_org = org.delete_contact() + + dictionary = None + dictionary = updated_org.as_dict() + assert len(dictionary['contacts']) == 0 + + delete_contact_link = ContactLinkModel.find_by_entity_id(entity.identifier) + assert delete_contact_link + + exist_contact_link = ContactLinkModel.find_by_org_id(org_id) + assert not exist_contact_link + + +def test_remove_member(session): # pylint:disable=unused-argument + """Assert that members for an org can be removed.""" + user = factory_user_model() + org = OrgService.create_org(TestOrgInfo.org1, user.id) + members = org.get_members() + + # test input id is not match with org's member id + with pytest.raises(BusinessException) as exception: + org.remove_member(0) + + assert exception.value.code == Error.DATA_NOT_FOUND.name + + # test remove + org.remove_member(member_id=members['members'][0]['id']) + response = org.get_members() + assert response + assert len(response['members']) == 0 + + # test remove again + with pytest.raises(BusinessException) as exception: + org.remove_member(member_id=members['members'][0]['id']) + + assert exception.value.code == Error.DATA_NOT_FOUND.name diff --git a/auth-api/tests/unit/services/test_user.py b/auth-api/tests/unit/services/test_user.py index a25c4d1b86..d4c08856e6 100644 --- a/auth-api/tests/unit/services/test_user.py +++ b/auth-api/tests/unit/services/test_user.py @@ -16,108 +16,37 @@ Test-Suite to ensure that the User Service is working as expected. """ +from unittest.mock import patch + import pytest from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error -from auth_api.models import Org as OrgModel -from auth_api.models import OrgStatus as OrgStatusModel -from auth_api.models import OrgType as OrgTypeModel -from auth_api.models import PaymentType as PaymentTypeModel +from auth_api.models import ContactLink as ContactLinkModel from auth_api.models import User as UserModel from auth_api.services import Org as OrgService from auth_api.services import User as UserService - - -TEST_TOKEN = { - 'preferred_username': 'testuser', - 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04', - 'realm_access': { - 'roles': [ - 'edit', - 'uma_authorization', - 'basic' - ] - } - } - -TEST_CONTACT_INFO = { - 'email': 'foo@bar.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -TEST_UPDATED_CONTACT_INFO = { - 'email': 'bar@foo.com', - 'phone': '(555) 555-5555', - 'phoneExtension': '123' -} - -TEST_ORG_INFO = { - 'name': 'My Test Org' -} - - -def factory_user_model(username, - firstname=None, - lastname=None, - roles=None, - keycloak_guid=None): - """Return a valid user object stamped with the supplied designation.""" - user = UserModel(username=username, - firstname=firstname, - lastname=lastname, - roles=roles, - keycloak_guid=keycloak_guid) - user.save() - return user - - -def factory_org_service(session, name): - """Produce a templated org service.""" - org_type = OrgTypeModel(code='TEST', desc='Test') - session.add(org_type) - session.commit() - - org_status = OrgStatusModel(code='TEST', desc='Test') - session.add(org_status) - session.commit() - - preferred_payment = PaymentTypeModel(code='TEST', desc='Test') - session.add(preferred_payment) - session.commit() - - org_model = OrgModel(name=name) - org_model.org_type = org_type - org_model.org_status = org_status - org_model.preferred_payment = preferred_payment - org_model.save() - - org = OrgService(org_model) - - return org +from tests.utilities.factory_scenarios import TestContactInfo, TestJwtClaims, TestOrgInfo, TestUserInfo +from tests.utilities.factory_utils import factory_contact_model, factory_user_model def test_as_dict(session): # pylint: disable=unused-argument """Assert that a user is rendered correctly as a dictionary.""" - user_model = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + user_model = factory_user_model() user = UserService(user_model) dictionary = user.as_dict() - assert dictionary['username'] == 'testuser' - assert dictionary['roles'] == '{edit,uma_authorization,basic}' - assert dictionary['keycloak_guid'] == '1b20db59-19a0-4727-affe-c6f64309fd04' + assert dictionary['username'] == TestUserInfo.user1['username'] + assert dictionary['roles'] == TestUserInfo.user1['roles'] def test_user_save_by_token(session): # pylint: disable=unused-argument """Assert that a user can be created by token.""" - user = UserService.save_from_jwt_token(TEST_TOKEN) + user = UserService.save_from_jwt_token(TestJwtClaims.user_test) assert user is not None dictionary = user.as_dict() - assert dictionary['username'] == TEST_TOKEN['preferred_username'] - assert dictionary['keycloak_guid'] == TEST_TOKEN['sub'] + assert dictionary['username'] == TestJwtClaims.user_test['preferred_username'] + assert dictionary['keycloak_guid'] == TestJwtClaims.user_test['sub'] def test_user_save_by_token_no_token(session): # pylint: disable=unused-argument @@ -126,95 +55,101 @@ def test_user_save_by_token_no_token(session): # pylint: disable=unused-argumen assert user is None +def test_user_save_by_token_fail(session): # pylint: disable=unused-argument + """Assert that a user cannot not be created.""" + with patch.object(UserModel, 'create_from_jwt_token', return_value=None): + user = UserService.save_from_jwt_token(TestJwtClaims.user_test) + assert user is None + + def test_add_contact_to_user(session): # pylint: disable=unused-argument """Assert that a contact can be added to a user.""" - factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + factory_user_model(user_info=TestUserInfo.user_test) - user = UserService.add_contact(TEST_TOKEN, TEST_CONTACT_INFO) + user = UserService.add_contact(TestJwtClaims.user_test, TestContactInfo.contact1) assert user is not None dictionary = user.as_dict() assert dictionary['contacts'] assert len(dictionary['contacts']) == 1 - assert dictionary['contacts'][0]['email'] == TEST_CONTACT_INFO['email'] - assert dictionary['contacts'][0]['phone'] == TEST_CONTACT_INFO['phone'] - assert dictionary['contacts'][0]['phoneExtension'] == TEST_CONTACT_INFO['phoneExtension'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact1['email'] + assert dictionary['contacts'][0]['phone'] == TestContactInfo.contact1['phone'] + assert dictionary['contacts'][0]['phoneExtension'] == TestContactInfo.contact1['phoneExtension'] def test_add_contact_user_no_user(session): # pylint: disable=unused-argument """Assert that a contact cannot be added to a user that does not exist.""" with pytest.raises(BusinessException) as exception: - UserService.add_contact(TEST_TOKEN, TEST_CONTACT_INFO) + UserService.add_contact(TestJwtClaims.user_test, TestContactInfo.contact1) assert exception.value.code == Error.DATA_NOT_FOUND.name def test_add_contact_to_user_already_exists(session): # pylint: disable=unused-argument """Assert that a contact cannot be added to a user that already has a contact.""" - factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + factory_user_model(user_info=TestUserInfo.user_test) - UserService.add_contact(TEST_TOKEN, TEST_CONTACT_INFO) + UserService.add_contact(TestJwtClaims.user_test, TestContactInfo.contact1) with pytest.raises(BusinessException) as exception: - UserService.add_contact(TEST_TOKEN, TEST_UPDATED_CONTACT_INFO) + UserService.add_contact(TestJwtClaims.user_test, TestContactInfo.contact2) assert exception.value.code == Error.DATA_ALREADY_EXISTS.name def test_update_contact_for_user(session): # pylint: disable=unused-argument """Assert that a contact can be updated for a user.""" - factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + factory_user_model(user_info=TestUserInfo.user_test) - user = UserService.add_contact(TEST_TOKEN, TEST_CONTACT_INFO) + user = UserService.add_contact(TestJwtClaims.user_test, TestContactInfo.contact1) assert user is not None dictionary = user.as_dict() assert dictionary['contacts'] assert len(dictionary['contacts']) == 1 - updated_user = UserService.update_contact(TEST_TOKEN, TEST_UPDATED_CONTACT_INFO) + updated_user = UserService.update_contact(TestJwtClaims.user_test, TestContactInfo.contact2) assert updated_user is not None dictionary = updated_user.as_dict() - assert dictionary['contacts'][0]['email'] == TEST_UPDATED_CONTACT_INFO['email'] + assert dictionary['contacts'][0]['email'] == TestContactInfo.contact2['email'] + + +def test_update_terms_of_use_for_user(session): # pylint: disable=unused-argument + """Assert that a terms of use can be updated for a user.""" + UserService.save_from_jwt_token(TestJwtClaims.user_test) + + updated_user = UserService.update_terms_of_use(TestJwtClaims.user_test, True, 1) + dictionary = updated_user.as_dict() + assert dictionary['is_terms_of_use_accepted'] is True def test_update_contact_for_user_no_user(session): # pylint: disable=unused-argument """Assert that a contact cannot be updated for a user that does not exist.""" with pytest.raises(BusinessException) as exception: - UserService.update_contact(TEST_TOKEN, TEST_UPDATED_CONTACT_INFO) + UserService.update_contact(TestJwtClaims.user_test, TestContactInfo.contact2) assert exception.value.code == Error.DATA_NOT_FOUND.name def test_update_contact_for_user_no_contact(session): # pylint: disable=unused-argument """Assert that a contact cannot be updated for a user with no contact.""" - factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + factory_user_model(user_info=TestUserInfo.user_test) with pytest.raises(BusinessException) as exception: - UserService.update_contact(TEST_TOKEN, TEST_UPDATED_CONTACT_INFO) + UserService.update_contact(TestJwtClaims.user_test, TestContactInfo.contact2) assert exception.value.code == Error.DATA_NOT_FOUND.name def test_delete_contact_for_user(session): # pylint: disable=unused-argument """Assert that a contact can be deleted for a user.""" - factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + factory_user_model(user_info=TestUserInfo.user_test) - user = UserService.add_contact(TEST_TOKEN, TEST_CONTACT_INFO) + user = UserService.add_contact(TestJwtClaims.user_test, TestContactInfo.contact1) assert user is not None dictionary = user.as_dict() assert dictionary['contacts'] assert len(dictionary['contacts']) == 1 - updated_user = UserService.delete_contact(TEST_TOKEN) + updated_user = UserService.delete_contact(TestJwtClaims.user_test) assert updated_user is not None dictionary = updated_user.as_dict() @@ -224,34 +159,24 @@ def test_delete_contact_for_user(session): # pylint: disable=unused-argument def test_delete_contact_for_user_no_user(session): # pylint: disable=unused-argument """Assert that deleting a contact for a non-existent user raises the right exception.""" with pytest.raises(BusinessException) as exception: - UserService.delete_contact(TEST_TOKEN) + UserService.delete_contact(TestJwtClaims.user_test) assert exception.value.code == Error.DATA_NOT_FOUND.name def test_delete_contact_for_user_no_contact(session): # pylint: disable=unused-argument """Assert that deleting a contact for a user with no contact raises the right exception.""" - factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + factory_user_model(user_info=TestUserInfo.user_test) with pytest.raises(BusinessException) as exception: - UserService.delete_contact(TEST_TOKEN) + UserService.delete_contact(TestJwtClaims.user_test) assert exception.value.code == Error.DATA_NOT_FOUND.name def test_find_users(session): # pylint: disable=unused-argument """Assert that a list of users can be retrieved and searched on.""" - factory_user_model(username='testuser', - firstname='Test', - lastname='User', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') - - factory_user_model(username='testuser2', - firstname='Test 2', - lastname='User', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd05') + factory_user_model() + + factory_user_model(user_info=TestUserInfo.user2) users = UserService.find_users(last_name='User') assert users is not None @@ -260,34 +185,36 @@ def test_find_users(session): # pylint: disable=unused-argument def test_user_find_by_token(session): # pylint: disable=unused-argument """Assert that a user can be found by token.""" - factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + factory_user_model(user_info=TestUserInfo.user_test) + + found_user = UserService.find_by_jwt_token(None) + assert found_user is None - found_user = UserService.find_by_jwt_token(TEST_TOKEN) + found_user = UserService.find_by_jwt_token(TestJwtClaims.user_test) assert found_user is not None dictionary = found_user.as_dict() - assert dictionary['username'] == TEST_TOKEN['preferred_username'] - assert dictionary['keycloak_guid'] == TEST_TOKEN['sub'] + assert dictionary['username'] == TestJwtClaims.user_test['preferred_username'] + assert dictionary['keycloak_guid'] == TestJwtClaims.user_test['sub'] def test_user_find_by_username(session): # pylint: disable=unused-argument """Assert that a user can be found by username.""" - user_model = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + user_model = factory_user_model() user = UserService(user_model) - user = UserService.find_by_username('testuser') + user = UserService.find_by_username(None) + assert user is None + + user = UserService.find_by_username(TestUserInfo.user_test['username']) assert user is not None dictionary = user.as_dict() - assert dictionary['username'] == 'testuser' + assert dictionary['username'] == TestUserInfo.user_test['username'] def test_user_find_by_username_no_model_object(session): # pylint: disable=unused-argument """Assert that the business can't be found with no model.""" - username = 'testuser' + username = TestUserInfo.user_test['username'] user = UserService.find_by_username(username) @@ -296,9 +223,7 @@ def test_user_find_by_username_no_model_object(session): # pylint: disable=unus def test_user_find_by_username_missing_username(session): # pylint: disable=unused-argument """Assert that the business can't be found by incorrect username.""" - user_model = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + user_model = factory_user_model(user_info=TestUserInfo.user_test) user = UserService(user_model) user = UserService.find_by_username('foo') @@ -308,14 +233,42 @@ def test_user_find_by_username_missing_username(session): # pylint: disable=unu def test_get_orgs(session): # pylint:disable=unused-argument """Assert that orgs for a user can be retrieved.""" - user_model = factory_user_model(username='testuser', - roles='{edit,uma_authorization,basic}', - keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') + user_model = factory_user_model(user_info=TestUserInfo.user_test) user = UserService(user_model) - OrgService.create_org(TEST_ORG_INFO, user_id=user.identifier) + OrgService.create_org(TestOrgInfo.org1, user_id=user.identifier) response = user.get_orgs() assert response['orgs'] assert len(response['orgs']) == 1 - assert response['orgs'][0]['name'] == TEST_ORG_INFO['name'] + assert response['orgs'][0]['name'] == TestOrgInfo.org1['name'] + + +def test_delete_contact_user_link(session, auth_mock): # pylint:disable=unused-argument + """Assert that a contact can not be deleted if contact link exists.""" + user_model = factory_user_model(user_info=TestUserInfo.user_test) + user = UserService(user_model) + + org = OrgService.create_org(TestOrgInfo.org1, user_id=user.identifier) + org_dictionary = org.as_dict() + org_id = org_dictionary['id'] + + contact = factory_contact_model() + + contact_link = ContactLinkModel() + contact_link.contact = contact + contact_link.user = user_model + contact_link.org = org._model # pylint:disable=protected-access + contact_link.commit() + + updated_user = user.delete_contact(TestJwtClaims.user_test) + + dictionary = None + dictionary = updated_user.as_dict() + assert len(dictionary['contacts']) == 0 + + delete_contact_link = ContactLinkModel.find_by_user_id(user.identifier) + assert not delete_contact_link + + exist_contact_link = ContactLinkModel.find_by_org_id(org_id) + assert exist_contact_link diff --git a/auth-api/tests/unit/utils/test_passcode.py b/auth-api/tests/unit/utils/test_passcode.py new file mode 100644 index 0000000000..bf37deb118 --- /dev/null +++ b/auth-api/tests/unit/utils/test_passcode.py @@ -0,0 +1,68 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests to assure the passcode hashing utilities. + +Test-Suite to ensure that the passcode hashing is working as expected. +""" +from auth_api.utils import passcode + + +def test_passcode_hash(): + """Assert that passcode can be hashed.""" + pass_code: str = '111111111' + hashed_pass_code: str = passcode.passcode_hash(pass_code) + assert hashed_pass_code + + +def test_passcode_hash_fail(): + """Assert that passcode can be hash.""" + pass_code: str = None + hashed_pass_code: str = passcode.passcode_hash(pass_code) + assert hashed_pass_code is None + + +def test_passcode_hash_different(): + """Assert that the same passcode get different hash value by multiple running.""" + pass_code: str = '111111111' + hashed_pass_code: str = passcode.passcode_hash(pass_code) + hashed_pass_code2: str = passcode.passcode_hash(pass_code) + assert hashed_pass_code != hashed_pass_code2 + + +def test_validate_passcode(): + """Assert that passcode can be validate.""" + pass_code: str = '111111111' + hashed_pass_code: str = passcode.passcode_hash(pass_code) + checked_pass_code: str = '111111111' + validated: bool = passcode.validate_passcode(checked_pass_code, hashed_pass_code) + assert validated + + +def test_validate_passcode_empty_input(): + """Assert that passcode can be validate.""" + pass_code: str = '111111111' + hashed_pass_code: str = passcode.passcode_hash(pass_code) + checked_pass_code: str = None + validated: bool = passcode.validate_passcode(checked_pass_code, hashed_pass_code) + assert not validated + + +def test_validate_passcode_fail(): + """Assert that passcode can be validate.""" + pass_code: str = '111111111' + hashed_pass_code: str = passcode.passcode_hash(pass_code) + checked_pass_code: str = '222222222' + validated: bool = passcode.validate_passcode(checked_pass_code, hashed_pass_code) + assert not validated diff --git a/auth-api/tests/unit/utils/test_util_camel_snake.py b/auth-api/tests/unit/utils/test_util_camel_snake.py new file mode 100644 index 0000000000..fc4df85503 --- /dev/null +++ b/auth-api/tests/unit/utils/test_util_camel_snake.py @@ -0,0 +1,43 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests to assure the CORS utilities. + +Test-Suite to ensure that the CORS decorator is working as expected. +""" +from auth_api.utils.util import camelback2snake, snake2camelback + + +TEST_CAMEL_DATA = {'loginSource': 'PASSCODE', 'userName': 'test name', 'realmAccess': { + 'roles': ['basic'] +}} + + +TEST_SNAKE_DATA = {'login_source': 'PASSCODE', 'user_name': 'test name', 'realm_access': { + 'roles': ['basic'] +}} + + +def test_camelback2snake(): + """Assert that the options methos is added to the class and that the correct access controls are set.""" + snake = camelback2snake(TEST_CAMEL_DATA) + + assert snake['login_source'] == TEST_SNAKE_DATA['login_source'] + + +def test_snake2camelback(): + """Assert that the options methos is added to the class and that the correct access controls are set.""" + camel = snake2camelback(TEST_SNAKE_DATA) + + assert camel['loginSource'] == TEST_CAMEL_DATA['loginSource'] diff --git a/auth-api/tests/utilities/factory_scenarios.py b/auth-api/tests/utilities/factory_scenarios.py new file mode 100644 index 0000000000..2c4e272f18 --- /dev/null +++ b/auth-api/tests/utilities/factory_scenarios.py @@ -0,0 +1,267 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test Utils. + +Test Utility for creating test scenarios. +""" +import os +import uuid +from enum import Enum + + +JWT_HEADER = { + 'alg': os.getenv('JWT_OIDC_ALGORITHMS'), + 'typ': 'JWT', + 'kid': os.getenv('JWT_OIDC_AUDIENCE') +} + + +class TestJwtClaims(dict, Enum): + """Test scenarios of jwt claims.""" + + no_role = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302065', + 'firstname': 'Test', + 'lastname': 'User 2', + 'preferred_username': 'testuser2', + 'realm_access': { + 'roles': [ + ] + } + } + + invalid = { + 'sub': 'barfoo', + 'firstname': 'Trouble', + 'lastname': 'Maker', + 'preferred_username': 'troublemaker' + } + + edit_role = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'firstname': 'Test', + 'lastname': 'User', + 'preferred_username': 'testuser', + 'realm_access': { + 'roles': [ + 'edit' + ] + } + } + + edit_role_2 = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302075', + 'firstname': 'Test', + 'lastname': 'User 2', + 'preferred_username': 'testuser2', + 'realm_access': { + 'roles': [ + 'edit' + ] + } + } + + view_role = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'firstname': 'Test', + 'lastname': 'User', + 'preferred_username': 'testuser', + 'realm_access': { + 'roles': [ + 'view' + ] + } + } + + staff_role = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'firstname': 'Test', + 'lastname': 'User', + 'preferred_username': 'testuser', + 'realm_access': { + 'roles': [ + 'staff' + ] + } + } + + system_role = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'firstname': 'Test', + 'lastname': 'User', + 'preferred_username': 'testuser', + 'realm_access': { + 'roles': [ + 'system' + ] + } + } + + passcode = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'firstname': 'Test', + 'lastname': 'User', + 'preferred_username': 'CP1234567', + 'username': 'CP1234567', + 'realm_access': { + 'roles': [ + 'system' + ] + }, + 'loginSource': 'PASSCODE' + } + + updated_test = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': 'f7a4a1d3-73a8-4cbc-a40f-bb1145302064', + 'firstname': 'Updated_Test', + 'lastname': 'User', + 'username': 'testuser', + 'realm_access': { + 'roles': [ + ] + } + } + user_test = { + 'iss': os.getenv('JWT_OIDC_ISSUER'), + 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04', + 'firstname': 'Test', + 'lastname': 'User', + 'preferred_username': 'CP1234567', + 'username': 'CP1234567', + 'realm_access': { + 'roles': [ + 'edit', 'uma_authorization', 'staff' + ] + }, + 'loginSource': 'PASSCODE' + } + + +class TestOrgTypeInfo(dict, Enum): + """Test scenarios of org type.""" + + test_type = {'code': 'TEST', 'desc': 'Test'} + implicit = {'code': 'IMPLICIT', 'desc': 'IMPLICIT'} + + +class TestPaymentTypeInfo(dict, Enum): + """Test scenarios of payment type.""" + + test_type = {'code': 'TEST', 'desc': 'Test'} + + +class TestOrgStatusInfo(dict, Enum): + """Test scenarios of org status.""" + + test_status = {'code': 'TEST', 'desc': 'Test'} + + +class TestOrgInfo(dict, Enum): + """Test scenarios of org.""" + + org1 = {'name': 'My Test Org'} + org2 = {'name': 'My Test Updated Org'} + invalid = {'foo': 'bar'} + + +class TestEntityInfo(dict, Enum): + """Test scenarios of entity.""" + + entity1 = {'businessIdentifier': 'CP1234567', + 'businessNumber': '791861073BC0001', + 'name': 'Foobar, Inc.', + 'passCode': ''} + entity2 = {'businessIdentifier': 'CP1234568', + 'businessNumber': '791861079BC0001', + 'name': 'BarFoo, Inc.', + 'passCode': ''} + entity_passcode = {'businessIdentifier': 'CP1234568', + 'businessNumber': '791861079BC0001', + 'name': 'Foobar, Inc.', + 'passCode': '111111111'} + entity_passcode2 = {'businessIdentifier': 'CP1234568', + 'businessNumber': '791861078BC0001', + 'name': 'BarFoo, Inc.', + 'passCode': '222222222'} + invalid = {'foo': 'bar'} + entity_lear_mock = {'businessIdentifier': 'CP0002103', + 'businessNumber': '791861078BC0001', + 'name': 'BarFoo, Inc.', + 'passCode': '222222222'} + entity_lear_mock2 = {'businessIdentifier': 'CP0002106', + 'businessNumber': '791861078BC0002', + 'name': 'Foobar, Inc.', + 'passCode': '222222222'} + + +class TestAffliationInfo(dict, Enum): + """Test scenarios of affliation.""" + + affliation1 = {'businessIdentifier': 'CP1234567'} + affliation2 = {'businessIdentifier': 'CP1234568'} + affiliation3 = {'businessIdentifier': 'CP0002103', 'passCode': '222222222'} + affiliation4 = {'businessIdentifier': 'CP0002106', 'passCode': '222222222'} + invalid = {'name': 'CP1234567'} + + +class TestContactInfo(dict, Enum): + """Test scenarios of contact.""" + + contact1 = { + 'email': 'foo@bar.com', + 'phone': '(555) 555-5555', + 'phoneExtension': '123' + } + + contact2 = { + 'email': 'bar@foo.com', + 'phone': '(555) 555-5555', + 'phoneExtension': '123' + } + + invalid = {'email': 'bar'} + + +class TestUserInfo(dict, Enum): + """Test scenarios of user.""" + + user1 = { + 'username': 'CP1234567', + 'firstname': 'Test', + 'lastname': 'User', + 'roles': '{edit, uma_authorization, staff}', + 'keycloak_guid': uuid.uuid4() + } + user2 = { + 'username': 'CP1234568', + 'firstname': 'Test 2', + 'lastname': 'User', + 'roles': '{edit, uma_authorization, staff}', + 'keycloak_guid': uuid.uuid4() + } + user_test = { + 'username': 'CP1234567', + 'firstname': 'Test', + 'lastname': 'User', + 'roles': '{edit, uma_authorization, staff}', + 'keycloak_guid': '1b20db59-19a0-4727-affe-c6f64309fd04' + } diff --git a/auth-api/tests/utilities/factory_utils.py b/auth-api/tests/utilities/factory_utils.py index 70d59175b0..2805ddacd3 100644 --- a/auth-api/tests/utilities/factory_utils.py +++ b/auth-api/tests/utilities/factory_utils.py @@ -15,30 +15,52 @@ Test Utility for creating model factory. """ -import uuid +import datetime from auth_api.models import Affiliation as AffiliationModel +from auth_api.models import Contact as ContactModel +from auth_api.models import Documents as DocumentsModel from auth_api.models import Entity as EntityModel from auth_api.models import Org as OrgModel from auth_api.models import OrgStatus as OrgStatusModel from auth_api.models import OrgType as OrgTypeModel from auth_api.models import PaymentType as PaymentTypeModel -from auth_api.models.membership import Membership -from auth_api.models.user import User +from auth_api.models.membership import Membership as MembershipModel +from auth_api.models.user import User as UserModel +from auth_api.services import Affiliation as AffiliationService +from auth_api.services import Entity as EntityService +from auth_api.services import Org as OrgService +from tests.utilities.factory_scenarios import ( + JWT_HEADER, TestContactInfo, TestEntityInfo, TestOrgInfo, TestOrgStatusInfo, TestOrgTypeInfo, TestPaymentTypeInfo, + TestUserInfo) -def factory_entity_model(): +def factory_auth_header(jwt, claims): + """Produce JWT tokens for use in tests.""" + return {'Authorization': 'Bearer ' + jwt.create_jwt(claims=claims, header=JWT_HEADER)} + + +def factory_entity_model(entity_info: dict = TestEntityInfo.entity1): """Produce a templated entity model.""" - entity = EntityModel(business_identifier='CP1234567', business_number='791861073BC0001', name='Foobar, Inc.') + entity = EntityModel.create_from_dict(entity_info) entity.save() return entity -def factory_user_model(): +def factory_entity_service(entity_info: dict = TestEntityInfo.entity1): + """Produce a templated entity service.""" + entity_model = factory_entity_model(entity_info) + entity_service = EntityService(entity_model) + return entity_service + + +def factory_user_model(user_info: dict = TestUserInfo.user1): """Produce a user model.""" - user = User(username='CP1234567', - roles='{edit, uma_authorization, staff}', - keycloak_guid=uuid.uuid4()) + user = UserModel(username=user_info['username'], + firstname=user_info['firstname'], + lastname=user_info['lastname'], + roles=user_info['roles'], + keycloak_guid=user_info['keycloak_guid']) user.save() return user @@ -46,26 +68,31 @@ def factory_user_model(): def factory_membership_model(user_id, org_id, member_type='OWNER'): """Produce a Membership model.""" - membership = Membership(user_id=user_id, - org_id=org_id, - membership_type_code=member_type) + membership = MembershipModel(user_id=user_id, + org_id=org_id, + membership_type_code=member_type) membership.save() return membership -def factory_org_model(name): +def factory_org_model(org_info: dict = TestOrgInfo.org1, + org_type_info: dict = TestOrgTypeInfo.test_type, + org_status_info: dict = TestOrgStatusInfo.test_status, + payment_type_info: dict = TestPaymentTypeInfo.test_type): """Produce a templated org model.""" - org_type = OrgTypeModel(code='TEST', desc='Test') - org_type.save() + org_type = OrgTypeModel.get_default_type() + if org_type_info['code'] != TestOrgTypeInfo.implicit['code']: + org_type = OrgTypeModel(code=org_type_info['code'], desc=org_type_info['desc']) + org_type.save() - org_status = OrgStatusModel(code='TEST', desc='Test') + org_status = OrgStatusModel(code=org_status_info['code'], desc=org_status_info['desc']) org_status.save() - preferred_payment = PaymentTypeModel(code='TEST', desc='Test') + preferred_payment = PaymentTypeModel(code=payment_type_info['code'], desc=payment_type_info['desc']) preferred_payment.save() - org = OrgModel(name=name) + org = OrgModel(name=org_info['name']) org.org_type = org_type org.org_status = org_status org.preferred_payment = preferred_payment @@ -74,8 +101,63 @@ def factory_org_model(name): return org +def factory_org_service(org_info: dict = TestOrgInfo.org1, + org_type_info: dict = TestOrgTypeInfo.test_type, + org_status_info: dict = TestOrgStatusInfo.test_status, + payment_type_info: dict = TestPaymentTypeInfo.test_type): + """Produce a templated org service.""" + org_model = factory_org_model(org_info=org_info, + org_type_info=org_type_info, + org_status_info=org_status_info, + payment_type_info=payment_type_info) + org_service = OrgService(org_model) + return org_service + + def factory_affiliation_model(entity_id, org_id): """Produce a templated affiliation model.""" affiliation = AffiliationModel(entity_id=entity_id, org_id=org_id) affiliation.save() return affiliation + + +def factory_affiliation_service(entity_id, org_id): + """Produce a templated affiliation service.""" + affiliation = AffiliationModel(entity=entity_id, org=org_id) + affiliation.save() + affiliation_service = AffiliationService(affiliation) + return affiliation_service + + +def factory_contact_model(contact_info: dict = TestContactInfo.contact1): + """Return a valid contact object with the provided fields.""" + contact = ContactModel(email=contact_info['email']) + contact.save() + return contact + + +def factory_invitation(org_id, + email='abc123@email.com', + sent_date=datetime.datetime.now().strftime('Y-%m-%d %H:%M:%S'), + membership_type='MEMBER'): + """Produce an invite for the given org and email.""" + return { + 'recipientEmail': email, + 'sentDate': sent_date, + 'membership': [ + { + 'membershipType': membership_type, + 'orgId': org_id + } + ] + } + + +def factory_document_model(version_id, doc_type, content): + """Produce a Document model.""" + document = DocumentsModel(version_id=version_id, + type=doc_type, + content=content) + + document.save() + return document diff --git a/auth-web/.env b/auth-web/.env index e99c2ea65c..4c7424568e 100644 --- a/auth-web/.env +++ b/auth-web/.env @@ -1 +1,2 @@ -VUE_APP_PATH = auth +VUE_APP_PATH = cooperatives/auth +VUE_APP_PATH_COOPS = cooperatives diff --git a/auth-web/.env.production b/auth-web/.env.production index e99c2ea65c..4c7424568e 100644 --- a/auth-web/.env.production +++ b/auth-web/.env.production @@ -1 +1,2 @@ -VUE_APP_PATH = auth +VUE_APP_PATH = cooperatives/auth +VUE_APP_PATH_COOPS = cooperatives diff --git a/auth-web/.eslintrc.js b/auth-web/.eslintrc.js index d639d09c89..088912a18d 100644 --- a/auth-web/.eslintrc.js +++ b/auth-web/.eslintrc.js @@ -10,7 +10,8 @@ module.exports = { ], rules: { 'no-console': 'error', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'sort-imports': 'error' }, parserOptions: { parser: '@typescript-eslint/parser' diff --git a/auth-web/jenkins/prod.groovy b/auth-web/jenkins/prod.groovy index cdeca26aa6..2e01409e89 100644 --- a/auth-web/jenkins/prod.groovy +++ b/auth-web/jenkins/prod.groovy @@ -43,6 +43,51 @@ def rocketChatNotificaiton(token, channel, comments) { script: "curl -X POST -H 'Content-Type: application/json' --data \'${payload}\' ${rocketChatUrl}") } + +@NonCPS +boolean triggerBuild(String contextDirectory) { + // Determine if code has changed within the source context directory. + def changeLogSets = currentBuild.changeSets + def filesChangeCnt = 0 + for (int i = 0; i < changeLogSets.size(); i++) { + def entries = changeLogSets[i].items + for (int j = 0; j < entries.length; j++) { + def entry = entries[j] + //echo "${entry.commitId} by ${entry.author} on ${new Date(entry.timestamp)}: ${entry.msg}" + def files = new ArrayList(entry.affectedFiles) + for (int k = 0; k < files.size(); k++) { + def file = files[k] + def filePath = file.path + //echo ">> ${file.path}" + if (filePath.contains(contextDirectory)) { + filesChangeCnt = 1 + k = files.size() + j = entries.length + } + } + } + } + + if ( filesChangeCnt < 1 ) { + echo('The changes do not require a build.') + return false + } else { + echo('The changes require a build.') + return true + } +} + +// Get an image's hash tag +String getImageTagHash(String imageName, String tag = "") { + + if(!tag?.trim()) { + tag = "latest" + } + + def istag = openshift.raw("get istag ${imageName}:${tag} -o template --template='{{.image.dockerImageReference}}'") + return istag.out.tokenize('@')[1].trim() +} + node { properties([[$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '', numToKeepStr: '10']]]) diff --git a/auth-web/openshift/Caddyfile b/auth-web/openshift/Caddyfile index 92a4c39f47..69fc55800d 100644 --- a/auth-web/openshift/Caddyfile +++ b/auth-web/openshift/Caddyfile @@ -8,6 +8,6 @@ errors stdout rewrite /auth { regexp .* - to {path} /auth/ + to {path} /cooperatives/auth/ } diff --git a/auth-web/openshift/templates/auth-web-deploy.json b/auth-web/openshift/templates/auth-web-deploy.json index ec9b3ca005..0bbdf78b47 100644 --- a/auth-web/openshift/templates/auth-web-deploy.json +++ b/auth-web/openshift/templates/auth-web-deploy.json @@ -306,7 +306,7 @@ "displayName": "WEB_APP_CONTEXT_PATH", "description": "The path at which web application is deployed.Context root for the web applicaton", "required": true, - "value": "auth" + "value": "cooperatives/auth" } ] } diff --git a/auth-web/openshift/templates/auth-web-pre-deploy.json b/auth-web/openshift/templates/auth-web-pre-deploy.json index f046a8eec9..801e43e13d 100644 --- a/auth-web/openshift/templates/auth-web-pre-deploy.json +++ b/auth-web/openshift/templates/auth-web-pre-deploy.json @@ -78,7 +78,7 @@ "displayName": "WEB_APP_CONTEXT_PATH", "description": "The path at which web application is deployed.Context root for the web applicaton", "required": true, - "value": "auth" + "value": "cooperatives/auth" } ] } diff --git a/auth-web/openshift/templates/auth-web-runtime-build.json b/auth-web/openshift/templates/auth-web-runtime-build.json index 88f0ec0ab1..fc3e82de23 100644 --- a/auth-web/openshift/templates/auth-web-runtime-build.json +++ b/auth-web/openshift/templates/auth-web-runtime-build.json @@ -30,7 +30,7 @@ "runPolicy": "Serial", "source": { "type": "Dockerfile", - "dockerfile": "FROM bcgov-s2i-caddy\nRUN mkdir /var/www/html/${WEB_APP_CONTEXT_PATH} \nCOPY dist /var/www/html/${WEB_APP_CONTEXT_PATH}", + "dockerfile": "FROM bcgov-s2i-caddy\nRUN mkdir -p /var/www/html/${WEB_APP_CONTEXT_PATH} \nCOPY dist /var/www/html/${WEB_APP_CONTEXT_PATH}", "images": [ { "from": { @@ -191,7 +191,7 @@ "displayName": "WEB_APP_CONTEXT_PATH", "description": "The path at which web application is deployed.Context root for the web applicaton", "required": true, - "value": "auth" + "value": "cooperatives/auth" } ] } diff --git a/auth-web/package.json b/auth-web/package.json index 47386a83f4..0ddbe10c62 100644 --- a/auth-web/package.json +++ b/auth-web/package.json @@ -14,41 +14,40 @@ }, "dependencies": { "axios": "^0.18.0", - "core-js": "^3.1.4", + "core-js": "^3.3.3", "keycloak-js": "^6.0.0", "lodash": "^4.17.15", "material-icons": "^0.3.1", "moment": "^2.24.0", "regenerator-runtime": "^0.13.3", "register-service-worker": "^1.6.2", - "sbc-common-components": "^2.0.1", + "sbc-common-components": "^2.0.17", "vue": "^2.6.10", "vue-i18n": "^8.0.0", "vue-property-decorator": "^8.1.1", "vue-router": "^3.0.1", "vue-the-mask": "^0.11.1", - "vuetify": "^2.0.11", - "vuex": "^3.0.1", - "vuex-persist": "^2.0.1" + "vuetify": "^2.1.5", + "vuex": "^3.0.1" }, "devDependencies": { "@kazupon/vue-i18n-loader": "^0.3.0", "@types/jest": "^23.1.4", "@types/lodash": "^4.14.138", - "@vue/cli-plugin-babel": "^3.5.0", - "@vue/cli-plugin-e2e-nightwatch": "^3.5.0", - "@vue/cli-plugin-eslint": "^3.5.0", - "@vue/cli-plugin-pwa": "^3.5.0", - "@vue/cli-plugin-typescript": "^3.5.0", - "@vue/cli-plugin-unit-jest": "^3.5.0", - "@vue/cli-service": "^3.5.0", + "@vue/cli-plugin-babel": "^3.12.1", + "@vue/cli-plugin-e2e-nightwatch": "^3.12.1", + "@vue/cli-plugin-eslint": "^3.12.1", + "@vue/cli-plugin-pwa": "^3.12.1", + "@vue/cli-plugin-typescript": "^3.12.1", + "@vue/cli-plugin-unit-jest": "^3.12.1", + "@vue/cli-service": "^3.12.1", "@vue/eslint-config-standard": "^4.0.0", "@vue/eslint-config-typescript": "^4.0.0", "@vue/test-utils": "1.0.0-beta.29", "babel-core": "7.0.0-bridge.0", "babel-eslint": "^10.0.1", - "eslint": "^5.8.0", - "eslint-plugin-vue": "^5.0.0", + "eslint": "^6.5.1", + "eslint-plugin-vue": "^5.2.3", "jest-localstorage-mock": "^2.4.0", "sass": "^1.22.10", "sass-loader": "^7.3.1", diff --git a/auth-web/public/config/configuration.json b/auth-web/public/config/configuration.json index 4a80bf38a8..85b02ffdcf 100644 --- a/auth-web/public/config/configuration.json +++ b/auth-web/public/config/configuration.json @@ -1,8 +1,11 @@ { "VUE_APP_COPS_REDIRECT_URL": "http://localhost:8081", "VUE_APP_PAY_ROOT_API": "https://pay-api-dev.pathfinder.gov.bc.ca/api/v1", - "VUE_APP_AUTH_ROOT_API": "https://auth-api-dev.pathfinder.gov.bc.ca/api/v1", + "VUE_APP_AUTH_ROOT_API": "http://localhost:5000/api/v1", "VUE_APP_LEGAL_ROOT_API": "https://legal-api-dev.pathfinder.gov.bc.ca/api/v1", "VUE_APP_AUTH_WEB_ROOT_URL": "https://auth-web-dev.pathfinder.gov.bc.ca/auth", - "VUE_APP_FLAVOR": "post-mvp" + "VUE_APP_FLAVOR": "post-mvp", + "VUE_APP_FEATURE_HIDE": { + "BCSC": false + } } diff --git a/auth-web/public/index.html b/auth-web/public/index.html index 32b6b27866..eb5ab7a515 100644 --- a/auth-web/public/index.html +++ b/auth-web/public/index.html @@ -4,7 +4,7 @@ - auth-web + Cooperatives Online diff --git a/auth-web/src/App.vue b/auth-web/src/App.vue index 58e4f7487f..0ae5bdbc12 100644 --- a/auth-web/src/App.vue +++ b/auth-web/src/App.vue @@ -1,8 +1,8 @@ diff --git a/auth-web/src/components/auth/BusinessContactForm.vue b/auth-web/src/components/auth/BusinessContactForm.vue index 5b366d0837..082bee5fe3 100644 --- a/auth-web/src/components/auth/BusinessContactForm.vue +++ b/auth-web/src/components/auth/BusinessContactForm.vue @@ -1,95 +1,100 @@ @@ -182,19 +185,18 @@ export default class BusinessContactForm extends Vue { diff --git a/auth-web/src/components/auth/InviteUsersForm.vue b/auth-web/src/components/auth/InviteUsersForm.vue index 85e767e951..a14c4bbf86 100644 --- a/auth-web/src/components/auth/InviteUsersForm.vue +++ b/auth-web/src/components/auth/InviteUsersForm.vue @@ -47,11 +47,11 @@ + + diff --git a/auth-web/src/components/auth/Unauthorized.vue b/auth-web/src/components/auth/Unauthorized.vue new file mode 100644 index 0000000000..a023a9594d --- /dev/null +++ b/auth-web/src/components/auth/Unauthorized.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/auth-web/src/components/auth/UserProfileForm.vue b/auth-web/src/components/auth/UserProfileForm.vue index 394d853d42..a0761c9e4f 100644 --- a/auth-web/src/components/auth/UserProfileForm.vue +++ b/auth-web/src/components/auth/UserProfileForm.vue @@ -2,12 +2,10 @@
- - {{formError}} + {{formError}}
@@ -15,23 +13,23 @@ @@ -40,12 +38,12 @@ @@ -53,12 +51,12 @@ @@ -66,32 +64,37 @@ + + + + + - Next + Save @@ -99,82 +102,114 @@ diff --git a/auth-web/src/components/pay/PaySystemAlert.vue b/auth-web/src/components/pay/PaySystemAlert.vue index 32c560a61f..782ffe7259 100644 --- a/auth-web/src/components/pay/PaySystemAlert.vue +++ b/auth-web/src/components/pay/PaySystemAlert.vue @@ -3,9 +3,9 @@ + + diff --git a/auth-web/src/views/BusinessProfile.vue b/auth-web/src/views/BusinessProfile.vue index 372c565d72..eda6d2849e 100644 --- a/auth-web/src/views/BusinessProfile.vue +++ b/auth-web/src/views/BusinessProfile.vue @@ -20,12 +20,12 @@ diff --git a/auth-web/src/views/CreateAccount.vue b/auth-web/src/views/CreateAccount.vue index ecb41355ec..a69fd2366f 100644 --- a/auth-web/src/views/CreateAccount.vue +++ b/auth-web/src/views/CreateAccount.vue @@ -29,12 +29,11 @@ @@ -67,7 +104,7 @@ export default class UserProfile extends Vue { margin-bottom: 3rem; } - // Sign In + // Profile Card .profile-card .container { padding: 1.5rem; } diff --git a/auth-web/src/views/management/EntityManagement.vue b/auth-web/src/views/management/EntityManagement.vue index 491610aacc..5ff611126a 100644 --- a/auth-web/src/views/management/EntityManagement.vue +++ b/auth-web/src/views/management/EntityManagement.vue @@ -22,11 +22,13 @@ :title="dialogTitle" :show-icon="false" :show-actions="false" - max-width="640"> + max-width="640" > + > - + + diff --git a/auth-web/tests/unit/PaymentReturnForm.spec.ts b/auth-web/tests/unit/PaymentReturnForm.spec.ts index fb1a9e9f3f..a958a56ed1 100644 --- a/auth-web/tests/unit/PaymentReturnForm.spec.ts +++ b/auth-web/tests/unit/PaymentReturnForm.spec.ts @@ -1,10 +1,10 @@ -import { shallowMount, mount } from '@vue/test-utils' +import { mount, shallowMount } from '@vue/test-utils' import PaymentReturnForm from '@/components/pay/PaymentReturnForm.vue' +import PaymentServices from '../../src/services/payment.services' import Vue from 'vue' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' -import PaymentServices from '../../src/services/payment.services' +import Vuetify from 'vuetify' Vue.use(Vuetify) Vue.use(VueRouter) diff --git a/auth-web/tests/unit/UserProfileForm.spec.ts b/auth-web/tests/unit/UserProfileForm.spec.ts index 3c2a5852c1..ad090acdc6 100644 --- a/auth-web/tests/unit/UserProfileForm.spec.ts +++ b/auth-web/tests/unit/UserProfileForm.spec.ts @@ -1,11 +1,10 @@ +import { Wrapper, createLocalVue, mount } from '@vue/test-utils' import UserModule from '@/store/modules/user' import UserProfileForm from '@/components/auth/UserProfileForm.vue' -import Vuex from 'vuex' -import { mount, createLocalVue, Wrapper } from '@vue/test-utils' import Vue from 'vue' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' -import VuexPersistence from 'vuex-persist' +import Vuetify from 'vuetify' +import Vuex from 'vuex' Vue.use(Vuetify) Vue.use(VueRouter) @@ -24,17 +23,11 @@ describe('UserProfileForm.vue', () => { const localVue = createLocalVue() localVue.use(Vuex) - const vuexPersist = new VuexPersistence({ - key: 'AUTH_WEB', - storage: sessionStorage - }) - const store = new Vuex.Store({ strict: false, modules: { user: UserModule - }, - plugins: [vuexPersist.plugin] + } }) let vuetify = new Vuetify({}) diff --git a/auth-web/tests/unit/affiliatedEntityList.spec.ts b/auth-web/tests/unit/affiliatedEntityList.spec.ts index 193e5a4d72..cce764450e 100644 --- a/auth-web/tests/unit/affiliatedEntityList.spec.ts +++ b/auth-web/tests/unit/affiliatedEntityList.spec.ts @@ -1,12 +1,11 @@ -import VuexPersistence from 'vuex-persist' +import { createLocalVue, shallowMount } from '@vue/test-utils' import AffiliatedEntityList from '@/components/auth/AffiliatedEntityList.vue' +import UserModule from '@/store/modules/user' +import UserService from '../../src/services/user.services' import Vue from 'vue' -import Vuex from 'vuex' -import { createLocalVue, shallowMount } from '@vue/test-utils' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' -import UserService from '../../src/services/user.services' -import UserModule from '@/store/modules/user' +import Vuetify from 'vuetify' +import Vuex from 'vuex' Vue.use(Vuetify) Vue.use(VueRouter) @@ -28,17 +27,11 @@ describe('AffiliatedEntityList.vue', () => { localVue = createLocalVue() localVue.use(Vuex) - const vuexPersist = new VuexPersistence({ - key: 'AUTH_WEB', - storage: sessionStorage - }) - store = new Vuex.Store({ strict: false, modules: { user: UserModule - }, - plugins: [vuexPersist.plugin] + } }) jest.resetModules() diff --git a/auth-web/tests/unit/businessContactForm.spec.ts b/auth-web/tests/unit/businessContactForm.spec.ts index 54ecdf134f..870563aa2a 100644 --- a/auth-web/tests/unit/businessContactForm.spec.ts +++ b/auth-web/tests/unit/businessContactForm.spec.ts @@ -1,11 +1,10 @@ -import BusinessModule from '@/store/modules/business' +import { Wrapper, createLocalVue, mount } from '@vue/test-utils' import BusinessContactForm from '@/components/auth/BusinessContactForm.vue' -import Vuex from 'vuex' -import { mount, createLocalVue, Wrapper } from '@vue/test-utils' +import BusinessModule from '@/store/modules/business' import Vue from 'vue' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' -import VuexPersistence from 'vuex-persist' +import Vuetify from 'vuetify' +import Vuex from 'vuex' Vue.use(Vuetify) Vue.use(VueRouter) @@ -23,17 +22,11 @@ describe('BusinessContactForm.vue', () => { const localVue = createLocalVue() localVue.use(Vuex) - const vuexPersist = new VuexPersistence({ - key: 'AUTH_WEB', - storage: sessionStorage - }) - const store = new Vuex.Store({ strict: false, modules: { business: BusinessModule - }, - plugins: [vuexPersist.plugin] + } }) wrapper = mount(BusinessContactForm, { diff --git a/auth-web/tests/unit/example.spec.ts b/auth-web/tests/unit/example.spec.ts index f84fb39dce..1ef794195e 100644 --- a/auth-web/tests/unit/example.spec.ts +++ b/auth-web/tests/unit/example.spec.ts @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils' import HelloWorld from '@/components/HelloWorld.vue' +import { shallowMount } from '@vue/test-utils' describe('HelloWorld.vue', () => { it('renders props.msg when passed', () => { diff --git a/auth-web/tests/unit/passcodeForm.spec.ts b/auth-web/tests/unit/passcodeForm.spec.ts index 214056ecad..35ab15a9dd 100644 --- a/auth-web/tests/unit/passcodeForm.spec.ts +++ b/auth-web/tests/unit/passcodeForm.spec.ts @@ -1,9 +1,9 @@ +import { createLocalVue, mount } from '@vue/test-utils' import PasscodeForm from '@/components/auth/PasscodeForm.vue' -import Vuex from 'vuex' -import { mount, createLocalVue } from '@vue/test-utils' import Vue from 'vue' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' +import Vuetify from 'vuetify' +import Vuex from 'vuex' Vue.use(Vuetify) Vue.use(VueRouter) diff --git a/auth-web/tests/unit/searchBusinessForm.spec.ts b/auth-web/tests/unit/searchBusinessForm.spec.ts index a016477794..9347e28ac3 100644 --- a/auth-web/tests/unit/searchBusinessForm.spec.ts +++ b/auth-web/tests/unit/searchBusinessForm.spec.ts @@ -1,11 +1,10 @@ +import { createLocalVue, mount } from '@vue/test-utils' import BusinessModule from '@/store/modules/business' import SearchBusinessForm from '@/components/auth/SearchBusinessForm.vue' -import Vuex from 'vuex' -import { mount, createLocalVue, Wrapper } from '@vue/test-utils' import Vue from 'vue' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' -import VuexPersistence from 'vuex-persist' +import Vuetify from 'vuetify' +import Vuex from 'vuex' Vue.use(Vuetify) Vue.use(VueRouter) @@ -29,17 +28,11 @@ describe('SearchBusinessForm.vue', () => { const localVue = createLocalVue() localVue.use(Vuex) - const vuexPersist = new VuexPersistence({ - key: 'AUTH_WEB', - storage: sessionStorage - }) - const store = new Vuex.Store({ strict: false, modules: { business: BusinessModule - }, - plugins: [vuexPersist.plugin] + } }) const $t = () => {} diff --git a/auth-web/tests/unit/signIn.spec.ts b/auth-web/tests/unit/signIn.spec.ts index 5333d72605..48b5b5502c 100644 --- a/auth-web/tests/unit/signIn.spec.ts +++ b/auth-web/tests/unit/signIn.spec.ts @@ -1,11 +1,10 @@ -import UserModule from '@/store/modules/user' +import { Wrapper, createLocalVue, mount } from '@vue/test-utils' import Signin from '@/components/auth/Signin.vue' -import Vuex from 'vuex' -import { mount, createLocalVue, Wrapper } from '@vue/test-utils' +import UserModule from '@/store/modules/user' import Vue from 'vue' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' -import VuexPersistence from 'vuex-persist' +import Vuetify from 'vuetify' +import Vuex from 'vuex' Vue.use(Vuetify) Vue.use(VueRouter) @@ -25,17 +24,11 @@ describe('Signin.vue', () => { const localVue = createLocalVue() localVue.use(Vuex) - const vuexPersist = new VuexPersistence({ - key: 'AUTH_WEB', - storage: sessionStorage - }) - const store = new Vuex.Store({ strict: false, modules: { user: UserModule - }, - plugins: [vuexPersist.plugin] + } }) let vuetify = new Vuetify({}) diff --git a/auth-web/tests/unit/signOut.spec.ts b/auth-web/tests/unit/signOut.spec.ts index a623044928..efd051bbb7 100644 --- a/auth-web/tests/unit/signOut.spec.ts +++ b/auth-web/tests/unit/signOut.spec.ts @@ -1,11 +1,10 @@ -import UserModule from '@/store/modules/user' +import { Wrapper, createLocalVue, mount } from '@vue/test-utils' import Signout from '@/components/auth/Signout.vue' -import Vuex from 'vuex' -import { mount, createLocalVue, Wrapper } from '@vue/test-utils' +import UserModule from '@/store/modules/user' import Vue from 'vue' -import Vuetify from 'vuetify' import VueRouter from 'vue-router' -import VuexPersistence from 'vuex-persist' +import Vuetify from 'vuetify' +import Vuex from 'vuex' Vue.use(Vuetify) Vue.use(VueRouter) @@ -17,17 +16,11 @@ describe('Signout.vue', () => { const localVue = createLocalVue() localVue.use(Vuex) - const vuexPersist = new VuexPersistence({ - key: 'AUTH_WEB', - storage: sessionStorage - }) - const store = new Vuex.Store({ strict: false, modules: { user: UserModule - }, - plugins: [vuexPersist.plugin] + } }) wrapper = mount(Signout, { diff --git a/auth-web/tests/unit/user.services.spec.ts b/auth-web/tests/unit/user.services.spec.ts index d753025616..04452e16c8 100644 --- a/auth-web/tests/unit/user.services.spec.ts +++ b/auth-web/tests/unit/user.services.spec.ts @@ -1,6 +1,6 @@ import Axios from 'axios' -import UserService from '../../src/services/user.services' import { Contact } from '../../src/models/contact' +import UserService from '../../src/services/user.services' jest.mock('axios', () => ({ get: jest.fn(), diff --git a/auth-web/vue.config.js b/auth-web/vue.config.js index 6bc2518717..b42de9ed58 100644 --- a/auth-web/vue.config.js +++ b/auth-web/vue.config.js @@ -1,24 +1,20 @@ +var path = require('path') module.exports = { configureWebpack: { - devtool: 'source-map' + devtool: 'source-map', + resolve: { + alias: { + 'vue': path.resolve('./node_modules/vue'), + '$assets': path.resolve('./src/assets/') + } + } }, publicPath: process.env.VUE_APP_PATH, - transpileDependencies: ['vuex-persist', 'vuetify'], + transpileDependencies: ['vuetify'], devServer: { - // not used - proxy: { - '/auth/api/*': { - target: 'https://auth-api-dev.pathfinder.gov.bc.ca/api/v1', // if your local server is running , use that here - pathRewrite: { - '/auth/api/': '' - } - }, - '/pay/api/*': { // TODO I havent tested this..but hopefully it will work.. - target: 'https://pay-api-dev.pathfinder.gov.bc.ca/api/v1', - pathRewrite: { - '/pay/api/': '' - } - } + overlay: { + warnings: true, + errors: true } } } diff --git a/browserstack-logo-white-small.png b/browserstack-logo-white-small.png new file mode 100644 index 0000000000..55472a6dcd Binary files /dev/null and b/browserstack-logo-white-small.png differ diff --git a/notify-api/jenkins/dev.groovy b/notify-api/jenkins/dev.groovy index cc480d8e61..7219f9f12b 100755 --- a/notify-api/jenkins/dev.groovy +++ b/notify-api/jenkins/dev.groovy @@ -163,12 +163,7 @@ if( run_pipeline ) { openshift.withProject("${NAMESPACE_BUILD}") { try { echo "Tagging ${APP_NAME} for deployment to ${E2E_TAG} ..." - - // Don't tag with BUILD_ID so the pruner can do it's job; it won't delete tagged images. - // Tag the images for deployment based on the image's hash - def IMAGE_HASH = getImageTagHash("${APP_NAME}") - echo "IMAGE_HASH: ${IMAGE_HASH}" - openshift.tag("${APP_NAME}@${IMAGE_HASH}", "${APP_NAME}:${E2E_TAG}") + openshift.tag("${APP_NAME}:${DESTINATION_TAG}", "${APP_NAME}:${E2E_TAG}") } catch (Exception e) { echo e.getMessage() build_ok = false diff --git a/notify-api/jenkins/prod.groovy b/notify-api/jenkins/prod.groovy index f537461d97..c9b5bd42fd 100755 --- a/notify-api/jenkins/prod.groovy +++ b/notify-api/jenkins/prod.groovy @@ -58,6 +58,11 @@ node { } openshift.withCluster() { openshift.withProject("${NAMESPACE_BUILD}") { + echo "Tagging ${APP_NAME}:${DESTINATION_TAG}-prev ..." + def IMAGE_HASH = getImageTagHash("${APP_NAME}", "${DESTINATION_TAG}") + echo "IMAGE_HASH: ${IMAGE_HASH}" + openshift.tag("${APP_NAME}@${IMAGE_HASH}", "${APP_NAME}:${DESTINATION_TAG}-prev") + echo "Tagging ${APP_NAME} for deployment to ${DESTINATION_TAG} ..." openshift.tag("${APP_NAME}:${SOURCE_TAG}", "${APP_NAME}:${DESTINATION_TAG}") } diff --git a/notify-api/openshift/templates/notify-api-deploy.json b/notify-api/openshift/templates/notify-api-deploy.json index 534ec530de..51be38c0cc 100755 --- a/notify-api/openshift/templates/notify-api-deploy.json +++ b/notify-api/openshift/templates/notify-api-deploy.json @@ -141,6 +141,13 @@ "protocol": "TCP" } ], + "envFrom": [ + { + "configMapRef": { + "name": "api-${TAG_NAME}-config" + } + } + ], "env": [ { "name": "DATABASE_USERNAME", @@ -231,177 +238,6 @@ "key": "DATABASE_TEST_PORT" } } - }, - { - "name": "JWT_OIDC_ALGORITHMS", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_ALGORITHMS" - } - } - }, - { - "name": "JWT_OIDC_AUDIENCE", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_AUDIENCE" - } - } - }, - { - "name": "JWT_OIDC_CLIENT_SECRET", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_CLIENT_SECRET" - } - } - }, - { - "name": "JWT_OIDC_WELL_KNOWN_CONFIG", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_WELL_KNOWN_CONFIG" - } - } - }, - { - "name": "JWT_OIDC_JWKS_CACHE_TIMEOUT", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "JWT_OIDC_JWKS_CACHE_TIMEOUT" - } - } - }, - { - "name": "KEYCLOAK_BASE_URL", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_BASE_URL" - } - } - }, - { - "name": "KEYCLOAK_REALMNAME", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_REALMNAME" - } - } - }, - { - "name": "KEYCLOAK_ADMIN_CLIENTID", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_ADMIN_CLIENTID" - } - } - }, - { - "name": "KEYCLOAK_ADMIN_SECRET", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_ADMIN_SECRET" - } - } - }, - { - "name": "KEYCLOAK_AUTH_AUDIENCE", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_AUTH_AUDIENCE" - } - } - }, - { - "name": "KEYCLOAK_AUTH_CLIENT_SECRET", - "valueFrom": { - "secretKeyRef": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "key": "KEYCLOAK_AUTH_CLIENT_SECRET" - } - } - }, - { - "name": "MAIL_SERVER", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "MAIL_SERVER" - } - } - }, - { - "name": "MAIL_PORT", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "MAIL_PORT" - } - } - }, - { - "name": "MAIL_USE_TLS", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "MAIL_USE_TLS" - } - } - }, - { - "name": "MAIL_USE_SSL", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "MAIL_USE_SSL" - } - } - }, - { - "name": "MAIL_USERNAME", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "MAIL_USERNAME" - } - } - }, - { - "name": "MAIL_PASSWORD", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "MAIL_PASSWORD" - } - } - }, - { - "name": "MAIL_FROM_ID", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "MAIL_FROM_ID" - } - } - }, - { - "name": "SENTRY_DSN", - "valueFrom": { - "configMapKeyRef": { - "name": "${NAME}-${TAG_NAME}-config", - "key": "SENTRY_DSN" - } - } } ], "resources": { diff --git a/notify-api/openshift/templates/notify-api-pre-deploy.json b/notify-api/openshift/templates/notify-api-pre-deploy.json deleted file mode 100755 index c367659a9e..0000000000 --- a/notify-api/openshift/templates/notify-api-pre-deploy.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "kind": "Template", - "apiVersion": "v1", - "metadata": { - "annotations": { - "description": "Pre deployment template for a auth api service. This template may not include real value of secrets; you need to manually replace the value in Openshift.", - "tags": "${NAME}-${TAG_NAME}" - }, - "name": "${NAME}-pre-deploy" - }, - "objects": [ - { - "kind": "ConfigMap", - "apiVersion": "v1", - "metadata": { - "name": "${NAME}-${TAG_NAME}-config", - "labels": { - "app": "${NAME}-${TAG_NAME}", - "app-group": "${APP_GROUP}", - "template": "${NAME}-pre-deploy" - } - }, - "data": { - "POD_TESTING": "True", - "MAIL_SERVER": "apps.smtp.gov.bc.ca", - "MAIL_PORT": "25", - "MAIL_USE_TLS": "False", - "MAIL_USE_SSL": "False", - "MAIL_USERNAME": "", - "MAIL_PASSWORD": "", - "MAIL_FROM_ID": "Sofi.Dev@gov.bc.ca", - "SENTRY_DSN": "" - } - } - ], - "parameters": [ - { - "name": "NAME", - "displayName": "Name", - "description": "The name assigned to all of the OpenShift resources associated to the server instance.", - "required": true, - "value": "notify-api" - }, - { - "name": "APP_GROUP", - "displayName": "App Group", - "description": "The name assigned to all of the deployments in this project.", - "required": true, - "value": "sbc-auth" - }, - { - "name": "TAG_NAME", - "displayName": "Environment TAG name", - "description": "The TAG name for this environment, e.g., dev, test, prod", - "required": true, - "value": "dev" - } - ] -} \ No newline at end of file diff --git a/notify-api/requirements.txt b/notify-api/requirements.txt index 53652ed21b..956a745090 100644 --- a/notify-api/requirements.txt +++ b/notify-api/requirements.txt @@ -5,7 +5,7 @@ blinker==1.4 certifi==2019.9.11 chardet==3.0.4 Click==7.0 -ecdsa==0.13.2 +ecdsa==0.13.3 Flask==1.1.1 flask-jwt-oidc==0.1.5 Flask-Mail==0.9.1 diff --git a/auth-api/openshift/templates/auth-api-pre-deploy.json b/openshift/templates/api-config.json similarity index 88% rename from auth-api/openshift/templates/auth-api-pre-deploy.json rename to openshift/templates/api-config.json index f754915c13..d487e5537e 100644 --- a/auth-api/openshift/templates/auth-api-pre-deploy.json +++ b/openshift/templates/api-config.json @@ -3,10 +3,10 @@ "apiVersion": "v1", "metadata": { "annotations": { - "description": "Pre deployment template for a auth api service. This template may not include real value of secrets; you need to manually replace the value in Openshift.", + "description": "Configmap template for the api service. This template may not include real value of secrets; you need to manually replace the value in Openshift.", "tags": "${NAME}-${TAG_NAME}" }, - "name": "${NAME}-pre-deploy" + "name": "${NAME}-config" }, "objects": [ { @@ -17,26 +17,12 @@ "labels": { "app": "${NAME}-${TAG_NAME}", "app-group": "${APP_GROUP}", - "template": "${NAME}-pre-deploy" + "template": "${NAME}-config" } }, "data": { "POD_TESTING": "true", - "SENTRY_DSN": "https://@sentry.io/" - } - }, - { - "kind": "Secret", - "apiVersion": "v1", - "metadata": { - "name": "${KEYCLOAK_NAME}-${TAG_NAME}-secret", - "labels": { - "app": "${NAME}-${TAG_NAME}", - "app-group": "${APP_GROUP}", - "template": "${NAME}-pre-deploy" - } - }, - "stringData": { + "SENTRY_DSN": "https://@sentry.io/", "JWT_OIDC_ALGORITHMS": "${JWT_OIDC_ALGORITHMS}", "JWT_OIDC_AUDIENCE": "${JWT_OIDC_AUDIENCE}", "JWT_OIDC_CLIENT_SECRET": "${JWT_OIDC_CLIENT_SECRET}", @@ -59,9 +45,9 @@ "AUTH_WEB_TOKEN_CONFIRM_URL": "${AUTH_WEB_TOKEN_CONFIRM_URL}", "EMAIL_SECURITY_PASSWORD_SALT": "${EMAIL_SECURITY_PASSWORD_SALT}", "EMAIL_TOKEN_SECRET_KEY": "${EMAIL_TOKEN_SECRET_KEY}", - "TOKEN_EXPIRY_PERIOD": "${TOKEN_EXPIRY_PERIOD}" - }, - "type": "Opaque" + "TOKEN_EXPIRY_PERIOD": "${TOKEN_EXPIRY_PERIOD}", + "LEGAL_API_URL": "" + } } ], "parameters": [ @@ -70,7 +56,7 @@ "displayName": "Name", "description": "The name assigned to all of the OpenShift resources associated to the server instance.", "required": true, - "value": "auth-api" + "value": "api" }, { "name": "APP_GROUP", @@ -239,4 +225,4 @@ "value": "https://account.sentry.ioo/project/id" } ] -} +} \ No newline at end of file