diff --git a/.gitignore b/.gitignore index e548abf22..d89843e79 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,5 @@ venv.bak/ # IDE /**/.vscode +*/.idea/* diff --git a/colin-api/.envrc b/colin-api/.envrc new file mode 100644 index 000000000..e922dd51a --- /dev/null +++ b/colin-api/.envrc @@ -0,0 +1,3 @@ +##while read -r line; do export $line; echo $line; done < .env +source venv/bin/activate +source .env diff --git a/colin-api/LICENSE b/colin-api/LICENSE new file mode 100644 index 000000000..18b5abc34 --- /dev/null +++ b/colin-api/LICENSE @@ -0,0 +1,13 @@ +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. diff --git a/colin-api/MANIFEST.in b/colin-api/MANIFEST.in new file mode 100644 index 000000000..a02917249 --- /dev/null +++ b/colin-api/MANIFEST.in @@ -0,0 +1,5 @@ +include requirements/prod.txt +include config.py +include logging.conf +include LICENSE +include README.md \ No newline at end of file diff --git a/colin-api/Makefile b/colin-api/Makefile new file mode 100644 index 000000000..929bff73c --- /dev/null +++ b/colin-api/Makefile @@ -0,0 +1,153 @@ +.PHONY: license +.PHONY: setup clean clean-build clean-pyc clean-test + +.PHONY: docker-setup network build start qa style safety test test-travis flake8 \ +isort isort-save stop docker-clean logs +.PHONY: mac-cov pylint flake8 + +SHELL:=/bin/bash +mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) +current_dir := $(notdir $(patsubst %/,%,$(dir $(mkfile_path)))) +current_abs_dir := $(patsubst %/,%,$(dir $(mkfile_path))) + +################################################################################# +# COMMANDS # +################################################################################# +clean: clean-build clean-pyc clean-test + rm -rf venv/ + +clean-build: + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: + find . -name '.pytest_cache' -exec rm -fr {} + + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +setup: clean venv/bin/activate install-dev + +venv/bin/activate: requirements/prod.txt requirements/dev.txt + rm -rf venv/ + test -f venv/bin/activate || python3 -m venv $(current_abs_dir)/venv + . venv/bin/activate ;\ + pip install --upgrade pip ;\ + pip install -Ur requirements/prod.txt ;\ + pip freeze | sort > requirements.txt ;\ + pip install -Ur requirements/dev.txt + touch venv/bin/activate # update so it's as new as requirements/prod.txt + +.PHONY: install-dev +install-dev: venv/bin/activate + . venv/bin/activate ; \ + pip install -e . + +.PHONY: activate +activate: venv/bin/activate + . venv/bin/activate + +.PHONY: local-test +local-test: venv/bin/activate + . venv/bin/activate ; \ + pytest + +.PHONY: local-coverage +local-coverage: venv/bin/activate + . venv/bin/activate ; \ + coverage run -m pytest + +.PHONY: coverage-report +coverage-report: local-coverage + . venv/bin/activate ; \ + coverage report ; \ + coverage html + +## Run the coverage report and display in a browser window +mac-cov: install-dev coverage-report + open -a "Google Chrome" htmlcov/index.html + +## run pylint on the package and tests +pylint: + pylint --rcfile=setup.cfg \ + --load-plugins=pylint_flask \ + --disable=C0301,W0511 \ + src/colin_api + +## run flake8 on the package and tests +flake8: + flake8 src/colin_api tests + +## Verify source code license headers. +license: + ./scripts/verify_license_headers.sh src/colin_api tests + +################################################################################# +# Self Documenting Commands # +################################################################################# + +.DEFAULT_GOAL := show-help + +# Inspired by +# sed script explained: +# /^##/: +# * save line in hold space +# * purge line +# * Loop: +# * append newline + line to hold space +# * go to next line +# * if line starts with doc comment, strip comment character off and loop +# * remove target prerequisites +# * append hold space (+ newline) to line +# * replace newline plus comments by `---` +# * print line +# Separate expressions are necessary because labels cannot be delimited by +# semicolon; see +.PHONY: show-help +show-help: + @echo "$$(tput bold)Available rules:$$(tput sgr0)" + @echo + @sed -n -e "/^## / { \ + h; \ + s/.*//; \ + :doc" \ + -e "H; \ + n; \ + s/^## //; \ + t doc" \ + -e "s/:.*//; \ + G; \ + s/\\n## /---/; \ + s/\\n/ /g; \ + p; \ + }" ${MAKEFILE_LIST} \ + | LC_ALL='C' sort --ignore-case \ + | awk -F '---' \ + -v ncol=$$(tput cols) \ + -v indent=19 \ + -v col_on="$$(tput setaf 6)" \ + -v col_off="$$(tput sgr0)" \ + '{ \ + printf "%s%*s%s ", col_on, -indent, $$1, col_off; \ + n = split($$2, words, " "); \ + line_length = ncol - indent; \ + for (i = 1; i <= n; i++) { \ + line_length -= length(words[i]) + 1; \ + if (line_length <= 0) { \ + line_length = ncol - indent - length(words[i]) - 1; \ + printf "\n%*s ", -indent, " "; \ + } \ + printf "%s ", words[i]; \ + } \ + printf "\n"; \ + }' \ + | more $(shell test $(shell uname) = Darwin && echo '--no-init --raw-control-chars') diff --git a/colin-api/README.md b/colin-api/README.md new file mode 100644 index 000000000..af9de7d2e --- /dev/null +++ b/colin-api/README.md @@ -0,0 +1,67 @@ + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) + + +# Application Name + +BC Registries Legal Entities COLIN API + +## Technology Stack Used +* Python, Flask + +## Third-Party Products/Libraries used and the the License they are covert by + +## Project Status +As of 2019-04-09 in Development + +## Documnentation + +GitHub Pages (https://guides.github.com/features/pages/) are a neat way to document you application/project. + +## Security + +TBD + +## Files in this repository + +``` +openshift/ - OpenShift-specific files +├── scripts - helper scripts +└── templates - application templates +``` + +## Deployment (Local Development) + +* Developer Workstation Requirements/Setup +* Application Specific Setup + +## Deployment (OpenShift) + +See (openshift/Readme.md) + +## Getting Help or Reporting an Issue + +To report bugs/issues/feature requests, please file an [issue](../../issues). + +## How to Contribute + +If you would like to contribute, please see our [CONTRIBUTING](./CONTRIBUTING.md) guidelines. + +Please note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). +By participating in this project you agree to abide by its terms. + +## License + + 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. diff --git a/colin-api/config.py b/colin-api/config.py new file mode 100644 index 000000000..6756d7d07 --- /dev/null +++ b/colin-api/config.py @@ -0,0 +1,176 @@ +# 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. +"""All of the configuration for the service is captured here + All items are loaded, or have Constants defined here that + are loaded into the Flask configuration. + All modules and lookups get their configuration from the + Flask config, rather than reading environment variables directly + or by accessing this configuration directly. +""" + +import os +import sys +from dotenv import load_dotenv, find_dotenv + +# this will load all the envars from a .env file located in the project root (api) +load_dotenv(find_dotenv()) + +CONFIGURATION = { + "development": "config.DevConfig", + "testing": "config.TestConfig", + "production": "config.ProdConfig", + "default": "config.ProdConfig" +} + + +def get_named_config(config_name: str = 'production'): + """Return the configuration object based on the name + + :raise: KeyError: if an unknown configuration is requested + """ + if config_name in['production', 'staging', 'default']: + config = ProdConfig() + elif config_name == 'testing': + config = TestConfig() + elif config_name == 'development': + config = DevConfig() + else: + raise KeyError(f"Unknown configuration '{config_name}'") + return config + + +class _Config(object): # pylint: disable=too-few-public-methods + """Base class configuration that should set reasonable defaults + for all the other configurations + """ + PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) + + SECRET_KEY = 'a secret' + + SQLALCHEMY_TRACK_MODIFICATIONS = False + + + # ORACLE - CDEV/CTST/CPRD + ORACLE_USER = os.getenv('ORACLE_USER', '') + ORACLE_SCHEMA = os.getenv('ORACLE_SCHEMA', None) + ORACLE_PASSWORD = os.getenv('ORACLE_PASSWORD', '') + ORACLE_DB_NAME = os.getenv('ORACLE_DB_NAME', '') + ORACLE_HOST = os.getenv('ORACLE_HOST', '') + ORACLE_PORT = int(os.getenv('ORACLE_PORT', '1521')) + + # JWT_OIDC Settings + JWT_OIDC_WELL_KNOWN_CONFIG = os.getenv('JWT_OIDC_WELL_KNOWN_CONFIG') + JWT_OIDC_ALGORITHMS = os.getenv('JWT_OIDC_ALGORITHMS') + JWT_OIDC_JWKS_URI = os.getenv('JWT_OIDC_JWKS_URI') + JWT_OIDC_ISSUER = os.getenv('JWT_OIDC_ISSUER') + JWT_OIDC_AUDIENCE = os.getenv('JWT_OIDC_AUDIENCE') + JWT_OIDC_CLIENT_SECRET = os.getenv('JWT_OIDC_CLIENT_SECRET') + JWT_OIDC_CACHING_ENABLED = os.getenv('JWT_OIDC_CACHING_ENABLED') + try: + JWT_OIDC_JWKS_CACHE_TIMEOUT = int(os.getenv('JWT_OIDC_JWKS_CACHE_TIMEOUT')) + except (ValueError, TypeError, ): + JWT_OIDC_JWKS_CACHE_TIMEOUT = 300 + + TESTING = False + DEBUG = False + + +class DevConfig(_Config): # pylint: disable=too-few-public-methods + TESTING = False + DEBUG = True + + +class TestConfig(_Config): # pylint: disable=too-few-public-methods + """In support of testing only + used by the py.test suite + """ + DEBUG = True + TESTING = True + + # TEST ORACLE + ORACLE_USER = os.getenv('TEST_ORACLE_USER', '') + ORACLE_SCHEMA = os.getenv('TEST_ORACLE_SCHEMA', None) + ORACLE_PASSWORD = os.getenv('TEST_ORACLE_PASSWORD', '') + ORACLE_DB_NAME = os.getenv('TEST_ORACLE_DB_NAME', '') + ORACLE_HOST = os.getenv('TEST_ORACLE_HOST', '') + ORACLE_PORT = int(os.getenv('TEST_ORACLE_PORT', '1521')) + + + # JWT OIDC settings + # JWT_OIDC_TEST_MODE will set jwt_manager to use + JWT_OIDC_TEST_MODE = True + JWT_OIDC_TEST_AUDIENCE = os.getenv('JWT_OIDC_AUDIENCE') + JWT_OIDC_TEST_CLIENT_SECRET = os.getenv('JWT_OIDC_CLIENT_SECRET') + JWT_OIDC_TEST_ISSUER = 'https://sso-dev.pathfinder.gov.bc.ca/auth/realms/sbc' + JWT_OIDC_TEST_KEYS = { + "keys": [ + { + "kid": "flask-jwt-oidc-test-client", + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "n": "AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR", + "e": "AQAB" + } + ] + } + + JWT_OIDC_TEST_PRIVATE_KEY_JWKS = { + "keys": [ + { + "kid": "flask-jwt-oidc-test-client", + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "n": "AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR", + "e": "AQAB", + "d": "C0G3QGI6OQ6tvbCNYGCqq043YI_8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhskURaDwk4-8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh_xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0", + "p": "APXcusFMQNHjh6KVD_hOUIw87lvK13WkDEeeuqAydai9Ig9JKEAAfV94W6Aftka7tGgE7ulg1vo3eJoLWJ1zvKM", + "q": "AOjX3OnPJnk0ZFUQBwhduCweRi37I6DAdLTnhDvcPTrrNWuKPg9uGwHjzFCJgKd8KBaDQ0X1rZTZLTqi3peT43s", + "dp": "AN9kBoA5o6_Rl9zeqdsIdWFmv4DB5lEqlEnC7HlAP-3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhc", + "dq": "ANtbSY6njfpPploQsF9sU26U0s7MsuLljM1E8uml8bVJE1mNsiu9MgpUvg39jEu9BtM2tDD7Y51AAIEmIQex1nM", + "qi": "XLE5O360x-MhsdFXx8Vwz4304-MJg-oGSJXCK_ZWYOB_FGXFRTfebxCsSYi0YwJo-oNu96bvZCuMplzRI1liZw" + } + ] + } + + JWT_OIDC_TEST_PRIVATE_KEY_PEM = """ + -----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDfn1nKQshOSj8xw44oC2klFWSNLmK3BnHONCJ1bZfq0EQ5gIfg + tlvB+Px8Ya+VS3OnK7Cdi4iU1fxO9ktN6c6TjmmmFevk8wIwqLthmCSF3r+3+h4e + ddj7hucMsXWv05QUrCPoL6YUUz7Cgpz7ra24rpAmK5z7lsV+f3BEvXkrUQIDAQAB + AoGAC0G3QGI6OQ6tvbCNYGCqq043YI/8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhs + kURaDwk4+8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh/ + xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0CQQD13LrBTEDR44ei + lQ/4TlCMPO5bytd1pAxHnrqgMnWovSIPSShAAH1feFugH7ZGu7RoBO7pYNb6N3ia + C1idc7yjAkEA6Nfc6c8meTRkVRAHCF24LB5GLfsjoMB0tOeEO9w9Ous1a4o+D24b + AePMUImAp3woFoNDRfWtlNktOqLel5PjewJBAN9kBoA5o6/Rl9zeqdsIdWFmv4DB + 5lEqlEnC7HlAP+3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhcCQQDb + W0mOp436T6ZaELBfbFNulNLOzLLi5YzNRPLppfG1SRNZjbIrvTIKVL4N/YxLvQbT + NrQw+2OdQACBJiEHsdZzAkBcsTk7frTH4yGx0VfHxXDPjfTj4wmD6gZIlcIr9lZg + 4H8UZcVFN95vEKxJiLRjAmj6g273pu9kK4ymXNEjWWJn + -----END RSA PRIVATE KEY-----""" + + +class ProdConfig(_Config): # pylint: disable=too-few-public-methods + """Production environment configuration.""" + + SECRET_KEY = os.getenv('SECRET_KEY', None) + + if not SECRET_KEY: + SECRET_KEY = os.urandom(24) + print("WARNING: SECRET_KEY being set as a one-shot", file=sys.stderr) + + TESTING = False + DEBUG = False diff --git a/colin-api/gunicorn_config.py b/colin-api/gunicorn_config.py new file mode 100644 index 000000000..c4eb50acf --- /dev/null +++ b/colin-api/gunicorn_config.py @@ -0,0 +1,24 @@ +# 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. +"""The configuration for gunicorn, which picks up the + runtime options from environment variables +""" + +import os + +workers = int(os.environ.get('GUNICORN_PROCESSES', '1')) # pylint: disable=invalid-name +threads = int(os.environ.get('GUNICORN_THREADS', '1')) # pylint: disable=invalid-name + +forwarded_allow_ips = '*' # pylint: disable=invalid-name +secure_scheme_headers = {'X-Forwarded-Proto': 'https'} # pylint: disable=invalid-name diff --git a/colin-api/logging.conf b/colin-api/logging.conf new file mode 100644 index 000000000..ffc1a01e3 --- /dev/null +++ b/colin-api/logging.conf @@ -0,0 +1,28 @@ +[loggers] +keys=root,api + +[handlers] +keys=console + +[formatters] +keys=simple + +[logger_root] +level=DEBUG +handlers=console + +[logger_api] +level=DEBUG +handlers=console +qualname=api +propagate=0 + +[handler_console] +class=StreamHandler +level=DEBUG +formatter=simple +args=(sys.stdout,) + +[formatter_simple] +format=%(asctime)s - %(name)s - %(levelname)s in %(module)s:%(filename)s:%(lineno)d - %(funcName)s: %(message)s +datefmt= \ No newline at end of file diff --git a/colin-api/manage.py b/colin-api/manage.py new file mode 100644 index 000000000..e94c7cb1d --- /dev/null +++ b/colin-api/manage.py @@ -0,0 +1,49 @@ +# 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. + +"""Manage the database and some other items required to run the API +""" +import logging + +from flask import url_for +from flask_script import Manager # class for handling a set of commands + +from colin_api import create_app + +APP = create_app() +MANAGER = Manager(APP) + + + +@MANAGER.command +def list_routes(): + output = [] + for rule in APP.url_map.iter_rules(): + + options = {} + for arg in rule.arguments: + options[arg] = "[{0}]".format(arg) + + methods = ','.join(rule.methods) + url = url_for(rule.endpoint, **options) + line = ("{:50s} {:20s} {}".format(rule.endpoint, methods, url)) + output.append(line) + + for line in sorted(output): + print(line) + + +if __name__ == '__main__': + logging.log(logging.INFO, 'Running the Manager') + MANAGER.run() diff --git a/colin-api/requirements/dev.txt b/colin-api/requirements/dev.txt new file mode 100644 index 000000000..78b116bd1 --- /dev/null +++ b/colin-api/requirements/dev.txt @@ -0,0 +1,21 @@ +# Everything the developer needs in addition to the production requirements +-r prod.txt + +# Testing +pytest<4.1 +pytest-mock +requests +pyhamcrest + +# Lint and code style +flake8 +flake8-blind-except +flake8-debugger +flake8-docstrings +flake8-isort +flake8-quotes +pep8-naming +autopep8 +coverage +pylint +pylint-flask diff --git a/colin-api/requirements/prod.txt b/colin-api/requirements/prod.txt new file mode 100644 index 000000000..3efba4fb1 --- /dev/null +++ b/colin-api/requirements/prod.txt @@ -0,0 +1,10 @@ +cx_Oracle +gunicorn +Flask +Flask-Script +Flask-Moment +Flask-RESTplus +flask-jwt-oidc>=0.1.5 +python-dotenv +psycopg2-binary +jsonschema diff --git a/colin-api/scripts/list_lint_exemptions.sh b/colin-api/scripts/list_lint_exemptions.sh new file mode 100644 index 000000000..5bb1a9ab1 --- /dev/null +++ b/colin-api/scripts/list_lint_exemptions.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# 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. + +# simple grep with full line output, to evaluate whether use of pylint disabling is valid +grep -r --color -n -s 'pylint: disable=.*' $@ + +exit 0 diff --git a/colin-api/scripts/verify_license_headers.sh b/colin-api/scripts/verify_license_headers.sh new file mode 100644 index 000000000..028b95c63 --- /dev/null +++ b/colin-api/scripts/verify_license_headers.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# 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. + + +COPYRIGHT="Copyright © 2019 Province of British Columbia" +RET=0 + +for file in $(find $@ -not \( -path */venv -prune \) -not \( -path */migrations -prune \) -not \( -path */tests -prune \) -not \( -path */.egg* -prune \) -name \*.py) +do + grep "${COPYRIGHT}" ${file} >/dev/null + if [[ $? != 0 ]] + then + echo "${file} missing copyright header" + RET=1 + fi +done +exit ${RET} diff --git a/colin-api/setup.cfg b/colin-api/setup.cfg new file mode 100644 index 000000000..24d389302 --- /dev/null +++ b/colin-api/setup.cfg @@ -0,0 +1,111 @@ +[metadata] +name = colin_api +url = https://github.com/bcgov/colin_api +author = team-le +author_email = katie@silverbirchsolutions.com +classifiers = + Development Status :: Beta + Intended Audience :: Developers / QA + Topic :: Legal Entities + License :: OSI Approved :: Apache Software License + Natural Language :: English + Programming Language :: Python :: 3.7 +license = Apache Software License Version 2.0 +description = A short description of the project +long_description = file: README.md +keywords = + +[options] +zip_safe = True +python_requires = >=3.6 +include_package_data = True +packages = find: + +[options.package_data] +colin_api = + +[wheel] +universal = 1 + +[bdist_wheel] +universal = 1 + +[aliases] +test = pytest + +[flake8] +exclude = .git,*migrations* +max-line-length = 120 +docstring-min-length=10 +per-file-ignores = + */__init__.py:F401 + +[pycodestyle] +max_line_length = 120 +ignore = E501 +docstring-min-length=10 +notes=FIXME,XXX # TODO is ignored +match_dir = src/colin_api +ignored-modules=flask_sqlalchemy + sqlalchemy +per-file-ignores = + */__init__.py:F401 +good-names= + b, + d, + i, + e, + f, + u, + rv, + logger, + +[pylint] +ignore=migrations,test +max_line_length=120 +notes=FIXME,XXX,TODO +ignored-modules=flask_sqlalchemy,sqlalchemy,SQLAlchemy,alembic,scoped_session +ignored-classes=scoped_session + +[isort] +line_length = 120 +indent = 4 +multi_line_output = 4 +lines_after_imports = 2 + +[tool:pytest] +minversion = 2.0 +testpaths = tests +addopts = --verbose + --strict + -p no:warnings +python_files = tests/*/test*.py +norecursedirs = .git .tox venv* requirements* build +log_cli = true +log_cli_level = 1 +filterwarnings = + ignore::UserWarning +markers = + slow + serial + +[coverage:run] +branch = True +source = + src/colin_api +omit = + src/colin_api/wsgi.py + src/colin_api/gunicorn_config.py + +[report:run] +exclude_lines = + pragma: no cover + from + import + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: diff --git a/colin-api/setup.py b/colin-api/setup.py new file mode 100644 index 000000000..e3de5590a --- /dev/null +++ b/colin-api/setup.py @@ -0,0 +1,61 @@ +# 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. +"""Installer and setup for this module +""" +from glob import glob +from os.path import basename, splitext + +from setuptools import setup, find_packages + + +def read_requirements(filename): + """ + Get application requirements from + the requirements.txt file. + :return: Python requirements + :rtype: list + """ + with open(filename, 'r') as req: + requirements = req.readlines() + install_requires = [r.strip() for r in requirements if r.find('git+') != 0] + return install_requires + + +def read(filepath): + """ + Read the contents from a file. + :param str filepath: path to the file to be read + :return: file contents + :rtype: str + """ + with open(filepath, 'r') as file_handle: + content = file_handle.read() + return content + + +REQUIREMENTS = read_requirements('requirements.txt') + +setup( + name="colin_api", + packages=find_packages('src'), + package_dir={'': 'src'}, + py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + include_package_data=True, + license=read('LICENSE'), + long_description=read('README.md'), + zip_safe=False, + install_requires=REQUIREMENTS, + setup_requires=["pytest-runner", ], + tests_require=["pytest", ], +) diff --git a/colin-api/src/colin_api/__init__.py b/colin-api/src/colin_api/__init__.py new file mode 100644 index 000000000..fd26c2d9b --- /dev/null +++ b/colin-api/src/colin_api/__init__.py @@ -0,0 +1,72 @@ +# 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. +"""The Legal API service. + +This module is the API for the Legal Entity system. +""" +import os + +from flask import Flask +from flask_jwt_oidc import JwtManager + +import config +from colin_api.resources import API_BLUEPRINT, OPS_BLUEPRINT +from colin_api.utils.logging import setup_logging +from colin_api.utils.run_version import get_run_version + + +setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first + +# lower case name as used by convention in most Flask apps +jwt = JwtManager() # pylint: disable=invalid-name + + +def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): + """Return a configured Flask App using the Factory method.""" + app = Flask(__name__) + app.config.from_object(config.CONFIGURATION[run_mode]) + + app.register_blueprint(API_BLUEPRINT) + app.register_blueprint(OPS_BLUEPRINT) + setup_jwt_manager(app, jwt) + + @app.after_request + def add_version(response): # pylint: disable=unused-variable + version = get_run_version() + response.headers['API'] = f'colin_api/{version}' + return response + + register_shellcontext(app) + + return app + + +def setup_jwt_manager(app, jwt_manager): + """Use flask app to configure the JWTManager to work for a particular Realm.""" + def get_roles(a_dict): + return a_dict['realm_access']['roles'] # pragma: no cover + app.config['JWT_ROLE_CALLBACK'] = get_roles + + jwt_manager.init_app(app) + + +def register_shellcontext(app): + """Register shell context objects.""" + def shell_context(): + """Shell context objects.""" + return { + 'app': app, + 'jwt': jwt} # pragma: no cover + + app.shell_context_processor(shell_context) diff --git a/colin-api/src/colin_api/exceptions/__init__.py b/colin-api/src/colin_api/exceptions/__init__.py new file mode 100644 index 000000000..c83973f9b --- /dev/null +++ b/colin-api/src/colin_api/exceptions/__init__.py @@ -0,0 +1,19 @@ +# 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. +"""Application Specific Exceptions, to manage the business errors. + +@log_error - a decorator to automatically log the exception to the logger provided + +""" +import functools diff --git a/colin-api/src/colin_api/resources/__init__.py b/colin-api/src/colin_api/resources/__init__.py new file mode 100644 index 000000000..4f60140ce --- /dev/null +++ b/colin-api/src/colin_api/resources/__init__.py @@ -0,0 +1,64 @@ +# 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. +"""Exposes all of the resource endpoints mounted in Flask-Blueprint style. + +Uses restplus namespaces to mount individual api endpoints into the service. + +All services have 2 defaults sets of endpoints: + - ops + - meta +That are used to expose operational health information about the service, and meta information. +""" +from flask import Blueprint +from flask_restplus import Api + +from .meta import API as META_API +from .ops import API as OPS_API +from .business import API as BUSINESS_API + + +__all__ = ('API_BLUEPRINT', 'OPS_BLUEPRINT') + +# This will add the Authorize button to the swagger docs +# TODO oauth2 & openid may not yet be supported by restplus <- check on this +AUTHORIZATIONS = { + 'apikey': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization' + } +} + +OPS_BLUEPRINT = Blueprint('API_OPS', __name__, url_prefix='/ops') + +API_OPS = Api(OPS_BLUEPRINT, + title='Service OPS API', + version='1.0', + description='The COLIN API for the Legal Entities System', + security=['apikey'], + authorizations=AUTHORIZATIONS) + +API_OPS.add_namespace(OPS_API, path='/') + +API_BLUEPRINT = Blueprint('API', __name__, url_prefix='/api/v1') + +API = Api(API_BLUEPRINT, + title='COLIN API', + version='1.0', + description='The COLIN API for the Legal Entities System', + security=['apikey'], + authorizations=AUTHORIZATIONS) + +API.add_namespace(META_API, path='/meta') +API.add_namespace(BUSINESS_API, path='/businesses') diff --git a/colin-api/src/colin_api/resources/business.py b/colin-api/src/colin_api/resources/business.py new file mode 100644 index 000000000..6a99fa14d --- /dev/null +++ b/colin-api/src/colin_api/resources/business.py @@ -0,0 +1,113 @@ +# 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. +"""Meta information about the service. + +Currently this only provides API versioning information +""" +from datetime import datetime +from flask import current_app, jsonify +from flask_restplus import Namespace, Resource, cors + +from colin_api.resources.db import db +from colin_api.utils.util import cors_preflight + +API = Namespace('businesses', description='Colin API Services - Businesses') + + +@cors_preflight('GET') +@API.route('/') +class Info(Resource): + """Meta information about the overall service.""" + + @staticmethod + @cors.crossdomain(origin='*') + def get(identifier): + """Return the complete business info.""" + try: + # get record + cursor = db.connection.cursor() + cursor.execute( + "select corp.CORP_NUM as identifier, CORP_FROZEN_TYP_CD, corp_typ_cd type, " + "LAST_AR_FILED_DT last_ar_filed_date, LAST_AGM_DATE, " + "corp_op_state.full_desc as state, t_name.corp_nme as legal_name, " + "t_assumed_name.CORP_NME as assumed_name, RECOGNITION_DTS as founding_date," + "TILMA_INVOLVED_IND, TILMA_CESSATION_DT, BN_15 as business_number, " + "CAN_JUR_TYP_CD, OTHR_JURIS_DESC, HOME_JURIS_NUM " + "from CORPORATION corp " + "left join CORP_NAME t_name on t_name.corp_num = corp.corp_num and t_name.CORP_NAME_TYP_CD='CO' " + "AND t_name.END_EVENT_ID is null " + "left join CORP_NAME t_assumed_name on t_assumed_name.corp_num = corp.corp_num " + "and t_assumed_name.CORP_NAME_TYP_CD='AS' AND t_assumed_name.END_EVENT_ID is null " + "join CORP_STATE on CORP_STATE.corp_num = corp.corp_num and CORP_STATE.end_event_id is null " + "join CORP_OP_STATE on CORP_OP_STATE.state_typ_cd = CORP_STATE.state_typ_cd " + "left join JURISDICTION on JURISDICTION.corp_num = corp.corp_num " + "where corp_typ_cd = 'CP'" # only include coops (not xpro coops) for now + "and corp.CORP_NUM='{}'".format(identifier)) + business = cursor.fetchone() + print(business) + + if not business: + return jsonify({'message': f'{identifier} not found'}), 404 + + # add column names to resultset to build out correct json structure and make manipulation below more robust (better than column numbers) + business = dict(zip([x[0].lower() for x in cursor.description], business)) + current_app.logger.debug(business) + + # if this is an XPRO, get correct jurisdiction; otherwise, it's BC + # DISABLED (if False) until XPROs are implemented + if False and business['type'] == 'XCP': + if business['can_jur_typ_cd'] == 'OT': + business['jurisdiction'] = business['othr_juris_desc'] + else: + business['jurisdiction'] = business['can_jur_typ_cd'] + + else: + business['jurisdiction'] = 'BC' + + # set name + if business['assumed_name']: + business['legal_name'] = business['assumed_name'] + + # set status - In Good Standing if certain criteria met, otherwise use original value + if business['state'] == 'Active' and \ + business['last_ar_filed_date'] is not None and type(business['last_ar_filed_date']) is datetime and \ + business['last_agm_date'] is not None and type(business['last_agm_date']) is datetime: + + if business['last_ar_filed_date'] > business['last_agm_date']: + business['status'] = 'In Good Standing' + else: + business['status'] = business['state'] + + + # remove unnecessary fields + del business['home_juris_num'] + del business['can_jur_typ_cd'] + del business['othr_juris_desc'] + del business['assumed_name'] + del business['state'] + + retval = { + 'business': { + 'business_info': business + } + } + + + return jsonify(retval) + + except Exception as err: + # general catch-all exception + current_app.logger.error(err.with_traceback(None)) + return jsonify( + {'message': "Error when trying to retrieve business record from COLIN"}), 500 diff --git a/colin-api/src/colin_api/resources/db.py b/colin-api/src/colin_api/resources/db.py new file mode 100644 index 000000000..a29645f15 --- /dev/null +++ b/colin-api/src/colin_api/resources/db.py @@ -0,0 +1,91 @@ +# 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. +"""Create Oracle database connection. + +These will get initialized by the application. +""" + +from flask import current_app, _app_ctx_stack +import cx_Oracle + + +class OracleDB(object): + + def __init__(self, app=None): + """initializer, supports setting the app context on instantiation""" + if app is not None: + self.init_app(app) + + def init_app(self, app): + """setup for the extension + :param app: Flask app + :return: naked + """ + self.app = app + app.teardown_appcontext(self.teardown) + + def teardown(self, exception): + # the oracle session pool will clean up after itself + ctx = _app_ctx_stack.top + if hasattr(ctx, 'oracle_pool'): + ctx.oracle_pool.close() + + def _create_pool(self): + """create the cx_oracle connection pool from the Flask Config Environment + + :return: an instance of the OCI Session Pool + """ + + # this uses the builtin session / connection pooling provided by + # the Oracle OCI driver + # setting threaded =True wraps the underlying calls in a Mutex + # so we don't have to that here + + def InitSession(conn, requestedTag): + cursor = conn.cursor() + cursor.execute("alter session set TIME_ZONE = 'America/Vancouver'") + + return cx_Oracle.SessionPool(user=current_app.config.get('ORACLE_USER'), + password=current_app.config.get('ORACLE_PASSWORD'), + dsn='{0}:{1}/{2}'.format(current_app.config.get('ORACLE_HOST'), + current_app.config.get('ORACLE_PORT'), + current_app.config.get('ORACLE_DB_NAME')), + min=1, + max=10, + increment=1, + connectiontype=cx_Oracle.Connection, + threaded=True, + getmode=cx_Oracle.SPOOL_ATTRVAL_NOWAIT, + waitTimeout=1500, + timeout=3600, + sessionCallback=InitSession) + + + @property + def connection(self): + """connection property of the NROService + If this is running in a Flask context, + then either get the existing connection pool or create a new one + and then return an acquired session + :return: cx_Oracle.connection type + """ + ctx = _app_ctx_stack.top + if ctx is not None: + if not hasattr(ctx, 'oracle_pool'): + ctx._oracle_pool = self._create_pool() + return ctx._oracle_pool.acquire() + + +# export instance of this class +db = OracleDB() diff --git a/colin-api/src/colin_api/resources/meta.py b/colin-api/src/colin_api/resources/meta.py new file mode 100644 index 000000000..2ffa433a1 --- /dev/null +++ b/colin-api/src/colin_api/resources/meta.py @@ -0,0 +1,35 @@ +# 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. +"""Meta information about the service. + +Currently this only provides API versioning information +""" +from flask import jsonify +from flask_restplus import Namespace, Resource + +from colin_api.utils.run_version import get_run_version + + +API = Namespace('Meta', description='Metadata') + + +@API.route('/info') +class Info(Resource): + """Meta information about the overall service.""" + + @staticmethod + def get(): + """Return a JSON object with meta information about the Service.""" + version = get_run_version() + return jsonify(API=f'colin_api/{version}') diff --git a/colin-api/src/colin_api/resources/ops.py b/colin-api/src/colin_api/resources/ops.py new file mode 100644 index 000000000..a8f79b2bb --- /dev/null +++ b/colin-api/src/colin_api/resources/ops.py @@ -0,0 +1,52 @@ +# 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. +"""Endpoints to check and manage the health of the service.""" +from flask_restplus import Namespace, Resource +import cx_Oracle +from colin_api.resources.db import db + +API = Namespace('OPS', description='Service - OPS checks') + + +@API.route('healthz') +class Healthz(Resource): + """Determines if the service and required dependnecies are still working. + + This could be thought of as a heartbeat for the service + """ + + @staticmethod + def get(): + """Return a JSON object stating the health of the Service and dependencies.""" + try: + # check db connection working TODO + cursor = db.connection.cursor() + cursor.execute("select 1 from dual") + + except cx_Oracle.DatabaseError: + return {'message': 'api is down'}, 500 + + # made it here, so all checks passed + return {'message': 'api is healthy'}, 200 + + +@API.route('readyz') +class Readyz(Resource): + """Determines if the service is ready to respond.""" + + @staticmethod + def get(): + """Return a JSON object that identifies if the service is setupAnd ready to work.""" + # TODO: add a poll to the DB when called + return {'message': 'api is ready'}, 200 diff --git a/colin-api/src/colin_api/utils/__init__.py b/colin-api/src/colin_api/utils/__init__.py new file mode 100644 index 000000000..9b5291dad --- /dev/null +++ b/colin-api/src/colin_api/utils/__init__.py @@ -0,0 +1,14 @@ +# 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. +"""This module holds general utility functions and helpers for the main package.""" diff --git a/colin-api/src/colin_api/utils/logging.py b/colin-api/src/colin_api/utils/logging.py new file mode 100644 index 000000000..9c737649a --- /dev/null +++ b/colin-api/src/colin_api/utils/logging.py @@ -0,0 +1,31 @@ +# 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. +"""Centralized setup of logging for the service.""" +import logging.config +import sys +from os import path + + +def setup_logging(conf): + """Create the services logger. + + TODO should be reworked to load in the proper loggers and remove others + """ + # log_file_path = path.join(path.abspath(path.dirname(__file__)), conf) + + if conf and path.isfile(conf): + logging.config.fileConfig(conf) + print('Configure logging, from conf:{}'.format(conf), file=sys.stdout) + else: + print('Unable to configure logging, attempted conf:{}'.format(conf), file=sys.stderr) diff --git a/colin-api/src/colin_api/utils/run_version.py b/colin-api/src/colin_api/utils/run_version.py new file mode 100644 index 000000000..80037a921 --- /dev/null +++ b/colin-api/src/colin_api/utils/run_version.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. +"""Supply version and commit hash info.""" +import os + +from colin_api.version import __version__ + + +def _get_build_openshift_commit_hash(): + return os.getenv('OPENSHIFT_BUILD_COMMIT', None) + + +def get_run_version(): + """Return a formatted version string for this service.""" + commit_hash = _get_build_openshift_commit_hash() + if commit_hash: + return f'{__version__}-{commit_hash}' + return __version__ diff --git a/colin-api/src/colin_api/utils/util.py b/colin-api/src/colin_api/utils/util.py new file mode 100644 index 000000000..bcfcf9b3a --- /dev/null +++ b/colin-api/src/colin_api/utils/util.py @@ -0,0 +1,33 @@ +# 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. + +"""CORS pre-flight decorator. + +A simple decorator to add the options method to a Request Class. +""" +# from functools import wraps + + +def cors_preflight(methods: str = 'GET'): + """Render an option method on the class.""" + def wrapper(f): + def options(self, *args, **kwargs): # pylint: disable=unused-argument + return {'Allow': 'GET'}, 200, \ + {'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': methods, + 'Access-Control-Allow-Headers': 'Authorization, Content-Type'} + + setattr(f, 'options', options) + return f + return wrapper diff --git a/colin-api/src/colin_api/version.py b/colin-api/src/colin_api/version.py new file mode 100644 index 000000000..9576ba6f8 --- /dev/null +++ b/colin-api/src/colin_api/version.py @@ -0,0 +1,25 @@ +# 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. + +"""Version of this service in PEP440. + +[N!]N(.N)*[{a|b|rc}N][.postN][.devN] +Epoch segment: N! +Release segment: N(.N)* +Pre-release segment: {a|b|rc}N +Post-release segment: .postN +Development release segment: .devN +""" + +__version__ = '0.1.0a0.dev' # pylint: disable=invalid-name diff --git a/colin-api/tests/__init__.py b/colin-api/tests/__init__.py new file mode 100644 index 000000000..7fad33180 --- /dev/null +++ b/colin-api/tests/__init__.py @@ -0,0 +1,16 @@ +# 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. +"""The Test Suites to ensure that the service is built and operating correctly.""" + +from .utilities.decorators import oracle_integration diff --git a/colin-api/tests/conftest.py b/colin-api/tests/conftest.py new file mode 100644 index 000000000..30c88f6b0 --- /dev/null +++ b/colin-api/tests/conftest.py @@ -0,0 +1,136 @@ +# 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. +"""Common setup and fixtures for the pytest suite used by this service.""" +import pytest +from flask_migrate import Migrate, upgrade +from sqlalchemy import event, text +from sqlalchemy.schema import DropConstraint, MetaData + +from colin_api import create_app +from colin_api import jwt as _jwt + + +@pytest.fixture(scope='session') +def app(): + """Return a session-wide application configured in TEST mode.""" + _app = create_app('testing') + + return _app + + +@pytest.fixture(scope='function') +def app_request(): + """Return a session-wide application configured in TEST mode.""" + _app = create_app('testing') + + return _app + + +@pytest.fixture(scope='session') +def client(app): # pylint: disable=redefined-outer-name + """Return a session-wide Flask test client.""" + return app.test_client() + + +@pytest.fixture(scope='session') +def jwt(): + """Return a session-wide jwt manager.""" + return _jwt + + +@pytest.fixture(scope='session') +def client_ctx(app): # pylint: disable=redefined-outer-name + """Return session-wide Flask test client.""" + with app.test_client() as _client: + yield _client + + +@pytest.fixture(scope='session') +def db(app): # pylint: disable=redefined-outer-name, invalid-name + """Return a session-wide initialised database. + + Drops all existing tables - Meta follows Postgres FKs + """ + with app.app_context(): + # Clear out any existing tables + metadata = MetaData(_db.engine) + metadata.reflect() + for table in metadata.tables.values(): + for fk in table.foreign_keys: # pylint: disable=invalid-name + _db.engine.execute(DropConstraint(fk.constraint)) + metadata.drop_all() + _db.drop_all() + + sequence_sql = """SELECT sequence_name FROM information_schema.sequences + WHERE sequence_schema='public' + """ + + sess = _db.session() + for seq in [name for (name,) in sess.execute(text(sequence_sql))]: + try: + sess.execute(text('DROP SEQUENCE public.%s ;' % seq)) + print('DROP SEQUENCE public.%s ' % seq) + except Exception as err: # pylint: disable=broad-except + print(f'Error: {err}') + sess.commit() + + # ############################################ + # There are 2 approaches, an empty database, or the same one that the app will use + # create the tables + # _db.create_all() + # or + # Use Alembic to load all of the DB revisions including supporting lookup data + # This is the path we'll use in colin_api!! + + # even though this isn't referenced directly, it sets up the internal configs that upgrade needs + Migrate(app, _db) + upgrade() + + return _db + + +@pytest.fixture(scope='function') +def session(app, db): # pylint: disable=redefined-outer-name, invalid-name + """Return a function-scoped session.""" + with app.app_context(): + conn = db.engine.connect() + txn = conn.begin() + + options = dict(bind=conn, binds={}) + sess = db.create_scoped_session(options=options) + + # establish a SAVEPOINT just before beginning the test + # (http://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#using-savepoint) + sess.begin_nested() + + @event.listens_for(sess(), 'after_transaction_end') + def restart_savepoint(sess2, trans): # pylint: disable=unused-variable + # Detecting whether this is indeed the nested transaction of the test + if trans.nested and not trans._parent.nested: # pylint: disable=protected-access + # Handle where test DOESN'T session.commit(), + sess2.expire_all() + sess.begin_nested() + + db.session = sess + + sql = text('select 1') + sess.execute(sql) + + yield sess + + # Cleanup + sess.remove() + # This instruction rollsback any commit that were executed in the tests. + txn.rollback() + conn.close() diff --git a/colin-api/tests/unit/__init__.py b/colin-api/tests/unit/__init__.py new file mode 100644 index 000000000..d755f6a1d --- /dev/null +++ b/colin-api/tests/unit/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""The Unit Test for the API. + +For our purposes this server and its Postgres Database are part of the Unit Test Suite. +""" diff --git a/colin-api/tests/unit/api/__init__.py b/colin-api/tests/unit/api/__init__.py new file mode 100644 index 000000000..02d5354ad --- /dev/null +++ b/colin-api/tests/unit/api/__init__.py @@ -0,0 +1,14 @@ +# 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-Suite for the API.""" diff --git a/colin-api/tests/unit/api/test_business.py b/colin-api/tests/unit/api/test_business.py new file mode 100644 index 000000000..bf75ca4d0 --- /dev/null +++ b/colin-api/tests/unit/api/test_business.py @@ -0,0 +1,47 @@ +# 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 ops end-point. + +Test-Suite to ensure that the /ops endpoint is working as expected. +""" + +from tests.utilities.schema_assertions import assert_valid_schema +from tests import oracle_integration + +@oracle_integration +def test_get_business(client): + """Assert that the business info for regular (not xpro) business is correct to spec.""" + rv = client.get('/api/v1/businesses/CP0000440') + + assert rv.status_code == 200 + assert_valid_schema(rv.json, 'business.json') + +''' +@oracle_integration +def test_get_xpro_business(client): + """Assert that the business info for XPRO business is correct to spec.""" + rv = client.get('/api/v1/businesses/XCP0001534') + + assert rv.status_code == 200 + assert_valid_schema(rv.json, 'business.json') +''' + +@oracle_integration +def test_get_business_no_results(client): + """Assert that the business info for regular (not xpro) business is correct to spec.""" + rv = client.get('/api/v1/businesses/CP0000000') + + assert rv.status_code == 404 + diff --git a/colin-api/tests/unit/api/test_meta.py b/colin-api/tests/unit/api/test_meta.py new file mode 100644 index 000000000..691e82ce3 --- /dev/null +++ b/colin-api/tests/unit/api/test_meta.py @@ -0,0 +1,40 @@ +# 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 meta end-point. + +Test-Suite to ensure that the /meta endpoint is working as expected. +""" + + +def test_meta_no_commit_hash(client): + """Assert that the endpoint returns just the services __version__.""" + from colin_api.version import __version__ + + rv = client.get('/api/v1/meta/info') + + assert rv.status_code == 200 + assert rv.json == {'API': f'colin_api/{__version__}'} + + +def test_meta_with_commit_hash(monkeypatch, client): + """Assert that the endpoint return __version__ and the last git hash used to build the services image.""" + from colin_api.version import __version__ + + commit_hash = 'deadbeef_ha' + monkeypatch.setenv('OPENSHIFT_BUILD_COMMIT', commit_hash) + + rv = client.get('/api/v1/meta/info') + assert rv.status_code == 200 + assert rv.json == {'API': f'colin_api/{__version__}-{commit_hash}'} diff --git a/colin-api/tests/unit/api/test_ops.py b/colin-api/tests/unit/api/test_ops.py new file mode 100644 index 000000000..85e8c612d --- /dev/null +++ b/colin-api/tests/unit/api/test_ops.py @@ -0,0 +1,46 @@ +# 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 ops end-point. + +Test-Suite to ensure that the /ops endpoint is working as expected. +""" + +from tests import oracle_integration + +@oracle_integration +def test_ops_healthz_success(client): + """Assert that the service is healthy if it can successfully access the database.""" + rv = client.get('/ops/healthz') + + assert rv.status_code == 200 + assert rv.json == {'message': 'api is healthy'} + + +def test_ops_healthz_fail(app_request): + """Assert that the service is unhealthy if a connection toThe database cannot be made.""" + app_request.config['ORACLE_DB_NAME'] = 'somethingnotreal' + with app_request.test_client() as client: + rv = client.get('/ops/healthz') + + assert rv.status_code == 500 + assert rv.json == {'message': 'api is down'} + + +def test_ops_readyz(client): + """Asserts that the service is ready to serve.""" + rv = client.get('/ops/readyz') + + assert rv.status_code == 200 + assert rv.json == {'message': 'api is ready'} diff --git a/colin-api/tests/unit/conf/__init__.py b/colin-api/tests/unit/conf/__init__.py new file mode 100644 index 000000000..47a9b4f52 --- /dev/null +++ b/colin-api/tests/unit/conf/__init__.py @@ -0,0 +1,14 @@ +# 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-Suite for the configuration system.""" diff --git a/colin-api/tests/unit/conf/test_configuration.py b/colin-api/tests/unit/conf/test_configuration.py new file mode 100644 index 000000000..66d103add --- /dev/null +++ b/colin-api/tests/unit/conf/test_configuration.py @@ -0,0 +1,66 @@ +# 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 configuration objects. + +Test-Suite to ensure that the Configuration Classes are working as expected. +""" +from importlib import reload + +import pytest + +import config + + +# testdata pattern is ({str: environment}, {expected return value}) +TEST_ENVIRONMENT_DATA = [ + ('valid', 'development', config.DevConfig), + ('valid', 'testing', config.TestConfig), + ('valid', 'default', config.ProdConfig), + ('valid', 'staging', config.ProdConfig), + ('valid', 'production', config.ProdConfig), + ('error', None, KeyError) +] + + +@pytest.mark.parametrize('test_type,environment,expected', TEST_ENVIRONMENT_DATA) +def test_get_named_config(test_type, environment, expected): + """Assert that the named configurations can be loaded. + + Or that a KeyError is returned for missing config types. + """ + if test_type == 'valid': + assert isinstance(config.get_named_config(environment), expected) + else: + with pytest.raises(KeyError): + config.get_named_config(environment) + + +def test_prod_config_secret_key(monkeypatch): # pylint: disable=missing-docstring + """Assert that the ProductionConfig is correct. + + The object either uses the SECRET_KEY from the environment, + or creates the SECRET_KEY on the fly. + """ + key = 'SECRET_KEY' + + # Assert that secret key will default to some value + # even if missed in the environment setup + monkeypatch.delenv(key, raising=False) + reload(config) + assert config.ProdConfig().SECRET_KEY is not None + + # Assert that the secret_key is set to the assigned environment value + monkeypatch.setenv(key, 'SECRET_KEY') + reload(config) + assert config.ProdConfig().SECRET_KEY == 'SECRET_KEY' diff --git a/colin-api/tests/unit/conf/test_version.py b/colin-api/tests/unit/conf/test_version.py new file mode 100644 index 000000000..2ef87cbe1 --- /dev/null +++ b/colin-api/tests/unit/conf/test_version.py @@ -0,0 +1,26 @@ +# 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 version utilities. + +Test-Suite to ensure that the version utilities are working as expected. +""" +from colin_api import utils +from colin_api.version import __version__ + + +def test_get_version(): + """Assert thatThe version is returned correctly.""" + rv = utils.run_version.get_run_version() + assert rv == __version__ diff --git a/colin-api/tests/unit/conftest.py b/colin-api/tests/unit/conftest.py new file mode 100644 index 000000000..30c88f6b0 --- /dev/null +++ b/colin-api/tests/unit/conftest.py @@ -0,0 +1,136 @@ +# 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. +"""Common setup and fixtures for the pytest suite used by this service.""" +import pytest +from flask_migrate import Migrate, upgrade +from sqlalchemy import event, text +from sqlalchemy.schema import DropConstraint, MetaData + +from colin_api import create_app +from colin_api import jwt as _jwt + + +@pytest.fixture(scope='session') +def app(): + """Return a session-wide application configured in TEST mode.""" + _app = create_app('testing') + + return _app + + +@pytest.fixture(scope='function') +def app_request(): + """Return a session-wide application configured in TEST mode.""" + _app = create_app('testing') + + return _app + + +@pytest.fixture(scope='session') +def client(app): # pylint: disable=redefined-outer-name + """Return a session-wide Flask test client.""" + return app.test_client() + + +@pytest.fixture(scope='session') +def jwt(): + """Return a session-wide jwt manager.""" + return _jwt + + +@pytest.fixture(scope='session') +def client_ctx(app): # pylint: disable=redefined-outer-name + """Return session-wide Flask test client.""" + with app.test_client() as _client: + yield _client + + +@pytest.fixture(scope='session') +def db(app): # pylint: disable=redefined-outer-name, invalid-name + """Return a session-wide initialised database. + + Drops all existing tables - Meta follows Postgres FKs + """ + with app.app_context(): + # Clear out any existing tables + metadata = MetaData(_db.engine) + metadata.reflect() + for table in metadata.tables.values(): + for fk in table.foreign_keys: # pylint: disable=invalid-name + _db.engine.execute(DropConstraint(fk.constraint)) + metadata.drop_all() + _db.drop_all() + + sequence_sql = """SELECT sequence_name FROM information_schema.sequences + WHERE sequence_schema='public' + """ + + sess = _db.session() + for seq in [name for (name,) in sess.execute(text(sequence_sql))]: + try: + sess.execute(text('DROP SEQUENCE public.%s ;' % seq)) + print('DROP SEQUENCE public.%s ' % seq) + except Exception as err: # pylint: disable=broad-except + print(f'Error: {err}') + sess.commit() + + # ############################################ + # There are 2 approaches, an empty database, or the same one that the app will use + # create the tables + # _db.create_all() + # or + # Use Alembic to load all of the DB revisions including supporting lookup data + # This is the path we'll use in colin_api!! + + # even though this isn't referenced directly, it sets up the internal configs that upgrade needs + Migrate(app, _db) + upgrade() + + return _db + + +@pytest.fixture(scope='function') +def session(app, db): # pylint: disable=redefined-outer-name, invalid-name + """Return a function-scoped session.""" + with app.app_context(): + conn = db.engine.connect() + txn = conn.begin() + + options = dict(bind=conn, binds={}) + sess = db.create_scoped_session(options=options) + + # establish a SAVEPOINT just before beginning the test + # (http://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#using-savepoint) + sess.begin_nested() + + @event.listens_for(sess(), 'after_transaction_end') + def restart_savepoint(sess2, trans): # pylint: disable=unused-variable + # Detecting whether this is indeed the nested transaction of the test + if trans.nested and not trans._parent.nested: # pylint: disable=protected-access + # Handle where test DOESN'T session.commit(), + sess2.expire_all() + sess.begin_nested() + + db.session = sess + + sql = text('select 1') + sess.execute(sql) + + yield sess + + # Cleanup + sess.remove() + # This instruction rollsback any commit that were executed in the tests. + txn.rollback() + conn.close() diff --git a/colin-api/tests/unit/utils/__init__.py b/colin-api/tests/unit/utils/__init__.py new file mode 100644 index 000000000..a41ec6419 --- /dev/null +++ b/colin-api/tests/unit/utils/__init__.py @@ -0,0 +1,14 @@ +# 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 Suite for the Utils package.""" diff --git a/colin-api/tests/unit/utils/logging.conf b/colin-api/tests/unit/utils/logging.conf new file mode 100644 index 000000000..ffc1a01e3 --- /dev/null +++ b/colin-api/tests/unit/utils/logging.conf @@ -0,0 +1,28 @@ +[loggers] +keys=root,api + +[handlers] +keys=console + +[formatters] +keys=simple + +[logger_root] +level=DEBUG +handlers=console + +[logger_api] +level=DEBUG +handlers=console +qualname=api +propagate=0 + +[handler_console] +class=StreamHandler +level=DEBUG +formatter=simple +args=(sys.stdout,) + +[formatter_simple] +format=%(asctime)s - %(name)s - %(levelname)s in %(module)s:%(filename)s:%(lineno)d - %(funcName)s: %(message)s +datefmt= \ No newline at end of file diff --git a/colin-api/tests/unit/utils/test_logging.py b/colin-api/tests/unit/utils/test_logging.py new file mode 100644 index 000000000..a8c0aba79 --- /dev/null +++ b/colin-api/tests/unit/utils/test_logging.py @@ -0,0 +1,42 @@ +# 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 logging utilities. + +Test-Suite to ensure that the logging setup is working as expected. +""" + +import os + +from colin_api.utils.logging import setup_logging + + +def test_logging_with_file(capsys): + """Assert that logging is setup with the configuration file.""" + file_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf') + setup_logging(file_path) # important to do this first + + captured = capsys.readouterr() + + assert captured.out.startswith('Configure logging, from conf') + + +def test_logging_with_missing_file(capsys): + """Assert that a message is sent to STDERR when the configuration doesn't exist.""" + file_path = None + setup_logging(file_path) # important to do this first + + captured = capsys.readouterr() + + assert captured.err.startswith('Unable to configure logging') diff --git a/colin-api/tests/unit/utils/test_util_cors.py b/colin-api/tests/unit/utils/test_util_cors.py new file mode 100644 index 000000000..cabf0da43 --- /dev/null +++ b/colin-api/tests/unit/utils/test_util_cors.py @@ -0,0 +1,45 @@ +# 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. +""" +import pytest + +from colin_api.utils.util import cors_preflight + + +TEST_CORS_METHODS_DATA = [ + ('GET'), + ('PUT'), + ('POST'), + ('GET,PUT'), + ('GET,POST'), + ('PUT,POST'), + ('GET,PUT,POST'), +] + + +@pytest.mark.parametrize('methods', TEST_CORS_METHODS_DATA) +def test_cors_preflight_post(methods): + """Assert that the options methos is added to the class and that the correct access controls are set.""" + @cors_preflight(methods) # pylint: disable=too-few-public-methods + class TestCors(): + pass + + rv = TestCors().options() # pylint: disable=no-member + + assert rv[2]['Access-Control-Allow-Origin'] == '*' + assert rv[2]['Access-Control-Allow-Methods'] == methods diff --git a/colin-api/tests/utilities/decorators.py b/colin-api/tests/utilities/decorators.py new file mode 100644 index 000000000..36a7d5812 --- /dev/null +++ b/colin-api/tests/utilities/decorators.py @@ -0,0 +1,25 @@ +# 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. + +import os + +import pytest +from dotenv import load_dotenv, find_dotenv + +#this will load all the envars from a .env file located in the project root (api) +load_dotenv(find_dotenv()) + + +oracle_integration = pytest.mark.skipif((os.getenv('ORACLE_INTEGRATION_TESTING', False) is False), + reason="requires access to a test version of Oracle CTST") diff --git a/colin-api/tests/utilities/schema_assertions.py b/colin-api/tests/utilities/schema_assertions.py new file mode 100644 index 000000000..24d3d1f8e --- /dev/null +++ b/colin-api/tests/utilities/schema_assertions.py @@ -0,0 +1,37 @@ +# 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. + +"""Utilities to load and validate against JSONSchemas. + +Test helper functions to load and assert that a JSON payload validates against a defined schema. +""" +import json +from os.path import dirname, join + +from jsonschema import validate + + +def assert_valid_schema(data: dict, schema_file: dict): + """Do assertion that data validates against the JSONSchema in schema_file.""" + schema = _load_json_schema(schema_file) + return validate(data, schema) + + +def _load_json_schema(filename: str): + """Return the given schema file identified by filename.""" + relative_path = join('schemas', filename) + absolute_path = join(dirname(__file__), relative_path) + + with open(absolute_path) as schema_file: + return json.loads(schema_file.read()) diff --git a/colin-api/tests/utilities/schemas/business.json b/colin-api/tests/utilities/schemas/business.json new file mode 100644 index 000000000..d138449cf --- /dev/null +++ b/colin-api/tests/utilities/schemas/business.json @@ -0,0 +1,83 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/root.json", + "type": "object", + "title": "The Businesses Schema", + "required": [ + "business" + ], + "properties": { + "business": { + "$id": "#business", + "type": "object", + "title": "The Business Schema", + "properties": { + "business_info": { + "type": "object", + "required": [ + "founding_date", + "identifier", + "legal_name" + ], + "properties": { + "dissolution_date": { + "type": "string", + "format": "date-time", + "title": "The Dissolution_date Schema", + "default": "", + "examples": [ + "1970-01-01T00:00:00+00:00" + ] + }, + "fiscal_year_end_date": { + "type": "string", + "format": "date-time", + "title": "The Fiscal_year_end_date Schema", + "default": "", + "examples": [ + "1970-01-01T00:00:00+00:00" + ] + }, + "founding_date": { + "type": "string", + "format": "date", + "title": "The Founding_date Schema", + "default": "", + "examples": [ + "1970-01-01" + ] + }, + "identifier": { + "type": "string", + "title": "The Identifier Schema", + "default": "", + "examples": [ + "CP1234567" + ], + "pattern": "^(CP)[0-9]{7}$" + }, + "legal_name": { + "type": "string", + "title": "The Legal_name Schema", + "default": "", + "examples": [ + "legal_name" + ], + "pattern": "^(.*)$" + }, + "tax_id": { + "type": "string", + "title": "The Tax_id Schema", + "default": "", + "examples": [ + "123456789" + ], + "pattern": "^[0-9]{9}$" + } + } + } + } + } + } +} diff --git a/colin-api/tests/utilities/schemas/user.json b/colin-api/tests/utilities/schemas/user.json new file mode 100644 index 000000000..792873841 --- /dev/null +++ b/colin-api/tests/utilities/schemas/user.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "User response schema", + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "type": { + "enum": [ + "users" + ] + }, + "id": { + "type": "number" + }, + "attributes": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "username": { + "type": "string" + } + }, + "required": [ + "email", + "username" + ] + } + }, + "required": [ + "type", + "id", + "attributes" + ] + } + }, + "additionalProperties": false, + "required": [ + "data" + ] +} \ No newline at end of file diff --git a/colin-api/wsgi.py b/colin-api/wsgi.py new file mode 100644 index 000000000..8f62b5718 --- /dev/null +++ b/colin-api/wsgi.py @@ -0,0 +1,22 @@ +# 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. +"""Provides the WSGI entry point for running the application +""" +from colin_api import create_app + +# Openshift s2i expects a lower case name of application +application = create_app() # pylint: disable=invalid-name + +if __name__ == "__main__": + application.run()