Merge pull request #43 from MycroftAI/account-api

added account profile endpoint to account API
pull/46/head
Chris Veilleux 2019-02-08 16:28:53 -06:00 committed by GitHub
commit 0d54672b10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 735 additions and 50 deletions

17
api/account/Pipfile Normal file
View File

@ -0,0 +1,17 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
flask = "*"
flask-restful = "*"
uwsgi = "*"
[dev-packages]
selene = {editable = true,path = "./../../shared"}
behave = "*"
pyhamcrest = "*"
[requires]
python_version = "3.7"

361
api/account/Pipfile.lock generated Normal file
View File

@ -0,0 +1,361 @@
{
"_meta": {
"hash": {
"sha256": "7d934739849ec9eb381b562adad7e4842dba13f11cad2ded35e154163bb4876b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aniso8601": {
"hashes": [
"sha256:03c0ffeeb04edeca1ed59684cc6836dc377f58e52e315dc7be3af879909889f4",
"sha256:ac30cceff24aec920c37b8d74d7d8a5dd37b1f62a90b4f268a6234cabe147080"
],
"version": "==4.1.0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"flask-restful": {
"hashes": [
"sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8",
"sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9"
],
"index": "pypi",
"version": "==0.3.7"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
],
"version": "==1.1.0"
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.9"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"uwsgi": {
"hashes": [
"sha256:d2318235c74665a60021a4fc7770e9c2756f9fc07de7b8c22805efe85b5ab277"
],
"index": "pypi",
"version": "==2.0.17.1"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
}
},
"develop": {
"aniso8601": {
"hashes": [
"sha256:03c0ffeeb04edeca1ed59684cc6836dc377f58e52e315dc7be3af879909889f4",
"sha256:ac30cceff24aec920c37b8d74d7d8a5dd37b1f62a90b4f268a6234cabe147080"
],
"version": "==4.1.0"
},
"behave": {
"hashes": [
"sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86",
"sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"
],
"index": "pypi",
"version": "==1.2.6"
},
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"deprecated": {
"hashes": [
"sha256:8bfeba6e630abf42b5d111b68a05f7fe3d6de7004391b3cd614947594f87a4ff",
"sha256:b784e0ca85a8c1e694d77e545c10827bd99772392e79d5f5442e761515a1246e"
],
"version": "==1.2.4"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"flask-restful": {
"hashes": [
"sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8",
"sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9"
],
"index": "pypi",
"version": "==0.3.7"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"markupsafe": {
"hashes": [
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
],
"version": "==1.1.0"
},
"parse": {
"hashes": [
"sha256:870dd675c1ee8951db3e29b81ebe44fd131e3eb8c03a79483a58ea574f3145c2"
],
"version": "==1.11.1"
},
"parse-type": {
"hashes": [
"sha256:6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9",
"sha256:f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c"
],
"version": "==0.4.2"
},
"passlib": {
"hashes": [
"sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0",
"sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"
],
"version": "==1.7.1"
},
"psycopg2-binary": {
"hashes": [
"sha256:19a2d1f3567b30f6c2bb3baea23f74f69d51f0c06c2e2082d0d9c28b0733a4c2",
"sha256:2b69cf4b0fa2716fd977aa4e1fd39af6110eb47b2bb30b4e5a469d8fbecfc102",
"sha256:2e952fa17ba48cbc2dc063ddeec37d7dc4ea0ef7db0ac1eda8906365a8543f31",
"sha256:348b49dd737ff74cfb5e663e18cb069b44c64f77ec0523b5794efafbfa7df0b8",
"sha256:3d72a5fdc5f00ca85160915eb9a973cf9a0ab8148f6eda40708bf672c55ac1d1",
"sha256:4957452f7868f43f32c090dadb4188e9c74a4687323c87a882e943c2bd4780c3",
"sha256:5138cec2ee1e53a671e11cc519505eb08aaaaf390c508f25b09605763d48de4b",
"sha256:587098ca4fc46c95736459d171102336af12f0d415b3b865972a79c03f06259f",
"sha256:5b79368bcdb1da4a05f931b62760bea0955ee2c81531d8e84625df2defd3f709",
"sha256:5cf43807392247d9bc99737160da32d3fa619e0bfd85ba24d1c78db205f472a4",
"sha256:676d1a80b1eebc0cacae8dd09b2fde24213173bf65650d22b038c5ed4039f392",
"sha256:6b0211ecda389101a7d1d3df2eba0cf7ffbdd2480ca6f1d2257c7bd739e84110",
"sha256:79cde4660de6f0bb523c229763bd8ad9a93ac6760b72c369cf1213955c430934",
"sha256:7aba9786ac32c2a6d5fb446002ed936b47d5e1f10c466ef7e48f66eb9f9ebe3b",
"sha256:7c8159352244e11bdd422226aa17651110b600d175220c451a9acf795e7414e0",
"sha256:945f2eedf4fc6b2432697eb90bb98cc467de5147869e57405bfc31fa0b824741",
"sha256:96b4e902cde37a7fc6ab306b3ac089a3949e6ce3d824eeca5b19dc0bedb9f6e2",
"sha256:9a7bccb1212e63f309eb9fab47b6eaef796f59850f169a25695b248ca1bf681b",
"sha256:a3bfcac727538ec11af304b5eccadbac952d4cca1a551a29b8fe554e3ad535dc",
"sha256:b19e9f1b85c5d6136f5a0549abdc55dcbd63aba18b4f10d0d063eb65ef2c68b4",
"sha256:b664011bb14ca1f2287c17185e222f2098f7b4c857961dbcf9badb28786dbbf4",
"sha256:bde7959ef012b628868d69c474ec4920252656d0800835ed999ba5e4f57e3e2e",
"sha256:cb095a0657d792c8de9f7c9a0452385a309dfb1bbbb3357d6b1e216353ade6ca",
"sha256:d16d42a1b9772152c1fe606f679b2316551f7e1a1ce273e7f808e82a136cdb3d",
"sha256:d444b1545430ffc1e7a24ce5a9be122ccd3b135a7b7e695c5862c5aff0b11159",
"sha256:d93ccc7bf409ec0a23f2ac70977507e0b8a8d8c54e5ee46109af2f0ec9e411f3",
"sha256:df6444f952ca849016902662e1a47abf4fa0678d75f92fd9dd27f20525f809cd",
"sha256:e63850d8c52ba2b502662bf3c02603175c2397a9acc756090e444ce49508d41e",
"sha256:ec43358c105794bc2b6fd34c68d27f92bea7102393c01889e93f4b6a70975728",
"sha256:f4c6926d9c03dadce7a3b378b40d2fea912c1344ef9b29869f984fb3d2a2420b"
],
"version": "==2.7.7"
},
"pygithub": {
"hashes": [
"sha256:263102b43a83e2943900c1313109db7a00b3b78aeeae2c9137ba694982864872"
],
"version": "==1.43.5"
},
"pyhamcrest": {
"hashes": [
"sha256:6b672c02fdf7470df9674ab82263841ce8333fb143f32f021f6cb26f0e512420",
"sha256:8ffaa0a53da57e89de14ced7185ac746227a8894dbd5a3c718bf05ddbd1d56cd"
],
"index": "pypi",
"version": "==1.9.0"
},
"pyjwt": {
"hashes": [
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
],
"version": "==1.7.1"
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.9"
},
"requests": {
"hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"version": "==2.21.0"
},
"selene": {
"editable": true,
"path": "./../../shared"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24.1"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
},
"wrapt": {
"hashes": [
"sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
],
"version": "==1.11.1"
}
}
}

View File

View File

@ -0,0 +1,15 @@
"""Entry point for the API that supports the Mycroft Marketplace."""
from flask import Flask
from flask_restful import Api
from selene.api import AccountEndpoint, get_base_config
from selene.api import JSON_MIMETYPE, output_json
# Define the Flask application
acct = Flask(__name__)
acct.config.from_object(get_base_config())
# Define the API and its endpoints.
acct_api = Api(acct)
acct_api.representations[JSON_MIMETYPE] = output_json
acct_api.add_resource(AccountEndpoint, '/api/account')

View File

@ -0,0 +1,55 @@
from datetime import date
import os
from behave import fixture, use_fixture
from account_api.api import acct
from selene.data.account import (
Account,
AccountAgreement,
AccountRepository,
AccountSubscription
)
from selene.util.db import get_db_connection
@fixture
def acct_api_client(context):
acct.testing = True
context.client_config = acct.config
context.client = acct.test_client()
yield context.client
def before_feature(context, _):
use_fixture(acct_api_client, context)
os.environ['SALT'] = 'testsalt'
def before_scenario(context, _):
test_account = Account(
id=None,
email_address='foo@mycroft.ai',
refresh_tokens=None,
subscription=AccountSubscription(
type='monthly supporter',
start_date=None,
stripe_customer_id='foo'
),
agreements=[
AccountAgreement(name='terms', signature_date=None)
]
)
with get_db_connection(context.client_config['DB_CONNECTION_POOL']) as db:
acct_repository = AccountRepository(db)
acct_repository.add(test_account, 'foo')
context.account = acct_repository.get_account_by_email(
test_account.email_address
)
def after_scenario(context, _):
with get_db_connection(context.client_config['DB_CONNECTION_POOL']) as db:
acct_repository = AccountRepository(db)
acct_repository.remove(context.account)

View File

@ -0,0 +1,8 @@
Feature: Manage account profiles
Test the ability of the account API to retrieve and manage a user's profile
settings.
Scenario: Retrieve authenticated user's account
Given an authenticated user
When account endpoint is called to get user profile
Then user profile is returned

View File

@ -0,0 +1,35 @@
from datetime import date
from http import HTTPStatus
import json
from behave import given, then, when
from hamcrest import assert_that, equal_to
from selene.api.testing import generate_auth_tokens
@given('an authenticated user')
def setup_authenticated_user(context):
generate_auth_tokens(context)
@when('account endpoint is called to get user profile')
def call_account_endpoint(context):
context.response = context.client.get('/api/account')
@then('user profile is returned')
def validate_response(context):
assert_that(context.response.status_code, equal_to(HTTPStatus.OK))
response_data = json.loads(context.response.data)
assert_that(
response_data['emailAddress'],
equal_to(context.account.email_address)
)
assert_that(response_data['id'], equal_to(context.account.id))
assert_that(response_data['subscription'], equal_to(
dict(type='monthly supporter', startDate=str(date.today()))
))
assert_that(response_data['agreements'], equal_to(
[dict(name='terms', signatureDate=str(date.today()))]
))

View File

@ -3,7 +3,12 @@ import os
from behave import fixture, use_fixture
from sso_api.api import sso
from selene.data.account import AccountRepository
from selene.data.account import (
Account,
AccountAgreement,
AccountRepository,
AccountSubscription
)
from selene.util.db import get_db_connection
@ -23,11 +28,26 @@ def before_feature(context, _):
def before_scenario(context, _):
with get_db_connection(context.db_pool) as db:
test_account = Account(
id=None,
email_address='foo@mycroft.ai',
refresh_tokens=None,
subscription=AccountSubscription(
type='monthly supporter',
start_date=None,
stripe_customer_id='foo'
),
agreements=[
AccountAgreement(name='terms', signature_date=None)
]
)
with get_db_connection(
context.client_config['DB_CONNECTION_POOL']) as db:
acct_repository = AccountRepository(db)
account_id = acct_repository.add('foo@mycroft.ai', 'foo')
account = acct_repository.get_account_by_id(account_id)
context.account = account
acct_repository.add(test_account, 'foo')
context.account = acct_repository.get_account_by_email(
test_account.email_address
)
def after_scenario(context, _):

View File

@ -2,13 +2,11 @@ from http import HTTPStatus
from behave import given, then, when
from hamcrest import assert_that, equal_to, has_item, is_not
from selene.data.account import RefreshTokenRepository
from selene.api.testing import get_account, validate_token_cookies
from selene.util.auth import AuthenticationTokenGenerator
from selene.util.db import get_db_connection
ACCESS_TOKEN_COOKIE_KEY = 'seleneAccess'
REFRESH_TOKEN_COOKIE_KEY = 'seleneRefresh'
from selene.api.testing import (
generate_auth_tokens,
get_account,
validate_token_cookies
)
@given('user "{email}" is authenticated')
@ -18,26 +16,7 @@ def save_email(context, email):
@when('user attempts to logout')
def call_logout_endpoint(context):
token_generator = AuthenticationTokenGenerator(
context.account.id,
context.client_config['ACCESS_SECRET'],
context.client_config['REFRESH_SECRET']
)
context.client.set_cookie(
context.client_config['DOMAIN'],
ACCESS_TOKEN_COOKIE_KEY,
token_generator.access_token
)
context.client.set_cookie(
context.client_config['DOMAIN'],
REFRESH_TOKEN_COOKIE_KEY,
token_generator.refresh_token
)
context.request_refresh_token = token_generator.refresh_token
with get_db_connection(context.client_config['DB_CONNECTION_POOL']) as db:
token_repository = RefreshTokenRepository(db, context.account)
token_repository.add_refresh_token(token_generator.refresh_token)
generate_auth_tokens(context)
context.response = context.client.get('/api/logout')

View File

@ -10,6 +10,7 @@ flask-restful = "*"
psycopg2-binary = "*"
passlib = "*"
pyhamcrest = "*"
validator-collection = "*"
[dev-packages]

16
shared/Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "c76a19553967e3b2b049bb52a83cf9b15ea4a298b53a8e9bb7ab9244ce11fc83"
"sha256": "c2bf160284a4d0bf594cf4cda88470b39cf188b80b9340a4fcbd127cdbd1700b"
},
"pipfile-spec": 6,
"requires": {
@ -87,6 +87,13 @@
],
"version": "==2.10"
},
"jsonschema": {
"hashes": [
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
],
"version": "==2.6.0"
},
"markupsafe": {
"hashes": [
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
@ -215,6 +222,13 @@
],
"version": "==1.24.1"
},
"validator-collection": {
"hashes": [
"sha256:1008f31ead2271e5caf1b655e1605ff42fde2b39620be7953558b58d5d8f1325"
],
"index": "pypi",
"version": "==1.3.1"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",

View File

@ -1,2 +1,4 @@
from .account_endpoint import AccountEndpoint
from .base_endpoint import APIError, SeleneEndpoint
from .base_config import get_base_config
from .response_data_formatter import JSON_MIMETYPE, output_json

View File

@ -0,0 +1,23 @@
"""API endpoint to return the a logged-in user's profile"""
from dataclasses import asdict
from http import HTTPStatus
from .base_endpoint import SeleneEndpoint
class AccountEndpoint(SeleneEndpoint):
"""Retrieve information about the user based on their UUID"""
def get(self):
"""Process HTTP GET request for an account."""
self._authenticate()
if self.authenticated:
self._build_response()
return self.response
def _build_response(self):
"""Build the response to the user info request."""
response_data = asdict(self.account)
del(response_data['refresh_tokens'])
self.response = (response_data, HTTPStatus.OK)

View File

@ -1 +1,7 @@
from .authentication import get_account, validate_token_cookies
from .authentication import (
ACCESS_TOKEN_COOKIE_KEY,
generate_auth_tokens,
get_account,
REFRESH_TOKEN_COOKIE_KEY,
validate_token_cookies
)

View File

@ -1,12 +1,40 @@
from hamcrest import assert_that, equal_to, has_item
from selene.data.account import Account, AccountRepository
from selene.data.account import (
Account,
AccountRepository,
RefreshTokenRepository
)
from selene.util.auth import AuthenticationTokenGenerator
from selene.util.db import get_db_connection
ACCESS_TOKEN_COOKIE_KEY = 'seleneAccess'
REFRESH_TOKEN_COOKIE_KEY = 'seleneRefresh'
def generate_auth_tokens(context):
token_generator = AuthenticationTokenGenerator(
context.account.id,
context.client_config['ACCESS_SECRET'],
context.client_config['REFRESH_SECRET']
)
context.client.set_cookie(
context.client_config['DOMAIN'],
ACCESS_TOKEN_COOKIE_KEY,
token_generator.access_token
)
context.client.set_cookie(
context.client_config['DOMAIN'],
REFRESH_TOKEN_COOKIE_KEY,
token_generator.refresh_token
)
context.request_refresh_token = token_generator.refresh_token
with get_db_connection(context.client_config['DB_CONNECTION_POOL']) as db:
token_repository = RefreshTokenRepository(db, context.account)
token_repository.add_refresh_token(token_generator.refresh_token)
def validate_token_cookies(context, expired=False):
for cookie in context.response.headers.getlist('Set-Cookie'):
ingredients = _parse_cookie(cookie)

View File

@ -1,3 +1,3 @@
from .entity.account import Account
from .entity.account import Account, AccountAgreement, AccountSubscription
from .repository.account import AccountRepository
from .repository.refresh_token import RefreshTokenRepository

View File

@ -2,14 +2,24 @@ from datetime import date
from dataclasses import dataclass
from typing import List
from validator_collection import validators
@dataclass
class AccountAgreement(object):
"""Representation of a 'signed' agreement"""
agreement: str
name: str
signature_date: date
@dataclass
class AccountSubscription(object):
"""Represents the subscription plan chosen by the user"""
type: str
start_date: date
stripe_customer_id: str
@dataclass
class Account(object):
"""Representation of a Mycroft user account."""
@ -17,4 +27,7 @@ class Account(object):
email_address: str
refresh_tokens: List[str]
agreements: List[AccountAgreement]
subscription: str
subscription: AccountSubscription
def __post_init__(self):
self.email_address = validators.email(self.email_address)

View File

@ -1,7 +1,12 @@
from passlib.hash import sha512_crypt
from os import environ, path
from selene.util.db import DatabaseRequest, Cursor, get_sql_from_file
from selene.util.db import (
DatabaseRequest,
Cursor,
get_sql_from_file,
use_transaction
)
from ..entity.account import Account
SQL_DIR = path.join(path.dirname(__file__), 'sql')
@ -18,25 +23,64 @@ def _encrypt_password(password):
class AccountRepository(object):
def __init__(self, db):
self.db = db
self.cursor = Cursor(db)
def add(self, email_address: str, password: str) -> str:
@use_transaction
def add(self, account: Account, password: str):
account.id = self._add_account(account, password)
self._add_agreement(account)
if account.subscription is not None:
self._add_subscription(account)
def _add_account(self, account: Account, password: str):
"""Add a row to the account table."""
encrypted_password = _encrypt_password(password)
request = DatabaseRequest(
sql=get_sql_from_file(path.join(SQL_DIR, 'add_account.sql')),
args=dict(email_address=email_address, password=encrypted_password)
args=dict(
email_address=account.email_address,
password=encrypted_password
)
)
cursor = Cursor(self.db)
result = cursor.insert_returning(request)
result = self.cursor.insert_returning(request)
return result['id']
def _add_agreement(self, account: Account):
"""Accounts cannot be added without agreeing to terms and privacy"""
for agreement in account.agreements:
request = DatabaseRequest(
sql=get_sql_from_file(
path.join(SQL_DIR, 'add_account_agreement.sql')
),
args=dict(
account_id=account.id,
agreement_name=agreement.name
)
)
self.cursor.insert(request)
def _add_subscription(self, account: Account):
"""A subscription is optional, add it if one was selected"""
request = DatabaseRequest(
sql=get_sql_from_file(
path.join(SQL_DIR, 'add_account_subscription.sql')
),
args=dict(
account_id=account.id,
subscription_type=account.subscription.type,
stripe_customer_id=account.subscription.stripe_customer_id
)
)
self.cursor.insert(request)
def remove(self, account: Account):
"""Delete and account and all of its children"""
request = DatabaseRequest(
sql=get_sql_from_file(path.join(SQL_DIR, 'remove_account.sql')),
args=dict(id=account.id)
)
cursor = Cursor(self.db)
cursor.delete(request)
self.cursor.delete(request)
def get_account_by_id(self, account_id: str) -> Account:
"""Use a given uuid to query the database for an account
@ -94,8 +138,7 @@ class AccountRepository(object):
def _get_account(self, db_request):
account = None
cursor = Cursor(self.db)
result = cursor.select_one(db_request)
result = self.cursor.select_one(db_request)
if result is not None:
account = Account(**result['account'])

View File

@ -0,0 +1,15 @@
INSERT INTO
account.account_agreement (account_id, agreement_id, agreement_ts_range)
VALUES
(
%(account_id)s,
(
SELECT
id
FROM
account.agreement
WHERE
agreement = %(agreement_name)s
),
'[now,]'
)

View File

@ -0,0 +1,21 @@
INSERT INTO
account.account_subscription (
account_id,
subscription_id,
subscription_ts_range,
stripe_customer_id
)
VALUES
(
%(account_id)s,
(
SELECT
id
FROM
account.subscription
WHERE
subscription = %(subscription_type)s
),
'[now,]',
%(stripe_customer_id)s
)

View File

@ -11,7 +11,7 @@ WITH
SELECT
array_agg(
json_build_object(
'agreement', ag.agreement,
'name', ag.agreement,
'signature_date', lower(aa.agreement_ts_range)::DATE
)
)
@ -24,7 +24,10 @@ WITH
),
subscription AS (
SELECT
s.subscription
json_build_object(
'type', s.subscription,
'start_date', lower(asub.subscription_ts_range)::DATE
)
FROM
account.account_subscription asub
INNER JOIN account.subscription s ON asub.subscription_id = s.id

View File

@ -1,3 +1,4 @@
from .connection import DatabaseConnectionConfig
from .connection_pool import allocate_db_connection_pool, get_db_connection
from .cursor import DatabaseRequest, Cursor, get_sql_from_file
from .transaction import use_transaction

View File

@ -0,0 +1,24 @@
"""Tools for executing sql within a transaction."""
from functools import wraps
def use_transaction(func):
"""Execute all sql statements within the wrapped function in a transaction
This is a decorator that assumes the function it is wrapping is a method
of a class with a "db" attribute that is a psycopg connection object.
:param func: function being decorated
:return: decorated function
"""
@wraps(func)
def execute_in_transaction(*args, **kwargs):
instance = args[0]
if hasattr(instance, "db"):
prev_autocommit = instance.db.autocommit
instance.db.autocommit = False
with instance.db:
func(*args, **kwargs)
instance.db.autocommit = prev_autocommit
return execute_in_transaction

View File

@ -16,6 +16,7 @@ setup(
'pygithub',
'pyhamcrest',
'pyjwt',
'psycopg2-binary'
'psycopg2-binary',
'validator-collection'
]
)