Merge branch 'staging' into 170690791-cleanup-minikube

This commit is contained in:
dandds 2020-01-16 08:53:37 -05:00 committed by GitHub
commit 76a29e9307
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 1626 additions and 595 deletions

View File

@ -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
View File

@ -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
View 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`

View 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 ###

View File

@ -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 ###

View File

@ -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)

View File

@ -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 {

View File

@ -2,4 +2,5 @@ from .portfolios import (
Portfolios,
PortfolioError,
PortfolioDeletionApplicationsExistError,
PortfolioStateMachines,
)

View File

@ -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()

View File

@ -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):

View File

@ -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)
]

View File

@ -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"

View File

@ -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:

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View 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)

View File

@ -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")

View 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

View File

@ -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,

View File

@ -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
View File

32
docs/EdgeControls.md Normal file
View 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 | |

View File

@ -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."
)

View File

@ -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'),

View File

@ -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`
}

View File

@ -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() {

View File

@ -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.',
},
}

View File

@ -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()

View File

@ -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

View File

@ -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)'

View File

@ -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 %}

View File

@ -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>

View File

@ -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())

View File

@ -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},
],
)

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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"}],
}
],
}

View File

@ -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)

View File

@ -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)

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">