├── .baked
├── .curling
├── .gitignore
├── .gitmodules
├── .travis.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── README.md
├── autoclean
├── bin
├── crontab
│ ├── crontab.tpl
│ └── gen-crons.py
├── docker
│ └── supervisor.conf
└── docker_run.sh
├── docker
└── README.md
├── docs
├── Makefile
├── _static
│ ├── .gitkeep
│ └── solitude.svg
├── _templates
│ └── .gitkeep
├── build-github.zsh
├── conf.py
├── index.rst
└── topics
│ ├── auth.rst
│ ├── bango.rst
│ ├── braintree.rst
│ ├── generic.rst
│ ├── overall.rst
│ ├── proxy.rst
│ ├── security.rst
│ ├── services.rst
│ ├── setup.rst
│ └── zippy.rst
├── fabfile.py
├── lib
├── __init__.py
├── bango
│ ├── __init__.py
│ ├── client.py
│ ├── constants.py
│ ├── errors.py
│ ├── forms.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ └── clean_statuses.py
│ ├── models.py
│ ├── serializers.py
│ ├── templates
│ │ └── bango
│ │ │ ├── terms-layout.html
│ │ │ └── terms
│ │ │ └── en-US.html
│ ├── tests
│ │ ├── __init__.py
│ │ ├── samples.py
│ │ ├── test_client.py
│ │ ├── test_commands.py
│ │ ├── test_constants.py
│ │ ├── test_forms.py
│ │ ├── test_notification.py
│ │ ├── test_resources.py
│ │ ├── test_utils_lib.py
│ │ └── utils.py
│ ├── urls.py
│ ├── utils.py
│ ├── views
│ │ ├── __init__.py
│ │ ├── bank.py
│ │ ├── base.py
│ │ ├── billing.py
│ │ ├── event.py
│ │ ├── login.py
│ │ ├── notification.py
│ │ ├── package.py
│ │ ├── premium.py
│ │ ├── product.py
│ │ ├── rating.py
│ │ ├── refund.py
│ │ ├── sbi.py
│ │ └── status.py
│ └── wsdl
│ │ ├── prod
│ │ ├── billing_configuration_service.wsdl
│ │ ├── billing_configuration_v2_0.wsdl
│ │ ├── direct_billing.wsdl
│ │ ├── direct_billing_service.xsd
│ │ ├── mozilla_exporter.wsdl
│ │ └── token_checker.wsdl
│ │ ├── readme.txt
│ │ └── test
│ │ ├── billing_configuration_service.wsdl
│ │ ├── billing_configuration_v2_0.wsdl
│ │ ├── direct_billing.wsdl
│ │ ├── direct_billing_service.xsd
│ │ ├── mozilla_exporter.wsdl
│ │ └── token_checker.wsdl
├── brains
│ ├── __init__.py
│ ├── client.py
│ ├── errors.py
│ ├── forms.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ ├── braintree_config.py
│ │ │ ├── braintree_reset.py
│ │ │ ├── braintree_webhook.py
│ │ │ └── samples
│ │ │ ├── __init__.py
│ │ │ └── webhooks.py
│ ├── models.py
│ ├── serializers.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── live
│ │ │ └── __init__.py
│ │ ├── test_base.py
│ │ ├── test_buyer.py
│ │ ├── test_client.py
│ │ ├── test_customer.py
│ │ ├── test_forms.py
│ │ ├── test_management.py
│ │ ├── test_models.py
│ │ ├── test_paymethod.py
│ │ ├── test_sale.py
│ │ ├── test_subscription.py
│ │ ├── test_token.py
│ │ ├── test_transaction.py
│ │ └── test_webhook.py
│ ├── urls.py
│ ├── views
│ │ ├── __init__.py
│ │ ├── buyer.py
│ │ ├── customer.py
│ │ ├── paymethod.py
│ │ ├── sale.py
│ │ ├── subscription.py
│ │ ├── token.py
│ │ ├── transaction.py
│ │ └── webhook.py
│ └── webhooks.py
├── buyers
│ ├── __init__.py
│ ├── constants.py
│ ├── field.py
│ ├── forms.py
│ ├── models.py
│ ├── serializers.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_api.py
│ │ ├── test_forms.py
│ │ └── test_models.py
│ ├── urls.py
│ └── views.py
├── provider
│ ├── __init__.py
│ ├── bango.py
│ ├── client.py
│ ├── errors.py
│ ├── reference.py
│ ├── serializers.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_bango.py
│ │ ├── test_client.py
│ │ ├── test_reference.py
│ │ ├── test_serializer.py
│ │ └── test_views.py
│ ├── urls.py
│ └── views.py
├── proxy
│ ├── __init__.py
│ ├── constants.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── sellers
│ ├── __init__.py
│ ├── constants.py
│ ├── models.py
│ ├── serializers.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── test_api.py
│ │ └── utils.py
│ ├── urls.py
│ └── views.py
├── services
│ ├── __init__.py
│ ├── resources.py
│ └── tests.py
└── transactions
│ ├── __init__.py
│ ├── constants.py
│ ├── forms.py
│ ├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── log.py
│ ├── models.py
│ ├── serializers.py
│ ├── tests
│ ├── __init__.py
│ ├── test_api.py
│ ├── test_commands.py
│ ├── test_forms.py
│ └── test_models.py
│ ├── urls.py
│ └── views.py
├── logs
└── .keep
├── manage.py
├── migrations
├── 01-noop.sql
├── 02-add-buyers.sql
├── 03-add-sellers.sql
├── 04-create-transactions.sql
├── 05-add-related.sql
├── 06-add-sellers-data.sql
├── 07-add-sellers-bluevia.sql
├── 08-add-source-column.sql
├── 09-add-datetime.sql
├── 10-increase-columns.sql
├── 11-add-product.sql
├── 12-add-pin-to-buyers.sql
├── 13-rename-secret.sql
├── 14-add-seller-bango.sql
├── 15-add-product-bango.sql
├── 16-redo-transactions.sql
├── 17-fix-transactions.sql
├── 18-celery-tasks.sql
├── 19-fix-product-bango.sql
├── 20-fix-transactions.sql
├── 21-fix-seller-product.sql
├── 22-fix-product-bango.sql
├── 23-add-delayable.sql
├── 24-add-pin-confirmed-to-buyer.sql
├── 25-add-active.sql
├── 26-amount-optional.sql
├── 27-add-notes.sql
├── 28-add-reset-to-buyer.sql
├── 29-add-pin-locking.sql
├── 30-add-sbi-expiration.sql
├── 31-add-public-id.sql
├── 32-populate-public-id.py
├── 33-add-unique.sql
├── 34-seller-product-access.sql
├── 35-add-pin-was-locked-to-buyer.sql
├── 36-add-bango-status.sql
├── 37-add-counter-field.sql
├── 38-alter-uid-pay.sql
├── 39-add-transaction-log.sql
├── 40-add-region-carrier.sql
├── 41-add-seller-boku.sql
├── 42-transaction-pay-url.sql
├── 43-remove-merchant-id.sql
├── 44-add-product-boku.sql
├── 45-add-seller-to-transactions.sql
├── 46-remove-unique-constraint-for-boku.sql
├── 47-add-product-reference.sql
├── 48-add-email-to-buyer.sql
├── 49-add-reference-id.sql
├── 50-fix-seller-product-reference.sql
├── 51-make-provider-optional.sql
├── 52-add-error-reason.sql
├── 53-remove-paypal.sql
├── 54-add-braintree-buyer.sql
├── 55-add-payment-method.sql
├── 56-add-subscriptions.sql
├── 57-add-braintree-transaction.sql
├── 58-add-locale.sql
├── 59-buyer-optional.sql
├── 60-fix-subscription.sql
├── 61-bt-subsription-amount.sql
├── 62-authenticated-buyer.sql
├── 63-buyer-email-sig.sql
├── 64-populate-email-hash.py
├── __init__.py
└── schematic_settings.py
├── requirements
├── compiled.txt
├── dev.txt
├── docs.txt
├── prod.txt
└── test.txt
├── samples
├── __init__.py
├── bango-basic.py
├── lib.py
├── readme.txt
└── zippy-basic.py
├── solitude
├── __init__.py
├── authentication.py
├── base.py
├── constants.py
├── errors.py
├── exceptions.py
├── fields.py
├── filter.py
├── locale
│ ├── en_US
│ │ └── LC_MESSAGES
│ │ │ └── messages.po
│ ├── fr
│ │ └── LC_MESSAGES
│ │ │ └── messages.po
│ └── templates
│ │ └── LC_MESSAGES
│ │ └── messages.pot
├── logger.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── push_s3.py
│ │ └── refresh_wsdl.py
├── middleware.py
├── paginator.py
├── processor.py
├── related_fields.py
├── settings
│ ├── __init__.py
│ ├── base.py
│ ├── mock-aes-sample.keys-dist
│ ├── sample.key
│ ├── sites
│ │ ├── __init__.py
│ │ ├── altdev
│ │ │ ├── __init__.py
│ │ │ ├── db.py
│ │ │ └── proxy.py
│ │ ├── dev
│ │ │ ├── __init__.py
│ │ │ ├── db.py
│ │ │ └── proxy.py
│ │ ├── paymentsalt
│ │ │ ├── __init__.py
│ │ │ ├── db.py
│ │ │ └── proxy.py
│ │ ├── prod
│ │ │ ├── __init__.py
│ │ │ ├── db.py
│ │ │ └── proxy.py
│ │ ├── s3dev
│ │ │ ├── __init__.py
│ │ │ ├── db.py
│ │ │ └── proxy.py
│ │ └── stage
│ │ │ ├── __init__.py
│ │ │ ├── db.py
│ │ │ └── proxy.py
│ └── test.py
├── tests
│ ├── __init__.py
│ ├── live.py
│ ├── resources.py
│ ├── test.py
│ ├── test_authentication.py
│ ├── test_error.py
│ ├── test_middleware.py
│ └── test_processor.py
├── urls.py
├── utils.py
└── views.py
└── wsgi
├── __init__.py
├── playdoh.py
└── proxy.py
/.baked:
--------------------------------------------------------------------------------
1 | {
2 | "order": ["stdlib", "django", "3rd", "local", "relative"],
3 | "fallback": "local",
4 | "from_order": {
5 | "local": false
6 | },
7 | "modules": {
8 | "stdlib": [
9 | "abc", "anydbm", "argparse", "array", "ast",
10 | "asynchat", "asyncore", "asyncio", "atexit", "base64", "BaseHTTPServer",
11 | "bisect", "bz2", "calendar", "cgitb", "cmd", "codecs", "collections",
12 | "commands", "compileall", "ConfigParser", "contextlib", "Cookie",
13 | "copy", "cPickle", "cProfile", "cStringIO", "csv", "datetime",
14 | "dbhash", "dbm", "decimal", "difflib", "dircache", "dis", "doctest",
15 | "dumbdbm", "EasyDialogs", "exceptions", "filecmp", "fileinput",
16 | "fnmatch", "fractions", "functools", "gc", "gdbm", "getopt",
17 | "getpass", "gettext", "glob", "grp", "gzip", "hashlib", "heapq",
18 | "hmac", "imaplib", "imp", "inspect", "itertools", "json", "linecache",
19 | "locale", "logging", "mailbox", "math", "mhlib", "mmap",
20 | "multiprocessing", "operator", "optparse", "os", "os.path", "pdb",
21 | "pickle", "pipes", "pkgutil", "platform", "plistlib", "pprint",
22 | "profile", "pstats", "pwd", "pyclbr", "pydoc", "Queue", "random",
23 | "re", "readline", "resource", "rlcompleter", "robotparser", "sched",
24 | "select", "shelve", "shlex", "shutil", "signal", "SimpleXMLRPCServer",
25 | "site", "sitecustomize", "smtpd", "smtplib", "socket", "SocketServer",
26 | "sqlite3", "string", "StringIO", "struct", "subprocess", "sys",
27 | "sysconfig", "tabnanny", "tarfile", "tempfile", "textwrap",
28 | "threading", "time", "timeit", "trace", "traceback", "unittest",
29 | "urllib", "urllib2", "urlparse", "usercustomize", "uuid", "warnings",
30 | "weakref", "webbrowser", "whichdb", "xml", "xml.etree.ElementTree",
31 | "xmlrpclib", "zipfile", "zipimport", "zlib", "posixpath",
32 | "mimetypes", "stat"
33 | ],
34 | "3rd": [
35 | "M2Crypto", "MySQLdb", "aesfield",
36 | "apiclient", "cef",
37 | "celery", "celery_tasktree", "celeryutils", "commonware",
38 | "csp", "dateutil", "django_statsd", "lxml",
39 | "mock", "mozpay", "nose", "oauth2", "oauth2client",
40 | "oauthlib", "requests", "slumber", "suds",
41 | "tastypie", "tastypie_services",
42 | "rest_framework", "django_paranoia", "django_filters",
43 | "commonware.log", "chardet", "cache_nuggets", "mobile_codes",
44 | "braintree", "payments_config"
45 | ],
46 | "relative" : [""],
47 | "django": ["django"]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/.curling:
--------------------------------------------------------------------------------
1 | {
2 | "solitude:2602":
3 | {
4 | "key": "local-curling-client",
5 | "secret": "please change this"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | settings_local.py
2 | settings/local.py
3 | */settings/local.py
4 | *.py[co]
5 | *.sw[po]
6 | .coverage
7 | pip-log.txt
8 | docs/_gh-pages
9 | docker/*
10 | build.py
11 | build
12 | .DS_Store
13 | *-min.css
14 | *-all.css
15 | *-min.js
16 | *-all.js
17 | .noseids
18 | tmp/*
19 | *~
20 | *.mo
21 | *.keys
22 | docs/_build
23 | logs/*
24 | supervisord.pid
25 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor"]
2 | path = vendor
3 | url = git://github.com/mozilla/playdoh-lib.git
4 | [submodule "vendor-local/django-statsd"]
5 | path = vendor-local/django-statsd
6 | url = https://github.com/andymckay/django-statsd.git
7 | [submodule "vendor-local/django-tastypie"]
8 | path = vendor-local/django-tastypie
9 | url = https://github.com/toastdriven/django-tastypie.git
10 | [submodule "vendor-local/django-mysql-aesfield"]
11 | path = vendor-local/django-mysql-aesfield
12 | url = git://github.com/andymckay/django-mysql-aesfield.git
13 | [submodule "vendor-local/raven"]
14 | path = vendor-local/raven
15 | url = git://github.com/dcramer/raven.git
16 | [submodule "vendor-local/simplejson"]
17 | path = vendor-local/simplejson
18 | url = git://github.com/simplejson/simplejson.git
19 | [submodule "vendor-local/pyjwt"]
20 | path = vendor-local/pyjwt
21 | url = git://github.com/rtilder/pyjwt.git
22 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: false
3 | python:
4 | - '2.7'
5 | install:
6 | - pip install --no-deps MySQL-Python==1.2.5 --use-mirrors
7 | - pip install --find-links https://pyrepo.stage.mozaws.net/ peep
8 | - peep install --no-deps -r requirements/test.txt -r requirements/docs.txt --find-links https://pyrepo.stage.mozaws.net/
9 | - peep install --no-deps -r requirements/compiled.txt --find-links https://pyrepo.stage.mozaws.net/travis/
10 | before_script:
11 | - mysql -e 'create database solitude;'
12 | - flake8 . --exclude=./docs/conf.py
13 | script: ./manage.py test --noinput -s && make -C docs/ html linkcheck
14 | notifications:
15 | irc:
16 | channels:
17 | - irc.mozilla.org#amo-bots
18 | on_success: change
19 | on_failure: always
20 | env:
21 | global:
22 | - secure: D4wiZ6EjJOgUf0AYdzSRDP153QEgv8g5jMMmwDYw2Jw7/vVqRddgqAYwAAxuB1d94Iux7BS2mFZzyYRcaOqwNkivu1aIp+pj8M7WCEXH4BxGEHPHM8gsIg6WstuWwO4JuINdwgslEs1D4/PYRq5oVI7O7zxvVzG7DJeQxP/79hw=
23 | - secure: gyV+8RH+6RQeoDbLFcSC9PL2nBYub1hIAZAJxBNwG5Z2Opah42m+b6k5otrFdsFVrQrmgA0D01s9u9Y11Ymf+ruagJa6ZIVP46aBxCRs5boa3QFIWB6l/RocWqJKZA6Z8XihSjeoQ+eXXTihyHUfl+HzltAqUr+WthO5eEGAiNc=
24 | - secure: Qjfu3W5kevg1zI2MlpIngoCt9mltQy7Ko0FrDEsXihSuXtAqsl6TcoJ5e0laYI0bnxWFohJwhWrH+R0+o5H2SnoIzb93u9xC/ytRQQ8zFFANWqflGmKPaxpVSt9hPhBOLVSusVwkPollpTdEr/ou77/qgU7p1CyPmVR6fDhsa88=
25 | matrix:
26 | - LIVE_TESTS=
27 | - LIVE_TESTS=live,!braintree
28 | - LIVE_TESTS=!live,braintree
29 | matrix:
30 | allow_failures:
31 | - env: LIVE_TESTS=!live,braintree
32 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Before contributing code, please note:
2 |
3 | * You agree to license your contributions under the [license](https://github.com/mozilla/solitude/blob/master/LICENSE).
4 | * Ask on the [dev-marketplace mailing list](https://lists.mozilla.org/listinfo/dev-marketplace) or in bugzilla before making changes.
5 | * Follow [PEP8](http://www.python.org/dev/peps/pep-0008/), [jshint](http://www.jshint.com/) and our other [style guide conventions](http://mozweb.readthedocs.org/en/latest/index.html).
6 | * Please write tests and read the docs on [solitude](http://solitude.readthedocs.org/en/latest/) and the [marketplace](http://marketplace.readthedocs.org/en/latest/).
7 |
8 | And thank you for contributing code.
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # This is designed to be run from fig as part of a
2 | # Marketplace development environment.
3 |
4 | # NOTE: this is not provided for production usage.
5 | FROM mozillamarketplace/centos-mysql-mkt:latest
6 |
7 | RUN yum install -y supervisor bash-completion cronie && yum clean all
8 |
9 | ENV IS_DOCKER 1
10 |
11 | # Copy requirements over first to cache the build.
12 | COPY requirements /srv/solitude/requirements
13 |
14 | # Download this securely from pyprepo first.
15 | RUN pip install --no-deps --find-links https://pyrepo.stage.mozaws.net/ peep
16 | RUN peep install \
17 | --no-deps \
18 | -r /srv/solitude/requirements/dev.txt \
19 | -r /srv/solitude/requirements/compiled.txt \
20 | --find-links https://pyrepo.stage.mozaws.net/
21 |
22 | # Ship the source in the container, its up to docker-compose to override it
23 | # if it wants to.
24 | COPY . /srv/solitude
25 | RUN cd /srv/solitude && git show -s --pretty="format:%h" > git-rev.txt
26 |
27 | # Technically this should be in supervisor.conf, if the value is placed there,
28 | # when you enter a bash prompt on the container this value is unset. Meaning
29 | # that tests, dbshell and other really useful commands fail.
30 | #
31 | # To compensate supervisor.conf sets this environment variable to a blank
32 | # string, proving that the solitude proxy can run without this value set.
33 | ENV SOLITUDE_DATABASE mysql://root:@mysql:3306/solitude
34 | EXPOSE 2602
35 |
36 | # Preserve bash history across image updates.
37 | # This works best when you link your local source code
38 | # as a volume.
39 | ENV HISTFILE /srv/solitude/docker/bash_history
40 | # Configure bash history.
41 | ENV HISTSIZE 50000
42 | ENV HISTIGNORE ls:exit:"cd .."
43 | # This prevents dupes but only in memory for the current session.
44 | ENV HISTCONTROL erasedups
45 |
46 | # Add in the cron jobs.
47 | RUN mkdir -p /var/log/solitude/transactions/
48 | RUN python /srv/solitude/bin/crontab/gen-crons.py -w /srv/solitude -p python --dir /var/log | crontab -
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include */*/templates *.*
2 | recursive-include */locale *.*
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://unmaintained.tech/)
2 | [](https://waffle.io/mozilla/solitude)
3 | [](https://travis-ci.org/mozilla/solitude)
4 |
5 | Solitude is the Payment Server for processing marketplace and addons payments.
6 |
7 | For more read our docs: http://readthedocs.org/docs/solitude/en/latest/
8 |
9 | Please note: this project is currently unmaintained and is not (or soon will not) be in active use by Mozilla.
10 |
--------------------------------------------------------------------------------
/autoclean:
--------------------------------------------------------------------------------
1 | find . -name '*.py' -exec baked -i {} \;
2 | find . -name '*.py' -exec autopep8 -i {} \;
3 | flake8 . --exclude=docs,solitude/settings/local.py
4 |
--------------------------------------------------------------------------------
/bin/crontab/crontab.tpl:
--------------------------------------------------------------------------------
1 | #
2 | # %(header)s
3 | #
4 |
5 | MAILTO=amo-developers@mozilla.org
6 |
7 | HOME=/tmp
8 |
9 | # Every 10 minutes run the stats log for today so we can see progress.
10 | */10 * * * * %(django)s log --type=stats --today %(dir)s
11 |
12 | # Once per day, generate stats log for yesterday so that we have a final log.
13 | 05 0 * * * %(django)s log --type=stats %(dir)s
14 |
15 | # Once per day, generate revenue log for monolith for yesterday.
16 | 10 0 * * * %(django)s log --type=revenue %(dir)s
17 |
18 | # Once per day, clean statuses older than BANGO_STATUSES_LIFETIME setting.
19 | 35 0 * * * %(django)s clean_statuses
20 |
21 | MAILTO=root
22 |
--------------------------------------------------------------------------------
/bin/crontab/gen-crons.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | from optparse import OptionParser
4 |
5 | HEADER = '!!AUTO-GENERATED!! Edit bin/crontab/crontab.tpl instead.'
6 | TEMPLATE = open(os.path.join(os.path.dirname(__file__), 'crontab.tpl')).read()
7 |
8 |
9 | def main():
10 | parser = OptionParser()
11 | parser.add_option('-w', '--webapp',
12 | help='Location of web app (required)')
13 | parser.add_option('-u', '--user',
14 | help=('Prefix cron with this user. '
15 | 'Only define for cron.d style crontabs.'))
16 | parser.add_option('-p', '--python', default='/usr/bin/python2.6',
17 | help='Python interpreter to use.')
18 | parser.add_option("-d", "--deprecations", default=False,
19 | help="Show deprecation warnings")
20 | parser.add_option('--dir', default=None,
21 | help='Output log directory')
22 |
23 | (opts, args) = parser.parse_args()
24 |
25 | if not opts.webapp:
26 | parser.error('-w must be defined')
27 |
28 | if not opts.deprecations:
29 | opts.python += ' -W ignore::DeprecationWarning'
30 |
31 | ctx = {'django': 'cd %s; %s manage.py' % (opts.webapp, opts.python)}
32 |
33 | if opts.user:
34 | for k, v in ctx.iteritems():
35 | ctx[k] = '%s %s' % (opts.user, v)
36 |
37 | # Needs to stay below the opts.user injection.
38 | ctx['python'] = opts.python
39 | ctx['header'] = HEADER
40 | ctx['dir'] = ('--dir {}/solitude/transactions/'.format(opts.dir)
41 | if opts.dir else '')
42 |
43 | print TEMPLATE % ctx
44 |
45 |
46 | if __name__ == '__main__':
47 | main()
48 |
--------------------------------------------------------------------------------
/bin/docker/supervisor.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | logfile=/srv/solitude/logs/supervisord.log
3 |
4 | [program:solitude]
5 | command=/bin/bash /srv/solitude/bin/docker_run.sh
6 | directory=/srv/solitude
7 | stopasgroup=true
8 | autostart=true
9 | redirect_stderr=true
10 | stdout_logfile=logs/docker.log
11 | stdout_logfile_maxbytes=1MB
12 | stopsignal=KILL
13 | environment=
14 | SOLITUDE_URL="http://solitude:2602"
15 |
16 | [inet_http_server]
17 | port=9001
18 |
19 | [rpcinterface:supervisor]
20 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
21 |
--------------------------------------------------------------------------------
/bin/docker_run.sh:
--------------------------------------------------------------------------------
1 | # Startup script for running Solitude under Docker.
2 |
3 | # Check for mysql being up and running.
4 | mysqladmin -u root --host mysql_1 --silent --wait=30 ping || exit 1
5 |
6 | # Check database exists. If not create it first.
7 | mysql -u root --host mysql_1 -e 'use solitude;'
8 | if [ $? -ne 0 ]; then
9 | echo "Solitude database doesn't exist. Let's create it"
10 | mysql -u root --host mysql_1 -e 'create database solitude'
11 | fi
12 |
13 | # Lets always run the migrations.
14 | schematic migrations
15 |
16 | if [ "$BRAINTREE_MERCHANT_ID" ]; then
17 | echo "Braintree merchant ID found in environment, running braintree_config."
18 | python manage.py braintree_config
19 | fi
20 |
21 | python manage.py runserver 0.0.0.0:2602
22 |
--------------------------------------------------------------------------------
/docker/README.md:
--------------------------------------------------------------------------------
1 | This directory contains ephemeral artifacts from your running Docker container.
2 |
--------------------------------------------------------------------------------
/docs/_static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/docs/_static/.gitkeep
--------------------------------------------------------------------------------
/docs/_templates/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/docs/_templates/.gitkeep
--------------------------------------------------------------------------------
/docs/build-github.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # A useful build script for projects hosted on github:
4 | # It can build your Sphinx docs and push them straight to your gh-pages branch.
5 |
6 | # Should be run from the docs directory: (cd docs && ./build-github.zsh)
7 |
8 | REPO=$(git config remote.origin.url)
9 | HERE=$(dirname $0)
10 | GH=$HERE/_gh-pages
11 |
12 |
13 | # Checkout the gh-pages branch, if necessary.
14 | if [[ ! -d $GH ]]; then
15 | git clone $REPO $GH
16 | pushd $GH
17 | git checkout -b gh-pages origin/gh-pages
18 | popd
19 | fi
20 |
21 | # Update and clean out the _gh-pages target dir.
22 | pushd $GH
23 | git pull && rm -rf *
24 | popd
25 |
26 | # Make a clean build.
27 | pushd $HERE
28 | make clean dirhtml
29 |
30 | # Move the fresh build over.
31 | cp -r _build/dirhtml/* $GH
32 | pushd $GH
33 |
34 | # Commit.
35 | git add .
36 | git commit -am "gh-pages build on $(date)"
37 | git push origin gh-pages
38 |
39 | popd
40 | popd
41 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | ========================================
2 | Solitude
3 | ========================================
4 |
5 | *Please note:* this project is currently unmaintained and is not (or soon will not) be in active use by Mozilla.
6 |
7 | Solitude is a payments server for processing payments for Mozilla's Marketplace
8 | and Addons site.
9 |
10 | .. figure:: _static/solitude.svg
11 | :align: right
12 | :target: http://www.breweryvivant.com/
13 |
14 | It provides a REST API for processing payments that you would plug into your
15 | site. We've implemented the APIs that we want to use for the Marketplace, not
16 | every API that the provider supports.
17 |
18 | Currently we support:
19 |
20 | * some `Bango `_ APIs
21 | * some `Braintree `_ APIs
22 | * some `Zippy `_ compliance
23 |
24 | In the past PayPal was supported, that has been removed.
25 |
26 | This project is based on **playdoh**. Mozilla's Playdoh is an open source
27 | web application template based on `Django `_.
28 |
29 | This document is available as a `PDF `_.
30 |
31 | Solitude is also a nice tasting beer from `Brewery Vivant `_. The logo is
32 | theirs.
33 |
34 | Contents
35 | --------
36 |
37 | .. toctree::
38 | :maxdepth: 2
39 |
40 | topics/setup.rst
41 | topics/security.rst
42 | topics/overall.rst
43 | topics/auth.rst
44 | topics/generic.rst
45 | topics/bango.rst
46 | topics/braintree.rst
47 | topics/zippy.rst
48 | topics/proxy.rst
49 | topics/services.rst
50 |
51 | Indices and tables
52 | ------------------
53 |
54 | * :ref:`genindex`
55 | * :ref:`modindex`
56 | * :ref:`search`
57 |
--------------------------------------------------------------------------------
/docs/topics/auth.rst:
--------------------------------------------------------------------------------
1 | .. _auth.rst:
2 |
3 | Authentication
4 | ##############
5 |
6 | Most API requests can enforce zero-legged OAuth by having a shared key and
7 | secret on the servers. This allows solitude to check the client sending
8 | requests is allowed to do so. By default, this is `True`::
9 |
10 | REQUIRE_OAUTH = True
11 | CLIENT_OAUTH_KEYS = {
12 | 'marketplace': 'please change this',
13 | 'webpay': 'please change this',
14 | }
15 |
16 | In development, you might want to connect with curl and other tools. For that
17 | alter the `REQUIRE_OAUTH` setting to `False`.
18 |
19 | .. note::
20 |
21 | Service URLs do not require JWT encoding.
22 |
--------------------------------------------------------------------------------
/docs/topics/security.rst:
--------------------------------------------------------------------------------
1 | .. _security:
2 |
3 | Security
4 | ########
5 |
6 | Encryption
7 | ==========
8 |
9 | Currently we use `django-aesfield `_
10 | to provide encryption on key fields. We'd recommend more levels of database
11 | encryption or file system encryption.
12 |
13 | The encryption uses AES to do this.
14 |
15 | Encrypted fields:
16 |
17 | * buyers email
18 | * sellers secret
19 | * bango signature
20 |
21 | The keys per field are mapped in settings. See :ref:`setup.rst` for more.
22 |
23 | Hashed fields
24 | =============
25 |
26 | Fields are
27 |
28 | * buyers pin
29 | * buyers new pin
30 |
31 | Requests
32 | ========
33 |
34 | All requests use OAuth 1.1 which signs the header using a secret key. Requests
35 | must be signed with that key or be rejected.
36 |
--------------------------------------------------------------------------------
/docs/topics/services.rst:
--------------------------------------------------------------------------------
1 | .. _services.rst:
2 |
3 | Services
4 | ########
5 |
6 | These are resources to provide information to clients about the status.
7 |
8 | .. http:get:: /services/request/
9 |
10 | Echoes information back about the requet.
11 |
12 | **Response**
13 |
14 | :param authenticated: the OAuth key used to authenticate.
15 | :status 200: successful.
16 |
17 | .. http:get:: /services/status/
18 |
19 | Returns information about things solitude needs. Useful for nagios.
20 |
21 | **Response**
22 |
23 | Example:
24 |
25 | .. code-block:: json
26 |
27 | {
28 | "meta":
29 | {
30 | "limit": 20,
31 | "next": null,
32 | "offset": 0,
33 | "previous": null,
34 | "total_count": 1
35 | },
36 | "objects":
37 | [{
38 | "cache": true,
39 | "db": true,
40 | "resource_uri": "",
41 | "settings": true
42 | }]
43 | }
44 |
45 | :status 200: successful.
46 | :status 500: theres a problem on the server.
47 |
--------------------------------------------------------------------------------
/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/__init__.py
--------------------------------------------------------------------------------
/lib/bango/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/__init__.py
--------------------------------------------------------------------------------
/lib/bango/errors.py:
--------------------------------------------------------------------------------
1 | class BangoError(Exception):
2 |
3 | def __init__(self, id, message):
4 | self.id = id
5 | self.message = message
6 |
7 | def __str__(self):
8 | return u'%s: %s' % (self.id, self.message)
9 |
10 |
11 | class AuthError(BangoError):
12 |
13 | """We've got the settings wrong on our end."""
14 |
15 |
16 | class BangoAnticipatedError(BangoError):
17 |
18 | """
19 | Something in the data we passed caused an error in the Bango end.
20 |
21 | This error is to denote that the error is going to be raised, but
22 | will be anticipated in some circumstances. This allows it to be caught
23 | and handled appropriately.
24 | """
25 |
26 |
27 | class BangoUnanticipatedError(BangoError):
28 |
29 | """
30 | Something in the data we passed caused Bango to return an error
31 | of not OK.
32 |
33 | This error is to denote that the error is going to be raised, but
34 | will be NOT anticipated. This allows it to be caught
35 | and handled appropriately.
36 | """
37 |
38 |
39 | class BangoImmediateError(Exception):
40 |
41 | """
42 | Something in the data we passed caused an error in Bango and rather
43 | than let it get trapped somewhere, we want to raise this immediately.
44 | """
45 |
46 |
47 | class ProxyError(Exception):
48 |
49 | """The proxy returned something we didn't like."""
50 |
51 |
52 | class ProcessError(Exception):
53 |
54 | def __init__(self, response):
55 | self.response = response
56 |
--------------------------------------------------------------------------------
/lib/bango/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/management/__init__.py
--------------------------------------------------------------------------------
/lib/bango/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/management/commands/__init__.py
--------------------------------------------------------------------------------
/lib/bango/management/commands/clean_statuses.py:
--------------------------------------------------------------------------------
1 | from datetime import date, timedelta
2 | from optparse import make_option
3 |
4 | from django.conf import settings
5 | from django.core.management.base import BaseCommand
6 |
7 | from lib.bango.models import Status
8 |
9 |
10 | class Command(BaseCommand):
11 | help = 'Deletes all statuses with a lifetime greater than the parameter.'
12 | option_list = BaseCommand.option_list + (
13 | make_option(
14 | '--lifetime',
15 | action='store_true',
16 | dest='lifetime',
17 | default=settings.BANGO_STATUSES_LIFETIME,
18 | help=('Set the maximum lifetime in days of cleaned statuses. '
19 | 'Default: %s (BANGO_STATUSES_LIFETIME setting)'
20 | % settings.BANGO_STATUSES_LIFETIME)
21 | ),
22 | )
23 |
24 | def handle(self, *args, **options):
25 | boundary_date = date.today() - timedelta(days=options['lifetime'])
26 | Status.objects.filter(created__lte=boundary_date).delete()
27 |
--------------------------------------------------------------------------------
/lib/bango/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from .constants import STATUS_CHOICES, STATUS_UNKNOWN
4 | from lib.sellers.models import SellerProductBango
5 | from solitude.base import Model
6 |
7 |
8 | class Status(Model):
9 | status = models.IntegerField(choices=STATUS_CHOICES,
10 | default=STATUS_UNKNOWN)
11 | errors = models.TextField()
12 | seller_product_bango = models.ForeignKey(SellerProductBango,
13 | related_name='status')
14 |
15 | class Meta(Model.Meta):
16 | db_table = 'status_bango'
17 |
--------------------------------------------------------------------------------
/lib/bango/templates/bango/terms-layout.html:
--------------------------------------------------------------------------------
1 | {% include terms %}
2 |
3 | SBI Agreement
4 | {{ sbi|linebreaksbr }}
5 |
--------------------------------------------------------------------------------
/lib/bango/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/tests/__init__.py
--------------------------------------------------------------------------------
/lib/bango/tests/test_commands.py:
--------------------------------------------------------------------------------
1 | from datetime import date, timedelta
2 |
3 | from django import test
4 | from django.core.management import call_command
5 |
6 | from nose.tools import eq_
7 |
8 | import utils
9 | from lib.bango.models import Status
10 |
11 |
12 | class TestCleanStatusesCommand(test.TestCase):
13 |
14 | def setUp(self):
15 | """
16 | Generates 3 statuses with different lifetimes of 5, 20 and 35 days
17 | to test both default and custom `lifetime` parameter.
18 | """
19 | sellers = utils.make_sellers()
20 | for i in (5, 20, 35):
21 | status = Status.objects.create(
22 | seller_product_bango=sellers.product_bango,
23 | )
24 | # Work around due to the `auto_now_add` option
25 | status.created = date.today() - timedelta(days=i)
26 | status.save()
27 |
28 | def test_command_call(self):
29 | with self.settings(BANGO_STATUSES_LIFETIME=30):
30 | call_command('clean_statuses')
31 | eq_(Status.objects.all().count(), 2)
32 |
33 | def test_lifetime_parameter(self):
34 | call_command('clean_statuses', **{'lifetime': 10})
35 | eq_(Status.objects.all().count(), 1)
36 |
--------------------------------------------------------------------------------
/lib/bango/tests/test_constants.py:
--------------------------------------------------------------------------------
1 | from ..constants import INVALID, match, OK
2 |
3 |
4 | def test_constants():
5 | assert match('OK', OK)
6 | assert match('INVALID_FOO', INVALID)
7 | assert not match('INVALID', INVALID)
8 |
--------------------------------------------------------------------------------
/lib/bango/tests/test_utils_lib.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tempfile
3 |
4 | from django import test
5 |
6 | from nose.tools import raises
7 |
8 | from lib.bango.utils import sign, terms, terms_directory, verify_sig
9 |
10 |
11 | class TestSigning(test.TestCase):
12 |
13 | def test_sign(self):
14 | sig = sign('123')
15 | assert verify_sig(sig, '123')
16 |
17 | def test_sign_unicode(self):
18 | sig = sign('123')
19 | assert verify_sig(sig, u'123')
20 |
21 | @raises(TypeError)
22 | def test_cannot_sign_non_ascii(self):
23 | sign(u'Ivan Krsti\u0107')
24 |
25 | def test_wrong_key(self):
26 | tmp = tempfile.NamedTemporaryFile(mode='w', delete=False)
27 | tmp.write('some secret')
28 | tmp.close()
29 | self.addCleanup(lambda: os.unlink(tmp.name))
30 | with self.settings(AES_KEYS={'bango:signature': tmp.name}):
31 | sig = sign('123')
32 | assert not verify_sig(sig, '123')
33 |
34 |
35 | class TestTerms(test.TestCase):
36 |
37 | def setUp(self):
38 | self.fr = os.path.join(terms_directory, 'fr.html')
39 |
40 | def tearDown(self):
41 | if os.path.exists(self.fr):
42 | os.remove(self.fr)
43 |
44 | def test_en(self):
45 | assert 'Bango Developer Terms' in terms('sbi')
46 |
47 | def test_fr(self):
48 | with open(self.fr, 'w') as fr:
49 | fr.write('fr')
50 | assert 'fr' in terms('sbi', language='fr')
51 |
52 | def test_fallback(self):
53 | assert 'Bango Developer Terms' in terms('sbi', language='de')
54 |
--------------------------------------------------------------------------------
/lib/bango/tests/utils.py:
--------------------------------------------------------------------------------
1 | import collections
2 |
3 | from lib.sellers.models import (Seller, SellerBango, SellerProduct,
4 | SellerProductBango)
5 |
6 | Sellers = collections.namedtuple('Seller', 'seller bango product')
7 | SellerProducts = collections.namedtuple('SellerProduct',
8 | 'product_bango seller bango product ')
9 |
10 |
11 | def make_no_product(uuid='sample:uuid', bangoid='sample:bangoid'):
12 | seller = Seller.objects.create(uuid=uuid)
13 | bango = SellerBango.objects.create(
14 | seller=seller,
15 | package_id=1,
16 | admin_person_id=3,
17 | support_person_id=3,
18 | finance_person_id=4,
19 | )
20 | product = SellerProduct.objects.create(
21 | seller=seller,
22 | external_id='xyz',
23 | )
24 | return Sellers(seller, bango, product)
25 |
26 |
27 | def make_sellers(uuid='sample:uuid', bangoid='sample:bangoid'):
28 | no = make_no_product(uuid=uuid, bangoid=bangoid)
29 | product_bango = SellerProductBango.objects.create(
30 | seller_product=no.product,
31 | seller_bango=no.bango,
32 | bango_id=bangoid,
33 | )
34 | return SellerProducts(product_bango, *no)
35 |
--------------------------------------------------------------------------------
/lib/bango/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include, patterns, url
2 |
3 | from rest_framework.routers import SimpleRouter
4 |
5 | from lib.bango.views.bank import bank
6 | from lib.bango.views.billing import billing
7 | from lib.bango.views.event import event
8 | from lib.bango.views.login import login
9 | from lib.bango.views.notification import notification
10 | from lib.bango.views.package import PackageViewSet
11 | from lib.bango.views.premium import premium
12 | from lib.bango.views.product import ProductViewSet
13 | from lib.bango.views.rating import rating
14 | from lib.bango.views.refund import RefundViewSet
15 | from lib.bango.views.sbi import sbi
16 | from lib.bango.views.status import DebugViewSet, StatusViewSet
17 |
18 | bango_drf = SimpleRouter()
19 | bango_drf.register('status', StatusViewSet, base_name='status')
20 | bango_drf.register('debug', DebugViewSet, base_name='debug')
21 | bango_drf.register('product', ProductViewSet, base_name='product')
22 | bango_drf.register('package', PackageViewSet, base_name='package')
23 | bango_drf.register('refund', RefundViewSet, base_name='refund')
24 |
25 | urlpatterns = patterns(
26 | '',
27 | url(r'^login/$', login, name='bango.login'),
28 | url(r'^bank/$', bank, name='bank'),
29 | url(r'^premium/$', premium, name='premium'),
30 | url(r'^rating/$', rating, name='rating'),
31 | url(r'^billing/$', billing, name='billing'),
32 | url(r'^sbi/$', sbi, name='sbi'),
33 | url(r'^notification/$', notification, name='notification'),
34 | url(r'^event/$', event, name='event'),
35 | url(r'^', include(bango_drf.urls)),
36 | )
37 |
--------------------------------------------------------------------------------
/lib/bango/utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import hashlib
3 | import hmac
4 | import os
5 |
6 | from django.conf import settings
7 | from django.template.loader import render_to_string
8 |
9 | from aesfield.default import lookup
10 |
11 | terms_directory = 'lib/bango/templates/bango/terms'
12 |
13 |
14 | def sign(msg):
15 | """
16 | Sign a message with a Bango key.
17 | """
18 | if isinstance(msg, unicode):
19 | try:
20 | msg = msg.encode('ascii')
21 | except UnicodeEncodeError, exc:
22 | raise TypeError('Cannot sign a non-ascii message. Error: %s'
23 | % exc)
24 | key = lookup(key='bango:signature')
25 | return hmac.new(key, msg, hashlib.sha256).hexdigest()
26 |
27 |
28 | def verify_sig(sig, msg):
29 | """
30 | Verify the signature of a message using a Bango key.
31 | """
32 | return str(sig) == sign(msg)
33 |
34 |
35 | def terms(sbi, language='en-US'):
36 | """
37 | Look for a file containing the Bango terms, if not present, it will fall
38 | back to the en-US.html file.
39 | """
40 | full = functools.partial(os.path.join, settings.ROOT, terms_directory)
41 | templates = (language + '.html', 'en-US.html')
42 | for template in templates:
43 | if os.path.exists(full(template)):
44 | break
45 |
46 | return render_to_string('bango/terms-layout.html',
47 | {'sbi': sbi, 'terms': full(template)})
48 |
--------------------------------------------------------------------------------
/lib/bango/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/views/__init__.py
--------------------------------------------------------------------------------
/lib/bango/views/bank.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.bango.errors import BangoError, ProcessError
5 | from lib.bango.forms import CreateBankDetailsForm
6 | from lib.bango.serializers import SellerBangoOnly
7 | from lib.bango.views.base import BangoResource
8 |
9 |
10 | @api_view(['POST'])
11 | def bank(request):
12 | view = BangoResource()
13 | view.error_lookup = {
14 | 'INVALID_COUNTRYISO': 'bankAddressIso',
15 | }
16 |
17 | try:
18 | serial, form = view.process(
19 | serial_class=SellerBangoOnly,
20 | form_class=CreateBankDetailsForm,
21 | request=request)
22 | except ProcessError, exc:
23 | return exc.response
24 |
25 | data = form.cleaned_data
26 | data['packageId'] = serial.object['seller_bango'].package_id
27 |
28 | try:
29 | view.client('CreateBankDetails', data)
30 | except BangoError, exc:
31 | return view.client_errors(exc)
32 |
33 | return Response(form.cleaned_data)
34 |
--------------------------------------------------------------------------------
/lib/bango/views/base.py:
--------------------------------------------------------------------------------
1 | from rest_framework.response import Response
2 |
3 | from lib.bango.client import format_client_error, get_client
4 | from lib.bango.errors import (
5 | BangoAnticipatedError, BangoImmediateError, BangoUnanticipatedError,
6 | ProcessError)
7 | from solitude.base import format_form_errors
8 |
9 |
10 | class BangoResource(object):
11 |
12 | def client(self, method, data, raise_on=None, client=None):
13 | """
14 | Client to call the bango client and process errors in a way that
15 | is relevant to the form. If you pass in a list of errors, these will
16 | be treated as errors the callee is going to deal with and will not
17 | be returning ImmediateHttpResponses. Instead the callee will have to
18 | cope with these BangoAnticipatedErrors as appropriate.
19 |
20 | You can optionally pass in a client to override the default.
21 | """
22 | raise_on = raise_on or []
23 | try:
24 | return getattr(client or get_client(), method)(data)
25 | except BangoUnanticipatedError, exc:
26 | # It was requested that the error that was passed in
27 | # was actually anticipated, so let's raise that type of error.
28 | if exc.id in raise_on:
29 | raise BangoAnticipatedError(exc.id, exc.message)
30 |
31 | res = self.client_errors(exc)
32 | raise BangoImmediateError(format_form_errors(res))
33 |
34 | def process(self, serial_class, form_class, request):
35 | form = form_class(request.DATA)
36 | if not form.is_valid():
37 | raise ProcessError(Response(format_form_errors(form), status=400))
38 |
39 | serial = serial_class(data=request.DATA)
40 | if not serial.is_valid():
41 | raise ProcessError(Response(serial.errors, status=400))
42 |
43 | return serial, form
44 |
45 | def client_errors(self, exc):
46 | key = getattr(self, 'error_lookup', {}).get(exc.id, '__all__')
47 | return format_client_error(key, exc)
48 |
49 | def form_errors(self, forms):
50 | return Response(format_form_errors(forms), status=400)
51 |
--------------------------------------------------------------------------------
/lib/bango/views/event.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.bango.forms import EventForm
5 | from lib.bango.views.base import BangoResource
6 | from lib.transactions.constants import STATUSES_INVERTED
7 | from solitude.base import log_cef
8 | from solitude.logger import getLogger
9 |
10 | log = getLogger('s.bango')
11 |
12 |
13 | @api_view(['POST'])
14 | def event(request):
15 | view = BangoResource()
16 | form = EventForm(request.DATA, request_encoding=request.encoding)
17 | if not form.is_valid():
18 | log.info('Event invalid.')
19 | return view.form_errors(form)
20 |
21 | notification = form.cleaned_data['notification']
22 | transaction = form.cleaned_data['transaction']
23 |
24 | if notification['new_status'] != transaction.status:
25 | old_status = transaction.status
26 | transaction.status = notification['new_status']
27 | transaction.save()
28 |
29 | log_cef('Transaction change success', request, severity=7,
30 | cs6Label='old', cs6=STATUSES_INVERTED[old_status],
31 | cs7Label='new', cs7=STATUSES_INVERTED[transaction.status])
32 | log.info('Transaction {0} changed to {1} from {2}'
33 | .format(transaction.pk, transaction.status,
34 | old_status))
35 |
36 | return Response(status=204)
37 |
--------------------------------------------------------------------------------
/lib/bango/views/login.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.bango.errors import BangoError
5 | from lib.bango.forms import GetEmailAddressesForm, GetLoginTokenForm
6 | from lib.bango.views.base import BangoResource
7 |
8 |
9 | @api_view(['POST'])
10 | def login(request):
11 | """
12 | Retrieve package's infos from the package id
13 | to be able to later retrieve the authentication token
14 | given that we do not store any emails/persons ids.
15 | """
16 | view = BangoResource()
17 | form = GetEmailAddressesForm(request.DATA)
18 | if not form.is_valid():
19 | return view.form_errors(form)
20 |
21 | try:
22 | address = view.client('GetEmailAddresses', form.cleaned_data)
23 | except BangoError, exc:
24 | return view.client_errors(exc)
25 |
26 | form = GetLoginTokenForm(data={
27 | 'packageId': form.cleaned_data['packageId'],
28 | 'emailAddress': address.adminEmailAddress,
29 | 'personId': address.adminPersonId,
30 | })
31 | if not form.is_valid():
32 | return view.form_errors(form)
33 |
34 | try:
35 | token = view.client('GetAutoAuthenticationLoginToken',
36 | form.cleaned_data)
37 | except BangoError, exc:
38 | return view.client_errors(exc)
39 |
40 | return Response({
41 | 'authentication_token': token.authenticationToken,
42 | 'person_id': address.adminPersonId,
43 | 'email_address': address.adminEmailAddress,
44 | })
45 |
--------------------------------------------------------------------------------
/lib/bango/views/notification.py:
--------------------------------------------------------------------------------
1 | from django_statsd.clients import statsd
2 | from rest_framework.decorators import api_view
3 | from rest_framework.response import Response
4 |
5 | from lib.bango.constants import CANCEL, OK
6 | from lib.bango.forms import NotificationForm
7 | from lib.bango.views.base import BangoResource
8 | from lib.transactions.constants import (STATUS_CANCELLED, STATUS_COMPLETED,
9 | STATUS_FAILED, STATUSES_INVERTED)
10 | from solitude.base import log_cef
11 | from solitude.logger import getLogger
12 |
13 | log = getLogger('s.bango')
14 |
15 |
16 | @api_view(['POST'])
17 | def notification(request):
18 | view = BangoResource()
19 | form = NotificationForm(request, request.DATA)
20 |
21 | bill_conf_id = form.data.get('billing_config_id')
22 | log.info('Received notification for billing_config_id %r: '
23 | 'bango_response_code: %r; bango_response_message: %r; '
24 | 'bango_trans_id: %r; bango_token: %r; moz_transaction: %r; '
25 | 'amount: %r; currency: %r'
26 | % (bill_conf_id,
27 | form.data.get('bango_response_code'),
28 | form.data.get('bango_response_message'),
29 | form.data.get('bango_trans_id'),
30 | form.data.get('bango_token'),
31 | form.data.get('moz_transaction'),
32 | form.data.get('amount'),
33 | form.data.get('currency')))
34 |
35 | if not form.is_valid():
36 | log.info(u'Notification invalid: %s' % bill_conf_id)
37 | return view.form_errors(form)
38 |
39 | trans = form.cleaned_data['moz_transaction']
40 | states = {OK: ['completed', STATUS_COMPLETED],
41 | CANCEL: ['cancelled', STATUS_CANCELLED]}
42 | message, state = states.get(form.cleaned_data['bango_response_code'],
43 | ['failed', STATUS_FAILED])
44 |
45 | log.info(u'Transaction %s: %s' % (message, trans.uuid))
46 | statsd.incr('bango.notification.%s' % message)
47 | statsd.decr('solitude.pending_transactions')
48 |
49 | log_cef('Transaction change success', request, severity=7,
50 | cs6Label='old', cs6=STATUSES_INVERTED.get(trans.status),
51 | cs7Label='new', cs7=STATUSES_INVERTED.get(state))
52 |
53 | trans.status = state
54 | # This is the id for the actual transaction, useful for refunds.
55 | trans.uid_support = form.cleaned_data['bango_trans_id']
56 | # The price/currency may be empty for error notifications.
57 | trans.amount = form.cleaned_data['amount']
58 | trans.currency = form.cleaned_data['currency']
59 | # Set carrier and region.
60 | if form.cleaned_data.get('network'):
61 | trans.carrier = form.cleaned_data['carrier']
62 | trans.region = form.cleaned_data['region']
63 |
64 | trans.save()
65 | return Response(status=204)
66 |
--------------------------------------------------------------------------------
/lib/bango/views/premium.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.bango.constants import BANGO_ALREADY_PREMIUM_ENABLED
5 | from lib.bango.errors import (
6 | BangoAnticipatedError, BangoError, BangoImmediateError, ProcessError)
7 | from lib.bango.forms import MakePremiumForm
8 | from lib.bango.serializers import SellerProductBangoOnly
9 | from lib.bango.views.base import BangoResource
10 |
11 |
12 | @api_view(['POST'])
13 | def premium(request):
14 | view = BangoResource()
15 | view.error_lookup = {
16 | 'INVALID_COUNTRYISO': 'currencyIso',
17 | }
18 |
19 | try:
20 | serial, form = view.process(
21 | serial_class=SellerProductBangoOnly,
22 | form_class=MakePremiumForm,
23 | request=request)
24 | except ProcessError, exc:
25 | return exc.response
26 |
27 | data = form.cleaned_data
28 | data['bango'] = serial.object['seller_product_bango'].bango_id
29 | if not data['bango']:
30 | # Note: that this error formatting seems quite inconsistent
31 | # with the rest of the errors. Something we should clean up.
32 | # https://github.com/mozilla/solitude/issues/349
33 | raise BangoImmediateError(
34 | {'seller_product_bango':
35 | ['Empty bango_id for: {0}'
36 | .format(serial.object['seller_product_bango'].pk)]}
37 | )
38 |
39 | try:
40 | view.client('MakePremiumPerAccess', data,
41 | raise_on=[BANGO_ALREADY_PREMIUM_ENABLED])
42 | except BangoAnticipatedError, exc:
43 | # This can occur and is expected, will return a 204 instead of
44 | # a 200 to distinguish in the client if you need to.
45 | return Response(status=204)
46 | except BangoError, exc:
47 | return view.client_errors(exc)
48 |
49 | return Response(form.cleaned_data)
50 |
--------------------------------------------------------------------------------
/lib/bango/views/product.py:
--------------------------------------------------------------------------------
1 | from rest_framework.response import Response
2 |
3 | from lib.bango.errors import ProcessError
4 | from lib.bango.forms import CreateBangoNumberForm
5 | from lib.bango.serializers import SellerProductBangoSerializer
6 | from lib.bango.views.base import BangoResource
7 | from lib.sellers.models import SellerProductBango
8 | from solitude.base import NonDeleteModelViewSet
9 |
10 |
11 | class ProductViewSet(NonDeleteModelViewSet, BangoResource):
12 | queryset = SellerProductBango.objects.filter()
13 | serializer_class = SellerProductBangoSerializer
14 | filter_fields = ('seller_product',
15 | 'seller_product__seller',
16 | 'seller_product__external_id')
17 |
18 | def create(self, request, *args, **kw):
19 | try:
20 | serial, form = self.process(
21 | serial_class=SellerProductBangoSerializer,
22 | form_class=CreateBangoNumberForm,
23 | request=request)
24 | except ProcessError, exc:
25 | return exc.response
26 |
27 | data = form.cleaned_data
28 | data['packageId'] = serial.object.seller_bango.package_id
29 |
30 | resp = self.client('CreateBangoNumber', data)
31 |
32 | product = SellerProductBango.objects.create(
33 | seller_bango=serial.object.seller_bango,
34 | seller_product=serial.object.seller_product,
35 | bango_id=resp.bango,
36 | )
37 |
38 | serializer = SellerProductBangoSerializer(product)
39 | return Response(serializer.data, status=201)
40 |
--------------------------------------------------------------------------------
/lib/bango/views/rating.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.bango.errors import BangoError, BangoImmediateError, ProcessError
5 | from lib.bango.forms import UpdateRatingForm
6 | from lib.bango.serializers import SellerProductBangoOnly
7 | from lib.bango.views.base import BangoResource
8 |
9 |
10 | @api_view(['POST'])
11 | def rating(request):
12 | view = BangoResource()
13 | view.error_lookup = {
14 | 'INVALID_RATING': 'rating',
15 | 'INVALID_RATING_SCHEME': 'ratingScheme',
16 | }
17 | try:
18 | serial, form = view.process(
19 | serial_class=SellerProductBangoOnly,
20 | form_class=UpdateRatingForm,
21 | request=request)
22 | except ProcessError, exc:
23 | return exc.response
24 |
25 | data = form.cleaned_data
26 | data['bango'] = serial.object['seller_product_bango'].bango_id
27 | if not data['bango']:
28 | # Note: that this error formatting seems quite inconsistent
29 | # with the rest of the errors. Something we should clean up.
30 | raise BangoImmediateError(
31 | {'seller_product_bango':
32 | ['Empty bango_id for: {0}'
33 | .format(serial.object['seller_product_bango'].pk)]})
34 |
35 | try:
36 | view.client('UpdateRating', data)
37 | except BangoError, exc:
38 | return view.client_errors(exc)
39 |
40 | return Response(form.cleaned_data)
41 |
--------------------------------------------------------------------------------
/lib/bango/views/sbi.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.bango.constants import SBI_ALREADY_ACCEPTED
5 | from lib.bango.errors import BangoAnticipatedError
6 | from lib.bango.serializers import EasyObject, SBISerializer, SellerBangoOnly
7 | from lib.bango.utils import terms
8 | from lib.bango.views.base import BangoResource
9 |
10 |
11 | @api_view(['POST', 'GET'])
12 | def sbi(request):
13 | view = BangoResource()
14 | if request.method.upper() == 'GET':
15 | return sbi_get(view, request)
16 | return sbi_post(view, request)
17 |
18 |
19 | def sbi_post(view, request):
20 | serial = SellerBangoOnly(data=request.DATA)
21 | if not serial.is_valid():
22 | return Response(serial.errors, status=400)
23 |
24 | data = {'packageId': serial.object['seller_bango'].package_id}
25 | try:
26 | res = view.client('AcceptSBIAgreement', data,
27 | raise_on=[SBI_ALREADY_ACCEPTED])
28 | except BangoAnticipatedError, exc:
29 | if exc.id != SBI_ALREADY_ACCEPTED:
30 | raise
31 |
32 | res = view.client('GetAcceptedSBIAgreement', data)
33 | seller_bango = serial.object['seller_bango']
34 | seller_bango.sbi_expires = res.sbiAgreementExpires
35 | seller_bango.save()
36 |
37 | obj = EasyObject(
38 | text='',
39 | valid=None,
40 | accepted=res.acceptedSBIAgreement,
41 | expires=res.sbiAgreementExpires
42 | )
43 | return Response(SBISerializer(obj).data)
44 |
45 |
46 | def sbi_get(view, request):
47 | serial = SellerBangoOnly(data=request.DATA)
48 | if not serial.is_valid():
49 | return Response(serial.errors, status=400)
50 |
51 | data = {'packageId': serial.object['seller_bango'].package_id}
52 | res = view.client('GetSBIAgreement', data)
53 | obj = EasyObject(
54 | text=terms(res.sbiAgreement),
55 | valid=res.sbiAgreementValidFrom,
56 | accepted=None,
57 | expires=None
58 | )
59 | return Response(SBISerializer(obj).data)
60 |
--------------------------------------------------------------------------------
/lib/bango/wsdl/prod/billing_configuration_service.wsdl:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/bango/wsdl/readme.txt:
--------------------------------------------------------------------------------
1 | A cached version of the WSDL. To update run:
2 |
3 | ./manage.py refresh_wsdl
4 |
5 | To edit WSDL URLs, see solitude/management/commands/refresh_wsdl.py
6 |
--------------------------------------------------------------------------------
/lib/brains/__init__.py:
--------------------------------------------------------------------------------
1 | # Called brains, because braintree is a python module for interacting with
2 | # braintree and was already taken.
3 |
4 | # Potential work around for https://bugs.python.org/issue7980,
5 | # Braintree uses strptime in their library and I was seeing intermittent
6 | # errors on parsing webhooks:
7 | #
8 | # File "...braintree/util/parser.py", line 42, in __convert_to_datetime
9 | # return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
10 | # AttributeError: _strptime
11 |
12 | import _strptime # noqa
13 |
--------------------------------------------------------------------------------
/lib/brains/client.py:
--------------------------------------------------------------------------------
1 | from urlparse import urlparse
2 |
3 | from django.conf import settings
4 | from django.core.exceptions import ImproperlyConfigured
5 |
6 | import braintree
7 | from django_statsd.clients import statsd
8 |
9 | from solitude.logger import getLogger
10 |
11 | log = getLogger('s.brains')
12 |
13 |
14 | class AuthEnvironment(braintree.environment.Environment):
15 |
16 | def __init__(self, real):
17 | self._url = urlparse(settings.BRAINTREE_PROXY)
18 | self._real = real
19 |
20 | super(AuthEnvironment, self).__init__(
21 | self._url.hostname, self._url.port, '',
22 | self._url.scheme == 'https', None)
23 |
24 |
25 | AuthSandbox = AuthEnvironment(braintree.environment.Environment.Sandbox)
26 | AuthProduction = AuthEnvironment(braintree.environment.Environment.Production)
27 |
28 |
29 | class Http(braintree.util.http.Http):
30 |
31 | def http_do(self, verb, path, headers, body):
32 | # Tell solitude-auth where we really want this request to go to.
33 | headers['x-solitude-service'] = self.environment._real.base_url + path
34 | # Set the URL of the request to point to the auth server.
35 | path = self.environment._url.path
36 |
37 | with statsd.timer('solitude.braintree.api'):
38 | status, text = super(Http, self).http_do(verb, path, headers, body)
39 | statsd.incr('solitude.braintree.response.{0}'.format(status))
40 | return status, text
41 |
42 |
43 | def get_client():
44 | """
45 | Use this to get the right client and communicate with Braintree.
46 | """
47 | environments = {
48 | 'sandbox': AuthSandbox,
49 | 'production': AuthProduction,
50 | }
51 |
52 | if not settings.BRAINTREE_PROXY:
53 | raise ImproperlyConfigured('BRAINTREE_PROXY must be set.')
54 |
55 | if not settings.BRAINTREE_MERCHANT_ID:
56 | raise ImproperlyConfigured('BRAINTREE_MERCHANT_ID must be set.')
57 |
58 | braintree.Configuration.configure(
59 | environments[settings.BRAINTREE_ENVIRONMENT],
60 | settings.BRAINTREE_MERCHANT_ID,
61 | 'public key added by solitude-auth',
62 | 'private key added by solitude-auth',
63 | http_strategy=Http
64 | )
65 | return braintree
66 |
--------------------------------------------------------------------------------
/lib/brains/errors.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | from django.core.exceptions import NON_FIELD_ERRORS
4 |
5 | from solitude.errors import ErrorFormatter
6 | from solitude.logger import getLogger
7 |
8 | log = getLogger('s.brains')
9 |
10 |
11 | class MockError(Exception):
12 |
13 | """
14 | An attempt was made to use the mock, without a corresponding entry
15 | in the mocks dictionary.
16 | """
17 |
18 |
19 | class BraintreeFormatter(ErrorFormatter):
20 |
21 | def format(self):
22 | errors = defaultdict(list)
23 | for error in self.error.result.errors.deep_errors:
24 | errors[error.attribute].append({
25 | 'code': error.code,
26 | 'message': error.message
27 | })
28 |
29 | # If there's not a verification object,
30 | # there will be a transaction object or neither.
31 | error = (self.error.result.credit_card_verification
32 | or self.error.result.transaction)
33 | if error:
34 | log.debug('Processing error: {}'.format(object))
35 |
36 | if error.status.startswith('gateway'):
37 | field = NON_FIELD_ERRORS
38 | # I think these are two cases we care about
39 | # http://bit.ly/1FbxYCE
40 | if error.cvv_response_code in ['N', 'U']:
41 | field = 'cvv'
42 |
43 | errors[field].append({
44 | 'code': error.gateway_rejection_reason,
45 | # There is no matching gateway_rejection_text.
46 | 'message': self.error.result.message
47 | })
48 |
49 | # This covers JCB (failed) and all others (processor declined)
50 | elif error.status.startswith(('processor', 'failed')):
51 | errors[NON_FIELD_ERRORS].append({
52 | 'code': error.processor_response_code,
53 | 'message': error.processor_response_text,
54 | })
55 |
56 | # If we haven't found anything fall back to grabbing the message
57 | # at least.
58 | if not errors:
59 | errors[NON_FIELD_ERRORS].append({
60 | 'code': 'unknown',
61 | 'message': self.error.result.message
62 | })
63 |
64 | return {'braintree': dict(errors)}
65 |
66 |
67 | class BraintreeResultError(Exception):
68 |
69 | """
70 | When an error occurs in the result that is not a standard error.
71 | """
72 | status_code = 422
73 | formatter = BraintreeFormatter
74 |
75 | def __init__(self, result):
76 | self.result = result
77 |
--------------------------------------------------------------------------------
/lib/brains/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/management/__init__.py
--------------------------------------------------------------------------------
/lib/brains/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/management/commands/__init__.py
--------------------------------------------------------------------------------
/lib/brains/management/commands/braintree_reset.py:
--------------------------------------------------------------------------------
1 | from optparse import make_option
2 |
3 | from django.core.management.base import BaseCommand, CommandError
4 |
5 | from lib.brains.models import BraintreePaymentMethod, BraintreeSubscription
6 | from lib.transactions.models import Transaction
7 |
8 |
9 | class Command(BaseCommand):
10 | help = 'Clean up braintree related data for development.'
11 | option_list = BaseCommand.option_list + (
12 | make_option(
13 | '--clear-subscriptions', action='store_true',
14 | dest='clear_subscriptions',
15 | help='Remove all subscription data'
16 | ),
17 | make_option(
18 | '--clear-transactions', action='store_true',
19 | dest='clear_transactions',
20 | help='Remove all transaction data'
21 | ),
22 | make_option(
23 | '--clear-paymethods', action='store_true',
24 | dest='clear_paymethods',
25 | help=('Remove all saved payment methods. This also removes data '
26 | 'tied to the payment methods.')
27 | ),
28 | )
29 |
30 | def handle(self, *args, **options):
31 | did_something = False
32 |
33 | if options['clear_subscriptions']:
34 | did_something = True
35 | self._clear(BraintreeSubscription)
36 | if options['clear_paymethods']:
37 | did_something = True
38 | self._clear(BraintreePaymentMethod)
39 | if options['clear_transactions']:
40 | did_something = True
41 | self._clear(Transaction)
42 |
43 | if not did_something:
44 | raise CommandError('Nothing to do. Try specifying some options.')
45 |
46 | def _clear(self, model):
47 | count = model.objects.count()
48 | model.objects.all().delete()
49 | print '{} objects deleted: {}'.format(model.__name__, count)
50 |
--------------------------------------------------------------------------------
/lib/brains/management/commands/samples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/management/commands/samples/__init__.py
--------------------------------------------------------------------------------
/lib/brains/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/tests/__init__.py
--------------------------------------------------------------------------------
/lib/brains/tests/live/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/tests/live/__init__.py
--------------------------------------------------------------------------------
/lib/brains/tests/test_buyer.py:
--------------------------------------------------------------------------------
1 | from django.core.urlresolvers import reverse
2 |
3 | from nose.tools import eq_
4 |
5 | from lib.brains.tests.base import BraintreeTest, create_braintree_buyer
6 |
7 |
8 | class TestCustomer(BraintreeTest):
9 |
10 | def setUp(self):
11 | self.url = reverse('braintree:mozilla:buyer-list')
12 |
13 | def test_allowed(self):
14 | self.allowed_verbs(self.url, ['get'])
15 |
16 | def test_patch_ok(self):
17 | buyer, braintree_buyer = create_braintree_buyer()
18 | url = reverse('braintree:mozilla:buyer-detail',
19 | kwargs={'pk': braintree_buyer.pk})
20 | self.client.patch(url, {'active': False})
21 | res = self.client.get(url)
22 | eq_(res.json['braintree_id'], 'sample:id')
23 |
24 | def test_patch_readonly(self):
25 | buyer, braintree_buyer = create_braintree_buyer()
26 | url = reverse('braintree:mozilla:buyer-detail',
27 | kwargs={'pk': braintree_buyer.pk})
28 | self.client.patch(url, {'active': False, 'braintree_id': 'foo'})
29 | res = self.client.get(url)
30 | eq_(res.json['braintree_id'], 'sample:id')
31 |
32 | def test_lookup(self):
33 | create_braintree_buyer()
34 | buyer, braintree_buyer = create_braintree_buyer(braintree_id='f:id')
35 | res = self.client.get(self.url, {'buyer': buyer.pk})
36 | eq_(res.json['meta']['total_count'], 1)
37 | eq_(res.json['objects'][0]['id'], braintree_buyer.pk)
38 |
--------------------------------------------------------------------------------
/lib/brains/tests/test_client.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ImproperlyConfigured
2 |
3 | from lib.brains.client import get_client, Http
4 | from lib.brains.tests.base import BraintreeTest
5 |
6 |
7 | class TestClient(BraintreeTest):
8 |
9 | def test_normal(self):
10 | with self.settings(BRAINTREE_PRIVATE_KEY='test-key'):
11 | assert isinstance(
12 | get_client().Configuration.instantiate()._http_strategy,
13 | Http)
14 |
15 | def test_missing(self):
16 | with self.settings(BRAINTREE_MERCHANT_ID=''):
17 | with self.assertRaises(ImproperlyConfigured):
18 | get_client()
19 |
20 | def test_missing_proxy(self):
21 | with self.settings(BRAINTREE_PROXY='', BRAINTREE_MERCHANT_ID='x'):
22 | with self.assertRaises(ImproperlyConfigured):
23 | get_client()
24 |
--------------------------------------------------------------------------------
/lib/brains/tests/test_customer.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from django.core.urlresolvers import reverse
4 |
5 | from braintree.customer import Customer
6 | from braintree.customer_gateway import CustomerGateway
7 | from braintree.successful_result import SuccessfulResult
8 | from nose.tools import eq_, ok_
9 |
10 | from lib.brains.models import BraintreeBuyer
11 | from lib.brains.tests.base import (
12 | BraintreeTest, create_braintree_buyer, create_buyer, error)
13 |
14 |
15 | def customer(**kw):
16 | customer = {
17 | 'id': 'customer-id',
18 | 'created_at': datetime.now(),
19 | 'updated_at': datetime.now()
20 | }
21 | customer.update(**kw)
22 | return Customer(None, customer)
23 |
24 |
25 | def successful_customer(**kw):
26 | return SuccessfulResult({'customer': customer(**kw)})
27 |
28 |
29 | class TestCustomer(BraintreeTest):
30 | gateways = {'customer': CustomerGateway}
31 |
32 | def setUp(self):
33 | super(TestCustomer, self).setUp()
34 | self.url = reverse('braintree:customer')
35 |
36 | def test_buyer_doesnt_exist(self):
37 | res = self.client.post(self.url, data={'uuid': 'nope'})
38 | eq_(res.status_code, 422)
39 | eq_(self.mozilla_error(res.json, 'uuid'), ['does_not_exist'])
40 |
41 | def test_braintree_buyer_exists(self):
42 | buyer, braintree_buyer = create_braintree_buyer()
43 | res = self.client.post(self.url, data={'uuid': buyer.uuid})
44 | eq_(res.status_code, 422)
45 | eq_(self.mozilla_error(res.json, 'uuid'), ['already_exists'])
46 |
47 | def test_ok(self):
48 | self.mocks['customer'].create.return_value = successful_customer()
49 |
50 | buyer = create_buyer()
51 | res = self.client.post(self.url, data={'uuid': buyer.uuid})
52 | eq_(res.status_code, 201)
53 |
54 | braintree_buyer = BraintreeBuyer.objects.get()
55 | eq_(res.json['mozilla']['resource_pk'], braintree_buyer.pk)
56 | eq_(res.json['mozilla']['braintree_id'], 'customer-id')
57 | eq_(res.json['braintree']['id'], 'customer-id')
58 |
59 | def test_error(self):
60 | self.mocks['customer'].create.return_value = error()
61 |
62 | buyer = create_buyer()
63 | res = self.client.post(self.url, data={'uuid': buyer.uuid})
64 |
65 | ok_(not BraintreeBuyer.objects.exists())
66 | eq_(res.status_code, 422)
67 |
68 | def test_no_uuid(self):
69 | res = self.client.post(self.url, data={})
70 | eq_(res.status_code, 422)
71 | eq_(self.mozilla_error(res.json, 'uuid'), ['required'])
72 |
--------------------------------------------------------------------------------
/lib/brains/tests/test_management.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import CommandError
2 |
3 | from braintree.plan import Plan
4 | from braintree.plan_gateway import PlanGateway
5 | from nose.tools import eq_, raises
6 |
7 | from lib.brains.client import get_client
8 | from lib.brains.management.commands import braintree_config as config
9 | from lib.brains.management.commands.braintree_config import (
10 | BraintreePlanDoesNotExist
11 | )
12 | from lib.brains.tests.base import BraintreeTest
13 |
14 |
15 | class TestManagement(BraintreeTest):
16 | gateways = {'plans': PlanGateway}
17 |
18 | def test_created(self):
19 | seller = config.get_or_create_seller('uuid:fxa')
20 | same_seller = config.get_or_create_seller('uuid:fxa')
21 | eq_(seller, same_seller)
22 |
23 | def test_created_product(self):
24 | seller = config.get_or_create_seller('uuid:fxa')
25 | seller_product = config.get_or_create_seller_product(
26 | external_id='some:product:uuid:concrete',
27 | public_id='some:product',
28 | seller=seller)
29 | eq_(seller_product, seller.product.get())
30 |
31 | def get_plans(self, plan=None):
32 | # Note price is a string not a decimal or something useful:
33 | # https://github.com/braintree/braintree_python/issues/52
34 | plans = [
35 | Plan(None, {
36 | 'billing_day_of_month': None,
37 | 'id': 'mozilla-concrete-mortar',
38 | 'price': '1',
39 | 'trial_period': None
40 | }),
41 | Plan(None, {
42 | 'billing_day_of_month': None,
43 | 'id': 'mozilla-concrete-brick',
44 | 'price': '10',
45 | 'trial_period': None
46 | })
47 | ]
48 | if plan:
49 | plans.append(plan)
50 | self.mocks['plans'].all.return_value = plans
51 | return config.get_plans(get_client())
52 |
53 | def test_plans(self):
54 | eq_(self.get_plans().keys(),
55 | [u'mozilla-concrete-mortar', u'mozilla-concrete-brick'])
56 |
57 | @raises(BraintreePlanDoesNotExist)
58 | def test_product_missing(self):
59 | config.product_exists(self.get_plans(), 'nope', 1)
60 |
61 | @raises(CommandError)
62 | def test_price_wrong(self):
63 | config.product_exists(self.get_plans(), 'mozilla-concrete-brick', 1)
64 |
65 | def test_plan_ok(self):
66 | config.product_exists(self.get_plans(), 'mozilla-concrete-brick', 10)
67 |
68 | @raises(CommandError)
69 | def test_trial_period(self):
70 | plan = Plan(None, {
71 | 'billing_day_of_month': None,
72 | 'id': 'trial',
73 | 'price': '1',
74 | 'trial_period': '1'
75 | })
76 | config.product_exists(self.get_plans(plan), 'trial', 1)
77 |
78 | @raises(CommandError)
79 | def test_billing_day_of_month(self):
80 | plan = Plan(None, {
81 | 'billing_day_of_month': 1,
82 | 'id': 'day',
83 | 'price': '1',
84 | 'trial_period': None
85 | })
86 | config.product_exists(self.get_plans(plan), 'day', 1)
87 |
--------------------------------------------------------------------------------
/lib/brains/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from braintree.payment_method_gateway import PaymentMethodGateway
2 | from braintree.subscription_gateway import SubscriptionGateway
3 | from nose.tools import eq_
4 |
5 | from lib.brains.tests.base import BraintreeTest, create_subscription
6 | from lib.brains.tests.test_paymethod import successful_method
7 | from lib.brains.tests.test_subscription import (
8 | create_method_all, successful_subscription)
9 |
10 |
11 | class TestClose(BraintreeTest):
12 | gateways = {
13 | 'pay': PaymentMethodGateway,
14 | 'sub': SubscriptionGateway,
15 | }
16 |
17 | def setUp(self):
18 | super(TestClose, self).setUp()
19 | self.method, self.product = create_method_all()
20 | self.buyer = self.method.braintree_buyer.buyer
21 | self.sub = create_subscription(self.method, self.product)
22 |
23 | def test_no_buyer(self):
24 | self.buyer.delete()
25 | self.buyer.close()
26 |
27 | def test_close(self):
28 | self.mocks['pay'].delete.return_value = successful_method()
29 | self.mocks['sub'].cancel.return_value = successful_subscription()
30 |
31 | self.buyer.close()
32 |
33 | self.mocks['pay'].delete.assert_called_with(self.method.provider_id)
34 | self.mocks['sub'].cancel.assert_called_with(self.sub.provider_id)
35 |
36 | eq_(self.method.reget().active, False)
37 | eq_(self.sub.reget().active, False)
38 |
39 | def test_listens_signal(self):
40 | self.mocks['pay'].delete.return_value = successful_method()
41 | self.mocks['sub'].cancel.return_value = successful_subscription()
42 |
43 | self.buyer.close_signal.send(
44 | buyer=self.buyer, sender=self.buyer.__class__)
45 |
46 | def test_inactive_method(self):
47 | # If a method is inactive, then we still go and call cancel on
48 | # the subscription, just in case solitude is out of sync.
49 | self.method.active = False
50 | self.method.save()
51 | self.mocks['sub'].cancel.return_value = successful_subscription()
52 |
53 | self.buyer.close()
54 |
55 | self.mocks['sub'].cancel.assert_called_with(self.sub.provider_id)
56 |
57 | def test_inactive_subscription(self):
58 | self.sub.active = False
59 | self.sub.save()
60 |
61 | self.mocks['pay'].delete.return_value = successful_method()
62 |
63 | self.buyer.close()
64 | # self.mocks['sub'] not called, no need test the BraintreeTest
65 | # setup deals with this.
66 |
--------------------------------------------------------------------------------
/lib/brains/tests/test_token.py:
--------------------------------------------------------------------------------
1 | from django.core.urlresolvers import reverse
2 |
3 | from braintree.client_token_gateway import ClientTokenGateway
4 | from nose.tools import eq_
5 |
6 | from lib.brains.tests.base import BraintreeLiveTestCase, BraintreeTest
7 |
8 |
9 | class TestToken(BraintreeTest):
10 | gateways = {'client': ClientTokenGateway}
11 |
12 | def test_token(self):
13 | self.mocks['client'].generate.return_value = 'a-sample-token'
14 | res = self.client.post(reverse('braintree:token.generate'))
15 | eq_(res.json['token'], 'a-sample-token')
16 |
17 |
18 | class TestLiveToken(BraintreeLiveTestCase):
19 |
20 | def test_token(self):
21 | res = self.request.by_url('/braintree/token/generate/').post('')
22 | assert res['token']
23 |
--------------------------------------------------------------------------------
/lib/brains/tests/test_transaction.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 | from urllib import urlencode
3 |
4 | from django.core.urlresolvers import reverse
5 |
6 | from nose.tools import eq_
7 |
8 | from lib.brains.models import BraintreeTransaction
9 | from lib.brains.tests.base import (
10 | create_braintree_buyer, create_method, create_seller, create_subscription)
11 | from lib.transactions.models import Transaction
12 | from solitude.base import APITest
13 |
14 |
15 | class TestBraintreeTransaction(APITest):
16 |
17 | def setUp(self):
18 | self.buyer, self.braintree_buyer = create_braintree_buyer()
19 | self.method = create_method(self.braintree_buyer)
20 | self.seller, self.seller_product = create_seller()
21 | self.sub = create_subscription(self.method, self.seller_product)
22 | self.url = reverse('braintree:mozilla:transaction-list')
23 | self.transaction = Transaction.objects.create(uuid='some:uid',
24 | buyer=self.buyer)
25 | super(TestBraintreeTransaction, self).setUp()
26 |
27 | def test_allowed(self):
28 | self.allowed_verbs(self.url, ['get'])
29 |
30 | def create(self, **attributes):
31 | final_attributes = dict(
32 | paymethod=self.method,
33 | subscription=self.sub,
34 | transaction=self.transaction,
35 | billing_period_end_date=datetime.today() + timedelta(days=29),
36 | billing_period_start_date=datetime.today(),
37 | kind='sample',
38 | next_billing_date=datetime.today() + timedelta(days=30),
39 | next_billing_period_amount='10',
40 | )
41 | final_attributes.update(attributes)
42 | return BraintreeTransaction.objects.create(**final_attributes)
43 |
44 | def get(self, **query):
45 | return self.client.get('{}?{}'.format(self.url, urlencode(query)))
46 |
47 | def test_get_transaction_by_pk(self):
48 | obj = self.create()
49 | eq_(self.client.get(obj.get_uri()).json['resource_pk'], obj.pk)
50 |
51 | def test_filter_by_buyer(self):
52 | # Create the first transaction:
53 | trans1 = self.create()
54 |
55 | # Create another transaction:
56 | gen_buyer2, bt_buyer2 = create_braintree_buyer(braintree_id='bt2')
57 | gen_trans2 = Transaction.objects.create(uuid='t2', buyer=gen_buyer2)
58 | paymethod2 = create_method(bt_buyer2)
59 | trans2 = self.create(paymethod=paymethod2, transaction=gen_trans2)
60 |
61 | objects = self.get(
62 | transaction__buyer__uuid=self.buyer.uuid).json['objects']
63 | eq_(len(objects), 1, objects)
64 | eq_(objects[0]['resource_uri'], trans1.get_uri())
65 |
66 | objects = self.get(
67 | transaction__buyer__uuid=gen_buyer2.uuid).json['objects']
68 | eq_(len(objects), 1, objects)
69 | eq_(objects[0]['resource_uri'], trans2.get_uri())
70 |
71 | def test_only_gets_are_allowed(self):
72 | obj = self.create()
73 | self.allowed_verbs(obj.get_uri(), ['get'])
74 |
--------------------------------------------------------------------------------
/lib/brains/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include, patterns, url
2 |
3 | from rest_framework.routers import DefaultRouter
4 |
5 | from lib.brains.views import buyer, paymethod, subscription, transaction
6 |
7 | router = DefaultRouter()
8 | router.register(r'buyer', buyer.BraintreeBuyerViewSet, base_name='buyer')
9 | router.register(r'paymethod', paymethod.PaymentMethodViewSet,
10 | base_name='paymethod')
11 | router.register(r'subscription', subscription.SubscriptionViewSet,
12 | base_name='subscription')
13 | router.register(r'transaction', transaction.TransactionViewSet,
14 | base_name='transaction')
15 |
16 | urlpatterns = patterns(
17 | 'lib.brains.views',
18 | url(r'^mozilla/', include(router.urls, namespace='mozilla')),
19 | url(r'^token/generate/$', 'token.generate', name='token.generate'),
20 | url(r'^customer/$', 'customer.create', name='customer'),
21 | url(r'^paymethod/$', 'paymethod.create', name='paymethod'),
22 | url(r'^paymethod/delete/$', 'paymethod.delete', name='paymethod.delete'),
23 | url(r'^sale/$', 'sale.create', name='sale'),
24 | url(r'^subscription/$', 'subscription.create', name='subscription'),
25 | url(r'^subscription/cancel/$', 'subscription.cancel',
26 | name='subscription.cancel'),
27 | url(r'^subscription/paymethod/change/$', 'subscription.change',
28 | name='subscription.change'),
29 | url(r'^webhook/$', 'webhook.webhook', name='webhook'),
30 | )
31 |
--------------------------------------------------------------------------------
/lib/brains/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/views/__init__.py
--------------------------------------------------------------------------------
/lib/brains/views/buyer.py:
--------------------------------------------------------------------------------
1 | from lib.brains.models import BraintreeBuyer
2 | from lib.brains.serializers import LocalBuyer
3 | from solitude.base import NoAddModelViewSet
4 |
5 |
6 | class BraintreeBuyerViewSet(NoAddModelViewSet):
7 | queryset = BraintreeBuyer.objects.all()
8 | serializer_class = LocalBuyer
9 | filter_fields = ('buyer', 'active')
10 |
--------------------------------------------------------------------------------
/lib/brains/views/customer.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.brains import serializers
5 | from lib.brains.client import get_client
6 | from lib.brains.errors import BraintreeResultError
7 | from lib.brains.forms import BuyerForm
8 | from lib.brains.models import BraintreeBuyer
9 | from solitude.errors import FormError
10 | from solitude.logger import getLogger
11 |
12 | log = getLogger('s.brains')
13 |
14 |
15 | @api_view(['POST'])
16 | def create(request):
17 | client = get_client().Customer
18 | form = BuyerForm(request.DATA)
19 |
20 | if not form.is_valid():
21 | raise FormError(form.errors)
22 |
23 | result = client.create()
24 | if not result.is_success:
25 | log.warning('Error on creating Customer: {0}, {1}'
26 | .format(form.cleaned_data['uuid'], result.message))
27 | raise BraintreeResultError(result)
28 |
29 | log.info('Braintree customer created: {0}'.format(result.customer.id))
30 |
31 | braintree_buyer = BraintreeBuyer.objects.create(
32 | buyer=form.buyer, braintree_id=result.customer.id)
33 |
34 | log.info('Braintree buyer created: {0}'.format(braintree_buyer.pk))
35 |
36 | res = serializers.Namespaced(
37 | mozilla=serializers.LocalBuyer(instance=braintree_buyer),
38 | braintree=serializers.Customer(instance=result.customer)
39 | )
40 | return Response(res.data, status=201)
41 |
--------------------------------------------------------------------------------
/lib/brains/views/paymethod.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.brains import serializers
5 | from lib.brains.client import get_client
6 | from lib.brains.errors import BraintreeResultError
7 | from lib.brains.forms import PaymentMethodForm, PayMethodDeleteForm
8 | from lib.brains.models import BraintreePaymentMethod
9 | from solitude.base import NoAddModelViewSet
10 | from solitude.constants import PAYMENT_METHOD_CARD
11 | from solitude.errors import FormError
12 | from solitude.logger import getLogger
13 |
14 | log = getLogger('s.brains')
15 |
16 |
17 | @api_view(['POST'])
18 | def delete(request):
19 | form = PayMethodDeleteForm(request.DATA)
20 |
21 | if not form.is_valid():
22 | raise FormError(form.errors)
23 |
24 | solitude_method = form.cleaned_data['paymethod']
25 | solitude_method.braintree_delete()
26 | solitude_method.active = False
27 | solitude_method.save()
28 |
29 | log.info('Payment method deleted from braintree: {}'
30 | .format(solitude_method.pk))
31 |
32 | return Response({}, status=204)
33 |
34 |
35 | @api_view(['POST'])
36 | def create(request):
37 | client = get_client().PaymentMethod
38 | form = PaymentMethodForm(request.DATA)
39 |
40 | if not form.is_valid():
41 | raise FormError(form.errors)
42 |
43 | buyer = form.buyer
44 | braintree_buyer = form.braintree_buyer
45 | result = client.create(form.braintree_data)
46 |
47 | if not result.is_success:
48 | log.warning('Error on creating Payment method: {0}, {1}'
49 | .format(buyer.uuid, result.message))
50 | raise BraintreeResultError(result)
51 |
52 | braintree_method = result.payment_method
53 | log.info('PaymentMethod created for: {0}'.format(buyer.uuid))
54 |
55 | solitude_method = BraintreePaymentMethod.objects.create(
56 | braintree_buyer=braintree_buyer,
57 | type=PAYMENT_METHOD_CARD,
58 | type_name=braintree_method.card_type,
59 | provider_id=braintree_method.token,
60 | truncated_id=result.payment_method.last_4
61 | )
62 | log.info('Method {0} created.'.format(solitude_method.pk))
63 |
64 | res = serializers.Namespaced(
65 | mozilla=serializers.LocalPayMethod(instance=solitude_method),
66 | braintree=serializers.PayMethod(instance=braintree_method)
67 | )
68 | return Response(res.data, status=201)
69 |
70 |
71 | class PaymentMethodViewSet(NoAddModelViewSet):
72 | queryset = BraintreePaymentMethod.objects.filter()
73 | serializer_class = serializers.LocalPayMethod
74 | filter_fields = ('braintree_buyer', 'braintree_buyer__buyer__uuid',
75 | 'active')
76 |
--------------------------------------------------------------------------------
/lib/brains/views/sale.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.brains.client import get_client
5 | from lib.brains.errors import BraintreeResultError
6 | from lib.brains.forms import SaleForm
7 | from lib.brains.models import BraintreeTransaction
8 | from lib.brains.serializers import LocalTransaction, Namespaced
9 | from lib.transactions import constants
10 | from lib.transactions.models import Transaction
11 | from lib.transactions.serializers import TransactionSerializer
12 | from solitude.errors import FormError
13 | from solitude.logger import getLogger
14 |
15 | log = getLogger('s.brains')
16 |
17 |
18 | @api_view(['POST'])
19 | def create(request):
20 | client = get_client().Transaction
21 | form = SaleForm(request.DATA)
22 |
23 | if not form.is_valid():
24 | raise FormError(form.errors)
25 |
26 | result = client.sale(form.braintree_data)
27 | if not result.is_success:
28 | log.warning('Error on one-off sale: {}'.format(form.braintree_data))
29 | raise BraintreeResultError(result)
30 |
31 | our_transaction = Transaction.objects.create(
32 | amount=result.transaction.amount,
33 | buyer=form.buyer,
34 | currency=result.transaction.currency_iso_code,
35 | provider=constants.PROVIDER_BRAINTREE,
36 | seller=form.seller_product.seller,
37 | seller_product=form.seller_product,
38 | status=constants.STATUS_CHECKED,
39 | type=constants.TYPE_PAYMENT,
40 | uid_support=result.transaction.id
41 | )
42 | our_transaction.uuid = our_transaction.create_short_uid()
43 | our_transaction.save()
44 | log.info('Transaction created: {}'.format(our_transaction.pk))
45 |
46 | braintree_transaction = BraintreeTransaction.objects.create(
47 | kind='submit_for_settlement',
48 | paymethod=form.cleaned_data['paymethod'],
49 | transaction=our_transaction,
50 |
51 | )
52 | log.info('BraintreeTransaction created: {}'
53 | .format(braintree_transaction.pk))
54 |
55 | res = Namespaced(
56 | braintree={}, # Not sure if there's anything useful to add here.
57 | mozilla={
58 | 'braintree': LocalTransaction(braintree_transaction),
59 | 'generic': TransactionSerializer(our_transaction)
60 | }
61 | )
62 | return Response(res.data, status=200)
63 |
--------------------------------------------------------------------------------
/lib/brains/views/token.py:
--------------------------------------------------------------------------------
1 | from rest_framework.decorators import api_view
2 | from rest_framework.response import Response
3 |
4 | from lib.brains.client import get_client
5 |
6 |
7 | @api_view(['POST'])
8 | def generate(request):
9 | return Response({'token': get_client().ClientToken.generate()})
10 |
--------------------------------------------------------------------------------
/lib/brains/views/transaction.py:
--------------------------------------------------------------------------------
1 | from rest_framework.response import Response
2 |
3 | from lib.brains.models import BraintreeTransaction
4 | from lib.brains.serializers import LocalTransaction
5 | from solitude.base import NoAddModelViewSet
6 |
7 |
8 | class TransactionViewSet(NoAddModelViewSet):
9 | queryset = BraintreeTransaction.objects.all()
10 | serializer_class = LocalTransaction
11 | filter_fields = ('transaction__buyer__uuid',)
12 |
13 | def update(self, *args, **kw):
14 | # Not sure what a patch to this object would do.
15 | return Response(status=405)
16 |
--------------------------------------------------------------------------------
/lib/brains/views/webhook.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 | from braintree.util.xml_util import XmlUtil
4 | from braintree.webhook_notification import WebhookNotification
5 | from rest_framework.decorators import api_view
6 | from rest_framework.response import Response
7 |
8 | from lib.brains.client import get_client
9 | from lib.brains.forms import WebhookParseForm, WebhookVerifyForm
10 | from lib.brains.webhooks import Processor
11 | from solitude.errors import FormError
12 | from solitude.logger import getLogger
13 |
14 | log = getLogger('s.brains')
15 | debug_log = getLogger('s.webhook')
16 |
17 |
18 | def webhook(request):
19 | if request.method.lower() == 'get':
20 | return verify(request)
21 | return parse(request)
22 |
23 |
24 | @api_view(['POST'])
25 | def parse(request):
26 | form = WebhookParseForm(request.DATA)
27 | if not form.is_valid():
28 | raise FormError(form.errors)
29 |
30 | # Parse the gateway without doing a validation on this server.
31 | # The validation has happened on the solitude-auth server.
32 | gateway = get_client().Configuration.instantiate().gateway()
33 | payload = base64.decodestring(form.cleaned_data['bt_payload'])
34 | attributes = XmlUtil.dict_from_xml(payload)
35 | parsed = WebhookNotification(gateway, attributes['notification'])
36 |
37 | log.info('Received webhook: {p.kind}.'.format(p=parsed))
38 | debug_log.debug(parsed)
39 |
40 | processor = Processor(parsed)
41 | processor.process()
42 | data = processor.data
43 | return Response(data, status=200 if data else 204)
44 |
45 |
46 | @api_view(['GET'])
47 | def verify(request):
48 | form = WebhookVerifyForm(request.QUERY_PARAMS)
49 | if not form.is_valid():
50 | raise FormError(form.errors)
51 |
52 | log.info('Received verification response: {r}'.format(r=form.response))
53 | return Response(form.response)
54 |
--------------------------------------------------------------------------------
/lib/buyers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/buyers/__init__.py
--------------------------------------------------------------------------------
/lib/buyers/constants.py:
--------------------------------------------------------------------------------
1 | # Be careful changing these strings because webpay depends on them.
2 | BUYER_UUID_ALREADY_EXISTS = 'BUYER_UUID_ALREADY_EXISTS'
3 | FIELD_REQUIRED = 'FIELD_REQUIRED'
4 | PIN_4_NUMBERS_LONG = 'PIN_4_NUMBERS_LONG'
5 | PIN_ONLY_NUMBERS = 'PIN_ONLY_NUMBERS'
6 |
--------------------------------------------------------------------------------
/lib/buyers/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.shortcuts import get_object_or_404
3 |
4 | from django_paranoia.forms import ParanoidForm
5 |
6 | from lib.buyers.constants import PIN_4_NUMBERS_LONG, PIN_ONLY_NUMBERS
7 | from lib.buyers.models import Buyer
8 |
9 |
10 | def clean_pin(pin):
11 | if pin is None or len(pin) == 0:
12 | return pin
13 |
14 | if not len(pin) == 4:
15 | raise forms.ValidationError(PIN_4_NUMBERS_LONG, code='invalid')
16 |
17 | if not pin.isdigit():
18 | raise forms.ValidationError(PIN_ONLY_NUMBERS, code='invalid')
19 |
20 | return pin
21 |
22 |
23 | class PinForm(ParanoidForm):
24 | uuid = forms.CharField(required=True)
25 | pin = forms.CharField(required=True)
26 |
27 | def clean_uuid(self):
28 | self.cleaned_data['buyer'] = get_object_or_404(
29 | Buyer,
30 | uuid=self.cleaned_data.get('uuid'))
31 | return self.cleaned_data['uuid']
32 |
33 | def clean_pin(self):
34 | return clean_pin(self.cleaned_data.get('pin'))
35 |
--------------------------------------------------------------------------------
/lib/buyers/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/buyers/tests/__init__.py
--------------------------------------------------------------------------------
/lib/buyers/tests/test_forms.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from lib.buyers.constants import PIN_4_NUMBERS_LONG, PIN_ONLY_NUMBERS
4 | from lib.buyers.forms import PinForm
5 | from lib.buyers.models import Buyer
6 |
7 |
8 | class BuyerFormTest(TestCase):
9 |
10 | def setUp(self):
11 | self.data = {'uuid': 'a:uuid'}
12 | Buyer.objects.create(uuid='a:uuid')
13 |
14 | def test_good_pin(self):
15 | self.data['pin'] = '1234'
16 | form = PinForm(self.data)
17 | assert form.is_valid()
18 |
19 | def test_too_long_pin(self):
20 | self.data['pin'] = '12345'
21 | form = PinForm(self.data)
22 | assert not form.is_valid()
23 | assert PIN_4_NUMBERS_LONG in form.errors['pin']
24 |
25 | def test_too_short_pin(self):
26 | self.data['pin'] = '123'
27 | form = PinForm(self.data)
28 | assert not form.is_valid()
29 | assert PIN_4_NUMBERS_LONG in form.errors['pin']
30 |
31 | def test_partially_numeric_pin(self):
32 | self.data['pin'] = '123a'
33 | form = PinForm(self.data)
34 | assert not form.is_valid()
35 | assert PIN_ONLY_NUMBERS in form.errors['pin']
36 |
37 | def test_completely_alpha_pin(self):
38 | self.data['pin'] = 'asfa'
39 | form = PinForm(self.data)
40 | assert not form.is_valid()
41 | assert PIN_ONLY_NUMBERS in form.errors['pin']
42 |
--------------------------------------------------------------------------------
/lib/buyers/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include, patterns, url
2 |
3 | from rest_framework.routers import DefaultRouter
4 |
5 | from lib.buyers import views
6 |
7 | router = DefaultRouter()
8 | router.register(r'buyer', views.BuyerViewSet)
9 |
10 | urlpatterns = patterns(
11 | '',
12 | url(r'', include(router.urls)),
13 | url(r'^generic/buyer/(?P[\d]+)/close/$', views.close, name='close'),
14 | url(r'^confirm_pin', views.confirm_pin, name='confirm'),
15 | url(r'^verify_pin', views.verify_pin, name='verify'),
16 | url(r'^reset_confirm_pin', views.reset_confirm_pin,
17 | name='reset_confirm_pin')
18 | )
19 |
--------------------------------------------------------------------------------
/lib/provider/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/provider/__init__.py
--------------------------------------------------------------------------------
/lib/provider/bango.py:
--------------------------------------------------------------------------------
1 | from rest_framework.response import Response
2 |
3 | from lib.bango.forms import (CreateBangoNumberForm, MakePremiumForm,
4 | UpdateRatingForm)
5 | from lib.bango.serializers import SellerProductBangoSerializer
6 | from lib.bango.views.base import BangoResource
7 | from lib.sellers.models import SellerProductBango
8 | from solitude.base import BaseAPIView
9 |
10 |
11 | class ProductView(BaseAPIView):
12 |
13 | """
14 | Override creating a product.
15 | """
16 |
17 | def post(self, request, *args, **kwargs):
18 | view = BangoResource()
19 | form = CreateBangoNumberForm(request.DATA)
20 | if not form.is_valid():
21 | return self.form_errors(form)
22 |
23 | serial = SellerProductBangoSerializer(data=request.DATA)
24 | if not serial.is_valid():
25 | return Response(serial.errors, status=400)
26 |
27 | # Create the product.
28 | data = form.cleaned_data
29 | data['packageId'] = serial.object.seller_bango.package_id
30 |
31 | resp = view.client('CreateBangoNumber', data)
32 |
33 | product = SellerProductBango.objects.create(
34 | seller_bango=serial.object.seller_bango,
35 | seller_product=serial.object.seller_product,
36 | bango_id=resp.bango,
37 | )
38 |
39 | # Make it premium.
40 | data = request.DATA.copy()
41 | data['bango'] = resp.bango
42 | data['price'] = '0.99'
43 | data['currencyIso'] = 'USD'
44 |
45 | form = MakePremiumForm(data)
46 | if not form.is_valid():
47 | return self.form_errors(form)
48 |
49 | data = form.cleaned_data
50 | data['bango'] = resp.bango
51 | view.client('MakePremiumPerAccess', data)
52 |
53 | for rating, scheme in (['UNIVERSAL', 'GLOBAL'],
54 | ['GENERAL', 'USA']):
55 | # Make it global and US rating.
56 | data.update({'rating': rating, 'ratingScheme': scheme})
57 | form = UpdateRatingForm(data)
58 | if not form.is_valid():
59 | return self.form_errors(form)
60 |
61 | data = form.cleaned_data
62 | data['bango'] = resp.bango
63 | view.client('UpdateRating', data)
64 |
65 | return Response(SellerProductBangoSerializer(product).data)
66 |
--------------------------------------------------------------------------------
/lib/provider/errors.py:
--------------------------------------------------------------------------------
1 | from django.http import Http404
2 |
3 |
4 | class NoReference(Http404):
5 |
6 | """
7 | The requested reference implementation did not exist.
8 | """
9 | pass
10 |
--------------------------------------------------------------------------------
/lib/provider/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/provider/tests/__init__.py
--------------------------------------------------------------------------------
/lib/provider/tests/test_bango.py:
--------------------------------------------------------------------------------
1 | from django.core.urlresolvers import reverse
2 |
3 | from mock import patch
4 | from nose.tools import eq_
5 |
6 | from lib.bango.client import ClientMock
7 | from lib.bango.constants import OK
8 | from lib.bango.tests import samples, utils
9 | from solitude.base import APITest
10 |
11 |
12 | class TestNope(APITest):
13 |
14 | def test(self):
15 | self.url = reverse('provider.bango.nope')
16 | eq_(self.client.get(self.url).status_code, 405)
17 |
18 |
19 | class TestProduct(APITest):
20 | uuid = 'sample:uuid'
21 |
22 | def setUp(self):
23 | self.objs = utils.make_no_product()
24 | self.url = reverse('provider.bango.product')
25 |
26 | def get_data(self):
27 | data = samples.good_bango_number
28 | data['seller_product'] = self.objs.product.get_uri()
29 | data['seller_bango'] = reverse('bango:package-detail',
30 | kwargs={'pk': self.objs.bango.pk})
31 | return data
32 |
33 | def test(self):
34 | res = self.client.post(self.url, data=self.get_data())
35 | eq_(res.status_code, 200, res.json)
36 |
37 | def test_form_fail(self):
38 | data = self.get_data()
39 | del data['seller_bango']
40 | res = self.client.post(self.url, data=data)
41 | eq_(res.status_code, 400, res.json)
42 |
43 | @patch.object(ClientMock, 'mock_results')
44 | def test_calls(self, mock_results):
45 | mock_results.return_value = {'responseCode': OK,
46 | 'bango': '1'}
47 | res = self.client.post(self.url, data=self.get_data())
48 | eq_(res.status_code, 200, res.json)
49 | eq_(len(mock_results.call_args_list), 4)
50 |
--------------------------------------------------------------------------------
/lib/provider/tests/test_client.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from nose.tools import eq_, ok_
4 |
5 | from ..client import Client, ClientProxy, get_client
6 |
7 |
8 | class TestClientObj(TestCase):
9 |
10 | def test_non_existant(self):
11 | client = Client('does-not-exist')
12 | eq_(client.api, None)
13 |
14 | def test_existing(self):
15 | config = {
16 | 'bob': {
17 | 'url': 'http://f.com',
18 | 'auth': {'key': 'k', 'secret': 's'}
19 | }
20 | }
21 |
22 | with self.settings(ZIPPY_CONFIGURATION=config):
23 | client = Client('bob')
24 | ok_(client.api)
25 | eq_(client.config, config['bob'])
26 |
27 | def test_proxy(self):
28 | with self.settings(ZIPPY_MOCK=False, ZIPPY_PROXY='http://blah/proxy'):
29 | client = get_client('bob')
30 | ok_(isinstance(client, ClientProxy))
31 | eq_(client.api._store['base_url'], 'http://blah/proxy/bob')
32 |
--------------------------------------------------------------------------------
/lib/provider/tests/test_serializer.py:
--------------------------------------------------------------------------------
1 | from nose.tools import eq_
2 | from rest_framework import serializers
3 |
4 | from lib.provider.serializers import Remote
5 | from lib.sellers.models import Seller
6 | from lib.sellers.tests.utils import SellerTest
7 |
8 |
9 | class Sample(Remote):
10 | pk = serializers.CharField(max_length=100)
11 | remote = serializers.CharField(max_length=100)
12 |
13 | class Meta:
14 | model = Seller
15 | fields = ['pk']
16 | remote = ['remote']
17 |
18 |
19 | class TestSample(SellerTest):
20 |
21 | def test_remote(self):
22 | eq_(Sample(data={'local': 'f', 'remote': 'b'}).remote_data,
23 | {'remote': 'b'})
24 |
25 | def test_restore(self):
26 | seller = self.create_seller()
27 | eq_(Sample().restore_object({'pk': seller.pk, 'remote': 'b'}).pk,
28 | seller.pk)
29 |
--------------------------------------------------------------------------------
/lib/provider/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include, patterns, url
2 |
3 | from rest_framework.routers import DefaultRouter
4 |
5 | from .bango import ProductView
6 | from .reference import SellerProductReference, SellerReference, Terms
7 | from .views import NotImplementedView, ProxyView
8 |
9 | bango_overrides = patterns(
10 | '',
11 | url(r'^product/$', ProductView.as_view(), name='provider.bango.product'),
12 | url(r'', NotImplementedView.as_view(), name='provider.bango.nope')
13 | )
14 |
15 | reference = DefaultRouter()
16 | reference.register('sellers', SellerReference, base_name='sellers')
17 | reference.register('products', SellerProductReference, base_name='products')
18 | reference.register('terms', Terms, base_name='terms')
19 |
20 | urlpatterns = patterns(
21 | '',
22 | url(r'^bango/', include(bango_overrides)),
23 | url(r'^reference/', include(reference.urls, namespace='reference')),
24 |
25 | # The catch all for everything else that has not be viewsetted.
26 | url(r'^(?P\w+)/(?P\w+)/(?P[^/]+)?/?',
27 | ProxyView.as_view(), name='provider.api_view'),
28 | )
29 |
--------------------------------------------------------------------------------
/lib/proxy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/proxy/__init__.py
--------------------------------------------------------------------------------
/lib/proxy/constants.py:
--------------------------------------------------------------------------------
1 | HEADERS_URL = 'x-solitude-url'
2 | HEADERS_TOKEN = 'x-solitude-token'
3 | HEADERS_URL_GET = 'HTTP_X_SOLITUDE_URL'
4 | HEADERS_TOKEN_GET = 'HTTP_X_SOLITUDE_TOKEN'
5 |
--------------------------------------------------------------------------------
/lib/proxy/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/proxy/models.py
--------------------------------------------------------------------------------
/lib/proxy/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import patterns, url
2 |
3 | urlpatterns = patterns(
4 | '',
5 | url(r'^bango$', 'lib.proxy.views.bango', name='bango.proxy'),
6 | url(r'^provider/(?P\w+)/', 'lib.proxy.views.provider',
7 | name='provider.proxy'),
8 | )
9 |
--------------------------------------------------------------------------------
/lib/sellers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/sellers/__init__.py
--------------------------------------------------------------------------------
/lib/sellers/constants.py:
--------------------------------------------------------------------------------
1 | from solitude.base import invert
2 |
3 |
4 | EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE = 'EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE'
5 |
6 | ACCESS_PURCHASE = 1
7 | ACCESS_SIMULATE = 2
8 |
9 | ACCESS_TYPES = {
10 | # The product can be purchased.
11 | 'purchase': ACCESS_PURCHASE,
12 | # The product can only go through a simulated purchase.
13 | 'simulate': ACCESS_SIMULATE,
14 | }
15 |
16 | ACCESS_CHOICES = invert(ACCESS_TYPES)
17 |
--------------------------------------------------------------------------------
/lib/sellers/serializers.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ObjectDoesNotExist
2 | from django.core.urlresolvers import reverse
3 |
4 | from rest_framework import serializers
5 |
6 | from lib.bango.serializers import SellerBangoSerializer
7 | from lib.sellers.constants import EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE
8 | from lib.sellers.models import Seller, SellerBango, SellerProduct
9 | from solitude.base import BaseSerializer
10 | from solitude.related_fields import PathRelatedField
11 |
12 |
13 | class SellerSerializer(BaseSerializer):
14 | bango = PathRelatedField(
15 | view_name='bango:package-detail',
16 | read_only=True
17 | )
18 |
19 | class Meta:
20 | model = Seller
21 |
22 | def transform_bango(self, obj, value):
23 | # This makes me so sad that we did this. Please not again.
24 | # https://github.com/mozilla/solitude/issues/343
25 | try:
26 | seller_bango = SellerBango.objects.get(seller=obj)
27 | return SellerBangoSerializer(seller_bango).data
28 | except ObjectDoesNotExist:
29 | return {}
30 |
31 | def resource_uri(self, pk):
32 | return reverse('generic:seller-detail', kwargs={'pk': pk})
33 |
34 |
35 | class SellerProductSerializer(BaseSerializer):
36 | seller_uuids = serializers.CharField(source='supported_providers',
37 | read_only=True)
38 | seller = PathRelatedField(
39 | view_name='generic:seller-detail',
40 | lookup_field='pk'
41 | )
42 |
43 | class Meta:
44 | model = SellerProduct
45 | # Note: fields are validated in this order, ensure that
46 | # external_id is after seller.
47 | fields = [
48 | 'seller', 'access', 'resource_uri', 'resource_pk', 'secret',
49 | 'seller_uuids', 'public_id', 'external_id',
50 | ]
51 |
52 | def validate_external_id(self, attrs, source):
53 | value = attrs.get(source)
54 | seller = attrs.get('seller')
55 |
56 | qs = SellerProduct.objects.filter(external_id=value)
57 | if seller:
58 | qs = qs.filter(seller=seller)
59 |
60 | if self.object:
61 | qs = qs.exclude(pk=self.object.pk)
62 |
63 | if qs.exists():
64 | raise serializers.ValidationError(
65 | EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE)
66 |
67 | return attrs
68 |
69 | def resource_uri(self, pk):
70 | return reverse('generic:sellerproduct-detail', kwargs={'pk': pk})
71 |
--------------------------------------------------------------------------------
/lib/sellers/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/sellers/tests/__init__.py
--------------------------------------------------------------------------------
/lib/sellers/tests/utils.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from django.core.urlresolvers import reverse
4 |
5 | from lib.sellers.models import Seller, SellerProduct
6 | from solitude.base import APITest
7 |
8 |
9 | class SellerTest(APITest):
10 |
11 | def create_seller(self, **kwargs):
12 | defaults = {'uuid': 'seller:' + str(uuid.uuid4())}
13 | defaults.update(kwargs)
14 | return Seller.objects.create(**defaults)
15 |
16 | def get_seller_uri(self, seller):
17 | return reverse('generic:seller-detail', kwargs={'pk': seller.pk})
18 |
19 | def create_seller_product(self, seller=None, **kwargs):
20 | defaults = {
21 | 'seller': seller or self.create_seller(),
22 | 'public_id': 'public:' + str(uuid.uuid4()),
23 | 'external_id': 'external:' + str(uuid.uuid4()),
24 | }
25 | defaults.update(kwargs)
26 |
27 | return SellerProduct.objects.create(**defaults)
28 |
29 | def get_seller_product_uri(self, seller_product):
30 | return seller_product.get_uri()
31 |
--------------------------------------------------------------------------------
/lib/sellers/urls.py:
--------------------------------------------------------------------------------
1 | from rest_framework.routers import DefaultRouter
2 |
3 | from lib.sellers import views
4 |
5 | router = DefaultRouter()
6 | router.register(r'seller', views.SellerViewSet)
7 | router.register(r'product', views.SellerProductViewSet)
8 |
9 | urlpatterns = router.urls
10 |
--------------------------------------------------------------------------------
/lib/sellers/views.py:
--------------------------------------------------------------------------------
1 | from lib.sellers.models import Seller, SellerProduct
2 | from lib.sellers.serializers import SellerProductSerializer, SellerSerializer
3 | from solitude.base import NonDeleteModelViewSet
4 |
5 |
6 | class SellerViewSet(NonDeleteModelViewSet):
7 | queryset = Seller.objects.all()
8 | serializer_class = SellerSerializer
9 | filter_fields = ('uuid', 'active')
10 |
11 |
12 | class SellerProductViewSet(NonDeleteModelViewSet):
13 | queryset = SellerProduct.objects.all()
14 | serializer_class = SellerProductSerializer
15 | filter_fields = (
16 | 'external_id', 'public_id', 'seller__uuid', 'seller__active',
17 | 'seller'
18 | )
19 |
--------------------------------------------------------------------------------
/lib/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/services/__init__.py
--------------------------------------------------------------------------------
/lib/services/tests.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core.cache import cache
3 | from django.core.urlresolvers import reverse
4 |
5 | from mock import patch
6 | from nose.tools import eq_
7 |
8 | from lib.services.resources import TestError
9 | from solitude.base import APITest
10 |
11 |
12 | @patch.object(settings, 'DEBUG', False)
13 | class TestStatus(APITest):
14 |
15 | def setUp(self):
16 | self.list_url = reverse('services.status')
17 |
18 | def failed(self, res, on):
19 | eq_(res.status_code, 500)
20 | assert False in res.json.values()
21 |
22 | @patch.object(cache, 'get', lambda x: None)
23 | def test_failure_status(self):
24 | res = self.client.get(self.list_url)
25 | self.failed(res, 'cache')
26 |
27 | # Note that Django will use the values in the settings, altering
28 | # CACHES right now will still work if your settings allow it. Urk.
29 | @patch('requests.get')
30 | @patch('lib.services.resources.StatusObject.test_cache')
31 | @patch('lib.services.resources.StatusObject.test_db')
32 | def test_proxy(self, test_db, test_cache, get):
33 | test_cache.return_value = True
34 | test_db.return_value = False
35 | with self.settings(SOLITUDE_PROXY=True,
36 | DATABASES={'default': {'ENGINE': ''}},
37 | CACHES={}):
38 | res = self.client.get(self.list_url)
39 | eq_(res.status_code, 200, res.content)
40 |
41 | @patch('requests.get')
42 | @patch('lib.services.resources.StatusObject.test_cache')
43 | @patch('lib.services.resources.StatusObject.test_db')
44 | def test_proxy_db(self, test_db, test_cache, requests):
45 | test_db.return_value = False
46 | test_cache.return_value = False
47 | with self.settings(SOLITUDE_PROXY=True,
48 | DATABASES={'default': {'ENGINE': 'foo'}},
49 | CACHES={}):
50 | self.failed(self.client.get(self.list_url), 'settings')
51 |
52 |
53 | class TestErrors(APITest):
54 |
55 | def test_throws_error(self):
56 | with self.assertRaises(TestError):
57 | self.client.get(reverse('services.error'))
58 |
59 |
60 | class TestNoop(APITest):
61 |
62 | def test_noop(self):
63 | eq_(self.client.get(reverse('services.request')).status_code, 200)
64 |
--------------------------------------------------------------------------------
/lib/transactions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/__init__.py
--------------------------------------------------------------------------------
/lib/transactions/constants.py:
--------------------------------------------------------------------------------
1 | from solitude.base import invert
2 |
3 | # Please see docs for an explanation of these.
4 | STATUS_PENDING = 0
5 | STATUS_COMPLETED = 1
6 | STATUS_CHECKED = 2
7 | STATUS_RECEIVED = 3
8 | STATUS_FAILED = 4
9 | STATUS_CANCELLED = 5
10 | # These are statuses that reflect the transactions state in solitude
11 | # as it is configured by the client.
12 | STATUS_STARTED = 6
13 | STATUS_ERRORED = 7
14 |
15 | STATUS_DEFAULT = STATUS_STARTED
16 |
17 | STATUSES = {
18 | 'cancelled': STATUS_CANCELLED,
19 | 'checked': STATUS_CHECKED,
20 | 'completed': STATUS_COMPLETED,
21 | 'failed': STATUS_FAILED,
22 | 'pending': STATUS_PENDING,
23 | 'received': STATUS_RECEIVED,
24 | 'started': STATUS_STARTED,
25 | 'errored': STATUS_ERRORED
26 | }
27 | STATUSES_INVERTED = dict((v, k) for k, v in STATUSES.items())
28 |
29 | TYPE_PAYMENT = 0
30 | TYPE_REFUND = 1
31 | TYPE_REVERSAL = 2
32 | TYPE_REFUND_MANUAL = 3
33 | TYPE_REVERSAL_MANUAL = 4
34 |
35 | TYPE_DEFAULT = TYPE_PAYMENT
36 |
37 | TYPES = {
38 | 'payment': TYPE_PAYMENT,
39 | 'refund': TYPE_REFUND,
40 | 'refund_manual': TYPE_REFUND_MANUAL,
41 | 'reversal': TYPE_REVERSAL,
42 | 'reversal_manual': TYPE_REVERSAL_MANUAL,
43 | }
44 |
45 | TYPE_REFUNDS = (TYPE_REFUND, TYPE_REFUND_MANUAL)
46 | TYPE_REVERSALS = (TYPE_REVERSAL, TYPE_REVERSAL_MANUAL)
47 | TYPE_REFUNDS_REVERSALS = TYPE_REFUNDS + TYPE_REVERSALS
48 |
49 | PROVIDER_BANGO = 1
50 | PROVIDER_REFERENCE = 2
51 | PROVIDER_BRAINTREE = 4
52 |
53 | PROVIDERS = {
54 | 'bango': PROVIDER_BANGO,
55 | 'reference': PROVIDER_REFERENCE,
56 | 'braintree': PROVIDER_BRAINTREE
57 | }
58 |
59 | LOG_STATS = 0
60 | LOG_REVENUE = 1
61 |
62 | LOGS = {
63 | 'stats': LOG_STATS,
64 | 'revenue': LOG_REVENUE
65 | }
66 |
67 | STATUSES_CHOICES = invert(STATUSES)
68 | TYPES_CHOICES = invert(TYPES)
69 | LOG_CHOICES = invert(LOGS)
70 | PROVIDERS_CHOICES = invert(PROVIDERS)
71 |
--------------------------------------------------------------------------------
/lib/transactions/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/management/__init__.py
--------------------------------------------------------------------------------
/lib/transactions/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/management/commands/__init__.py
--------------------------------------------------------------------------------
/lib/transactions/serializers.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from django.core.urlresolvers import reverse
4 |
5 | from rest_framework import serializers
6 |
7 | from lib.transactions.models import Transaction
8 | from solitude.base import BaseSerializer
9 | from solitude.related_fields import PathRelatedField
10 |
11 |
12 | class TransactionSerializer(BaseSerializer):
13 | buyer = PathRelatedField(view_name='generic:buyer-detail', required=False)
14 | seller = PathRelatedField(view_name='generic:seller-detail',
15 | required=False)
16 | seller_product = PathRelatedField(
17 | view_name='generic:sellerproduct-detail', required=False)
18 | related = PathRelatedField(
19 | view_name='generic:transaction-detail', required=False)
20 | uuid = serializers.CharField(required=False)
21 |
22 | class Meta:
23 | model = Transaction
24 | fields = [
25 | 'amount', 'buyer', 'carrier', 'created', 'currency', 'notes',
26 | 'pay_url', 'provider', 'region', 'related', 'relations',
27 | 'resource_pk', 'resource_uri', 'seller',
28 | 'seller_product', 'source', 'status', 'status_reason', 'type',
29 | 'uid_pay', 'uid_support', 'uuid'
30 | ]
31 |
32 | def transform_relations(self, obj, value):
33 | objs = []
34 | if obj:
35 | relations = Transaction.objects.filter(related=obj)
36 | for relation in relations:
37 | # Note that if this relation has more relations, it will fail
38 | # to serialize on the recursiveness. This can be fixed in
39 | # DRF 3.x with recursivefield (if we want to).
40 | objs.append(TransactionSerializer(relation).data)
41 |
42 | return objs
43 |
44 | def resource_uri(self, pk):
45 | return reverse('generic:transaction-detail', kwargs={'pk': pk})
46 |
47 | def validate_uuid(self, attrs, source):
48 | # Provide a default uuid.
49 | if not attrs.get('uuid') and not self.object:
50 | attrs['uuid'] = 'solitude:' + str(uuid.uuid4())
51 | return attrs
52 |
--------------------------------------------------------------------------------
/lib/transactions/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/tests/__init__.py
--------------------------------------------------------------------------------
/lib/transactions/tests/test_commands.py:
--------------------------------------------------------------------------------
1 | import csv
2 | from tempfile import NamedTemporaryFile
3 |
4 | from django import test
5 |
6 | from nose.tools import eq_, raises
7 |
8 | from lib.sellers.models import Seller, SellerProduct
9 | from lib.transactions import constants
10 | from lib.transactions.management.commands.log import generate_log
11 | from lib.transactions.models import Transaction
12 |
13 |
14 | class TestLog(test.TestCase):
15 |
16 | def setUp(self):
17 | self.name = NamedTemporaryFile().name
18 | seller = Seller.objects.create(uuid='uuid')
19 | self.product = SellerProduct.objects.create(seller=seller,
20 | external_id='xyz')
21 | self.first = Transaction.objects.create(
22 | provider=1,
23 | seller_product=self.product, uuid='uuid')
24 | self.date = self.first.modified.date()
25 |
26 | def results(self):
27 | return csv.reader(open(self.name, 'rb'))
28 |
29 | def test_filter(self):
30 | generate_log(self.date, self.name, 'stats')
31 | output = self.results()
32 | eq_(next(output)[0], 'version')
33 | eq_(next(output)[1], 'uuid')
34 |
35 | @raises(StopIteration)
36 | def test_stats_log_stops(self):
37 | generate_log(self.date, self.name, 'revenue')
38 | output = self.results()
39 | eq_(next(output)[0], 'version')
40 | next(output) # There is no line 1, transaction not written.
41 |
42 | def test_stats_log(self):
43 | self.first.status = constants.STATUS_CHECKED
44 | self.first.save()
45 |
46 | generate_log(self.date, self.name, 'revenue')
47 | output = self.results()
48 | eq_(next(output)[0], 'version')
49 | eq_(next(output)[1], 'uuid')
50 |
51 | @raises(StopIteration)
52 | def test_multiple(self):
53 | self.first.status = constants.STATUS_CHECKED
54 | self.first.log.create(type=constants.LOG_REVENUE)
55 | self.first.save()
56 |
57 | generate_log(self.date, self.name, 'revenue')
58 | output = self.results()
59 | eq_(next(output)[0], 'version')
60 | next(output) # There is no line 1, transaction not written.
61 |
62 | def test_other(self):
63 | self.first.status = constants.STATUS_CHECKED
64 | self.first.log.create(type=constants.LOG_STATS)
65 | self.first.save()
66 |
67 | generate_log(self.date, self.name, 'revenue')
68 | output = self.results()
69 | eq_(next(output)[0], 'version')
70 | eq_(next(output)[1], 'uuid')
71 |
72 | def test_no_seller(self):
73 | self.first.seller_product = None
74 | self.first.save()
75 | generate_log(self.date, self.name, 'stats')
76 |
--------------------------------------------------------------------------------
/lib/transactions/urls.py:
--------------------------------------------------------------------------------
1 | from rest_framework.routers import DefaultRouter
2 |
3 | from lib.transactions import views
4 |
5 | router = DefaultRouter()
6 | router.register(r'transaction', views.TransactionViewSet)
7 |
8 | urlpatterns = router.urls
9 |
--------------------------------------------------------------------------------
/lib/transactions/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.response import Response
2 |
3 | from lib.transactions.forms import UpdateForm
4 | from lib.transactions.models import Transaction
5 | from lib.transactions.serializers import TransactionSerializer
6 | from solitude.base import NonDeleteModelViewSet
7 |
8 |
9 | class TransactionViewSet(NonDeleteModelViewSet):
10 | queryset = Transaction.objects.all()
11 | serializer_class = TransactionSerializer
12 | filter_fields = ('uuid', 'seller', 'provider')
13 |
14 | def update(self, request, *args, **kwargs):
15 | # Disallow PUT, but allow PATCH.
16 | if not kwargs.get('partial', False):
17 | return Response(status=405)
18 |
19 | # We only allow very limited transaction changes.
20 | self.object = self.get_object_or_none()
21 | form = UpdateForm(
22 | request.DATA, original_data=self.object.to_dict(), request=request)
23 | if form.is_valid():
24 | return (
25 | super(TransactionViewSet, self)
26 | .update(request, *args, **kwargs)
27 | )
28 |
29 | return self.form_errors(form)
30 |
--------------------------------------------------------------------------------
/logs/.keep:
--------------------------------------------------------------------------------
1 | Docker logs will be written to this directory.
2 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | # Edit this if necessary or override the variable in your environment.
6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'solitude.settings')
7 | if len(sys.argv) > 1 and sys.argv[1] == 'test':
8 | os.environ['DJANGO_SETTINGS_MODULE'] = 'solitude.settings.test'
9 |
10 |
11 | # Specifically importing once the environment has been setup.
12 | from django.conf import settings
13 | newrelic_ini = getattr(settings, 'NEWRELIC_INI', None)
14 |
15 | if newrelic_ini and os.path.exists(newrelic_ini):
16 | import newrelic.agent
17 | try:
18 | newrelic.agent.initialize(newrelic_ini)
19 | except newrelic.api.exceptions.ConfigurationError:
20 | import logging
21 | startup_logger = logging.getLogger('s.startup')
22 | startup_logger.exception('Failed to load new relic config.')
23 |
24 | from solitude.utils import validate_settings
25 | validate_settings()
26 |
27 |
28 | # Alter solitude to run on a particular port as per the
29 | # marketplace docs, unless overridden.
30 | from django.core.management.commands import runserver
31 | runserver.DEFAULT_PORT = 2602
32 |
33 | if __name__ == "__main__":
34 | from django.core.management import execute_from_command_line
35 | execute_from_command_line(sys.argv)
36 |
--------------------------------------------------------------------------------
/migrations/01-noop.sql:
--------------------------------------------------------------------------------
1 | -- Example database migration for schematic. Remove this, if you like.
2 |
--------------------------------------------------------------------------------
/migrations/02-add-buyers.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 | CREATE TABLE `buyer` (
3 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
4 | `uuid` varchar(255) UNIQUE NOT NULL
5 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
6 |
7 | CREATE TABLE `buyer_paypal` (
8 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
9 | `key` varchar(255),
10 | `expiry` date,
11 | `currency` varchar(3),
12 | `buyer_id` int(11) unsigned NOT NULL UNIQUE
13 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
14 |
15 | ALTER TABLE `buyer_paypal` ADD CONSTRAINT `buyer_id_refs_id_34d8ab16`
16 | FOREIGN KEY (`buyer_id`) REFERENCES `buyer` (`id`);
17 | COMMIT;
18 |
--------------------------------------------------------------------------------
/migrations/03-add-sellers.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 | CREATE TABLE `seller` (
3 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
4 | `uuid` varchar(255) NOT NULL UNIQUE
5 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
6 |
7 | CREATE TABLE `seller_paypal` (
8 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
9 | `paypal_id` varchar(255),
10 | `token` varchar(255),
11 | `secret` varchar(255),
12 | `seller_id` int(11) unsigned NOT NULL UNIQUE
13 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
14 |
15 | ALTER TABLE `seller_paypal` ADD CONSTRAINT `seller_id_refs_id_829de2a6`
16 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`);
17 | COMMIT;
18 |
--------------------------------------------------------------------------------
/migrations/04-create-transactions.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 | CREATE TABLE `transaction_paypal` (
3 | `id` int(11) UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
4 | `uuid` varchar(255) NOT NULL,
5 | `seller_id` int(11) UNSIGNED NOT NULL,
6 | `amount` numeric(9, 2) NOT NULL,
7 | `currency` varchar(3) NOT NULL,
8 | `pay_key` varchar(255) NOT NULL,
9 | `correlation_id` varchar(255) NOT NULL,
10 | `type` int(11) UNSIGNED NOT NULL,
11 | `status` int(11) UNSIGNED NOT NULL
12 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
13 |
14 | ALTER TABLE `transaction_paypal` ADD CONSTRAINT `seller_id_refs_id_8e346857`
15 | FOREIGN KEY (`seller_id`) REFERENCES `seller_paypal` (`id`);
16 | CREATE INDEX `transaction_paypal_2bbc74ae` ON `transaction_paypal` (`uuid`);
17 | CREATE INDEX `transaction_paypal_2ef613c9` ON `transaction_paypal` (`seller_id`);
18 | CREATE INDEX `transaction_paypal_278d2c0e` ON `transaction_paypal` (`pay_key`);
19 | CREATE INDEX `transaction_paypal_6fa770c7` ON `transaction_paypal` (`correlation_id`);
20 | COMMIT;
21 |
--------------------------------------------------------------------------------
/migrations/05-add-related.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 | ALTER TABLE `transaction_paypal` ADD COLUMN `related_id` int(11) unsigned;
3 | ALTER TABLE `transaction_paypal` ADD CONSTRAINT `related_id_refs_id_d6e42cbd`
4 | FOREIGN KEY (`related_id`) REFERENCES `transaction_paypal` (`id`);
5 | CREATE INDEX `transaction_paypal_cb822826` ON `transaction_paypal` (`related_id`);
6 | COMMIT;
7 |
--------------------------------------------------------------------------------
/migrations/06-add-sellers-data.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `seller_paypal`
2 | ADD COLUMN `first_name` varchar(255) NOT NULL,
3 | ADD COLUMN `last_name` varchar(255) NOT NULL,
4 | ADD COLUMN `full_name` varchar(255) NOT NULL,
5 | ADD COLUMN `business_name` varchar(255) NOT NULL,
6 | ADD COLUMN `country` varchar(64) NOT NULL,
7 | ADD COLUMN `address_one` varchar(255) NOT NULL,
8 | ADD COLUMN `address_two` varchar(255) NOT NULL,
9 | ADD COLUMN `post_code` varchar(128) NOT NULL,
10 | ADD COLUMN `city` varchar(128) NOT NULL,
11 | ADD COLUMN `state` varchar(64) NOT NULL,
12 | ADD COLUMN `phone` varchar(32) NOT NULL;
13 |
--------------------------------------------------------------------------------
/migrations/07-add-sellers-bluevia.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `seller_bluevia` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `bluevia_id` varchar(255),
4 | `seller_id` int(11) unsigned NOT NULL UNIQUE
5 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
6 |
--------------------------------------------------------------------------------
/migrations/08-add-source-column.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `transaction_paypal` ADD COLUMN `source` varchar(255);
2 |
--------------------------------------------------------------------------------
/migrations/09-add-datetime.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE buyer ADD COLUMN created datetime NOT NULL;
2 | ALTER TABLE buyer ADD COLUMN modified datetime NOT NULL;
3 |
4 | ALTER TABLE buyer_paypal ADD COLUMN created datetime NOT NULL;
5 | ALTER TABLE buyer_paypal ADD COLUMN modified datetime NOT NULL;
6 |
7 | ALTER TABLE seller ADD COLUMN created datetime NOT NULL;
8 | ALTER TABLE seller ADD COLUMN modified datetime NOT NULL;
9 |
10 | ALTER TABLE seller_paypal ADD COLUMN created datetime NOT NULL;
11 | ALTER TABLE seller_paypal ADD COLUMN modified datetime NOT NULL;
12 |
13 | ALTER TABLE seller_bluevia ADD COLUMN created datetime NOT NULL;
14 | ALTER TABLE seller_bluevia ADD COLUMN modified datetime NOT NULL;
15 |
16 | ALTER TABLE transaction_paypal ADD COLUMN created datetime NOT NULL;
17 | ALTER TABLE transaction_paypal ADD COLUMN modified datetime NOT NULL;
18 |
--------------------------------------------------------------------------------
/migrations/10-increase-columns.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE buyer_paypal MODIFY `key` longtext;
2 | ALTER TABLE seller_paypal MODIFY `token` longtext;
3 | ALTER TABLE seller_paypal MODIFY `paypal_id` longtext;
4 | ALTER TABLE seller_paypal MODIFY `secret` longtext;
5 | ALTER TABLE seller_bluevia MODIFY `bluevia_id` longtext;
6 |
--------------------------------------------------------------------------------
/migrations/11-add-product.sql:
--------------------------------------------------------------------------------
1 |
2 | CREATE TABLE `seller_product` (
3 | `id` int(11) AUTO_INCREMENT NOT NULL PRIMARY KEY,
4 | `created` datetime NOT NULL,
5 | `modified` datetime NOT NULL,
6 | `seller_id` int(11) unsigned NOT NULL,
7 | `bango_secret` longtext
8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
9 |
10 | ALTER TABLE `seller_product` ADD CONSTRAINT `seller_id_refs_id_product`
11 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`);
12 | CREATE INDEX `seller_product` ON `seller_product` (`seller_id`);
13 |
--------------------------------------------------------------------------------
/migrations/12-add-pin-to-buyers.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `pin` varchar(255);
2 |
--------------------------------------------------------------------------------
/migrations/13-rename-secret.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_product CHANGE bango_secret secret LONGTEXT;
2 |
--------------------------------------------------------------------------------
/migrations/14-add-seller-bango.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `seller_bango` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `seller_id` int(11) unsigned NOT NULL UNIQUE,
6 | `package_id` int(11) NOT NULL UNIQUE,
7 | `admin_person_id` int(11) NOT NULL,
8 | `support_person_id` int(11) NOT NULL,
9 | `finance_person_id` int(11) NOT NULL
10 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
11 |
12 | ALTER TABLE `seller_bango` ADD CONSTRAINT `seller_id_refs_id_c6c7badb`
13 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`);
14 |
--------------------------------------------------------------------------------
/migrations/15-add-product-bango.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_product MODIFY id int(11) UNSIGNED AUTO_INCREMENT;
2 |
3 | CREATE TABLE `seller_product_bango` (
4 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
5 | `created` datetime NOT NULL,
6 | `modified` datetime NOT NULL,
7 | `seller_product_id` int(11) unsigned NOT NULL UNIQUE,
8 | `seller_bango_id` int(11) unsigned NOT NULL UNIQUE,
9 | `bango_id` varchar(50) NOT NULL
10 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
11 |
12 | ALTER TABLE `seller_product_bango` ADD CONSTRAINT `seller_product_id_refs_id_bango`
13 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`);
14 | ALTER TABLE `seller_product_bango` ADD CONSTRAINT `seller_bango_id_refs_id_bango`
15 | FOREIGN KEY (`seller_bango_id`) REFERENCES `seller_bango` (`id`);
16 |
--------------------------------------------------------------------------------
/migrations/16-redo-transactions.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE transaction_paypal;
2 | CREATE TABLE `transaction` (
3 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
4 | `created` datetime NOT NULL,
5 | `modified` datetime NOT NULL,
6 | `amount` numeric(9, 2) NOT NULL,
7 | `buyer_id` int(11) UNSIGNED,
8 | `currency` varchar(3) NOT NULL,
9 | `provider` int(11) UNSIGNED NOT NULL,
10 | `related_id` int(11) UNSIGNED ,
11 | `seller_id` int(11) UNSIGNED NOT NULL,
12 | `status` int(11) UNSIGNED NOT NULL,
13 | `source` varchar(255),
14 | `type` int(11) UNSIGNED NOT NULL,
15 | `uid_support` varchar(255) NOT NULL UNIQUE,
16 | `uid_pay` varchar(255) NOT NULL UNIQUE,
17 | `uuid` varchar(255) NOT NULL UNIQUE
18 | )
19 | ;
20 | ALTER TABLE `transaction` ADD CONSTRAINT `buyer_id_refs_id_buyer`
21 | FOREIGN KEY (`buyer_id`) REFERENCES `buyer` (`id`);
22 | ALTER TABLE `transaction` ADD CONSTRAINT `seller_id_refs_id_seller`
23 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`);
24 | ALTER TABLE `transaction` ADD CONSTRAINT `related_id_refs_id_transaction`
25 | FOREIGN KEY (`related_id`) REFERENCES `transaction` (`id`);
26 | CREATE INDEX `transaction_buyer` ON `transaction` (`buyer_id`);
27 | CREATE INDEX `transaction_related` ON `transaction` (`related_id`);
28 | CREATE INDEX `transaction_seller` ON `transaction` (`seller_id`);
29 | CREATE INDEX `transaction_source` ON `transaction` (`source`);
30 |
--------------------------------------------------------------------------------
/migrations/17-fix-transactions.sql:
--------------------------------------------------------------------------------
1 | -- Do stuff I forgot to do first time.
2 | ALTER TABLE transaction ENGINE = InnoDB AUTO_INCREMENT = 1
3 | DEFAULT CHARSET = utf8 COLLATE=utf8_unicode_ci;
4 |
5 | -- Make these columns unique when combined with provider.
6 | ALTER TABLE transaction DROP INDEX uid_pay;
7 | ALTER TABLE transaction DROP INDEX uid_support;
8 | ALTER TABLE transaction ADD INDEX (`uid_pay`, `provider`);
9 | ALTER TABLE transaction ADD INDEX (`uid_support`, `provider`);
10 |
11 | -- Make uid_support nullable;
12 | ALTER TABLE transaction modify uid_support varchar(255);
13 |
--------------------------------------------------------------------------------
/migrations/19-fix-product-bango.sql:
--------------------------------------------------------------------------------
1 | DELETE FROM seller_product_bango;
2 | ALTER TABLE seller_product_bango ADD COLUMN uuid varchar(255) NOT NULL UNIQUE;
3 |
--------------------------------------------------------------------------------
/migrations/20-fix-transactions.sql:
--------------------------------------------------------------------------------
1 | DELETE FROM `transaction`;
2 | ALTER TABLE `transaction` DROP FOREIGN KEY `seller_id_refs_id_seller`;
3 | ALTER TABLE `transaction` DROP COLUMN `seller_id`;
4 | ALTER TABLE `transaction` ADD COLUMN `seller_product_id` int(11) unsigned NOT NULL;
5 | ALTER TABLE `transaction` ADD CONSTRAINT `seller_product_id_refs_id_product`
6 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`);
7 |
--------------------------------------------------------------------------------
/migrations/21-fix-seller-product.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_product_bango DROP COLUMN uuid;
2 | DELETE FROM seller_product_bango;
3 | DELETE FROM seller_product;
4 | ALTER TABLE seller_product ADD COLUMN external_id varchar(255) NOT NULL;
5 | ALTER TABLE seller_product ADD UNIQUE (`seller_id`, `external_id`);
6 | CREATE INDEX `seller_product_d5e787` ON `seller_product` (`external_id`);
7 |
--------------------------------------------------------------------------------
/migrations/22-fix-product-bango.sql:
--------------------------------------------------------------------------------
1 | # seller_bango is many-to-one and should not be unique.
2 | ALTER TABLE seller_product_bango DROP FOREIGN KEY seller_bango_id_refs_id_bango;
3 | ALTER TABLE seller_product_bango DROP INDEX seller_bango_id;
4 | ALTER TABLE `seller_product_bango` ADD CONSTRAINT `seller_bango_id_refs_id_bango`
5 | FOREIGN KEY (`seller_bango_id`) REFERENCES `seller_bango` (`id`);
6 |
--------------------------------------------------------------------------------
/migrations/23-add-delayable.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `delayable` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `uuid` varchar(36) NOT NULL,
6 | `run` bool NOT NULL,
7 | `status_code` int(11) unsigned NOT NULL,
8 | `content` longtext NOT NULL
9 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
10 |
--------------------------------------------------------------------------------
/migrations/24-add-pin-confirmed-to-buyer.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `pin_confirmed` boolean NOT NULL DEFAULT 0;
2 |
--------------------------------------------------------------------------------
/migrations/25-add-active.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `active` bool NOT NULL DEFAULT True;
2 | CREATE INDEX `buyer_active` ON `buyer` (`active`);
3 | ALTER TABLE `seller` ADD COLUMN `active` bool NOT NULL DEFAULT True;
4 | CREATE INDEX `seller_active` ON `buyer` (`active`);
5 |
--------------------------------------------------------------------------------
/migrations/26-amount-optional.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE transaction MODIFY amount decimal(9,2);
2 |
--------------------------------------------------------------------------------
/migrations/27-add-notes.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE transaction ADD COLUMN notes longtext;
2 |
--------------------------------------------------------------------------------
/migrations/28-add-reset-to-buyer.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `new_pin` varchar(255);
2 | ALTER TABLE `buyer` ADD COLUMN `needs_pin_reset` boolean NOT NULL DEFAULT 0;
3 |
--------------------------------------------------------------------------------
/migrations/29-add-pin-locking.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `pin_failures` integer NOT NULL;
2 | ALTER TABLE `buyer` ADD COLUMN `pin_locked_out` datetime;
3 |
--------------------------------------------------------------------------------
/migrations/30-add-sbi-expiration.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_bango ADD COLUMN sbi_expires datetime;
2 |
--------------------------------------------------------------------------------
/migrations/31-add-public-id.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_product ADD COLUMN public_id varchar(255) NOT NULL;
2 |
--------------------------------------------------------------------------------
/migrations/32-populate-public-id.py:
--------------------------------------------------------------------------------
1 | # Removed to be able to run future migrations altering the SellerProduct model.
2 |
--------------------------------------------------------------------------------
/migrations/33-add-unique.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_product MODIFY public_id varchar(255) NOT NULL UNIQUE;
2 |
--------------------------------------------------------------------------------
/migrations/34-seller-product-access.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_product ADD COLUMN access int(11) UNSIGNED NOT NULL DEFAULT 1;
2 | ALTER TABLE seller_product ALTER COLUMN access DROP DEFAULT;
3 |
--------------------------------------------------------------------------------
/migrations/35-add-pin-was-locked-to-buyer.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `pin_was_locked_out` boolean NOT NULL DEFAULT 0;
2 |
--------------------------------------------------------------------------------
/migrations/36-add-bango-status.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `status_bango` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `status` int(11) NOT NULL,
6 | `errors` longtext NOT NULL,
7 | `seller_product_bango_id` int(11) unsigned NOT NULL
8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
9 |
10 | ALTER TABLE `status_bango` ADD CONSTRAINT `seller_product_bango_id_refs_id_1c77c54e` FOREIGN KEY (`seller_product_bango_id`) REFERENCES `seller_product_bango` (`id`);
11 | CREATE INDEX `status_bango_eae63669` ON `status_bango` (`seller_product_bango_id`);
12 |
--------------------------------------------------------------------------------
/migrations/37-add-counter-field.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
2 | ALTER TABLE `buyer_paypal` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
3 | ALTER TABLE `delayable` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
4 | ALTER TABLE `seller` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
5 | ALTER TABLE `seller_bango` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
6 | ALTER TABLE `seller_paypal` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
7 | ALTER TABLE `seller_product` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
8 | ALTER TABLE `seller_product_bango` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
9 | ALTER TABLE `status_bango` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
10 | ALTER TABLE `transaction` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0;
11 |
--------------------------------------------------------------------------------
/migrations/38-alter-uid-pay.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE transaction CHANGE COLUMN uid_pay uid_pay varchar(255);
2 |
--------------------------------------------------------------------------------
/migrations/39-add-transaction-log.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `transaction_log` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `counter` bigint,
6 | `transaction_id` int(11) unsigned NOT NULL,
7 | `type` int(11) unsigned NOT NULL
8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
9 |
10 | ALTER TABLE `transaction_log` ADD CONSTRAINT `transaction_id_id` FOREIGN KEY (`transaction_id`) REFERENCES `transaction` (`id`);
11 |
--------------------------------------------------------------------------------
/migrations/40-add-region-carrier.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `transaction` ADD COLUMN `carrier` varchar(255);
2 | ALTER TABLE `transaction` ADD COLUMN `region` varchar(255);
3 |
--------------------------------------------------------------------------------
/migrations/41-add-seller-boku.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `seller_boku` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `counter` bigint,
6 | `seller_id` int(11) unsigned NOT NULL UNIQUE,
7 | `merchant_id` varchar(255) NOT NULL,
8 | `service_id` varchar(255) NOT NULL
9 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
10 |
11 | ALTER TABLE `seller_boku` ADD CONSTRAINT `seller_id_refs_id_d3a72381` FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`);
12 |
--------------------------------------------------------------------------------
/migrations/42-transaction-pay-url.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `transaction` ADD COLUMN `pay_url` varchar(255) NULL;
2 |
--------------------------------------------------------------------------------
/migrations/43-remove-merchant-id.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `seller_boku` DROP COLUMN `merchant_id`;
2 |
--------------------------------------------------------------------------------
/migrations/44-add-product-boku.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `seller_product_boku` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `counter` bigint,
6 | `seller_product_id` int(11) unsigned NOT NULL UNIQUE,
7 | `seller_boku_id` int(11) unsigned NOT NULL UNIQUE
8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
9 |
10 | ALTER TABLE `seller_product_boku` ADD CONSTRAINT `seller_product_id_refs_id_boku`
11 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`);
12 | ALTER TABLE `seller_product_boku` ADD CONSTRAINT `seller_boku_id_refs_id_boku`
13 | FOREIGN KEY (`seller_boku_id`) REFERENCES `seller_boku` (`id`);
14 |
--------------------------------------------------------------------------------
/migrations/45-add-seller-to-transactions.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `transaction` ADD COLUMN `seller_id` int(11) unsigned NULL;
2 | ALTER TABLE `transaction` ADD CONSTRAINT `seller_id_refs_id_seller`
3 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`);
4 |
--------------------------------------------------------------------------------
/migrations/46-remove-unique-constraint-for-boku.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `seller_product_boku`
2 | DROP FOREIGN KEY `seller_boku_id_refs_id_boku`,
3 | DROP INDEX `seller_boku_id`;
4 | ALTER TABLE `seller_product_boku`
5 | ADD INDEX `seller_boku_id` (`seller_boku_id`),
6 | ADD CONSTRAINT `seller_boku_id_refs_id_boku` FOREIGN KEY (`seller_boku_id`) REFERENCES `seller_boku` (`id`);
7 |
--------------------------------------------------------------------------------
/migrations/47-add-product-reference.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `seller_reference` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `counter` bigint,
6 | `seller_id` int(11) unsigned NOT NULL UNIQUE,
7 | `merchant_id` varchar(255) NOT NULL
8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
9 |
10 | ALTER TABLE `seller_reference` ADD CONSTRAINT `seller_id_refs_id_reference` FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`);
11 |
12 | CREATE TABLE `seller_product_reference` (
13 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
14 | `created` datetime NOT NULL,
15 | `modified` datetime NOT NULL,
16 | `counter` bigint,
17 | `seller_product_id` int(11) unsigned NOT NULL UNIQUE,
18 | `seller_reference_id` int(11) unsigned NOT NULL UNIQUE
19 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
20 |
21 | ALTER TABLE `seller_product_reference` ADD CONSTRAINT `seller_product_id_refs_id_reference`
22 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`);
23 | ALTER TABLE `seller_product_reference` ADD CONSTRAINT `seller_reference_id_refs_id_reference`
24 | FOREIGN KEY (`seller_reference_id`) REFERENCES `seller_reference` (`id`);
25 |
--------------------------------------------------------------------------------
/migrations/48-add-email-to-buyer.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE buyer ADD COLUMN `email` longtext;
2 |
--------------------------------------------------------------------------------
/migrations/49-add-reference-id.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `seller_product_reference` ADD COLUMN `reference_id` VARCHAR(255) NOT NULL;
2 | ALTER TABLE `seller_reference` CHANGE `merchant_id` `reference_id` VARCHAR(255) NOT NULL;
3 |
--------------------------------------------------------------------------------
/migrations/50-fix-seller-product-reference.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE seller_product_reference DROP FOREIGN KEY seller_reference_id_refs_id_reference;
2 | # Remove the unique index.
3 | DROP INDEX seller_reference_id ON seller_product_reference;
4 | ALTER TABLE `seller_product_reference` ADD CONSTRAINT `seller_reference_id_refs_id_reference`
5 | FOREIGN KEY (`seller_reference_id`) REFERENCES `seller_reference` (`id`);
6 |
--------------------------------------------------------------------------------
/migrations/51-make-provider-optional.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE transaction CHANGE COLUMN `seller_product_id` `seller_product_id` int(11) unsigned DEFAULT NULL;
2 | ALTER TABLE transaction CHANGE COLUMN `provider` `provider` int(11) unsigned DEFAULT NULL;
3 |
--------------------------------------------------------------------------------
/migrations/52-add-error-reason.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE transaction ADD COLUMN status_reason varchar(255) DEFAULT NULL;
2 |
--------------------------------------------------------------------------------
/migrations/53-remove-paypal.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE seller_paypal;
2 |
--------------------------------------------------------------------------------
/migrations/54-add-braintree-buyer.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `buyer_braintree` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `counter` bigint,
6 | `active` bool NOT NULL,
7 | `braintree_id` varchar(255) NOT NULL UNIQUE,
8 | `buyer_id` int(11) UNSIGNED NOT NULL UNIQUE
9 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
10 | ALTER TABLE `buyer_braintree` ADD CONSTRAINT `buyer_id` FOREIGN KEY (`buyer_id`) REFERENCES `buyer` (`id`);
11 |
--------------------------------------------------------------------------------
/migrations/55-add-payment-method.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `braintree_pay_method` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `counter` bigint,
6 | `active` bool NOT NULL,
7 | `braintree_buyer_id` int(11) UNSIGNED NOT NULL,
8 | `provider_id` varchar(255) NOT NULL,
9 | `type` integer UNSIGNED NOT NULL,
10 | `type_name` varchar(255) NOT NULL,
11 | `truncated_id` varchar(255) NOT NULL
12 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
13 | ALTER TABLE `braintree_pay_method` ADD CONSTRAINT `braintree_buyer_id_refs_id_f404a9d7` FOREIGN KEY (`braintree_buyer_id`) REFERENCES `buyer_braintree` (`id`);
14 | CREATE INDEX `braintree_pay_method_ab6bc121` ON `braintree_pay_method` (`braintree_buyer_id`);
15 |
--------------------------------------------------------------------------------
/migrations/56-add-subscriptions.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `braintree_subscription` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime NOT NULL,
4 | `modified` datetime NOT NULL,
5 | `counter` bigint,
6 | `active` bool NOT NULL,
7 | `paymethod_id` int(11) unsigned NOT NULL,
8 | `seller_product_id` int(11) unsigned NOT NULL,
9 | `provider_id` varchar(255) NOT NULL,
10 | UNIQUE (`paymethod_id`, `seller_product_id`)
11 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
12 | ALTER TABLE `braintree_subscription` ADD CONSTRAINT `seller_product_id_refs_id_b5dd89a4` FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`);
13 | ALTER TABLE `braintree_subscription` ADD CONSTRAINT `paymethod_id_refs_id_1812d9f7` FOREIGN KEY (`paymethod_id`) REFERENCES `braintree_pay_method` (`id`);
14 | CREATE INDEX `braintree_subscription_2bb89b7c` ON `braintree_subscription` (`paymethod_id`);
15 | CREATE INDEX `braintree_subscription_d18be639` ON `braintree_subscription` (`seller_product_id`);
16 |
--------------------------------------------------------------------------------
/migrations/57-add-braintree-transaction.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `braintree_transaction` (
2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY,
3 | `created` datetime(6) NOT NULL,
4 | `modified` datetime(6) NOT NULL,
5 | `counter` bigint,
6 | `transaction_id` int(11) unsigned NOT NULL UNIQUE,
7 | `paymethod_id` int(11) unsigned NOT NULL,
8 | `subscription_id` int(11) unsigned NOT NULL,
9 | `billing_period_end_date` datetime(6) NOT NULL,
10 | `billing_period_start_date` datetime(6) NOT NULL,
11 | `kind` varchar(255) NOT NULL,
12 | `next_billing_date` datetime(6) NOT NULL,
13 | `next_billing_period_amount` numeric(9, 2)
14 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
15 | ALTER TABLE `braintree_transaction` ADD CONSTRAINT `subscription_id_refs_id_76dfe860` FOREIGN KEY (`subscription_id`) REFERENCES `braintree_subscription` (`id`);
16 | ALTER TABLE `braintree_transaction` ADD CONSTRAINT `paymethod_id_refs_id_e9b15dff` FOREIGN KEY (`paymethod_id`) REFERENCES `braintree_pay_method` (`id`);
17 | ALTER TABLE `braintree_transaction` ADD CONSTRAINT `transaction_id_refs_id_35773a94` FOREIGN KEY (`transaction_id`) REFERENCES `transaction` (`id`);
18 | CREATE INDEX `braintree_transaction_2bb89b7c` ON `braintree_transaction` (`paymethod_id`);
19 | CREATE INDEX `braintree_transaction_b75baf19` ON `braintree_transaction` (`subscription_id`);
20 |
--------------------------------------------------------------------------------
/migrations/58-add-locale.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `locale` VARCHAR(255);
2 |
--------------------------------------------------------------------------------
/migrations/59-buyer-optional.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `paymethod_id` `paymethod_id` int(11) unsigned;
2 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `subscription_id` `subscription` int(11) unsigned;
3 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `billing_period_end_date` `billing_period_end_date` datetime(6);
4 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `billing_period_start_date` `billing_period_start_date` datetime(6);
5 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `next_billing_date` `next_billing_date` datetime(6);
6 |
--------------------------------------------------------------------------------
/migrations/60-fix-subscription.sql:
--------------------------------------------------------------------------------
1 | # This was accidentally changed in https://github.com/mozilla/solitude/pull/535
2 | # This patch puts it back.
3 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `subscription` `subscription_id` int(11) unsigned;
4 |
--------------------------------------------------------------------------------
/migrations/61-bt-subsription-amount.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE braintree_subscription ADD COLUMN `amount` numeric(9, 2);
2 |
--------------------------------------------------------------------------------
/migrations/62-authenticated-buyer.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `authenticated` boolean NOT NULL DEFAULT 1;
2 |
--------------------------------------------------------------------------------
/migrations/63-buyer-email-sig.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `buyer` ADD COLUMN `email_sig` varchar(255);
2 |
--------------------------------------------------------------------------------
/migrations/64-populate-email-hash.py:
--------------------------------------------------------------------------------
1 | from lib.buyers.models import Buyer
2 | from solitude.logger import getLogger
3 |
4 | log = getLogger(__name__)
5 |
6 |
7 | def run():
8 | for buyer in Buyer.objects.values('id', 'email'):
9 | if not buyer['email']:
10 | continue
11 |
12 | log.info('Updating email_sig for {}'.format(buyer['id']))
13 | Buyer.objects.get(pk=buyer['id']).save()
14 |
--------------------------------------------------------------------------------
/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/migrations/__init__.py
--------------------------------------------------------------------------------
/migrations/schematic_settings.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5 |
6 | # Set up playdoh.
7 | import manage # noqa
8 | from django.conf import settings
9 |
10 | config = settings.DATABASES['default']
11 | config['HOST'] = config.get('HOST', 'localhost')
12 | config['PORT'] = config.get('PORT', '3306')
13 |
14 | if not config['HOST'] or config['HOST'].endswith('.sock'):
15 | """Oh, you meant 'localhost'!"""
16 | config['HOST'] = 'localhost'
17 |
18 | s = 'mysql --silent {NAME} -h{HOST} -u{USER}'
19 |
20 | if config['PASSWORD']:
21 | os.environ['MYSQL_PWD'] = config['PASSWORD']
22 | del config['PASSWORD']
23 |
24 | if config['PORT']:
25 | s += ' -P{PORT}'
26 | else:
27 | del config['PORT']
28 |
29 | db = s.format(**config)
30 | table = 'schema_version'
31 | handlers = {'.py': sys.executable + ' -B manage.py runscript migrations.%s'}
32 |
--------------------------------------------------------------------------------
/requirements/compiled.txt:
--------------------------------------------------------------------------------
1 | # pyrepo (for docker and fabfile)
2 | # sha256: GgH9vZMrh5fOLSq3tfsLLLsLZqizC1qbEOPiOzhd4GM
3 | # pyrepo/travis (for travis)
4 | # sha256: eSnjUWPzFnUf4tM90UBXKeP6cJ-U_BopmJ7-gnFwXzk
5 | M2Crypto==0.22.3
6 |
--------------------------------------------------------------------------------
/requirements/dev.txt:
--------------------------------------------------------------------------------
1 | # This file pulls in everything a developer needs. If it's a basic package
2 | # needed to run the site, it belongs in requirements/prod.txt. If it's a
3 | # package for developers (testing, docs, etc.), it goes in this file.
4 |
5 | -r test.txt
6 |
7 | # sha256: bpQ8fq1eguaZWQ5Uh551R1RA4bTk2gfsHkyiNCdT4bw
8 | autopep8==1.1.1
9 |
10 | # sha256: d8kqqvXXiwo6jDT-0cYikggqtS4mHE831h2S4MO3vy8
11 | baked==0.2.5
12 |
--------------------------------------------------------------------------------
/requirements/docs.txt:
--------------------------------------------------------------------------------
1 | # For buildings docs (also used by RTD)
2 |
3 | # sha256: BwPB6lpq8LttDOwkcIMBM0lJ1W68f5XGQCjZxm-djV0
4 | alabaster==0.7.3
5 |
6 | # sha256: nwLQNXGE3h8JPBABK1LnRUoQCL5qXBhat6Mwes6x0S4
7 | babel==1.3
8 |
9 | # sha256: rkt0OyT9AsmZlDoV8BSzl2KoeKmdk38QK_aBpINhP6M
10 | docutils==0.12
11 |
12 | # sha256: LiSsXQBNtXFJdqBKwOgMbfbkfpjDVMssDYL4h51Pj9s
13 | Jinja2==2.7.3
14 |
15 | # sha256: RDoLYzhx6Cc4e8xA4mqZdKbsmJHjZWRUOPBsszhuv2Q
16 | MarkupSafe==0.23
17 |
18 | # sha256: CjoiZeHvsN76ci0fQpcw3J35da3DxgrH0rQyvfA8BB0
19 | Pygments==2.0.2
20 |
21 | # sha256: NYsFoMJgXBXPgzfRcBbrla_qrQfJAN-cLN524hlKklg
22 | pytz==2015.2
23 |
24 | # sha256: QYqTw5en7asj5ViNvAZ6x0pyPts9VBvUk295R252Rdo
25 | six==1.9.0
26 |
27 | # sha256: FcW-zVBL1OnVt8BieEaXBrtZ343SoriwJ4D0wUgL7_4
28 | snowballstemmer==1.1.0
29 |
30 | # sha256: Ld8Y2jsGIfpD_uS3KQ2grnibRvuJkniorMzaGVxJeac
31 | Sphinx==1.3.1
32 |
33 | # sha256: qKLiOalzG2hcfepiSF3sgc7zYB6-UKd6lalr3yl6-_c
34 | sphinx-rtd-theme==0.1.7
35 |
36 | # sha256: alM5uT6s8XI2Wnzex-CtWm5YNJ_BNu9umUx0nKXGG6c
37 | sphinxcontrib-httpdomain==1.3.0
38 |
--------------------------------------------------------------------------------
/requirements/test.txt:
--------------------------------------------------------------------------------
1 | # This file pulls in everything Jenkins needs for CI. If it's a basic package
2 | # needed to run the site, it belongs in requirements/prod.txt. If it's a
3 | # package for developers (testing, docs, etc.), it goes in dev.txt.
4 | # If it's something we only need on Jenkins it goes here.
5 | -r prod.txt
6 |
7 | # Flake8 requirements
8 | # sha256: yZzJcW1mVdnIvLHndjK4YVvwq9KC16vZ9cIUjK1_xmk
9 | flake8==2.3.0
10 |
11 | # sha256: rEVxaVwQzhU2vNuhopS58tPmzJ0OoXG2fVCghkzj4EI
12 | pyflakes==0.8.1
13 |
14 | # sha256: Yuh_1UU1-5MrSk2Uho21IyV6EDHIvGvTWMIBVDPmRts
15 | pep8==1.5.7
16 |
17 | # sha256: ic9r9P9kT_i5EuL6fiq7aNV6jn6Rjjk8coQNMNLrzws
18 | mccabe==0.3
19 |
20 | # sha256: Utw-E4etWsB_KruzYtWovPUmUW7UoFF6ay6EGr2O6rw
21 | # sha256: ku47hrXBkhwtnawhe--JkMX4Usi8TZAYyv4PPzuBzTo
22 | nosenicedots==0.5
23 |
24 | # sha256: Zf-5k0lWWuoQyYANEgM4TUhuwUp8gz31dVdsRfTp8Dk
25 | # sha256: c0XZOWs_PApljFy328eZw6lq-kgpnZbZeB6rprqwt7A
26 | nose-blockage==0.1.2
27 |
--------------------------------------------------------------------------------
/samples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/samples/__init__.py
--------------------------------------------------------------------------------
/samples/lib.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pprint
3 | import sys
4 |
5 | import requests
6 |
7 |
8 | # TODO: rewrite this to use curling?
9 | def call(root, url, method, data):
10 | method = getattr(requests, method)
11 | url = root + url
12 | print method.__name__.upper(), url
13 | print 'Request data:'
14 | pprint.pprint(data)
15 | data = json.dumps(data)
16 | result = method(url, data=data,
17 | headers={
18 | 'content-type': 'application/json',
19 | 'accept': 'application/json',
20 | })
21 | print 'Status code:', result.status_code
22 | if result.status_code not in (200, 201, 202, 204):
23 | print 'Error:'
24 | try:
25 | print pprint.pprint(json.loads(result.content))
26 | except ValueError:
27 | print result.content
28 | sys.exit()
29 |
30 | if result.content:
31 | print 'Response data:'
32 | data = result.json() if callable(result.json) else result.json
33 | pprint.pprint(data)
34 | print
35 | return data
36 |
37 | print
38 |
--------------------------------------------------------------------------------
/samples/readme.txt:
--------------------------------------------------------------------------------
1 | A list of samples of doing certain tasks.
2 |
--------------------------------------------------------------------------------
/solitude/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/__init__.py
--------------------------------------------------------------------------------
/solitude/constants.py:
--------------------------------------------------------------------------------
1 | # From zamboni, the different payment types that zamboni knows about.
2 | PAYMENT_METHOD_OPERATOR = 0
3 | PAYMENT_METHOD_CARD = 1
4 | PAYMENT_METHOD_ALL = 2
5 |
6 | PAYMENT_METHOD_CHOICES = (
7 | PAYMENT_METHOD_OPERATOR,
8 | PAYMENT_METHOD_CARD,
9 | PAYMENT_METHOD_ALL
10 | )
11 |
12 | # Not including "all" which is really "both" because for Payment Methods that
13 | # doesn't make sense.
14 | SINGLE_PAYMENT_METHOD = (
15 | (PAYMENT_METHOD_OPERATOR, PAYMENT_METHOD_OPERATOR),
16 | (PAYMENT_METHOD_OPERATOR, PAYMENT_METHOD_CARD)
17 | )
18 |
--------------------------------------------------------------------------------
/solitude/errors.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | from rest_framework.exceptions import ParseError
4 |
5 |
6 | class ErrorFormatter(object):
7 |
8 | def __init__(self, error):
9 | self.error = error
10 |
11 |
12 | class MozillaFormatter(ErrorFormatter):
13 |
14 | def format(self):
15 | errors = defaultdict(list)
16 | for k, error in self.error.detail.as_data().items():
17 | for v in error:
18 | errors[k].append({
19 | 'code': v.code,
20 | 'message': unicode(v.message)
21 | })
22 | return {'mozilla': dict(errors)}
23 |
24 |
25 | class FormError(ParseError):
26 | status_code = 422
27 | default_detail = 'Error parsing form.'
28 | formatter = MozillaFormatter
29 |
30 |
31 | class InvalidQueryParams(ParseError):
32 | status_code = 400
33 | default_detail = 'Incorrect query parameters.'
34 |
--------------------------------------------------------------------------------
/solitude/exceptions.py:
--------------------------------------------------------------------------------
1 |
2 | from django.db.transaction import set_rollback
3 |
4 | from rest_framework.response import Response
5 | from rest_framework.views import exception_handler
6 |
7 | from lib.bango.errors import BangoImmediateError
8 | from solitude.logger import getLogger
9 |
10 | log = getLogger('s')
11 |
12 |
13 | def custom_exception_handler(exc):
14 | # If you raise an error in solitude, it comes to here and
15 | # we rollback the transaction.
16 | log.info('Handling exception, about to roll back for: {}, {}'
17 | .format(type(exc), exc.message))
18 | set_rollback(True)
19 |
20 | if hasattr(exc, 'formatter'):
21 | try:
22 | return Response(exc.formatter(exc).format(),
23 | status=getattr(exc, 'status_code', 422))
24 | except:
25 | # If the formatter fails, fall back to the standard
26 | # error formatting.
27 | log.exception('Failed to use formatter.')
28 |
29 | if isinstance(exc, BangoImmediateError):
30 | return Response(exc.message, status=400)
31 |
32 | return exception_handler(exc)
33 |
--------------------------------------------------------------------------------
/solitude/fields.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 |
4 | class ListField(forms.CharField):
5 |
6 | def clean(self, value):
7 | if not isinstance(value, list):
8 | raise forms.ValidationError('Invalid list.')
9 | return value
10 |
--------------------------------------------------------------------------------
/solitude/filter.py:
--------------------------------------------------------------------------------
1 | from rest_framework.filters import DjangoFilterBackend
2 |
3 | from solitude.errors import InvalidQueryParams
4 | from solitude.logger import getLogger
5 |
6 | log = getLogger('s.filter')
7 |
8 |
9 | class StrictQueryFilter(DjangoFilterBackend):
10 |
11 | """
12 | Don't allow people to typo request params and return all the objects.
13 | Instead limit it down to the parameters allowed in filter_fields.
14 | """
15 |
16 | def get_filter_class(self, view, queryset=None):
17 | klass = (super(StrictQueryFilter, self)
18 | .get_filter_class(view, queryset=queryset))
19 | try:
20 | # If an ordering exists on the model, use that.
21 | klass._meta.order_by = klass.Meta.model.Meta.ordering
22 | except AttributeError:
23 | pass
24 | return klass
25 |
26 | def filter_queryset(self, request, queryset, view):
27 | requested = set(request.QUERY_PARAMS.keys())
28 | allowed = set(getattr(view, 'filter_fields', []))
29 | difference = requested.difference(allowed)
30 | if difference:
31 | raise InvalidQueryParams(
32 | detail='Incorrect query parameters: ' + ','.join(difference))
33 |
34 | return (super(StrictQueryFilter, self)
35 | .filter_queryset(request, queryset, view))
36 |
--------------------------------------------------------------------------------
/solitude/locale/en_US/LC_MESSAGES/messages.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: PACKAGE VERSION\n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2011-05-26 18:11-0700\n"
6 | "PO-Revision-Date: 2011-05-26 18:11-0700\n"
7 | "Last-Translator: Automatically generated\n"
8 | "Language-Team: none\n"
9 | "Language: en_US\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 | "X-Generator: Translate Toolkit 1.8.0\n"
14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
15 |
16 | #: apps/examples/templates/examples/home.html:5
17 | msgid "Hello world"
18 | msgstr "Hello world"
19 |
20 | #. This is a localizer comment
21 | #: apps/examples/templates/examples/home.html:9
22 | msgid "This is a test view."
23 | msgstr "This is a test view."
24 |
25 | #: apps/examples/templates/examples/home.html:11
26 | msgid "Learn you some Playdoh and then go build something awesome."
27 | msgstr "Learn you some Playdoh and then go build something awesome."
28 |
29 | #: apps/examples/templates/examples/home.html:17
30 | msgid "Current locale: %(LANG)s.
Available locales: %(langs)s."
31 | msgstr "Current locale: %(LANG)s.
Available locales: %(langs)s."
32 |
--------------------------------------------------------------------------------
/solitude/locale/fr/LC_MESSAGES/messages.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: PACKAGE VERSION\n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2011-06-03 19:07-0700\n"
6 | "Last-Translator: Automatically generated\n"
7 | "Language-Team: none\n"
8 | "Language: fr\n"
9 | "MIME-Version: 1.0\n"
10 | "Content-Type: text/plain; charset=UTF-8\n"
11 | "Content-Transfer-Encoding: 8bit\n"
12 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
13 |
14 | #: apps/examples/templates/examples/home.html:5
15 | msgid "Hello world"
16 | msgstr "Bonjour le monde"
17 |
18 | #. This is a localizer comment
19 | #: apps/examples/templates/examples/home.html:9
20 | msgid "This is a test view."
21 | msgstr "Ceci est une vue de test."
22 |
23 | #: apps/examples/templates/examples/home.html:11
24 | msgid "Learn you some Playdoh and then go build something awesome."
25 | msgstr "Apprends à jouer avec Playdoh et construis quelque chose de génial."
26 |
27 | #: apps/examples/templates/examples/home.html:17
28 | msgid "Current locale: %(LANG)s.
Available locales: %(langs)s."
29 | msgstr "Langue active : %(LANG)s.
Langues disponibles : %(langs)s."
30 |
--------------------------------------------------------------------------------
/solitude/locale/templates/LC_MESSAGES/messages.pot:
--------------------------------------------------------------------------------
1 | #, fuzzy
2 | msgid ""
3 | msgstr ""
4 | "Project-Id-Version: PACKAGE VERSION\n"
5 | "Report-Msgid-Bugs-To: \n"
6 | "POT-Creation-Date: 2011-06-03 19:07-0700\n"
7 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
8 | "Last-Translator: FULL NAME \n"
9 | "Language-Team: LANGUAGE \n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=utf-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 | "X-Generator: Translate Toolkit 1.8.0\n"
14 |
15 | #: apps/examples/templates/examples/home.html:5
16 | msgid "Hello world"
17 | msgstr ""
18 |
19 | #. This is a localizer comment
20 | #: apps/examples/templates/examples/home.html:9
21 | msgid "This is a test view."
22 | msgstr ""
23 |
24 | #: apps/examples/templates/examples/home.html:11
25 | msgid ""
26 | "Learn you some Playdoh and then go build "
27 | "something awesome."
28 | msgstr ""
29 |
30 | #: apps/examples/templates/examples/home.html:17
31 | msgid "Current locale: %(LANG)s.
Available locales: %(langs)s."
32 | msgstr ""
33 |
--------------------------------------------------------------------------------
/solitude/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.conf import settings
4 |
5 | from solitude.middleware import get_oauth_key, get_transaction_id
6 |
7 |
8 | def getLogger(name=None):
9 | logger = logging.getLogger(name)
10 | return SolitudeAdapter(logger)
11 |
12 |
13 | # This really should be fulfilled by a logging filter which would remove the
14 | # need to do all this crap. However I've got no idea how to do that and I
15 | # wasted far too long on this.
16 | class SolitudeAdapter(logging.LoggerAdapter):
17 |
18 | """Adds OAuth user and transaction id to every logging message's kwargs."""
19 |
20 | def __init__(self, logger, extra=None):
21 | logging.LoggerAdapter.__init__(self, logger, extra or {})
22 |
23 | def process(self, msg, kwargs):
24 | kwargs['extra'] = {'OAUTH_KEY': get_oauth_key(),
25 | 'TRANSACTION_ID': get_transaction_id()}
26 | return msg, kwargs
27 |
28 |
29 | class SolitudeFormatter(logging.Formatter):
30 |
31 | def format(self, record):
32 | tag = '%s: ' % settings.SYSLOG_TAG
33 | if not self._fmt.startswith(tag):
34 | self._fmt = '%s %s' % (tag, self._fmt)
35 | for name in 'OAUTH_KEY', 'TRANSACTION_ID':
36 | record.__dict__.setdefault(name, '')
37 | return logging.Formatter.format(self, record)
38 |
--------------------------------------------------------------------------------
/solitude/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/management/__init__.py
--------------------------------------------------------------------------------
/solitude/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/management/commands/__init__.py
--------------------------------------------------------------------------------
/solitude/management/commands/push_s3.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from optparse import make_option
4 |
5 | from django.conf import settings
6 | from django.core.management.base import BaseCommand
7 |
8 | import boto
9 | from boto.s3.key import Key
10 | from solitude.logger import getLogger
11 |
12 | log = getLogger('s.s3')
13 |
14 |
15 | def push(source):
16 | if not all(settings.S3_AUTH.values() + [settings.S3_BUCKET, ]):
17 | print 'Settings incomplete, cannot push to S3.'
18 | sys.exit(1)
19 |
20 | dest = os.path.basename(source)
21 | conn = boto.connect_s3(settings.S3_AUTH['key'],
22 | settings.S3_AUTH['secret'])
23 | bucket = conn.get_bucket(settings.S3_BUCKET)
24 | k = Key(bucket)
25 | k.key = dest
26 | k.set_contents_from_filename(source)
27 | log.debug('Uploaded: {0} to: {1}'.format(source, dest))
28 |
29 |
30 | class Command(BaseCommand):
31 | help = 'Push a file to S3'
32 | option_list = BaseCommand.option_list + (
33 | make_option('--file', action='store', type='string', dest='file',
34 | default=''),
35 | )
36 |
37 | def handle(self, *args, **options):
38 | source = options['file']
39 | if not source or not os.path.exists(source):
40 | print 'File not found: {0}'.format(source)
41 | sys.exit(1)
42 |
43 | push(source)
44 |
--------------------------------------------------------------------------------
/solitude/management/commands/refresh_wsdl.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.conf import settings
4 | from django.core.management.base import BaseCommand
5 |
6 | import requests
7 |
8 | from lib.bango.constants import WSDL_MAP
9 |
10 | root = os.path.join(settings.ROOT, 'lib', 'bango', 'wsdl')
11 |
12 |
13 | class Command(BaseCommand):
14 | help = "Refresh the WSDLs."
15 |
16 | def handle(self, *args, **kw):
17 | for dir, wsdls in WSDL_MAP.items():
18 | for wsdl in wsdls.values():
19 | filename = wsdl['file']
20 | src = wsdl['url']
21 | dest = os.path.join(root, dir, filename)
22 | top = os.path.dirname(dest)
23 | if not os.path.exists(top):
24 | os.makedirs(top)
25 |
26 | print 'Getting', src
27 | response = requests.get(src, verify=False)
28 | if response.status_code != 200:
29 | print ('...returned a {0}, skipping'
30 | .format(response.status_code))
31 | continue
32 |
33 | open(dest, 'w').write(response.text)
34 | print '...written to', dest
35 |
--------------------------------------------------------------------------------
/solitude/middleware.py:
--------------------------------------------------------------------------------
1 | import threading
2 |
3 | _local = threading.local()
4 |
5 |
6 | def get_oauth_key():
7 | return getattr(_local, 'OAUTH_KEY', '')
8 |
9 |
10 | def get_transaction_id():
11 | return getattr(_local, 'TRANSACTION_ID', None)
12 |
13 |
14 | def set_oauth_key(key):
15 | _local.OAUTH_KEY = key
16 |
17 |
18 | class LoggerMiddleware(object):
19 |
20 | def process_request(self, request):
21 | _local.TRANSACTION_ID = request.META.get('HTTP_TRANSACTION_ID', '-')
22 | # At the beginning of the request we won't have done authentication
23 | # yet so this sets the value to anon. When authentication is completed
24 | # that will update the oauth_key with the authenticated value.
25 | set_oauth_key(getattr(request, 'OAUTH_KEY', ''))
26 |
--------------------------------------------------------------------------------
/solitude/paginator.py:
--------------------------------------------------------------------------------
1 | from rest_framework import pagination
2 | from rest_framework import serializers
3 | from rest_framework.templatetags.rest_framework import replace_query_param
4 |
5 |
6 | class NextPageField(serializers.Field):
7 |
8 | """Wrapper to remove absolute_uri."""
9 | page_field = 'page'
10 |
11 | def to_native(self, value):
12 | if not value.has_next():
13 | return None
14 | page = value.next_page_number()
15 | request = self.context.get('request')
16 | url = request and request.get_full_path() or ''
17 | return replace_query_param(url, self.page_field, page)
18 |
19 |
20 | class PreviousPageField(serializers.Field):
21 |
22 | """Wrapper to remove absolute_uri."""
23 | page_field = 'page'
24 |
25 | def to_native(self, value):
26 | if not value.has_previous():
27 | return None
28 | page = value.previous_page_number()
29 | request = self.context.get('request')
30 | url = request and request.get_full_path() or ''
31 | return replace_query_param(url, self.page_field, page)
32 |
33 |
34 | class MetaSerializer(serializers.Serializer):
35 | next = NextPageField(source='*')
36 | prev = PreviousPageField(source='*')
37 | page = serializers.Field(source='number')
38 | total_count = serializers.Field(source='paginator.count')
39 |
40 |
41 | class CustomPaginationSerializer(pagination.BasePaginationSerializer):
42 | meta = MetaSerializer(source='*') # Takes the page object as the source
43 | results_field = 'objects'
44 |
--------------------------------------------------------------------------------
/solitude/processor.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django.conf import settings
4 |
5 | from raven.processors import Processor
6 |
7 |
8 | class JSONProcessor(Processor):
9 |
10 | """
11 | This is a sentry wrapper to process the JSON and remove anything from it
12 | that could be considered as leaking sensitive data. Sentry has some
13 | processor for doing this, but they don't work with JSON posted in the body.
14 | """
15 |
16 | def process(self, data, **kwargs):
17 | http = data.get('sentry.interfaces.Http', None)
18 | if not http:
19 | return data
20 | try:
21 | http['data'] = json.dumps(sanitise(json.loads(http['data'])))
22 | except (TypeError, ValueError):
23 | # At this point we've got invalid JSON so things likely went
24 | # horribly wrong.
25 | pass
26 |
27 | return data
28 |
29 |
30 | def sanitise(data, keys=None):
31 | """Sanitises keys in a dictionary."""
32 | keys = keys or settings.SENSITIVE_DATA_KEYS
33 |
34 | def recurse(leaf):
35 | for k, v in leaf.iteritems():
36 | if isinstance(v, dict):
37 | recurse(v)
38 | if k in keys:
39 | leaf[k] = '*' * 8
40 |
41 | try:
42 | recurse(data)
43 | except AttributeError:
44 | return data
45 |
46 | return data
47 |
--------------------------------------------------------------------------------
/solitude/related_fields.py:
--------------------------------------------------------------------------------
1 | import urlparse
2 |
3 | from django.forms.fields import Field
4 |
5 | from rest_framework.relations import HyperlinkedRelatedField
6 |
7 |
8 | class RelativePathMixin(object):
9 |
10 | def get_url(self, *args, **kwargs):
11 | url = super(RelativePathMixin, self).get_url(*args, **kwargs)
12 | parsed = urlparse.urlparse(url)
13 | return parsed.path
14 |
15 |
16 | class PathRelatedField(RelativePathMixin, HyperlinkedRelatedField):
17 | pass
18 |
19 |
20 | class PathRelatedFormField(RelativePathMixin, HyperlinkedRelatedField, Field):
21 |
22 | """
23 | A variant of the PathRelatedField that can be used in Django Forms.
24 | """
25 |
26 | def __init__(self, *args, **kwargs):
27 | # This is in DRF 3.0, remove this in bug #416.
28 | self.allow_null = kwargs.pop('allow_null', False)
29 |
30 | queryset = kwargs.pop('queryset')
31 | super(PathRelatedFormField, self).__init__(*args, **kwargs)
32 | # __init__ sets queryset to be None, ensure we set it afterwards.
33 | self.queryset = queryset
34 |
35 | def to_python(self, value):
36 | if not value and self.allow_null:
37 | return None
38 | # Map the form method to the serializer version.
39 | return self.from_native(value)
40 |
--------------------------------------------------------------------------------
/solitude/settings/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import * # noqa
2 | try:
3 | from .local import * # noqa
4 | except ImportError:
5 | print 'No local.py imported, skipping.'
6 |
--------------------------------------------------------------------------------
/solitude/settings/mock-aes-sample.keys-dist:
--------------------------------------------------------------------------------
1 | this is a sample aes key file, you would never, ever use this in production, but for mock solitude on paas its just fine
2 |
--------------------------------------------------------------------------------
/solitude/settings/sample.key:
--------------------------------------------------------------------------------
1 | please change this
2 |
--------------------------------------------------------------------------------
/solitude/settings/sites/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/__init__.py
--------------------------------------------------------------------------------
/solitude/settings/sites/altdev/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/altdev/__init__.py
--------------------------------------------------------------------------------
/solitude/settings/sites/altdev/db.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import dj_database_url
6 |
7 | import private_base as private
8 |
9 | from solitude.settings import base
10 | from django_sha2 import get_password_hashers
11 |
12 |
13 | ADMINS = ()
14 | ALLOWED_HOSTS = ['payments-altdev.allizom.org', 'localhost']
15 |
16 | DATABASES = {}
17 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL)
18 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
19 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'}
20 |
21 | DEBUG = False
22 | DEBUG_PROPAGATE_EXCEPTIONS = False
23 |
24 | HMAC_KEYS = private.HMAC_KEYS
25 |
26 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
27 |
28 | LOG_LEVEL = logging.DEBUG
29 |
30 | SECRET_KEY = private.SECRET_KEY
31 |
32 | SENTRY_DSN = private.SENTRY_DSN
33 |
34 | STATSD_HOST = private.STATSD_HOST
35 | STATSD_PORT = private.STATSD_PORT
36 | STATSD_PREFIX = private.STATSD_PREFIX
37 |
38 | SYSLOG_TAG = 'http_app_payments_altdev'
39 | TEMPLATE_DEBUG = DEBUG
40 |
41 | # Solitude specific settings.
42 | AES_KEYS = private.AES_KEYS
43 |
44 | CLEANSED_SETTINGS_ACCESS = True
45 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS
46 |
47 | SITE_URL = 'https://payments-altdev.allizom.org'
48 |
49 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET}
50 | S3_BUCKET = private.S3_BUCKET
51 |
52 | NEWRELIC_INI = None
53 |
54 | # Below is configuration of payment providers.
55 |
56 | ZIPPY_PROXY = 'https://payments-proxy-altdev.allizom.org/proxy/provider'
57 | ZIPPY_CONFIGURATION = {
58 | 'reference': {
59 | 'url': 'https://zippy-dev.allizom.org'
60 | },
61 | }
62 |
63 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH
64 | BANGO_FAKE_REFUNDS = True
65 | BANGO_PROXY = private.BANGO_PROXY
66 | BANGO_NOTIFICATION_URL = (
67 | 'https://marketplace-altdev.allizom.org/mozpay/bango/notification')
68 |
69 | NOSE_PLUGINS = []
70 |
--------------------------------------------------------------------------------
/solitude/settings/sites/altdev/proxy.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import private_base as private
6 |
7 | from solitude.settings import base
8 | from django_sha2 import get_password_hashers
9 |
10 |
11 | ADMINS = ()
12 | ALLOWED_HOSTS = ['payments-proxy-altdev.allizom.org']
13 |
14 | DATABASES = {'default': {}}
15 |
16 | DEBUG = False
17 | DEBUG_PROPAGATE_EXCEPTIONS = False
18 |
19 | HMAC_KEYS = private.HMAC_KEYS
20 |
21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
22 |
23 | LOG_LEVEL = logging.DEBUG
24 |
25 | SECRET_KEY = private.SECRET_KEY
26 |
27 | SENTRY_DSN = private.SENTRY_DSN
28 |
29 | STATSD_HOST = private.STATSD_HOST
30 | STATSD_PORT = private.STATSD_PORT
31 | STATSD_PREFIX = private.STATSD_PREFIX
32 |
33 | SYSLOG_TAG = 'http_app_payments_altdev'
34 | TEMPLATE_DEBUG = DEBUG
35 |
36 | # Solitude specific settings.
37 | AES_KEYS = {}
38 |
39 | CLEANSED_SETTINGS_ACCESS = True
40 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS
41 |
42 | NEWRELIC_INI = None
43 | NOSE_PLUGINS = []
44 |
45 | REQUIRE_OAUTH = False
46 |
47 | # Below is configuration of payment providers.
48 | BANGO_ENV = 'test'
49 | BANGO_AUTH = private.BANGO_AUTH
50 |
51 | ZIPPY_CONFIGURATION = {
52 | 'reference': {
53 | 'url': 'https://zippy-dev.allizom.org',
54 | 'auth': {'key': private.ZIPPY_PAAS_KEY,
55 | 'secret': private.ZIPPY_PAAS_SECRET,
56 | 'realm': 'zippy-dev.allizom.org'}
57 | },
58 | }
59 |
--------------------------------------------------------------------------------
/solitude/settings/sites/dev/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/dev/__init__.py
--------------------------------------------------------------------------------
/solitude/settings/sites/dev/db.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import dj_database_url
6 |
7 | import private_base as private
8 |
9 | from solitude.settings import base
10 | from django_sha2 import get_password_hashers
11 |
12 |
13 | ADMINS = ()
14 | ALLOWED_HOSTS = [
15 | 'payments-dev.allizom.org',
16 | 'payments.dev.mozaws.net',
17 | 'localhost'
18 | ]
19 |
20 | DATABASES = {}
21 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL)
22 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
23 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'}
24 | DATABASES['default']['ATOMIC_REQUESTS'] = True
25 |
26 | DEBUG = False
27 | DEBUG_PROPAGATE_EXCEPTIONS = False
28 |
29 | HMAC_KEYS = private.HMAC_KEYS
30 |
31 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
32 |
33 | LOG_LEVEL = logging.DEBUG
34 |
35 | SECRET_KEY = private.SECRET_KEY
36 |
37 | SENTRY_DSN = private.SENTRY_DSN
38 |
39 | STATSD_HOST = private.STATSD_HOST
40 | STATSD_PORT = private.STATSD_PORT
41 | STATSD_PREFIX = private.STATSD_PREFIX
42 |
43 | SYSLOG_TAG = 'http_app_payments_dev'
44 | TEMPLATE_DEBUG = DEBUG
45 |
46 | # Solitude specific settings.
47 | AES_KEYS = private.AES_KEYS
48 |
49 | CLEANSED_SETTINGS_ACCESS = True
50 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS
51 |
52 | SITE_URL = 'https://payments-dev.allizom.org'
53 |
54 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET}
55 | S3_BUCKET = private.S3_BUCKET
56 |
57 | NEWRELIC_INI = '/etc/newrelic.d/payments-dev.allizom.org.ini'
58 |
59 | # Below is configuration of payment providers.
60 |
61 | ZIPPY_PROXY = 'https://payments-proxy-dev.allizom.org/proxy/provider'
62 | ZIPPY_CONFIGURATION = {
63 | 'reference': {
64 | 'url': 'https://zippy-dev.allizom.org'
65 | },
66 | }
67 |
68 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH
69 | BANGO_FAKE_REFUNDS = True
70 | BANGO_PROXY = private.BANGO_PROXY
71 | BANGO_NOTIFICATION_URL = (
72 | 'https://marketplace-dev.allizom.org/mozpay/bango/notification')
73 |
74 | NOSE_PLUGINS = []
75 |
--------------------------------------------------------------------------------
/solitude/settings/sites/dev/proxy.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import private_base as private
6 |
7 | from solitude.settings import base
8 | from django_sha2 import get_password_hashers
9 |
10 |
11 | ADMINS = ()
12 | ALLOWED_HOSTS = [
13 | 'payments-proxy-dev.allizom.org',
14 | 'payments-proxy.dev.mozaws.net'
15 | ]
16 |
17 | DATABASES = {'default': {}}
18 |
19 | DEBUG = False
20 | DEBUG_PROPAGATE_EXCEPTIONS = False
21 |
22 | HMAC_KEYS = private.HMAC_KEYS
23 |
24 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
25 |
26 | LOG_LEVEL = logging.DEBUG
27 |
28 | SECRET_KEY = private.SECRET_KEY
29 |
30 | SENTRY_DSN = private.SENTRY_DSN
31 |
32 | STATSD_HOST = private.STATSD_HOST
33 | STATSD_PORT = private.STATSD_PORT
34 | STATSD_PREFIX = private.STATSD_PREFIX
35 |
36 | SYSLOG_TAG = 'http_app_payments_dev'
37 | TEMPLATE_DEBUG = DEBUG
38 |
39 | # Solitude specific settings.
40 | AES_KEYS = {}
41 |
42 | CLEANSED_SETTINGS_ACCESS = True
43 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS
44 |
45 | NEWRELIC_INI = '/etc/newrelic.d/payments-proxy-dev.allizom.org.ini'
46 | NOSE_PLUGINS = []
47 |
48 | REQUIRE_OAUTH = False
49 |
50 | # Below is configuration of payment providers.
51 | BANGO_ENV = 'test'
52 | BANGO_AUTH = private.BANGO_AUTH
53 |
54 | ZIPPY_CONFIGURATION = {
55 | 'reference': {
56 | 'url': 'https://zippy-dev.allizom.org',
57 | 'auth': {'key': private.ZIPPY_PAAS_KEY,
58 | 'secret': private.ZIPPY_PAAS_SECRET,
59 | 'realm': 'zippy-dev.allizom.org'}
60 | },
61 | }
62 |
--------------------------------------------------------------------------------
/solitude/settings/sites/paymentsalt/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/paymentsalt/__init__.py
--------------------------------------------------------------------------------
/solitude/settings/sites/paymentsalt/db.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import dj_database_url
6 |
7 | import private_base as private
8 |
9 | from solitude.settings import base
10 | from django_sha2 import get_password_hashers
11 |
12 |
13 | ADMINS = ()
14 | ALLOWED_HOSTS = ['*']
15 |
16 | DATABASES = {}
17 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL)
18 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
19 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'}
20 | DATABASES['default']['ATOMIC_REQUESTS'] = True
21 |
22 | DEBUG = False
23 | DEBUG_PROPAGATE_EXCEPTIONS = False
24 |
25 | HMAC_KEYS = private.HMAC_KEYS
26 |
27 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
28 |
29 | LOG_LEVEL = logging.DEBUG
30 |
31 | SECRET_KEY = private.SECRET_KEY
32 |
33 | SENTRY_DSN = private.SENTRY_DSN
34 |
35 | STATSD_HOST = private.STATSD_HOST
36 | STATSD_PORT = private.STATSD_PORT
37 | STATSD_PREFIX = private.STATSD_PREFIX
38 |
39 | SYSLOG_TAG = 'http_app_payments_paymentsalt'
40 | TEMPLATE_DEBUG = DEBUG
41 |
42 | # Solitude specific settings.
43 | AES_KEYS = private.AES_KEYS
44 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS
45 |
46 | SITE_URL = 'https://payments-alt-solitude.allizom.org'
47 |
48 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET}
49 | S3_BUCKET = private.S3_BUCKET
50 |
51 | NEWRELIC_INI = '/etc/newrelic.d/payments-alt.allizom.org.ini'
52 |
53 | REQUIRE_OAUTH = True
54 |
55 | # Below is configuration of payment providers.
56 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH
57 | BANGO_ENV = 'prod'
58 | BANGO_INSERT_STAGE = 'FROM STAGE '
59 | BANGO_PROXY = private.BANGO_PROXY
60 | BANGO_NOTIFICATION_URL = (
61 | 'https://payments-alt.allizom.org/mozpay/bango/notification')
62 |
63 | ZIPPY_PROXY = 'https://payments-alt-solitude-proxy.allizom.org/proxy/provider'
64 | ZIPPY_CONFIGURATION = {
65 | 'reference': {
66 | 'url': 'https://zippy-dev.allizom.org'
67 | },
68 | }
69 |
70 | NOSE_PLUGINS = []
71 |
--------------------------------------------------------------------------------
/solitude/settings/sites/paymentsalt/proxy.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import private_base as private
6 |
7 | from solitude.settings import base
8 | from django_sha2 import get_password_hashers
9 |
10 |
11 | ADMINS = ()
12 | ALLOWED_HOSTS = ['*']
13 |
14 | DATABASES = {'default': {}}
15 |
16 | DEBUG = False
17 | DEBUG_PROPAGATE_EXCEPTIONS = False
18 |
19 | HMAC_KEYS = private.HMAC_KEYS
20 |
21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
22 |
23 | LOG_LEVEL = logging.DEBUG
24 |
25 | SECRET_KEY = private.SECRET_KEY
26 |
27 | SENTRY_DSN = private.SENTRY_DSN
28 |
29 | STATSD_HOST = private.STATSD_HOST
30 | STATSD_PORT = private.STATSD_PORT
31 | STATSD_PREFIX = private.STATSD_PREFIX
32 |
33 | SYSLOG_TAG = 'http_app_payments_paymentsalt'
34 | TEMPLATE_DEBUG = DEBUG
35 |
36 | # Solitude specific settings.
37 | AES_KEYS = {}
38 |
39 | CLEANSED_SETTINGS_ACCESS = True
40 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS
41 |
42 | NEWRELIC_INI = '/etc/newrelic.d/payments-alt-proxy.allizom.org.ini'
43 | NOSE_PLUGINS = []
44 |
45 | REQUIRE_OAUTH = False
46 | SITE_URL = 'https://payments-alt-solitude-proxy.allizom.org'
47 |
48 | # Below is configuration of payment providers.
49 | BANGO_AUTH = private.BANGO_AUTH
50 | BANGO_ENV = 'prod'
51 |
52 | ZIPPY_CONFIGURATION = {
53 | 'reference': {
54 | 'url': 'https://zippy-dev.allizom.org',
55 | 'auth': {'key': private.ZIPPY_PAAS_KEY,
56 | 'secret': private.ZIPPY_PAAS_SECRET,
57 | 'realm': 'zippy-dev.allizom.org'}
58 | },
59 | }
60 |
--------------------------------------------------------------------------------
/solitude/settings/sites/prod/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/prod/__init__.py
--------------------------------------------------------------------------------
/solitude/settings/sites/prod/db.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import dj_database_url
6 |
7 | import private_base as private
8 |
9 | from solitude.settings import base
10 | from django_sha2 import get_password_hashers
11 |
12 | ADMINS = ()
13 | ALLOWED_HOSTS = ('*',)
14 |
15 | DATABASES = {}
16 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL)
17 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
18 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'}
19 | DATABASES['default']['ATOMIC_REQUESTS'] = True
20 |
21 | DEBUG = False
22 | DEBUG_PROPAGATE_EXCEPTIONS = False
23 |
24 | HMAC_KEYS = private.HMAC_KEYS
25 |
26 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
27 |
28 | LOG_LEVEL = logging.DEBUG
29 |
30 | SECRET_KEY = private.SECRET_KEY
31 |
32 | SENTRY_DSN = private.SENTRY_DSN
33 |
34 | STATSD_HOST = private.STATSD_HOST
35 | STATSD_PORT = private.STATSD_PORT
36 | STATSD_PREFIX = private.STATSD_PREFIX
37 |
38 | SYSLOG_TAG = 'http_app_payments'
39 | TEMPLATE_DEBUG = DEBUG
40 |
41 | # Solitude specific settings.
42 | AES_KEYS = private.AES_KEYS
43 |
44 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS
45 |
46 | REQUIRE_OAUTH = True
47 |
48 | SITE_URL = 'https://payments.firefox.com'
49 |
50 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET}
51 | S3_BUCKET = private.S3_BUCKET
52 |
53 | # Below is configuration of payment providers.
54 |
55 | ZIPPY_CONFIGURATION = {}
56 |
57 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH
58 | BANGO_ENV = 'prod'
59 | BANGO_FAKE_REFUNDS = False
60 | BANGO_PROXY = private.BANGO_PROXY
61 | BANGO_NOTIFICATION_URL = (
62 | 'https://marketplace.firefox.com/mozpay/bango/notification')
63 |
64 | NOSE_PLUGINS = []
65 |
--------------------------------------------------------------------------------
/solitude/settings/sites/prod/proxy.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import private_base as private
6 |
7 | from solitude.settings import base
8 | from django_sha2 import get_password_hashers
9 |
10 |
11 | ADMINS = ()
12 | ALLOWED_HOSTS = ('*',)
13 |
14 | DATABASES = {'default': {}}
15 |
16 | DEBUG = False
17 | DEBUG_PROPAGATE_EXCEPTIONS = False
18 |
19 | HMAC_KEYS = private.HMAC_KEYS
20 |
21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
22 |
23 | LOG_LEVEL = logging.DEBUG
24 |
25 | SECRET_KEY = private.SECRET_KEY
26 |
27 | SENTRY_DSN = private.SENTRY_DSN
28 |
29 | STATSD_HOST = private.STATSD_HOST
30 | STATSD_PORT = private.STATSD_PORT
31 | STATSD_PREFIX = private.STATSD_PREFIX
32 |
33 | SYSLOG_TAG = 'http_app_payments'
34 | TEMPLATE_DEBUG = DEBUG
35 |
36 |
37 | # Solitude specific settings.
38 | AES_KEYS = {}
39 |
40 | CLEANSED_SETTINGS_ACCESS = True
41 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS
42 | NOSE_PLUGINS = []
43 |
44 | REQUIRE_OAUTH = False
45 | SITE_URL = private.SITE_URL
46 |
47 | # Below is configuration of payment providers.
48 | ZIPPY_CONFIGURATION = {}
49 |
50 | BANGO_AUTH = private.BANGO_AUTH
51 | BANGO_ENV = 'prod'
52 |
--------------------------------------------------------------------------------
/solitude/settings/sites/s3dev/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/s3dev/__init__.py
--------------------------------------------------------------------------------
/solitude/settings/sites/s3dev/db.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import dj_database_url
6 |
7 | import private_base as private
8 |
9 | from solitude.settings import base
10 | from django_sha2 import get_password_hashers
11 |
12 |
13 | ADMINS = ()
14 | ALLOWED_HOSTS = [
15 | 'payments-s3dev.allizom.org'
16 | ]
17 |
18 | DATABASES = {}
19 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL)
20 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
21 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'}
22 | DATABASES['default']['ATOMIC_REQUESTS'] = True
23 |
24 | DEBUG = False
25 | DEBUG_PROPAGATE_EXCEPTIONS = False
26 |
27 | HMAC_KEYS = private.HMAC_KEYS
28 |
29 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
30 |
31 | LOG_LEVEL = logging.DEBUG
32 |
33 | SECRET_KEY = private.SECRET_KEY
34 |
35 | SENTRY_DSN = private.SENTRY_DSN
36 |
37 | STATSD_HOST = private.STATSD_HOST
38 | STATSD_PORT = private.STATSD_PORT
39 | STATSD_PREFIX = private.STATSD_PREFIX
40 |
41 | SYSLOG_TAG = 'http_app_payments_s3dev'
42 | TEMPLATE_DEBUG = DEBUG
43 |
44 | # Solitude specific settings.
45 | AES_KEYS = private.AES_KEYS
46 |
47 | CLEANSED_SETTINGS_ACCESS = True
48 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS
49 |
50 | SITE_URL = 'https://payments-s3dev.allizom.org'
51 |
52 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET}
53 | S3_BUCKET = private.S3_BUCKET
54 |
55 | NEWRELIC_INI = '/etc/newrelic.d/payments-s3dev.allizom.org.ini'
56 |
57 | # Below is configuration of payment providers.
58 |
59 | ZIPPY_PROXY = 'https://payments-proxy-s3dev.allizom.org/proxy/provider'
60 | ZIPPY_CONFIGURATION = {
61 | 'reference': {
62 | 'url': 'https://zippy-dev.allizom.org'
63 | },
64 | }
65 |
66 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH
67 | BANGO_FAKE_REFUNDS = True
68 | BANGO_PROXY = private.BANGO_PROXY
69 | BANGO_NOTIFICATION_URL = (
70 | 'https://marketplace-s3dev.allizom.org/mozpay/bango/notification')
71 |
72 | NOSE_PLUGINS = []
73 |
--------------------------------------------------------------------------------
/solitude/settings/sites/s3dev/proxy.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import private_base as private
6 |
7 | from solitude.settings import base
8 | from django_sha2 import get_password_hashers
9 |
10 |
11 | ADMINS = ()
12 | ALLOWED_HOSTS = [
13 | 'payments-proxy-s3dev.allizom.org'
14 | ]
15 |
16 | DATABASES = {'default': {}}
17 |
18 | DEBUG = False
19 | DEBUG_PROPAGATE_EXCEPTIONS = False
20 |
21 | HMAC_KEYS = private.HMAC_KEYS
22 |
23 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
24 |
25 | LOG_LEVEL = logging.DEBUG
26 |
27 | SECRET_KEY = private.SECRET_KEY
28 |
29 | SENTRY_DSN = private.SENTRY_DSN
30 |
31 | STATSD_HOST = private.STATSD_HOST
32 | STATSD_PORT = private.STATSD_PORT
33 | STATSD_PREFIX = private.STATSD_PREFIX
34 |
35 | SYSLOG_TAG = 'http_app_payments_s3dev'
36 | TEMPLATE_DEBUG = DEBUG
37 |
38 | # Solitude specific settings.
39 | AES_KEYS = {}
40 |
41 | CLEANSED_SETTINGS_ACCESS = True
42 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS
43 |
44 | NEWRELIC_INI = '/etc/newrelic.d/payments-proxy-s3dev.allizom.org.ini'
45 | NOSE_PLUGINS = []
46 |
47 | REQUIRE_OAUTH = False
48 |
49 | # Below is configuration of payment providers.
50 | BANGO_ENV = 'test'
51 | BANGO_AUTH = private.BANGO_AUTH
52 |
53 | ZIPPY_CONFIGURATION = {
54 | 'reference': {
55 | 'url': 'https://zippy-dev.allizom.org',
56 | 'auth': {'key': private.ZIPPY_PAAS_KEY,
57 | 'secret': private.ZIPPY_PAAS_SECRET,
58 | 'realm': 'zippy-dev.allizom.org'}
59 | },
60 | }
61 |
--------------------------------------------------------------------------------
/solitude/settings/sites/stage/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/stage/__init__.py
--------------------------------------------------------------------------------
/solitude/settings/sites/stage/db.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import dj_database_url
6 |
7 | import private_base as private
8 |
9 | from solitude.settings import base
10 | from django_sha2 import get_password_hashers
11 |
12 |
13 | ADMINS = ()
14 | ALLOWED_HOSTS = ['*']
15 |
16 | DATABASES = {}
17 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL)
18 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
19 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'}
20 | DATABASES['default']['ATOMIC_REQUESTS'] = True
21 |
22 | DEBUG = False
23 | DEBUG_PROPAGATE_EXCEPTIONS = False
24 |
25 | HMAC_KEYS = private.HMAC_KEYS
26 |
27 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
28 |
29 | LOG_LEVEL = logging.DEBUG
30 |
31 | SECRET_KEY = private.SECRET_KEY
32 |
33 | SENTRY_DSN = private.SENTRY_DSN
34 |
35 | STATSD_HOST = private.STATSD_HOST
36 | STATSD_PORT = private.STATSD_PORT
37 | STATSD_PREFIX = private.STATSD_PREFIX
38 |
39 | SYSLOG_TAG = 'http_app_payments_stage'
40 | TEMPLATE_DEBUG = DEBUG
41 |
42 | # Solitude specific settings.
43 | AES_KEYS = private.AES_KEYS
44 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS
45 |
46 | SITE_URL = 'https://payments.allizom.org'
47 |
48 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET}
49 | S3_BUCKET = private.S3_BUCKET
50 |
51 | REQUIRE_OAUTH = True
52 |
53 | NEWRELIC_INI = '/etc/newrelic.d/payments.allizom.org.ini'
54 |
55 | ZIPPY_CONFIGURATION = {}
56 |
57 | # Below is configuration of payment providers.
58 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH
59 | BANGO_ENV = 'prod'
60 | BANGO_FAKE_REFUNDS = False
61 | BANGO_INSERT_STAGE = 'FROM STAGE '
62 | BANGO_PROXY = private.BANGO_PROXY
63 | BANGO_NOTIFICATION_URL = (
64 | 'https://marketplace.allizom.org/mozpay/bango/notification')
65 |
66 | NOSE_PLUGINS = []
67 |
--------------------------------------------------------------------------------
/solitude/settings/sites/stage/proxy.py:
--------------------------------------------------------------------------------
1 | """private_base will be populated from puppet and placed in this directory"""
2 |
3 | import logging
4 |
5 | import private_base as private
6 |
7 | from solitude.settings import base
8 | from django_sha2 import get_password_hashers
9 |
10 |
11 | ADMINS = ()
12 | ALLOWED_HOSTS = ['*']
13 |
14 | DATABASES = {'default': {}}
15 |
16 | DEBUG = False
17 | DEBUG_PROPAGATE_EXCEPTIONS = False
18 |
19 | HMAC_KEYS = private.HMAC_KEYS
20 |
21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS)
22 |
23 | LOG_LEVEL = logging.DEBUG
24 |
25 | SECRET_KEY = private.SECRET_KEY
26 |
27 | SENTRY_DSN = private.SENTRY_DSN
28 |
29 | STATSD_HOST = private.STATSD_HOST
30 | STATSD_PORT = private.STATSD_PORT
31 | STATSD_PREFIX = private.STATSD_PREFIX
32 |
33 | SYSLOG_TAG = 'http_app_payments_stage'
34 | TEMPLATE_DEBUG = DEBUG
35 |
36 | # Solitude specific settings.
37 | AES_KEYS = {}
38 |
39 | CLEANSED_SETTINGS_ACCESS = True
40 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS
41 |
42 | NEWRELIC_INI = '/etc/newrelic.d/payments-proxy.allizom.org.ini'
43 | NOSE_PLUGINS = []
44 |
45 | REQUIRE_OAUTH = False
46 |
47 | # Below is configuration of payment providers.
48 | BANGO_ENV = 'prod'
49 | BANGO_AUTH = private.BANGO_AUTH
50 |
51 | ZIPPY_CONFIGURATION = {}
52 |
--------------------------------------------------------------------------------
/solitude/settings/test.py:
--------------------------------------------------------------------------------
1 | # test_utils picks this file up for testing.
2 | import atexit
3 | import os
4 | import shutil
5 | from tempfile import gettempdir
6 |
7 | from solitude.settings.base import * # noqa
8 |
9 | filename = os.path.join(os.path.dirname(__file__), 'sample.key')
10 |
11 | AES_KEYS = {
12 | 'buyeremail:key': filename,
13 | 'sellerproduct:secret': filename,
14 | 'bango:signature': filename,
15 | }
16 |
17 | SOLITUDE_PROXY = False
18 |
19 | if os.environ.get('SOLITUDE_PROXY', 'disabled') == 'enabled':
20 | raise ValueError('You have the environment variable SOLITUDE_PROXY set to '
21 | '"enabled", this breaks the tests, aborting.')
22 |
23 | PAYPAL_PROXY = False
24 | PAYPAL_URLS_ALLOWED = ('https://marketplace-dev.allizom.org',)
25 |
26 | DEBUG = True
27 | DEBUG_PROPAGATE_EXCEPTIONS = False
28 |
29 | DUMP_REQUESTS = False
30 |
31 | HMAC_KEYS = {'2011-01-01': 'cheesecake'}
32 | from django_sha2 import get_password_hashers
33 | PASSWORD_HASHERS = get_password_hashers(BASE_PASSWORD_HASHERS, HMAC_KEYS)
34 |
35 | # Celery.
36 | CELERY_ALWAYS_EAGER = True
37 |
38 | # Send all statsd to nose.
39 | STATSD_CLIENT = 'django_statsd.clients.nose'
40 |
41 | # No need for paranoia in tests.
42 | from django_paranoia.signals import process
43 | process.disconnect(dispatch_uid='paranoia.reporter.django_paranoia'
44 | '.reporters.cef_')
45 | process.disconnect(dispatch_uid='paranoia.reporter.django_paranoia'
46 | '.reporters.log')
47 | DJANGO_PARANOIA_REPORTERS = []
48 |
49 | # We don't want to hit the live servers in tests.
50 | BANGO_MOCK = True
51 | ZIPPY_MOCK = True
52 |
53 | BRAINTREE_MERCHANT_ID = 'test'
54 | BRAINTREE_PROXY = 'http://m.o'
55 |
56 | SITE_URL = 'http://localhost/'
57 |
58 | SEND_USER_ID_TO_BANGO = True
59 | CHECK_BANGO_TOKEN = True
60 |
61 | REQUIRE_OAUTH = False
62 |
63 | # Live server tests require this, otherwise they will fail. There is no static
64 | # content on our site, so meh.
65 | STATIC_URL = '/'
66 | STATIC_ROOT = '/'
67 |
68 | # Not using bcrypt for hashing is a major test speed up.
69 | PASSWORD_HASHERS = (
70 | 'django.contrib.auth.hashers.MD5PasswordHasher',
71 | )
72 |
73 |
74 | # Suds keeps a cache of the WSDL around, so after completing the test run,
75 | # lets remove that so it doesn't affect the next test run.
76 | def _cleanup():
77 | target = os.path.join(gettempdir(), 'suds')
78 | if os.path.exists(target):
79 | shutil.rmtree(target)
80 |
81 | atexit.register(_cleanup)
82 |
--------------------------------------------------------------------------------
/solitude/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/tests/__init__.py
--------------------------------------------------------------------------------
/solitude/tests/live.py:
--------------------------------------------------------------------------------
1 |
2 | from django.test import LiveServerTestCase
3 | from django.test.utils import override_settings
4 |
5 | from nose.plugins.attrib import attr
6 |
7 | from curling import lib
8 | from solitude.base import getLogger
9 |
10 | log = getLogger('s.tests')
11 |
12 | configs = {
13 | 'REQUIRE_OAUTH': True,
14 | 'SITE_URL': 'http://localhost:8081',
15 | 'CLIENT_OAUTH_KEYS': {'foo': 'bar'},
16 | 'DEBUG': False,
17 | 'DEBUG_PROPAGATE_EXCEPTIONS': False,
18 | }
19 |
20 |
21 | @attr('live')
22 | @override_settings(**configs)
23 | class LiveTestCase(LiveServerTestCase):
24 |
25 | @property
26 | def request(self):
27 | api = lib.API(self.live_server_url)
28 | api.activate_oauth(*configs['CLIENT_OAUTH_KEYS'].items()[0])
29 | return api
30 |
--------------------------------------------------------------------------------
/solitude/tests/resources.py:
--------------------------------------------------------------------------------
1 | from tastypie import fields
2 |
3 | from solitude.base import Resource
4 |
5 |
6 | class Fake(object):
7 | name = ''
8 | pk = 0
9 |
10 |
11 | class FakeResource(Resource):
12 | name = fields.CharField(attribute='name')
13 |
14 | class Meta(Resource.Meta):
15 | resource_name = 'fake'
16 | list_allowed_methods = ['post']
17 | object_class = Fake
18 |
19 | def obj_create(self, bundle, request, **kwargs):
20 | bundle.obj = Fake()
21 | return bundle
22 |
23 | def get_resource_uri(self, bundle):
24 | return '/'
25 |
--------------------------------------------------------------------------------
/solitude/tests/test_authentication.py:
--------------------------------------------------------------------------------
1 | from django import test
2 | from django.conf import settings
3 | from django.test import RequestFactory
4 |
5 | from mock import patch
6 | from nose.tools import eq_, ok_, raises
7 | from rest_framework.exceptions import AuthenticationFailed
8 | from slumber.exceptions import HttpClientError
9 |
10 | from solitude.authentication import Consumer, RestOAuthAuthentication
11 | from solitude.tests.live import LiveTestCase
12 |
13 | keys = {'foo': 'bar'}
14 | keys_dict = {'key': 'foo', 'secret': 'bar'}
15 |
16 |
17 | @patch.object(settings, 'CLIENT_OAUTH_KEYS', keys)
18 | class TestDRFAuthentication(test.TestCase):
19 |
20 | def setUp(self):
21 | self.authentication = RestOAuthAuthentication()
22 | self.factory = RequestFactory()
23 | self.consumer = Consumer(*keys.items()[0])
24 |
25 | def test_skip(self):
26 | req = self.factory.get('/skip-oauth/')
27 | with self.settings(REQUIRE_OAUTH=True, SKIP_OAUTH=['/skip-oauth/']):
28 | ok_(self.authentication.authenticate(req))
29 |
30 | @raises(AuthenticationFailed)
31 | def test_not_skip(self):
32 | req = self.factory.get('/require-oauth/')
33 | with self.settings(REQUIRE_OAUTH=True, SKIP_OAUTH=['/skip-oauth/']):
34 | eq_(self.authentication.authenticate(req))
35 |
36 |
37 | class TestAuthentication(LiveTestCase):
38 |
39 | def test_valid_auth(self):
40 | # If there was an error with auth, an assertion would be raised.
41 | self.request.by_url('/generic/transaction/').get()
42 |
43 | def test_invalid_auth(self):
44 | with self.settings(CLIENT_OAUTH_KEYS={'f': 'b'}):
45 | with self.assertRaises(HttpClientError) as err:
46 | self.request.by_url('/generic/transaction/').get()
47 | eq_(err.exception.response.status_code, 403)
48 |
--------------------------------------------------------------------------------
/solitude/tests/test_error.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django import test
3 | from django.db import transaction
4 |
5 | from nose.tools import eq_, ok_
6 | from slumber.exceptions import HttpClientError
7 |
8 | from curling import lib
9 | from solitude.errors import FormError, MozillaFormatter
10 | from solitude.exceptions import custom_exception_handler
11 | from solitude.tests.live import LiveTestCase
12 |
13 |
14 | class TestErrors(LiveTestCase):
15 |
16 | def test_not_found(self):
17 | with self.assertRaises(HttpClientError) as error:
18 | self.request.by_url('/this/does-not-exist/').get()
19 | eq_(error.exception.response.json, {})
20 |
21 | def test_403_html(self):
22 | with self.assertRaises(HttpClientError) as error:
23 | lib.API(self.live_server_url).by_url('/this/does-not-exist/').get()
24 | eq_(error.exception.response.json, {})
25 |
26 | def test_drf_403_html(self):
27 | with self.assertRaises(HttpClientError) as error:
28 | lib.API(self.live_server_url).by_url('/generic/transaction/').get()
29 | eq_(error.exception.response.json,
30 | {'detail': 'Incorrect authentication credentials.'})
31 |
32 |
33 | class TestRollback(test.TestCase):
34 |
35 | def test_did_rollback(self):
36 | custom_exception_handler(Exception())
37 | ok_(transaction.get_connection().get_rollback())
38 |
39 |
40 | class DummyForm(forms.Form):
41 | name = forms.CharField()
42 |
43 | def clean_name(self):
44 | raise forms.ValidationError([
45 | forms.ValidationError('First error', 'first'),
46 | forms.ValidationError('Second error', 'second')
47 | ])
48 |
49 | def clean(self):
50 | raise forms.ValidationError('Non field error', code='non-field')
51 |
52 |
53 | class TestFormatting(test.TestCase):
54 |
55 | def setUp(self):
56 | self.moz = MozillaFormatter
57 | self.form = DummyForm({'name': 'bob'})
58 | ok_(not self.form.is_valid())
59 |
60 | def test_form_error(self):
61 | eq_(self.moz(FormError(self.form.errors)).format(),
62 | {'mozilla':
63 | {'name': [
64 | {'message': u'First error', 'code': 'first'},
65 | {'message': u'Second error', 'code': 'second'}
66 | ], '__all__': [
67 | {'message': u'Non field error', 'code': 'non-field'}
68 | ]}
69 | })
70 |
--------------------------------------------------------------------------------
/solitude/tests/test_middleware.py:
--------------------------------------------------------------------------------
1 | from django.test import RequestFactory, TestCase
2 |
3 | from nose.tools import eq_
4 |
5 | from solitude.logger import get_oauth_key, get_transaction_id
6 | from solitude.middleware import LoggerMiddleware
7 |
8 |
9 | class TestMiddleware(TestCase):
10 |
11 | def test_nothing(self):
12 | req = RequestFactory().get('/')
13 | LoggerMiddleware().process_request(req)
14 | eq_(get_oauth_key(), '')
15 | eq_(get_transaction_id(), '-')
16 |
17 | def test_something(self):
18 | req = RequestFactory().get('/', HTTP_TRANSACTION_ID='foo')
19 | req.OAUTH_KEY = 'bar'
20 | LoggerMiddleware().process_request(req)
21 | eq_(get_oauth_key(), 'bar')
22 | eq_(get_transaction_id(), 'foo')
23 |
--------------------------------------------------------------------------------
/solitude/tests/test_processor.py:
--------------------------------------------------------------------------------
1 | from nose.tools import eq_
2 |
3 | from solitude.processor import JSONProcessor, sanitise
4 |
5 |
6 | sanitise_dicts = [
7 | [None, None],
8 | [{'foo': 'bar'}, {'foo': 'bar'}],
9 | [{'foo': {'pin': 'bar'}}, {'foo': {'pin': '*' * 8}}]
10 | ]
11 |
12 | process_data = [
13 | [{'sentry.interfaces.Http': {'data': '{"pin": "1234"}'}},
14 | {'sentry.interfaces.Http': {'data': '{"pin": "********"}'}}],
15 | # All the following remain unchanged.
16 | [{'sentry.interfaces.Http': {}},
17 | {'sentry.interfaces.Http': {}}],
18 | [{'sentry.interfaces.Http': 'None'},
19 | {'sentry.interfaces.Http': 'None'}],
20 | [{'sentry.interfaces.Http': {'data': 'blargh!'}},
21 | {'sentry.interfaces.Http': {'data': 'blargh!'}}],
22 | [{}, {}],
23 | ]
24 |
25 |
26 | def test_processor():
27 | for value, expected in process_data:
28 | eq_(JSONProcessor('').process(value), expected)
29 |
30 |
31 | def test_sanitise():
32 | for value, expected in sanitise_dicts:
33 | eq_(sanitise(value, ['pin']), expected)
34 |
--------------------------------------------------------------------------------
/solitude/urls.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.conf.urls import include, patterns, url
4 |
5 |
6 | services_patterns = patterns(
7 | 'lib.services.resources',
8 | url(r'^settings/$', 'settings_list', name='services.settings'),
9 | url(r'^settings/(?P[^/<>]+)/$', 'settings_view',
10 | name='services.setting'),
11 | url(r'^error/', 'error', name='services.error'),
12 | url(r'^logs/', 'logs', name='services.log'),
13 | url(r'^status/', 'status', name='services.status'),
14 | url(r'^request/', 'request_resource', name='services.request'),
15 | url(r'^failures/transactions/', 'transactions_failures',
16 | name='services.failures.transactions'),
17 | url(r'^failures/statuses/', 'statuses_failures',
18 | name='services.failures.statuses'),
19 | )
20 |
21 | generic_urls = patterns(
22 | '',
23 | url('', include('lib.buyers.urls')),
24 | url('', include('lib.sellers.urls')),
25 | url('', include('lib.transactions.urls')),
26 | )
27 |
28 | urls = [
29 | url(r'^$', 'solitude.views.home', name='home'),
30 | url(r'^generic/', include(generic_urls, namespace='generic')),
31 | url(r'^proxy/', include('lib.proxy.urls')),
32 | url(r'^bango/', include('lib.bango.urls', namespace='bango')),
33 | url(r'^braintree/', include('lib.brains.urls', namespace='braintree')),
34 | url(r'^provider/', include('lib.provider.urls')),
35 | url(r'^services/', include(services_patterns))
36 | ]
37 |
38 | if os.getenv('IS_DOCKER'):
39 | urls.append(url(r'^solitude/services/', include(services_patterns)))
40 |
41 | urlpatterns = patterns('', *urls)
42 |
43 | handler500 = 'solitude.views.error_500'
44 | handler404 = 'solitude.views.error_404'
45 | handler403 = 'solitude.views.error_403'
46 |
--------------------------------------------------------------------------------
/solitude/utils.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core.exceptions import ImproperlyConfigured
3 |
4 |
5 | # Note deliberately ignoring o, O, 0, i, l and 1 as too similar.
6 | choices = 'abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
7 |
8 |
9 | def shorter(integer):
10 | """
11 | Convert an integer into something shorter.
12 | """
13 | output = []
14 | while integer > 0:
15 | remainder = integer % len(choices)
16 | output.append(choices[remainder])
17 | integer /= len(choices)
18 | return ''.join(reversed(output))
19 |
20 |
21 | def validate_settings():
22 | """
23 | Validate that if not in DEBUG mode, key settings have been changed.
24 | """
25 | if settings.DEBUG:
26 | return
27 |
28 | # Things that values must not be.
29 | for key, value in [
30 | ('SECRET_KEY', 'please change this'),
31 | ('HMAC_KEYS', {'2011-01-01': 'please change me'})]:
32 | if getattr(settings, key) == value:
33 | raise ImproperlyConfigured('{0} must be changed from default'
34 | .format(key))
35 |
36 | for key, value in settings.AES_KEYS.items():
37 | if value == 'solitude/settings/sample.key':
38 | raise ImproperlyConfigured('AES_KEY {0} must be changed from '
39 | 'default'.format(key))
40 |
41 | if settings.REQUIRE_OAUTH:
42 | for key, value in settings.CLIENT_OAUTH_KEYS.items():
43 | if value == 'please change this':
44 | raise ImproperlyConfigured('CLIENT_OAUTH_KEYS {0} must be '
45 | 'changed from'.format(key))
46 |
--------------------------------------------------------------------------------
/solitude/views.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from django.http import JsonResponse
4 |
5 | from solitude.base import log_cef
6 |
7 |
8 | def home(request):
9 | return response(request,
10 | data={'message': 'A home page in solitude.'},
11 | status=200)
12 |
13 |
14 | def response(request, status=500, data=None):
15 | # If returning JSON, then we can't send back an empty body, unless we
16 | # return a 204 - the client should be able to parse it.
17 | #
18 | # This assumes the client can receive JSON. We could send a HTTP 406
19 | # to anyone not accepting JSON, but that seems unusually cruel punishment
20 | # and will mask the real error.
21 | message = data if data else {}
22 | return JsonResponse(message, status=status)
23 |
24 |
25 | def error_500(request):
26 | exception = sys.exc_info()[1]
27 | log_cef(str(exception), request, severity=3)
28 | return response(request, status=500,
29 | data={'error': exception.__class__.__name__})
30 |
31 |
32 | def error_403(request):
33 | return response(request, status=403)
34 |
35 |
36 | def error_404(request):
37 | return response(request, status=404)
38 |
--------------------------------------------------------------------------------
/wsgi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/wsgi/__init__.py
--------------------------------------------------------------------------------
/wsgi/playdoh.py:
--------------------------------------------------------------------------------
1 | import os
2 | import site
3 |
4 | os.environ.setdefault('CELERY_LOADER', 'django')
5 | # NOTE: you can also set DJANGO_SETTINGS_MODULE in your environment to override
6 | # the default value in manage.py
7 |
8 | # Add the app dir to the python path so we can import manage.
9 | wsgidir = os.path.dirname(__file__)
10 | site.addsitedir(os.path.abspath(os.path.join(wsgidir, '../')))
11 |
12 | # manage adds /apps, /lib, and /vendor to the Python path.
13 | import manage # noqa
14 |
15 | from django.core.wsgi import get_wsgi_application
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/wsgi/proxy.py:
--------------------------------------------------------------------------------
1 | import os
2 | import site
3 |
4 | os.environ.setdefault('SOLITUDE_PROXY', 'enabled')
5 | os.environ.setdefault('CELERY_LOADER', 'django')
6 | # NOTE: you can also set DJANGO_SETTINGS_MODULE in your environment to override
7 | # the default value in manage.py
8 |
9 | # Add the app dir to the python path so we can import manage.
10 | wsgidir = os.path.dirname(__file__)
11 | site.addsitedir(os.path.abspath(os.path.join(wsgidir, '../')))
12 |
13 | # manage adds /apps, /lib, and /vendor to the Python path.
14 | import manage # noqa
15 |
16 | from django.core.wsgi import get_wsgi_application
17 | application = get_wsgi_application()
18 |
--------------------------------------------------------------------------------