Merge branch 'staging' into 170690791-cleanup-minikube
This commit is contained in:
commit
76a29e9307
3
Pipfile
3
Pipfile
@ -9,6 +9,8 @@ Unipath = "*"
|
||||
pendulum = "*"
|
||||
redis = "*"
|
||||
sqlalchemy = ">=1.3.12"
|
||||
sqlalchemy-json = "*"
|
||||
pydantic = "*"
|
||||
alembic = "*"
|
||||
"psycopg2-binary" = "*"
|
||||
flask = "*"
|
||||
@ -30,6 +32,7 @@ msrestazure = "*"
|
||||
azure-mgmt-authorization = "*"
|
||||
azure-mgmt-managementgroups = "*"
|
||||
azure-mgmt-resource = "*"
|
||||
transitions = "*"
|
||||
|
||||
[dev-packages]
|
||||
bandit = "*"
|
||||
|
200
Pipfile.lock
generated
200
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "1bf62937e2d8187deb11c56188ec763f56ec055d65c87773c945384ffff68dcc"
|
||||
"sha256": "63b8f9d203f306a6f0ff20514b024909aa7e64917e1befcc9ea79931b5b4bd34"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -257,11 +257,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"isodate": {
|
||||
"hashes": [
|
||||
@ -340,10 +340,10 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
],
|
||||
"version": "==8.0.2"
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"msrest": {
|
||||
"hashes": [
|
||||
@ -423,6 +423,26 @@
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:176885123dfdd8f7ab6e7ba1b66d4197de75ba830bb44d921af88b3d977b8aa5",
|
||||
"sha256:2b32a5f14558c36e39aeefda0c550bfc0f47fc32b4ce16d80dc4df2b33838ed8",
|
||||
"sha256:2eab7d548b0e530bf65bee7855ad8164c2f6a889975d5e9c4eefd1e7c98245dc",
|
||||
"sha256:479ca8dc7cc41418751bf10302ee0a1b1f8eedb2de6c4f4c0f3cf8372b204f9a",
|
||||
"sha256:59235324dd7dc5363a654cd14271ea8631f1a43de5d4fc29c782318fcc498002",
|
||||
"sha256:87673d1de790c8d5282153cab0b09271be77c49aabcedf3ac5ab1a1fd4dcbac0",
|
||||
"sha256:8a8e089aec18c26561e09ee6daf15a3cc06df05bdc67de60a8684535ef54562f",
|
||||
"sha256:b60f2b3b0e0dd74f1800a57d1bbd597839d16faf267e45fa4a5407b15d311085",
|
||||
"sha256:c0da48978382c83f9488c6bbe4350e065ea5c83e85ca5cfb8fa14ac11de3c296",
|
||||
"sha256:cbe284bd5ad67333d49ecc0dc27fa52c25b4c2fe72802a5c060b5f922db58bef",
|
||||
"sha256:d03df07b7611004140b0fef91548878c2b5f48c520a8cb76d11d20e9887a495e",
|
||||
"sha256:d4bb6a75abc2f04f6993124f1ed4221724c9dc3bd9df5cb54132e0b68775d375",
|
||||
"sha256:dacb79144bb3fdb57cf9435e1bd16c35586bc44256215cfaa33bf21565d926ae",
|
||||
"sha256:dd9359db7644317898816f6142f378aa48848dcc5cf14a481236235fde11a148"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3"
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
|
||||
@ -521,6 +541,27 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.3.12"
|
||||
},
|
||||
"sqlalchemy-json": {
|
||||
"hashes": [
|
||||
"sha256:d17952e771eecd9023c0f683d2a6aaa27ce1a6dbf57b0fe2bf4d5aef4c5dad1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.3"
|
||||
},
|
||||
"sqlalchemy-utils": {
|
||||
"hashes": [
|
||||
"sha256:4e637c88bf3ac5f99b7d72342092a1f636bea1287b2e3e17d441b0413771f86e"
|
||||
],
|
||||
"version": "==0.36.1"
|
||||
},
|
||||
"transitions": {
|
||||
"hashes": [
|
||||
"sha256:011afaefa1244177cad3d960d836c0c4a201403252371bd4c555cf8c17ce7d3c",
|
||||
"sha256:5566c9d32e438ee9eb1f046e3ac1a0b2689f32807b47859210162084d4c84ab7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.2"
|
||||
},
|
||||
"unipath": {
|
||||
"hashes": [
|
||||
"sha256:09839adcc72e8a24d4f76d63656f30b5a1f721fc40c9bcd79d8c67bdd8b47dae",
|
||||
@ -568,10 +609,10 @@
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"version": "==1.0.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -687,39 +728,39 @@
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:189aac76d6e0d7af15572c51892e7326ee451c076c5a50a9d266406cd6c49708",
|
||||
"sha256:1bf7ba2af1d373a1750888724f84cffdfc697738f29a353c98195f98fc011509",
|
||||
"sha256:1f4ee8e2e4243971618bc16fcc4478317405205f135e95226c2496e2a3b8dbbf",
|
||||
"sha256:225e79a5d485bc1642cb7ba02281419c633c216cdc6b26c26494ba959f09e69f",
|
||||
"sha256:23688ff75adfa8bfa2a67254d889f9bdf9302c27241d746e17547c42c732d3f4",
|
||||
"sha256:28f7f73b34a05e23758e860a89a7f649b85c6749e252eff60ebb05532d180e86",
|
||||
"sha256:2d0cb9b1fe6ad0d915d45ad3d87f03a38e979093a98597e755930db1f897afae",
|
||||
"sha256:47874b4711c5aeb295c31b228a758ce3d096be83dc37bd56da48ed99efb8813b",
|
||||
"sha256:511ec0c00840e12fb4e852e4db58fa6a01ca4da72f36a9766fae344c3d502033",
|
||||
"sha256:53e7438fef0c97bc248f88ba1edd10268cd94d5609970aaf87abbe493691af87",
|
||||
"sha256:569f9ee3025682afda6e9b0f5bb14897c0db03f1a1dc088b083dd36e743f92bb",
|
||||
"sha256:593853aa1ac6dcc6405324d877544c596c9d948ef20d2e9512a0f5d2d3202356",
|
||||
"sha256:5b0a07158360d22492f9abd02a0f2ee7981b33f0646bf796598b7673f6bbab14",
|
||||
"sha256:7ca3db38a61f3655a2613ee2c190d63639215a7a736d3c64cc7bbdb002ce6310",
|
||||
"sha256:7d1cc7acc9ce55179616cf72154f9e648136ea55987edf84addbcd9886ffeba2",
|
||||
"sha256:88b51153657612aea68fa684a5b88037597925260392b7bb4509d4f9b0bdd889",
|
||||
"sha256:955ec084f549128fa2702f0b2dc696392001d986b71acd8fd47424f28289a9c3",
|
||||
"sha256:b251c7092cbb6d789d62dc9c9e7c4fb448c9138b51285c36aeb72462cad3600e",
|
||||
"sha256:bd82b684bb498c60ef47bb1541a50e6d006dde8579934dcbdbc61d67d1ea70d9",
|
||||
"sha256:bfe102659e2ec13b86c7f3b1db6c9a4e7beea4255058d006351339e6b342d5d2",
|
||||
"sha256:c1e4e39e43057396a5e9d069bfbb6ffeee892e40c5d2effbd8cd71f34ee66c4d",
|
||||
"sha256:cb2b74c123f65e8166f7e1265829a6c8ed755c3cd16d7f50e75a83456a5f3fd7",
|
||||
"sha256:cca38ded59105f7705ef6ffe1e960b8db6c7d8279c1e71654a4775ab4454ca15",
|
||||
"sha256:cf908840896f7aa62d0ec693beb53264b154f972eb8226fb864ac38975590c4f",
|
||||
"sha256:d095a7b473f8a95f7efe821f92058c8a2ecfb18f8db6677ae3819e15dc11aaae",
|
||||
"sha256:d22b4297e7e4225ccf01f1aa55e7a96412ea0796b532dd614c3fcbafa341128e",
|
||||
"sha256:d4a2b578a7a70e0c71f662705262f87a456f1e6c1e40ada7ea699abaf070a76d",
|
||||
"sha256:ddeb42a3d5419434742bf4cc71c9eaa22df3b76808e23a82bd0b0bd360f1a9f1",
|
||||
"sha256:e65a5aa1670db6263f19fdc03daee1d7dbbadb5cb67fd0a1f16033659db13c1d",
|
||||
"sha256:eaad65bd20955131bcdb3967a4dea66b4e4d4ca488efed7c00d91ee0173387e8",
|
||||
"sha256:f45fba420b94165c17896861bb0e8b27fb7abdcedfeb154895d8553df90b7b00"
|
||||
"sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
|
||||
"sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
|
||||
"sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
|
||||
"sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
|
||||
"sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
|
||||
"sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
|
||||
"sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
|
||||
"sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
|
||||
"sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
|
||||
"sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
|
||||
"sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
|
||||
"sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
|
||||
"sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
|
||||
"sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
|
||||
"sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
|
||||
"sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
|
||||
"sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
|
||||
"sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
|
||||
"sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
|
||||
"sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
|
||||
"sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
|
||||
"sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
|
||||
"sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
|
||||
"sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
|
||||
"sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
|
||||
"sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
|
||||
"sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
|
||||
"sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
|
||||
"sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
|
||||
"sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
|
||||
"sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
|
||||
],
|
||||
"version": "==5.0.2"
|
||||
"version": "==5.0.3"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
@ -752,10 +793,10 @@
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11",
|
||||
"sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432"
|
||||
"sha256:047d4d1791bfb3756264da670d99df13d799bb36e7d88774b1585a82d05dbaec",
|
||||
"sha256:1b1a58961683b30c574520d0c739c4443e0ef6a185c04382e8cc888273dbebed"
|
||||
],
|
||||
"version": "==3.0.0"
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
@ -796,11 +837,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"ipdb": {
|
||||
"hashes": [
|
||||
@ -920,10 +961,10 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
],
|
||||
"version": "==8.0.2"
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
@ -1143,12 +1184,12 @@
|
||||
},
|
||||
"rope": {
|
||||
"hashes": [
|
||||
"sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969",
|
||||
"sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf",
|
||||
"sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"
|
||||
"sha256:52423a7eebb5306a6d63bdc91a7c657db51ac9babfb8341c9a1440831ecf3203",
|
||||
"sha256:ae1fa2fd56f64f4cc9be46493ce54bed0dd12dee03980c61a4393d89d84029ad",
|
||||
"sha256:d2830142c2e046f5fc26a022fe680675b6f48f81c7fc1f03a950706e746e9dfe"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.14.0"
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"selenium": {
|
||||
"hashes": [
|
||||
@ -1209,29 +1250,30 @@
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
|
||||
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
|
||||
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
|
||||
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
|
||||
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
|
||||
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
|
||||
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
|
||||
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
|
||||
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
|
||||
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
|
||||
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
|
||||
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
|
||||
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
|
||||
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
|
||||
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
|
||||
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
|
||||
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
|
||||
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
|
||||
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
|
||||
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
|
||||
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
|
||||
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
|
||||
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
|
||||
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
|
||||
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
|
||||
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
|
||||
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
|
||||
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
|
||||
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
|
||||
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
|
||||
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
|
||||
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
|
||||
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
|
||||
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
|
||||
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
|
||||
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
|
||||
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
|
||||
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
|
||||
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
|
||||
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
|
||||
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
|
||||
],
|
||||
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
|
||||
"version": "==1.4.0"
|
||||
"version": "==1.4.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
@ -1277,10 +1319,10 @@
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"version": "==1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
70
PortfolioProvision.md
Normal file
70
PortfolioProvision.md
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
Each CSP will have a set of "stages" that are required to be completed before the provisioning process can be considered complete.
|
||||
|
||||
Azure Stages:
|
||||
tenant,
|
||||
billing profile,
|
||||
admin subscription
|
||||
etc.
|
||||
|
||||
`atst.models.mixins.state_machines` module contains:
|
||||
|
||||
python Enum classes that define the stages for a CSP
|
||||
|
||||
class AzureStages(Enum):
|
||||
TENANT = "tenant"
|
||||
BILLING_PROFILE = "billing profile"
|
||||
ADMIN_SUBSCRIPTION = "admin subscription"
|
||||
|
||||
there are two types of python dataclass subclasses defined in `atst.models.portoflio_state_machine` module.
|
||||
|
||||
one holds the data that is submitted to the CSP
|
||||
|
||||
@dataclass
|
||||
class TenantCSPPayload():
|
||||
user_id: str
|
||||
password: str
|
||||
etc.
|
||||
|
||||
the other holds the results of the call to the CSP
|
||||
@dataclass
|
||||
class TenantCSPResult():
|
||||
user_id: str
|
||||
tenant_id: str
|
||||
user_object_id: str
|
||||
etc.
|
||||
|
||||
A Finite State Machine `atst.models.portoflio_state_machine.PortfolioStateMachine` is created for each provisioning process and tied to an instance of Portfolio class.
|
||||
|
||||
Aach time the FSM is created/accessed it will generate a list of States and Transitions between the states.
|
||||
|
||||
There is a set of "system" states such as UNSTARTED, STARTING, STARTED, COMPLETED, FAILED etc
|
||||
|
||||
There is a set of CSP specific states generated for each "stage" in the FSM.
|
||||
TENANT_IN_PROGRESS
|
||||
TENANT_IN_COMPLETED
|
||||
TENANT_IN_FAILED
|
||||
BILLING_PROFILE_IN_PROGRESS
|
||||
BILLING_PROFILE_IN_COMPLETED
|
||||
BILLING_PROFILE_IN_FAILED
|
||||
etc.
|
||||
|
||||
There is a set of callbacks defined that are triggered as the process transitions between stages.
|
||||
|
||||
callback `PortfolioStateMachine.after_in_progress_callback`
|
||||
The CSP api call is made as the process transitions into IN_PROGESS state for each state.
|
||||
|
||||
callback `PortfolioStateMachine.is_csp_data_valid`
|
||||
validates the collected data.
|
||||
|
||||
A transition into the next state can be triggered using PortfolioStateMachine.trigger_next_transition`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
113
alembic/versions/59973fa17ded_portfolio_state_machine_table.py
Normal file
113
alembic/versions/59973fa17ded_portfolio_state_machine_table.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""portfolio state machine table.
|
||||
|
||||
Revision ID: 59973fa17ded
|
||||
Revises: 828d8c188dce
|
||||
Create Date: 2020-01-08 10:37:32.924245
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
import sqlalchemy_json
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "59973fa17ded" # pragma: allowlist secret
|
||||
down_revision = "828d8c188dce" # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"portfolio_job_failures",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("task_id", sa.String(), nullable=False),
|
||||
sa.Column("portfolio_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["portfolio_id"], ["portfolios.id"],),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"portfolio_state_machines",
|
||||
sa.Column(
|
||||
"time_created",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"time_updated",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"deleted", sa.Boolean(), server_default=sa.text("false"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"id",
|
||||
postgresql.UUID(as_uuid=True),
|
||||
server_default=sa.text("uuid_generate_v4()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("portfolio_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column(
|
||||
"state",
|
||||
sa.Enum(
|
||||
"UNSTARTED",
|
||||
"STARTING",
|
||||
"STARTED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"TENANT_CREATED",
|
||||
"TENANT_IN_PROGRESS",
|
||||
"TENANT_FAILED",
|
||||
"BILLING_PROFILE_CREATED",
|
||||
"BILLING_PROFILE_IN_PROGRESS",
|
||||
"BILLING_PROFILE_FAILED",
|
||||
"ADMIN_SUBSCRIPTION_CREATED",
|
||||
"ADMIN_SUBSCRIPTION_IN_PROGRESS",
|
||||
"ADMIN_SUBSCRIPTION_FAILED",
|
||||
name="fsmstates",
|
||||
native_enum=False,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["portfolio_id"], ["portfolios.id"],),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.add_column("portfolios", sa.Column("app_migration", sa.String(), nullable=True))
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("complexity", sa.ARRAY(sa.String()), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("complexity_other", sa.String(), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"portfolios",
|
||||
sa.Column("csp_data", sqlalchemy_json.NestedMutableJson(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("dev_team", sa.ARRAY(sa.String()), nullable=True)
|
||||
)
|
||||
op.add_column("portfolios", sa.Column("dev_team_other", sa.String(), nullable=True))
|
||||
op.add_column("portfolios", sa.Column("native_apps", sa.String(), nullable=True))
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("team_experience", sa.String(), nullable=True)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("portfolios", "team_experience")
|
||||
op.drop_column("portfolios", "native_apps")
|
||||
op.drop_column("portfolios", "dev_team_other")
|
||||
op.drop_column("portfolios", "dev_team")
|
||||
op.drop_column("portfolios", "csp_data")
|
||||
op.drop_column("portfolios", "complexity_other")
|
||||
op.drop_column("portfolios", "complexity")
|
||||
op.drop_column("portfolios", "app_migration")
|
||||
op.drop_table("portfolio_state_machines")
|
||||
op.drop_table("portfolio_job_failures")
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,58 @@
|
||||
"""update environment_roles enum list
|
||||
|
||||
Revision ID: 828d8c188dce
|
||||
Revises: 5d7198d34b91
|
||||
Create Date: 2020-01-08 16:08:03.879881
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '828d8c188dce' # pragma: allowlist secret
|
||||
down_revision = '5d7198d34b91' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE environment_roles
|
||||
SET role = NULL
|
||||
"""
|
||||
)
|
||||
|
||||
op.alter_column(
|
||||
"environment_roles",
|
||||
"role",
|
||||
type_=sa.Enum(
|
||||
"ADMIN",
|
||||
"BILLING_READ",
|
||||
"CONTRIBUTOR",
|
||||
name="role",
|
||||
native_enum=False,
|
||||
),
|
||||
existing_type=sa.VARCHAR(),
|
||||
nullable=True,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column(
|
||||
"environment_roles",
|
||||
"status",
|
||||
type_=sa.VARCHAR(),
|
||||
existing_type=sa.Enum(
|
||||
"ADMIN",
|
||||
"BILLING_READ",
|
||||
"CONTRIBUTOR",
|
||||
name="status",
|
||||
native_enum=False,
|
||||
),
|
||||
)
|
||||
# ### end Alembic commands ###
|
@ -1,3 +1,5 @@
|
||||
import importlib
|
||||
|
||||
from .cloud import MockCloudProvider
|
||||
from .file_uploads import AzureUploader, MockUploader
|
||||
from .reports import MockReportingProvider
|
||||
@ -29,3 +31,22 @@ def make_csp_provider(app, csp=None):
|
||||
app.csp = MockCSP(app, test_mode=True)
|
||||
else:
|
||||
app.csp = MockCSP(app)
|
||||
|
||||
|
||||
def _stage_to_classname(stage):
|
||||
return "".join(
|
||||
map(lambda word: word.capitalize(), stage.replace("_", " ").split(" "))
|
||||
)
|
||||
|
||||
|
||||
def get_stage_csp_class(stage, class_type):
|
||||
"""
|
||||
given a stage name and class_type return the class
|
||||
class_type is either 'payload' or 'result'
|
||||
|
||||
"""
|
||||
cls_name = "".join([_stage_to_classname(stage), "CSP", class_type.capitalize()])
|
||||
try:
|
||||
return getattr(importlib.import_module("atst.domain.csp.cloud"), cls_name)
|
||||
except AttributeError:
|
||||
print("could not import CSP Result class <%s>" % cls_name)
|
||||
|
@ -1,12 +1,12 @@
|
||||
from typing import Dict
|
||||
import re
|
||||
from typing import Dict
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from atst.models.user import User
|
||||
from atst.models.application import Application
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from .policy import AzurePolicyManager
|
||||
|
||||
|
||||
class GeneralCSPException(Exception):
|
||||
@ -142,6 +142,97 @@ class BaselineProvisionException(GeneralCSPException):
|
||||
)
|
||||
|
||||
|
||||
class BaseCSPPayload(BaseModel):
|
||||
# {"username": "mock-cloud", "pass": "shh"}
|
||||
creds: Dict
|
||||
|
||||
|
||||
class TenantCSPPayload(BaseCSPPayload):
|
||||
user_id: str
|
||||
password: str
|
||||
domain_name: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
country_code: str
|
||||
password_recovery_email_address: str
|
||||
|
||||
|
||||
class TenantCSPResult(BaseModel):
|
||||
user_id: str
|
||||
tenant_id: str
|
||||
user_object_id: str
|
||||
|
||||
|
||||
class BillingProfileAddress(BaseModel):
|
||||
address: Dict
|
||||
"""
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"""
|
||||
|
||||
|
||||
class BillingProfileCLINBudget(BaseModel):
|
||||
clinBudget: Dict
|
||||
"""
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class BillingProfileCSPPayload(
|
||||
BaseCSPPayload, BillingProfileAddress, BillingProfileCLINBudget
|
||||
):
|
||||
displayName: str
|
||||
poNumber: str
|
||||
invoiceEmailOptIn: str
|
||||
|
||||
"""
|
||||
{
|
||||
"displayName": "string",
|
||||
"poNumber": "string",
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"invoiceEmailOptIn": true,
|
||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class CloudProviderInterface:
|
||||
def root_creds(self) -> Dict:
|
||||
raise NotImplementedError()
|
||||
@ -325,6 +416,68 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
return {"id": self._id(), "credentials": self._auth_credentials}
|
||||
|
||||
def create_tenant(self, payload):
|
||||
"""
|
||||
payload is an instance of TenantCSPPayload data class
|
||||
"""
|
||||
|
||||
self._authorize(payload.creds)
|
||||
|
||||
self._delay(1, 5)
|
||||
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
# return tenant id, tenant owner id and tenant owner object id from:
|
||||
response = {"tenantId": "string", "userId": "string", "objectId": "string"}
|
||||
return {
|
||||
"tenant_id": response["tenantId"],
|
||||
"user_id": response["userId"],
|
||||
"user_object_id": response["objectId"],
|
||||
}
|
||||
|
||||
def create_billing_profile(self, creds, tenant_admin_details, billing_owner_id):
|
||||
# call billing profile creation endpoint, specifying owner
|
||||
# Payload:
|
||||
"""
|
||||
{
|
||||
"displayName": "string",
|
||||
"poNumber": "string",
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"invoiceEmailOptIn": true,
|
||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
# response will be mostly the same as the body, but we only really care about the id
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
response = {"id": "string"}
|
||||
return {"billing_profile_id": response["id"]}
|
||||
|
||||
def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
|
||||
self._authorize(auth_credentials)
|
||||
|
||||
@ -401,18 +554,15 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
|
||||
|
||||
class AzureSDKProvider(object):
|
||||
def __init__(self):
|
||||
from azure.mgmt import subscription, authorization, managementgroups
|
||||
from azure.mgmt.resource import policy
|
||||
from azure.mgmt import subscription, authorization
|
||||
import azure.graphrbac as graphrbac
|
||||
import azure.common.credentials as credentials
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
|
||||
self.subscription = subscription
|
||||
self.authorization = authorization
|
||||
self.managementgroups = managementgroups
|
||||
self.graphrbac = graphrbac
|
||||
self.credentials = credentials
|
||||
self.policy = policy
|
||||
# may change to a JEDI cloud
|
||||
self.cloud = AZURE_PUBLIC_CLOUD
|
||||
|
||||
@ -430,28 +580,45 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
else:
|
||||
self.sdk = azure_sdk_provider
|
||||
|
||||
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
|
||||
|
||||
def create_environment(
|
||||
self, auth_credentials: Dict, user: User, environment: Environment
|
||||
):
|
||||
# since this operation would only occur within a tenant, should we source the tenant
|
||||
# via lookup from environment once we've created the portfolio csp data schema
|
||||
# something like this:
|
||||
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
|
||||
# though we'd probably source the whole credentials for these calls from the portfolio csp
|
||||
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
|
||||
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
|
||||
credentials = self._get_credential_obj(self._root_creds)
|
||||
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
||||
management_group_id = "?" # management group id chained from environment
|
||||
parent_id = "?" # from environment.application
|
||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||
|
||||
management_group = self._create_management_group(
|
||||
credentials, management_group_id, display_name, parent_id,
|
||||
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
||||
|
||||
billing_profile_id = "?" # something chained from environment?
|
||||
sku_id = AZURE_SKU_ID
|
||||
# we want to set AT-AT as an owner here
|
||||
# we could potentially associate subscriptions with "management groups" per DOD component
|
||||
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
||||
display_name,
|
||||
billing_profile_id,
|
||||
sku_id,
|
||||
# owner=<AdPrincipal: for AT-AT user>
|
||||
)
|
||||
|
||||
return management_group
|
||||
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
||||
# TOs filed for the environment
|
||||
billing_account_name = "?"
|
||||
invoice_section_name = "?"
|
||||
# We may also want to create billing sections in the enrollment account
|
||||
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
||||
billing_account_name, invoice_section_name, body
|
||||
)
|
||||
|
||||
# the resulting object from this process is a link to the new subscription
|
||||
# not a subscription model, so we'll have to unpack the ID
|
||||
new_sub = sub_creation_operation.result()
|
||||
|
||||
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
||||
if subscription_id:
|
||||
return subscription_id
|
||||
else:
|
||||
# troublesome error, subscription should exist at this point
|
||||
# but we just don't have a valid ID
|
||||
pass
|
||||
|
||||
def create_atat_admin_user(
|
||||
self, auth_credentials: Dict, csp_environment_id: str
|
||||
@ -490,126 +657,135 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"role_name": role_assignment_id,
|
||||
}
|
||||
|
||||
def _create_application(self, auth_credentials: Dict, application: Application):
|
||||
management_group_name = str(uuid4()) # can be anything, not just uuid
|
||||
display_name = application.name # Does this need to be unique?
|
||||
credentials = self._get_credential_obj(auth_credentials)
|
||||
parent_id = "?" # application.portfolio.csp_details.management_group_id
|
||||
def create_tenant(self, payload):
|
||||
# auth as SP that is allowed to create tenant? (tenant creation sp creds)
|
||||
# create tenant with owner details (populated from portfolio point of contact, pw is generated)
|
||||
|
||||
return self._create_management_group(
|
||||
credentials, management_group_name, display_name, parent_id,
|
||||
# return tenant id, tenant owner id and tenant owner object id from:
|
||||
response = {"tenantId": "string", "userId": "string", "objectId": "string"}
|
||||
return self._ok(
|
||||
{
|
||||
"tenant_id": response["tenantId"],
|
||||
"user_id": response["userId"],
|
||||
"user_object_id": response["objectId"],
|
||||
}
|
||||
)
|
||||
|
||||
def _create_management_group(
|
||||
self, credentials, management_group_id, display_name, parent_id=None,
|
||||
):
|
||||
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
|
||||
create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo(
|
||||
id=parent_id
|
||||
)
|
||||
create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails(
|
||||
parent=create_parent_grp_info
|
||||
)
|
||||
mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest(
|
||||
name=management_group_id,
|
||||
display_name=display_name,
|
||||
details=create_mgmt_grp_details,
|
||||
)
|
||||
create_request = mgmgt_group_client.management_groups.create_or_update(
|
||||
management_group_id, mgmt_grp_create
|
||||
)
|
||||
def create_billing_owner(self, creds, tenant_admin_details):
|
||||
# authenticate as tenant_admin
|
||||
# create billing owner identity
|
||||
|
||||
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
|
||||
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally
|
||||
return create_request.result()
|
||||
# TODO: Lookup response format
|
||||
# Managed service identity?
|
||||
response = {"id": "string"}
|
||||
return self._ok({"billing_owner_id": response["id"]})
|
||||
|
||||
def _create_subscription(
|
||||
self,
|
||||
credentials,
|
||||
display_name,
|
||||
billing_profile_id,
|
||||
sku_id,
|
||||
management_group_id,
|
||||
billing_account_name,
|
||||
invoice_section_name,
|
||||
):
|
||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||
|
||||
billing_profile_id = "?" # where do we source this?
|
||||
sku_id = AZURE_SKU_ID
|
||||
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
||||
# TOs filed for the environment
|
||||
billing_account_name = "?" # from TO?
|
||||
invoice_section_name = "?" # from TO?
|
||||
|
||||
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
||||
display_name=display_name,
|
||||
billing_profile_id=billing_profile_id,
|
||||
sku_id=sku_id,
|
||||
management_group_id=management_group_id,
|
||||
)
|
||||
|
||||
# We may also want to create billing sections in the enrollment account
|
||||
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
||||
billing_account_name, invoice_section_name, body
|
||||
)
|
||||
|
||||
# the resulting object from this process is a link to the new subscription
|
||||
# not a subscription model, so we'll have to unpack the ID
|
||||
new_sub = sub_creation_operation.result()
|
||||
|
||||
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
||||
if subscription_id:
|
||||
return subscription_id
|
||||
else:
|
||||
# troublesome error, subscription should exist at this point
|
||||
# but we just don't have a valid ID
|
||||
pass
|
||||
|
||||
AZURE_MANAGEMENT_API = "https://management.azure.com"
|
||||
|
||||
def _create_policy_definition(
|
||||
self, credentials, subscription_id, management_group_id, properties,
|
||||
):
|
||||
def assign_billing_owner(self, creds, billing_owner_id, tenant_id):
|
||||
# TODO: Do we source role definition ID from config, api or self-defined?
|
||||
# TODO: If from api,
|
||||
"""
|
||||
Requires credentials that have AZURE_MANAGEMENT_API
|
||||
specified as the resource. The Service Principal
|
||||
specified in the credentials must have the "Resource
|
||||
Policy Contributor" role assigned with a scope at least
|
||||
as high as the management group specified by
|
||||
management_group_id.
|
||||
|
||||
Arguments:
|
||||
credentials -- ServicePrincipalCredentials
|
||||
subscription_id -- str, ID of the subscription (just the UUID, not the path)
|
||||
management_group_id -- str, ID of the management group (just the UUID, not the path)
|
||||
properties -- dictionary, the "properties" section of a valid Azure policy definition document
|
||||
|
||||
Returns:
|
||||
azure.mgmt.resource.policy.[api version].models.PolicyDefinition: the PolicyDefinition object provided to Azure
|
||||
|
||||
Raises:
|
||||
TBD
|
||||
{
|
||||
"principalId": "string",
|
||||
"principalTenantId": "string",
|
||||
"billingRoleDefinitionId": "string"
|
||||
}
|
||||
"""
|
||||
# TODO: which subscription would this be?
|
||||
client = self.sdk.policy.PolicyClient(credentials, subscription_id)
|
||||
|
||||
definition = client.policy_definitions.models.PolicyDefinition(
|
||||
policy_type=properties.get("policyType"),
|
||||
mode=properties.get("mode"),
|
||||
display_name=properties.get("displayName"),
|
||||
description=properties.get("description"),
|
||||
policy_rule=properties.get("policyRule"),
|
||||
parameters=properties.get("parameters"),
|
||||
return self.ok()
|
||||
|
||||
def create_billing_profile(self, creds, tenant_admin_details, billing_owner_id):
|
||||
# call billing profile creation endpoint, specifying owner
|
||||
# Payload:
|
||||
"""
|
||||
{
|
||||
"displayName": "string",
|
||||
"poNumber": "string",
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"invoiceEmailOptIn": true,
|
||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# response will be mostly the same as the body, but we only really care about the id
|
||||
response = {"id": "string"}
|
||||
return self._ok({"billing_profile_id": response["id"]})
|
||||
|
||||
def report_clin(self, creds, clin_id, clin_amount, clin_start, clin_end, clin_to):
|
||||
# should consumer be responsible for reporting each clin or
|
||||
# should this take a list and manage the sequential reporting?
|
||||
""" Payload
|
||||
{
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# we don't need any of the returned info for this
|
||||
return self._ok()
|
||||
|
||||
def create_remote_admin(self, creds, tenant_details):
|
||||
# create app/service principal within tenant, with name constructed from tenant details
|
||||
# assign principal global admin
|
||||
|
||||
# needs to call out to CLI with tenant owner username/password, prototyping for that underway
|
||||
|
||||
# return identifier and creds to consumer for storage
|
||||
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"}
|
||||
return self._ok(
|
||||
{
|
||||
"client_id": response["clientId"],
|
||||
"secret_key": response["secret_key"],
|
||||
"tenant_id": response["tenantId"],
|
||||
}
|
||||
)
|
||||
|
||||
name = properties.get("displayName")
|
||||
def force_tenant_admin_pw_update(self, creds, tenant_owner_id):
|
||||
# use creds to update to force password recovery?
|
||||
# not sure what the endpoint/method for this is, yet
|
||||
|
||||
return client.policy_definitions.create_or_update_at_management_group(
|
||||
policy_definition_name=name,
|
||||
parameters=definition,
|
||||
management_group_id=management_group_id,
|
||||
)
|
||||
return self._ok()
|
||||
|
||||
def create_billing_alerts(self, TBD):
|
||||
# TODO: Add azure-mgmt-consumption for Budget and Notification entities/operations
|
||||
# TODO: Determine how to auth against that API using the SDK, doesn't seeem possible at the moment
|
||||
# TODO: billing alerts are registered as Notifications on Budget objects, which have start/end dates
|
||||
# TODO: determine what the keys in the Notifications dict are supposed to be
|
||||
# we may need to rotate budget objects when new TOs/CLINs are reported?
|
||||
|
||||
# we likely only want the budget ID, can be updated or replaced?
|
||||
response = {"id": "id"}
|
||||
return self._ok({"budget_id": response["id"]})
|
||||
|
||||
def _get_management_service_principal(self):
|
||||
# we really should be using graph.microsoft.com, but i'm getting
|
||||
@ -663,6 +839,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
return sub_id_match.group(1)
|
||||
|
||||
def _get_credential_obj(self, creds, resource=None):
|
||||
|
||||
return self.sdk.credentials.ServicePrincipalCredentials(
|
||||
client_id=creds.get("client_id"),
|
||||
secret=creds.get("secret_key"),
|
||||
@ -671,6 +848,27 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
cloud_environment=self.sdk.cloud,
|
||||
)
|
||||
|
||||
def _make_tenant_admin_cred_obj(self, username, password):
|
||||
return self.sdk.credentials.UserPassCredentials(username, password)
|
||||
|
||||
def _ok(self, body=None):
|
||||
return self._make_response("ok", body)
|
||||
|
||||
def _error(self, body=None):
|
||||
return self._make_response("error", body)
|
||||
|
||||
def _make_response(self, status, body=dict()):
|
||||
"""Create body for responses from API
|
||||
|
||||
Arguments:
|
||||
status {string} -- "ok" or "error"
|
||||
body {dict} -- dict containing details of response or error, if applicable
|
||||
|
||||
Returns:
|
||||
dict -- status of call with body containing details
|
||||
"""
|
||||
return {"status": status, "body": body}
|
||||
|
||||
@property
|
||||
def _root_creds(self):
|
||||
return {
|
||||
|
@ -2,4 +2,5 @@ from .portfolios import (
|
||||
Portfolios,
|
||||
PortfolioError,
|
||||
PortfolioDeletionApplicationsExistError,
|
||||
PortfolioStateMachines,
|
||||
)
|
||||
|
@ -1,11 +1,23 @@
|
||||
from sqlalchemy import or_
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from atst.database import db
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.domain.portfolio_roles import PortfolioRoles
|
||||
from atst.domain.invitations import PortfolioInvitations
|
||||
from atst.models import Permissions, PortfolioRole, PortfolioRoleStatus
|
||||
|
||||
from .query import PortfoliosQuery
|
||||
from atst.domain.invitations import PortfolioInvitations
|
||||
from atst.models import (
|
||||
Portfolio,
|
||||
PortfolioStateMachine,
|
||||
FSMStates,
|
||||
Permissions,
|
||||
PortfolioRole,
|
||||
PortfolioRoleStatus,
|
||||
)
|
||||
|
||||
from .query import PortfoliosQuery, PortfolioStateMachinesQuery
|
||||
from .scopes import ScopedPortfolio
|
||||
|
||||
|
||||
@ -17,7 +29,22 @@ class PortfolioDeletionApplicationsExistError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PortfolioStateMachines(object):
|
||||
@classmethod
|
||||
def create(cls, portfolio, **sm_attrs):
|
||||
sm_attrs.update({"portfolio": portfolio})
|
||||
sm = PortfolioStateMachinesQuery.create(**sm_attrs)
|
||||
return sm
|
||||
|
||||
|
||||
class Portfolios(object):
|
||||
@classmethod
|
||||
def get_or_create_state_machine(cls, portfolio):
|
||||
"""
|
||||
get or create Portfolio State Machine for a Portfolio
|
||||
"""
|
||||
return portfolio.state_machine or PortfolioStateMachines.create(portfolio)
|
||||
|
||||
@classmethod
|
||||
def create(cls, user, portfolio_attrs):
|
||||
portfolio = PortfoliosQuery.create(**portfolio_attrs)
|
||||
@ -111,3 +138,37 @@ class Portfolios(object):
|
||||
portfolio.description = new_data["description"]
|
||||
|
||||
PortfoliosQuery.add_and_commit(portfolio)
|
||||
|
||||
@classmethod
|
||||
def base_provision_query(cls):
|
||||
return db.session.query(Portfolio.id)
|
||||
|
||||
@classmethod
|
||||
def get_portfolios_pending_provisioning(cls) -> List[UUID]:
|
||||
"""
|
||||
Any portfolio with a corresponding State Machine that is either:
|
||||
not started yet,
|
||||
failed in creating a tenant
|
||||
failed
|
||||
"""
|
||||
|
||||
results = (
|
||||
cls.base_provision_query()
|
||||
.join(PortfolioStateMachine)
|
||||
.filter(
|
||||
or_(
|
||||
PortfolioStateMachine.state == FSMStates.UNSTARTED,
|
||||
PortfolioStateMachine.state == FSMStates.FAILED,
|
||||
PortfolioStateMachine.state == FSMStates.TENANT_FAILED,
|
||||
)
|
||||
)
|
||||
)
|
||||
return [id_ for id_, in results]
|
||||
|
||||
# db.session.query(PortfolioStateMachine).\
|
||||
# filter(
|
||||
# or_(
|
||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
||||
# )
|
||||
# ).all()
|
||||
|
@ -8,6 +8,13 @@ from atst.models.application_role import (
|
||||
Status as ApplicationRoleStatus,
|
||||
)
|
||||
from atst.models.application import Application
|
||||
from atst.models.portfolio_state_machine import PortfolioStateMachine
|
||||
|
||||
# from atst.models.application import Application
|
||||
|
||||
|
||||
class PortfolioStateMachinesQuery(Query):
|
||||
model = PortfolioStateMachine
|
||||
|
||||
|
||||
class PortfoliosQuery(Query):
|
||||
|
@ -14,7 +14,7 @@ SERVICE_BRANCHES = [
|
||||
]
|
||||
|
||||
ENV_ROLE_NO_ACCESS = "No Access"
|
||||
ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [
|
||||
ENV_ROLES = [(role.name, role.value) for role in CSPRole] + [
|
||||
(ENV_ROLE_NO_ACCESS, ENV_ROLE_NO_ACCESS)
|
||||
]
|
||||
|
||||
|
@ -7,7 +7,7 @@ from wtforms.fields import (
|
||||
HiddenField,
|
||||
)
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.validators import Required, Length, NumberRange, ValidationError
|
||||
from wtforms.validators import Required, Length, NumberRange, ValidationError, Regexp
|
||||
from flask_wtf import FlaskForm
|
||||
from numbers import Number
|
||||
|
||||
@ -15,6 +15,7 @@ from .data import JEDI_CLIN_TYPES
|
||||
from .fields import SelectField
|
||||
from .forms import BaseForm, remove_empty_string
|
||||
from atst.utils.localization import translate
|
||||
from .validators import REGEX_ALPHA_NUMERIC
|
||||
from flask import current_app as app
|
||||
|
||||
MAX_CLIN_AMOUNT = 1000000000
|
||||
@ -116,7 +117,10 @@ class AttachmentForm(BaseForm):
|
||||
filename = HiddenField(
|
||||
id="attachment_filename",
|
||||
validators=[
|
||||
Length(max=100, message=translate("forms.attachment.filename.length_error"))
|
||||
Length(
|
||||
max=100, message=translate("forms.attachment.filename.length_error")
|
||||
),
|
||||
Regexp(regex=REGEX_ALPHA_NUMERIC),
|
||||
],
|
||||
)
|
||||
object_name = HiddenField(
|
||||
@ -124,7 +128,8 @@ class AttachmentForm(BaseForm):
|
||||
validators=[
|
||||
Length(
|
||||
max=40, message=translate("forms.attachment.object_name.length_error")
|
||||
)
|
||||
),
|
||||
Regexp(regex=REGEX_ALPHA_NUMERIC),
|
||||
],
|
||||
)
|
||||
accept = ".pdf,application/pdf"
|
||||
|
@ -8,6 +8,9 @@ import pendulum
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
REGEX_ALPHA_NUMERIC = "^[A-Za-z0-9\-_ \.]*$"
|
||||
|
||||
|
||||
def DateRange(lower_bound=None, upper_bound=None, message=None):
|
||||
def _date_range(form, field):
|
||||
if field.data is None:
|
||||
|
33
atst/jobs.py
33
atst/jobs.py
@ -7,14 +7,27 @@ from atst.models import (
|
||||
EnvironmentJobFailure,
|
||||
EnvironmentRoleJobFailure,
|
||||
EnvironmentRole,
|
||||
PortfolioJobFailure,
|
||||
)
|
||||
from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.portfolios import Portfolios
|
||||
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.models.utils import claim_for_update
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class RecordPortfolioFailure(celery.Task):
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
if "portfolio_id" in kwargs:
|
||||
failure = PortfolioJobFailure(
|
||||
portfolio_id=kwargs["portfolio_id"], task_id=task_id
|
||||
)
|
||||
db.session.add(failure)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class RecordEnvironmentFailure(celery.Task):
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
if "environment_id" in kwargs:
|
||||
@ -125,6 +138,17 @@ def do_work(fn, task, csp, **kwargs):
|
||||
raise task.retry(exc=e)
|
||||
|
||||
|
||||
def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
fsm = Portfolios.get_or_create_state_machine(portfolio)
|
||||
fsm.trigger_next_transition()
|
||||
|
||||
|
||||
@celery.task(bind=True, base=RecordPortfolioFailure)
|
||||
def provision_portfolio(self, portfolio_id=None):
|
||||
do_work(do_provision_portfolio, self, app.csp.cloud, portfolio_id=portfolio_id)
|
||||
|
||||
|
||||
@celery.task(bind=True, base=RecordEnvironmentFailure)
|
||||
def create_environment(self, environment_id=None):
|
||||
do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id)
|
||||
@ -144,6 +168,15 @@ def provision_user(self, environment_role_id=None):
|
||||
)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_provision_portfolio(self):
|
||||
"""
|
||||
Iterate over portfolios with a corresponding State Machine that have not completed.
|
||||
"""
|
||||
for portfolio_id in Portfolios.get_portfolios_pending_provisioning():
|
||||
provision_portfolio.delay(portfolio_id=portfolio_id)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_create_environment(self):
|
||||
for environment_id in Environments.get_environments_pending_creation(
|
||||
|
@ -7,11 +7,16 @@ from .audit_event import AuditEvent
|
||||
from .clin import CLIN, JEDICLINType
|
||||
from .environment import Environment
|
||||
from .environment_role import EnvironmentRole, CSPRole
|
||||
from .job_failure import EnvironmentJobFailure, EnvironmentRoleJobFailure
|
||||
from .job_failure import (
|
||||
EnvironmentJobFailure,
|
||||
EnvironmentRoleJobFailure,
|
||||
PortfolioJobFailure,
|
||||
)
|
||||
from .notification_recipient import NotificationRecipient
|
||||
from .permissions import Permissions
|
||||
from .permission_set import PermissionSet
|
||||
from .portfolio import Portfolio
|
||||
from .portfolio_state_machine import PortfolioStateMachine, FSMStates
|
||||
from .portfolio_invitation import PortfolioInvitation
|
||||
from .portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
|
||||
from .task_order import TaskOrder
|
||||
|
@ -9,10 +9,9 @@ import atst.models.types as types
|
||||
|
||||
|
||||
class CSPRole(Enum):
|
||||
BASIC_ACCESS = "Basic Access"
|
||||
NETWORK_ADMIN = "Network Admin"
|
||||
BUSINESS_READ = "Business Read-only"
|
||||
TECHNICAL_READ = "Technical Read-only"
|
||||
ADMIN = "Admin"
|
||||
BILLING_READ = "Billing Read-only"
|
||||
CONTRIBUTOR = "Contributor"
|
||||
|
||||
|
||||
class EnvironmentRole(
|
||||
@ -26,7 +25,7 @@ class EnvironmentRole(
|
||||
)
|
||||
environment = relationship("Environment")
|
||||
|
||||
role = Column(String())
|
||||
role = Column(SQLAEnum(CSPRole, native_enum=False), nullable=True)
|
||||
|
||||
application_role_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("application_roles.id"), nullable=False
|
||||
|
@ -14,3 +14,9 @@ class EnvironmentRoleJobFailure(Base, mixins.JobFailureMixin):
|
||||
__tablename__ = "environment_role_job_failures"
|
||||
|
||||
environment_role_id = Column(ForeignKey("environment_roles.id"), nullable=False)
|
||||
|
||||
|
||||
class PortfolioJobFailure(Base, mixins.JobFailureMixin):
|
||||
__tablename__ = "portfolio_job_failures"
|
||||
|
||||
portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)
|
||||
|
@ -4,3 +4,4 @@ from .permissions import PermissionsMixin
|
||||
from .deletable import DeletableMixin
|
||||
from .invites import InvitesMixin
|
||||
from .job_failure import JobFailureMixin
|
||||
from .state_machines import FSMMixin
|
||||
|
137
atst/models/mixins/state_machines.py
Normal file
137
atst/models/mixins/state_machines.py
Normal file
@ -0,0 +1,137 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class StageStates(Enum):
|
||||
CREATED = "created"
|
||||
IN_PROGRESS = "in progress"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class AzureStages(Enum):
|
||||
TENANT = "tenant"
|
||||
BILLING_PROFILE = "billing profile"
|
||||
ADMIN_SUBSCRIPTION = "admin subscription"
|
||||
|
||||
|
||||
def _build_csp_states(csp_stages):
|
||||
states = {
|
||||
"UNSTARTED": "unstarted",
|
||||
"STARTING": "starting",
|
||||
"STARTED": "started",
|
||||
"COMPLETED": "completed",
|
||||
"FAILED": "failed",
|
||||
}
|
||||
for csp_stage in csp_stages:
|
||||
for state in StageStates:
|
||||
states[csp_stage.name + "_" + state.name] = (
|
||||
csp_stage.value + " " + state.value
|
||||
)
|
||||
return states
|
||||
|
||||
|
||||
FSMStates = Enum("FSMStates", _build_csp_states(AzureStages))
|
||||
|
||||
|
||||
def _build_transitions(csp_stages):
|
||||
transitions = []
|
||||
states = []
|
||||
compose_state = lambda csp_stage, state: getattr(
|
||||
FSMStates, "_".join([csp_stage.name, state.name])
|
||||
)
|
||||
|
||||
for stage_i, csp_stage in enumerate(csp_stages):
|
||||
for state in StageStates:
|
||||
states.append(
|
||||
dict(
|
||||
name=compose_state(csp_stage, state),
|
||||
tags=[csp_stage.name, state.name],
|
||||
)
|
||||
)
|
||||
if state == StageStates.CREATED:
|
||||
if stage_i > 0:
|
||||
src = compose_state(
|
||||
list(csp_stages)[stage_i - 1], StageStates.CREATED
|
||||
)
|
||||
else:
|
||||
src = FSMStates.STARTED
|
||||
transitions.append(
|
||||
dict(
|
||||
trigger="create_" + csp_stage.name.lower(),
|
||||
source=src,
|
||||
dest=compose_state(csp_stage, StageStates.IN_PROGRESS),
|
||||
after="after_in_progress_callback",
|
||||
)
|
||||
)
|
||||
if state == StageStates.IN_PROGRESS:
|
||||
transitions.append(
|
||||
dict(
|
||||
trigger="finish_" + csp_stage.name.lower(),
|
||||
source=compose_state(csp_stage, state),
|
||||
dest=compose_state(csp_stage, StageStates.CREATED),
|
||||
conditions=["is_csp_data_valid"],
|
||||
)
|
||||
)
|
||||
if state == StageStates.FAILED:
|
||||
transitions.append(
|
||||
dict(
|
||||
trigger="fail_" + csp_stage.name.lower(),
|
||||
source=compose_state(csp_stage, StageStates.IN_PROGRESS),
|
||||
dest=compose_state(csp_stage, StageStates.FAILED),
|
||||
)
|
||||
)
|
||||
return states, transitions
|
||||
|
||||
|
||||
class FSMMixin:
|
||||
|
||||
system_states = [
|
||||
{"name": FSMStates.UNSTARTED.name, "tags": ["system"]},
|
||||
{"name": FSMStates.STARTING.name, "tags": ["system"]},
|
||||
{"name": FSMStates.STARTED.name, "tags": ["system"]},
|
||||
{"name": FSMStates.FAILED.name, "tags": ["system"]},
|
||||
{"name": FSMStates.COMPLETED.name, "tags": ["system"]},
|
||||
]
|
||||
|
||||
system_transitions = [
|
||||
{"trigger": "init", "source": FSMStates.UNSTARTED, "dest": FSMStates.STARTING},
|
||||
{"trigger": "start", "source": FSMStates.STARTING, "dest": FSMStates.STARTED},
|
||||
{"trigger": "reset", "source": "*", "dest": FSMStates.UNSTARTED},
|
||||
{"trigger": "fail", "source": "*", "dest": FSMStates.FAILED,},
|
||||
]
|
||||
|
||||
def prepare_init(self, event):
|
||||
pass
|
||||
|
||||
def before_init(self, event):
|
||||
pass
|
||||
|
||||
def after_init(self, event):
|
||||
pass
|
||||
|
||||
def prepare_start(self, event):
|
||||
pass
|
||||
|
||||
def before_start(self, event):
|
||||
pass
|
||||
|
||||
def after_start(self, event):
|
||||
pass
|
||||
|
||||
def prepare_reset(self, event):
|
||||
pass
|
||||
|
||||
def before_reset(self, event):
|
||||
pass
|
||||
|
||||
def after_reset(self, event):
|
||||
pass
|
||||
|
||||
def fail_stage(self, stage):
|
||||
fail_trigger = "fail" + stage
|
||||
if fail_trigger in self.machine.get_triggers(self.current_state.name):
|
||||
self.trigger(fail_trigger)
|
||||
|
||||
def finish_stage(self, stage):
|
||||
finish_trigger = "finish_" + stage
|
||||
if finish_trigger in self.machine.get_triggers(self.current_state.name):
|
||||
self.trigger(finish_trigger)
|
@ -11,6 +11,8 @@ from atst.domain.permission_sets import PermissionSets
|
||||
from atst.utils import first_or_none
|
||||
from atst.database import db
|
||||
|
||||
from sqlalchemy_json import NestedMutableJson
|
||||
|
||||
|
||||
class Portfolio(
|
||||
Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin
|
||||
@ -19,16 +21,31 @@ class Portfolio(
|
||||
|
||||
id = types.Id()
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
defense_component = Column(
|
||||
ARRAY(String), nullable=False
|
||||
String, nullable=False
|
||||
) # Department of Defense Component
|
||||
|
||||
app_migration = Column(String) # App Migration
|
||||
complexity = Column(ARRAY(String)) # Application Complexity
|
||||
complexity_other = Column(String)
|
||||
description = Column(String)
|
||||
dev_team = Column(ARRAY(String)) # Development Team
|
||||
dev_team_other = Column(String)
|
||||
native_apps = Column(String) # Native Apps
|
||||
team_experience = Column(String) # Team Experience
|
||||
|
||||
csp_data = Column(NestedMutableJson, nullable=True)
|
||||
|
||||
applications = relationship(
|
||||
"Application",
|
||||
back_populates="portfolio",
|
||||
primaryjoin="and_(Application.portfolio_id == Portfolio.id, Application.deleted == False)",
|
||||
)
|
||||
|
||||
state_machine = relationship(
|
||||
"PortfolioStateMachine", uselist=False, back_populates="portfolio"
|
||||
)
|
||||
|
||||
roles = relationship("PortfolioRole")
|
||||
|
||||
task_orders = relationship("TaskOrder")
|
||||
|
181
atst/models/portfolio_state_machine.py
Normal file
181
atst/models/portfolio_state_machine.py
Normal file
@ -0,0 +1,181 @@
|
||||
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum
|
||||
from sqlalchemy.orm import relationship, reconstructor
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from transitions import Machine
|
||||
from transitions.extensions.states import add_state_features, Tags
|
||||
|
||||
from flask import current_app as app
|
||||
|
||||
from atst.domain.csp.cloud import ConnectionException, UnknownServerException
|
||||
from atst.domain.csp import MockCSP, AzureCSP, get_stage_csp_class
|
||||
from atst.database import db
|
||||
from atst.models.types import Id
|
||||
from atst.models.base import Base
|
||||
import atst.models.mixins as mixins
|
||||
from atst.models.mixins.state_machines import FSMStates, AzureStages, _build_transitions
|
||||
|
||||
|
||||
@add_state_features(Tags)
|
||||
class StateMachineWithTags(Machine):
|
||||
pass
|
||||
|
||||
|
||||
class PortfolioStateMachine(
|
||||
Base,
|
||||
mixins.TimestampsMixin,
|
||||
mixins.AuditableMixin,
|
||||
mixins.DeletableMixin,
|
||||
mixins.FSMMixin,
|
||||
):
|
||||
__tablename__ = "portfolio_state_machines"
|
||||
|
||||
id = Id()
|
||||
|
||||
portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"),)
|
||||
portfolio = relationship("Portfolio", back_populates="state_machine")
|
||||
|
||||
state = Column(
|
||||
SQLAEnum(FSMStates, native_enum=False, create_constraint=False),
|
||||
default=FSMStates.UNSTARTED,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def __init__(self, portfolio, csp=None, **kwargs):
|
||||
self.portfolio = portfolio
|
||||
self.attach_machine()
|
||||
|
||||
def after_state_change(self, event):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
@reconstructor
|
||||
def attach_machine(self):
|
||||
"""
|
||||
This is called as a result of a sqlalchemy query.
|
||||
Attach a machine depending on the current state.
|
||||
"""
|
||||
self.machine = StateMachineWithTags(
|
||||
model=self,
|
||||
send_event=True,
|
||||
initial=self.current_state if self.state else FSMStates.UNSTARTED,
|
||||
auto_transitions=False,
|
||||
after_state_change="after_state_change",
|
||||
)
|
||||
states, transitions = _build_transitions(AzureStages)
|
||||
self.machine.add_states(self.system_states + states)
|
||||
self.machine.add_transitions(self.system_transitions + transitions)
|
||||
|
||||
@property
|
||||
def current_state(self):
|
||||
if isinstance(self.state, str):
|
||||
return getattr(FSMStates, self.state)
|
||||
return self.state
|
||||
|
||||
def trigger_next_transition(self):
|
||||
state_obj = self.machine.get_state(self.state)
|
||||
|
||||
if state_obj.is_system:
|
||||
if self.current_state in (FSMStates.UNSTARTED, FSMStates.STARTING):
|
||||
# call the first trigger availabe for these two system states
|
||||
trigger_name = self.machine.get_triggers(self.current_state.name)[0]
|
||||
self.trigger(trigger_name)
|
||||
|
||||
elif self.current_state == FSMStates.STARTED:
|
||||
# get the first trigger that starts with 'create_'
|
||||
create_trigger = list(
|
||||
filter(
|
||||
lambda trigger: trigger.startswith("create_"),
|
||||
self.machine.get_triggers(FSMStates.STARTED.name),
|
||||
)
|
||||
)[0]
|
||||
self.trigger(create_trigger)
|
||||
|
||||
elif state_obj.is_IN_PROGRESS:
|
||||
pass
|
||||
|
||||
# elif state_obj.is_TENANT:
|
||||
# pass
|
||||
# elif state_obj.is_BILLING_PROFILE:
|
||||
# pass
|
||||
|
||||
# @with_payload
|
||||
def after_in_progress_callback(self, event):
|
||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
||||
if stage == "tenant":
|
||||
payload = dict( # nosec
|
||||
creds={"username": "mock-cloud", "pass": "shh"},
|
||||
user_id="123",
|
||||
password="123",
|
||||
domain_name="123",
|
||||
first_name="john",
|
||||
last_name="doe",
|
||||
country_code="US",
|
||||
password_recovery_email_address="password@email.com",
|
||||
)
|
||||
elif stage == "billing_profile":
|
||||
payload = dict(creds={"username": "mock-cloud", "pass": "shh"},)
|
||||
|
||||
payload_data_cls = get_stage_csp_class(stage, "payload")
|
||||
if not payload_data_cls:
|
||||
self.fail_stage(stage)
|
||||
try:
|
||||
payload_data = payload_data_cls(**payload)
|
||||
except PydanticValidationError as exc:
|
||||
print(exc.json())
|
||||
self.fail_stage(stage)
|
||||
|
||||
csp = event.kwargs.get("csp")
|
||||
if csp is not None:
|
||||
self.csp = AzureCSP(app).cloud
|
||||
else:
|
||||
self.csp = MockCSP(app).cloud
|
||||
|
||||
for attempt in range(5):
|
||||
try:
|
||||
response = getattr(self.csp, "create_" + stage)(payload_data)
|
||||
except (ConnectionException, UnknownServerException) as exc:
|
||||
print("caught exception. retry", attempt)
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
# failed all attempts
|
||||
self.fail_stage(stage)
|
||||
|
||||
if self.portfolio.csp_data is None:
|
||||
self.portfolio.csp_data = {}
|
||||
self.portfolio.csp_data[stage + "_data"] = response
|
||||
db.session.add(self.portfolio)
|
||||
db.session.commit()
|
||||
|
||||
self.finish_stage(stage)
|
||||
|
||||
def is_csp_data_valid(self, event):
|
||||
# check portfolio csp details json field for fields
|
||||
|
||||
if self.portfolio.csp_data is None or not isinstance(
|
||||
self.portfolio.csp_data, dict
|
||||
):
|
||||
return False
|
||||
|
||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
||||
stage_data = self.portfolio.csp_data.get(stage + "_data")
|
||||
cls = get_stage_csp_class(stage, "result")
|
||||
if not cls:
|
||||
return False
|
||||
|
||||
try:
|
||||
cls(**stage_data)
|
||||
except PydanticValidationError as exc:
|
||||
print(exc.json())
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# print('failed condition', self.portfolio.csp_data)
|
||||
|
||||
@property
|
||||
def application_id(self):
|
||||
return None
|
@ -7,6 +7,10 @@ celery = Celery(__name__)
|
||||
def update_celery(celery, app):
|
||||
celery.conf.update(app.config)
|
||||
celery.conf.CELERYBEAT_SCHEDULE = {
|
||||
"beat-dispatch_provision_portfolio": {
|
||||
"task": "atst.jobs.dispatch_provision_portfolio",
|
||||
"schedule": 60,
|
||||
},
|
||||
"beat-dispatch_create_environment": {
|
||||
"task": "atst.jobs.dispatch_create_environment",
|
||||
"schedule": 60,
|
||||
|
@ -78,7 +78,7 @@ def filter_env_roles_data(roles):
|
||||
{
|
||||
"environment_id": str(role.environment.id),
|
||||
"environment_name": role.environment.name,
|
||||
"role": role.role,
|
||||
"role": (role.role.value if role.role else "None"),
|
||||
}
|
||||
for role in roles
|
||||
],
|
||||
@ -99,8 +99,9 @@ def filter_env_roles_form_data(member, environments):
|
||||
|
||||
if len(env_roles_set) == 1:
|
||||
(env_role,) = env_roles_set
|
||||
env_data["role"] = env_role.role
|
||||
env_data["disabled"] = env_role.disabled
|
||||
if env_role.role:
|
||||
env_data["role"] = env_role.role.name
|
||||
|
||||
env_roles_form_data.append(env_data)
|
||||
|
||||
|
0
docs/ATATArchitecture.md
Normal file
0
docs/ATATArchitecture.md
Normal file
32
docs/EdgeControls.md
Normal file
32
docs/EdgeControls.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Edge Control
|
||||
This document describes the expected connections and listening services.
|
||||
|
||||
## Transient Connections
|
||||
| Service | Direction | Ports | Protocol | Encrypted? | Ciphers |
|
||||
| --------|-----------|-------|----------|------------|--------------|
|
||||
| Azure Container Registry | Egress | 443 | HTTP | Yes | MSFT Managed |
|
||||
| DOD CRL Service | Egress | 443 | HTTP | Yes | DOD Managed |
|
||||
| Azure Storage | Egress | 443 | HTTP | Yes | MSFT Managed|
|
||||
| Redis | Egress | 6380 | HTTP | Yes | MSFT Managed|
|
||||
| Postgres | Egress | 5432 | HTTP | Yes | MSFT Managed|
|
||||
|
||||
# Listening Ports / Services
|
||||
| Service/App | Port | Protocol| Encrypted? | Accessible |
|
||||
|-------------|---------|---------|------------|--------|
|
||||
| ATAT App | 80, 443 | HTTP | Both | Load Balancer Only
|
||||
| ATAT Auth | 80, 443 | HTTP | Both | Load Balancer Only
|
||||
|
||||
# Host List
|
||||
## Dev
|
||||
| Service| Host |
|
||||
|--------|------|
|
||||
| Redis | cloudzero-dev-redis.redis.cache.windows.net |
|
||||
| Postgres| cloudzero-dev-sql.postgres.database.azure.com |
|
||||
| Docker Container Registry | cloudzerodevregistry.azurecr.io |
|
||||
|
||||
## Production
|
||||
| Service | Host |
|
||||
|---------|------|
|
||||
| Redis | |
|
||||
| Postgres| |
|
||||
| Docker Container Registry | |
|
@ -6,4 +6,6 @@ app = make_app(make_config())
|
||||
ctx = app.app_context()
|
||||
ctx.push()
|
||||
|
||||
print("\nWelcome to atst. This shell has all models in scope, and a SQLAlchemy session called db.")
|
||||
print(
|
||||
"\nWelcome to atst. This shell has all models in scope, and a SQLAlchemy session called db."
|
||||
)
|
||||
|
@ -70,7 +70,7 @@ describe('UploadInput Test', () => {
|
||||
})
|
||||
|
||||
const component = wrapper.find(uploadinput)
|
||||
const event = { target: { value: '', files: [{ name: '' }] } }
|
||||
const event = { target: { value: '', files: [{ name: 'sample.pdf' }] } }
|
||||
|
||||
component.setMethods({
|
||||
getUploader: async () => new MockUploader('token', 'objectName'),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { emitFieldChange } from '../lib/emitters'
|
||||
import escape from '../lib/escape'
|
||||
import optionsinput from './options_input'
|
||||
import textinput from './text_input'
|
||||
import clindollaramount from './clin_dollar_amount'
|
||||
@ -100,7 +99,7 @@ export default {
|
||||
computed: {
|
||||
clinTitle: function() {
|
||||
if (!!this.clinNumber) {
|
||||
return escape(`CLIN ${this.clinNumber}`)
|
||||
return `CLIN ${this.clinNumber}`
|
||||
} else {
|
||||
return `CLIN`
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { buildUploader } from '../lib/upload'
|
||||
import { emitFieldChange } from '../lib/emitters'
|
||||
import inputValidations from '../lib/input_validations'
|
||||
|
||||
export default {
|
||||
name: 'uploadinput',
|
||||
@ -28,6 +29,7 @@ export default {
|
||||
changed: false,
|
||||
uploadError: false,
|
||||
sizeError: false,
|
||||
filenameError: false,
|
||||
downloadLink: '',
|
||||
}
|
||||
},
|
||||
@ -50,6 +52,10 @@ export default {
|
||||
this.sizeError = true
|
||||
return
|
||||
}
|
||||
if (!this.validateFileName(file.name)) {
|
||||
this.filenameError = true
|
||||
return
|
||||
}
|
||||
|
||||
const uploader = await this.getUploader()
|
||||
const response = await uploader.upload(file)
|
||||
@ -71,6 +77,10 @@ export default {
|
||||
this.uploadError = true
|
||||
}
|
||||
},
|
||||
validateFileName: function(name) {
|
||||
const regex = inputValidations.restrictedFileName.match
|
||||
return regex.test(name)
|
||||
},
|
||||
removeAttachment: function(e) {
|
||||
e.preventDefault()
|
||||
this.attachment = null
|
||||
@ -118,7 +128,8 @@ export default {
|
||||
return (
|
||||
(!this.changed && this.initialErrors) ||
|
||||
this.uploadError ||
|
||||
this.sizeError
|
||||
this.sizeError ||
|
||||
this.filenameError
|
||||
)
|
||||
},
|
||||
valid: function() {
|
||||
|
@ -104,4 +104,11 @@ export default {
|
||||
unmask: ['(', ')', '-', ' '],
|
||||
validationError: 'Please enter a 10-digit phone number',
|
||||
},
|
||||
restrictedFileName: {
|
||||
mask: false,
|
||||
match: /^[A-Za-z0-9\-_ \.]+$/,
|
||||
unmask: [],
|
||||
validationError:
|
||||
'File names can only contain the characters A-Z, 0-9, space, hyphen, underscore, and period.',
|
||||
},
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ PASSWORD = os.getenv("ATAT_BA_PASSWORD", "")
|
||||
DISABLE_VERIFY = os.getenv("DISABLE_VERIFY", "true").lower() == "true"
|
||||
|
||||
# Alpha numerics for random entity names
|
||||
LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" #pragma: allowlist secret
|
||||
LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" # pragma: allowlist secret
|
||||
|
||||
NEW_PORTFOLIO_CHANCE = 10
|
||||
NEW_APPLICATION_CHANCE = 10
|
||||
@ -29,10 +29,6 @@ def logout(l):
|
||||
l.client.get("/logout")
|
||||
|
||||
|
||||
def get_index(l):
|
||||
l.client.get("/")
|
||||
|
||||
|
||||
def get_csrf_token(response):
|
||||
d = pq(response.text)
|
||||
return d("#csrf_token").val()
|
||||
@ -52,14 +48,9 @@ def extract_id(path):
|
||||
|
||||
|
||||
def get_portfolios(l):
|
||||
response = l.client.get("/portfolios")
|
||||
response = l.client.get("/home")
|
||||
d = pq(response.text)
|
||||
portfolio_links = [
|
||||
p.attr("href")
|
||||
for p in d(
|
||||
".global-panel-container .atat-table tbody tr td:first-child a"
|
||||
).items()
|
||||
]
|
||||
portfolio_links = [p.attr("href") for p in d(".sidenav__link").items()]
|
||||
force_new_portfolio = randrange(0, 100) < NEW_PORTFOLIO_CHANCE
|
||||
if len(portfolio_links) == 0 or force_new_portfolio:
|
||||
portfolio_links += [create_portfolio(l)]
|
||||
@ -73,7 +64,7 @@ def get_portfolio(l):
|
||||
d = pq(response.text)
|
||||
application_links = [
|
||||
p.attr("href")
|
||||
for p in d(".application-list .accordion__actions a:first-child").items()
|
||||
for p in d(".portfolio-applications .accordion__header-text a").items()
|
||||
]
|
||||
if len(application_links) > 0:
|
||||
portfolio_id = extract_id(portfolio_link)
|
||||
@ -161,18 +152,14 @@ class UserBehavior(TaskSequence):
|
||||
login(self)
|
||||
|
||||
@seq_task(1)
|
||||
def home(l):
|
||||
get_index(l)
|
||||
|
||||
@seq_task(2)
|
||||
def portfolios(l):
|
||||
get_portfolios(l)
|
||||
|
||||
@seq_task(3)
|
||||
@seq_task(2)
|
||||
def pick_a_portfolio(l):
|
||||
get_portfolio(l)
|
||||
|
||||
@seq_task(4)
|
||||
@seq_task(3)
|
||||
def pick_an_app(l):
|
||||
get_app(l)
|
||||
|
||||
@ -189,4 +176,3 @@ class WebsiteUser(HttpLocust):
|
||||
if __name__ == "__main__":
|
||||
# if run as the main file, will spin up a single locust
|
||||
WebsiteUser().run()
|
||||
|
||||
|
@ -33,7 +33,7 @@ $title-font-size: 5.2rem;
|
||||
$h1-font-size: 4rem;
|
||||
$h2-font-size: 3rem;
|
||||
$h3-font-size: 2.3rem;
|
||||
$h4-font-size: 1.7rem;
|
||||
$h4-font-size: 1.9rem;
|
||||
$h5-font-size: 1.5rem;
|
||||
$h6-font-size: 1.3rem;
|
||||
$base-line-height: 1.5;
|
||||
@ -44,6 +44,7 @@ $font-sans: "Source Sans Pro", sans-serif;
|
||||
$font-serif: "Merriweather", serif;
|
||||
|
||||
$font-normal: 400;
|
||||
$font-semibold: 600;
|
||||
$font-bold: 700;
|
||||
|
||||
// Color
|
||||
|
@ -23,7 +23,7 @@
|
||||
inline-template>
|
||||
<div class="clin-card" v-if="showClin">
|
||||
<div class="card__title">
|
||||
<span class="h4" v-html='clinTitle'></span>
|
||||
<span class="h4" v-text='clinTitle'></span>
|
||||
<button
|
||||
v-if='clinIndex > 0'
|
||||
class="icon-link icon-link__remove-clin"
|
||||
@ -119,7 +119,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="h5 clin-card__title">Percent Obligated</div>
|
||||
<p id="percent-obligated" v-html='percentObligated'></p>
|
||||
<p id="percent-obligated" v-text='percentObligated'></p>
|
||||
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
@ -140,7 +140,7 @@
|
||||
<div class='modal__dialog' role='dialog' aria-modal='true'>
|
||||
<div class='modal__body'>
|
||||
<div class="task-order__modal-cancel">
|
||||
<h1 v-html='"{{ 'task_orders.form.clin_remove_text' | translate }}" + clinTitle + "?"'></h1>
|
||||
<h1 v-text='"{{ 'task_orders.form.clin_remove_text' | translate }}" + clinTitle + "?"'></h1>
|
||||
<div class="task-order__modal-cancel_buttons">
|
||||
<button
|
||||
v-on:click='closeModal(removeModalId)'
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div>
|
||||
<div v-show="valid" class="uploaded-file">
|
||||
{{ Icon("ok") }}
|
||||
<a class="uploaded-file__name" v-html="baseName" v-bind:href="downloadLink"></a>
|
||||
<a class="uploaded-file__name" v-text="baseName" v-bind:href="downloadLink"></a>
|
||||
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
||||
</div>
|
||||
<div v-show="valid === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
||||
@ -49,6 +49,9 @@
|
||||
<template v-if="sizeError">
|
||||
<span class="usa-input__message">{{ "forms.task_order.size_error" | translate }}</span>
|
||||
</template>
|
||||
<template v-if="filenameError">
|
||||
<span class="usa-input__message">{{ "forms.task_order.filename_error" | translate }}</span>
|
||||
</template>
|
||||
{% for error, error_messages in field.errors.items() %}
|
||||
<span class="usa-input__message">{{error_messages[0]}}</span>
|
||||
{% endfor %}
|
||||
|
@ -37,19 +37,19 @@
|
||||
<tr>
|
||||
<td>
|
||||
<button v-on:click='toggle($event, applicationIndex)' class='icon-link icon-link--large'>
|
||||
<span v-html='application.name'></span>
|
||||
<span v-text='application.name'></span>
|
||||
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
|
||||
<template v-else>{{ Icon('caret_up') }}</template>
|
||||
</button>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application.this_month || 0)'></span>
|
||||
<span v-text='formatDollars(application.this_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application.last_month || 0)'></span>
|
||||
<span v-text='formatDollars(application.last_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application.total || 0)'></span>
|
||||
<span v-text='formatDollars(application.total || 0)'></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
@ -58,16 +58,16 @@
|
||||
v-bind:class="[ index == application.environments.length -1 ? 'reporting-spend-table__env-row--last' : '']"
|
||||
>
|
||||
<td>
|
||||
<span class="reporting-spend-table__env-row-label" v-html='environment.name'></span>
|
||||
<span class="reporting-spend-table__env-row-label" v-text='environment.name'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment.this_month || 0)'></span>
|
||||
<span v-text='formatDollars(environment.this_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment.last_month || 0)'></span>
|
||||
<span v-text='formatDollars(environment.last_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment.total || 0)'></span>
|
||||
<span v-text='formatDollars(environment.total || 0)'></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
@ -16,6 +16,7 @@ from tests.factories import EnvironmentFactory, ApplicationFactory
|
||||
#
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
||||
environment = EnvironmentFactory.create()
|
||||
|
||||
@ -50,12 +51,14 @@ def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result == subscription_id
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
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(
|
||||
**spec_dict
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||
environment = EnvironmentFactory.create()
|
||||
|
||||
@ -68,6 +71,7 @@ def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result.id == "Test Id"
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
||||
application = ApplicationFactory.create()
|
||||
|
||||
@ -78,6 +82,7 @@ def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result.id == "Test Id"
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
||||
environment_id = str(uuid4())
|
||||
|
||||
@ -92,6 +97,7 @@ def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
||||
assert result.get("csp_user_id") == csp_user_id
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
||||
subscription_id = str(uuid4())
|
||||
management_group_id = str(uuid4())
|
||||
|
@ -147,7 +147,7 @@ def test_invite():
|
||||
user_data=user_data,
|
||||
permission_sets_names=permission_sets_names,
|
||||
environment_roles_data=[
|
||||
{"environment_id": env1.id, "role": CSPRole.BASIC_ACCESS.value},
|
||||
{"environment_id": env1.id, "role": CSPRole.ADMIN},
|
||||
{"environment_id": env2.id, "role": None},
|
||||
],
|
||||
)
|
||||
@ -173,8 +173,8 @@ def test_invite_to_nonexistent_environment():
|
||||
inviter=application.portfolio.owner,
|
||||
user_data=user_data,
|
||||
environment_roles_data=[
|
||||
{"environment_id": env1.id, "role": CSPRole.BASIC_ACCESS.value},
|
||||
{"environment_id": uuid4(), "role": CSPRole.BASIC_ACCESS.value},
|
||||
{"environment_id": env1.id, "role": CSPRole.ADMIN},
|
||||
{"environment_id": uuid4(), "role": CSPRole.ADMIN},
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -26,8 +26,8 @@ def test_create_environments():
|
||||
|
||||
|
||||
def test_update_env_role():
|
||||
env_role = EnvironmentRoleFactory.create(role=CSPRole.BASIC_ACCESS.value)
|
||||
new_role = CSPRole.TECHNICAL_READ.value
|
||||
env_role = EnvironmentRoleFactory.create(role=CSPRole.ADMIN)
|
||||
new_role = CSPRole.BILLING_READ
|
||||
Environments.update_env_role(
|
||||
env_role.environment, env_role.application_role, new_role
|
||||
)
|
||||
@ -35,7 +35,7 @@ def test_update_env_role():
|
||||
|
||||
|
||||
def test_update_env_role_no_access():
|
||||
env_role = EnvironmentRoleFactory.create(role=CSPRole.BASIC_ACCESS.value)
|
||||
env_role = EnvironmentRoleFactory.create(role=CSPRole.ADMIN)
|
||||
Environments.update_env_role(env_role.environment, env_role.application_role, None)
|
||||
|
||||
assert not EnvironmentRoles.get(
|
||||
@ -46,15 +46,13 @@ def test_update_env_role_no_access():
|
||||
|
||||
|
||||
def test_update_env_role_disabled_role():
|
||||
env_role = EnvironmentRoleFactory.create(role=CSPRole.BASIC_ACCESS.value)
|
||||
env_role = EnvironmentRoleFactory.create(role=CSPRole.ADMIN)
|
||||
Environments.update_env_role(env_role.environment, env_role.application_role, None)
|
||||
|
||||
# An exception should be raised when a new role is passed to Environments.update_env_role
|
||||
with pytest.raises(DisabledError):
|
||||
Environments.update_env_role(
|
||||
env_role.environment,
|
||||
env_role.application_role,
|
||||
CSPRole.TECHNICAL_READ.value,
|
||||
env_role.environment, env_role.application_role, CSPRole.BILLING_READ,
|
||||
)
|
||||
|
||||
assert env_role.role is None
|
||||
|
36
tests/domain/test_portfolio_state_machine.py
Normal file
36
tests/domain/test_portfolio_state_machine.py
Normal file
@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
|
||||
from tests.factories import (
|
||||
PortfolioFactory,
|
||||
PortfolioStateMachineFactory,
|
||||
)
|
||||
|
||||
from atst.models import FSMStates
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def portfolio():
|
||||
portfolio = PortfolioFactory.create()
|
||||
return portfolio
|
||||
|
||||
|
||||
def test_fsm_creation(portfolio):
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
assert sm.portfolio
|
||||
|
||||
|
||||
def test_fsm_transition_start(portfolio):
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
assert sm.portfolio
|
||||
assert sm.state == FSMStates.UNSTARTED
|
||||
|
||||
# next_state does not create the trigger callbacks !!!
|
||||
# sm.next_state()
|
||||
|
||||
sm.init()
|
||||
assert sm.state == FSMStates.STARTING
|
||||
|
||||
sm.start()
|
||||
assert sm.state == FSMStates.STARTED
|
||||
sm.create_tenant(a=1, b=2)
|
||||
assert sm.state == FSMStates.TENANT_CREATED
|
@ -1,5 +1,4 @@
|
||||
import pytest
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
from atst.domain.exceptions import NotFoundError, UnauthorizedError
|
||||
@ -7,6 +6,7 @@ from atst.domain.portfolios import (
|
||||
Portfolios,
|
||||
PortfolioError,
|
||||
PortfolioDeletionApplicationsExistError,
|
||||
PortfolioStateMachines,
|
||||
)
|
||||
from atst.domain.portfolio_roles import PortfolioRoles
|
||||
from atst.domain.applications import Applications
|
||||
@ -15,6 +15,7 @@ from atst.domain.environments import Environments
|
||||
from atst.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS
|
||||
from atst.models.application_role import Status as ApplicationRoleStatus
|
||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
from atst.models import FSMStates
|
||||
|
||||
from tests.factories import (
|
||||
ApplicationFactory,
|
||||
@ -22,6 +23,7 @@ from tests.factories import (
|
||||
UserFactory,
|
||||
PortfolioRoleFactory,
|
||||
PortfolioFactory,
|
||||
PortfolioStateMachineFactory,
|
||||
get_all_portfolio_permission_sets,
|
||||
)
|
||||
|
||||
@ -94,11 +96,11 @@ def test_scoped_portfolio_for_admin_missing_view_apps_perms(portfolio_owner, por
|
||||
def test_scoped_portfolio_returns_all_applications_for_portfolio_admin(
|
||||
portfolio, portfolio_owner
|
||||
):
|
||||
for _ in range(5):
|
||||
for i in range(5):
|
||||
Applications.create(
|
||||
portfolio.owner,
|
||||
portfolio,
|
||||
"My Application %s" % (random.randrange(1, 1000)),
|
||||
f"My Application {i}",
|
||||
"My application",
|
||||
["dev", "staging", "prod"],
|
||||
)
|
||||
@ -117,11 +119,11 @@ def test_scoped_portfolio_returns_all_applications_for_portfolio_admin(
|
||||
def test_scoped_portfolio_returns_all_applications_for_portfolio_owner(
|
||||
portfolio, portfolio_owner
|
||||
):
|
||||
for _ in range(5):
|
||||
for i in range(5):
|
||||
Applications.create(
|
||||
portfolio.owner,
|
||||
portfolio,
|
||||
"My Application %s" % (random.randrange(1, 1000)),
|
||||
f"My Application {i}",
|
||||
"My application",
|
||||
["dev", "staging", "prod"],
|
||||
)
|
||||
@ -254,3 +256,17 @@ def test_for_user_does_not_include_deleted_application_roles():
|
||||
status=ApplicationRoleStatus.ACTIVE, user=user2, application=app, deleted=True
|
||||
)
|
||||
assert len(Portfolios.for_user(user2)) == 0
|
||||
|
||||
|
||||
def test_create_state_machine(portfolio):
|
||||
fsm = PortfolioStateMachines.create(portfolio)
|
||||
assert fsm
|
||||
|
||||
|
||||
def test_get_portfolios_pending_provisioning(session):
|
||||
for x in range(5):
|
||||
portfolio = PortfolioFactory.create()
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
if x == 2:
|
||||
sm.state = FSMStates.COMPLETED
|
||||
assert len(Portfolios.get_portfolios_pending_provisioning()) == 4
|
||||
|
@ -255,7 +255,7 @@ class EnvironmentRoleFactory(Base):
|
||||
model = EnvironmentRole
|
||||
|
||||
environment = factory.SubFactory(EnvironmentFactory)
|
||||
role = random.choice([e.value for e in CSPRole])
|
||||
role = random.choice([e for e in CSPRole])
|
||||
application_role = factory.SubFactory(ApplicationRoleFactory)
|
||||
|
||||
|
||||
@ -342,3 +342,17 @@ class NotificationRecipientFactory(Base):
|
||||
model = NotificationRecipient
|
||||
|
||||
email = factory.Faker("email")
|
||||
|
||||
|
||||
class PortfolioStateMachineFactory(Base):
|
||||
class Meta:
|
||||
model = PortfolioStateMachine
|
||||
|
||||
portfolio = factory.SubFactory(PortfolioFactory)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, model_class, *args, **kwargs):
|
||||
portfolio = kwargs.pop("portfolio", PortfolioFactory.create())
|
||||
kwargs.update({"portfolio": portfolio})
|
||||
fsm = super()._create(model_class, *args, **kwargs)
|
||||
return fsm
|
||||
|
@ -28,7 +28,7 @@ def test_add_user_to_environment():
|
||||
EnvironmentRoleFactory.create(
|
||||
application_role=application_role,
|
||||
environment=dev_environment,
|
||||
role=CSPRole.BASIC_ACCESS.value,
|
||||
role=CSPRole.ADMIN,
|
||||
)
|
||||
assert developer in dev_environment.users
|
||||
|
||||
@ -75,9 +75,9 @@ def test_environment_provisioning_status(env_data, expected_status):
|
||||
|
||||
def test_environment_roles_do_not_include_deleted():
|
||||
member_list = [
|
||||
{"role_name": CSPRole.BASIC_ACCESS.value},
|
||||
{"role_name": CSPRole.BASIC_ACCESS.value},
|
||||
{"role_name": CSPRole.BASIC_ACCESS.value},
|
||||
{"role_name": CSPRole.ADMIN},
|
||||
{"role_name": CSPRole.ADMIN},
|
||||
{"role_name": CSPRole.ADMIN},
|
||||
]
|
||||
env = EnvironmentFactory.create(members=member_list)
|
||||
role_1 = env.roles[0]
|
||||
|
@ -9,9 +9,7 @@ def test_environment_access_with_env_role(client, user_session):
|
||||
app_role = ApplicationRoleFactory.create(
|
||||
user=user, application=environment.application
|
||||
)
|
||||
EnvironmentRoleFactory.create(
|
||||
application_role=app_role, environment=environment, role="developer"
|
||||
)
|
||||
EnvironmentRoleFactory.create(application_role=app_role, environment=environment)
|
||||
user_session(user)
|
||||
response = client.get(
|
||||
url_for("applications.access_environment", environment_id=environment.id)
|
||||
|
@ -153,7 +153,7 @@ def test_post_new_member(monkeypatch, client, user_session, session):
|
||||
"user_data-dod_id": user.dod_id,
|
||||
"user_data-email": user.email,
|
||||
"environment_roles-0-environment_id": env.id,
|
||||
"environment_roles-0-role": "Basic Access",
|
||||
"environment_roles-0-role": "ADMIN",
|
||||
"environment_roles-0-environment_name": env.name,
|
||||
"environment_roles-1-environment_id": env_1.id,
|
||||
"environment_roles-1-role": NO_ACCESS,
|
||||
@ -201,7 +201,7 @@ def test_post_update_member(client, user_session):
|
||||
),
|
||||
data={
|
||||
"environment_roles-0-environment_id": env.id,
|
||||
"environment_roles-0-role": "Basic Access",
|
||||
"environment_roles-0-role": "ADMIN",
|
||||
"environment_roles-0-environment_name": env.name,
|
||||
"environment_roles-1-environment_id": env_1.id,
|
||||
"environment_roles-1-role": NO_ACCESS,
|
||||
|
@ -129,11 +129,11 @@ def test_edit_application_environments_obj(app, client, user_session):
|
||||
env = application.environments[0]
|
||||
app_role1 = ApplicationRoleFactory.create(application=application)
|
||||
env_role1 = EnvironmentRoleFactory.create(
|
||||
application_role=app_role1, environment=env, role=CSPRole.BASIC_ACCESS.value
|
||||
application_role=app_role1, environment=env, role=CSPRole.ADMIN
|
||||
)
|
||||
app_role2 = ApplicationRoleFactory.create(application=application, user=None)
|
||||
env_role2 = EnvironmentRoleFactory.create(
|
||||
application_role=app_role2, environment=env, role=CSPRole.NETWORK_ADMIN.value
|
||||
application_role=app_role2, environment=env, role=CSPRole.CONTRIBUTOR
|
||||
)
|
||||
|
||||
user_session(portfolio.owner)
|
||||
@ -180,7 +180,7 @@ def test_get_members_data(app, client, user_session):
|
||||
environments=[
|
||||
{
|
||||
"name": "testing",
|
||||
"members": [{"user": user, "role_name": CSPRole.BASIC_ACCESS.value}],
|
||||
"members": [{"user": user, "role_name": CSPRole.ADMIN}],
|
||||
}
|
||||
],
|
||||
)
|
||||
@ -212,7 +212,7 @@ def test_get_members_data(app, client, user_session):
|
||||
{
|
||||
"environment_id": str(environment.id),
|
||||
"environment_name": environment.name,
|
||||
"role": env_role.role,
|
||||
"role": env_role.role.value,
|
||||
}
|
||||
]
|
||||
assert member["role_status"]
|
||||
@ -402,7 +402,7 @@ def test_create_member(monkeypatch, client, user_session, session):
|
||||
"user_data-dod_id": user.dod_id,
|
||||
"user_data-email": user.email,
|
||||
"environment_roles-0-environment_id": env.id,
|
||||
"environment_roles-0-role": "Basic Access",
|
||||
"environment_roles-0-role": "ADMIN",
|
||||
"environment_roles-0-environment_name": env.name,
|
||||
"environment_roles-1-environment_id": env_1.id,
|
||||
"environment_roles-1-role": NO_ACCESS,
|
||||
@ -511,10 +511,10 @@ def test_update_member(client, user_session, session):
|
||||
env_2 = EnvironmentFactory.create(application=application)
|
||||
# add user to two of the environments: env and env_1
|
||||
updated_role = EnvironmentRoleFactory.create(
|
||||
environment=env, application_role=app_role, role=CSPRole.BASIC_ACCESS.value
|
||||
environment=env, application_role=app_role, role=CSPRole.ADMIN
|
||||
)
|
||||
suspended_role = EnvironmentRoleFactory.create(
|
||||
environment=env_1, application_role=app_role, role=CSPRole.BASIC_ACCESS.value
|
||||
environment=env_1, application_role=app_role, role=CSPRole.ADMIN
|
||||
)
|
||||
|
||||
user_session(application.portfolio.owner)
|
||||
@ -528,13 +528,13 @@ def test_update_member(client, user_session, session):
|
||||
),
|
||||
data={
|
||||
"environment_roles-0-environment_id": env.id,
|
||||
"environment_roles-0-role": CSPRole.TECHNICAL_READ.value,
|
||||
"environment_roles-0-role": "CONTRIBUTOR",
|
||||
"environment_roles-0-environment_name": env.name,
|
||||
"environment_roles-1-environment_id": env_1.id,
|
||||
"environment_roles-1-environment_name": env_1.name,
|
||||
"environment_roles-1-disabled": "True",
|
||||
"environment_roles-2-environment_id": env_2.id,
|
||||
"environment_roles-2-role": CSPRole.NETWORK_ADMIN.value,
|
||||
"environment_roles-2-role": "BILLING_READ",
|
||||
"environment_roles-2-environment_name": env_2.name,
|
||||
"perms_env_mgmt": True,
|
||||
"perms_team_mgmt": True,
|
||||
@ -565,7 +565,7 @@ def test_update_member(client, user_session, session):
|
||||
environment_roles = application.roles[0].environment_roles
|
||||
# check that the user has roles in the correct envs
|
||||
assert len(environment_roles) == 3
|
||||
assert updated_role.role == CSPRole.TECHNICAL_READ.value
|
||||
assert updated_role.role == CSPRole.CONTRIBUTOR
|
||||
assert suspended_role.disabled
|
||||
|
||||
|
||||
@ -695,7 +695,7 @@ def test_handle_create_member(monkeypatch, set_g, session):
|
||||
"user_data-dod_id": user.dod_id,
|
||||
"user_data-email": user.email,
|
||||
"environment_roles-0-environment_id": env.id,
|
||||
"environment_roles-0-role": "Basic Access",
|
||||
"environment_roles-0-role": "ADMIN",
|
||||
"environment_roles-0-environment_name": env.name,
|
||||
"environment_roles-1-environment_id": env_1.id,
|
||||
"environment_roles-1-role": NO_ACCESS,
|
||||
@ -718,7 +718,7 @@ def test_handle_create_member(monkeypatch, set_g, session):
|
||||
assert job_mock.called
|
||||
|
||||
|
||||
def test_handle_update_member(set_g):
|
||||
def test_handle_update_member_success(set_g):
|
||||
user = UserFactory.create()
|
||||
application = ApplicationFactory.create(
|
||||
environments=[{"name": "Naboo"}, {"name": "Endor"}]
|
||||
@ -732,7 +732,7 @@ def test_handle_update_member(set_g):
|
||||
form_data = ImmutableMultiDict(
|
||||
{
|
||||
"environment_roles-0-environment_id": env.id,
|
||||
"environment_roles-0-role": "Basic Access",
|
||||
"environment_roles-0-role": "ADMIN",
|
||||
"environment_roles-0-environment_name": env.name,
|
||||
"environment_roles-1-environment_id": env_1.id,
|
||||
"environment_roles-1-role": NO_ACCESS,
|
||||
@ -772,7 +772,7 @@ def test_handle_update_member_with_error(set_g, monkeypatch, mock_logger):
|
||||
form_data = ImmutableMultiDict(
|
||||
{
|
||||
"environment_roles-0-environment_id": env.id,
|
||||
"environment_roles-0-role": "Basic Access",
|
||||
"environment_roles-0-role": "ADMIN",
|
||||
"environment_roles-0-environment_name": env.name,
|
||||
"environment_roles-1-environment_id": env_1.id,
|
||||
"environment_roles-1-role": NO_ACCESS,
|
||||
|
@ -213,7 +213,7 @@ def test_applications_access_environment_access(get_url_assert_status):
|
||||
"environments": [
|
||||
{
|
||||
"name": "thebar",
|
||||
"members": [{"user": dev, "role_name": "devops"}],
|
||||
"members": [{"user": dev, "role_name": "ADMIN"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
@ -5,16 +5,20 @@ from unittest.mock import Mock
|
||||
from threading import Thread
|
||||
|
||||
from atst.domain.csp.cloud import MockCloudProvider
|
||||
from atst.domain.portfolios import Portfolios
|
||||
|
||||
from atst.jobs import (
|
||||
RecordEnvironmentFailure,
|
||||
RecordEnvironmentRoleFailure,
|
||||
do_create_environment,
|
||||
do_create_atat_admin_user,
|
||||
dispatch_create_environment,
|
||||
dispatch_create_atat_admin_user,
|
||||
create_environment,
|
||||
dispatch_provision_portfolio,
|
||||
dispatch_provision_user,
|
||||
create_environment,
|
||||
do_provision_user,
|
||||
do_provision_portfolio,
|
||||
do_create_environment,
|
||||
do_create_atat_admin_user,
|
||||
)
|
||||
from atst.models.utils import claim_for_update
|
||||
from atst.domain.exceptions import ClaimFailedException
|
||||
@ -22,9 +26,10 @@ from tests.factories import (
|
||||
EnvironmentFactory,
|
||||
EnvironmentRoleFactory,
|
||||
PortfolioFactory,
|
||||
PortfolioStateMachineFactory,
|
||||
ApplicationRoleFactory,
|
||||
)
|
||||
from atst.models import EnvironmentRole, ApplicationRoleStatus
|
||||
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="function")
|
||||
@ -32,6 +37,12 @@ def csp():
|
||||
return Mock(wraps=MockCloudProvider({}, with_delay=False, with_failure=False))
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def portfolio():
|
||||
portfolio = PortfolioFactory.create()
|
||||
return portfolio
|
||||
|
||||
|
||||
def test_environment_job_failure(celery_app, celery_worker):
|
||||
@celery_app.task(bind=True, base=RecordEnvironmentFailure)
|
||||
def _fail_hard(self, environment_id=None):
|
||||
@ -248,6 +259,7 @@ def test_claim_for_update(session):
|
||||
|
||||
|
||||
def test_dispatch_provision_user(csp, session, celery_app, celery_worker, monkeypatch):
|
||||
|
||||
# Given that I have four environment roles:
|
||||
# (A) one of which has a completed status
|
||||
# (B) one of which has an environment that has not been provisioned
|
||||
@ -293,7 +305,7 @@ def test_do_provision_user(csp, session):
|
||||
environment_role = EnvironmentRoleFactory.create(
|
||||
environment=provisioned_environment,
|
||||
status=EnvironmentRole.Status.PENDING,
|
||||
role="my_role",
|
||||
role="ADMIN",
|
||||
)
|
||||
|
||||
# When I call the user provisoning task
|
||||
@ -302,7 +314,33 @@ def test_do_provision_user(csp, session):
|
||||
session.refresh(environment_role)
|
||||
# I expect that the CSP create_or_update_user method will be called
|
||||
csp.create_or_update_user.assert_called_once_with(
|
||||
credentials, environment_role, "my_role"
|
||||
credentials, environment_role, CSPRole.ADMIN
|
||||
)
|
||||
# I expect that the EnvironmentRole now has a csp_user_id
|
||||
assert environment_role.csp_user_id
|
||||
|
||||
|
||||
def test_dispatch_provision_portfolio(
|
||||
csp, session, portfolio, celery_app, celery_worker, monkeypatch
|
||||
):
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
mock = Mock()
|
||||
monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
||||
dispatch_provision_portfolio.run()
|
||||
mock.delay.assert_called_once_with(portfolio_id=portfolio.id)
|
||||
|
||||
|
||||
def test_do_provision_portfolio(csp, session, portfolio):
|
||||
do_provision_portfolio(csp=csp, portfolio_id=portfolio.id)
|
||||
session.refresh(portfolio)
|
||||
assert portfolio.state_machine
|
||||
|
||||
|
||||
def test_provision_portfolio_create_tenant(
|
||||
csp, session, portfolio, celery_app, celery_worker, monkeypatch
|
||||
):
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
# mock = Mock()
|
||||
# monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
||||
# dispatch_provision_portfolio.run()
|
||||
# mock.delay.assert_called_once_with(portfolio_id=portfolio.id)
|
||||
|
@ -292,6 +292,7 @@ forms:
|
||||
task_order:
|
||||
upload_error: There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.
|
||||
size_error: The file you have selected is too large. Please choose a file no larger than 64MB.
|
||||
filename_error: File names can only contain the characters A-Z, 0-9, space, hyphen, underscore, and period.
|
||||
defense_component_label: Select DoD component(s) funding your Portfolio
|
||||
file_format_not_allowed: Only PDF or PNG files can be uploaded.
|
||||
number_description: Task order number (13 digits)
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -176,7 +176,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -188,7 +188,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -490,12 +490,12 @@ Imported from: AT-AT CI - New App Step 1-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#add-app-mem > div > div:nth-of-type(1) > h1</td>
|
||||
<td>css=#add-app-mem > div > div > div.member-form > h2</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=#add-app-mem > div > div:nth-of-type(1) > h1</td>
|
||||
<td>css=#add-app-mem > div > div > div.member-form > h2</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -595,12 +595,12 @@ Imported from: AT-AT CI - New App Step 1-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#add-app-mem > div > div:nth-of-type(2) > h2</td>
|
||||
<td>css=#add-app-mem > div > div > div:nth-of-type(2) > h2</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=#add-app-mem > div > div:nth-of-type(2) > h2</td>
|
||||
<td>css=#add-app-mem > div > div > div:nth-of-type(2) > h2</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -111,7 +111,7 @@
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -182,7 +182,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -291,12 +291,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=td.name</td>
|
||||
<td>css=th.table-cell--third</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=td.name</td>
|
||||
<td>css=th.table-cell--third</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -313,7 +313,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=button.usa-button.usa-button-primary.usa-button-big</td>
|
||||
<td>Save</td>
|
||||
<td>Save Changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -336,30 +336,14 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<!--Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=input.usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=input.usa-button.usa-button-primary</td>
|
||||
<td>Save</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=a.icon-link.modal-link</td>
|
||||
<td>css=a.usa-button.usa-button-secondary.add-new-button</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=a.icon-link.modal-link</td>
|
||||
<td>css=a.usa-button.usa-button-secondary.add-new-button</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -369,13 +353,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#add-port-mem > div > div:nth-of-type(1) > h1</td>
|
||||
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=#add-port-mem > div > div:nth-of-type(1) > h1</td>
|
||||
<td>*Invite new portfolio member*</td>
|
||||
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
|
||||
<td>*Add Manager*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -459,13 +443,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#add-port-mem > div > div:nth-of-type(2) > h1</td>
|
||||
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=#add-port-mem > div > div:nth-of-type(2) > h1</td>
|
||||
<td>*Assign member permissions*</td>
|
||||
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
|
||||
<td>*Set Portfolio Permissions*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -474,12 +458,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt</td>
|
||||
<td>css=#perms_app_mgmt-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt</td>
|
||||
<td>css=#perms_app_mgmt-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -489,12 +473,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td>
|
||||
<td>css=#perms_funding-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td>
|
||||
<td>css=#perms_funding-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -504,12 +488,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_funding</td>
|
||||
<td>css=#perms_reporting-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_funding</td>
|
||||
<td>css=#perms_reporting-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -519,57 +503,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_funding > option:nth-of-type(1)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_funding > option:nth-of-type(1)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_reporting</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_reporting</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_reporting > option:nth-of-type(1)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_reporting > option:nth-of-type(1)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_portfolio_mgmt</td>
|
||||
<td>css=#perms_portfolio_mgmt-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#permission_sets-perms_portfolio_mgmt</td>
|
||||
<td>css=#perms_portfolio_mgmt-None</td>
|
||||
<td>edit_portfolio_admin</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -579,21 +518,6 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_portfolio_mgmt > option:nth-of-type(2)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_portfolio_mgmt > option:nth-of-type(2)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=input[type="submit"].action-group__action</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@ -609,27 +533,27 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.name</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.name</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.usa-alert-body > p:nth-of-type(2)</td>
|
||||
<td>css=table.atat-table > tbody > tr > td > span.label.label--success.label--below</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.usa-alert-body > p:nth-of-type(2)</td>
|
||||
<td>css=table.atat-table > tbody > tr > td > span.label.label--success.label--below</td>
|
||||
<td>*invite pending*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.usa-alert-body</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.usa-alert-body</td>
|
||||
<td>*You have successfully invited Brandon Buchannan to the portfolio.*</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -219,12 +219,12 @@ Imported from: AT-AT CI - login-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=td.name</td>
|
||||
<td>css=th.table-cell--third</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=td.name</td>
|
||||
<td>css=th.table-cell--third</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -240,7 +240,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=button.usa-button.usa-button-primary.usa-button-big</td>
|
||||
<td>Save</td>
|
||||
<td>Save Changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -257,21 +257,6 @@ Imported from: AT-AT CI - login-->
|
||||
<td>css=button.usa-button.usa-button-primary</td>
|
||||
<td>*Update*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=input.usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=input.usa-button.usa-button-primary</td>
|
||||
<td>Save</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -609,12 +609,12 @@ Ghost Inspector flow control because button tends to take a little while to beco
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td>*$100,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -624,15 +624,13 @@ Ghost Inspector flow control because button tends to take a little while to beco
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>*October 01, 2019
|
||||
-
|
||||
June 30, 2020*</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td>*October 01, 2019 - June 30, 2020*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -641,12 +639,12 @@ Ghost Inspector flow control because button tends to take a little while to beco
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > h5.summary-item__header > .summary-item__header-text</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > h5.summary-item__header > .summary-item__header-text</td>
|
||||
<td>*Days Remaining*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -662,7 +660,7 @@ Ghost Inspector flow control because button tends to take a little while to beco
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=h3.h5</td>
|
||||
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td>
|
||||
<td>*IDIQ CLIN 0002*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -219,12 +219,12 @@ Imported from: AT-AT CI - login-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td>*$0.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -234,12 +234,12 @@ Imported from: AT-AT CI - login-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > .summary-item__value</td>
|
||||
<td>*0 days*</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -646,12 +646,12 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<!--Imported from: AT-AT CI - Reports - Basics-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td>*$100,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -662,15 +662,13 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<!--Imported from: AT-AT CI - Reports - Basics-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>*October 01, 2019
|
||||
-
|
||||
June 30, 2020*</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td>*October 01, 2019 - June 30, 2020*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -680,12 +678,12 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<!--Imported from: AT-AT CI - Reports - Basics-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > h5.summary-item__header > .summary-item__header-text</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > h5.summary-item__header > .summary-item__header-text</td>
|
||||
<td>*Days Remaining*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -702,7 +700,7 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=h3.h5</td>
|
||||
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td>
|
||||
<td>*IDIQ CLIN 0002*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -646,12 +646,12 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<!--Imported from: AT-AT CI - Reports - Basics-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td>*$100,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -662,15 +662,13 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<!--Imported from: AT-AT CI - Reports - Basics-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>*October 01, 2019
|
||||
-
|
||||
June 30, 2020*</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td>*October 01, 2019 - June 30, 2020*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -680,12 +678,12 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<!--Imported from: AT-AT CI - Reports - Basics-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > h5.summary-item__header > .summary-item__header-text</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > h5.summary-item__header > .summary-item__header-text</td>
|
||||
<td>*Days Remaining*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -702,7 +700,7 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=h3.h5</td>
|
||||
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td>
|
||||
<td>*IDIQ CLIN 0002*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -828,7 +826,7 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Task order details*</td>
|
||||
<td>*Task Order #*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -837,12 +835,12 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(2)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(2)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td>
|
||||
<td>*$100,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -852,29 +850,14 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(4)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(4)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td>
|
||||
<td>*$800,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.col.task-order__details > div:nth-of-type(2)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=.col.task-order__details > div:nth-of-type(2)</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -1387,12 +1387,12 @@ Ghost Inspector flow control because button tends to take a little while to beco
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value</td>
|
||||
<td>*$100,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -1402,15 +1402,13 @@ Ghost Inspector flow control because button tends to take a little while to beco
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value</td>
|
||||
<td>*October 01, 2019
|
||||
-
|
||||
June 30, 2020*</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value</td>
|
||||
<td>*October 01, 2019 - June 30, 2020*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -1425,7 +1423,7 @@ Ghost Inspector flow control because button tends to take a little while to beco
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=h3.h5</td>
|
||||
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td>
|
||||
<td>*IDIQ CLIN 0002*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -626,12 +626,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.application-member__user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]</td>
|
||||
<td>css=.user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.application-member__user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]</td>
|
||||
<td>css=.user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]</td>
|
||||
<td>*Brandon*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -135,7 +135,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -135,7 +135,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -727,12 +727,27 @@ Imported from: AT-AT CI - TO Step 2-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(2)</td>
|
||||
<td>css=.task-order__header > p > strong</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=.task-order__header > p > strong</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(2)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td>
|
||||
<td>*$100,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -742,12 +757,12 @@ Imported from: AT-AT CI - TO Step 2-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(4)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(4)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td>
|
||||
<td>*$800,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -757,13 +772,13 @@ Imported from: AT-AT CI - TO Step 2-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.col.task-order__details > div:nth-of-type(2)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=.col.task-order__details > div:nth-of-type(2)</td>
|
||||
<td></td>
|
||||
<td>verifyText</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > .summary-item__value--large</td>
|
||||
<td>*$0.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -141,7 +141,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -769,12 +769,28 @@ Imported from: AT-AT CI - TO Step 3-->
|
||||
<!--Imported from: AT-AT CI - TO Step 4-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(2)</td>
|
||||
<td>css=.task-order__header > p > strong</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=.task-order__header > p > strong</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<!--Imported from: AT-AT CI - TO Step 4-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(2)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td>
|
||||
<td>*$100,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -785,12 +801,12 @@ Imported from: AT-AT CI - TO Step 3-->
|
||||
<!--Imported from: AT-AT CI - TO Step 4-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(4)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.totals-box > .h3:nth-of-type(4)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td>
|
||||
<td>*$800,000.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -801,13 +817,13 @@ Imported from: AT-AT CI - TO Step 3-->
|
||||
<!--Imported from: AT-AT CI - TO Step 4-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.col.task-order__details > div:nth-of-type(2)</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > .summary-item__value--large</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertElementPresent</td>
|
||||
<td>css=.col.task-order__details > div:nth-of-type(2)</td>
|
||||
<td></td>
|
||||
<td>verifyText</td>
|
||||
<td>css=.row > .col.col--grow.summary-item:nth-of-type(3) > .summary-item__value--large</td>
|
||||
<td>*$0.00*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -998,7 +1014,7 @@ Imported from: AT-AT CI - TO Step 3-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.usa-alert-text</td>
|
||||
<td>*Your task order form for Tatooine Energy Maintenance Systems has been submitted.*</td>
|
||||
<td>*Your task order form for Tatooine Energy Maintenance Systems*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<meta name="ghost-inspector-screenshotTarget" content="" />
|
||||
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
|
||||
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
|
Loading…
x
Reference in New Issue
Block a user