Merge pull request #1389 from dod-ccpo/state-machine-error-handling

state machine triggers for resuming progress from a failed state
This commit is contained in:
tomdds 2020-02-18 15:35:27 -05:00 committed by GitHub
commit e3397390d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1596 additions and 416 deletions

159
Pipfile.lock generated
View File

@ -26,10 +26,10 @@
}, },
"alembic": { "alembic": {
"hashes": [ "hashes": [
"sha256:d412982920653db6e5a44bfd13b1d0db5685cbaaccaf226195749c706e1e862a" "sha256:2df2519a5b002f881517693b95626905a39c5faf4b5a1f94de4f1441095d1d26"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.3" "version": "==1.4.0"
}, },
"amqp": { "amqp": {
"hashes": [ "hashes": [
@ -47,10 +47,10 @@
}, },
"azure-core": { "azure-core": {
"hashes": [ "hashes": [
"sha256:b8ccbd901d085048e4e3e72627b066923c5bd3780e4c43cf9cf9948aee9bdf9e", "sha256:8bdb12b8e937c5bdf495faadf7741ea1958436e2d24c9c5ca7bd7a5ca7a9e42f",
"sha256:e2cd99f0c0aef12c168d498cb5bc47a3a45c8ab08112183e3ec97e4dcb33ceb9" "sha256:bcfc4502c4cfbdcbe82301119439f52542fa4a42dfc1dadd647bba8b01819823"
], ],
"version": "==1.2.1" "version": "==1.2.2"
}, },
"azure-graphrbac": { "azure-graphrbac": {
"hashes": [ "hashes": [
@ -62,11 +62,11 @@
}, },
"azure-identity": { "azure-identity": {
"hashes": [ "hashes": [
"sha256:4ce65058461c277991763ed3f121efc6b9eb9c2edefb62c414dfa85c814690d3", "sha256:17fa904e0447fd2a2dc19909379edb769b05656dbaf4863b8c4fdfb2bb54350c",
"sha256:b32acd1cdb6202bfe10d9a0858dc463d8960295da70ae18097eb3b85ab12cb91" "sha256:7e9c85e3f82f1e29e5edfc7beb3030b25e8b8fd02b65d5ea1c67f13cde01da0f"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.2.0" "version": "==1.3.0"
}, },
"azure-keyvault": { "azure-keyvault": {
"hashes": [ "hashes": [
@ -78,17 +78,17 @@
}, },
"azure-keyvault-keys": { "azure-keyvault-keys": {
"hashes": [ "hashes": [
"sha256:2983fa42e20a0e6bf6b87976716129c108e613e0292d34c5b0f0c8dc1d488e89", "sha256:1c230c052b9f0b9ecaee97347fe4ebf3fcc798f92edbfd618ea264efc61ad554",
"sha256:38c27322637a2c52620a8b96da1942ad6a8d22d09b5a01f6fa257f7a51e52ed0" "sha256:711af402a0000ac329406253470c1198cc452adc8638608461ae54c8dce92afc"
], ],
"version": "==4.0.0" "version": "==4.0.1"
}, },
"azure-keyvault-secrets": { "azure-keyvault-secrets": {
"hashes": [ "hashes": [
"sha256:2eae9264a8f6f59277e1a9bfdbc8b0a15969ee5a80d8efe403d7744805b4a481", "sha256:0afd85eaa94962fc8ad9e71348ba0873d723d531163894ee3175f138c3180fe4",
"sha256:97a602406a833e8f117c540c66059c818f4321a35168dd17365fab1e4527d718" "sha256:0f92705444f55ca0d5a892172eda898f42678602834f03fa8012e9979e5fe619"
], ],
"version": "==4.0.0" "version": "==4.0.1"
}, },
"azure-mgmt-authorization": { "azure-mgmt-authorization": {
"hashes": [ "hashes": [
@ -116,11 +116,11 @@
}, },
"azure-mgmt-resource": { "azure-mgmt-resource": {
"hashes": [ "hashes": [
"sha256:455a10bbae15673c7879d7515b38e1548cb1a8982dd35029ab3192565262c573", "sha256:a77707bad5551bd558da450045cd2f7097fb8cbaf68610a510a9e413f8a9cf3e",
"sha256:c2ad10cab63999c0a88ee498bc36200ee7f6e6e5d4bf82712bde882eda11146f" "sha256:d90b7d8f237b71b54cfd06480dc1ecd7dac81b22301bf2f4ead98a53cf269b6a"
], ],
"index": "pypi", "index": "pypi",
"version": "==8.0.0" "version": "==8.0.1"
}, },
"azure-mgmt-subscription": { "azure-mgmt-subscription": {
"hashes": [ "hashes": [
@ -186,41 +186,36 @@
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
], ],
"version": "==1.13.2" "version": "==1.14.0"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@ -296,11 +291,11 @@
}, },
"flask-wtf": { "flask-wtf": {
"hashes": [ "hashes": [
"sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2",
"sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.14.2" "version": "==0.14.3"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
@ -333,10 +328,10 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484",
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668"
], ],
"version": "==2.11.1" "version": "==3.0.0a1"
}, },
"kombu": { "kombu": {
"hashes": [ "hashes": [
@ -685,10 +680,10 @@
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [
"sha256:ccc94ed0909b58ffe34430ea5451f07bc0c76467d7081619a454bf5c98b89e28", "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50",
"sha256:feae2f18633c32fc71f2de629bfb3bd3c9325cd4419642b1f1da42ee488d9b98" "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e"
], ],
"version": "==2.1.0" "version": "==2.2.0"
} }
}, },
"develop": { "develop": {
@ -699,14 +694,6 @@
], ],
"version": "==1.4.3" "version": "==1.4.3"
}, },
"appnope": {
"hashes": [
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.0"
},
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a",
@ -871,10 +858,10 @@
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", "sha256:99c77677f31f255e130f3fed4c8e0eebb35f1a09df98ff965fff6774f71688cf",
"sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" "sha256:99cd0403cecd8a13b95d2e045b9fcaa7837137fcc5ec3105f2c413305d82c143"
], ],
"version": "==3.0.5" "version": "==3.0.7"
}, },
"honcho": { "honcho": {
"hashes": [ "hashes": [
@ -944,10 +931,10 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484",
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668"
], ],
"version": "==2.11.1" "version": "==3.0.0a1"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
"hashes": [ "hashes": [
@ -1056,10 +1043,10 @@
}, },
"parso": { "parso": {
"hashes": [ "hashes": [
"sha256:1376bdc8cb81377ca481976933773295218a2df47d3e1182ba76d372b1acb128", "sha256:56b2105a80e9c4df49de85e125feb6be69f49920e121406f15e7acde6c9dfc57",
"sha256:597f36de5102a8db05ffdf7ecdc761838b86565a4a111604c6e78beaedf1b045" "sha256:951af01f61e6dccd04159042a0706a31ad437864ec6e25d0d7a96a9fbb9b0095"
], ],
"version": "==0.6.0" "version": "==0.6.1"
}, },
"pathspec": { "pathspec": {
"hashes": [ "hashes": [
@ -1158,11 +1145,11 @@
}, },
"pytest-flask": { "pytest-flask": {
"hashes": [ "hashes": [
"sha256:283730b469604ecb94caac28df99a40b7c785b828dd8d3323596718b51dfaeb2", "sha256:9001f6128c5c4a0d243ce46c117f3691052828d2faf39ac151b8388657dce447",
"sha256:d874781b622210d8c5d8061cdb091cb059fcb12203125110bd8e6f9256ccbf49" "sha256:cbd8c5b9f8f1b83e9c159ac4294964807c4934317a5fba181739ac15e1b823e6"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.15.0" "version": "==0.15.1"
}, },
"pytest-mock": { "pytest-mock": {
"hashes": [ "hashes": [
@ -1270,10 +1257,10 @@
}, },
"stevedore": { "stevedore": {
"hashes": [ "hashes": [
"sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b",
"sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14" "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"
], ],
"version": "==1.31.0" "version": "==1.32.0"
}, },
"text-unidecode": { "text-unidecode": {
"hashes": [ "hashes": [
@ -1361,10 +1348,10 @@
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [
"sha256:ccc94ed0909b58ffe34430ea5451f07bc0c76467d7081619a454bf5c98b89e28", "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50",
"sha256:feae2f18633c32fc71f2de629bfb3bd3c9325cd4419642b1f1da42ee488d9b98" "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e"
], ],
"version": "==2.1.0" "version": "==2.2.0"
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -61,12 +61,13 @@ class UnknownServerException(GeneralCSPException):
"""An error occured on the CSP side (5xx) and we don't know why """An error occured on the CSP side (5xx) and we don't know why
""" """
def __init__(self, server_error): def __init__(self, status_code, server_error):
self.status_code = status_code
self.server_error = server_error self.server_error = server_error
@property @property
def message(self): def message(self):
return "A server error occured: {}".format(self.server_error) return f"A server error with status code [{self.status_code}] occured: {self.server_error}"
class EnvironmentCreationException(GeneralCSPException): class EnvironmentCreationException(GeneralCSPException):

View File

@ -72,7 +72,7 @@ class MockCloudProvider(CloudProviderInterface):
AUTHENTICATION_EXCEPTION = AuthenticationException("Authentication failure.") AUTHENTICATION_EXCEPTION = AuthenticationException("Authentication failure.")
AUTHORIZATION_EXCEPTION = AuthorizationException("Not authorized.") AUTHORIZATION_EXCEPTION = AuthorizationException("Not authorized.")
NETWORK_EXCEPTION = ConnectionException("Network failure.") NETWORK_EXCEPTION = ConnectionException("Network failure.")
SERVER_EXCEPTION = UnknownServerException("Not our fault.") SERVER_EXCEPTION = UnknownServerException(500, "Not our fault.")
SERVER_FAILURE_PCT = 1 SERVER_FAILURE_PCT = 1
NETWORK_FAILURE_PCT = 7 NETWORK_FAILURE_PCT = 7
@ -129,10 +129,6 @@ class MockCloudProvider(CloudProviderInterface):
payload is an instance of TenantCSPPayload data class payload is an instance of TenantCSPPayload data class
""" """
self._authorize("admin")
self._delay(1, 5)
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)

View File

@ -109,6 +109,14 @@ def _build_transitions(csp_stages):
dest=compose_state(csp_stage, StageStates.FAILED), dest=compose_state(csp_stage, StageStates.FAILED),
) )
) )
transitions.append(
dict(
trigger="resume_progress_" + csp_stage.name.lower(),
source=compose_state(csp_stage, StageStates.FAILED),
dest=compose_state(csp_stage, StageStates.IN_PROGRESS),
conditions=["is_ready_resume_progress"],
)
)
return states, transitions return states, transitions
@ -130,15 +138,20 @@ class FSMMixin:
] ]
def fail_stage(self, stage): def fail_stage(self, stage):
fail_trigger = "fail" + stage fail_trigger = f"fail_{stage}"
if fail_trigger in self.machine.get_triggers(self.current_state.name): if fail_trigger in self.machine.get_triggers(self.current_state.name):
self.trigger(fail_trigger) self.trigger(fail_trigger)
app.logger.info( app.logger.info(
f"calling fail trigger '{fail_trigger}' for '{self.__repr__()}'" f"calling fail trigger '{fail_trigger}' for '{self.__repr__()}'"
) )
else:
app.logger.info(
f"could not locate fail trigger '{fail_trigger}' for '{self.__repr__()}'"
)
def finish_stage(self, stage): def finish_stage(self, stage):
finish_trigger = "finish_" + stage finish_trigger = f"finish_{stage}"
if finish_trigger in self.machine.get_triggers(self.current_state.name): if finish_trigger in self.machine.get_triggers(self.current_state.name):
app.logger.info( app.logger.info(
f"calling finish trigger '{finish_trigger}' for '{self.__repr__()}'" f"calling finish trigger '{finish_trigger}' for '{self.__repr__()}'"

View File

@ -15,13 +15,31 @@ from atst.database import db
from atst.models.types import Id from atst.models.types import Id
from atst.models.base import Base from atst.models.base import Base
import atst.models.mixins as mixins import atst.models.mixins as mixins
from atst.models.mixins.state_machines import FSMStates, AzureStages, _build_transitions from atst.models.mixins.state_machines import (
FSMStates,
AzureStages,
StageStates,
_build_transitions,
)
class StateMachineMisconfiguredError(Exception):
def __init__(self, class_details):
self.class_details = class_details
@property
def message(self):
return self.class_details
def _stage_to_classname(stage): def _stage_to_classname(stage):
return "".join(map(lambda word: word.capitalize(), stage.split("_"))) return "".join(map(lambda word: word.capitalize(), stage.split("_")))
def _stage_state_to_stage_name(state, stage_state):
return state.name.split(f"_{stage_state.name}")[0].lower()
def get_stage_csp_class(stage, class_type): def get_stage_csp_class(stage, class_type):
""" """
given a stage name and class_type return the class given a stage name and class_type return the class
@ -34,7 +52,9 @@ def get_stage_csp_class(stage, class_type):
importlib.import_module("atst.domain.csp.cloud.models"), cls_name importlib.import_module("atst.domain.csp.cloud.models"), cls_name
) )
except AttributeError: except AttributeError:
print("could not import CSP Result class <%s>" % cls_name) raise StateMachineMisconfiguredError(
f"could not import CSP Payload/Result class {cls_name}"
)
@add_state_features(Tags) @add_state_features(Tags)
@ -74,7 +94,7 @@ class PortfolioStateMachine(
return f"<PortfolioStateMachine(state='{self.current_state.name}', portfolio='{self.portfolio.name}'" return f"<PortfolioStateMachine(state='{self.current_state.name}', portfolio='{self.portfolio.name}'"
@reconstructor @reconstructor
def attach_machine(self): def attach_machine(self, stages=AzureStages):
""" """
This is called as a result of a sqlalchemy query. This is called as a result of a sqlalchemy query.
Attach a machine depending on the current state. Attach a machine depending on the current state.
@ -86,7 +106,7 @@ class PortfolioStateMachine(
auto_transitions=False, auto_transitions=False,
after_state_change="after_state_change", after_state_change="after_state_change",
) )
states, transitions = _build_transitions(AzureStages) states, transitions = _build_transitions(stages)
self.machine.add_states(self.system_states + states) self.machine.add_states(self.system_states + states)
self.machine.add_transitions(self.system_transitions + transitions) self.machine.add_transitions(self.system_transitions + transitions)
@ -122,7 +142,23 @@ class PortfolioStateMachine(
app.logger.info( app.logger.info(
f"could not locate 'create trigger' for {self.__repr__()}" f"could not locate 'create trigger' for {self.__repr__()}"
) )
self.fail_stage(stage) self.trigger("fail")
elif self.current_state == FSMStates.FAILED:
# get the first trigger that starts with 'resume_progress_'
resume_progress_trigger = next(
filter(
lambda trigger: trigger.startswith("resume_progress_"),
self.machine.get_triggers(FSMStates.FAILED.name),
),
None,
)
if resume_progress_trigger:
self.trigger(resume_progress_trigger, **kwargs)
else:
app.logger.info(
f"could not locate 'resume progress trigger' for {self.__repr__()}"
)
elif state_obj.is_CREATED: elif state_obj.is_CREATED:
# if last CREATED state then transition to COMPLETED # if last CREATED state then transition to COMPLETED
@ -147,15 +183,18 @@ class PortfolioStateMachine(
self.trigger(create_trigger, **kwargs) self.trigger(create_trigger, **kwargs)
def after_in_progress_callback(self, event): def after_in_progress_callback(self, event):
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
# Accumulate payload w/ creds # Accumulate payload w/ creds
payload = event.kwargs.get("csp_data") payload = event.kwargs.get("csp_data")
current_stage = _stage_state_to_stage_name(
self.current_state, StageStates.IN_PROGRESS
)
payload_data_cls = get_stage_csp_class(current_stage, "payload")
payload_data_cls = get_stage_csp_class(stage, "payload")
if not payload_data_cls: if not payload_data_cls:
app.logger.info(f"could not resolve payload data class for stage {stage}") app.logger.info(
self.fail_stage(stage) f"could not resolve payload data class for stage {current_stage}"
)
self.fail_stage(current_stage)
try: try:
payload_data = payload_data_cls(**payload) payload_data = payload_data_cls(**payload)
except PydanticValidationError as exc: except PydanticValidationError as exc:
@ -165,39 +204,38 @@ class PortfolioStateMachine(
app.logger.info(exc.json()) app.logger.info(exc.json())
print(exc.json()) print(exc.json())
app.logger.info(payload) app.logger.info(payload)
self.fail_stage(stage) self.fail_stage(current_stage)
else:
# TODO: Determine best place to do this, maybe @reconstructor
self.csp = app.csp.cloud
try: # TODO: Determine best place to do this, maybe @reconstructor
func_name = f"create_{stage}" self.csp = app.csp.cloud
response = getattr(self.csp, func_name)(payload_data)
if self.portfolio.csp_data is None:
self.portfolio.csp_data = {}
self.portfolio.csp_data.update(response.dict())
db.session.add(self.portfolio)
db.session.commit()
except PydanticValidationError as exc:
app.logger.error(
f"Failed to cast response to valid result class {self.__repr__()}:",
exc_info=1,
)
app.logger.info(exc.json())
print(exc.json())
app.logger.info(payload_data)
# TODO: Ensure that failing the stage does not preclude a Celery retry
self.fail_stage(stage)
# TODO: catch and handle general CSP exception here
except (ConnectionException, UnknownServerException) as exc:
app.logger.error(
f"CSP api call. Caught exception for {self.__repr__()}.",
exc_info=1,
)
# TODO: Ensure that failing the stage does not preclude a Celery retry
self.fail_stage(stage)
self.finish_stage(stage) try:
func_name = f"create_{current_stage}"
response = getattr(self.csp, func_name)(payload_data)
if self.portfolio.csp_data is None:
self.portfolio.csp_data = {}
self.portfolio.csp_data.update(response.dict())
db.session.add(self.portfolio)
db.session.commit()
except PydanticValidationError as exc:
app.logger.error(
f"Failed to cast response to valid result class {self.__repr__()}:",
exc_info=1,
)
app.logger.info(exc.json())
print(exc.json())
app.logger.info(payload_data)
# TODO: Ensure that failing the stage does not preclude a Celery retry
self.fail_stage(current_stage)
# TODO: catch and handle general CSP exception here
except (ConnectionException, UnknownServerException) as exc:
app.logger.error(
f"CSP api call. Caught exception for {self.__repr__()}.", exc_info=1,
)
# TODO: Ensure that failing the stage does not preclude a Celery retry
self.fail_stage(current_stage)
self.finish_stage(current_stage)
def is_csp_data_valid(self, event): def is_csp_data_valid(self, event):
""" """
@ -211,6 +249,13 @@ class PortfolioStateMachine(
return True return True
def is_ready_resume_progress(self, event):
"""
This function guards advancing states from FAILED to *_IN_PROGRESS.
"""
return True
@property @property
def application_id(self): def application_id(self):
return None return None

View File

@ -1,13 +1,20 @@
import json import json
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from uuid import uuid4 from uuid import uuid4
import pendulum import pendulum
import pydantic import pydantic
import pytest import pytest
from tests.factories import ApplicationFactory, EnvironmentFactory from tests.factories import ApplicationFactory, EnvironmentFactory
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
from atst.domain.csp.cloud.exceptions import (
AuthenticationException,
UserProvisioningException,
ConnectionException,
UnknownServerException,
SecretException,
)
from atst.domain.csp.cloud import AzureCloudProvider from atst.domain.csp.cloud import AzureCloudProvider
from atst.domain.csp.cloud.models import ( from atst.domain.csp.cloud.models import (
AdminRoleDefinitionCSPPayload, AdminRoleDefinitionCSPPayload,
@ -66,6 +73,23 @@ from atst.domain.csp.cloud.exceptions import UserProvisioningException
BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31" BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31"
def mock_requests_response(
status=200, content="CONTENT", json_data=None, raise_for_status=None
):
mock_resp = Mock()
# mock raise_for_status call w/optional error
mock_resp.raise_for_status = Mock()
if raise_for_status:
mock_resp.raise_for_status.side_effect = raise_for_status
# set status code and content
mock_resp.status_code = status
mock_resp.content = content
# add json data if provided
if json_data:
mock_resp.json = mock.Mock(return_value=json_data)
return mock_resp
def mock_management_group_create(mock_azure, spec_dict): def mock_management_group_create(mock_azure, spec_dict):
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = ( mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = (
spec_dict spec_dict
@ -190,7 +214,18 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
"userId": "1153801116406515559", "userId": "1153801116406515559",
} }
mock_result.status_code = 200 mock_result.status_code = 200
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = TenantCSPPayload( payload = TenantCSPPayload(
**dict( **dict(
user_id="admin", user_id="admin",
@ -203,30 +238,16 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
) )
) )
mock_azure = mock_get_secret(mock_azure) mock_azure = mock_get_secret(mock_azure)
result = mock_azure.create_tenant(payload)
body: TenantCSPResult = result.get("body")
assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
with pytest.raises(ConnectionException):
mock_azure.create_tenant(payload)
with pytest.raises(ConnectionException):
mock_azure.create_tenant(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_tenant(payload)
def test_create_tenant_fails(mock_azure: AzureCloudProvider): result: TenantCSPResult = mock_azure.create_tenant(payload)
mock_result = Mock() assert result.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
mock_result.json.return_value = {"error": "body"}
mock_result.status_code = 403
mock_azure.sdk.requests.post.return_value = mock_result
payload = TenantCSPPayload(
**dict(
user_id="admin",
password="JediJan13$coot", # pragma: allowlist secret
domain_name="jediccpospawnedtenant2",
first_name="Tedry",
last_name="Tenet",
country_code="US",
password_recovery_email_address="thomas@promptworks.com",
)
)
mock_azure = mock_get_secret(mock_azure)
result = mock_azure.create_tenant(payload)
assert result.get("status") == "error"
def test_create_billing_profile_creation(mock_azure: AzureCloudProvider): def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
@ -240,7 +261,20 @@ def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
"Retry-After": "10", "Retry-After": "10",
} }
mock_result.status_code = 202 mock_result.status_code = 202
mock_azure.sdk.requests.post.return_value = mock_result
mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = BillingProfileCreationCSPPayload( payload = BillingProfileCreationCSPPayload(
**dict( **dict(
address=dict( address=dict(
@ -256,8 +290,16 @@ def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
billing_account_name=BILLING_ACCOUNT_NAME, billing_account_name=BILLING_ACCOUNT_NAME,
) )
) )
result = mock_azure.create_billing_profile_creation(payload) with pytest.raises(ConnectionException):
body: BillingProfileCreationCSPResult = result.get("body") mock_azure.create_billing_profile_creation(payload)
with pytest.raises(ConnectionException):
mock_azure.create_billing_profile_creation(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_billing_profile_creation(payload)
body: BillingProfileCreationCSPResult = mock_azure.create_billing_profile_creation(
payload
)
assert body.billing_profile_retry_after == 10 assert body.billing_profile_retry_after == 10
@ -297,7 +339,18 @@ def test_validate_billing_profile_creation(mock_azure: AzureCloudProvider):
}, },
"type": "Microsoft.Billing/billingAccounts/billingProfiles", "type": "Microsoft.Billing/billingAccounts/billingProfiles",
} }
mock_azure.sdk.requests.get.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.get.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = BillingProfileVerificationCSPPayload( payload = BillingProfileVerificationCSPPayload(
**dict( **dict(
@ -305,9 +358,16 @@ def test_validate_billing_profile_creation(mock_azure: AzureCloudProvider):
billing_profile_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", billing_profile_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_billing_profile_verification(payload)
with pytest.raises(ConnectionException):
mock_azure.create_billing_profile_verification(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_billing_profile_verification(payload)
result = mock_azure.create_billing_profile_verification(payload) body: BillingProfileVerificationCSPResult = mock_azure.create_billing_profile_verification(
body: BillingProfileVerificationCSPResult = result.get("body") payload
)
assert body.billing_profile_name == "KQWI-W2SU-BG7-TGB" assert body.billing_profile_name == "KQWI-W2SU-BG7-TGB"
assert ( assert (
body.billing_profile_properties.billing_profile_display_name body.billing_profile_properties.billing_profile_display_name
@ -336,7 +396,18 @@ def test_create_billing_profile_tenant_access(mock_azure: AzureCloudProvider):
"type": "Microsoft.Billing/billingRoleAssignments", "type": "Microsoft.Billing/billingRoleAssignments",
} }
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = BillingProfileTenantAccessCSPPayload( payload = BillingProfileTenantAccessCSPPayload(
**dict( **dict(
@ -346,9 +417,16 @@ def test_create_billing_profile_tenant_access(mock_azure: AzureCloudProvider):
billing_profile_name="KQWI-W2SU-BG7-TGB", billing_profile_name="KQWI-W2SU-BG7-TGB",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_billing_profile_tenant_access(payload)
with pytest.raises(ConnectionException):
mock_azure.create_billing_profile_tenant_access(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_billing_profile_tenant_access(payload)
result = mock_azure.create_billing_profile_tenant_access(payload) body: BillingProfileTenantAccessCSPResult = mock_azure.create_billing_profile_tenant_access(
body: BillingProfileTenantAccessCSPResult = result.get("body") payload
)
assert ( assert (
body.billing_role_assignment_name body.billing_role_assignment_name
== "40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d" == "40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d"
@ -367,7 +445,18 @@ def test_create_task_order_billing_creation(mock_azure: AzureCloudProvider):
"Retry-After": "10", "Retry-After": "10",
} }
mock_azure.sdk.requests.patch.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.patch.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = TaskOrderBillingCreationCSPPayload( payload = TaskOrderBillingCreationCSPPayload(
**dict( **dict(
@ -376,9 +465,16 @@ def test_create_task_order_billing_creation(mock_azure: AzureCloudProvider):
billing_profile_name="KQWI-W2SU-BG7-TGB", billing_profile_name="KQWI-W2SU-BG7-TGB",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_task_order_billing_creation(payload)
with pytest.raises(ConnectionException):
mock_azure.create_task_order_billing_creation(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_task_order_billing_creation(payload)
result = mock_azure.create_task_order_billing_creation(payload) body: TaskOrderBillingCreationCSPResult = mock_azure.create_task_order_billing_creation(
body: TaskOrderBillingCreationCSPResult = result.get("body") payload
)
assert ( assert (
body.task_order_billing_verify_url body.task_order_billing_verify_url
@ -428,7 +524,18 @@ def test_create_task_order_billing_verification(mock_azure):
}, },
"type": "Microsoft.Billing/billingAccounts/billingProfiles", "type": "Microsoft.Billing/billingAccounts/billingProfiles",
} }
mock_azure.sdk.requests.get.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.get.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = TaskOrderBillingVerificationCSPPayload( payload = TaskOrderBillingVerificationCSPPayload(
**dict( **dict(
@ -436,9 +543,16 @@ def test_create_task_order_billing_verification(mock_azure):
task_order_billing_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", task_order_billing_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_task_order_billing_verification(payload)
with pytest.raises(ConnectionException):
mock_azure.create_task_order_billing_verification(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_task_order_billing_verification(payload)
result = mock_azure.create_task_order_billing_verification(payload) body: TaskOrderBillingVerificationCSPResult = mock_azure.create_task_order_billing_verification(
body: TaskOrderBillingVerificationCSPResult = result.get("body") payload
)
assert body.billing_profile_name == "KQWI-W2SU-BG7-TGB" assert body.billing_profile_name == "KQWI-W2SU-BG7-TGB"
assert ( assert (
body.billing_profile_enabled_plan_details.enabled_azure_plans[0].get("skuId") body.billing_profile_enabled_plan_details.enabled_azure_plans[0].get("skuId")
@ -463,7 +577,18 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
"type": "Microsoft.Billing/billingAccounts/billingProfiles/billingInstructions", "type": "Microsoft.Billing/billingAccounts/billingProfiles/billingInstructions",
} }
mock_azure.sdk.requests.put.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.put.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = BillingInstructionCSPPayload( payload = BillingInstructionCSPPayload(
**dict( **dict(
@ -477,8 +602,13 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
billing_profile_name="KQWI-W2SU-BG7-TGB", billing_profile_name="KQWI-W2SU-BG7-TGB",
) )
) )
result = mock_azure.create_billing_instruction(payload) with pytest.raises(ConnectionException):
body: BillingInstructionCSPResult = result.get("body") mock_azure.create_billing_instruction(payload)
with pytest.raises(ConnectionException):
mock_azure.create_billing_instruction(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_billing_instruction(payload)
body: BillingInstructionCSPResult = mock_azure.create_billing_instruction(payload)
assert body.reported_clin_name == "TO1:CLIN001" assert body.reported_clin_name == "TO1:CLIN001"
@ -494,7 +624,18 @@ def test_create_product_purchase(mock_azure: AzureCloudProvider):
"Retry-After": "10", "Retry-After": "10",
} }
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = ProductPurchaseCSPPayload( payload = ProductPurchaseCSPPayload(
**dict( **dict(
@ -503,9 +644,14 @@ def test_create_product_purchase(mock_azure: AzureCloudProvider):
billing_profile_name="KQWI-W2SU-BG7-TGB", billing_profile_name="KQWI-W2SU-BG7-TGB",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_product_purchase(payload)
with pytest.raises(ConnectionException):
mock_azure.create_product_purchase(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_product_purchase(payload)
result = mock_azure.create_product_purchase(payload) body: ProductPurchaseCSPResult = mock_azure.create_product_purchase(payload)
body: ProductPurchaseCSPResult = result.get("body")
assert ( assert (
body.product_purchase_verify_url body.product_purchase_verify_url
== "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/patchBillingProfile_KQWI-W2SU-BG7-TGB:02715576-4118-466c-bca7-b1cd3169ff46?api-version=2019-10-01-preview" == "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/patchBillingProfile_KQWI-W2SU-BG7-TGB:02715576-4118-466c-bca7-b1cd3169ff46?api-version=2019-10-01-preview"
@ -540,7 +686,18 @@ def test_create_product_purchase_verification(mock_azure):
"type": "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections/products", "type": "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections/products",
} }
mock_azure.sdk.requests.get.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.get.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = ProductPurchaseVerificationCSPPayload( payload = ProductPurchaseVerificationCSPPayload(
**dict( **dict(
@ -548,9 +705,16 @@ def test_create_product_purchase_verification(mock_azure):
product_purchase_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", product_purchase_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_product_purchase_verification(payload)
with pytest.raises(ConnectionException):
mock_azure.create_product_purchase_verification(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_product_purchase_verification(payload)
result = mock_azure.create_product_purchase_verification(payload) body: ProductPurchaseVerificationCSPResult = mock_azure.create_product_purchase_verification(
body: ProductPurchaseVerificationCSPResult = result.get("body") payload
)
assert body.premium_purchase_date == "01/31/2020" assert body.premium_purchase_date == "01/31/2020"
@ -566,12 +730,31 @@ def test_create_tenant_principal_app(mock_azure: AzureCloudProvider):
mock_result.ok = True mock_result.ok = True
mock_result.json.return_value = {"appId": "appId", "id": "id"} mock_result.json.return_value = {"appId": "appId", "id": "id"}
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
mock_azure = mock_get_secret(mock_azure) mock_azure = mock_get_secret(mock_azure)
payload = TenantPrincipalAppCSPPayload( payload = TenantPrincipalAppCSPPayload(
**{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"} **{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"}
) )
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal_app(payload)
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal_app(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_tenant_principal_app(payload)
result: TenantPrincipalAppCSPResult = mock_azure.create_tenant_principal_app( result: TenantPrincipalAppCSPResult = mock_azure.create_tenant_principal_app(
payload payload
) )
@ -591,7 +774,19 @@ def test_create_tenant_principal(mock_azure: AzureCloudProvider):
mock_result.ok = True mock_result.ok = True
mock_result.json.return_value = {"id": "principal_id"} mock_result.json.return_value = {"id": "principal_id"}
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
mock_azure = mock_get_secret(mock_azure) mock_azure = mock_get_secret(mock_azure)
payload = TenantPrincipalCSPPayload( payload = TenantPrincipalCSPPayload(
@ -600,6 +795,12 @@ def test_create_tenant_principal(mock_azure: AzureCloudProvider):
"principal_app_id": "appId", "principal_app_id": "appId",
} }
) )
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal(payload)
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_tenant_principal(payload)
result: TenantPrincipalCSPResult = mock_azure.create_tenant_principal(payload) result: TenantPrincipalCSPResult = mock_azure.create_tenant_principal(payload)
@ -618,7 +819,18 @@ def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider):
mock_result.ok = True mock_result.ok = True
mock_result.json.return_value = {"secretText": "new secret key"} mock_result.json.return_value = {"secretText": "new secret key"}
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
mock_azure = mock_get_secret(mock_azure) mock_azure = mock_get_secret(mock_azure)
@ -629,6 +841,12 @@ def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider):
"principal_app_object_id": "appObjId", "principal_app_object_id": "appObjId",
} }
) )
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal_credential(payload)
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal_credential(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_tenant_principal_credential(payload)
result: TenantPrincipalCredentialCSPResult = mock_azure.create_tenant_principal_credential( result: TenantPrincipalCredentialCSPResult = mock_azure.create_tenant_principal_credential(
payload payload
@ -654,12 +872,29 @@ def test_create_admin_role_definition(mock_azure: AzureCloudProvider):
] ]
} }
mock_azure.sdk.requests.get.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.get.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
mock_azure = mock_get_secret(mock_azure) mock_azure = mock_get_secret(mock_azure)
payload = AdminRoleDefinitionCSPPayload( payload = AdminRoleDefinitionCSPPayload(
**{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"} **{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"}
) )
with pytest.raises(ConnectionException):
mock_azure.create_admin_role_definition(payload)
with pytest.raises(ConnectionException):
mock_azure.create_admin_role_definition(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_admin_role_definition(payload)
result: AdminRoleDefinitionCSPResult = mock_azure.create_admin_role_definition( result: AdminRoleDefinitionCSPResult = mock_azure.create_admin_role_definition(
payload payload
@ -680,7 +915,18 @@ def test_create_tenant_admin_ownership(mock_azure: AzureCloudProvider):
mock_result.ok = True mock_result.ok = True
mock_result.json.return_value = {"id": "id"} mock_result.json.return_value = {"id": "id"}
mock_azure.sdk.requests.put.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.put.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = TenantAdminOwnershipCSPPayload( payload = TenantAdminOwnershipCSPPayload(
**{ **{
@ -688,6 +934,12 @@ def test_create_tenant_admin_ownership(mock_azure: AzureCloudProvider):
"user_object_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d", "user_object_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d",
} }
) )
with pytest.raises(ConnectionException):
mock_azure.create_tenant_admin_ownership(payload)
with pytest.raises(ConnectionException):
mock_azure.create_tenant_admin_ownership(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_tenant_admin_ownership(payload)
result: TenantAdminOwnershipCSPResult = mock_azure.create_tenant_admin_ownership( result: TenantAdminOwnershipCSPResult = mock_azure.create_tenant_admin_ownership(
payload payload
@ -708,7 +960,18 @@ def test_create_tenant_principal_ownership(mock_azure: AzureCloudProvider):
mock_result.ok = True mock_result.ok = True
mock_result.json.return_value = {"id": "id"} mock_result.json.return_value = {"id": "id"}
mock_azure.sdk.requests.put.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.put.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = TenantPrincipalOwnershipCSPPayload( payload = TenantPrincipalOwnershipCSPPayload(
**{ **{
@ -716,6 +979,12 @@ def test_create_tenant_principal_ownership(mock_azure: AzureCloudProvider):
"principal_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d", "principal_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d",
} }
) )
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal_ownership(payload)
with pytest.raises(ConnectionException):
mock_azure.create_tenant_principal_ownership(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_tenant_principal_ownership(payload)
result: TenantPrincipalOwnershipCSPResult = mock_azure.create_tenant_principal_ownership( result: TenantPrincipalOwnershipCSPResult = mock_azure.create_tenant_principal_ownership(
payload payload
@ -736,7 +1005,18 @@ def test_create_principal_admin_role(mock_azure: AzureCloudProvider):
mock_result.ok = True mock_result.ok = True
mock_result.json.return_value = {"id": "id"} mock_result.json.return_value = {"id": "id"}
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = PrincipalAdminRoleCSPPayload( payload = PrincipalAdminRoleCSPPayload(
**{ **{
@ -745,6 +1025,12 @@ def test_create_principal_admin_role(mock_azure: AzureCloudProvider):
"admin_role_def_id": uuid4().hex, "admin_role_def_id": uuid4().hex,
} }
) )
with pytest.raises(ConnectionException):
mock_azure.create_principal_admin_role(payload)
with pytest.raises(ConnectionException):
mock_azure.create_principal_admin_role(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_principal_admin_role(payload)
result: PrincipalAdminRoleCSPResult = mock_azure.create_principal_admin_role( result: PrincipalAdminRoleCSPResult = mock_azure.create_principal_admin_role(
payload payload
@ -768,7 +1054,20 @@ def test_create_subscription_creation(mock_azure: AzureCloudProvider):
"Retry-After": 10, "Retry-After": 10,
} }
mock_result.json.return_value = {} mock_result.json.return_value = {}
mock_azure.sdk.requests.put.return_value = mock_result
mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.put.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
management_group_id = str(uuid4()) management_group_id = str(uuid4())
payload = SubscriptionCreationCSPPayload( payload = SubscriptionCreationCSPPayload(
**dict( **dict(
@ -780,6 +1079,12 @@ def test_create_subscription_creation(mock_azure: AzureCloudProvider):
invoice_section_name="6HMZ-2HLO-PJA-TGB", invoice_section_name="6HMZ-2HLO-PJA-TGB",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_subscription_creation(payload)
with pytest.raises(ConnectionException):
mock_azure.create_subscription_creation(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_subscription_creation(payload)
result: SubscriptionCreationCSPResult = mock_azure.create_subscription_creation( result: SubscriptionCreationCSPResult = mock_azure.create_subscription_creation(
payload payload
@ -801,7 +1106,19 @@ def test_create_subscription_verification(mock_azure: AzureCloudProvider):
mock_result.json.return_value = { mock_result.json.return_value = {
"subscriptionLink": "/subscriptions/60fbbb72-0516-4253-ab18-c92432ba3230" "subscriptionLink": "/subscriptions/60fbbb72-0516-4253-ab18-c92432ba3230"
} }
mock_azure.sdk.requests.get.return_value = mock_result
mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.get.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = SubscriptionVerificationCSPPayload( payload = SubscriptionVerificationCSPPayload(
**dict( **dict(
@ -809,6 +1126,12 @@ def test_create_subscription_verification(mock_azure: AzureCloudProvider):
subscription_verify_url="https://verify.me", subscription_verify_url="https://verify.me",
) )
) )
with pytest.raises(ConnectionException):
mock_azure.create_subscription_verification(payload)
with pytest.raises(ConnectionException):
mock_azure.create_subscription_verification(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.create_subscription_verification(payload)
result: SuscriptionVerificationCSPResult = mock_azure.create_subscription_verification( result: SuscriptionVerificationCSPResult = mock_azure.create_subscription_verification(
payload payload
@ -837,7 +1160,20 @@ def test_get_reporting_data(mock_azure: AzureCloudProvider):
"type": "Microsoft.CostManagement/query", "type": "Microsoft.CostManagement/query",
} }
mock_result.ok = True mock_result.ok = True
mock_azure.sdk.requests.post.return_value = mock_result
mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
mock_azure = mock_get_secret(mock_azure) mock_azure = mock_get_secret(mock_azure)
# Subset of a profile's CSP data that we care about for reporting # Subset of a profile's CSP data that we care about for reporting
@ -851,14 +1187,19 @@ def test_get_reporting_data(mock_azure: AzureCloudProvider):
], ],
}, },
} }
payload = ReportingCSPPayload(
data: CostManagementQueryCSPResult = mock_azure.get_reporting_data( from_date=pendulum.now().subtract(years=1).add(days=1).format("YYYY-MM-DD"),
ReportingCSPPayload( to_date=pendulum.now().format("YYYY-MM-DD"),
from_date=pendulum.now().subtract(years=1).add(days=1).format("YYYY-MM-DD"), **csp_data,
to_date=pendulum.now().format("YYYY-MM-DD"),
**csp_data,
)
) )
with pytest.raises(ConnectionException):
mock_azure.get_reporting_data(payload)
with pytest.raises(ConnectionException):
mock_azure.get_reporting_data(payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure.get_reporting_data(payload)
data: CostManagementQueryCSPResult = mock_azure.get_reporting_data(payload)
assert isinstance(data, CostManagementQueryCSPResult) assert isinstance(data, CostManagementQueryCSPResult)
assert data.name == "e82d0cda-2ffb-4476-a98a-425c83c216f9" assert data.name == "e82d0cda-2ffb-4476-a98a-425c83c216f9"
@ -868,7 +1209,20 @@ def test_get_reporting_data(mock_azure: AzureCloudProvider):
def test_get_reporting_data_malformed_payload(mock_azure: AzureCloudProvider): def test_get_reporting_data_malformed_payload(mock_azure: AzureCloudProvider):
mock_result = Mock() mock_result = Mock()
mock_result.ok = True mock_result.ok = True
mock_azure.sdk.requests.post.return_value = mock_result
mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
mock_azure = mock_get_secret(mock_azure) mock_azure = mock_get_secret(mock_azure)
# Malformed csp_data payloads that should throw pydantic validation errors # Malformed csp_data payloads that should throw pydantic validation errors
@ -905,6 +1259,26 @@ def test_get_secret(mock_azure: AzureCloudProvider):
assert mock_azure.get_secret("secret key") == "my secret" assert mock_azure.get_secret("secret key") == "my secret"
def test_get_secret_secret_exception(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_client_secret_credential_obj",
wraps=mock_azure._get_client_secret_credential_obj,
) as _get_client_secret_credential_obj:
_get_client_secret_credential_obj.return_value = {}
mock_azure.sdk.secrets.SecretClient.return_value.get_secret.return_value = (
"my secret"
)
mock_azure.sdk.secrets.SecretClient.return_value.get_secret.side_effect = [
mock_azure.sdk.azure_exceptions.HttpResponseError,
]
with pytest.raises(SecretException):
mock_azure.get_secret("secret key") == "my secret"
def test_set_secret(mock_azure: AzureCloudProvider): def test_set_secret(mock_azure: AzureCloudProvider):
with patch.object( with patch.object(
AzureCloudProvider, AzureCloudProvider,
@ -916,15 +1290,45 @@ def test_set_secret(mock_azure: AzureCloudProvider):
mock_azure.sdk.secrets.SecretClient.return_value.set_secret.return_value = ( mock_azure.sdk.secrets.SecretClient.return_value.set_secret.return_value = (
"my secret" "my secret"
) )
assert mock_azure.set_secret("secret key", "secret_value") == "my secret" assert mock_azure.set_secret("secret key", "secret_value") == "my secret"
def test_set_secret_secret_exception(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_client_secret_credential_obj",
wraps=mock_azure._get_client_secret_credential_obj,
) as _get_client_secret_credential_obj:
_get_client_secret_credential_obj.return_value = {}
mock_azure.sdk.secrets.SecretClient.return_value.set_secret.return_value = (
"my secret"
)
mock_azure.sdk.secrets.SecretClient.return_value.set_secret.side_effect = [
mock_azure.sdk.azure_exceptions.HttpResponseError,
]
with pytest.raises(SecretException):
mock_azure.set_secret("secret key", "secret_value")
def test_create_active_directory_user(mock_azure: AzureCloudProvider): def test_create_active_directory_user(mock_azure: AzureCloudProvider):
mock_result = Mock() mock_result = Mock()
mock_result.ok = True mock_result.ok = True
mock_result.json.return_value = {"id": "id"} mock_result.json.return_value = {"id": "id"}
mock_azure.sdk.requests.post.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.post.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = UserCSPPayload( payload = UserCSPPayload(
tenant_id=uuid4().hex, tenant_id=uuid4().hex,
@ -933,6 +1337,12 @@ def test_create_active_directory_user(mock_azure: AzureCloudProvider):
email="test@testerson.test", email="test@testerson.test",
password="asdfghjkl", # pragma: allowlist secret password="asdfghjkl", # pragma: allowlist secret
) )
with pytest.raises(ConnectionException):
mock_azure._create_active_directory_user("token", payload)
with pytest.raises(ConnectionException):
mock_azure._create_active_directory_user("token", payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure._create_active_directory_user("token", payload)
result = mock_azure._create_active_directory_user("token", payload) result = mock_azure._create_active_directory_user("token", payload)
@ -942,8 +1352,18 @@ def test_create_active_directory_user(mock_azure: AzureCloudProvider):
def test_update_active_directory_user_email(mock_azure: AzureCloudProvider): def test_update_active_directory_user_email(mock_azure: AzureCloudProvider):
mock_result = Mock() mock_result = Mock()
mock_result.ok = True mock_result.ok = True
mock_azure.sdk.requests.patch.return_value = mock_result mock_http_error_resp = mock_requests_response(
status=500,
raise_for_status=mock_azure.sdk.requests.exceptions.HTTPError(
"500 Server Error"
),
)
mock_azure.sdk.requests.patch.side_effect = [
mock_azure.sdk.requests.exceptions.ConnectionError,
mock_azure.sdk.requests.exceptions.Timeout,
mock_http_error_resp,
mock_result,
]
payload = UserCSPPayload( payload = UserCSPPayload(
tenant_id=uuid4().hex, tenant_id=uuid4().hex,
display_name="Test Testerson", display_name="Test Testerson",
@ -951,6 +1371,12 @@ def test_update_active_directory_user_email(mock_azure: AzureCloudProvider):
email="test@testerson.test", email="test@testerson.test",
password="asdfghjkl", # pragma: allowlist secret password="asdfghjkl", # pragma: allowlist secret
) )
with pytest.raises(ConnectionException):
mock_azure._update_active_directory_user_email("token", uuid4().hex, payload)
with pytest.raises(ConnectionException):
mock_azure._update_active_directory_user_email("token", uuid4().hex, payload)
with pytest.raises(UnknownServerException, match=r".*500 Server Error.*"):
mock_azure._update_active_directory_user_email("token", uuid4().hex, payload)
result = mock_azure._update_active_directory_user_email( result = mock_azure._update_active_directory_user_email(
"token", uuid4().hex, payload "token", uuid4().hex, payload
@ -985,7 +1411,6 @@ def test_create_user(mock_azure: AzureCloudProvider):
) )
result = mock_azure.create_user(payload) result = mock_azure.create_user(payload)
assert result.id == "id" assert result.id == "id"

View File

@ -1,7 +1,14 @@
import pytest import pytest
from atst.domain.csp import MockCloudProvider from atst.domain.csp import MockCloudProvider
from atst.domain.csp.cloud.models import EnvironmentCSPPayload, EnvironmentCSPResult from atst.domain.csp.cloud.models import (
EnvironmentCSPPayload,
EnvironmentCSPResult,
TenantCSPPayload,
TenantCSPResult,
BillingProfileCreationCSPPayload,
BillingProfileCreationCSPResult,
)
from tests.factories import EnvironmentFactory, EnvironmentRoleFactory, UserFactory from tests.factories import EnvironmentFactory, EnvironmentRoleFactory, UserFactory
@ -28,6 +35,43 @@ def test_create_environment(mock_csp: MockCloudProvider):
assert isinstance(result, EnvironmentCSPResult) assert isinstance(result, EnvironmentCSPResult)
def test_create_tenant(mock_csp: MockCloudProvider):
payload = TenantCSPPayload(
**dict(
user_id="admin",
password="JediJan13$coot", # pragma: allowlist secret
domain_name="jediccpospawnedtenant2",
first_name="Tedry",
last_name="Tenet",
country_code="US",
password_recovery_email_address="thomas@promptworks.com",
)
)
result = mock_csp.create_tenant(payload)
assert isinstance(result, TenantCSPResult)
def test_create_billing_profile_creation(mock_csp: MockCloudProvider):
payload = BillingProfileCreationCSPPayload(
**dict(
address=dict(
address_line_1="123 S Broad Street, Suite 2400",
company_name="Promptworks",
city="Philadelphia",
region="PA",
country="US",
postal_code="19109",
),
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
billing_profile_display_name="Test Billing Profile",
billing_account_name="123123",
)
)
result = mock_csp.create_billing_profile_creation(payload)
assert isinstance(result, BillingProfileCreationCSPResult)
def test_create_or_update_user(mock_csp: MockCloudProvider): def test_create_or_update_user(mock_csp: MockCloudProvider):
env_role = EnvironmentRoleFactory.create() env_role = EnvironmentRoleFactory.create()
csp_user_id = mock_csp.create_or_update_user(CREDENTIALS, env_role, "csp_role_id") csp_user_id = mock_csp.create_or_update_user(CREDENTIALS, env_role, "csp_role_id")

View File

@ -1,6 +1,9 @@
import pytest import pytest
import pydantic
import re import re
from unittest import mock from unittest import mock
from enum import Enum
from tests.factories import ( from tests.factories import (
PortfolioStateMachineFactory, PortfolioStateMachineFactory,
@ -8,13 +11,30 @@ from tests.factories import (
) )
from atst.models import FSMStates, PortfolioStateMachine, TaskOrder from atst.models import FSMStates, PortfolioStateMachine, TaskOrder
from atst.models.mixins.state_machines import AzureStages, StageStates, compose_state from atst.domain.csp.cloud.models import BillingProfileCreationCSPPayload
from atst.models.mixins.state_machines import (
AzureStages,
StageStates,
compose_state,
_build_transitions,
_build_csp_states,
)
from atst.models.portfolio import Portfolio from atst.models.portfolio import Portfolio
from atst.models.portfolio_state_machine import get_stage_csp_class from atst.models.portfolio_state_machine import (
get_stage_csp_class,
_stage_to_classname,
_stage_state_to_stage_name,
StateMachineMisconfiguredError,
)
# TODO: Write failure case tests # TODO: Write failure case tests
class AzureStagesTest(Enum):
TENANT = "tenant"
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def portfolio(): def portfolio():
# TODO: setup clin/to as active/funded/ready # TODO: setup clin/to as active/funded/ready
@ -37,14 +57,65 @@ def test_state_machine_trigger_next_transition(portfolio):
assert sm.current_state == FSMStates.STARTED assert sm.current_state == FSMStates.STARTED
def test_state_machine_compose_state(portfolio): def test_state_machine_compose_state():
PortfolioStateMachineFactory.create(portfolio=portfolio)
assert ( assert (
compose_state(AzureStages.TENANT, StageStates.CREATED) compose_state(AzureStages.TENANT, StageStates.CREATED)
== FSMStates.TENANT_CREATED == FSMStates.TENANT_CREATED
) )
def test_stage_to_classname():
assert (
_stage_to_classname(AzureStages.BILLING_PROFILE_CREATION.name)
== "BillingProfileCreation"
)
def test_get_stage_csp_class():
csp_class = get_stage_csp_class(list(AzureStages)[0].name.lower(), "payload")
assert isinstance(csp_class, pydantic.main.ModelMetaclass)
def test_get_stage_csp_class_import_fail():
with pytest.raises(StateMachineMisconfiguredError):
csp_class = get_stage_csp_class("doesnotexist", "payload")
def test_build_transitions():
states, transitions = _build_transitions(AzureStagesTest)
assert [s.get("name").name for s in states] == [
"TENANT_CREATED",
"TENANT_IN_PROGRESS",
"TENANT_FAILED",
]
assert [s.get("tags") for s in states] == [
["TENANT", "CREATED"],
["TENANT", "IN_PROGRESS"],
["TENANT", "FAILED"],
]
assert [t.get("trigger") for t in transitions] == [
"complete",
"create_tenant",
"finish_tenant",
"fail_tenant",
"resume_progress_tenant",
]
def test_build_csp_states():
states = _build_csp_states(AzureStagesTest)
assert list(states) == [
"UNSTARTED",
"STARTING",
"STARTED",
"COMPLETED",
"FAILED",
"TENANT_CREATED",
"TENANT_IN_PROGRESS",
"TENANT_FAILED",
]
def test_state_machine_valid_data_classes_for_stages(portfolio): def test_state_machine_valid_data_classes_for_stages(portfolio):
PortfolioStateMachineFactory.create(portfolio=portfolio) PortfolioStateMachineFactory.create(portfolio=portfolio)
for stage in AzureStages: for stage in AzureStages:
@ -52,6 +123,37 @@ def test_state_machine_valid_data_classes_for_stages(portfolio):
assert get_stage_csp_class(stage.name.lower(), "result") is not None assert get_stage_csp_class(stage.name.lower(), "result") is not None
def test_attach_machine(portfolio):
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
sm.machine = None
sm.attach_machine(stages=AzureStagesTest)
assert list(sm.machine.events) == [
"init",
"start",
"reset",
"fail",
"complete",
"create_tenant",
"finish_tenant",
"fail_tenant",
"resume_progress_tenant",
]
def test_fail_stage(portfolio):
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
sm.state = FSMStates.TENANT_IN_PROGRESS
sm.fail_stage("tenant")
assert sm.state == FSMStates.TENANT_FAILED
def test_stage_state_to_stage_name():
stage = _stage_state_to_stage_name(
FSMStates.TENANT_IN_PROGRESS, StageStates.IN_PROGRESS
)
assert stage == "tenant"
def test_state_machine_initialization(portfolio): def test_state_machine_initialization(portfolio):
sm = PortfolioStateMachineFactory.create(portfolio=portfolio) sm = PortfolioStateMachineFactory.create(portfolio=portfolio)

View File

@ -66,6 +66,12 @@ def mock_policy():
return Mock(spec=policy) return Mock(spec=policy)
def mock_azure_exceptions():
from azure.core import exceptions
return exceptions
def mock_adal(): def mock_adal():
import adal import adal
@ -75,7 +81,9 @@ def mock_adal():
def mock_requests(): def mock_requests():
import requests import requests
return Mock(spec=requests) mock_requests = Mock(wraps=requests)
mock_requests.exceptions = requests.exceptions
return mock_requests
def mock_secrets(): def mock_secrets():
@ -101,6 +109,7 @@ class MockAzureSDK(object):
self.graphrbac = mock_graphrbac() self.graphrbac = mock_graphrbac()
self.credentials = mock_credentials() self.credentials = mock_credentials()
self.identity = mock_identity() self.identity = mock_identity()
self.azure_exceptions = mock_azure_exceptions()
self.policy = mock_policy() self.policy = mock_policy()
self.secrets = mock_secrets() self.secrets = mock_secrets()
self.requests = mock_requests() self.requests = mock_requests()