├── .coveragerc ├── .dockerignore ├── .gdbinit ├── .gitignore ├── .pre-commit-config.yaml ├── .secrets.baseline ├── CODE_OF_CONDUCT.md ├── Dockerfile.base ├── LICENSE ├── README.md ├── Vagrantfile ├── automation_requirements.txt ├── bandit.yml ├── bin ├── aws-credentials.sh ├── build.sh ├── deploy.sh ├── install-packages.sh └── vagrant.sh ├── codecov.yml ├── docker-compose.yml ├── docs ├── backups.md ├── debugging.md ├── docker.md ├── doit.md ├── environment.md ├── prerequisites.md ├── rest.md ├── sre_info.md └── vagrant.md ├── dodo.py ├── etc ├── alpine-packages └── license-header ├── mypy.ini ├── package.json ├── pylintrc ├── reports ├── .gitignore └── README.md ├── services └── fxa │ ├── README.md │ ├── accounts.yml │ ├── env.yml │ ├── functions.yml │ ├── hubhandler.py │ ├── miahandler.py │ ├── resources │ ├── sns-topic.yml │ └── tags.yml │ ├── serverless.yml │ └── whitelist.yml ├── setup.cfg ├── setup.py ├── src ├── app_requirements.txt ├── hub │ ├── Dockerfile │ ├── __init__.py │ ├── app.py │ ├── routes │ │ ├── __init__.py │ │ ├── abstract.py │ │ ├── firefox.py │ │ ├── pipeline.py │ │ ├── salesforce.py │ │ └── static.py │ ├── shared │ ├── swagger.yaml │ ├── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── unit │ │ │ ├── __init__.py │ │ │ ├── fixtures │ │ │ ├── invalid_plan_response.json │ │ │ ├── stripe_ch_test1.json │ │ │ ├── stripe_ch_test2.json │ │ │ ├── stripe_cust_created_event.json │ │ │ ├── stripe_cust_created_event_missing_name.json │ │ │ ├── stripe_cust_no_metadata.json │ │ │ ├── stripe_cust_test1.json │ │ │ ├── stripe_cust_test1_deleted.json │ │ │ ├── stripe_cust_test2.json │ │ │ ├── stripe_cust_updated_event.json │ │ │ ├── stripe_cust_updated_event_missing_name.json │ │ │ ├── stripe_customer_deleted_event.json │ │ │ ├── stripe_customer_deleted_event_no_metadata.json │ │ │ ├── stripe_in_payment_failed_event.json │ │ │ ├── stripe_in_payment_failed_event_sub_create.json │ │ │ ├── stripe_in_test1.json │ │ │ ├── stripe_in_test2.json │ │ │ ├── stripe_invoice_payment_succeeded_new_event.json │ │ │ ├── stripe_plan_test1.json │ │ │ ├── stripe_plan_test2.json │ │ │ ├── stripe_plan_test3.json │ │ │ ├── stripe_previous_plan1.json │ │ │ ├── stripe_prod_bad_test1.json │ │ │ ├── stripe_prod_test1.json │ │ │ ├── stripe_prod_test2.json │ │ │ ├── stripe_source_expiring_event.json │ │ │ ├── stripe_sub_created_event.json │ │ │ ├── stripe_sub_deleted_event.json │ │ │ ├── stripe_sub_test1.json │ │ │ ├── stripe_sub_test2.json │ │ │ ├── stripe_sub_test3.json │ │ │ ├── stripe_sub_test4.json │ │ │ ├── stripe_sub_test5.json │ │ │ ├── stripe_sub_test6.json │ │ │ ├── stripe_sub_test7.json │ │ │ ├── stripe_sub_test8.json │ │ │ ├── stripe_sub_test_expanded.json │ │ │ ├── stripe_sub_updated_event_cancel.json │ │ │ ├── stripe_sub_updated_event_change.json │ │ │ ├── stripe_sub_updated_event_charge.json │ │ │ ├── stripe_sub_updated_event_no_trigger.json │ │ │ ├── stripe_sub_updated_event_reactivate.json │ │ │ └── valid_plan_response.json │ │ │ ├── routes │ │ │ ├── __init__.py │ │ │ └── test_pipeline.py │ │ │ ├── stripe │ │ │ ├── __init__.py │ │ │ ├── charge │ │ │ │ ├── __init__.py │ │ │ │ ├── badpayload.json │ │ │ │ ├── charge-captured.json │ │ │ │ ├── charge-dispute-closed.json │ │ │ │ ├── charge-dispute-created.json │ │ │ │ ├── charge-dispute-funds_reinstated.json │ │ │ │ ├── charge-dispute-funds_withdrawn.json │ │ │ │ ├── charge-dispute-updated.json │ │ │ │ ├── charge-expired.json │ │ │ │ ├── charge-failed.json │ │ │ │ ├── charge-pending.json │ │ │ │ ├── charge-refund-updated.json │ │ │ │ ├── charge-refunded.json │ │ │ │ ├── charge-succeeded.json │ │ │ │ ├── charge-updated.json │ │ │ │ └── test_stripe_charge.py │ │ │ ├── customer │ │ │ │ ├── __init__.py │ │ │ │ └── test_stripe_customer.py │ │ │ ├── event │ │ │ │ ├── __init__.py │ │ │ │ ├── event.json │ │ │ │ ├── more_event.json │ │ │ │ └── test_stripe_events.py │ │ │ ├── invoice │ │ │ │ ├── __init__.py │ │ │ │ ├── invoice-finalized.json │ │ │ │ ├── invoice-payment-failed.json │ │ │ │ └── test_stripe_invoice.py │ │ │ ├── payment │ │ │ │ ├── __init__.py │ │ │ │ ├── payment-intent-succeeded.json │ │ │ │ └── test_stripe_payments.py │ │ │ └── test_app.py │ │ │ ├── test_app.py │ │ │ └── test_stripe_controller.py │ ├── vendor │ │ ├── __init__.py │ │ ├── abstract.py │ │ ├── controller.py │ │ ├── customer.py │ │ ├── events.py │ │ └── invoices.py │ └── verifications │ │ ├── __init__.py │ │ └── events_check.py ├── shared │ ├── __init__.py │ ├── authentication.py │ ├── cfg.py │ ├── db.py │ ├── deployed.py │ ├── dynamodb.py │ ├── exceptions.py │ ├── headers.py │ ├── log.py │ ├── secrets.py │ ├── tests │ │ └── unit │ │ │ ├── fixtures │ │ │ ├── stripe_ch_test1.json │ │ │ ├── stripe_ch_test2.json │ │ │ ├── stripe_cust_test1.json │ │ │ ├── stripe_deleted_cust.json │ │ │ ├── stripe_in_test1.json │ │ │ ├── stripe_plan_test1.json │ │ │ ├── stripe_prod_test1.json │ │ │ └── stripe_sub_test1.json │ │ │ ├── test_deployed.py │ │ │ ├── test_exceptions.py │ │ │ ├── test_headers.py │ │ │ ├── test_secrets.py │ │ │ ├── test_vendor.py │ │ │ ├── test_vendor_utils.py │ │ │ ├── test_version.py │ │ │ └── utils.py │ ├── types.py │ ├── utils.py │ ├── vendor.py │ ├── vendor_utils.py │ └── version.py └── test_requirements.txt ├── terraform ├── .gitignore ├── .terraform-version ├── README.md ├── backend.hcl ├── dynamodb.tf ├── global │ ├── dynamodb │ │ ├── README.md │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── terraform.tfvars │ │ ├── variables.tf │ │ └── versions.tf │ └── s3 │ │ ├── README.md │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── terraform.tfvars │ │ ├── variables.tf │ │ └── versions.tf ├── main.tf ├── outputs.tf ├── terraform.tfvars ├── variables.tf └── versions.tf ├── tox.ini └── yarn.lock /.coveragerc: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | [run] 6 | branch = True 7 | parallel = True 8 | # Store coverage at the location, reports/.coverage 9 | data_file = reports/.coverage 10 | # Collect coverage from the following sources 11 | source = src/shared, src/hub 12 | 13 | [report] 14 | # Ignore source errors that can't be found 15 | ignore_errors = True 16 | exclude_lines = 17 | pragma: no cover 18 | def __repr__ 19 | if .debug: 20 | raise NotImplementedError 21 | if __name__ == .__main__.: 22 | logger. 23 | stripe.log 24 | stripe.verify_ssl_certs 25 | stripe.api_base 26 | from 27 | import 28 | omit = 29 | */tests/* 30 | */.venv/* 31 | 32 | [html] 33 | # HTML report title 34 | title = Subhub Coverage 35 | # Write the HTML reports to the reports/html directory 36 | directory = reports/html 37 | 38 | [xml] 39 | # Write the XML report to reports/coverage.xml 40 | output = reports/coverage.xml 41 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Dynalite 2 | *.out 3 | 4 | # OSX 5 | .DS_Store 6 | 7 | # doit 8 | .doit.db 9 | 10 | # Virtual Environment 11 | venv 12 | 13 | # Eggs 14 | .eggs 15 | subhub.egg-info 16 | 17 | # Node 18 | node_modules 19 | 20 | # Visual Studio Code 21 | .vscode 22 | 23 | # PyCharm 24 | .idea 25 | 26 | .vscode/ 27 | .tox 28 | venv 29 | .doit.db 30 | 31 | # Dockerfiles 32 | src/sub/Dockerfile 33 | src/hub/Dockerfile 34 | Dockerfile.base 35 | -------------------------------------------------------------------------------- /.gdbinit: -------------------------------------------------------------------------------- 1 | # Persistent history: 2 | set history save 3 | set history filename ~/.gdb_history 4 | 5 | set auto-load python-scripts on 6 | show auto-load python-scripts on 7 | info auto-load python-scripts 8 | 9 | # Pretty Print Output 10 | set print pretty 11 | 12 | # Print the full stack trace when it crashes: 13 | set python print-stack full 14 | 15 | # Colored prompt: 16 | set prompt \001\033[1;32m\002(gdb)\001\033[0m\002\040 17 | 18 | # When displaying a pointer to an object, identify the actual (derived) type of the object rather than the declared type, using the virtual function table. 19 | set print object on 20 | 21 | # Print using only seven-bit characters 22 | set print sevenbit-strings off 23 | 24 | # Convert GDB to interpret in Python 25 | python 26 | import sys 27 | import os 28 | import subprocess 29 | # Execute a Python using the user's shell and pull out the sys.path 30 | # from that version 31 | sys.path.insert(0, join(dirname(realpath(__file__)), 'src')) 32 | paths = eval(subprocess.check_output('python -c "import sys;print(sys.path)"', 33 | shell=True).strip()) 34 | print(paths) 35 | # Extend the current GDB instance's Python paths 36 | sys.path.extend(paths) 37 | end 38 | 39 | # References 40 | # 1. https://chezsoi.org/lucas/blog/gdb-python-macros.html 41 | # 2. https://interrupt.memfault.com/blog/using-pypi-packages-with-GDB 42 | # 3. https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_57.html 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pipfile 2 | Pipfile 3 | Pipfile.lock 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | # serverless 111 | .serverless 112 | node_modules/ 113 | 114 | # doit 115 | .doit.db 116 | 117 | # idea 118 | .idea/ 119 | 120 | 121 | # dynamodb local 122 | .dynamodb/ 123 | 124 | # vscode blocker 125 | .vscode/ 126 | 127 | # out files 128 | *.out 129 | 130 | # Profiling 131 | *.prof 132 | 133 | # GraphViz 134 | *.png 135 | *.dot 136 | 137 | # src tarballs 138 | .src.tar.gz 139 | 140 | # OSX 141 | .DS_Store 142 | 143 | # Vagrant 144 | .vagrant 145 | 146 | # ignore specific symlink 147 | services/fxa/sls 148 | 149 | # GDB History 150 | .gdb_history 151 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | # 5 | # Available pre-commit hooks 6 | # https://pre-commit.com/hooks.html 7 | 8 | default_language_version: 9 | python: python3.7 10 | fail_fast: true 11 | repos: 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v2.1.0 14 | hooks: 15 | - id: check-added-large-files 16 | - id: check-json 17 | - id: detect-private-key 18 | - id: end-of-file-fixer 19 | - id: requirements-txt-fixer 20 | - id: pretty-format-json 21 | args: [ 22 | '--autofix', 23 | '--indent', '4', 24 | '--no-sort-keys', 25 | ] 26 | - id: requirements-txt-fixer 27 | - id: trailing-whitespace 28 | - repo: meta 29 | hooks: 30 | - id: check-useless-excludes 31 | - repo: https://github.com/pre-commit/mirrors-pylint 32 | rev: v1.9.1 33 | hooks: 34 | - id: pylint 35 | additional_dependencies: [pylint-venv] 36 | args: ['--disable=all'] 37 | - repo: https://github.com/psf/black 38 | rev: stable 39 | hooks: 40 | - id: black 41 | language_version: python3.7 42 | exclude: dodo.py 43 | - repo: https://github.com/pre-commit/mirrors-mypy 44 | rev: v0.740 45 | hooks: 46 | - id: mypy 47 | args: [--no-strict-optional, --ignore-missing-imports] 48 | - repo: https://github.com/Lucas-C/pre-commit-hooks-safety 49 | rev: v1.1.0 50 | hooks: 51 | - id: python-safety-dependencies-check 52 | - repo: https://github.com/PyCQA/bandit 53 | rev: 1.6.2 54 | hooks: 55 | - id: bandit 56 | args: [ 57 | '-c', 'bandit.yml' 58 | ] 59 | name: 'Checking vulnerabilities' 60 | description: 'Bandit is a tool for finding common security issues in Python code' 61 | entry: bandit 62 | language: python 63 | types: [python] 64 | - repo: https://github.com/Lucas-C/pre-commit-hooks 65 | rev: v1.1.7 66 | hooks: 67 | - id: insert-license 68 | files: \.py 69 | args: 70 | - --license-filepath 71 | - etc/license-header 72 | - id: insert-license 73 | files: \.yml 74 | args: 75 | - --license-filepath 76 | - etc/license-header 77 | - id: insert-license 78 | files: \.txt 79 | args: 80 | - --license-filepath 81 | - etc/license-header 82 | - id: insert-license 83 | files: \.ini 84 | args: 85 | - --license-filepath 86 | - etc/license-header 87 | - id: insert-license 88 | files: \.tf 89 | args: 90 | - --license-filepath 91 | - etc/license-header 92 | - id: insert-license 93 | files: \.hcl 94 | args: 95 | - --license-filepath 96 | - etc/license-header 97 | - repo: https://github.com/antonbabenko/pre-commit-terraform 98 | rev: v1.19.0 99 | hooks: 100 | - id: terraform_fmt 101 | - id: terraform_validate 102 | - repo: https://github.com/Yelp/detect-secrets 103 | rev: v0.13.0 104 | hooks: 105 | - id: detect-secrets 106 | args: ['--baseline', '.secrets.baseline'] 107 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | MAINTAINER Stewart Henderson 3 | 4 | ENV PYTHONDONTWRITEBYTECODE 1 \ 5 | PYTHONUNBUFFERED 1 6 | 7 | RUN mkdir -p /base/etc 8 | COPY etc /base/etc 9 | 10 | RUN mkdir -p /base/bin 11 | COPY bin /base/bin 12 | 13 | WORKDIR /base 14 | COPY automation_requirements.txt /base 15 | COPY src/app_requirements.txt /base 16 | COPY src/test_requirements.txt /base 17 | 18 | RUN apk add bash && \ 19 | bin/install-packages.sh && \ 20 | pip3 install -r automation_requirements.txt && \ 21 | pip3 install -r app_requirements.txt && \ 22 | pip3 install -r test_requirements.txt && \ 23 | pip3 install awscli==1.16.213 && \ 24 | pip3 install "connexion[swagger-ui]" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SubHub 2 | 3 | [![Build Status](https://travis-ci.org/mozilla/subhub.svg?branch=master)](https://travis-ci.org/mozilla/subhub) 4 | 5 | 6 | [![codecov](https://codecov.io/gh/mozilla/subhub/branch/master/graph/badge.svg)](https://codecov.io/gh/mozilla/subhub) 7 | 8 | 9 | Payment subscription REST api for customers: 10 | - [FxA](https://github.com/mozilla/fxa), Firefox Accounts 11 | 12 | ### [Prerequisites](./docs/prerequisites.md) 13 | 14 | ### [Environment](./docs/environment.md) 15 | 16 | ### [Build System](./docs/doit.md) 17 | 18 | ### [Docker](./docs/docker.md) 19 | 20 | ### [Debugging](./docs/debugging.md) 21 | 22 | ### [RESTful Service Interactions](./docs/rest.md) 23 | 24 | ### [Site Reliability Information (SRE)](./docs/sre_info.md) 25 | 26 | ### [AWS DynamoDB Backups](./docs/backups.md) 27 | 28 | ### [Vagrant support to replicate TravisCI](./docs/vagrant.md) 29 | 30 | ### [Code of Conduct](./CODE_OF_CONDUCT.md) 31 | 32 | ### [License](./LICENSE.md) 33 | 34 | ## Authors 35 | 36 | Scott Idler Stewart Henderson Marty Ballard 37 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | # This Vagrantfile is meant to provide a mechanism by which to replicate the build server 4 | # for testing against that environment locally should you not be able to replicate an 5 | # issue such as a failing unit test locally. 6 | # 7 | # The TravisCI file defines the build environment: 8 | # https://github.com/mozilla/subhub/blob/master/.travis.yml 9 | 10 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 11 | VAGRANTFILE_API_VERSION = "2" 12 | 13 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 14 | # Ubuntu Xenial 15 | # https://app.vagrantup.com/ubuntu/boxes/xenial64 16 | config.vm.box = "ubuntu/xenial64" 17 | 18 | config.vm.box_check_update = true 19 | 20 | config.ssh.insert_key = true 21 | config.ssh.forward_agent = true 22 | 23 | config.vm.network :private_network, ip: "10.10.10.10" 24 | 25 | config.vm.synced_folder ".", "/vagrant", disabled: true 26 | config.vm.synced_folder "./", "/opt/subhub" 27 | 28 | config.vm.provider "virtualbox" do |vb| 29 | vb.name = "TravisCI" 30 | 31 | # Disallow Desktop login 32 | vb.gui = false 33 | 34 | vb.memory = "2048" 35 | vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 36 | vb.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/v-root", "1"] 37 | end 38 | 39 | config.vm.provision :shell, path: "./bin/vagrant.sh" 40 | config.vm.provision :shell, path: "./bin/build.sh", privileged: false 41 | end 42 | -------------------------------------------------------------------------------- /automation_requirements.txt: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | black==19.10b0 6 | codecov==2.0.15 7 | # requirements for getting doit commands to run 8 | # this is for automation of dev tasks, not required to run 9 | doit==0.32.0 10 | flake8==3.7.8 11 | locustio==0.11.0 12 | pre-commit==1.20.0 13 | pyparsing==2.4.0 14 | python-decouple==3.1 15 | ruamel.yaml==0.15.97 16 | structlog==19.1.0 17 | tox==3.14.0 18 | -------------------------------------------------------------------------------- /bandit.yml: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | exclude_dirs: 6 | - src/shared/tests 7 | - src/hub/tests 8 | skips: [B101, B404] 9 | -------------------------------------------------------------------------------- /bin/aws-credentials.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # NOTE: This script is used to provision both TravisCI and Jenkins, AWS credentials and configuration 4 | # Reference AWS Environment Variables 5 | # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html 6 | 7 | mkdir -p ~/.aws 8 | 9 | cat > ~/.aws/credentials << EOL 10 | [default] 11 | aws_access_key_id = ${AWS_ACCESS_KEY_ID:-fake-id} 12 | aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY:-fake-key} 13 | EOL 14 | 15 | cat >~/.aws/config <<-EOF 16 | [default] 17 | output=json 18 | region=${AWS_DEFAULT_REGION:-us-west-2} 19 | EOF -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd /opt/subhub 4 | pip3 install -r automation_requirements.txt 5 | python3 dodo.py 6 | -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ $TRAVIS_EVENT_TYPE == cron ]]; then 4 | exit 1; 5 | fi 6 | 7 | case "$TRAVIS_BRANCH" in 8 | 'feature/staging') 9 | DEPLOY_ENV=staging 10 | ;; 11 | 'release/prod-test') 12 | DEPLOY_ENV=prod-test 13 | doit deploy 14 | ;; 15 | 'release/prod') 16 | DEPLOY_ENV=prod 17 | ;; 18 | *) 19 | echo "No DEPLOY_ENV to set." 20 | ;; 21 | esac 22 | 23 | if [ -z "$DEPLOY_ENV" ]; then 24 | echo "Not deployinng" 25 | else 26 | echo "Deploying to $DEPLOY_ENV" 27 | doit deploy 28 | fi -------------------------------------------------------------------------------- /bin/install-packages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Alpine Registry for package versions, https://pkgs.alpinelinux.org/packages 4 | apk update 5 | 6 | ALPINE_VERSION=v3.9 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | IFS=$'\n' read -d '' -r -a lines < "${DIR}/../etc/alpine-packages" 9 | apk add --no-cache --update-cache --repository "http://nl.alpinelinux.org/alpine/${ALPINE_VERSION}/main" "${lines[@]}" 10 | -------------------------------------------------------------------------------- /bin/vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo apt-get update 4 | sudo apt-get uninstall -y python 5 | 6 | # Install Python 3.7 and pip 7 | sudo add-apt-repository ppa:deadsnakes/ppa 8 | sudo apt-get update 9 | sudo apt-get install -y python3.7 python3.7-dev python3-pip python3.7-venv python3.7-gdb gdb 10 | 11 | # Install Yarn 12 | curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - 13 | sudo apt-get install -y nodejs 14 | 15 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - 16 | echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 17 | sudo apt-get update 18 | sudo apt-get install -y yarn 19 | 20 | # Install AWSCLI 21 | sudo apt-get install -y awscli 22 | 23 | # Install docker-compose 24 | sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 25 | sudo chmod +x /usr/local/bin/docker-compose 26 | 27 | # doit dependency 28 | # doit.dependency.DatabaseException: db type is dbm.gnu, but the module is not available 29 | sudo apt-get install -y python3.7-gdbm 30 | sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 2 31 | 32 | 33 | echo 'alias python=python3.7' >> ~/.bashrc 34 | echo 'alias pip=pip3' >> ~/.bashrc 35 | echo 'cd /opt/subhub' >> /home/vagrant/.bashrc 36 | 37 | source ~/.bashrc 38 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # codecov.io Documentation: 6 | # https://docs.codecov.io/docs 7 | codecov: 8 | branch: master 9 | notify: 10 | require_ci_to_pass: yes 11 | 12 | coverage: 13 | precision: 0 14 | round: down 15 | range: "70...100" 16 | 17 | status: 18 | project: 19 | # disable the default status that measures entire project 20 | default: false 21 | hub: 22 | paths: 23 | # only include coverage in "src/hub" folder 24 | - src/hub 25 | enabled: yes 26 | threshold: 1% 27 | shared: 28 | paths: 29 | # only include coverage in "src/shared" folder 30 | - src/shared 31 | enabled: yes 32 | threshold: 1% 33 | patch: 34 | default: 35 | enabled: yes 36 | threshold: 1% 37 | changes: no 38 | 39 | ignore: 40 | - "docs/*" 41 | - "node_modules/*" 42 | - "etc/*" 43 | - "bin/*" 44 | parsers: 45 | gcov: 46 | branch_detection: 47 | conditional: yes 48 | loop: yes 49 | method: no 50 | macro: no 51 | comment: 52 | layout: "reach, diff, files" 53 | require_changes: yes 54 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | version: "3.7" 6 | 7 | services: 8 | base: 9 | image: mozilla/subhub-base 10 | container_name: base 11 | build: 12 | context: . 13 | dockerfile: Dockerfile.base 14 | hub: 15 | container_name: hub 16 | image: mozilla/hub 17 | command: python3 hub/app.py 18 | build: 19 | context: src/hub 20 | dockerfile: Dockerfile 21 | args: 22 | LOCAL_FLASK_PORT: 5001 23 | DYNALITE_PORT: 8000 24 | environment: 25 | AWS_ACCESS_KEY_ID: "fake-id" 26 | AWS_SECRET_ACCESS_KEY: "fake-key" 27 | BASKET_API_KEY: $BASKET_API_KEY 28 | BRANCH: $BRANCH 29 | DELETED_USER_TABLE: $DELETED_USER_TABLE 30 | DEPLOYED_BY: $DEPLOYED_BY 31 | DEPLOYED_ENV: $DEPLOYED_ENV 32 | DEPLOYED_WHEN: $DEPLOYED_WHEN 33 | EVENT_TABLE: $EVENT_TABLE 34 | LOCAL_HUB_FLASK_PORT: $LOCAL_HUB_FLASK_PORT 35 | LOG_LEVEL: $LOG_LEVEL 36 | HUB_API_KEY: $HUB_API_KEY 37 | PROCESS_EVENTS_HOURS: 6 38 | PROFILING_ENABLED: $PROFILING_ENABLED 39 | PROJECT_NAME: $PROJECT_NAME 40 | REMOTE_ORIGIN_URL: $REMOTE_ORIGIN_URL 41 | SALESFORCE_BASKET_URI: $SALESFORCE_BASKET_URI 42 | STRIPE_API_KEY: $STRIPE_API_KEY 43 | STRIPE_LOCAL: $STRIPE_LOCAL 44 | STRIPE_MOCK_PORT: $STRIPE_MOCK_PORT 45 | STRIPE_REQUEST_TIMEOUT: $STRIPE_REQUEST_TIMEOUT 46 | VERSION: $VERSION 47 | REVISION: $REVISION 48 | ports: 49 | - "5001:5001" 50 | depends_on: 51 | - base 52 | - stripe 53 | - dynamodb 54 | 55 | dynamodb: 56 | image: amazon/dynamodb-local:latest 57 | container_name: dynamodb-local 58 | ports: 59 | - 8000:8000 60 | 61 | stripe: 62 | image: stripemock/stripe-mock:latest 63 | container_name: stripe 64 | ports: 65 | - "$STRIPE_MOCK_PORT:12112" 66 | -------------------------------------------------------------------------------- /docs/backups.md: -------------------------------------------------------------------------------- 1 | # Backups 2 | 3 | ## DynamoDB tables 4 | All the DynamoDB tables created via Serverless have Point-In-Time-Recovery (from now on PITR) backups enabled. This kind of backup is completely managed by AWS an consists on incremental backups triggered by modifications to the table. They are billed by space used and are kept and automatically rotated after 35 days, furthermore when a table is deleted AWS triggers an on-demand backup of the table which will be present for 35 days free of charge. 5 | 6 | For a more in-depth understanding of PITR visit the official AWS documentation [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/PointInTimeRecovery.html). 7 | 8 | ### Restoring a PITR backup 9 | The process for restoring a PITR backup is straightforward and can de done using AWS UI console or CLI tool, the official documentation describes step by step how to do it [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/PointInTimeRecovery.Tutorial.html). It's recommended to use the AWS console for its simplicity. 10 | 11 | When restoring a table a user has to choose: a name for the new database and the point in time (i.e: month, day and time) from when to restore the database. 12 | After some minutes, the backup will be restored into a new database with the name you chose. 13 | 14 | ### Restoring from an on-demand backup 15 | On-demand backups are created by AWS when a table is deleted, also when triggered by a user. Restoring an on-demand backup involves the same process that restoring a PITR but instead of choosing the time when the backup was created, the user has to choose the name of the backup. If the backup was automatically created by the system on table deletion, it will be named as the original table plus the suffix "$DeletedTableBackup". 16 | 17 | ### Disaster recovery 18 | In a disaster recovery scenario, you'd probably want to start using this newly restored table, because DynamoDB does not allow changing the name of a table, at this point there are 2 options: pointing the code to the new restored database or update the old database with the contents of the restored one. 19 | 20 | -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | ## GDB 4 | 5 | 1. Set environment variables for the environment to be debugged. 6 | 2. Spin up associated docker container for dynalite if required, `docker-compose up dynalite`. If you are debugging against AWS DynamoDB itself be sure to setup your AWS Credentials accordingly. 7 | 3. Install packages: `sudo pip install -r src/app_requirements.txt` 8 | 4. Set the PYTHONPATH for gdb: `export PYTHONPATH=/opt/subhub/src/:PYTHONPATH` 9 | 5. Start gdb: `gdb python3.7` 10 | 6. Run the application of choise: `run APPLICATION` 11 | 12 | Where APPLICATION is either 13 | * `src/hub/app.py` 14 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | This project uses docker and docker-compose for running the project locally. As opposed a typical docker build in which artifacts are placed into a docker container 4 | during the build process, this project requires external work to overcome obstacles 5 | with symbolic links. This is done via the build system and can be ran via the `doit tar` task. This method of operation though does create some unfortunate side effects of not being directly able to debug the application with tools such as the 6 | [GNU Project Debugger](https://www.gnu.org/software/gdb/). 7 | 8 | ## Running 9 | 10 | You may run locally with the task, `doit local`. This spins up a few components out of the box: 11 | 12 | * hub 13 | * stripe-mock 14 | * Dynamodb mock 15 | 16 | These components allow for isolated testing of the ensemble but comes with some caveats. The Stripe API key for the interactions with stripe-mock should be in the 17 | form of `sk_test_123`. This is required should you set the environment with the variable of `STRIPE_LOCAL` to be `true`. If this is not set to `true` then the application will not proxy to the docker container but to `api.stripe.com`. In this case, the application will require a `STRIPE_API_KEY` as setup from Stripe. 18 | -------------------------------------------------------------------------------- /docs/prerequisites.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | - python3.7: requires python3.7 interpreter for creating virtual environments for testing and running subhub 4 | - [yarn](https://yarnpkg.com): package manager for node modules for setting up serverless for running and deploying subhub 5 | - Docker 6 | - Docker Compose 7 | -------------------------------------------------------------------------------- /docs/rest.md: -------------------------------------------------------------------------------- 1 | # RESTful Interactions 2 | 3 | ## Swagger User Interface 4 | 5 | Navigate to `http://127.0.0.1:5001/v1/ui/` in a browser of choice. 6 | 7 | 8 | ## Get Hub function version 9 | ``` 10 | curl --silent http://127.0.0.1:5001/v1/hub/version 11 | { 12 | "BRANCH": "CURRENT_BRANCH", 13 | "REVISION": "CURRENT_REVISION", 14 | "VERSION": "CURRENT_VERSION" 15 | } 16 | ``` 17 | 18 | Where, 19 | 20 | * `CURRENT_BRANCH` is the currently deployed branch 21 | * `CURRENT_REVISION` is the currently deployed revision 22 | * `CURRENT_VERSION` is the currently deployed version 23 | 24 | ## Get Hub function deployment information 25 | ``` 26 | curl --silent http://127.0.0.1:5001/v1/hub/deployed 27 | { 28 | "DEPLOYED_BY": "root@7259f8263190", 29 | "DEPLOYED_ENV": "local", 30 | "DEPLOYED_WHEN": "2019-09-17T14:14:04.975523" 31 | } 32 | ``` 33 | 34 | ## Postman 35 | 36 | A [Postman](https://www.getpostman.com/) URL collection is available for testing, learning, 37 | etc [here](https://www.getpostman.com/collections/ab233178aa256e424668). 38 | -------------------------------------------------------------------------------- /docs/sre_info.md: -------------------------------------------------------------------------------- 1 | # SRE Info 2 | 3 | This is the SRE_INFO.md file which should be found in the root of any source code that is 4 | administered by the Mozilla IT SRE team. We are available on #it-sre on slack. 5 | 6 | ## Infra Access 7 | 8 | [SRE aws-vault setup](https://mana.mozilla.org/wiki/display/SRE/aws-vault) 9 | 10 | [SRE account guide](https://mana.mozilla.org/wiki/display/SRE/AWS+Account+access+guide) 11 | 12 | [SRE AWS accounts](https://github.com/mozilla-it/itsre-accounts/blob/master/accounts/mozilla-itsre/terraform.tfvars#L5) 13 | 14 | ## Secrets 15 | 16 | Secrets in this project all reside in AWS Secrets Manager. There is one set of secrets for each 17 | environment: prod, stage, qa, dev. These secrets are loaded as environment variables via the 18 | src/shared/secrets.py file and then generally used via the env loading mechanism in src/shared/cfg.py which 19 | uses decouple to load them as fields. 20 | 21 | ## Source Repos 22 | 23 | [subhub](https://github.com/mozilla/subhub) 24 | 25 | ## Monitoring 26 | 27 | ### SSL Expiry checks in New Relic 28 | 29 | ## Cloud Account 30 | 31 | AWS account mozilla-subhub 903937621340 32 | -------------------------------------------------------------------------------- /docs/vagrant.md: -------------------------------------------------------------------------------- 1 | # Vagrant 2 | 3 | ## Setup MacOS 4 | * Install Virtualbox `brew cask install virtualbox` 5 | * Install Vagrant `brew cask install vagrant` 6 | 7 | ## Running 8 | 9 | * Starting: `vagrant up --provision` 10 | * Stopping: `vagrant halt` 11 | * Destroying: `vagrant destroy` 12 | * SSH: `vagrant ssh` 13 | * (Re) Provisioning: `vagrant provision` 14 | 15 | ## Running Unit Tests 16 | 17 | * `cd /opt/subhub` 18 | * `pip3 install -r automation_requirements.txt` 19 | * `python3 dodo.py` 20 | * `doit test` 21 | 22 | ## Author(s) 23 | 24 | Stewart Henderson 25 | -------------------------------------------------------------------------------- /etc/alpine-packages: -------------------------------------------------------------------------------- 1 | build-base 2 | libgit2-dev 3 | libc-dev 4 | python3-dev 5 | libffi-dev 6 | zeromq-dev 7 | linux-headers 8 | openssl-dev 9 | nodejs 10 | curl 11 | yarn 12 | gcc 13 | g++ 14 | musl-dev 15 | pkgconfig 16 | git -------------------------------------------------------------------------------- /etc/license-header: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | [mypy] 6 | disallow_untyped_calls = True 7 | follow_imports = normal 8 | ignore_missing_imports = True 9 | python_version = 3.7 10 | strict_optional = True 11 | warn_no_return = False 12 | warn_redundant_casts = True 13 | warn_unused_ignores = True 14 | warn_unreachable = False 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "automation tools for running locally and deploying to aws", 3 | "repository": { 4 | "type": "git", 5 | "url": "git+https://github.com/mozilla/subhub" 6 | }, 7 | "license": "MPL-2.0", 8 | "devDependencies": { 9 | "@hapi/hoek": "8.2.4", 10 | "serverless": "1.57.0", 11 | "serverless-domain-manager": "3.3.0", 12 | "serverless-package-external": "1.1.1", 13 | "serverless-plugin-aws-alerts": "1.4.0", 14 | "serverless-plugin-canary-deployments": "0.4.8", 15 | "serverless-plugin-tracing": "2.0.0", 16 | "serverless-python-requirements": "5.0.1", 17 | "serverless-wsgi": "1.7.3", 18 | "serverless-stack-termination-protection": "1.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | [MASTER] 6 | 7 | # Profiled execution. 8 | profile=no 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=.git scripts bin docs 13 | 14 | [MESSAGES CONTROL] 15 | 16 | # Enable the message, report, category or checker with the given id(s). You can 17 | # either give multiple identifier separated by comma (,) or put this option 18 | # multiple time. 19 | #enable= 20 | 21 | # Disable the message, report, category or checker with the given id(s). You 22 | # can either give multiple identifier separated by comma (,) or put this option 23 | # multiple time (only on the command line, not in the configuration file where 24 | # it should appear only once). 25 | 26 | # Brain-dead errors regarding standard language features 27 | # W0142 = *args and **kwargs support 28 | # W0403 = Relative imports 29 | 30 | # Pointless whinging 31 | # R0201 = Method could be a function 32 | # W0212 = Accessing protected attribute of client class 33 | # W0613 = Unused argument 34 | # W0232 = Class has no __init__ method 35 | # R0903 = Too few public methods 36 | # C0301 = Line too long 37 | # R0913 = Too many arguments 38 | # C0103 = Invalid name 39 | # R0914 = Too many local variables 40 | 41 | # PyLint's module importation is unreliable 42 | # F0401 = Unable to import module 43 | # W0402 = Uses of a deprecated module 44 | 45 | # Already an error when wildcard imports are used 46 | # W0614 = Unused import from wildcard 47 | 48 | # Sometimes disabled depending on how bad a module is 49 | # C0111 = Missing docstring 50 | 51 | # Disable the message(s) with the given id(s). 52 | disable=W0142,W0403,R0201,W0212,W0613,W0232,R0903,W0614,C0111,C0301,R0913,C0103,F0401,W0402,R0914,I0011 53 | 54 | [REPORTS] 55 | 56 | # Set the output format. Available formats are text, parseable, colorized, msvs 57 | # (visual studio) and html 58 | output-format=text 59 | 60 | # Include message's id in output 61 | include-ids=no 62 | 63 | [BASIC] 64 | # Bad variable names which should always be refused, separated by a comma 65 | bad-names=foo,bar,baz 66 | 67 | [FORMAT] 68 | 69 | # Maximum number of characters on a single line. 70 | max-line-length=80 71 | 72 | # Maximum number of lines in a module 73 | max-module-lines=1000 74 | 75 | [SIMILARITIES] 76 | 77 | # Minimum lines number of a similarity. 78 | min-similarity-lines=4 79 | 80 | # Ignore comments when computing similarities. 81 | ignore-comments=yes 82 | 83 | # Ignore docstrings when computing similarities. 84 | ignore-docstrings=yes 85 | -------------------------------------------------------------------------------- /reports/.gitignore: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # coverage data file 6 | .coverage 7 | 8 | # HTML reports 9 | html/* 10 | 11 | # XML coverage report 12 | coverage.xml 13 | -------------------------------------------------------------------------------- /reports/README.md: -------------------------------------------------------------------------------- 1 | # Reports 2 | 3 | This directory contains the coverage reporting data from test runs. 4 | 5 | ## Author(s) 6 | 7 | Stewart Henderson 8 | -------------------------------------------------------------------------------- /services/fxa/README.md: -------------------------------------------------------------------------------- 1 | # Serverless 2 | 3 | ## Commands 4 | 5 | From this (`services/fxa`) directory, execute the following commands of interest. NOTE: If you require extra detail of the Serverless framework, 6 | you will need to set the follow environment variable. 7 | 8 | `export SLS_DEBUG=*` 9 | 10 | ### Offline Testing 11 | 12 | Start DynamoDB locally, `sls dynamodb start &` 13 | 14 | Start offline services, `serverless offline start --host 0.0.0.0 --stage dev` 15 | 16 | Once this is done, you can access the DynamoDB Javascript Shell by 17 | navigating [here](http://localhost:8000/shell/). Additionally, you may interact with the application as you would on AWS via commands such as: 18 | * Perform a HTTP GET of `http://localhost:3000/v1/hub/version` 19 | 20 | ### Domain Creation 21 | 22 | `sls create_domain` 23 | 24 | ### Packaging 25 | 26 | `sls package --stage ENVIRONMENT` 27 | 28 | Where `ENVIRONMENT` is in the set of (dev, staging, prod). 29 | 30 | You may inspect the contents of each packages with: 31 | 32 | `zipinfo .serverless/{ARCHIVE}.zip` 33 | 34 | Where `ARCHIVE` is a member of 35 | 36 | * hub 37 | * mia 38 | 39 | ### Logs 40 | 41 | You can inspect the Serverless logs by function via the command: 42 | 43 | `sls logs -f {FUNCTION}` 44 | 45 | Where `FUNCTION` is a member of 46 | 47 | * hub 48 | * mia 49 | 50 | #### Live Tailing of the Logs 51 | 52 | `serverless logs -f {FUNCTION} --tail` 53 | 54 | ### Running 55 | 56 | `sls wsgi serve` 57 | 58 | ### To-do 59 | 60 | * [Investigate Serverless Termination Protection for Production](https://www.npmjs.com/package/serverless-termination-protection) 61 | * [Investigate metering requests via apiKeySourceType](https://serverless.com/framework/docs/providers/aws/events/apigateway/) 62 | 63 | ## References 64 | 65 | 1. [SLS_DEBUG](https://github.com/serverless/serverless/pull/1729/files) 66 | 2. [API Gateway Resource Policy Support](https://github.com/serverless/serverless/issues/4926) 67 | 3. [Add apig resource policy](https://github.com/serverless/serverless/pull/5071) 68 | 4. [add PRIVATE endpointType](https://github.com/serverless/serverless/pull/5080) 69 | 5. [Serverless AWS Lambda Events](https://serverless.com/framework/docs/providers/aws/events/) 70 | -------------------------------------------------------------------------------- /services/fxa/accounts.yml: -------------------------------------------------------------------------------- 1 | fxa: 2 | prod: 361527076523 3 | prod-test: 142069644989 4 | stage: 142069644989 5 | qa: 142069644989 6 | dev: 927034868273 7 | fab: 927034868273 8 | -------------------------------------------------------------------------------- /services/fxa/env.yml: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | default: 6 | DEPLOYED_BY: ${env:DEPLOYED_BY} 7 | DEPLOYED_ENV: ${env:DEPLOYED_ENV} 8 | DEPLOYED_WHEN: ${env:DEPLOYED_WHEN} 9 | STAGE: ${self:provider.stage} 10 | PROJECT_NAME: ${env:PROJECT_NAME} 11 | BRANCH: ${env:BRANCH} 12 | REVISION: ${env:REVISION} 13 | VERSION: ${env:VERSION} 14 | REMOTE_ORIGIN_URL: ${env:REMOTE_ORIGIN_URL} 15 | LOG_LEVEL: ${env:LOG_LEVEL} 16 | PROFILING_ENABLED: ${env:PROFILING_ENABLED} 17 | PROCESS_EVENTS_HOURS: 6 18 | USER_TABLE: ${env:USER_TABLE} 19 | DELETED_USER_TABLE: ${env:DELETED_USER_TABLE} 20 | EVENT_TABLE: ${env:EVENT_TABLE} 21 | STRIPE_REQUEST_TIMEOUT: ${env:STRIPE_REQUEST_TIMEOUT} 22 | SENTRY_URL: ${env:SENTRY_URL} -------------------------------------------------------------------------------- /services/fxa/functions.yml: -------------------------------------------------------------------------------- 1 | prod: 2 | LAMBDA_MEMORY_SIZE: 512 3 | LAMBDA_RESERVED_CONCURRENCY: 5 4 | LAMBDA_TIMEOUT: 10 5 | MIA_RATE_SCHEDULE: "6 hours" 6 | DEPLOYMENT_TYPE: Linear10PercentEvery1Minute 7 | prod-test: 8 | LAMBDA_MEMORY_SIZE: 512 9 | LAMBDA_RESERVED_CONCURRENCY: 5 10 | LAMBDA_TIMEOUT: 10 11 | MIA_RATE_SCHEDULE: "30 days" 12 | DEPLOYMENT_TYPE: AllAtOnce 13 | stage: 14 | LAMBDA_MEMORY_SIZE: 512 15 | LAMBDA_RESERVED_CONCURRENCY: 5 16 | LAMBDA_TIMEOUT: 10 17 | MIA_RATE_SCHEDULE: "6 hours" 18 | DEPLOYMENT_TYPE: AllAtOnce 19 | qa: 20 | LAMBDA_MEMORY_SIZE: 512 21 | LAMBDA_RESERVED_CONCURRENCY: 5 22 | LAMBDA_TIMEOUT: 10 23 | MIA_RATE_SCHEDULE: "30 days" 24 | DEPLOYMENT_TYPE: AllAtOnce 25 | dev: 26 | LAMBDA_MEMORY_SIZE: 256 27 | LAMBDA_RESERVED_CONCURRENCY: 5 28 | LAMBDA_TIMEOUT: 10 29 | MIA_RATE_SCHEDULE: "2 hours" 30 | DEPLOYMENT_TYPE: AllAtOnce 31 | fab: 32 | LAMBDA_MEMORY_SIZE: 256 33 | LAMBDA_RESERVED_CONCURRENCY: 5 34 | LAMBDA_TIMEOUT: 10 35 | MIA_RATE_SCHEDULE: "2 hours" 36 | DEPLOYMENT_TYPE: AllAtOnce 37 | -------------------------------------------------------------------------------- /services/fxa/hubhandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | import os 8 | import sys 9 | import serverless_wsgi 10 | 11 | from sentry_sdk import init, capture_message 12 | from os.path import join, dirname, realpath 13 | 14 | serverless_wsgi.TEXT_MIME_TYPES.append("application/custom+json") 15 | 16 | # First some funky path manipulation so that we can work properly in 17 | # the AWS environment 18 | sys.path.insert(0, join(dirname(realpath(__file__)), "src")) 19 | 20 | from hub.app import create_app 21 | from shared.cfg import CFG 22 | from shared.log import get_logger 23 | 24 | init(CFG.SENTRY_URL) 25 | logger = get_logger() 26 | hub_app = create_app() 27 | 28 | # NOTE: The context object has the following available to it. 29 | # https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html#python-context-object-props 30 | # NOTE: Available environment passed to the Flask from serverless-wsgi 31 | # https://github.com/logandk/serverless-wsgi/blob/2911d69a87ae8057110a1dcf0c21288477e07ce1/serverless_wsgi.py#L126 32 | def handle(event, context): 33 | try: 34 | return serverless_wsgi.handle_request(hub_app.app, event, context) 35 | except Exception as e: # pylint: disable=broad-except 36 | logger.exception( 37 | "exception occurred", subhub_event=event, context=context, error=e 38 | ) 39 | raise 40 | finally: 41 | logger.info("handling hub event", subhub_event=event, context=context) 42 | -------------------------------------------------------------------------------- /services/fxa/miahandler.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import sys 7 | import serverless_wsgi 8 | import structlog 9 | 10 | from sentry_sdk import init, capture_message 11 | from os.path import join, dirname, realpath 12 | 13 | # First some funky path manipulation so that we can work properly in 14 | # the AWS environment 15 | sys.path.insert(0, join(dirname(realpath(__file__)), "src")) 16 | 17 | from hub.verifications import events_check 18 | from shared.log import get_logger 19 | from shared.cfg import CFG 20 | 21 | init(CFG.SENTRY_URL) 22 | 23 | logger = get_logger() 24 | 25 | # NOTE: The context object has the following available to it. 26 | # https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html#python-context-object-props 27 | # NOTE: Available environment passed to the Flask from serverless-wsgi 28 | # https://github.com/logandk/serverless-wsgi/blob/2911d69a87ae8057110a1dcf0c21288477e07ce1/serverless_wsgi.py#L126 29 | def handle(event, context): 30 | try: 31 | processing_duration = int(os.getenv("PROCESS_EVENTS_HOURS", "6")) 32 | events_check.process_events(processing_duration) 33 | except Exception as e: # pylint: disable=broad-except 34 | logger.exception( 35 | "exception occurred", subhub_event=event, context=context, error=e 36 | ) 37 | raise 38 | finally: 39 | logger.info("handling mia event", subhub_event=event, context=context) 40 | -------------------------------------------------------------------------------- /services/fxa/resources/sns-topic.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | SubHubSNS: 3 | Type: AWS::SNS::Topic 4 | Properties: 5 | DisplayName: FxA ${self:provider.stage} Event Data 6 | TopicName: ${self:provider.stage}-fxa-event-data 7 | SubHubTopicPolicy: 8 | Type: AWS::SNS::TopicPolicy 9 | Properties: 10 | PolicyDocument: 11 | Id: AWSAccountTopicAccess 12 | Version: '2008-10-17' 13 | Statement: 14 | - Sid: FxAStageAccess 15 | Effect: Allow 16 | Principal: 17 | AWS: arn:aws:iam::${self:provider.snsaccount}:root 18 | Action: 19 | - SNS:Subscribe 20 | - SNS:Receive 21 | - SNS:GetTopicAttributes 22 | Resource: arn:aws:sns:us-west-2:903937621340:${self:provider.stage}-fxa-event-data 23 | Topics: 24 | - Ref: SubHubSNS -------------------------------------------------------------------------------- /services/fxa/resources/tags.yml: -------------------------------------------------------------------------------- 1 | default: 2 | cost-center: 1440 3 | project-name: Subhub 4 | project-desc: Payment subscription REST API for customers 5 | project-email: subhub@mozilla.com 6 | deployed-by: ${env:DEPLOYED_BY} 7 | deployed-env: ${env:DEPLOYED_ENV} 8 | deployed-when: ${env:DEPLOYED_WHEN} 9 | deployed-method: serverless 10 | sources: https://github.com/mozilla/subhub 11 | urls: prod.fxa.mozilla-subhub.app/v1 12 | keywords: subscriptions:flask:serverless:swagger 13 | branch: ${env:BRANCH} 14 | revision: ${env:REVISION} 15 | version: ${env:VERSION} 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 3 | ignore=E266,W504,W503 4 | 5 | [aliases] 6 | test=pytest 7 | 8 | [tool:pytest] 9 | testpaths = subhub 10 | 11 | [pytest] 12 | addopts = -p no:warnings 13 | 14 | [mypy] 15 | disallow_untyped_calls = True 16 | follow_imports = normal 17 | ignore_missing_imports = True 18 | python_version = 3.7 19 | strict_optional = True 20 | warn_no_return = False 21 | warn_redundant_casts = True 22 | warn_unused_ignores = True 23 | warn_unreachable = False 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | #from cfg import CFG #FIXME: this causes import errors 6 | 7 | with open("README.md", "r") as fh: 8 | long_description = fh.read() 9 | 10 | with open('src/app_requirements.txt') as f: 11 | app_requirements = f.read().splitlines() 12 | 13 | with open('src/test_requirements.txt') as f: 14 | test_requirements = f.read().splitlines() 15 | 16 | setup_requirements = [ 17 | 'pytest-runner', 18 | 'setuptools>=40.5.0', 19 | ] 20 | 21 | extras = {'test': test_requirements} 22 | 23 | setup( 24 | name='subhub', 25 | #version=CFG.VERSION, #FIXME: would be nice to have this 26 | version='v0.1', 27 | author='Scott Idler', 28 | author_email='sidler@mozilla.com', 29 | description='Flask application for facilitating Subscription Services', 30 | long_description=long_description, 31 | url='https://github.com/mozilla-it/subhub', 32 | classifiers=( 33 | 'Programming Language :: Python :: 3', 34 | 'License :: OSI Approved :: Mozilla Public License', 35 | 'Operating System :: OS Independent', 36 | ), 37 | install_requires=app_requirements, 38 | license='Mozilla Public License 2.0', 39 | include_package_data=True, 40 | packages=find_packages(include=['src']), 41 | setup_requires=setup_requirements, 42 | test_suite='tests', 43 | tests_require=test_requirements, 44 | extras_require=extras, 45 | zip_safe=False, 46 | ) 47 | -------------------------------------------------------------------------------- /src/app_requirements.txt: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # requirements for hub.application to run 6 | # do not add testing reqs or automation reqs here 7 | attrdict==2.0.1 8 | boto3==1.9.245 9 | botocore==1.12.245 10 | cachetools==3.1.1 11 | certifi==2019.6.16 12 | chardet==3.0.4 13 | colorama==0.4.1 14 | connexion==2.6.0 15 | connexion[swagger-ui] 16 | docutils==0.14 17 | Flask==1.1.0 18 | Flask-Cors==3.0.8 19 | idna==2.8 20 | inflection==0.3.1 21 | itsdangerous==1.1.0 22 | Jinja2==2.10.1 23 | jmespath==0.9.4 24 | jsonschema==2.5.1 25 | MarkupSafe==1.1.1 26 | openapi-spec-validator==0.2.7 27 | pathlib==1.0.1 28 | psutil==5.6.6 29 | pyinstrument==3.0.3 30 | pynamodb==4.0.0 31 | python-dateutil==2.8.0 32 | python-decouple==3.1 33 | python-json-logger==0.1.11 34 | PyYAML==5.1.1 35 | raven==6.10.0 36 | requests==2.22.0 37 | s3transfer==0.2.1 38 | sentry-sdk==0.14.3 39 | serverless-wsgi==1.7.3 40 | six==1.12.0 41 | stripe==2.35.1 42 | structlog==19.1.0 43 | tenacity==5.1.1 44 | urllib3==1.25.3 45 | -------------------------------------------------------------------------------- /src/hub/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mozilla/subhub-base:latest 2 | MAINTAINER Stewart Henderson 3 | 4 | ARG STRIPE_API_KEY 5 | ARG AWS_ACCESS_KEY_ID 6 | ARG AWS_SECRET_ACCESS_KEY 7 | ARG LOCAL_HUB_FLASK_PORT 8 | ARG SUPPORT_API_KEY 9 | ARG DYNALITE_PORT 10 | ARG STRIPE_REQUEST_TIMEOUT 11 | 12 | ENV STRIPE_API_KEY=$STRIPE_API_KEY \ 13 | AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ 14 | AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 15 | LOCAL_HUB_FLASK_PORT=$LOCAL_HUB_FLASK_PORT \ 16 | HUB_API_KEY=$HUB_API_KEY \ 17 | DYNALITE_PORT=$DYNALITE_PORT \ 18 | USER_TABLE=$USER_TABLE \ 19 | DELETED_USER_TABLE=$DELETED_USER_TABLE \ 20 | EVENT_TABLE=$EVENT_TABLE \ 21 | FLASK_ENV=development \ 22 | STRIPE_REQUEST_TIMEOUT=$STRIPE_REQUEST_TIMEOUT 23 | 24 | EXPOSE $LOCAL_FLASK_PORT 25 | 26 | RUN mkdir -p /subhub 27 | ADD .src.tar.gz /subhub/hub 28 | WORKDIR /subhub 29 | ENV PYTHONPATH=. 30 | -------------------------------------------------------------------------------- /src/hub/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/routes/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/routes/abstract.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import flask 6 | 7 | from abc import ABC 8 | 9 | from shared.log import get_logger 10 | 11 | logger = get_logger() 12 | 13 | 14 | class AbstractRoute(ABC): 15 | def __init__(self, payload) -> None: 16 | self.payload = payload 17 | 18 | def report_route(self, payload: dict, sent_system: str) -> None: 19 | logger.info("report route", payload=payload, sent_system=sent_system) 20 | if payload.get("event_id"): 21 | event_id = payload["event_id"] 22 | else: 23 | event_id = payload["Event_Id__c"] 24 | existing = flask.g.hub_table.get_event(event_id) 25 | if not existing: 26 | created_event = flask.g.hub_table.new_event( 27 | event_id=event_id, sent_system=sent_system 28 | ) 29 | saved = flask.g.hub_table.save_event(created_event) 30 | try: 31 | logger.info("new event created", created_event=created_event) 32 | except Exception as e: 33 | logger.error("Error logging created", error=e) 34 | try: 35 | logger.info("new event saved", saved=saved) 36 | except Exception as e: 37 | logger.error("Error logging saved", error=e) 38 | else: 39 | updated = flask.g.hub_table.append_event( 40 | event_id=event_id, sent_system=sent_system 41 | ) 42 | logger.info("updated event", existing=existing, updated=updated) 43 | 44 | def report_route_error(self, payload) -> None: 45 | logger.error("report route error", payload=payload) 46 | -------------------------------------------------------------------------------- /src/hub/routes/firefox.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import boto3 6 | import json 7 | 8 | from typing import Dict, Any 9 | from botocore.exceptions import ClientError 10 | from stripe.error import APIConnectionError 11 | 12 | from hub.routes.abstract import AbstractRoute 13 | from hub.shared.cfg import CFG 14 | from shared.log import get_logger 15 | 16 | logger = get_logger() 17 | 18 | 19 | class FirefoxRoute(AbstractRoute): 20 | def route(self) -> Dict[str, Any]: 21 | try: 22 | sns_client = boto3.client("sns", region_name=CFG.AWS_REGION) 23 | response = sns_client.publish( 24 | TopicArn=CFG.TOPIC_ARN_KEY, 25 | Message=json.dumps( 26 | {"default": self.payload} 27 | ), # json.dumps is required by FxA 28 | MessageStructure="json", 29 | ) 30 | if response["ResponseMetadata"]["HTTPStatusCode"] == 200: 31 | logger.info("message sent to Firefox queue", response=response) 32 | logger.info("firefox payload", payload=self.payload) 33 | if isinstance(self.payload, dict): 34 | route_payload = self.payload 35 | else: 36 | route_payload = json.loads(self.payload) 37 | self.report_route(route_payload, "firefox") 38 | return response 39 | except ClientError as e: 40 | logger.error("Firefox error", error=e) 41 | self.report_route_error(self.payload) 42 | -------------------------------------------------------------------------------- /src/hub/routes/pipeline.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from typing import Any, Dict, Optional, List 6 | 7 | from hub.routes.salesforce import SalesforceRoute 8 | from hub.routes.static import StaticRoutes 9 | from hub.shared.exceptions import UnsupportedStaticRouteError, UnsupportedDataError 10 | 11 | 12 | class RoutesPipeline: 13 | def __init__(self, report_routes, data) -> None: 14 | self.report_routes = report_routes 15 | self.data = data 16 | 17 | def run(self) -> Optional[Any]: 18 | for r in self.report_routes: 19 | if r == StaticRoutes.SALESFORCE_ROUTE: 20 | return self.send_to_salesforce(self.data) 21 | else: 22 | raise UnsupportedStaticRouteError(r, StaticRoutes) # type: ignore 23 | 24 | def send_to_salesforce(self, data: Dict[str, Any]) -> int: 25 | salesforce_send = SalesforceRoute(data).route() 26 | return salesforce_send 27 | 28 | 29 | class AllRoutes: 30 | def __init__(self, messages_to_routes: List[Dict[str, Any]]) -> None: 31 | self.messages_to_routes = messages_to_routes 32 | 33 | def run(self) -> Optional[Any]: 34 | for m in self.messages_to_routes: 35 | if m["route_type"] == "salesforce_route": 36 | return self.send_to_salesforce(data=m.get("data")) 37 | else: 38 | raise UnsupportedDataError( # type: ignore 39 | m, m["route_type"], StaticRoutes 40 | ) 41 | 42 | @staticmethod 43 | def send_to_salesforce(data: Dict[str, Any]) -> int: 44 | salesforce_send = SalesforceRoute(data).route() 45 | return salesforce_send 46 | -------------------------------------------------------------------------------- /src/hub/routes/salesforce.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import json 6 | import requests 7 | from requests import Response 8 | 9 | from typing import Dict, Tuple, Any 10 | 11 | from hub.routes.abstract import AbstractRoute 12 | from shared.cfg import CFG 13 | from shared.log import get_logger 14 | 15 | logger = get_logger() 16 | 17 | 18 | class SalesforceRoute(AbstractRoute): 19 | def route(self) -> int: 20 | if isinstance(self.payload, dict): 21 | route_payload = self.payload 22 | else: 23 | route_payload = json.loads(self.payload) 24 | headers = {"x-api-key": CFG.BASKET_API_KEY} 25 | basket_url = CFG.SALESFORCE_BASKET_URI 26 | request_post = requests.post(basket_url, json=route_payload, headers=headers) 27 | self.report_route(route_payload, "salesforce") 28 | logger.info( 29 | "sending to salesforce", payload=self.payload, request_post=request_post 30 | ) 31 | return request_post.status_code 32 | -------------------------------------------------------------------------------- /src/hub/routes/static.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | 6 | class StaticRoutes: 7 | FIREFOX_ROUTE = "firefox_route" 8 | SALESFORCE_ROUTE = "salesforce_route" 9 | -------------------------------------------------------------------------------- /src/hub/shared: -------------------------------------------------------------------------------- 1 | ../shared -------------------------------------------------------------------------------- /src/hub/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import sys 7 | import signal 8 | import subprocess 9 | import logging 10 | import psutil 11 | import pytest 12 | import stripe 13 | 14 | from flask import g 15 | 16 | from hub.shared.cfg import CFG 17 | from hub.app import create_app 18 | from shared.log import get_logger 19 | from shared.dynamodb import dynamodb 20 | 21 | logger = get_logger() 22 | 23 | 24 | def pytest_configure(): 25 | # Latest boto3 now wants fake credentials around, so here we are. 26 | os.environ["AWS_ACCESS_KEY_ID"] = "fake" 27 | os.environ["AWS_SECRET_ACCESS_KEY"] = "fake" 28 | os.environ["EVENT_TABLE"] = "events-testing" 29 | os.environ["ALLOWED_ORIGIN_SYSTEMS"] = "Test_system,Test_System,Test_System1" 30 | sys._called_from_test = True 31 | 32 | 33 | @pytest.fixture(autouse=True, scope="module") 34 | def app(dynamodb): 35 | os.environ["DYNALITE_URL"] = dynamodb 36 | app = create_app() 37 | with app.app.app_context(): 38 | g.hub_table = app.app.hub_table 39 | g.subhub_deleted_users = app.app.subhub_deleted_users 40 | yield app 41 | -------------------------------------------------------------------------------- /src/hub/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/invalid_plan_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "amount": "500", 4 | "currency": "usd", 5 | "frequency": "month" 6 | } 7 | ] -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_created_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.created", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "cus_00000000000000", 13 | "object": "customer", 14 | "account_balance": 0, 15 | "address": null, 16 | "balance": 0, 17 | "created": 1559595009, 18 | "currency": null, 19 | "default_source": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 20 | "delinquent": false, 21 | "description": "process_test", 22 | "discount": null, 23 | "email": "user123@tester.com", 24 | "invoice_prefix": "063A2048", 25 | "invoice_settings": { 26 | "custom_fields": null, 27 | "default_payment_method": null, 28 | "footer": null 29 | }, 30 | "livemode": false, 31 | "metadata": { 32 | "userid": "user123" 33 | }, 34 | "name": "Jon Tester", 35 | "phone": null, 36 | "preferred_locales": [ 37 | ], 38 | "shipping": null, 39 | "sources": { 40 | "object": "list", 41 | "data": [ 42 | { 43 | "id": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 44 | "object": "card", 45 | "address_city": null, 46 | "address_country": null, 47 | "address_line1": null, 48 | "address_line1_check": null, 49 | "address_line2": null, 50 | "address_state": null, 51 | "address_zip": null, 52 | "address_zip_check": null, 53 | "brand": "Visa", 54 | "country": "US", 55 | "customer": "cus_FBkbtyY63q01Jd", 56 | "cvc_check": null, 57 | "dynamic_last4": null, 58 | "exp_month": 6, 59 | "exp_year": 2020, 60 | "fingerprint": "eYFLxJ8weKg24R6o", 61 | "funding": "credit", 62 | "last4": "4242", 63 | "metadata": { 64 | }, 65 | "name": null, 66 | "tokenization_method": null 67 | } 68 | ], 69 | "has_more": false, 70 | "total_count": 1, 71 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/sources" 72 | }, 73 | "subscriptions": { 74 | "object": "list", 75 | "data": [ 76 | ], 77 | "has_more": false, 78 | "total_count": 0, 79 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/subscriptions" 80 | }, 81 | "tax_exempt": "none", 82 | "tax_ids": { 83 | "object": "list", 84 | "data": [ 85 | ], 86 | "has_more": false, 87 | "total_count": 0, 88 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/tax_ids" 89 | }, 90 | "tax_info": null, 91 | "tax_info_verification": null 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_created_event_missing_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.created", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "cus_00000000000000", 13 | "object": "customer", 14 | "account_balance": 0, 15 | "address": null, 16 | "balance": 0, 17 | "created": 1559595009, 18 | "currency": null, 19 | "default_source": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 20 | "delinquent": false, 21 | "description": "process_test", 22 | "discount": null, 23 | "email": "user123@tester.com", 24 | "invoice_prefix": "063A2048", 25 | "invoice_settings": { 26 | "custom_fields": null, 27 | "default_payment_method": null, 28 | "footer": null 29 | }, 30 | "livemode": false, 31 | "metadata": { 32 | "userid": "user123" 33 | }, 34 | "name": null, 35 | "phone": null, 36 | "preferred_locales": [ 37 | ], 38 | "shipping": null, 39 | "sources": { 40 | "object": "list", 41 | "data": [ 42 | { 43 | "id": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 44 | "object": "card", 45 | "address_city": null, 46 | "address_country": null, 47 | "address_line1": null, 48 | "address_line1_check": null, 49 | "address_line2": null, 50 | "address_state": null, 51 | "address_zip": null, 52 | "address_zip_check": null, 53 | "brand": "Visa", 54 | "country": "US", 55 | "customer": "cus_FBkbtyY63q01Jd", 56 | "cvc_check": null, 57 | "dynamic_last4": null, 58 | "exp_month": 6, 59 | "exp_year": 2020, 60 | "fingerprint": "eYFLxJ8weKg24R6o", 61 | "funding": "credit", 62 | "last4": "4242", 63 | "metadata": { 64 | }, 65 | "name": null, 66 | "tokenization_method": null 67 | } 68 | ], 69 | "has_more": false, 70 | "total_count": 1, 71 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/sources" 72 | }, 73 | "subscriptions": { 74 | "object": "list", 75 | "data": [ 76 | ], 77 | "has_more": false, 78 | "total_count": 0, 79 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/subscriptions" 80 | }, 81 | "tax_exempt": "none", 82 | "tax_ids": { 83 | "object": "list", 84 | "data": [ 85 | ], 86 | "has_more": false, 87 | "total_count": 0, 88 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/tax_ids" 89 | }, 90 | "tax_info": null, 91 | "tax_info_verification": null 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_no_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cus_test1", 3 | "object": "customer", 4 | "account_balance": 0, 5 | "address": null, 6 | "balance": 0, 7 | "created": 1567635511, 8 | "currency": "usd", 9 | "default_source": null, 10 | "delinquent": false, 11 | "description": null, 12 | "discount": null, 13 | "email": null, 14 | "invoice_prefix": "F351DA7", 15 | "invoice_settings": { 16 | "custom_fields": null, 17 | "default_payment_method": null, 18 | "footer": null 19 | }, 20 | "livemode": false, 21 | "metadata": {}, 22 | "name": null, 23 | "phone": null, 24 | "preferred_locales": [], 25 | "shipping": null, 26 | "sources": { 27 | "object": "list", 28 | "data": [ 29 | { 30 | "funding": "credit", 31 | "last4": "1234", 32 | "exp_month": 12, 33 | "exp_year": 2020 34 | } 35 | ], 36 | "has_more": false, 37 | "total_count": 0, 38 | "url": "/v1/customers/cus_test1/sources" 39 | }, 40 | "subscriptions": { 41 | "object": "list", 42 | "data": [], 43 | "has_more": false, 44 | "total_count": 0, 45 | "url": "/v1/customers/cus_test1/subscriptions" 46 | }, 47 | "tax_exempt": "none", 48 | "tax_ids": { 49 | "object": "list", 50 | "data": [], 51 | "has_more": false, 52 | "total_count": 0, 53 | "url": "/v1/customers/cus_test1/tax_ids" 54 | }, 55 | "tax_info": null, 56 | "tax_info_verification": null 57 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cus_test1", 3 | "object": "customer", 4 | "account_balance": 0, 5 | "address": null, 6 | "balance": 0, 7 | "created": 1567635511, 8 | "currency": "usd", 9 | "default_source": null, 10 | "delinquent": false, 11 | "description": null, 12 | "discount": null, 13 | "email": "test@example.com", 14 | "invoice_prefix": "F351DA7", 15 | "invoice_settings": { 16 | "custom_fields": null, 17 | "default_payment_method": null, 18 | "footer": null 19 | }, 20 | "livemode": false, 21 | "metadata": { 22 | "userid": "user123" 23 | }, 24 | "name": "Bob Tester", 25 | "phone": null, 26 | "preferred_locales": [], 27 | "shipping": null, 28 | "sources": { 29 | "object": "list", 30 | "data": [ 31 | { 32 | "funding": "credit", 33 | "last4": "1234", 34 | "exp_month": 12, 35 | "exp_year": 2020 36 | } 37 | ], 38 | "has_more": false, 39 | "total_count": 0, 40 | "url": "/v1/customers/cus_test1/sources" 41 | }, 42 | "subscriptions": { 43 | "object": "list", 44 | "data": [], 45 | "has_more": false, 46 | "total_count": 0, 47 | "url": "/v1/customers/cus_test1/subscriptions" 48 | }, 49 | "tax_exempt": "none", 50 | "tax_ids": { 51 | "object": "list", 52 | "data": [], 53 | "has_more": false, 54 | "total_count": 0, 55 | "url": "/v1/customers/cus_test1/tax_ids" 56 | }, 57 | "tax_info": null, 58 | "tax_info_verification": null 59 | } 60 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_test1_deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cust_test1", 3 | "object": "customer", 4 | "deleted": true 5 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cus_00000000000000", 3 | "object": "customer", 4 | "account_balance": 0, 5 | "address": null, 6 | "balance": 0, 7 | "created": 1567635511, 8 | "currency": "usd", 9 | "default_source": null, 10 | "delinquent": false, 11 | "description": null, 12 | "discount": null, 13 | "email": "test@example.com", 14 | "invoice_prefix": "F351DA7", 15 | "invoice_settings": { 16 | "custom_fields": null, 17 | "default_payment_method": null, 18 | "footer": null 19 | }, 20 | "livemode": false, 21 | "metadata": { 22 | "userid": "user123", 23 | "delete": true 24 | }, 25 | "name": null, 26 | "phone": null, 27 | "preferred_locales": [], 28 | "shipping": null, 29 | "sources": { 30 | "object": "list", 31 | "data": [ 32 | { 33 | "funding": "credit", 34 | "last4": "1234", 35 | "exp_month": 12, 36 | "exp_year": 2020 37 | } 38 | ], 39 | "has_more": false, 40 | "total_count": 0, 41 | "url": "/v1/customers/cus_test1/sources" 42 | }, 43 | "subscriptions": { 44 | "object": "list", 45 | "data": [ 46 | { 47 | "id": "sub_test1", 48 | "object": "subscription", 49 | "application_fee_percent": null, 50 | "billing": "charge_automatically", 51 | "billing_cycle_anchor": 1567634953, 52 | "billing_thresholds": null, 53 | "cancel_at": null, 54 | "cancel_at_period_end": false, 55 | "canceled_at": null, 56 | "collection_method": "charge_automatically", 57 | "created": 1567634953, 58 | "current_period_end": 1570226953, 59 | "current_period_start": 1567634953, 60 | "customer": "cus_test1", 61 | "days_until_due": null, 62 | "default_payment_method": null, 63 | "default_source": null, 64 | "default_tax_rates": [], 65 | "discount": null, 66 | "ended_at": null, 67 | "items": {}, 68 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 69 | "livemode": false, 70 | "metadata": {}, 71 | "pending_setup_intent": null, 72 | "plan": { 73 | "nickname": "test plan", 74 | "interval": "ion", 75 | "amount": 199, 76 | "product": "prod_noarealprod" 77 | }, 78 | "quantity": 1, 79 | "schedule": null, 80 | "start": 1567634953, 81 | "start_date": 1567634953, 82 | "status": "active", 83 | "tax_percent": null, 84 | "trial_end": null, 85 | "trial_start": null 86 | } 87 | ], 88 | "has_more": false, 89 | "total_count": 0, 90 | "url": "/v1/customers/cus_test1/subscriptions" 91 | }, 92 | "tax_exempt": "none", 93 | "tax_ids": { 94 | "object": "list", 95 | "data": [], 96 | "has_more": false, 97 | "total_count": 0, 98 | "url": "/v1/customers/cus_test1/tax_ids" 99 | }, 100 | "tax_info": null, 101 | "tax_info_verification": null 102 | } 103 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_updated_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.updated", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "cus_00000000000000", 13 | "object": "customer", 14 | "account_balance": 0, 15 | "address": null, 16 | "balance": 0, 17 | "created": 1559595009, 18 | "currency": null, 19 | "default_source": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 20 | "delinquent": false, 21 | "description": "process_test", 22 | "discount": null, 23 | "email": "user123@tester.com", 24 | "invoice_prefix": "063A2048", 25 | "invoice_settings": { 26 | "custom_fields": null, 27 | "default_payment_method": null, 28 | "footer": null 29 | }, 30 | "livemode": false, 31 | "metadata": { 32 | "userid": "user123", 33 | "delete": "true" 34 | }, 35 | "name": "Jon Tester", 36 | "phone": null, 37 | "preferred_locales": [], 38 | "shipping": null, 39 | "sources": { 40 | "object": "list", 41 | "data": [ 42 | { 43 | "id": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 44 | "object": "card", 45 | "address_city": null, 46 | "address_country": null, 47 | "address_line1": null, 48 | "address_line1_check": null, 49 | "address_line2": null, 50 | "address_state": null, 51 | "address_zip": null, 52 | "address_zip_check": null, 53 | "brand": "Visa", 54 | "country": "US", 55 | "customer": "cus_FBkbtyY63q01Jd", 56 | "cvc_check": null, 57 | "dynamic_last4": null, 58 | "exp_month": 6, 59 | "exp_year": 2020, 60 | "fingerprint": "eYFLxJ8weKg24R6o", 61 | "funding": "credit", 62 | "last4": "4242", 63 | "metadata": {}, 64 | "name": null, 65 | "tokenization_method": null 66 | } 67 | ], 68 | "has_more": false, 69 | "total_count": 1, 70 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/sources" 71 | }, 72 | "subscriptions": { 73 | "object": "list", 74 | "data": [ 75 | { 76 | "id": "sub_00000000000000" 77 | } 78 | ], 79 | "has_more": false, 80 | "total_count": 0, 81 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/subscriptions" 82 | }, 83 | "tax_exempt": "none", 84 | "tax_ids": { 85 | "object": "list", 86 | "data": [], 87 | "has_more": false, 88 | "total_count": 0, 89 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/tax_ids" 90 | }, 91 | "tax_info": null, 92 | "tax_info_verification": null 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_cust_updated_event_missing_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.updated", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "cus_00000000000000", 13 | "object": "customer", 14 | "account_balance": 0, 15 | "address": null, 16 | "balance": 0, 17 | "created": 1559595009, 18 | "currency": null, 19 | "default_source": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 20 | "delinquent": false, 21 | "description": "process_test", 22 | "discount": null, 23 | "email": "user123@tester.com", 24 | "invoice_prefix": "063A2048", 25 | "invoice_settings": { 26 | "custom_fields": null, 27 | "default_payment_method": null, 28 | "footer": null 29 | }, 30 | "livemode": false, 31 | "metadata": { 32 | "userid": "user123", 33 | "delete": true 34 | }, 35 | "name": null, 36 | "phone": null, 37 | "preferred_locales": [], 38 | "shipping": null, 39 | "sources": { 40 | "object": "list", 41 | "data": [ 42 | { 43 | "id": "card_1EhN6bJNcmPzuWtR4kLlHwXN", 44 | "object": "card", 45 | "address_city": null, 46 | "address_country": null, 47 | "address_line1": null, 48 | "address_line1_check": null, 49 | "address_line2": null, 50 | "address_state": null, 51 | "address_zip": null, 52 | "address_zip_check": null, 53 | "brand": "Visa", 54 | "country": "US", 55 | "customer": "cus_FBkbtyY63q01Jd", 56 | "cvc_check": null, 57 | "dynamic_last4": null, 58 | "exp_month": 6, 59 | "exp_year": 2020, 60 | "fingerprint": "eYFLxJ8weKg24R6o", 61 | "funding": "credit", 62 | "last4": "4242", 63 | "metadata": {}, 64 | "name": null, 65 | "tokenization_method": null 66 | } 67 | ], 68 | "has_more": false, 69 | "total_count": 1, 70 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/sources" 71 | }, 72 | "subscriptions": { 73 | "object": "list", 74 | "data": [], 75 | "has_more": false, 76 | "total_count": 0, 77 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/subscriptions" 78 | }, 79 | "tax_exempt": "none", 80 | "tax_ids": { 81 | "object": "list", 82 | "data": [], 83 | "has_more": false, 84 | "total_count": 0, 85 | "url": "/v1/customers/cus_FBkbtyY63q01Jd/tax_ids" 86 | }, 87 | "tax_info": null, 88 | "tax_info_verification": null 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_customer_deleted_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.deleted", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "cus_123", 13 | "object": "customer", 14 | "account_balance": 0, 15 | "address": { 16 | "city": "Moraga", 17 | "country": "US", 18 | "line1": "123 Jon St", 19 | "line2": "", 20 | "postal_code": "94556", 21 | "state": "CA" 22 | }, 23 | "created": 1557511290, 24 | "currency": "usd", 25 | "default_source": "card_1EYd3nAlBpjODNgi9JzSO5as", 26 | "delinquent": false, 27 | "description": null, 28 | "discount": null, 29 | "email": "jon@tester.com", 30 | "invoice_prefix": "307DDAA9", 31 | "invoice_settings": { 32 | "custom_fields": null, 33 | "default_payment_method": null, 34 | "footer": null 35 | }, 36 | "livemode": false, 37 | "metadata": { 38 | "userid": "user123" 39 | }, 40 | "name": "Jon Tester", 41 | "phone": null, 42 | "preferred_locales": [ 43 | "en" 44 | ], 45 | "shipping": { 46 | "address": { 47 | "city": "Moraga", 48 | "country": "US", 49 | "line1": "123 Jon St", 50 | "line2": "", 51 | "postal_code": "94556", 52 | "state": "CA" 53 | }, 54 | "name": "Jon Tester", 55 | "phone": "" 56 | }, 57 | "sources": { 58 | "object": "list", 59 | "data": [ 60 | { 61 | "id": "card_00000000000000", 62 | "object": "card", 63 | "address_city": null, 64 | "address_country": null, 65 | "address_line1": null, 66 | "address_line1_check": null, 67 | "address_line2": null, 68 | "address_state": null, 69 | "address_zip": null, 70 | "address_zip_check": null, 71 | "brand": "Visa", 72 | "country": "US", 73 | "customer": "cus_00000000000000", 74 | "cvc_check": "pass", 75 | "dynamic_last4": null, 76 | "exp_month": 3, 77 | "exp_year": 2025, 78 | "fingerprint": "iD9ADJSA5KjPlK9O", 79 | "funding": "unknown", 80 | "last4": "1111", 81 | "metadata": { 82 | "userid": "user123" 83 | }, 84 | "name": null, 85 | "tokenization_method": null 86 | } 87 | ], 88 | "has_more": false, 89 | "total_count": 1, 90 | "url": "/v1/customers/cus_F2iTQb6gg5VVIl/sources" 91 | }, 92 | "subscriptions": { 93 | "object": "list", 94 | "data": [ 95 | ], 96 | "has_more": false, 97 | "total_count": 0, 98 | "url": "/v1/customers/cus_F2iTQb6gg5VVIl/subscriptions" 99 | }, 100 | "tax_exempt": "none", 101 | "tax_ids": { 102 | "object": "list", 103 | "data": [ 104 | ], 105 | "has_more": false, 106 | "total_count": 0, 107 | "url": "/v1/customers/cus_F2iTQb6gg5VVIl/tax_ids" 108 | }, 109 | "tax_info": null, 110 | "tax_info_verification": null 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_customer_deleted_event_no_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.deleted", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "cus_123", 13 | "object": "customer", 14 | "account_balance": 0, 15 | "address": { 16 | "city": "Moraga", 17 | "country": "US", 18 | "line1": "123 Jon St", 19 | "line2": "", 20 | "postal_code": "94556", 21 | "state": "CA" 22 | }, 23 | "created": 1557511290, 24 | "currency": "usd", 25 | "default_source": "card_1EYd3nAlBpjODNgi9JzSO5as", 26 | "delinquent": false, 27 | "description": null, 28 | "discount": null, 29 | "email": "jon@tester.com", 30 | "invoice_prefix": "307DDAA9", 31 | "invoice_settings": { 32 | "custom_fields": null, 33 | "default_payment_method": null, 34 | "footer": null 35 | }, 36 | "livemode": false, 37 | "metadata": { 38 | }, 39 | "name": "Jon Tester", 40 | "phone": null, 41 | "preferred_locales": [ 42 | "en" 43 | ], 44 | "shipping": { 45 | "address": { 46 | "city": "Moraga", 47 | "country": "US", 48 | "line1": "123 Jon St", 49 | "line2": "", 50 | "postal_code": "94556", 51 | "state": "CA" 52 | }, 53 | "name": "Jon Tester", 54 | "phone": "" 55 | }, 56 | "sources": { 57 | "object": "list", 58 | "data": [ 59 | { 60 | "id": "card_00000000000000", 61 | "object": "card", 62 | "address_city": null, 63 | "address_country": null, 64 | "address_line1": null, 65 | "address_line1_check": null, 66 | "address_line2": null, 67 | "address_state": null, 68 | "address_zip": null, 69 | "address_zip_check": null, 70 | "brand": "Visa", 71 | "country": "US", 72 | "customer": "cus_00000000000000", 73 | "cvc_check": "pass", 74 | "dynamic_last4": null, 75 | "exp_month": 3, 76 | "exp_year": 2025, 77 | "fingerprint": "iD9ADJSA5KjPlK9O", 78 | "funding": "unknown", 79 | "last4": "1111", 80 | "metadata": { 81 | }, 82 | "name": null, 83 | "tokenization_method": null 84 | } 85 | ], 86 | "has_more": false, 87 | "total_count": 1, 88 | "url": "/v1/customers/cus_F2iTQb6gg5VVIl/sources" 89 | }, 90 | "subscriptions": { 91 | "object": "list", 92 | "data": [ 93 | ], 94 | "has_more": false, 95 | "total_count": 0, 96 | "url": "/v1/customers/cus_F2iTQb6gg5VVIl/subscriptions" 97 | }, 98 | "tax_exempt": "none", 99 | "tax_ids": { 100 | "object": "list", 101 | "data": [ 102 | ], 103 | "has_more": false, 104 | "total_count": 0, 105 | "url": "/v1/customers/cus_F2iTQb6gg5VVIl/tax_ids" 106 | }, 107 | "tax_info": null, 108 | "tax_info_verification": null 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_in_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "in_test1", 3 | "object": "invoice", 4 | "account_country": "US", 5 | "account_name": "Mozilla Corporation", 6 | "amount_due": 1000, 7 | "amount_paid": 1000, 8 | "amount_remaining": 0, 9 | "application_fee_amount": null, 10 | "attempt_count": 1, 11 | "attempted": true, 12 | "auto_advance": false, 13 | "billing": "charge_automatically", 14 | "billing_reason": "subscription_create", 15 | "charge": "ch_test1", 16 | "collection_method": "charge_automatically", 17 | "created": 1555354567, 18 | "currency": "usd", 19 | "custom_fields": null, 20 | "customer": "cus_test1", 21 | "customer_address": null, 22 | "customer_email": "test_fixture@tester.com", 23 | "customer_name": null, 24 | "customer_phone": null, 25 | "customer_shipping": null, 26 | "customer_tax_exempt": "none", 27 | "customer_tax_ids": [], 28 | "default_payment_method": null, 29 | "default_source": null, 30 | "default_tax_rates": [], 31 | "description": null, 32 | "discount": null, 33 | "due_date": null, 34 | "ending_balance": 0, 35 | "footer": null, 36 | "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_ZPQW1BXsP9LEjkKf4oXxhNMeeS", 37 | "invoice_pdf": "https://pay.stripe.com/invoice/invst_ZPQW1BXsP9LEjkKf4oXxhNMeeS/pdf", 38 | "lines": { 39 | "data": [ 40 | { 41 | "id": "sli_2182b279422cba", 42 | "object": "line_item", 43 | "amount": 1000, 44 | "currency": "usd", 45 | "description": "1 Moz-Sub × Moz_Sub (at $10.00 / month)", 46 | "discountable": true, 47 | "livemode": false, 48 | "metadata": {}, 49 | "period": { 50 | "end": 1572905353, 51 | "start": 1570226953 52 | }, 53 | "plan": {}, 54 | "proration": false, 55 | "quantity": 1, 56 | "subscription": "sub_FkbsOxUMt9qxhO", 57 | "subscription_item": "si_FkbsnTorDMAHh3", 58 | "tax_amounts": [], 59 | "tax_rates": [], 60 | "type": "subscription" 61 | } 62 | ], 63 | "has_more": false, 64 | "object": "list", 65 | "url": "/v1/invoices/in_test1/lines" 66 | }, 67 | "livemode": false, 68 | "metadata": {}, 69 | "next_payment_attempt": null, 70 | "number": "3B74E3D0-0001", 71 | "paid": true, 72 | "payment_intent": "pi_1EPZyNJNcmPzuWtR9U3SsJ4w", 73 | "period_end": 1555354567, 74 | "period_start": 1555354567, 75 | "post_payment_credit_notes_amount": 0, 76 | "pre_payment_credit_notes_amount": 0, 77 | "receipt_number": null, 78 | "starting_balance": 0, 79 | "statement_descriptor": null, 80 | "status": "paid", 81 | "status_transitions": { 82 | "finalized_at": 1555354567, 83 | "marked_uncollectible_at": null, 84 | "paid_at": 1555354568, 85 | "voided_at": null 86 | }, 87 | "subscription": "sub_test2", 88 | "subtotal": 1000, 89 | "tax": null, 90 | "tax_percent": null, 91 | "total": 1000, 92 | "total_tax_amounts": [], 93 | "webhooks_delivered_at": 1555354569 94 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_plan_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "plan_test1", 3 | "object": "plan", 4 | "active": true, 5 | "aggregate_usage": null, 6 | "amount": 100, 7 | "amount_decimal": "100", 8 | "billing_scheme": "per_unit", 9 | "created": 1561581476, 10 | "currency": "usd", 11 | "interval": "month", 12 | "interval_count": 1, 13 | "livemode": false, 14 | "metadata": {}, 15 | "nickname": "Free", 16 | "product": "prod_test1", 17 | "tiers": null, 18 | "tiers_mode": null, 19 | "transform_usage": null, 20 | "trial_period_days": null, 21 | "usage_type": "licensed" 22 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_plan_test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "plan_test2", 3 | "object": "plan", 4 | "active": true, 5 | "aggregate_usage": null, 6 | "amount": 1000, 7 | "amount_decimal": "1000", 8 | "billing_scheme": "per_unit", 9 | "created": 1561581476, 10 | "currency": "usd", 11 | "interval": "year", 12 | "interval_count": 1, 13 | "livemode": false, 14 | "metadata": {}, 15 | "nickname": "Free", 16 | "product": "prod_test1", 17 | "tiers": null, 18 | "tiers_mode": null, 19 | "transform_usage": null, 20 | "trial_period_days": null, 21 | "usage_type": "licensed" 22 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_plan_test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "plan_test3", 3 | "object": "plan", 4 | "active": true, 5 | "aggregate_usage": null, 6 | "amount": 1000, 7 | "amount_decimal": "1000", 8 | "billing_scheme": "per_unit", 9 | "created": 1561581476, 10 | "currency": "usd", 11 | "interval": "year", 12 | "interval_count": 1, 13 | "livemode": false, 14 | "metadata": {}, 15 | "nickname": "Free", 16 | "product": "prod_test2", 17 | "tiers": null, 18 | "tiers_mode": null, 19 | "transform_usage": null, 20 | "trial_period_days": null, 21 | "usage_type": "licensed" 22 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_previous_plan1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "plan_123", 3 | "amount": 499, 4 | "amount_decimal": "499", 5 | "created": 1573274099, 6 | "nickname": "Previous Product", 7 | "product": "prod_test1", 8 | "interval": "month" 9 | } 10 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_prod_bad_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "prod_test1", 3 | "object": "product", 4 | "active": true, 5 | "attributes": [], 6 | "caption": null, 7 | "created": 1567100773, 8 | "deactivate_on": [], 9 | "description": null, 10 | "images": [], 11 | "livemode": false, 12 | "metadata": {}, 13 | "name": "Project Guardian", 14 | "package_dimensions": null, 15 | "shippable": null, 16 | "statement_descriptor": "Firefox Guardian", 17 | "type": "service", 18 | "unit_label": null, 19 | "updated": 1567100794, 20 | "url": null 21 | } 22 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_prod_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "prod_test1", 3 | "object": "product", 4 | "active": true, 5 | "attributes": [], 6 | "caption": null, 7 | "created": 1567100773, 8 | "deactivate_on": [], 9 | "description": null, 10 | "images": [], 11 | "livemode": false, 12 | "metadata": { 13 | "productSetOrder": 1 14 | }, 15 | "name": "Project Guardian", 16 | "package_dimensions": null, 17 | "shippable": null, 18 | "statement_descriptor": "Firefox Guardian", 19 | "type": "service", 20 | "unit_label": null, 21 | "updated": 1567100794, 22 | "url": null 23 | } 24 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_prod_test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "prod_test2", 3 | "object": "product", 4 | "active": true, 5 | "attributes": [], 6 | "caption": null, 7 | "created": 1567100773, 8 | "deactivate_on": [], 9 | "description": null, 10 | "images": [], 11 | "livemode": false, 12 | "metadata": { 13 | "productSetOrder": 2 14 | }, 15 | "name": "Firefox VPN", 16 | "package_dimensions": null, 17 | "shippable": null, 18 | "statement_descriptor": "Firefox VPN", 19 | "type": "service", 20 | "unit_label": null, 21 | "updated": 1567100794, 22 | "url": null 23 | } 24 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_source_expiring_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.source.expiring", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "card_1EaRqsJNcmPzuWtRr0ro1vhX", 13 | "object": "card", 14 | "address_city": "Katy", 15 | "address_country": "USA", 16 | "address_line1": "123 Main", 17 | "address_line1_check": "pass", 18 | "address_line2": null, 19 | "address_state": "TX", 20 | "address_zip": "77494", 21 | "address_zip_check": "pass", 22 | "brand": "Visa", 23 | "country": "US", 24 | "customer": "cus_00000000000000", 25 | "cvc_check": null, 26 | "dynamic_last4": null, 27 | "exp_month": 5, 28 | "exp_year": 2019, 29 | "fingerprint": "eYFLxJ8weKg24R6o", 30 | "funding": "credit", 31 | "last4": "4242", 32 | "metadata": { 33 | }, 34 | "name": "Bob Jones", 35 | "tokenization_method": null 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_created_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.subscription.created", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "sub_00000000000000", 13 | "object": "subscription", 14 | "application_fee_percent": null, 15 | "billing": "charge_automatically", 16 | "billing_cycle_anchor": 1519435009, 17 | "billing_thresholds": null, 18 | "cancel_at": null, 19 | "cancel_at_period_end": false, 20 | "canceled_at": 1519680008, 21 | "created": 1519435009, 22 | "current_period_end": 1521854209, 23 | "current_period_start": 1519435009, 24 | "customer": "cus_00000000000000", 25 | "days_until_due": null, 26 | "default_payment_method": null, 27 | "default_source": null, 28 | "default_tax_rates": [ 29 | ], 30 | "discount": null, 31 | "ended_at": 1519680008, 32 | "items": { 33 | "object": "list", 34 | "data": [ 35 | { 36 | "id": "si_00000000000000", 37 | "object": "subscription_item", 38 | "billing_thresholds": null, 39 | "created": 1519435009, 40 | "metadata": { 41 | }, 42 | "plan": { 43 | "id": "subhub-plan-api_00000000000000", 44 | "object": "plan", 45 | "active": true, 46 | "aggregate_usage": null, 47 | "amount": 500, 48 | "billing_scheme": "per_unit", 49 | "created": 1519363457, 50 | "currency": "usd", 51 | "interval": "month", 52 | "interval_count": 1, 53 | "livemode": false, 54 | "metadata": { 55 | }, 56 | "nickname": "subhub", 57 | "product": "prod_00000000000000", 58 | "tiers": null, 59 | "tiers_mode": null, 60 | "transform_usage": null, 61 | "trial_period_days": null, 62 | "usage_type": "licensed" 63 | }, 64 | "quantity": 1, 65 | "subscription": "sub_00000000000000" 66 | } 67 | ], 68 | "has_more": false, 69 | "total_count": 1, 70 | "url": "/v1/subscription_items?subscription=sub_CNcuSboLSixkQV" 71 | }, 72 | "latest_invoice": "in_test123", 73 | "livemode": false, 74 | "metadata": { 75 | "userid": "tester123" 76 | }, 77 | "plan": { 78 | "id": "subhub-plan-api_00000000000000", 79 | "object": "plan", 80 | "active": true, 81 | "aggregate_usage": null, 82 | "amount": 500, 83 | "billing_scheme": "per_unit", 84 | "created": 1519363457, 85 | "currency": "usd", 86 | "interval": "month", 87 | "interval_count": 1, 88 | "livemode": false, 89 | "metadata": { 90 | }, 91 | "nickname": "subhub", 92 | "product": "prod_00000000000000", 93 | "tiers": null, 94 | "tiers_mode": null, 95 | "transform_usage": null, 96 | "trial_period_days": null, 97 | "usage_type": "licensed" 98 | }, 99 | "quantity": 1, 100 | "schedule": null, 101 | "start": 1519435009, 102 | "status": "active", 103 | "tax_percent": null, 104 | "trial_end": null, 105 | "trial_start": null 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_deleted_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.subscription.deleted", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "sub_00000000000000", 13 | "object": "subscription", 14 | "application_fee_percent": null, 15 | "billing": "charge_automatically", 16 | "billing_cycle_anchor": 1519435009, 17 | "billing_thresholds": null, 18 | "cancel_at": null, 19 | "cancel_at_period_end": false, 20 | "canceled_at": 1519680008, 21 | "created": 1519435009, 22 | "current_period_end": 1521854209, 23 | "current_period_start": 1519435009, 24 | "customer": "cus_00000000000000", 25 | "days_until_due": null, 26 | "default_payment_method": null, 27 | "default_source": null, 28 | "default_tax_rates": [ 29 | ], 30 | "discount": null, 31 | "ended_at": 1557780556, 32 | "items": { 33 | "object": "list", 34 | "data": [ 35 | { 36 | "id": "si_00000000000000", 37 | "object": "subscription_item", 38 | "billing_thresholds": null, 39 | "created": 1519435009, 40 | "metadata": { 41 | }, 42 | "plan": { 43 | "id": "jollybilling-plan-api_00000000000000", 44 | "object": "plan", 45 | "active": true, 46 | "aggregate_usage": null, 47 | "amount": 500, 48 | "billing_scheme": "per_unit", 49 | "created": 1519363457, 50 | "currency": "usd", 51 | "interval": "month", 52 | "interval_count": 1, 53 | "livemode": false, 54 | "metadata": { 55 | }, 56 | "nickname": "jollybilling", 57 | "product": "prod_00000000000000", 58 | "tiers": null, 59 | "tiers_mode": null, 60 | "transform_usage": null, 61 | "trial_period_days": null, 62 | "usage_type": "licensed" 63 | }, 64 | "quantity": 1, 65 | "subscription": "sub_00000000000000" 66 | } 67 | ], 68 | "has_more": false, 69 | "total_count": 1, 70 | "url": "/v1/subscription_items?subscription=sub_CNcuSboLSixkQV" 71 | }, 72 | "latest_invoice": null, 73 | "livemode": false, 74 | "metadata": { 75 | "userid": "tester123" 76 | }, 77 | "plan": { 78 | "id": "jollybilling-plan-api_00000000000000", 79 | "object": "plan", 80 | "active": true, 81 | "aggregate_usage": null, 82 | "amount": 500, 83 | "billing_scheme": "per_unit", 84 | "created": 1519363457, 85 | "currency": "usd", 86 | "interval": "month", 87 | "interval_count": 1, 88 | "livemode": false, 89 | "metadata": { 90 | }, 91 | "nickname": "jollybilling", 92 | "product": "prod_00000000000000", 93 | "tiers": null, 94 | "tiers_mode": null, 95 | "transform_usage": null, 96 | "trial_period_days": null, 97 | "usage_type": "licensed" 98 | }, 99 | "quantity": 1, 100 | "schedule": null, 101 | "start": 1519435009, 102 | "status": "canceled", 103 | "tax_percent": null, 104 | "trial_end": null, 105 | "trial_start": null 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test1", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": "cus_test1", 16 | "days_until_due": null, 17 | "default_payment_method": null, 18 | "default_source": null, 19 | "default_tax_rates": [], 20 | "discount": null, 21 | "ended_at": null, 22 | "items": {}, 23 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 24 | "livemode": false, 25 | "metadata": {}, 26 | "pending_setup_intent": null, 27 | "plan": {}, 28 | "quantity": 1, 29 | "schedule": null, 30 | "start": 1567634953, 31 | "start_date": 1567634953, 32 | "status": "active", 33 | "tax_percent": null, 34 | "trial_end": null, 35 | "trial_start": null 36 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test2", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": "cus_FkbsWZ82NIFGmc", 16 | "days_until_due": null, 17 | "default_payment_method": null, 18 | "default_source": null, 19 | "default_tax_rates": [], 20 | "discount": null, 21 | "ended_at": null, 22 | "items": {}, 23 | "latest_invoice": "in_test1", 24 | "livemode": false, 25 | "metadata": {}, 26 | "pending_setup_intent": null, 27 | "plan": {}, 28 | "quantity": 1, 29 | "schedule": null, 30 | "start": 1567634953, 31 | "start_date": 1567634953, 32 | "status": "incomplete", 33 | "tax_percent": null, 34 | "trial_end": null, 35 | "trial_start": null 36 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test2", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": "cus_test1", 16 | "days_until_due": null, 17 | "default_payment_method": null, 18 | "default_source": null, 19 | "default_tax_rates": [], 20 | "discount": null, 21 | "ended_at": null, 22 | "items": {}, 23 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 24 | "livemode": false, 25 | "metadata": {}, 26 | "pending_setup_intent": null, 27 | "plan": { 28 | "nickname": "test plan", 29 | "interval": "ion", 30 | "amount": 199, 31 | "product": "prod_noarealprod" 32 | }, 33 | "quantity": 1, 34 | "schedule": null, 35 | "start": 1567634953, 36 | "start_date": 1567634953, 37 | "status": "active", 38 | "tax_percent": null, 39 | "trial_end": null, 40 | "trial_start": null 41 | } 42 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test4", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": "cus_test1", 16 | "days_until_due": null, 17 | "default_payment_method": null, 18 | "default_source": null, 19 | "default_tax_rates": [], 20 | "discount": null, 21 | "ended_at": null, 22 | "items": {}, 23 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 24 | "livemode": false, 25 | "metadata": {}, 26 | "pending_setup_intent": null, 27 | "plan": { 28 | "nickname": "test plan", 29 | "interval": "month", 30 | "amount": 199, 31 | "product": "prod_noarealprod" 32 | }, 33 | "quantity": 1, 34 | "schedule": null, 35 | "start": 1567634953, 36 | "start_date": 1567634953, 37 | "status": "active", 38 | "tax_percent": null, 39 | "trial_end": null, 40 | "trial_start": null 41 | } 42 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test5.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test5", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": "cus_test1", 16 | "days_until_due": null, 17 | "default_payment_method": null, 18 | "default_source": null, 19 | "default_tax_rates": [], 20 | "discount": null, 21 | "ended_at": null, 22 | "items": {}, 23 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 24 | "livemode": false, 25 | "metadata": {}, 26 | "pending_setup_intent": null, 27 | "plan": { 28 | "nickname": "test plan", 29 | "interval": "day", 30 | "amount": 199, 31 | "product": "prod_noarealprod" 32 | }, 33 | "quantity": 1, 34 | "schedule": null, 35 | "start": 1567634953, 36 | "start_date": 1567634953, 37 | "status": "active", 38 | "tax_percent": null, 39 | "trial_end": null, 40 | "trial_start": null 41 | } 42 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test6.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test6", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": { 16 | "id": "cus_test1", 17 | "object": "customer", 18 | "account_balance": 0, 19 | "address": null, 20 | "balance": 0, 21 | "created": 1567635511, 22 | "currency": "usd", 23 | "default_source": null, 24 | "delinquent": false, 25 | "description": null, 26 | "discount": null, 27 | "email": "test@example.com", 28 | "invoice_prefix": "F351DA7", 29 | "invoice_settings": { 30 | "custom_fields": null, 31 | "default_payment_method": null, 32 | "footer": null 33 | }, 34 | "livemode": false, 35 | "metadata": { 36 | "userid": "user123" 37 | }, 38 | "name": "Bob Tester", 39 | "phone": null, 40 | "preferred_locales": [], 41 | "shipping": null, 42 | "sources": { 43 | "object": "list", 44 | "data": [ 45 | { 46 | "funding": "credit", 47 | "last4": "1234", 48 | "exp_month": 12, 49 | "exp_year": 2020 50 | } 51 | ], 52 | "has_more": false, 53 | "total_count": 0, 54 | "url": "/v1/customers/cus_test1/sources" 55 | }, 56 | "subscriptions": { 57 | "object": "list", 58 | "data": [], 59 | "has_more": false, 60 | "total_count": 0, 61 | "url": "/v1/customers/cus_test1/subscriptions" 62 | }, 63 | "tax_exempt": "none", 64 | "tax_ids": { 65 | "object": "list", 66 | "data": [], 67 | "has_more": false, 68 | "total_count": 0, 69 | "url": "/v1/customers/cus_test1/tax_ids" 70 | }, 71 | "tax_info": null, 72 | "tax_info_verification": null 73 | }, 74 | "days_until_due": null, 75 | "default_payment_method": null, 76 | "default_source": null, 77 | "default_tax_rates": [], 78 | "discount": null, 79 | "ended_at": null, 80 | "items": {}, 81 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 82 | "livemode": false, 83 | "metadata": {}, 84 | "pending_setup_intent": null, 85 | "plan": { 86 | "id": "plan_test1", 87 | "object": "plan", 88 | "active": true, 89 | "created": 1561581476, 90 | "interval": "month", 91 | "nickname": "Free", 92 | "product": "prod_test1" 93 | }, 94 | "quantity": 1, 95 | "schedule": null, 96 | "start": 1567634953, 97 | "start_date": 1567634953, 98 | "status": "active", 99 | "tax_percent": null, 100 | "trial_end": null, 101 | "trial_start": null 102 | } 103 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test7.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test7", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": { 16 | "id": "cus_test1", 17 | "object": "customer", 18 | "account_balance": 0, 19 | "address": null, 20 | "balance": 0, 21 | "created": 1567635511, 22 | "currency": "usd", 23 | "default_source": null, 24 | "delinquent": false, 25 | "description": null, 26 | "discount": null, 27 | "email": "test@example.com", 28 | "invoice_prefix": "F351DA7", 29 | "invoice_settings": { 30 | "custom_fields": null, 31 | "default_payment_method": null, 32 | "footer": null 33 | }, 34 | "livemode": false, 35 | "metadata": { 36 | "userid": "user123" 37 | }, 38 | "name": "Bob Tester", 39 | "phone": null, 40 | "preferred_locales": [], 41 | "shipping": null, 42 | "sources": { 43 | "object": "list", 44 | "data": [ 45 | { 46 | "funding": "credit", 47 | "last4": "1234", 48 | "exp_month": 12, 49 | "exp_year": 2020 50 | } 51 | ], 52 | "has_more": false, 53 | "total_count": 0, 54 | "url": "/v1/customers/cus_test1/sources" 55 | }, 56 | "subscriptions": { 57 | "object": "list", 58 | "data": [], 59 | "has_more": false, 60 | "total_count": 0, 61 | "url": "/v1/customers/cus_test1/subscriptions" 62 | }, 63 | "tax_exempt": "none", 64 | "tax_ids": { 65 | "object": "list", 66 | "data": [], 67 | "has_more": false, 68 | "total_count": 0, 69 | "url": "/v1/customers/cus_test1/tax_ids" 70 | }, 71 | "tax_info": null, 72 | "tax_info_verification": null 73 | }, 74 | "days_until_due": null, 75 | "default_payment_method": null, 76 | "default_source": null, 77 | "default_tax_rates": [], 78 | "discount": null, 79 | "ended_at": null, 80 | "items": {}, 81 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 82 | "livemode": false, 83 | "metadata": {}, 84 | "pending_setup_intent": null, 85 | "plan": { 86 | "id": "plan_test1", 87 | "object": "plan", 88 | "active": true, 89 | "created": 1561581476, 90 | "interval": "month", 91 | "nickname": "Free", 92 | "product": "prod_test1" 93 | }, 94 | "quantity": 1, 95 | "schedule": null, 96 | "start": 1567634953, 97 | "start_date": 1567634953, 98 | "status": "active", 99 | "tax_percent": null, 100 | "trial_end": null, 101 | "trial_start": null 102 | } 103 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test8.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test7", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": { 16 | "id": "cus_test1", 17 | "object": "customer", 18 | "account_balance": 0, 19 | "address": null, 20 | "balance": 0, 21 | "created": 1567635511, 22 | "currency": "usd", 23 | "default_source": null, 24 | "delinquent": false, 25 | "description": null, 26 | "discount": null, 27 | "email": "test@example.com", 28 | "invoice_prefix": "F351DA7", 29 | "invoice_settings": { 30 | "custom_fields": null, 31 | "default_payment_method": null, 32 | "footer": null 33 | }, 34 | "livemode": false, 35 | "metadata": { 36 | "userid": "user123" 37 | }, 38 | "name": "Bob Tester", 39 | "phone": null, 40 | "preferred_locales": [], 41 | "shipping": null, 42 | "sources": { 43 | "object": "list", 44 | "data": [ 45 | { 46 | "funding": "credit", 47 | "last4": "1234", 48 | "exp_month": 12, 49 | "exp_year": 2020 50 | } 51 | ], 52 | "has_more": false, 53 | "total_count": 0, 54 | "url": "/v1/customers/cus_test1/sources" 55 | }, 56 | "subscriptions": { 57 | "object": "list", 58 | "data": [], 59 | "has_more": false, 60 | "total_count": 0, 61 | "url": "/v1/customers/cus_test1/subscriptions" 62 | }, 63 | "tax_exempt": "none", 64 | "tax_ids": { 65 | "object": "list", 66 | "data": [], 67 | "has_more": false, 68 | "total_count": 0, 69 | "url": "/v1/customers/cus_test1/tax_ids" 70 | }, 71 | "tax_info": null, 72 | "tax_info_verification": null 73 | }, 74 | "days_until_due": null, 75 | "default_payment_method": null, 76 | "default_source": null, 77 | "default_tax_rates": [], 78 | "discount": null, 79 | "ended_at": null, 80 | "items": {}, 81 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 82 | "livemode": false, 83 | "metadata": {}, 84 | "pending_setup_intent": null, 85 | "plan": { 86 | "id": "plan_test1", 87 | "object": "plan", 88 | "active": true, 89 | "created": 1561581476, 90 | "interval": "week", 91 | "nickname": "Free", 92 | "product": "prod_test1" 93 | }, 94 | "quantity": 1, 95 | "schedule": null, 96 | "start": 1567634953, 97 | "start_date": 1567634953, 98 | "status": "active", 99 | "tax_percent": null, 100 | "trial_end": null, 101 | "trial_start": null 102 | } 103 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_test_expanded.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test1", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": { 16 | "id": "cus_test1", 17 | "object": "customer", 18 | "account_balance": 0, 19 | "address": null, 20 | "balance": 0, 21 | "created": 1567635511, 22 | "currency": "usd", 23 | "default_source": null, 24 | "delinquent": false, 25 | "description": null, 26 | "discount": null, 27 | "email": "test@example.com", 28 | "invoice_prefix": "F351DA7", 29 | "invoice_settings": { 30 | "custom_fields": null, 31 | "default_payment_method": null, 32 | "footer": null 33 | }, 34 | "livemode": false, 35 | "metadata": { 36 | "userid": "user123" 37 | }, 38 | "name": "Bob Tester", 39 | "phone": null, 40 | "preferred_locales": [], 41 | "shipping": null, 42 | "sources": { 43 | "object": "list", 44 | "data": [ 45 | { 46 | "funding": "credit", 47 | "last4": "1234", 48 | "exp_month": 12, 49 | "exp_year": 2020 50 | } 51 | ], 52 | "has_more": false, 53 | "total_count": 0, 54 | "url": "/v1/customers/cus_test1/sources" 55 | }, 56 | "subscriptions": { 57 | "object": "list", 58 | "data": [], 59 | "has_more": false, 60 | "total_count": 0, 61 | "url": "/v1/customers/cus_test1/subscriptions" 62 | }, 63 | "tax_exempt": "none", 64 | "tax_ids": { 65 | "object": "list", 66 | "data": [], 67 | "has_more": false, 68 | "total_count": 0, 69 | "url": "/v1/customers/cus_test1/tax_ids" 70 | }, 71 | "tax_info": null, 72 | "tax_info_verification": null 73 | }, 74 | "days_until_due": null, 75 | "default_payment_method": null, 76 | "default_source": null, 77 | "default_tax_rates": [], 78 | "discount": null, 79 | "ended_at": null, 80 | "items": {}, 81 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 82 | "livemode": false, 83 | "metadata": {}, 84 | "pending_setup_intent": null, 85 | "plan": { 86 | "id": "plan_test1", 87 | "object": "plan", 88 | "active": true, 89 | "aggregate_usage": null, 90 | "amount": 100, 91 | "amount_decimal": "100", 92 | "billing_scheme": "per_unit", 93 | "created": 1561581476, 94 | "currency": "usd", 95 | "interval": "month", 96 | "interval_count": 1, 97 | "livemode": false, 98 | "metadata": {}, 99 | "nickname": "Free", 100 | "product": "prod_test1", 101 | "tiers": null, 102 | "tiers_mode": null, 103 | "transform_usage": null, 104 | "trial_period_days": null, 105 | "usage_type": "licensed" 106 | }, 107 | "quantity": 1, 108 | "schedule": null, 109 | "start": 1567634953, 110 | "start_date": 1567634953, 111 | "status": "active", 112 | "tax_percent": null, 113 | "trial_end": null, 114 | "trial_start": null 115 | } 116 | -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_updated_event_cancel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_1FXDCFJNcmPzuWtRrogbWpRZ", 3 | "object": "event", 4 | "api_version": "2019-03-14", 5 | "created": 1571949975, 6 | "data": { 7 | "object": { 8 | "id": "sub_FCUzkHmNY3Mbj1", 9 | "object": "subscription", 10 | "application_fee_percent": null, 11 | "billing": "charge_automatically", 12 | "billing_cycle_anchor": 1559767571, 13 | "billing_thresholds": null, 14 | "cancel_at": null, 15 | "cancel_at_period_end": true, 16 | "canceled_at": null, 17 | "collection_method": "charge_automatically", 18 | "created": 1559767571, 19 | "current_period_end": 1572036371, 20 | "current_period_start": 1571949971, 21 | "customer": "cus_FCUzOhOp9iutWa", 22 | "days_until_due": null, 23 | "default_payment_method": null, 24 | "default_source": null, 25 | "default_tax_rates": [], 26 | "discount": null, 27 | "ended_at": null, 28 | "items": { 29 | "object": "list", 30 | "data": [ 31 | { 32 | "id": "si_FCUzTOlS7Ltzbr", 33 | "object": "subscription_item", 34 | "billing_thresholds": null, 35 | "created": 1559767572, 36 | "metadata": {}, 37 | "plan": { 38 | "id": "plan_F4G9jB3x5i6Dpj", 39 | "object": "plan", 40 | "active": true, 41 | "aggregate_usage": null, 42 | "amount": 100, 43 | "amount_decimal": "100", 44 | "billing_scheme": "per_unit", 45 | "created": 1557867224, 46 | "currency": "usd", 47 | "interval": "day", 48 | "interval_count": 1, 49 | "livemode": false, 50 | "metadata": {}, 51 | "nickname": "Daily Subscription", 52 | "product": "prod_EtMczoDntN9YEa", 53 | "tiers": null, 54 | "tiers_mode": null, 55 | "transform_usage": null, 56 | "trial_period_days": null, 57 | "usage_type": "licensed" 58 | }, 59 | "quantity": 1, 60 | "subscription": "sub_FCUzkHmNY3Mbj1", 61 | "tax_rates": [] 62 | } 63 | ], 64 | "has_more": false, 65 | "total_count": 1, 66 | "url": "/v1/subscription_items?subscription=sub_FCUzkHmNY3Mbj1" 67 | }, 68 | "latest_invoice": "in_1FXDCFJNcmPzuWtRT9U5Xvcz", 69 | "livemode": false, 70 | "metadata": {}, 71 | "next_pending_invoice_item_invoice": null, 72 | "pending_invoice_item_interval": null, 73 | "pending_setup_intent": null, 74 | "plan": { 75 | "id": "plan_F4G9jB3x5i6Dpj", 76 | "object": "plan", 77 | "active": true, 78 | "aggregate_usage": null, 79 | "amount": 100, 80 | "amount_decimal": "100", 81 | "billing_scheme": "per_unit", 82 | "created": 1557867224, 83 | "currency": "usd", 84 | "interval": "day", 85 | "interval_count": 1, 86 | "livemode": false, 87 | "metadata": {}, 88 | "nickname": "Daily Subscription", 89 | "product": "prod_EtMczoDntN9YEa", 90 | "tiers": null, 91 | "tiers_mode": null, 92 | "transform_usage": null, 93 | "trial_period_days": null, 94 | "usage_type": "licensed" 95 | }, 96 | "quantity": 1, 97 | "schedule": null, 98 | "start": 1559767571, 99 | "start_date": 1559767571, 100 | "status": "active", 101 | "tax_percent": null, 102 | "trial_end": null, 103 | "trial_start": null 104 | }, 105 | "previous_attributes": { 106 | "cancel_at_period_end": false 107 | } 108 | }, 109 | "livemode": false, 110 | "pending_webhooks": 1, 111 | "request": { 112 | "id": null, 113 | "idempotency_key": null 114 | }, 115 | "type": "customer.subscription.updated" 116 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_updated_event_no_trigger.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_1FXDCFJNcmPzuWtRrogbWpRZ", 3 | "object": "event", 4 | "api_version": "2019-03-14", 5 | "created": 1571949975, 6 | "data": { 7 | "object": { 8 | "id": "sub_FCUzkHmNY3Mbj1", 9 | "object": "subscription", 10 | "application_fee_percent": null, 11 | "billing": "charge_automatically", 12 | "billing_cycle_anchor": 1559767571, 13 | "billing_thresholds": null, 14 | "cancel_at": null, 15 | "cancel_at_period_end": false, 16 | "canceled_at": null, 17 | "collection_method": "charge_automatically", 18 | "created": 1559767571, 19 | "current_period_end": 1572036371, 20 | "current_period_start": 1571949971, 21 | "customer": "cus_FCUzOhOp9iutWa", 22 | "days_until_due": null, 23 | "default_payment_method": null, 24 | "default_source": null, 25 | "default_tax_rates": [], 26 | "discount": null, 27 | "ended_at": null, 28 | "items": { 29 | "object": "list", 30 | "data": [ 31 | { 32 | "id": "si_FCUzTOlS7Ltzbr", 33 | "object": "subscription_item", 34 | "billing_thresholds": null, 35 | "created": 1559767572, 36 | "metadata": {}, 37 | "plan": { 38 | "id": "plan_F4G9jB3x5i6Dpj", 39 | "object": "plan", 40 | "active": true, 41 | "aggregate_usage": null, 42 | "amount": 100, 43 | "amount_decimal": "100", 44 | "billing_scheme": "per_unit", 45 | "created": 1557867224, 46 | "currency": "usd", 47 | "interval": "day", 48 | "interval_count": 1, 49 | "livemode": false, 50 | "metadata": {}, 51 | "nickname": "Daily Subscription", 52 | "product": "prod_EtMczoDntN9YEa", 53 | "tiers": null, 54 | "tiers_mode": null, 55 | "transform_usage": null, 56 | "trial_period_days": null, 57 | "usage_type": "licensed" 58 | }, 59 | "quantity": 1, 60 | "subscription": "sub_FCUzkHmNY3Mbj1", 61 | "tax_rates": [] 62 | } 63 | ], 64 | "has_more": false, 65 | "total_count": 1, 66 | "url": "/v1/subscription_items?subscription=sub_FCUzkHmNY3Mbj1" 67 | }, 68 | "latest_invoice": "in_1FXDCFJNcmPzuWtRT9U5Xvcz", 69 | "livemode": false, 70 | "metadata": {}, 71 | "next_pending_invoice_item_invoice": null, 72 | "pending_invoice_item_interval": null, 73 | "pending_setup_intent": null, 74 | "plan": { 75 | "id": "plan_F4G9jB3x5i6Dpj", 76 | "object": "plan", 77 | "active": true, 78 | "aggregate_usage": null, 79 | "amount": 100, 80 | "amount_decimal": "100", 81 | "billing_scheme": "per_unit", 82 | "created": 1557867224, 83 | "currency": "usd", 84 | "interval": "day", 85 | "interval_count": 1, 86 | "livemode": false, 87 | "metadata": {}, 88 | "nickname": "Daily Subscription", 89 | "product": "prod_EtMczoDntN9YEa", 90 | "tiers": null, 91 | "tiers_mode": null, 92 | "transform_usage": null, 93 | "trial_period_days": null, 94 | "usage_type": "licensed" 95 | }, 96 | "quantity": 1, 97 | "schedule": null, 98 | "start": 1559767571, 99 | "start_date": 1559767571, 100 | "status": "trial", 101 | "tax_percent": null, 102 | "trial_end": null, 103 | "trial_start": null 104 | }, 105 | "previous_attributes": { 106 | "status": "cancel" 107 | } 108 | }, 109 | "livemode": false, 110 | "pending_webhooks": 1, 111 | "request": { 112 | "id": null, 113 | "idempotency_key": null 114 | }, 115 | "type": "customer.subscription.updated" 116 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/stripe_sub_updated_event_reactivate.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_1FXDCFJNcmPzuWtRrogbWpRZ", 3 | "object": "event", 4 | "api_version": "2019-03-14", 5 | "created": 1571949975, 6 | "data": { 7 | "object": { 8 | "id": "sub_FCUzkHmNY3Mbj1", 9 | "object": "subscription", 10 | "application_fee_percent": null, 11 | "billing": "charge_automatically", 12 | "billing_cycle_anchor": 1559767571, 13 | "billing_thresholds": null, 14 | "cancel_at": null, 15 | "cancel_at_period_end": false, 16 | "canceled_at": null, 17 | "collection_method": "charge_automatically", 18 | "created": 1559767571, 19 | "current_period_end": 1572036371, 20 | "current_period_start": 1571949971, 21 | "customer": "cus_FCUzOhOp9iutWa", 22 | "days_until_due": null, 23 | "default_payment_method": null, 24 | "default_source": null, 25 | "default_tax_rates": [], 26 | "discount": null, 27 | "ended_at": null, 28 | "items": { 29 | "object": "list", 30 | "data": [ 31 | { 32 | "id": "si_FCUzTOlS7Ltzbr", 33 | "object": "subscription_item", 34 | "billing_thresholds": null, 35 | "created": 1559767572, 36 | "metadata": {}, 37 | "plan": { 38 | "id": "plan_F4G9jB3x5i6Dpj", 39 | "object": "plan", 40 | "active": true, 41 | "aggregate_usage": null, 42 | "amount": 100, 43 | "amount_decimal": "100", 44 | "billing_scheme": "per_unit", 45 | "created": 1557867224, 46 | "currency": "usd", 47 | "interval": "day", 48 | "interval_count": 1, 49 | "livemode": false, 50 | "metadata": {}, 51 | "nickname": "Daily Subscription", 52 | "product": "prod_EtMczoDntN9YEa", 53 | "tiers": null, 54 | "tiers_mode": null, 55 | "transform_usage": null, 56 | "trial_period_days": null, 57 | "usage_type": "licensed" 58 | }, 59 | "quantity": 1, 60 | "subscription": "sub_FCUzkHmNY3Mbj1", 61 | "tax_rates": [] 62 | } 63 | ], 64 | "has_more": false, 65 | "total_count": 1, 66 | "url": "/v1/subscription_items?subscription=sub_FCUzkHmNY3Mbj1" 67 | }, 68 | "latest_invoice": "in_1FXDCFJNcmPzuWtRT9U5Xvcz", 69 | "livemode": false, 70 | "metadata": {}, 71 | "next_pending_invoice_item_invoice": null, 72 | "pending_invoice_item_interval": null, 73 | "pending_setup_intent": null, 74 | "plan": { 75 | "id": "plan_F4G9jB3x5i6Dpj", 76 | "object": "plan", 77 | "active": true, 78 | "aggregate_usage": null, 79 | "amount": 100, 80 | "amount_decimal": "100", 81 | "billing_scheme": "per_unit", 82 | "created": 1557867224, 83 | "currency": "usd", 84 | "interval": "day", 85 | "interval_count": 1, 86 | "livemode": false, 87 | "metadata": {}, 88 | "nickname": "Daily Subscription", 89 | "product": "prod_EtMczoDntN9YEa", 90 | "tiers": null, 91 | "tiers_mode": null, 92 | "transform_usage": null, 93 | "trial_period_days": null, 94 | "usage_type": "licensed" 95 | }, 96 | "quantity": 1, 97 | "schedule": null, 98 | "start": 1559767571, 99 | "start_date": 1559767571, 100 | "status": "active", 101 | "tax_percent": null, 102 | "trial_end": null, 103 | "trial_start": null 104 | }, 105 | "previous_attributes": { 106 | "cancel_at_period_end": true 107 | } 108 | }, 109 | "livemode": false, 110 | "pending_webhooks": 1, 111 | "request": { 112 | "id": null, 113 | "idempotency_key": null 114 | }, 115 | "type": "customer.subscription.updated" 116 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/fixtures/valid_plan_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "plan_234", 4 | "object": "plan", 5 | "active": true, 6 | "aggregate_usage": null, 7 | "amount": 499, 8 | "amount_decimal": "499", 9 | "billing_scheme": "per_unit", 10 | "created": 1567100792, 11 | "currency": "usd", 12 | "interval": "month", 13 | "interval_count": 1, 14 | "livemode": false, 15 | "metadata": { 16 | "downgrades": "plan_123" 17 | }, 18 | "nickname": "test", 19 | "product": "prod_test1", 20 | "tiers": null, 21 | "tiers_mode": null, 22 | "transform_usage": null, 23 | "trial_period_days": null, 24 | "usage_type": "licensed" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /src/hub/tests/unit/routes/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/charge-dispute-closed.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.closed", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "dp_00000000000000", 13 | "object": "dispute", 14 | "amount": 1000, 15 | "balance_transaction": "txn_00000000000000", 16 | "balance_transactions": [ 17 | ], 18 | "charge": "ch_00000000000000", 19 | "created": 1557190345, 20 | "currency": "usd", 21 | "evidence": { 22 | "access_activity_log": null, 23 | "billing_address": null, 24 | "cancellation_policy": null, 25 | "cancellation_policy_disclosure": null, 26 | "cancellation_rebuttal": null, 27 | "customer_communication": null, 28 | "customer_email_address": null, 29 | "customer_name": null, 30 | "customer_purchase_ip": null, 31 | "customer_signature": null, 32 | "duplicate_charge_documentation": null, 33 | "duplicate_charge_explanation": null, 34 | "duplicate_charge_id": null, 35 | "product_description": null, 36 | "receipt": null, 37 | "refund_policy": null, 38 | "refund_policy_disclosure": null, 39 | "refund_refusal_explanation": null, 40 | "service_date": null, 41 | "service_documentation": null, 42 | "shipping_address": null, 43 | "shipping_carrier": null, 44 | "shipping_date": null, 45 | "shipping_documentation": null, 46 | "shipping_tracking_number": null, 47 | "uncategorized_file": null, 48 | "uncategorized_text": "Here is some evidence" 49 | }, 50 | "evidence_details": { 51 | "due_by": 1558915199, 52 | "has_evidence": false, 53 | "past_due": false, 54 | "submission_count": 0 55 | }, 56 | "is_charge_refundable": true, 57 | "livemode": false, 58 | "metadata": { 59 | }, 60 | "reason": "general", 61 | "status": "won" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/charge-dispute-created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.created", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "dp_00000000000000", 13 | "object": "dispute", 14 | "amount": 1000, 15 | "balance_transaction": "txn_00000000000000", 16 | "balance_transactions": [ 17 | ], 18 | "charge": "ch_00000000000000", 19 | "created": 1557190387, 20 | "currency": "usd", 21 | "evidence": { 22 | "access_activity_log": null, 23 | "billing_address": null, 24 | "cancellation_policy": null, 25 | "cancellation_policy_disclosure": null, 26 | "cancellation_rebuttal": null, 27 | "customer_communication": null, 28 | "customer_email_address": null, 29 | "customer_name": null, 30 | "customer_purchase_ip": null, 31 | "customer_signature": null, 32 | "duplicate_charge_documentation": null, 33 | "duplicate_charge_explanation": null, 34 | "duplicate_charge_id": null, 35 | "product_description": null, 36 | "receipt": null, 37 | "refund_policy": null, 38 | "refund_policy_disclosure": null, 39 | "refund_refusal_explanation": null, 40 | "service_date": null, 41 | "service_documentation": null, 42 | "shipping_address": null, 43 | "shipping_carrier": null, 44 | "shipping_date": null, 45 | "shipping_documentation": null, 46 | "shipping_tracking_number": null, 47 | "uncategorized_file": null, 48 | "uncategorized_text": null 49 | }, 50 | "evidence_details": { 51 | "due_by": 1558915199, 52 | "has_evidence": false, 53 | "past_due": false, 54 | "submission_count": 0 55 | }, 56 | "is_charge_refundable": true, 57 | "livemode": false, 58 | "metadata": { 59 | }, 60 | "reason": "general", 61 | "status": "needs_response" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/charge-dispute-funds_reinstated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.funds_reinstated", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "dp_00000000000000", 13 | "object": "dispute", 14 | "amount": 1000, 15 | "balance_transaction": "txn_00000000000000", 16 | "balance_transactions": [ 17 | ], 18 | "charge": "ch_00000000000000", 19 | "created": 1557190440, 20 | "currency": "usd", 21 | "evidence": { 22 | "access_activity_log": null, 23 | "billing_address": null, 24 | "cancellation_policy": null, 25 | "cancellation_policy_disclosure": null, 26 | "cancellation_rebuttal": null, 27 | "customer_communication": null, 28 | "customer_email_address": null, 29 | "customer_name": null, 30 | "customer_purchase_ip": null, 31 | "customer_signature": null, 32 | "duplicate_charge_documentation": null, 33 | "duplicate_charge_explanation": null, 34 | "duplicate_charge_id": null, 35 | "product_description": null, 36 | "receipt": null, 37 | "refund_policy": null, 38 | "refund_policy_disclosure": null, 39 | "refund_refusal_explanation": null, 40 | "service_date": null, 41 | "service_documentation": null, 42 | "shipping_address": null, 43 | "shipping_carrier": null, 44 | "shipping_date": null, 45 | "shipping_documentation": null, 46 | "shipping_tracking_number": null, 47 | "uncategorized_file": null, 48 | "uncategorized_text": null 49 | }, 50 | "evidence_details": { 51 | "due_by": 1558915199, 52 | "has_evidence": false, 53 | "past_due": false, 54 | "submission_count": 0 55 | }, 56 | "is_charge_refundable": true, 57 | "livemode": false, 58 | "metadata": { 59 | }, 60 | "reason": "general", 61 | "status": "warning_needs_response" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/charge-dispute-funds_withdrawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.funds_withdrawn", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "dp_00000000000000", 13 | "object": "dispute", 14 | "amount": 1000, 15 | "balance_transaction": "txn_00000000000000", 16 | "balance_transactions": [ 17 | ], 18 | "charge": "ch_00000000000000", 19 | "created": 1557190512, 20 | "currency": "usd", 21 | "evidence": { 22 | "access_activity_log": null, 23 | "billing_address": null, 24 | "cancellation_policy": null, 25 | "cancellation_policy_disclosure": null, 26 | "cancellation_rebuttal": null, 27 | "customer_communication": null, 28 | "customer_email_address": null, 29 | "customer_name": null, 30 | "customer_purchase_ip": null, 31 | "customer_signature": null, 32 | "duplicate_charge_documentation": null, 33 | "duplicate_charge_explanation": null, 34 | "duplicate_charge_id": null, 35 | "product_description": null, 36 | "receipt": null, 37 | "refund_policy": null, 38 | "refund_policy_disclosure": null, 39 | "refund_refusal_explanation": null, 40 | "service_date": null, 41 | "service_documentation": null, 42 | "shipping_address": null, 43 | "shipping_carrier": null, 44 | "shipping_date": null, 45 | "shipping_documentation": null, 46 | "shipping_tracking_number": null, 47 | "uncategorized_file": null, 48 | "uncategorized_text": null 49 | }, 50 | "evidence_details": { 51 | "due_by": 1558915199, 52 | "has_evidence": false, 53 | "past_due": false, 54 | "submission_count": 0 55 | }, 56 | "is_charge_refundable": true, 57 | "livemode": false, 58 | "metadata": { 59 | }, 60 | "reason": "general", 61 | "status": "warning_needs_response" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/charge-dispute-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.updated", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "dp_00000000000000", 13 | "object": "dispute", 14 | "amount": 1000, 15 | "balance_transaction": "txn_00000000000000", 16 | "balance_transactions": [ 17 | ], 18 | "charge": "ch_00000000000000", 19 | "created": 1557190561, 20 | "currency": "usd", 21 | "evidence": { 22 | "access_activity_log": null, 23 | "billing_address": null, 24 | "cancellation_policy": null, 25 | "cancellation_policy_disclosure": null, 26 | "cancellation_rebuttal": null, 27 | "customer_communication": null, 28 | "customer_email_address": null, 29 | "customer_name": null, 30 | "customer_purchase_ip": null, 31 | "customer_signature": null, 32 | "duplicate_charge_documentation": null, 33 | "duplicate_charge_explanation": null, 34 | "duplicate_charge_id": null, 35 | "product_description": null, 36 | "receipt": null, 37 | "refund_policy": null, 38 | "refund_policy_disclosure": null, 39 | "refund_refusal_explanation": null, 40 | "service_date": null, 41 | "service_documentation": null, 42 | "shipping_address": null, 43 | "shipping_carrier": null, 44 | "shipping_date": null, 45 | "shipping_documentation": null, 46 | "shipping_tracking_number": null, 47 | "uncategorized_file": null, 48 | "uncategorized_text": "Here is some evidence" 49 | }, 50 | "evidence_details": { 51 | "due_by": 1558915199, 52 | "has_evidence": false, 53 | "past_due": false, 54 | "submission_count": 0 55 | }, 56 | "is_charge_refundable": true, 57 | "livemode": false, 58 | "metadata": { 59 | }, 60 | "reason": "general", 61 | "status": "under_review" 62 | }, 63 | "previous_attributes": { 64 | "evidence": { 65 | "access_activity_log": null, 66 | "billing_address": null, 67 | "cancellation_policy": null, 68 | "cancellation_policy_disclosure": null, 69 | "cancellation_rebuttal": null, 70 | "customer_communication": null, 71 | "customer_email_address": null, 72 | "customer_name": null, 73 | "customer_purchase_ip": null, 74 | "customer_signature": null, 75 | "duplicate_charge_documentation": null, 76 | "duplicate_charge_explanation": null, 77 | "duplicate_charge_id": null, 78 | "product_description": null, 79 | "receipt": null, 80 | "refund_policy": null, 81 | "refund_policy_disclosure": null, 82 | "refund_refusal_explanation": null, 83 | "service_date": null, 84 | "service_documentation": null, 85 | "shipping_address": null, 86 | "shipping_carrier": null, 87 | "shipping_date": null, 88 | "shipping_documentation": null, 89 | "shipping_tracking_number": null, 90 | "uncategorized_file": null, 91 | "uncategorized_text": "Old uncategorized text" 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/charge-refund-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.refund.updated", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2018-02-06", 10 | "data": { 11 | "object": { 12 | "id": "re_00000000000000", 13 | "object": "refund", 14 | "amount": 100, 15 | "balance_transaction": null, 16 | "charge": "ch_00000000000000", 17 | "created": 1557190616, 18 | "currency": "usd", 19 | "metadata": { 20 | }, 21 | "reason": null, 22 | "receipt_number": null, 23 | "source_transfer_reversal": null, 24 | "status": "succeeded", 25 | "transfer_reversal": null 26 | }, 27 | "previous_attributes": { 28 | "metadata": { 29 | "order_id": "old_order_id" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/charge/test_stripe_charge.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import mockito 7 | import requests 8 | import boto3 9 | import flask 10 | 11 | from hub.shared.cfg import CFG 12 | from hub.shared.tests.unit.utils import run_test, MockSqsClient 13 | 14 | CWD = os.path.realpath(os.path.dirname(__file__)) 15 | 16 | 17 | def test_stripe_hub_succeeded(mocker): 18 | response = mockito.mock({"status_code": 200, "text": "Ok"}, spec=requests.Response) 19 | data = { 20 | "event_id": "evt_00000000000000", 21 | "event_type": "charge.succeeded", 22 | "charge_id": "evt_00000000000000", 23 | "invoice_id": None, 24 | "customer_id": None, 25 | "order_id": "6735", 26 | "card_last4": "4444", 27 | "card_brand": "mastercard", 28 | "card_exp_month": 8, 29 | "card_exp_year": 2019, 30 | "application_fee": None, 31 | "transaction_amount": 2000, 32 | "transaction_currency": "usd", 33 | "created_date": 1326853478, 34 | } 35 | 36 | # using mockito 37 | basket_url = CFG.SALESFORCE_BASKET_URI + CFG.BASKET_API_KEY 38 | mockito.when(requests).post(basket_url, json=data).thenReturn(response) 39 | mockito.when(boto3).client( 40 | "sqs", 41 | region_name=CFG.AWS_REGION, 42 | aws_access_key_id=CFG.AWS_ACCESS_KEY_ID, 43 | aws_secret_access_key=CFG.AWS_SECRET_ACCESS_KEY, 44 | ).thenReturn(MockSqsClient) 45 | 46 | # using pytest mock 47 | mocker.patch.object(flask, "g") 48 | flask.g.return_value = "" 49 | flask.g.hub_table.get_event.return_value = "" 50 | 51 | # run the test 52 | run_test("charge-succeeded.json", cwd=CWD) 53 | 54 | 55 | def test_stripe_hub_badpayload(): 56 | try: 57 | run_test("badpayload.json", cwd=CWD) 58 | except ValueError as e: 59 | assert "this.will.break is not supported" == str(e) 60 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/customer/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/event/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/event/test_stripe_events.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import time 7 | import json 8 | import boto3 9 | import flask 10 | import stripe 11 | import requests 12 | 13 | from flask import Response 14 | from mockito import when, mock, unstub 15 | from datetime import datetime, timedelta 16 | 17 | from hub.tests import conftest 18 | 19 | from hub.shared.tests.unit.utils import run_view, run_event_process 20 | from hub.shared.cfg import CFG 21 | from shared.log import get_logger 22 | 23 | logger = get_logger() 24 | 25 | CWD = os.path.realpath(os.path.dirname(__file__)) 26 | 27 | 28 | def test_hours_back(): 29 | from hub.verifications.events_check import EventCheck 30 | 31 | event_check_class = EventCheck(hours_back=1) 32 | assert isinstance( 33 | event_check_class.get_time_h_hours_ago(hours_back=event_check_class.hours_back), 34 | int, 35 | ) 36 | 37 | 38 | def test_process_missing_event(dynamodb): 39 | from hub.verifications.events_check import EventCheck, process_events 40 | 41 | missing_event = "event.json" 42 | with open(os.path.join(CWD, missing_event)) as f: 43 | event_check = EventCheck(6) 44 | event_check.process_missing_event(json.load(f)) 45 | 46 | 47 | def test_retrieve_events(dynamodb): 48 | from hub.verifications.events_check import EventCheck, process_events 49 | 50 | missing_event = "event.json" 51 | 52 | def get_hours_back(): 53 | h_hours_ago = datetime.now() - timedelta(hours=6) 54 | return int(time.mktime(h_hours_ago.timetuple())) 55 | 56 | with open(os.path.join(CWD, missing_event)) as f: 57 | event_response = mock(json.load(f)) 58 | 59 | when(stripe.Event).list( 60 | limit=100, types=CFG.PAYMENT_EVENT_LIST, created={"gt": get_hours_back()} 61 | ).thenReturn(event_response) 62 | event_check = EventCheck(6) 63 | event_check.retrieve_events("") 64 | unstub() 65 | 66 | 67 | def test_retrieve_events_more(): 68 | from hub.verifications.events_check import EventCheck 69 | 70 | missing_event = "more_event.json" 71 | 72 | def get_hours_back(): 73 | h_hours_ago = datetime.now() - timedelta(hours=6) 74 | return int(time.mktime(h_hours_ago.timetuple())) 75 | 76 | with open(os.path.join(CWD, missing_event)) as f: 77 | event_response = mock(json.load(f)) 78 | 79 | when(stripe.Event).list( 80 | limit=100, 81 | types=CFG.PAYMENT_EVENT_LIST, 82 | created={"gt": get_hours_back()}, 83 | starting_after="evt_001", 84 | ).thenReturn(event_response) 85 | event_check = EventCheck(6) 86 | event_check.get_events_with_last_event("evt_001") 87 | unstub() 88 | 89 | 90 | def test_process_events(dynamodb): 91 | os.environ["DYNALITE_URL"] = dynamodb 92 | missing_event = "event.json" 93 | from hub.verifications.events_check import process_events 94 | 95 | def get_hours_back(): 96 | h_hours_ago = datetime.now() - timedelta(hours=6) 97 | return int(time.mktime(h_hours_ago.timetuple())) 98 | 99 | with open(os.path.join(CWD, missing_event)) as f: 100 | event_response = mock(json.load(f)) 101 | 102 | when(stripe.Event).list( 103 | limit=100, types=CFG.PAYMENT_EVENT_LIST, created={"gt": get_hours_back()} 104 | ).thenReturn(event_response) 105 | process_events(6) 106 | unstub() 107 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/invoice/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/payment/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/payment/test_stripe_payments.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import mockito 7 | import requests 8 | import boto3 9 | import flask 10 | 11 | import stripe.error 12 | from mockito import when, mock, unstub 13 | 14 | from hub.shared.cfg import CFG 15 | from hub.shared import secrets 16 | from hub.shared.tests.unit.utils import run_test, MockSqsClient 17 | 18 | CWD = os.path.realpath(os.path.dirname(__file__)) 19 | 20 | 21 | def run_customer(mocker, data, filename): 22 | # using pytest mock 23 | mocker.patch.object(flask, "g") 24 | flask.g.return_value = "" 25 | 26 | run_test(filename, cwd=CWD) 27 | 28 | 29 | def test_stripe_payment_intent_succeeded(mocker): 30 | invoice_response = mock( 31 | { 32 | "id": "in_000000", 33 | "object": "customer", 34 | "account_balance": 0, 35 | "created": 1563287210, 36 | "currency": "usd", 37 | "subscription": "sub_000000", 38 | "period_start": 1563287210, 39 | "period_end": 1563287210, 40 | }, 41 | spec=stripe.Invoice, 42 | ) 43 | when(stripe.Invoice).retrieve(id="in_000000").thenReturn(invoice_response) 44 | data = { 45 | "event_id": "evt_00000000000000", 46 | "event_type": "payment_intent.succeeded", 47 | "brand": "Visa", 48 | "last4": "4242", 49 | "exp_month": 6, 50 | "exp_year": 2020, 51 | "charge_id": "ch_000000", 52 | "invoice_id": "in_000000", 53 | "customer_id": "cus_000000", 54 | "amount_paid": 1000, 55 | "created": 1559568879, 56 | "subscription_id": "sub_000000", 57 | "period_start": 1563287210, 58 | "period_end": 1563287210, 59 | "currency": "usd", 60 | } 61 | basket_url = CFG.SALESFORCE_BASKET_URI + CFG.BASKET_API_KEY 62 | response = mockito.mock({"status_code": 200, "text": "Ok"}, spec=requests.Response) 63 | mockito.when(boto3).client("sqs", region_name=CFG.AWS_REGION).thenReturn( 64 | MockSqsClient 65 | ) 66 | mockito.when(requests).post(basket_url, json=data).thenReturn(response) 67 | filename = "payment-intent-succeeded.json" 68 | run_customer(mocker, data, filename) 69 | unstub() 70 | -------------------------------------------------------------------------------- /src/hub/tests/unit/stripe/test_app.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from flask import jsonify 6 | from stripe.error import AuthenticationError, CardError, StripeError 7 | 8 | from hub.app import create_app 9 | from hub.app import server_stripe_error 10 | from hub.app import intermittent_stripe_error 11 | from hub.app import server_stripe_error_with_params 12 | from hub.app import server_stripe_card_error 13 | from shared.cfg import CFG 14 | 15 | 16 | def test_create_app(): 17 | app = create_app() 18 | assert app 19 | 20 | 21 | def test_intermittent_stripe_error(): 22 | expected = jsonify({"message": "something"}), 503 23 | error = StripeError("something") 24 | actual = intermittent_stripe_error(error) 25 | assert actual[0].json == expected[0].json 26 | assert actual[1] == expected[1] 27 | 28 | 29 | def test_server_stripe_error(): 30 | expected = ( 31 | jsonify({"message": "Internal Server Error", "code": "500", "params": None}), 32 | 500, 33 | ) 34 | error = AuthenticationError("something", code="500") 35 | actual = server_stripe_error(error) 36 | assert actual[0].json == expected[0].json 37 | assert actual[1] == expected[1] 38 | 39 | 40 | def test_server_stripe_error_with_params(): 41 | expected = jsonify({"message": "something", "params": "param1", "code": "500"}), 500 42 | error = CardError("something", "param1", "500") 43 | actual = server_stripe_error_with_params(error) 44 | assert actual[0].json == expected[0].json 45 | assert actual[1] == expected[1] 46 | 47 | 48 | def test_server_stripe_card_error(): 49 | expected = jsonify({"message": "something", "code": "402"}), 402 50 | error = CardError("something", "param1", "402") 51 | actual = server_stripe_card_error(error) 52 | assert actual[0].json == expected[0].json 53 | assert actual[1] == expected[1] 54 | -------------------------------------------------------------------------------- /src/hub/tests/unit/test_app.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | import json 5 | from unittest import TestCase 6 | from mock import patch 7 | 8 | from flask import jsonify 9 | from stripe.error import AuthenticationError, CardError, StripeError 10 | from hub.shared.exceptions import SubHubError 11 | 12 | from hub.app import create_app 13 | from hub.app import server_stripe_error 14 | from hub.app import intermittent_stripe_error 15 | from hub.app import server_stripe_error_with_params 16 | from hub.app import server_stripe_card_error 17 | from shared.cfg import CFG 18 | from shared.log import get_logger 19 | 20 | logger = get_logger() 21 | 22 | 23 | def test_create_app(): 24 | app = create_app() 25 | assert app 26 | # subhub_error = app.display_subhub_errors("bad things") 27 | print(f"subhub error {dir(app)} app= {app}") 28 | 29 | 30 | def test_intermittent_stripe_error(): 31 | expected = jsonify({"message": "something"}), 503 32 | error = StripeError("something") 33 | actual = intermittent_stripe_error(error) 34 | assert actual[0].json == expected[0].json 35 | assert actual[1] == expected[1] 36 | 37 | 38 | def test_server_stripe_error(): 39 | expected = ( 40 | jsonify({"message": "Internal Server Error", "code": "500", "params": None}), 41 | 500, 42 | ) 43 | error = AuthenticationError("something", code="500") 44 | actual = server_stripe_error(error) 45 | assert actual[0].json == expected[0].json 46 | assert actual[1] == expected[1] 47 | 48 | 49 | def test_server_stripe_error_with_params(): 50 | expected = jsonify({"message": "something", "params": "param1", "code": "500"}), 500 51 | error = CardError("something", "param1", "500") 52 | actual = server_stripe_error_with_params(error) 53 | assert actual[0].json == expected[0].json 54 | assert actual[1] == expected[1] 55 | 56 | 57 | def test_server_stripe_card_error(): 58 | expected = jsonify({"message": "something", "code": "402"}), 402 59 | error = CardError("something", "param1", "402") 60 | actual = server_stripe_card_error(error) 61 | assert actual[0].json == expected[0].json 62 | assert actual[1] == expected[1] 63 | 64 | 65 | class TestApp(TestCase): 66 | def setUp(self) -> None: 67 | self.app = create_app() 68 | self.client = self.app.app.test_client() 69 | 70 | def test_custom_404(self): 71 | path = "/v1/versions" 72 | response = self.client.get(path) 73 | self.assertEqual(response.status_code, 404) 74 | print(f"path {path} data {response}") 75 | -------------------------------------------------------------------------------- /src/hub/tests/unit/test_stripe_controller.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import flask 6 | 7 | from flask import Response 8 | from mockito import unstub 9 | 10 | from hub.shared.tests.unit.utils import run_event_process 11 | from shared.log import get_logger 12 | 13 | logger = get_logger() 14 | 15 | 16 | def run_webhook(mocker, data): 17 | mocker.patch.object(flask, "g") 18 | flask.g.return_value = "" 19 | return run_event_process(data) 20 | 21 | 22 | def test_controller_view(mocker): 23 | data = { 24 | "event_id": "evt_00000000000000", 25 | "type": "payment_intent.succeeded", 26 | "brand": "Visa", 27 | "last4": "4242", 28 | "exp_month": 6, 29 | "exp_year": 2020, 30 | "charge_id": "ch_000000", 31 | "invoice_id": "in_000000", 32 | "customer_id": "cus_000000", 33 | "amount_paid": 1000, 34 | "created": 1559568879, 35 | "subscription_id": "sub_000000", 36 | "period_start": 1563287210, 37 | "period_end": 1563287210, 38 | "currency": "usd", 39 | } 40 | webhook = run_webhook(mocker, data) 41 | assert isinstance(webhook, Response) 42 | unstub() 43 | 44 | 45 | def test_controller_view_bad_data(mocker): 46 | data = "imalittleteapot" 47 | webhook = run_webhook(mocker, data) 48 | assert isinstance(webhook, Response) 49 | unstub() 50 | -------------------------------------------------------------------------------- /src/hub/vendor/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/vendor/abstract.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | import requests 5 | import json 6 | 7 | from abc import ABC, abstractmethod 8 | from typing import Dict 9 | from attrdict import AttrDict 10 | 11 | from hub.routes.pipeline import RoutesPipeline, AllRoutes 12 | from shared.cfg import CFG 13 | from shared.log import get_logger 14 | 15 | logger = get_logger() 16 | 17 | 18 | class AbstractStripeHubEvent(ABC): 19 | def __init__(self, payload) -> None: 20 | self.payload = AttrDict(payload) 21 | 22 | @property 23 | def is_active_or_trialing(self) -> bool: 24 | return self.payload.data.object.status in ("active", "trialing") 25 | 26 | @staticmethod 27 | def send_to_routes(report_routes, message_to_route) -> None: 28 | logger.info( 29 | "send to routes", 30 | report_routes=report_routes, 31 | message_to_route=message_to_route, 32 | ) 33 | RoutesPipeline(report_routes, message_to_route).run() 34 | 35 | @staticmethod 36 | def send_to_all_routes(messages_to_routes) -> None: 37 | logger.info("send to all routes", messages_to_routes=messages_to_routes) 38 | AllRoutes(messages_to_routes).run() 39 | 40 | @staticmethod 41 | def send_to_salesforce(self, payload) -> None: 42 | logger.info("sending to salesforce", payload=payload) 43 | uri = CFG.SALESFORCE_BASKET_URI 44 | requests.post(uri, data=payload) 45 | 46 | @staticmethod 47 | def unhandled_event(payload) -> None: 48 | logger.info("Event not handled", payload=payload) 49 | 50 | @abstractmethod 51 | def run(self) -> bool: 52 | raise NotImplementedError 53 | 54 | def create_data(self, **kwargs) -> Dict[str, str]: 55 | return dict( 56 | Event_Id__c=self.payload.id, Event_Name__c=self.payload.type, **kwargs 57 | ) 58 | 59 | def customer_event_to_all_routes(self, data_projection, data) -> None: 60 | subsets = [] 61 | for route in data_projection: 62 | try: 63 | logger.debug("sending to", key=route) 64 | subset = dict((k, data[k]) for k in data_projection[route] if k in data) 65 | payload = {"type": route, "data": json.dumps(subset)} 66 | subsets.append(payload) 67 | logger.info("subset", subset=subset) 68 | logger.debug("sent to", key=route) 69 | except Exception as e: 70 | # log something and maybe change the exception type. 71 | logger.error("projection exception", error=e) 72 | else: 73 | self.send_to_all_routes(subsets) 74 | -------------------------------------------------------------------------------- /src/hub/vendor/events.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import time 6 | 7 | from abc import ABC, abstractmethod 8 | from typing import Dict, Any 9 | from attrdict import AttrDict 10 | 11 | from shared.cfg import CFG 12 | from shared.log import get_logger 13 | 14 | logger = get_logger() 15 | 16 | 17 | class EventMaker(ABC): 18 | def __init__(self, payload) -> None: 19 | self.payload = AttrDict(payload) 20 | 21 | def get_complete_event(self) -> Dict[str, Any]: 22 | logger.debug( 23 | "complete pay", payload=self.payload, event_type=self.payload["type"] 24 | ) 25 | return self.payload 26 | -------------------------------------------------------------------------------- /src/hub/verifications/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/hub/verifications/events_check.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | import sys 5 | import time 6 | import stripe 7 | 8 | from abc import ABC 9 | from datetime import datetime, timedelta 10 | from typing import Dict, Any 11 | from flask import current_app 12 | 13 | from hub.app import create_app, g 14 | from hub.vendor.controller import event_process 15 | from shared.cfg import CFG 16 | from shared.log import get_logger 17 | 18 | logger = get_logger() 19 | 20 | if not hasattr(sys, "_called_from_test"): 21 | 22 | try: 23 | app = create_app() 24 | except Exception: # pylint: disable=broad-except 25 | logger.exception("Exception occurred while loading app") 26 | raise 27 | else: 28 | app = create_app() 29 | 30 | 31 | class EventCheck(ABC): 32 | def __init__(self, hours_back) -> None: 33 | self.hours_back = hours_back 34 | 35 | def retrieve_events(self, last_event=str()) -> None: 36 | retrieved_events = 0 37 | has_more = True 38 | while has_more: 39 | if not last_event: 40 | events = self.get_events() 41 | else: 42 | events = self.get_events_with_last_event(last_event) 43 | for e in events.data: # type: ignore 44 | existing_event = g.hub_table.get_event(e["id"]) 45 | if not existing_event: 46 | logger.info("is existing event", is_event=existing_event) 47 | self.process_missing_event(e) 48 | retrieved_events += len(events.data) # type: ignore 49 | 50 | has_more = events.has_more # type: ignore 51 | if has_more: 52 | last_event = events.data[-1]["id"] # type: ignore 53 | logger.info("last_event", last_event=last_event) 54 | logger.info("number events", number_of_events=retrieved_events) 55 | 56 | def get_events(self) -> Dict[str, Any]: 57 | return stripe.Event.list( 58 | limit=100, 59 | types=CFG.PAYMENT_EVENT_LIST, 60 | created={"gt": self.get_time_h_hours_ago(self.hours_back)}, 61 | ) 62 | 63 | def get_events_with_last_event(self, last_event) -> Dict[str, Any]: 64 | return stripe.Event.list( 65 | limit=100, 66 | types=CFG.PAYMENT_EVENT_LIST, 67 | created={"gt": self.get_time_h_hours_ago(self.hours_back)}, 68 | starting_after=last_event, 69 | ) 70 | 71 | @staticmethod 72 | def get_time_h_hours_ago(hours_back: int) -> int: 73 | h_hours_ago = datetime.now() - timedelta(hours=hours_back) 74 | return int(time.mktime(h_hours_ago.timetuple())) 75 | 76 | @staticmethod 77 | def process_missing_event(missing_event) -> None: 78 | event_process(missing_event) 79 | 80 | 81 | def process_events(hours_back: int) -> None: 82 | with app.app.app_context(): 83 | g.hub_table = current_app.hub_table 84 | event_check = EventCheck(hours_back) 85 | event_check.retrieve_events("") 86 | -------------------------------------------------------------------------------- /src/shared/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /src/shared/authentication.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from shared import secrets 6 | from shared.cfg import CFG 7 | from shared.log import get_logger 8 | 9 | logger = get_logger() 10 | 11 | 12 | def test_token(test_api_token, cfg_api_token): 13 | # Make sure the config API token has a meaningful value set, 14 | # to avoid an auth bypass on empty comparisons 15 | if cfg_api_token in (None, "None", ""): 16 | return None 17 | 18 | if test_api_token == cfg_api_token: 19 | return {"value": True} 20 | 21 | return None 22 | 23 | 24 | def payment_auth(api_token, required_scopes=None): 25 | return test_token(api_token, CFG.PAYMENT_API_KEY) 26 | 27 | 28 | def support_auth(api_token, required_scopes=None): 29 | return test_token(api_token, CFG.SUPPORT_API_KEY) 30 | 31 | 32 | def hub_auth(api_token, required_scopes=None): 33 | return test_token(api_token, CFG.HUB_API_KEY) 34 | -------------------------------------------------------------------------------- /src/shared/deployed.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from shared.types import FlaskResponse 6 | from shared.cfg import CFG 7 | from shared.log import get_logger 8 | 9 | logger = get_logger() 10 | 11 | 12 | def get_deployed() -> FlaskResponse: 13 | deployed = dict( 14 | DEPLOYED_BY=CFG.DEPLOYED_BY, 15 | DEPLOYED_ENV=CFG.DEPLOYED_ENV, 16 | DEPLOYED_WHEN=CFG.DEPLOYED_WHEN, 17 | ) 18 | logger.debug("deployed", deployed=deployed) 19 | return deployed, 200 20 | -------------------------------------------------------------------------------- /src/shared/dynamodb.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import backoff 6 | import docker 7 | import psutil 8 | import pytest 9 | import socket 10 | import requests 11 | import string 12 | import random 13 | import json 14 | 15 | from shared.log import get_logger 16 | 17 | logger = get_logger() 18 | 19 | 20 | def random_label(length: int) -> str: 21 | letters = string.ascii_lowercase 22 | return "".join(random.choice(letters) for i in range(length)) 23 | 24 | 25 | # Amazon's DynamoDB Local 26 | # https://hub.docker.com/r/amazon/dynamodb-local 27 | IMAGE = "amazon/dynamodb-local:latest" 28 | CONTAINER_FOR_TESTING_LABEL = random_label(128) 29 | 30 | 31 | def get_free_tcp_port(): 32 | tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 33 | tcp.bind(("", 0)) 34 | addr, port = tcp.getsockname() 35 | logger.debug("port", port=port) 36 | tcp.close() 37 | return port 38 | 39 | 40 | def pull_image(image): 41 | docker_client = docker.from_env() 42 | response = docker_client.api.pull(image) 43 | lines = [line for line in response.splitlines() if line] 44 | pull_result = json.loads(lines[-1]) 45 | if "error" in pull_result: 46 | raise Exception("Could not pull {}: {}".format(image, pull_result["error"])) 47 | 48 | 49 | @pytest.yield_fixture(scope="module", autouse=True) 50 | def dynamodb(): 51 | pull_image(IMAGE) 52 | docker_client = docker.from_env() 53 | host_port = get_free_tcp_port() 54 | port_bindings = {8000: host_port} 55 | host_config = docker_client.api.create_host_config(port_bindings=port_bindings) 56 | container = docker_client.api.create_container( 57 | image=IMAGE, 58 | labels=[CONTAINER_FOR_TESTING_LABEL], 59 | host_config=host_config, 60 | ports=[4567], 61 | ) 62 | docker_client.api.start(container=container["Id"]) 63 | container_info = docker_client.api.inspect_container(container.get("Id")) 64 | host_ip = container_info["NetworkSettings"]["Ports"]["8000/tcp"][0]["HostIp"] 65 | host_port = container_info["NetworkSettings"]["Ports"]["8000/tcp"][0]["HostPort"] 66 | url = f"http://{host_ip}:{host_port}" 67 | _check_container(url) 68 | yield url 69 | docker_client.api.remove_container(container=container["Id"], force=True) 70 | 71 | 72 | @backoff.on_exception(backoff.fibo, Exception, max_tries=8) 73 | def _check_container(url): 74 | return requests.get(url) 75 | -------------------------------------------------------------------------------- /src/shared/exceptions.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | from typing import Dict, Any 5 | 6 | 7 | class SubHubError(Exception): 8 | """Base SubHub Exception""" 9 | 10 | status_code = 500 11 | 12 | def __init__(self, message, status_code, payload) -> None: 13 | super().__init__(message) 14 | self.status_code = status_code 15 | self.payload = payload 16 | 17 | def to_dict(self) -> Dict[str, Any]: 18 | result = dict(self.payload or ()) 19 | result["message"] = self.args[0] 20 | return result 21 | 22 | def __repr__(self): 23 | return f"{self.__class__.__name__}(message={self.args[0]}, status_code={self.status_code}, payload={self.payload})" 24 | 25 | __str__ = __repr__ 26 | 27 | 28 | class IntermittentError(SubHubError): 29 | """Intermittent Exception""" 30 | 31 | status_code = 503 32 | 33 | def __init__(self, message, status_code=None, payload=None) -> None: 34 | super().__init__( 35 | message, 36 | status_code=status_code or IntermittentError.status_code, 37 | payload=payload, 38 | ) 39 | 40 | 41 | class ClientError(SubHubError): 42 | """Client Exception""" 43 | 44 | status_code = 400 45 | 46 | def __init__(self, message, status_code=None, payload=None) -> None: 47 | super().__init__( 48 | message, status_code=status_code or ClientError.status_code, payload=payload 49 | ) 50 | 51 | 52 | class EntityNotFoundError(ClientError): 53 | """Not Found Exception""" 54 | 55 | def __init__(self, message: str, error_number: int) -> None: 56 | payload = {"errno": error_number} 57 | 58 | super().__init__(message, status_code=404, payload=payload) 59 | 60 | 61 | class ValidationError(ClientError): 62 | """Input Validation Exception""" 63 | 64 | def __init__(self, message: str, error_number: int) -> None: 65 | payload = {"errno": error_number} 66 | 67 | super().__init__(message, status_code=400, payload=payload) 68 | 69 | 70 | class ServerError(SubHubError): 71 | """Server Exception""" 72 | 73 | status_code = 500 74 | 75 | def __init__(self, message, status_code=None, payload=None) -> None: 76 | super().__init__( 77 | message, status_code=status_code or ServerError.status_code, payload=payload 78 | ) 79 | 80 | 81 | class SecretStringMissingError(Exception): 82 | def __init__(self, secret) -> None: 83 | message = f"SecretString missing from secret={secret}" 84 | super().__init__(message) 85 | 86 | 87 | class UnsupportedStaticRouteError(Exception): 88 | def __init__(self, route, routes): 89 | message = f"{route} is unsupported; should be one of {routes}" 90 | super().__init__(message) 91 | 92 | 93 | class UnsupportedDataError(Exception): 94 | def __init__(self, data, route, routes): 95 | message = f"{route} is unsupported for {data}; should be one of {routes}" 96 | super().__init__(message) 97 | -------------------------------------------------------------------------------- /src/shared/headers.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | # HEADERS_WHITE_LIST is an array collection containing 6 | # request header entities that are acceptable items to 7 | # be logged into the application's produced logs. 8 | HEADERS_WHITE_LIST = ["Content-Length", "Content-Type", "Host", "X-Amzn-Trace-Id"] 9 | 10 | # `dump_headers` is a method to dump from headers from the `requests` library's 11 | # headers and compare against a known list of safe headers for utilization in 12 | # items such as logging and metrics. It is an O(n) algorithm so as the amount 13 | # of provided headers expands, our runtime thusly does but this being said, the 14 | # network packet transmission time would be expected to dominate this cost. 15 | def dump_safe_headers(request_headers): 16 | safe_headers = {} 17 | if request_headers is None: 18 | return safe_headers 19 | for header_key in request_headers.keys(): 20 | if header_key not in HEADERS_WHITE_LIST: 21 | continue 22 | safe_headers[header_key] = request_headers[header_key] 23 | return safe_headers 24 | 25 | 26 | def extract_safe(request_headers, key): 27 | if request_headers is None: 28 | return "" 29 | if key in request_headers: 30 | return request_headers[key] 31 | return "" 32 | -------------------------------------------------------------------------------- /src/shared/secrets.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import boto3 7 | import base64 8 | import json 9 | 10 | from typing import Dict, Any 11 | 12 | from shared.cfg import CFG 13 | from shared.exceptions import SecretStringMissingError 14 | 15 | 16 | def get_secret(secret_id) -> Dict[str, Any]: 17 | """Fetch secret via boto3.""" 18 | client = boto3.client(service_name="secretsmanager") 19 | get_secret_value_response = client.get_secret_value(SecretId=secret_id) 20 | 21 | if "SecretString" in get_secret_value_response: 22 | secret = get_secret_value_response["SecretString"] 23 | return json.loads(secret) 24 | raise SecretStringMissingError(secret) 25 | 26 | 27 | if CFG.AWS_EXECUTION_ENV: 28 | os.environ.update(get_secret(f"{CFG.DEPLOYED_ENV}/{CFG.PROJECT_NAME}")) 29 | -------------------------------------------------------------------------------- /src/shared/tests/unit/fixtures/stripe_deleted_cust.json: -------------------------------------------------------------------------------- 1 | { 2 | "deleted": true, 3 | "id": "cus_321", 4 | "object": "customer" 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/tests/unit/fixtures/stripe_in_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "in_test1", 3 | "object": "invoice", 4 | "account_country": "US", 5 | "account_name": "Mozilla Corporation", 6 | "amount_due": 1000, 7 | "amount_paid": 1000, 8 | "amount_remaining": 0, 9 | "application_fee_amount": null, 10 | "attempt_count": 1, 11 | "attempted": true, 12 | "auto_advance": false, 13 | "billing": "charge_automatically", 14 | "billing_reason": "subscription_create", 15 | "charge": "ch_test1", 16 | "collection_method": "charge_automatically", 17 | "created": 1555354567, 18 | "currency": "usd", 19 | "custom_fields": null, 20 | "customer": "cus_test1", 21 | "customer_address": null, 22 | "customer_email": "test_fixture@tester.com", 23 | "customer_name": null, 24 | "customer_phone": null, 25 | "customer_shipping": null, 26 | "customer_tax_exempt": "none", 27 | "customer_tax_ids": [], 28 | "default_payment_method": null, 29 | "default_source": null, 30 | "default_tax_rates": [], 31 | "description": null, 32 | "discount": null, 33 | "due_date": null, 34 | "ending_balance": 0, 35 | "footer": null, 36 | "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_ZPQW1BXsP9LEjkKf4oXxhNMeeS", 37 | "invoice_pdf": "https://pay.stripe.com/invoice/invst_ZPQW1BXsP9LEjkKf4oXxhNMeeS/pdf", 38 | "lines": { 39 | "data": [ 40 | { 41 | "id": "sli_2182b279422cba", 42 | "object": "line_item", 43 | "amount": 1000, 44 | "currency": "usd", 45 | "description": "1 Moz-Sub \u00d7 Moz_Sub (at $10.00 / month)", 46 | "discountable": true, 47 | "livemode": false, 48 | "metadata": {}, 49 | "period": { 50 | "end": 1572905353, 51 | "start": 1570226953 52 | }, 53 | "plan": {}, 54 | "proration": false, 55 | "quantity": 1, 56 | "subscription": "sub_FkbsOxUMt9qxhO", 57 | "subscription_item": "si_FkbsnTorDMAHh3", 58 | "tax_amounts": [], 59 | "tax_rates": [], 60 | "type": "subscription" 61 | } 62 | ], 63 | "has_more": false, 64 | "object": "list", 65 | "url": "/v1/invoices/in_test1/lines" 66 | }, 67 | "livemode": false, 68 | "metadata": {}, 69 | "next_payment_attempt": null, 70 | "number": "3B74E3D0-0001", 71 | "paid": true, 72 | "payment_intent": "pi_1EPZyNJNcmPzuWtR9U3SsJ4w", 73 | "period_end": 1555354567, 74 | "period_start": 1555354567, 75 | "post_payment_credit_notes_amount": 0, 76 | "pre_payment_credit_notes_amount": 0, 77 | "receipt_number": null, 78 | "starting_balance": 0, 79 | "statement_descriptor": null, 80 | "status": "paid", 81 | "status_transitions": { 82 | "finalized_at": 1555354567, 83 | "marked_uncollectible_at": null, 84 | "paid_at": 1555354568, 85 | "voided_at": null 86 | }, 87 | "subscription": "sub_test2", 88 | "subtotal": 1000, 89 | "tax": null, 90 | "tax_percent": null, 91 | "total": 1000, 92 | "total_tax_amounts": [], 93 | "webhooks_delivered_at": 1555354569 94 | } 95 | -------------------------------------------------------------------------------- /src/shared/tests/unit/fixtures/stripe_plan_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "plan_test1", 3 | "object": "plan", 4 | "active": true, 5 | "aggregate_usage": null, 6 | "amount": 100, 7 | "amount_decimal": "100", 8 | "billing_scheme": "per_unit", 9 | "created": 1561581476, 10 | "currency": "usd", 11 | "interval": "month", 12 | "interval_count": 1, 13 | "livemode": false, 14 | "metadata": {}, 15 | "nickname": "Free", 16 | "product": "prod_test1", 17 | "tiers": null, 18 | "tiers_mode": null, 19 | "transform_usage": null, 20 | "trial_period_days": null, 21 | "usage_type": "licensed" 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/tests/unit/fixtures/stripe_prod_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "prod_test1", 3 | "object": "product", 4 | "active": true, 5 | "attributes": [], 6 | "caption": null, 7 | "created": 1567100773, 8 | "deactivate_on": [], 9 | "description": null, 10 | "images": [], 11 | "livemode": false, 12 | "metadata": { 13 | "productSet": "FPN" 14 | }, 15 | "name": "Project Guardian", 16 | "package_dimensions": null, 17 | "shippable": null, 18 | "statement_descriptor": "Firefox Guardian", 19 | "type": "service", 20 | "unit_label": null, 21 | "updated": 1567100794, 22 | "url": null 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/tests/unit/fixtures/stripe_sub_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sub_test1", 3 | "object": "subscription", 4 | "application_fee_percent": null, 5 | "billing": "charge_automatically", 6 | "billing_cycle_anchor": 1567634953, 7 | "billing_thresholds": null, 8 | "cancel_at": null, 9 | "cancel_at_period_end": false, 10 | "canceled_at": null, 11 | "collection_method": "charge_automatically", 12 | "created": 1567634953, 13 | "current_period_end": 1570226953, 14 | "current_period_start": 1567634953, 15 | "customer": "cus_test1", 16 | "days_until_due": null, 17 | "default_payment_method": null, 18 | "default_source": null, 19 | "default_tax_rates": [], 20 | "discount": null, 21 | "ended_at": null, 22 | "items": {}, 23 | "latest_invoice": "in_1FF6f7JNcmPzuWtRv2OaJjBO", 24 | "livemode": false, 25 | "metadata": {}, 26 | "pending_setup_intent": null, 27 | "plan": {}, 28 | "quantity": 1, 29 | "schedule": null, 30 | "start": 1567634953, 31 | "start_date": 1567634953, 32 | "status": "active", 33 | "tax_percent": null, 34 | "trial_end": null, 35 | "trial_start": null 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/tests/unit/test_deployed.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from hub.shared.cfg import CFG 6 | from hub.shared.deployed import get_deployed 7 | 8 | 9 | def test_get_deployed(): 10 | """ 11 | test get_deployed 12 | """ 13 | expect = dict( 14 | DEPLOYED_BY=CFG.DEPLOYED_BY, 15 | DEPLOYED_ENV=CFG.DEPLOYED_ENV, 16 | DEPLOYED_WHEN=CFG.DEPLOYED_WHEN, 17 | ) 18 | actual, rc = get_deployed() 19 | assert rc == 200 20 | assert actual["DEPLOYED_BY"] == expect["DEPLOYED_BY"] 21 | assert actual["DEPLOYED_ENV"] == expect["DEPLOYED_ENV"] 22 | -------------------------------------------------------------------------------- /src/shared/tests/unit/test_headers.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import json 6 | import requests 7 | import responses 8 | 9 | from hub.shared.headers import dump_safe_headers, extract_safe 10 | 11 | CONTENT_TYPE_HEADER_ONLY_HEADERS = {"content-type": "application/json"} 12 | AUTHORIZATION_AND_HOST_ONLY_HEADERS = { 13 | "Authorization": "728d329e-0e86-11e4-a748-0c84dc037c13", 14 | "Host": "127.0.0.1", 15 | } 16 | 17 | 18 | def test_no_headers_provided(): 19 | headers = dump_safe_headers(None) 20 | assert len(headers) == 0 21 | 22 | 23 | def test_safe_extract_with_no_header_available(): 24 | headers = extract_safe(None, "foo") 25 | assert len(headers) == 0 26 | 27 | 28 | def test_safe_extract_with_header_available(): 29 | headers = extract_safe(AUTHORIZATION_AND_HOST_ONLY_HEADERS, "Host") 30 | assert "127.0.0.1" == headers 31 | 32 | 33 | @responses.activate 34 | def test_non_safe_headers_no_provided(): 35 | def request_callback(request): 36 | payload = json.loads(request.body) 37 | resp_body = {"value": sum(payload["numbers"])} 38 | headers = AUTHORIZATION_AND_HOST_ONLY_HEADERS 39 | return (200, headers, json.dumps(resp_body)) 40 | 41 | responses.add_callback( 42 | responses.POST, 43 | "http://dev.fxa.mozilla-subhub.app/plans", 44 | callback=request_callback, 45 | content_type="application/json", 46 | ) 47 | 48 | resp = requests.post( 49 | "http://dev.fxa.mozilla-subhub.app/plans", 50 | json.dumps({"numbers": [1, 2, 3]}), 51 | headers=CONTENT_TYPE_HEADER_ONLY_HEADERS, 52 | ) 53 | 54 | headers = dump_safe_headers(resp.headers) 55 | assert len(headers) == 2 56 | assert responses.calls[0].response.headers["Host"] == "127.0.0.1" 57 | assert responses.calls[0].response.headers["Content-Type"] == "application/json" 58 | -------------------------------------------------------------------------------- /src/shared/tests/unit/test_secrets.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import json 6 | import boto3 7 | 8 | from mockito import when 9 | 10 | from hub.shared.exceptions import SecretStringMissingError 11 | from hub.shared import secrets 12 | 13 | 14 | EXPECTED = { 15 | "STRIPE_API_KEY": "stripe_api_key_fake", 16 | "PAYMENT_API_KEY": "payment_api_key_fake", 17 | "SUPPORT_API_KEY": "support_api_key_fake", 18 | "HUB_API_KEY": "hub_api_key_fake", 19 | "SALESFORCE_BASKET_URI": "salesforce_basket_uri_fake", 20 | "FXA_SQS_URI": "fxa_sqs_uri_fake", 21 | "TOPIC_ARN_KEY": "topic_arn_key_fake", 22 | "BASKET_API_KEY": "basket_api_key_fake", 23 | } 24 | 25 | 26 | class MockSecretsManager: 27 | """ 28 | This is the object for mocking the return values from boto3 for Secrets Manager 29 | """ 30 | 31 | def get_secret_value(SecretId=None, VersionId=None, VersionStage=None): 32 | if SecretId in ("prod/subhub", "stage/subhub", "qa/subhub", "dev/subhub"): 33 | return {"Name": SecretId, "SecretString": json.dumps(EXPECTED)} 34 | return {"Name": SecretId} 35 | 36 | 37 | def test_get_secret(): 38 | """ 39 | mock the boto3 return object and test expected vs actual 40 | """ 41 | when(boto3).client(service_name="secretsmanager").thenReturn(MockSecretsManager) 42 | actual = secrets.get_secret("dev/subhub") 43 | assert actual == EXPECTED 44 | 45 | 46 | def test_get_secret_exception(): 47 | """ 48 | if SecretString is not in returned value, throw exception; this exercises that 49 | """ 50 | when(boto3).client(service_name="secretsmanager").thenReturn(MockSecretsManager) 51 | when(secrets).get_secret("bad_id").thenRaise(SecretStringMissingError) 52 | -------------------------------------------------------------------------------- /src/shared/tests/unit/test_vendor_utils.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from hub.shared import vendor_utils 6 | 7 | 8 | def test_format_brand(): 9 | """ 10 | Given brand is visa 11 | Test that brand is found in brand list and correct value is returned 12 | :return: 13 | """ 14 | brand = "visa" 15 | found_brand = vendor_utils.format_brand(brand) 16 | assert found_brand == "Visa" 17 | 18 | 19 | def test_format_brand_unknown(): 20 | """ 21 | Given brand is not in list 22 | Test that brand is not found in brand list and Unknown is returned 23 | :return: 24 | """ 25 | brand = "test" 26 | found_brand = vendor_utils.format_brand(brand) 27 | assert found_brand == "Unknown" 28 | -------------------------------------------------------------------------------- /src/shared/tests/unit/test_version.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from hub.shared.version import get_version 6 | from hub.shared.cfg import CFG 7 | 8 | 9 | def test_get_version(): 10 | """ 11 | test get_version 12 | """ 13 | expect = dict(BRANCH=CFG.BRANCH, VERSION=CFG.VERSION, REVISION=CFG.REVISION) 14 | actual, rc = get_version() 15 | assert rc == 200 16 | assert actual == expect 17 | -------------------------------------------------------------------------------- /src/shared/tests/unit/utils.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import os 6 | import json 7 | 8 | from flask import Response 9 | from typing import Dict, Any 10 | 11 | from hub.vendor.controller import StripeHubEventPipeline, event_process, view 12 | 13 | CWD = os.path.realpath(os.path.dirname(__file__)) 14 | 15 | 16 | def run_test(filename, cwd=CWD) -> None: 17 | with open(os.path.join(cwd, filename)) as f: 18 | pipeline = StripeHubEventPipeline(json.load(f)) 19 | pipeline.run() 20 | 21 | 22 | def run_view(request) -> None: 23 | view() 24 | 25 | 26 | def run_event_process(event) -> Response: 27 | return event_process(event) 28 | 29 | 30 | class MockSqsClient: 31 | @staticmethod 32 | def list_queues(QueueNamePrefix=None) -> Any: # type: ignore 33 | return {"QueueUrls": ["DevSub"]} 34 | 35 | @staticmethod 36 | def send_message(QueueUrl=None, MessageBody=None) -> Any: # type: ignore 37 | return {"ResponseMetadata": {"HTTPStatusCode": 200}} 38 | 39 | 40 | class MockSnsClient: 41 | @staticmethod 42 | def publish( # type: ignore 43 | Message: dict = None, MessageStructure: str = "json", TopicArn: str = None 44 | ) -> Dict[str, Dict[str, int]]: 45 | return {"ResponseMetadata": {"HTTPStatusCode": 200}} 46 | 47 | 48 | class MockSubhubAccount: 49 | def subhub_account(self) -> None: 50 | pass 51 | 52 | 53 | class MockSubhubUser: 54 | id = "123" 55 | cust_id = "cust_123" 56 | -------------------------------------------------------------------------------- /src/shared/types.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from typing import Any, Dict, List, Tuple 6 | 7 | # API types 8 | JsonDict = Dict[str, Any] 9 | FlaskResponse = Tuple[JsonDict, int] 10 | FlaskListResponse = Tuple[List[JsonDict], int] 11 | -------------------------------------------------------------------------------- /src/shared/utils.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | import uuid 6 | 7 | from shared.cfg import CFG 8 | 9 | 10 | def get_indempotency_key() -> str: 11 | return uuid.uuid4().hex 12 | -------------------------------------------------------------------------------- /src/shared/vendor_utils.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | 6 | def format_brand(brand: str) -> str: 7 | """ 8 | Format brand for emails prior to sending to Salesforce 9 | :param brand: 10 | :return: 11 | """ 12 | brand_list = [ 13 | ("amex", "American Express"), 14 | ("diners", "Diners Club"), 15 | ("discover", "Discover"), 16 | ("jcb", "JCB"), 17 | ("mastercard", "MasterCard"), 18 | ("unionpay", "UnionPay"), 19 | ("visa", "Visa"), 20 | ] 21 | try: 22 | return [item[1] for item in brand_list if item[0] == brand][0] 23 | except IndexError as e: 24 | return "Unknown" 25 | -------------------------------------------------------------------------------- /src/shared/version.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | from shared.types import FlaskResponse 6 | from shared.cfg import CFG 7 | from shared.log import get_logger 8 | 9 | logger = get_logger() 10 | 11 | 12 | def get_version() -> FlaskResponse: 13 | version = dict(BRANCH=CFG.BRANCH, VERSION=CFG.VERSION, REVISION=CFG.REVISION) 14 | logger.debug("version", version=version) 15 | return version, 200 16 | -------------------------------------------------------------------------------- /src/test_requirements.txt: -------------------------------------------------------------------------------- 1 | backoff==1.8.0 2 | docker==4.1.0 3 | jsoncompare==0.1.2 4 | locustio==0.11.0 5 | mock==3.0.5 6 | # requirements for subhub testing to run 7 | # do not add requirements for subhub, src/app_requirements.txt already handles that 8 | mockito==1.1.1 9 | mypy==0.720 10 | patch==1.16 11 | purl==1.5 12 | pyparsing==2.4.0 13 | pytest==5.0.1 14 | pytest-bdd==3.2.1 15 | pytest-cov==2.7.1 16 | pytest-mock==1.10.4 17 | pytest-watch==4.2.0 18 | requests==2.22.0 19 | responses==0.10.6 20 | -------------------------------------------------------------------------------- /terraform/.gitignore: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | 6 | .terraform 7 | terraform.tfstate 8 | terraform.tfstate.* 9 | -------------------------------------------------------------------------------- /terraform/.terraform-version: -------------------------------------------------------------------------------- 1 | 0.12.8 2 | -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | # Terraform 2 | 3 | ## Up and Running 4 | 5 | 1. Bootstrap dependencies 6 | 2. Initialize Terraform 7 | 3. Inspect the Terraform plan 8 | 4. Apply the changes 9 | 10 | ## Bootstrapping 11 | 12 | ## Initialize Terraform 13 | 14 | First, boostrap the dependencies according to: 15 | 1. [global/s3](./global/s3/README.md) 16 | 2. [global/dynamodb](./global/dynamodb/README.md) 17 | 18 | Next initialize Terraform with the following command 19 | 20 | ``` 21 | terraform init -backend-config=backend.hcl 22 | ``` 23 | 24 | ## Inspect the Terraform Plan 25 | 26 | ``` 27 | terraform plan 28 | ``` 29 | 30 | ## Apply the Changes 31 | 32 | ``` 33 | terraform apply -var="USER_TABLE=USER_TABLE_NAME_GOES_HERE" -var="DELETED_USER_TABLE=DELETED_USER_TABLE_NAME_GOES_HERE" -var="EVENT_TABLE=EVENT_TABLE_NAME_GOES_HERE" 34 | ``` 35 | 36 | ## Author(s) 37 | 38 | Stewart Henderson 39 | -------------------------------------------------------------------------------- /terraform/backend.hcl: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | bucket = "dev-terraform764202a65682ec50" 6 | region = "us-west-2" 7 | dynamodb_table = "dev-terraform-state6107a0a78baa8c89" 8 | encrypt = true 9 | -------------------------------------------------------------------------------- /terraform/dynamodb.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | resource "aws_dynamodb_table" "users_table" { 6 | name = "${var.users_table}" 7 | billing_mode = "PROVISIONED" 8 | read_capacity = "${var.users_table_read_capacity}" 9 | write_capacity = "${var.users_table_write_capacity}" 10 | hash_key = "user_id" 11 | attribute { 12 | name = "user_id" 13 | type = "S" 14 | } 15 | tags = var.tags 16 | } 17 | 18 | resource "aws_dynamodb_table" "deleted_users_table" { 19 | name = "${var.deleted_users_table}" 20 | billing_mode = "PROVISIONED" 21 | read_capacity = "${var.deleted_users_table_read_capacity}" 22 | write_capacity = "${var.deleted_users_table_write_capacity}" 23 | hash_key = "user_id" 24 | range_key = "cust_id" 25 | attribute { 26 | name = "user_id" 27 | type = "S" 28 | } 29 | attribute { 30 | name = "cust_id" 31 | type = "S" 32 | } 33 | tags = var.tags 34 | } 35 | 36 | resource "aws_dynamodb_table" "events_table" { 37 | name = "${var.events_table}" 38 | billing_mode = "PROVISIONED" 39 | read_capacity = "${var.events_table_read_capacity}" 40 | write_capacity = "${var.events_table_write_capacity}" 41 | hash_key = "event_id" 42 | attribute { 43 | name = "event_id" 44 | type = "S" 45 | } 46 | tags = var.tags 47 | } 48 | -------------------------------------------------------------------------------- /terraform/global/dynamodb/README.md: -------------------------------------------------------------------------------- 1 | # Terraform Global S3 2 | 3 | This directory provides the bootstrapping of the Terraform state S3 bucket. 4 | 5 | ## Bootstrapping 6 | 7 | Bootstrapping is a 1 time process in which the AWS DynamoDB table is created. The AWS 8 | DynamoDB table is used for lock management. 9 | 10 | ### Terraform Initialization 11 | 12 | You will need to initialize Terraform first. This is done with the following command, 13 | 14 | ``` 15 | terraform init 16 | ``` 17 | 18 | 19 | ### Observe the Plan 20 | 21 | Execute the following plan to understand what Terraform is going to do when an action is 22 | applied. This will tell you if you are in fact, going to create the aforementioned objects. 23 | 24 | ``` 25 | terraform plan 26 | ``` 27 | 28 | ### Apply the Changes 29 | 30 | If you agree with the changes in the above step then merely apply them with the following command. 31 | 32 | ``` 33 | terraform apply 34 | ``` 35 | 36 | After this step is performed, you will get values printed to the terminal that represent the names of the created objects. They are outputted as follows: 37 | 38 | * `terraform_dynamodb_lock_table` 39 | 40 | This value will need to be placed into the top level Terraform directory, at the file 41 | `backend.hcl` in the variable, `dynamodb_table`. 42 | 43 | ## Author(s) 44 | 45 | Stewart Henderson 46 | -------------------------------------------------------------------------------- /terraform/global/dynamodb/main.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | resource "random_id" "terraform_state_dynamo_table_name" { 6 | prefix = "${var.deployed_env}-terraform-state" 7 | byte_length = 8 8 | } 9 | 10 | resource "aws_dynamodb_table" "terraform_locks" { 11 | name = random_id.terraform_state_dynamo_table_name.hex 12 | billing_mode = "PAY_PER_REQUEST" 13 | hash_key = "LockID" 14 | attribute { 15 | name = "LockID" 16 | type = "S" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /terraform/global/dynamodb/outputs.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | output "terraform_dynamodb_lock_table" { 6 | value = aws_dynamodb_table.terraform_locks.name 7 | description = "The name of the DynamoDB table" 8 | } 9 | 10 | output "deployed_at" { 11 | value = formatdate("YYYYMMDDhhmmss", timestamp()) 12 | } 13 | -------------------------------------------------------------------------------- /terraform/global/dynamodb/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | deployed_env = "dev" 6 | -------------------------------------------------------------------------------- /terraform/global/dynamodb/variables.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | variable "deployed_env" { 6 | type = "string" 7 | } 8 | -------------------------------------------------------------------------------- /terraform/global/dynamodb/versions.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | terraform { 6 | required_version = "= 0.12.8" 7 | # https://www.terraform.io/docs/configuration/terraform.html#specifying-required-provider-versions 8 | required_providers { 9 | # For latest releases checkout out the release page on 10 | # Github at: 11 | # https://github.com/terraform-providers/terraform-provider-aws/releases 12 | # For information on this release checkout: 13 | # https://github.com/terraform-providers/terraform-provider-aws/releases/tag/v2.42.0 14 | aws = "= 2.43.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /terraform/global/s3/README.md: -------------------------------------------------------------------------------- 1 | # Terraform Global S3 2 | 3 | This directory provides the bootstrapping of the Terraform state S3 bucket. 4 | 5 | ## Bootstrapping 6 | 7 | Bootstrapping requires the creation of dependent resources that Terraform requires 8 | for both state and lock management. These are covered in the global direction. In 9 | this directory, there are 2 sub directories that cover each of these aspects: 10 | 11 | * `s3`, this directory bootstraps an AWS S3 bucket for the purposes of Terraform state management. 12 | * `dynamodb`, this directory bootstraps an AWS S3 bucket for the purposes of Terraform lock management. 13 | 14 | 15 | ### Terraform Initialization 16 | 17 | You will need to initialize Terraform first. This is done with the following command, 18 | 19 | ``` 20 | terraform init 21 | ``` 22 | 23 | 24 | ### Observe the Plan 25 | 26 | Execute the following plan to understand what Terraform is going to do when an action is 27 | applied. This will tell you if you are in fact, going to create the aforementioned object. 28 | 29 | ``` 30 | terraform plan 31 | ``` 32 | 33 | ### Apply the Changes 34 | 35 | If you agree with the changes in the above step then merely apply them with the following command. 36 | 37 | ``` 38 | terraform apply 39 | ``` 40 | 41 | After this step is performed, you will get values printed to the terminal that represent the names of the created objects. They are outputted as follows: 42 | 43 | * `terraform_s3_id` 44 | 45 | This value will need to be placed into the top level Terraform directory, at the file 46 | `backend.hcl` in the variable, `bucket`. 47 | 48 | ## Author(s) 49 | 50 | Stewart Henderson 51 | -------------------------------------------------------------------------------- /terraform/global/s3/main.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | data "aws_caller_identity" "current" {} 6 | 7 | resource "aws_s3_bucket" "terraform_state" { 8 | bucket = "subhub-state-${data.aws_caller_identity.current.account_id}" 9 | lifecycle { 10 | prevent_destroy = true 11 | } 12 | versioning { 13 | enabled = true 14 | } 15 | server_side_encryption_configuration { 16 | rule { 17 | apply_server_side_encryption_by_default { 18 | sse_algorithm = "AES256" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /terraform/global/s3/outputs.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | output "terraform_s3_id" { 6 | value = aws_s3_bucket.terraform_state.id 7 | description = "The ID of the S3 Terraform state bucket" 8 | } 9 | 10 | output "deployed_at" { 11 | value = formatdate("YYYYMMDDhhmmss", timestamp()) 12 | } 13 | -------------------------------------------------------------------------------- /terraform/global/s3/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | deployed_env = "dev" 6 | -------------------------------------------------------------------------------- /terraform/global/s3/variables.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | variable "deployed_env" { 6 | type = "string" 7 | } 8 | 9 | variable "account_id" { 10 | type = "string" 11 | } 12 | -------------------------------------------------------------------------------- /terraform/global/s3/versions.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | terraform { 6 | required_version = "= 0.12.8" 7 | # https://www.terraform.io/docs/configuration/terraform.html#specifying-required-provider-versions 8 | required_providers { 9 | # For latest releases checkout out the release page on 10 | # Github at: 11 | # https://github.com/terraform-providers/terraform-provider-aws/releases 12 | # For information on this release checkout: 13 | # https://github.com/terraform-providers/terraform-provider-aws/releases/tag/v2.42.0 14 | aws = "= 2.43.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | 6 | # https://www.terraform.io/docs/providers/aws/ 7 | provider "aws" { 8 | region = "${var.region}" 9 | } 10 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | output "deployed_at" { 6 | value = formatdate("YYYYMMDDhhmmss", timestamp()) 7 | } 8 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | variable "aws_region" { 6 | type = "string" 7 | default = "us-west-2" 8 | description = "the region where to provision the stack." 9 | } 10 | 11 | ###################################### 12 | # DynamoDB Configuration 13 | ###################################### 14 | 15 | variable "users_table_read_capacity" { 16 | type = "string" 17 | description = "AWS DynamoDB read capacity setting for the Users table." 18 | default = "5" 19 | } 20 | 21 | variable "users_table_write_capacity" { 22 | type = "string" 23 | description = "AWS DynamoDB read capacity setting for the Users table." 24 | default = "5" 25 | } 26 | 27 | variable "deleted_users_table_read_capacity" { 28 | type = "string" 29 | description = "AWS DynamoDB read capacity setting for the Users table." 30 | default = "5" 31 | } 32 | 33 | variable "deleted_users_table_write_capacity" { 34 | type = "string" 35 | description = "AWS DynamoDB read capacity setting for the Users table." 36 | default = "5" 37 | } 38 | 39 | variable "events_table_read_capacity" { 40 | type = "string" 41 | description = "AWS DynamoDB read capacity setting for the Users table." 42 | default = "5" 43 | } 44 | 45 | variable "events_table_write_capacity" { 46 | type = "string" 47 | description = "AWS DynamoDB write capacity setting for the Users table." 48 | default = "5" 49 | } 50 | 51 | variable "region" { 52 | type = "string" 53 | description = "AWS Region." 54 | default = "us-west-2" 55 | } 56 | 57 | variable "DEPLOYED_ENV" { 58 | type = "string" 59 | default = "dev" 60 | } 61 | 62 | variable "users_table" { 63 | type = "string" 64 | default = "NOT_SET_USER_TABLE" 65 | } 66 | 67 | variable "deleted_users_table" { 68 | type = "string" 69 | default = "NOT_SET_DELETED_USER_TABLE" 70 | } 71 | 72 | variable "events_table" { 73 | type = "string" 74 | default = "NOT_SET_EVENT_TABLE" 75 | } 76 | 77 | # AWS Tags 78 | # The tagging guidelines can be found at 79 | # https://mana.mozilla.org/wiki/pages/viewpage.action?spaceKey=SRE&title=Tagging 80 | variable "tags" { 81 | type = "map" 82 | default = { 83 | name = "subhub" 84 | environment = "" 85 | cost-center = "1440" 86 | project-name = "subhub" 87 | project-desc = "subhub" 88 | project-email = "subhub@mozilla.com" 89 | deployed-env = "" 90 | deploy-method = "terraform" 91 | sources = "test" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /terraform/versions.tf: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | terraform { 6 | required_version = "= 0.12.8" 7 | # https://www.terraform.io/docs/configuration/terraform.html#specifying-required-provider-versions 8 | required_providers { 9 | # For latest releases checkout out the release page on 10 | # Github at: 11 | # https://github.com/terraform-providers/terraform-provider-aws/releases 12 | # For information on this release checkout: 13 | # https://github.com/terraform-providers/terraform-provider-aws/releases/tag/v2.42.0 14 | aws = "= 2.43.0" 15 | } 16 | backend "s3" { 17 | key = "global/s3/terraform.tfstate" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | [tox] 6 | minversion = 3.5.3 7 | envlist = py{37} 8 | skipsdist=true 9 | 10 | [testenv] 11 | ; NOTE: DEPLOYED_ENV is being set in .travis.yml but cannot be passed in as 12 | ; it caused a configuration test breakage. 13 | passenv = 14 | AWS_REGION 15 | CI 16 | EVENT_TABLE 17 | FXA_SQS_URI 18 | LOG_LEVEL 19 | HUB_API_KEY 20 | PAYMENT_API_KEY 21 | SALESFORCE_BASKET_URI 22 | STRIPE_API_KEY 23 | SUPPORT_API_KEY 24 | TRAVIS 25 | TRAVIS_* 26 | USER_TABLE 27 | setenv = 28 | PYTHONDONTWRITEBYTECODE=1 29 | envdir = {toxinidir}/venv 30 | deps = 31 | -r{toxinidir}/src/test_requirements.txt 32 | -r{toxinidir}/src/app_requirements.txt 33 | commands = 34 | ; This is valuable to ignore the symlinked shared components into 35 | ; both the sub and hub modules: 36 | ; `--ignore=src/sub/shared` 37 | ; 38 | ; The test output has been disabled (not captured) here. If this is 39 | ; ever desired again, merely add in `--capture=sys` 40 | ; 41 | ; `--cov-append` is being leveraged as parallel is being specified in the 42 | ; `.coveragerc` file. This may result in out of order results being 43 | ; returned and thus append has been added to each result to form the whole 44 | ; coverage report. 45 | ; 46 | ; `--no-cov-on-fail` provide no code coverage on a failing test run. 47 | py.test --cov-config={toxinidir}/.coveragerc --cov-report term-missing --cov-append --cov-branch --no-cov-on-fail --cov=src/hub -k src/hub --ignore=src/hub/shared 48 | py.test --cov-config={toxinidir}/.coveragerc --cov-report term-missing --cov-append --cov-branch --no-cov-on-fail --cov=src/shared -k src/shared 49 | [pytest] 50 | addopts = --maxfail=6 51 | norecursedirs = docs *.egg-info .git appdir .tox .venv env services 52 | log_format = %(asctime)s %(levelname)s %(message)s 53 | log_date_format = %Y-%m-%d %H:%M:%S 54 | log_level=INFO 55 | 56 | filterwarnings = 57 | ignore::FutureWarning 58 | ignore::DeprecationWarning 59 | --------------------------------------------------------------------------------