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 = "*" pendulum = "*"
redis = "*" redis = "*"
sqlalchemy = ">=1.3.12" sqlalchemy = ">=1.3.12"
sqlalchemy-json = "*"
pydantic = "*"
alembic = "*" alembic = "*"
"psycopg2-binary" = "*" "psycopg2-binary" = "*"
flask = "*" flask = "*"
@ -30,6 +32,7 @@ msrestazure = "*"
azure-mgmt-authorization = "*" azure-mgmt-authorization = "*"
azure-mgmt-managementgroups = "*" azure-mgmt-managementgroups = "*"
azure-mgmt-resource = "*" azure-mgmt-resource = "*"
transitions = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"

200
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "1bf62937e2d8187deb11c56188ec763f56ec055d65c87773c945384ffff68dcc" "sha256": "63b8f9d203f306a6f0ff20514b024909aa7e64917e1befcc9ea79931b5b4bd34"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -257,11 +257,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
], ],
"markers": "python_version < '3.8'", "markers": "python_version < '3.8'",
"version": "==1.3.0" "version": "==1.4.0"
}, },
"isodate": { "isodate": {
"hashes": [ "hashes": [
@ -340,10 +340,10 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
], ],
"version": "==8.0.2" "version": "==8.1.0"
}, },
"msrest": { "msrest": {
"hashes": [ "hashes": [
@ -423,6 +423,26 @@
], ],
"version": "==2.19" "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": { "pyjwt": {
"hashes": [ "hashes": [
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
@ -521,6 +541,27 @@
"index": "pypi", "index": "pypi",
"version": "==1.3.12" "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": { "unipath": {
"hashes": [ "hashes": [
"sha256:09839adcc72e8a24d4f76d63656f30b5a1f721fc40c9bcd79d8c67bdd8b47dae", "sha256:09839adcc72e8a24d4f76d63656f30b5a1f721fc40c9bcd79d8c67bdd8b47dae",
@ -568,10 +609,10 @@
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" "sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
], ],
"version": "==0.6.0" "version": "==1.0.0"
} }
}, },
"develop": { "develop": {
@ -687,39 +728,39 @@
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:189aac76d6e0d7af15572c51892e7326ee451c076c5a50a9d266406cd6c49708", "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
"sha256:1bf7ba2af1d373a1750888724f84cffdfc697738f29a353c98195f98fc011509", "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
"sha256:1f4ee8e2e4243971618bc16fcc4478317405205f135e95226c2496e2a3b8dbbf", "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
"sha256:225e79a5d485bc1642cb7ba02281419c633c216cdc6b26c26494ba959f09e69f", "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
"sha256:23688ff75adfa8bfa2a67254d889f9bdf9302c27241d746e17547c42c732d3f4", "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
"sha256:28f7f73b34a05e23758e860a89a7f649b85c6749e252eff60ebb05532d180e86", "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
"sha256:2d0cb9b1fe6ad0d915d45ad3d87f03a38e979093a98597e755930db1f897afae", "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
"sha256:47874b4711c5aeb295c31b228a758ce3d096be83dc37bd56da48ed99efb8813b", "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
"sha256:511ec0c00840e12fb4e852e4db58fa6a01ca4da72f36a9766fae344c3d502033", "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
"sha256:53e7438fef0c97bc248f88ba1edd10268cd94d5609970aaf87abbe493691af87", "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
"sha256:569f9ee3025682afda6e9b0f5bb14897c0db03f1a1dc088b083dd36e743f92bb", "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
"sha256:593853aa1ac6dcc6405324d877544c596c9d948ef20d2e9512a0f5d2d3202356", "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
"sha256:5b0a07158360d22492f9abd02a0f2ee7981b33f0646bf796598b7673f6bbab14", "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
"sha256:7ca3db38a61f3655a2613ee2c190d63639215a7a736d3c64cc7bbdb002ce6310", "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
"sha256:7d1cc7acc9ce55179616cf72154f9e648136ea55987edf84addbcd9886ffeba2", "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
"sha256:88b51153657612aea68fa684a5b88037597925260392b7bb4509d4f9b0bdd889", "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
"sha256:955ec084f549128fa2702f0b2dc696392001d986b71acd8fd47424f28289a9c3", "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
"sha256:b251c7092cbb6d789d62dc9c9e7c4fb448c9138b51285c36aeb72462cad3600e", "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
"sha256:bd82b684bb498c60ef47bb1541a50e6d006dde8579934dcbdbc61d67d1ea70d9", "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
"sha256:bfe102659e2ec13b86c7f3b1db6c9a4e7beea4255058d006351339e6b342d5d2", "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
"sha256:c1e4e39e43057396a5e9d069bfbb6ffeee892e40c5d2effbd8cd71f34ee66c4d", "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
"sha256:cb2b74c123f65e8166f7e1265829a6c8ed755c3cd16d7f50e75a83456a5f3fd7", "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
"sha256:cca38ded59105f7705ef6ffe1e960b8db6c7d8279c1e71654a4775ab4454ca15", "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
"sha256:cf908840896f7aa62d0ec693beb53264b154f972eb8226fb864ac38975590c4f", "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
"sha256:d095a7b473f8a95f7efe821f92058c8a2ecfb18f8db6677ae3819e15dc11aaae", "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
"sha256:d22b4297e7e4225ccf01f1aa55e7a96412ea0796b532dd614c3fcbafa341128e", "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
"sha256:d4a2b578a7a70e0c71f662705262f87a456f1e6c1e40ada7ea699abaf070a76d", "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
"sha256:ddeb42a3d5419434742bf4cc71c9eaa22df3b76808e23a82bd0b0bd360f1a9f1", "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
"sha256:e65a5aa1670db6263f19fdc03daee1d7dbbadb5cb67fd0a1f16033659db13c1d", "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
"sha256:eaad65bd20955131bcdb3967a4dea66b4e4d4ca488efed7c00d91ee0173387e8", "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
"sha256:f45fba420b94165c17896861bb0e8b27fb7abdcedfeb154895d8553df90b7b00" "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
], ],
"version": "==5.0.2" "version": "==5.0.3"
}, },
"decorator": { "decorator": {
"hashes": [ "hashes": [
@ -752,10 +793,10 @@
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11", "sha256:047d4d1791bfb3756264da670d99df13d799bb36e7d88774b1585a82d05dbaec",
"sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432" "sha256:1b1a58961683b30c574520d0c739c4443e0ef6a185c04382e8cc888273dbebed"
], ],
"version": "==3.0.0" "version": "==4.0.0"
}, },
"flask": { "flask": {
"hashes": [ "hashes": [
@ -796,11 +837,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
], ],
"markers": "python_version < '3.8'", "markers": "python_version < '3.8'",
"version": "==1.3.0" "version": "==1.4.0"
}, },
"ipdb": { "ipdb": {
"hashes": [ "hashes": [
@ -920,10 +961,10 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
], ],
"version": "==8.0.2" "version": "==8.1.0"
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
@ -1143,12 +1184,12 @@
}, },
"rope": { "rope": {
"hashes": [ "hashes": [
"sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "sha256:52423a7eebb5306a6d63bdc91a7c657db51ac9babfb8341c9a1440831ecf3203",
"sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", "sha256:ae1fa2fd56f64f4cc9be46493ce54bed0dd12dee03980c61a4393d89d84029ad",
"sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf" "sha256:d2830142c2e046f5fc26a022fe680675b6f48f81c7fc1f03a950706e746e9dfe"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.14.0" "version": "==0.16.0"
}, },
"selenium": { "selenium": {
"hashes": [ "hashes": [
@ -1209,29 +1250,30 @@
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
], ],
"markers": "implementation_name == 'cpython' and python_version < '3.8'", "markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.0" "version": "==1.4.1"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
@ -1277,10 +1319,10 @@
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" "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 .cloud import MockCloudProvider
from .file_uploads import AzureUploader, MockUploader from .file_uploads import AzureUploader, MockUploader
from .reports import MockReportingProvider from .reports import MockReportingProvider
@ -29,3 +31,22 @@ def make_csp_provider(app, csp=None):
app.csp = MockCSP(app, test_mode=True) app.csp = MockCSP(app, test_mode=True)
else: else:
app.csp = MockCSP(app) 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 import re
from typing import Dict
from uuid import uuid4 from uuid import uuid4
from pydantic import BaseModel
from atst.models.user import User from atst.models.user import User
from atst.models.application import Application
from atst.models.environment import Environment from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole from atst.models.environment_role import EnvironmentRole
from .policy import AzurePolicyManager
class GeneralCSPException(Exception): 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: class CloudProviderInterface:
def root_creds(self) -> Dict: def root_creds(self) -> Dict:
raise NotImplementedError() raise NotImplementedError()
@ -325,6 +416,68 @@ class MockCloudProvider(CloudProviderInterface):
return {"id": self._id(), "credentials": self._auth_credentials} 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): def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
self._authorize(auth_credentials) self._authorize(auth_credentials)
@ -401,18 +554,15 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
class AzureSDKProvider(object): class AzureSDKProvider(object):
def __init__(self): def __init__(self):
from azure.mgmt import subscription, authorization, managementgroups from azure.mgmt import subscription, authorization
from azure.mgmt.resource import policy
import azure.graphrbac as graphrbac import azure.graphrbac as graphrbac
import azure.common.credentials as credentials import azure.common.credentials as credentials
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
self.subscription = subscription self.subscription = subscription
self.authorization = authorization self.authorization = authorization
self.managementgroups = managementgroups
self.graphrbac = graphrbac self.graphrbac = graphrbac
self.credentials = credentials self.credentials = credentials
self.policy = policy
# may change to a JEDI cloud # may change to a JEDI cloud
self.cloud = AZURE_PUBLIC_CLOUD self.cloud = AZURE_PUBLIC_CLOUD
@ -430,28 +580,45 @@ class AzureCloudProvider(CloudProviderInterface):
else: else:
self.sdk = azure_sdk_provider self.sdk = azure_sdk_provider
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
def create_environment( def create_environment(
self, auth_credentials: Dict, user: User, environment: 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) credentials = self._get_credential_obj(self._root_creds)
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format sub_client = self.sdk.subscription.SubscriptionClient(credentials)
management_group_id = "?" # management group id chained from environment
parent_id = "?" # from environment.application
management_group = self._create_management_group( display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
credentials, management_group_id, display_name, parent_id,
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( def create_atat_admin_user(
self, auth_credentials: Dict, csp_environment_id: str self, auth_credentials: Dict, csp_environment_id: str
@ -490,126 +657,135 @@ class AzureCloudProvider(CloudProviderInterface):
"role_name": role_assignment_id, "role_name": role_assignment_id,
} }
def _create_application(self, auth_credentials: Dict, application: Application): def create_tenant(self, payload):
management_group_name = str(uuid4()) # can be anything, not just uuid # auth as SP that is allowed to create tenant? (tenant creation sp creds)
display_name = application.name # Does this need to be unique? # create tenant with owner details (populated from portfolio point of contact, pw is generated)
credentials = self._get_credential_obj(auth_credentials)
parent_id = "?" # application.portfolio.csp_details.management_group_id
return self._create_management_group( # return tenant id, tenant owner id and tenant owner object id from:
credentials, management_group_name, display_name, parent_id, 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( def create_billing_owner(self, creds, tenant_admin_details):
self, credentials, management_group_id, display_name, parent_id=None, # authenticate as tenant_admin
): # create billing owner identity
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
)
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create # TODO: Lookup response format
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally # Managed service identity?
return create_request.result() response = {"id": "string"}
return self._ok({"billing_owner_id": response["id"]})
def _create_subscription( def assign_billing_owner(self, creds, billing_owner_id, tenant_id):
self, # TODO: Do we source role definition ID from config, api or self-defined?
credentials, # TODO: If from api,
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,
):
""" """
Requires credentials that have AZURE_MANAGEMENT_API {
specified as the resource. The Service Principal "principalId": "string",
specified in the credentials must have the "Resource "principalTenantId": "string",
Policy Contributor" role assigned with a scope at least "billingRoleDefinitionId": "string"
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
""" """
# TODO: which subscription would this be?
client = self.sdk.policy.PolicyClient(credentials, subscription_id)
definition = client.policy_definitions.models.PolicyDefinition( return self.ok()
policy_type=properties.get("policyType"),
mode=properties.get("mode"), def create_billing_profile(self, creds, tenant_admin_details, billing_owner_id):
display_name=properties.get("displayName"), # call billing profile creation endpoint, specifying owner
description=properties.get("description"), # Payload:
policy_rule=properties.get("policyRule"), """
parameters=properties.get("parameters"), {
"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( return self._ok()
policy_definition_name=name,
parameters=definition, def create_billing_alerts(self, TBD):
management_group_id=management_group_id, # 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): def _get_management_service_principal(self):
# we really should be using graph.microsoft.com, but i'm getting # 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) return sub_id_match.group(1)
def _get_credential_obj(self, creds, resource=None): def _get_credential_obj(self, creds, resource=None):
return self.sdk.credentials.ServicePrincipalCredentials( return self.sdk.credentials.ServicePrincipalCredentials(
client_id=creds.get("client_id"), client_id=creds.get("client_id"),
secret=creds.get("secret_key"), secret=creds.get("secret_key"),
@ -671,6 +848,27 @@ class AzureCloudProvider(CloudProviderInterface):
cloud_environment=self.sdk.cloud, 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 @property
def _root_creds(self): def _root_creds(self):
return { return {

View File

@ -2,4 +2,5 @@ from .portfolios import (
Portfolios, Portfolios,
PortfolioError, PortfolioError,
PortfolioDeletionApplicationsExistError, 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.database import db
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.domain.authz import Authorization from atst.domain.authz import Authorization
from atst.domain.portfolio_roles import PortfolioRoles 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 from .scopes import ScopedPortfolio
@ -17,7 +29,22 @@ class PortfolioDeletionApplicationsExistError(Exception):
pass 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): 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 @classmethod
def create(cls, user, portfolio_attrs): def create(cls, user, portfolio_attrs):
portfolio = PortfoliosQuery.create(**portfolio_attrs) portfolio = PortfoliosQuery.create(**portfolio_attrs)
@ -111,3 +138,37 @@ class Portfolios(object):
portfolio.description = new_data["description"] portfolio.description = new_data["description"]
PortfoliosQuery.add_and_commit(portfolio) 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, Status as ApplicationRoleStatus,
) )
from atst.models.application import Application 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): class PortfoliosQuery(Query):

View File

@ -14,7 +14,7 @@ SERVICE_BRANCHES = [
] ]
ENV_ROLE_NO_ACCESS = "No Access" 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) (ENV_ROLE_NO_ACCESS, ENV_ROLE_NO_ACCESS)
] ]

View File

@ -7,7 +7,7 @@ from wtforms.fields import (
HiddenField, HiddenField,
) )
from wtforms.fields.html5 import DateField 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 flask_wtf import FlaskForm
from numbers import Number from numbers import Number
@ -15,6 +15,7 @@ from .data import JEDI_CLIN_TYPES
from .fields import SelectField from .fields import SelectField
from .forms import BaseForm, remove_empty_string from .forms import BaseForm, remove_empty_string
from atst.utils.localization import translate from atst.utils.localization import translate
from .validators import REGEX_ALPHA_NUMERIC
from flask import current_app as app from flask import current_app as app
MAX_CLIN_AMOUNT = 1000000000 MAX_CLIN_AMOUNT = 1000000000
@ -116,7 +117,10 @@ class AttachmentForm(BaseForm):
filename = HiddenField( filename = HiddenField(
id="attachment_filename", id="attachment_filename",
validators=[ 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( object_name = HiddenField(
@ -124,7 +128,8 @@ class AttachmentForm(BaseForm):
validators=[ validators=[
Length( Length(
max=40, message=translate("forms.attachment.object_name.length_error") max=40, message=translate("forms.attachment.object_name.length_error")
) ),
Regexp(regex=REGEX_ALPHA_NUMERIC),
], ],
) )
accept = ".pdf,application/pdf" accept = ".pdf,application/pdf"

View File

@ -8,6 +8,9 @@ import pendulum
from atst.utils.localization import translate from atst.utils.localization import translate
REGEX_ALPHA_NUMERIC = "^[A-Za-z0-9\-_ \.]*$"
def DateRange(lower_bound=None, upper_bound=None, message=None): def DateRange(lower_bound=None, upper_bound=None, message=None):
def _date_range(form, field): def _date_range(form, field):
if field.data is None: if field.data is None:

View File

@ -7,14 +7,27 @@ from atst.models import (
EnvironmentJobFailure, EnvironmentJobFailure,
EnvironmentRoleJobFailure, EnvironmentRoleJobFailure,
EnvironmentRole, EnvironmentRole,
PortfolioJobFailure,
) )
from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.portfolios import Portfolios
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.models.utils import claim_for_update from atst.models.utils import claim_for_update
from atst.utils.localization import translate 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): class RecordEnvironmentFailure(celery.Task):
def on_failure(self, exc, task_id, args, kwargs, einfo): def on_failure(self, exc, task_id, args, kwargs, einfo):
if "environment_id" in kwargs: if "environment_id" in kwargs:
@ -125,6 +138,17 @@ def do_work(fn, task, csp, **kwargs):
raise task.retry(exc=e) 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) @celery.task(bind=True, base=RecordEnvironmentFailure)
def create_environment(self, environment_id=None): def create_environment(self, environment_id=None):
do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id) 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) @celery.task(bind=True)
def dispatch_create_environment(self): def dispatch_create_environment(self):
for environment_id in Environments.get_environments_pending_creation( 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 .clin import CLIN, JEDICLINType
from .environment import Environment from .environment import Environment
from .environment_role import EnvironmentRole, CSPRole 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 .notification_recipient import NotificationRecipient
from .permissions import Permissions from .permissions import Permissions
from .permission_set import PermissionSet from .permission_set import PermissionSet
from .portfolio import Portfolio from .portfolio import Portfolio
from .portfolio_state_machine import PortfolioStateMachine, FSMStates
from .portfolio_invitation import PortfolioInvitation from .portfolio_invitation import PortfolioInvitation
from .portfolio_role import PortfolioRole, Status as PortfolioRoleStatus from .portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
from .task_order import TaskOrder from .task_order import TaskOrder

View File

@ -9,10 +9,9 @@ import atst.models.types as types
class CSPRole(Enum): class CSPRole(Enum):
BASIC_ACCESS = "Basic Access" ADMIN = "Admin"
NETWORK_ADMIN = "Network Admin" BILLING_READ = "Billing Read-only"
BUSINESS_READ = "Business Read-only" CONTRIBUTOR = "Contributor"
TECHNICAL_READ = "Technical Read-only"
class EnvironmentRole( class EnvironmentRole(
@ -26,7 +25,7 @@ class EnvironmentRole(
) )
environment = relationship("Environment") environment = relationship("Environment")
role = Column(String()) role = Column(SQLAEnum(CSPRole, native_enum=False), nullable=True)
application_role_id = Column( application_role_id = Column(
UUID(as_uuid=True), ForeignKey("application_roles.id"), nullable=False 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" __tablename__ = "environment_role_job_failures"
environment_role_id = Column(ForeignKey("environment_roles.id"), nullable=False) 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 .deletable import DeletableMixin
from .invites import InvitesMixin from .invites import InvitesMixin
from .job_failure import JobFailureMixin 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.utils import first_or_none
from atst.database import db from atst.database import db
from sqlalchemy_json import NestedMutableJson
class Portfolio( class Portfolio(
Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin
@ -19,16 +21,31 @@ class Portfolio(
id = types.Id() id = types.Id()
name = Column(String, nullable=False) name = Column(String, nullable=False)
description = Column(String)
defense_component = Column( defense_component = Column(
ARRAY(String), nullable=False String, nullable=False
) # Department of Defense Component ) # 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( applications = relationship(
"Application", "Application",
back_populates="portfolio", back_populates="portfolio",
primaryjoin="and_(Application.portfolio_id == Portfolio.id, Application.deleted == False)", primaryjoin="and_(Application.portfolio_id == Portfolio.id, Application.deleted == False)",
) )
state_machine = relationship(
"PortfolioStateMachine", uselist=False, back_populates="portfolio"
)
roles = relationship("PortfolioRole") roles = relationship("PortfolioRole")
task_orders = relationship("TaskOrder") 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): def update_celery(celery, app):
celery.conf.update(app.config) celery.conf.update(app.config)
celery.conf.CELERYBEAT_SCHEDULE = { celery.conf.CELERYBEAT_SCHEDULE = {
"beat-dispatch_provision_portfolio": {
"task": "atst.jobs.dispatch_provision_portfolio",
"schedule": 60,
},
"beat-dispatch_create_environment": { "beat-dispatch_create_environment": {
"task": "atst.jobs.dispatch_create_environment", "task": "atst.jobs.dispatch_create_environment",
"schedule": 60, "schedule": 60,

View File

@ -78,7 +78,7 @@ def filter_env_roles_data(roles):
{ {
"environment_id": str(role.environment.id), "environment_id": str(role.environment.id),
"environment_name": role.environment.name, "environment_name": role.environment.name,
"role": role.role, "role": (role.role.value if role.role else "None"),
} }
for role in roles for role in roles
], ],
@ -99,8 +99,9 @@ def filter_env_roles_form_data(member, environments):
if len(env_roles_set) == 1: if len(env_roles_set) == 1:
(env_role,) = env_roles_set (env_role,) = env_roles_set
env_data["role"] = env_role.role
env_data["disabled"] = env_role.disabled env_data["disabled"] = env_role.disabled
if env_role.role:
env_data["role"] = env_role.role.name
env_roles_form_data.append(env_data) 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 = app.app_context()
ctx.push() 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 component = wrapper.find(uploadinput)
const event = { target: { value: '', files: [{ name: '' }] } } const event = { target: { value: '', files: [{ name: 'sample.pdf' }] } }
component.setMethods({ component.setMethods({
getUploader: async () => new MockUploader('token', 'objectName'), getUploader: async () => new MockUploader('token', 'objectName'),

View File

@ -1,5 +1,4 @@
import { emitFieldChange } from '../lib/emitters' import { emitFieldChange } from '../lib/emitters'
import escape from '../lib/escape'
import optionsinput from './options_input' import optionsinput from './options_input'
import textinput from './text_input' import textinput from './text_input'
import clindollaramount from './clin_dollar_amount' import clindollaramount from './clin_dollar_amount'
@ -100,7 +99,7 @@ export default {
computed: { computed: {
clinTitle: function() { clinTitle: function() {
if (!!this.clinNumber) { if (!!this.clinNumber) {
return escape(`CLIN ${this.clinNumber}`) return `CLIN ${this.clinNumber}`
} else { } else {
return `CLIN` return `CLIN`
} }

View File

@ -1,5 +1,6 @@
import { buildUploader } from '../lib/upload' import { buildUploader } from '../lib/upload'
import { emitFieldChange } from '../lib/emitters' import { emitFieldChange } from '../lib/emitters'
import inputValidations from '../lib/input_validations'
export default { export default {
name: 'uploadinput', name: 'uploadinput',
@ -28,6 +29,7 @@ export default {
changed: false, changed: false,
uploadError: false, uploadError: false,
sizeError: false, sizeError: false,
filenameError: false,
downloadLink: '', downloadLink: '',
} }
}, },
@ -50,6 +52,10 @@ export default {
this.sizeError = true this.sizeError = true
return return
} }
if (!this.validateFileName(file.name)) {
this.filenameError = true
return
}
const uploader = await this.getUploader() const uploader = await this.getUploader()
const response = await uploader.upload(file) const response = await uploader.upload(file)
@ -71,6 +77,10 @@ export default {
this.uploadError = true this.uploadError = true
} }
}, },
validateFileName: function(name) {
const regex = inputValidations.restrictedFileName.match
return regex.test(name)
},
removeAttachment: function(e) { removeAttachment: function(e) {
e.preventDefault() e.preventDefault()
this.attachment = null this.attachment = null
@ -118,7 +128,8 @@ export default {
return ( return (
(!this.changed && this.initialErrors) || (!this.changed && this.initialErrors) ||
this.uploadError || this.uploadError ||
this.sizeError this.sizeError ||
this.filenameError
) )
}, },
valid: function() { valid: function() {

View File

@ -104,4 +104,11 @@ export default {
unmask: ['(', ')', '-', ' '], unmask: ['(', ')', '-', ' '],
validationError: 'Please enter a 10-digit phone number', 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

@ -15,7 +15,7 @@ PASSWORD = os.getenv("ATAT_BA_PASSWORD", "")
DISABLE_VERIFY = os.getenv("DISABLE_VERIFY", "true").lower() == "true" DISABLE_VERIFY = os.getenv("DISABLE_VERIFY", "true").lower() == "true"
# Alpha numerics for random entity names # Alpha numerics for random entity names
LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" #pragma: allowlist secret LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" # pragma: allowlist secret
NEW_PORTFOLIO_CHANCE = 10 NEW_PORTFOLIO_CHANCE = 10
NEW_APPLICATION_CHANCE = 10 NEW_APPLICATION_CHANCE = 10
@ -29,10 +29,6 @@ def logout(l):
l.client.get("/logout") l.client.get("/logout")
def get_index(l):
l.client.get("/")
def get_csrf_token(response): def get_csrf_token(response):
d = pq(response.text) d = pq(response.text)
return d("#csrf_token").val() return d("#csrf_token").val()
@ -52,14 +48,9 @@ def extract_id(path):
def get_portfolios(l): def get_portfolios(l):
response = l.client.get("/portfolios") response = l.client.get("/home")
d = pq(response.text) d = pq(response.text)
portfolio_links = [ portfolio_links = [p.attr("href") for p in d(".sidenav__link").items()]
p.attr("href")
for p in d(
".global-panel-container .atat-table tbody tr td:first-child a"
).items()
]
force_new_portfolio = randrange(0, 100) < NEW_PORTFOLIO_CHANCE force_new_portfolio = randrange(0, 100) < NEW_PORTFOLIO_CHANCE
if len(portfolio_links) == 0 or force_new_portfolio: if len(portfolio_links) == 0 or force_new_portfolio:
portfolio_links += [create_portfolio(l)] portfolio_links += [create_portfolio(l)]
@ -73,7 +64,7 @@ def get_portfolio(l):
d = pq(response.text) d = pq(response.text)
application_links = [ application_links = [
p.attr("href") 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: if len(application_links) > 0:
portfolio_id = extract_id(portfolio_link) portfolio_id = extract_id(portfolio_link)
@ -161,18 +152,14 @@ class UserBehavior(TaskSequence):
login(self) login(self)
@seq_task(1) @seq_task(1)
def home(l):
get_index(l)
@seq_task(2)
def portfolios(l): def portfolios(l):
get_portfolios(l) get_portfolios(l)
@seq_task(3) @seq_task(2)
def pick_a_portfolio(l): def pick_a_portfolio(l):
get_portfolio(l) get_portfolio(l)
@seq_task(4) @seq_task(3)
def pick_an_app(l): def pick_an_app(l):
get_app(l) get_app(l)
@ -189,4 +176,3 @@ class WebsiteUser(HttpLocust):
if __name__ == "__main__": if __name__ == "__main__":
# if run as the main file, will spin up a single locust # if run as the main file, will spin up a single locust
WebsiteUser().run() WebsiteUser().run()

View File

@ -33,7 +33,7 @@ $title-font-size: 5.2rem;
$h1-font-size: 4rem; $h1-font-size: 4rem;
$h2-font-size: 3rem; $h2-font-size: 3rem;
$h3-font-size: 2.3rem; $h3-font-size: 2.3rem;
$h4-font-size: 1.7rem; $h4-font-size: 1.9rem;
$h5-font-size: 1.5rem; $h5-font-size: 1.5rem;
$h6-font-size: 1.3rem; $h6-font-size: 1.3rem;
$base-line-height: 1.5; $base-line-height: 1.5;
@ -44,6 +44,7 @@ $font-sans: "Source Sans Pro", sans-serif;
$font-serif: "Merriweather", serif; $font-serif: "Merriweather", serif;
$font-normal: 400; $font-normal: 400;
$font-semibold: 600;
$font-bold: 700; $font-bold: 700;
// Color // Color

View File

@ -23,7 +23,7 @@
inline-template> inline-template>
<div class="clin-card" v-if="showClin"> <div class="clin-card" v-if="showClin">
<div class="card__title"> <div class="card__title">
<span class="h4" v-html='clinTitle'></span> <span class="h4" v-text='clinTitle'></span>
<button <button
v-if='clinIndex > 0' v-if='clinIndex > 0'
class="icon-link icon-link__remove-clin" class="icon-link icon-link__remove-clin"
@ -119,7 +119,7 @@
{% endif %} {% endif %}
<div class="h5 clin-card__title">Percent Obligated</div> <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> <hr>
<div class="form-row"> <div class="form-row">
@ -140,7 +140,7 @@
<div class='modal__dialog' role='dialog' aria-modal='true'> <div class='modal__dialog' role='dialog' aria-modal='true'>
<div class='modal__body'> <div class='modal__body'>
<div class="task-order__modal-cancel"> <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"> <div class="task-order__modal-cancel_buttons">
<button <button
v-on:click='closeModal(removeModalId)' v-on:click='closeModal(removeModalId)'

View File

@ -15,7 +15,7 @@
<div> <div>
<div v-show="valid" class="uploaded-file"> <div v-show="valid" class="uploaded-file">
{{ Icon("ok") }} {{ 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> <a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
</div> </div>
<div v-show="valid === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'> <div v-show="valid === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
@ -49,6 +49,9 @@
<template v-if="sizeError"> <template v-if="sizeError">
<span class="usa-input__message">{{ "forms.task_order.size_error" | translate }}</span> <span class="usa-input__message">{{ "forms.task_order.size_error" | translate }}</span>
</template> </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() %} {% for error, error_messages in field.errors.items() %}
<span class="usa-input__message">{{error_messages[0]}}</span> <span class="usa-input__message">{{error_messages[0]}}</span>
{% endfor %} {% endfor %}

View File

@ -37,19 +37,19 @@
<tr> <tr>
<td> <td>
<button v-on:click='toggle($event, applicationIndex)' class='icon-link icon-link--large'> <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-if='application.isVisible'>{{ Icon('caret_down') }}</template>
<template v-else>{{ Icon('caret_up') }}</template> <template v-else>{{ Icon('caret_up') }}</template>
</button> </button>
</td> </td>
<td class="table-cell--align-right"> <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>
<td class="table-cell--align-right"> <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>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(application.total || 0)'></span> <span v-text='formatDollars(application.total || 0)'></span>
</td> </td>
</tr> </tr>
<tr <tr
@ -58,16 +58,16 @@
v-bind:class="[ index == application.environments.length -1 ? 'reporting-spend-table__env-row--last' : '']" v-bind:class="[ index == application.environments.length -1 ? 'reporting-spend-table__env-row--last' : '']"
> >
<td> <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>
<td class="table-cell--align-right"> <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>
<td class="table-cell--align-right"> <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>
<td class="table-cell--align-right"> <td class="table-cell--align-right">
<span v-html='formatDollars(environment.total || 0)'></span> <span v-text='formatDollars(environment.total || 0)'></span>
</td> </td>
</tr> </tr>
</template> </template>

View File

@ -16,6 +16,7 @@ from tests.factories import EnvironmentFactory, ApplicationFactory
# #
@pytest.mark.skip()
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider): def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create() environment = EnvironmentFactory.create()
@ -50,12 +51,14 @@ def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
assert result == subscription_id assert result == subscription_id
@pytest.mark.skip()
def mock_management_group_create(mock_azure, spec_dict): def mock_management_group_create(mock_azure, spec_dict):
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock( mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock(
**spec_dict **spec_dict
) )
@pytest.mark.skip()
def test_create_environment_succeeds(mock_azure: AzureCloudProvider): def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create() environment = EnvironmentFactory.create()
@ -68,6 +71,7 @@ def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
assert result.id == "Test Id" assert result.id == "Test Id"
@pytest.mark.skip()
def test_create_application_succeeds(mock_azure: AzureCloudProvider): def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create() application = ApplicationFactory.create()
@ -78,6 +82,7 @@ def test_create_application_succeeds(mock_azure: AzureCloudProvider):
assert result.id == "Test Id" assert result.id == "Test Id"
@pytest.mark.skip()
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider): def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
environment_id = str(uuid4()) 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 assert result.get("csp_user_id") == csp_user_id
@pytest.mark.skip()
def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider): def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
subscription_id = str(uuid4()) subscription_id = str(uuid4())
management_group_id = str(uuid4()) management_group_id = str(uuid4())

View File

@ -147,7 +147,7 @@ def test_invite():
user_data=user_data, user_data=user_data,
permission_sets_names=permission_sets_names, permission_sets_names=permission_sets_names,
environment_roles_data=[ 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}, {"environment_id": env2.id, "role": None},
], ],
) )
@ -173,8 +173,8 @@ def test_invite_to_nonexistent_environment():
inviter=application.portfolio.owner, inviter=application.portfolio.owner,
user_data=user_data, user_data=user_data,
environment_roles_data=[ environment_roles_data=[
{"environment_id": env1.id, "role": CSPRole.BASIC_ACCESS.value}, {"environment_id": env1.id, "role": CSPRole.ADMIN},
{"environment_id": uuid4(), "role": CSPRole.BASIC_ACCESS.value}, {"environment_id": uuid4(), "role": CSPRole.ADMIN},
], ],
) )

View File

@ -26,8 +26,8 @@ def test_create_environments():
def test_update_env_role(): def test_update_env_role():
env_role = EnvironmentRoleFactory.create(role=CSPRole.BASIC_ACCESS.value) env_role = EnvironmentRoleFactory.create(role=CSPRole.ADMIN)
new_role = CSPRole.TECHNICAL_READ.value new_role = CSPRole.BILLING_READ
Environments.update_env_role( Environments.update_env_role(
env_role.environment, env_role.application_role, new_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(): 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) Environments.update_env_role(env_role.environment, env_role.application_role, None)
assert not EnvironmentRoles.get( assert not EnvironmentRoles.get(
@ -46,15 +46,13 @@ def test_update_env_role_no_access():
def test_update_env_role_disabled_role(): 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) 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 # An exception should be raised when a new role is passed to Environments.update_env_role
with pytest.raises(DisabledError): with pytest.raises(DisabledError):
Environments.update_env_role( Environments.update_env_role(
env_role.environment, env_role.environment, env_role.application_role, CSPRole.BILLING_READ,
env_role.application_role,
CSPRole.TECHNICAL_READ.value,
) )
assert env_role.role is None 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 pytest
import random
from uuid import uuid4 from uuid import uuid4
from atst.domain.exceptions import NotFoundError, UnauthorizedError from atst.domain.exceptions import NotFoundError, UnauthorizedError
@ -7,6 +6,7 @@ from atst.domain.portfolios import (
Portfolios, Portfolios,
PortfolioError, PortfolioError,
PortfolioDeletionApplicationsExistError, PortfolioDeletionApplicationsExistError,
PortfolioStateMachines,
) )
from atst.domain.portfolio_roles import PortfolioRoles from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.applications import Applications 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.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS
from atst.models.application_role import Status as ApplicationRoleStatus from atst.models.application_role import Status as ApplicationRoleStatus
from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.models.portfolio_role import Status as PortfolioRoleStatus
from atst.models import FSMStates
from tests.factories import ( from tests.factories import (
ApplicationFactory, ApplicationFactory,
@ -22,6 +23,7 @@ from tests.factories import (
UserFactory, UserFactory,
PortfolioRoleFactory, PortfolioRoleFactory,
PortfolioFactory, PortfolioFactory,
PortfolioStateMachineFactory,
get_all_portfolio_permission_sets, 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( def test_scoped_portfolio_returns_all_applications_for_portfolio_admin(
portfolio, portfolio_owner portfolio, portfolio_owner
): ):
for _ in range(5): for i in range(5):
Applications.create( Applications.create(
portfolio.owner, portfolio.owner,
portfolio, portfolio,
"My Application %s" % (random.randrange(1, 1000)), f"My Application {i}",
"My application", "My application",
["dev", "staging", "prod"], ["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( def test_scoped_portfolio_returns_all_applications_for_portfolio_owner(
portfolio, portfolio_owner portfolio, portfolio_owner
): ):
for _ in range(5): for i in range(5):
Applications.create( Applications.create(
portfolio.owner, portfolio.owner,
portfolio, portfolio,
"My Application %s" % (random.randrange(1, 1000)), f"My Application {i}",
"My application", "My application",
["dev", "staging", "prod"], ["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 status=ApplicationRoleStatus.ACTIVE, user=user2, application=app, deleted=True
) )
assert len(Portfolios.for_user(user2)) == 0 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 model = EnvironmentRole
environment = factory.SubFactory(EnvironmentFactory) 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) application_role = factory.SubFactory(ApplicationRoleFactory)
@ -342,3 +342,17 @@ class NotificationRecipientFactory(Base):
model = NotificationRecipient model = NotificationRecipient
email = factory.Faker("email") 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( EnvironmentRoleFactory.create(
application_role=application_role, application_role=application_role,
environment=dev_environment, environment=dev_environment,
role=CSPRole.BASIC_ACCESS.value, role=CSPRole.ADMIN,
) )
assert developer in dev_environment.users 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(): def test_environment_roles_do_not_include_deleted():
member_list = [ member_list = [
{"role_name": CSPRole.BASIC_ACCESS.value}, {"role_name": CSPRole.ADMIN},
{"role_name": CSPRole.BASIC_ACCESS.value}, {"role_name": CSPRole.ADMIN},
{"role_name": CSPRole.BASIC_ACCESS.value}, {"role_name": CSPRole.ADMIN},
] ]
env = EnvironmentFactory.create(members=member_list) env = EnvironmentFactory.create(members=member_list)
role_1 = env.roles[0] 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( app_role = ApplicationRoleFactory.create(
user=user, application=environment.application user=user, application=environment.application
) )
EnvironmentRoleFactory.create( EnvironmentRoleFactory.create(application_role=app_role, environment=environment)
application_role=app_role, environment=environment, role="developer"
)
user_session(user) user_session(user)
response = client.get( response = client.get(
url_for("applications.access_environment", environment_id=environment.id) 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-dod_id": user.dod_id,
"user_data-email": user.email, "user_data-email": user.email,
"environment_roles-0-environment_id": env.id, "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-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id, "environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS, "environment_roles-1-role": NO_ACCESS,
@ -201,7 +201,7 @@ def test_post_update_member(client, user_session):
), ),
data={ data={
"environment_roles-0-environment_id": env.id, "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-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id, "environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS, "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] env = application.environments[0]
app_role1 = ApplicationRoleFactory.create(application=application) app_role1 = ApplicationRoleFactory.create(application=application)
env_role1 = EnvironmentRoleFactory.create( 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) app_role2 = ApplicationRoleFactory.create(application=application, user=None)
env_role2 = EnvironmentRoleFactory.create( 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) user_session(portfolio.owner)
@ -180,7 +180,7 @@ def test_get_members_data(app, client, user_session):
environments=[ environments=[
{ {
"name": "testing", "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_id": str(environment.id),
"environment_name": environment.name, "environment_name": environment.name,
"role": env_role.role, "role": env_role.role.value,
} }
] ]
assert member["role_status"] 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-dod_id": user.dod_id,
"user_data-email": user.email, "user_data-email": user.email,
"environment_roles-0-environment_id": env.id, "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-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id, "environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS, "environment_roles-1-role": NO_ACCESS,
@ -511,10 +511,10 @@ def test_update_member(client, user_session, session):
env_2 = EnvironmentFactory.create(application=application) env_2 = EnvironmentFactory.create(application=application)
# add user to two of the environments: env and env_1 # add user to two of the environments: env and env_1
updated_role = EnvironmentRoleFactory.create( 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( 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) user_session(application.portfolio.owner)
@ -528,13 +528,13 @@ def test_update_member(client, user_session, session):
), ),
data={ data={
"environment_roles-0-environment_id": env.id, "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-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id, "environment_roles-1-environment_id": env_1.id,
"environment_roles-1-environment_name": env_1.name, "environment_roles-1-environment_name": env_1.name,
"environment_roles-1-disabled": "True", "environment_roles-1-disabled": "True",
"environment_roles-2-environment_id": env_2.id, "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, "environment_roles-2-environment_name": env_2.name,
"perms_env_mgmt": True, "perms_env_mgmt": True,
"perms_team_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 environment_roles = application.roles[0].environment_roles
# check that the user has roles in the correct envs # check that the user has roles in the correct envs
assert len(environment_roles) == 3 assert len(environment_roles) == 3
assert updated_role.role == CSPRole.TECHNICAL_READ.value assert updated_role.role == CSPRole.CONTRIBUTOR
assert suspended_role.disabled 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-dod_id": user.dod_id,
"user_data-email": user.email, "user_data-email": user.email,
"environment_roles-0-environment_id": env.id, "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-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id, "environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS, "environment_roles-1-role": NO_ACCESS,
@ -718,7 +718,7 @@ def test_handle_create_member(monkeypatch, set_g, session):
assert job_mock.called assert job_mock.called
def test_handle_update_member(set_g): def test_handle_update_member_success(set_g):
user = UserFactory.create() user = UserFactory.create()
application = ApplicationFactory.create( application = ApplicationFactory.create(
environments=[{"name": "Naboo"}, {"name": "Endor"}] environments=[{"name": "Naboo"}, {"name": "Endor"}]
@ -732,7 +732,7 @@ def test_handle_update_member(set_g):
form_data = ImmutableMultiDict( form_data = ImmutableMultiDict(
{ {
"environment_roles-0-environment_id": env.id, "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-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id, "environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS, "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( form_data = ImmutableMultiDict(
{ {
"environment_roles-0-environment_id": env.id, "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-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id, "environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS, "environment_roles-1-role": NO_ACCESS,

View File

@ -213,7 +213,7 @@ def test_applications_access_environment_access(get_url_assert_status):
"environments": [ "environments": [
{ {
"name": "thebar", "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 threading import Thread
from atst.domain.csp.cloud import MockCloudProvider from atst.domain.csp.cloud import MockCloudProvider
from atst.domain.portfolios import Portfolios
from atst.jobs import ( from atst.jobs import (
RecordEnvironmentFailure, RecordEnvironmentFailure,
RecordEnvironmentRoleFailure, RecordEnvironmentRoleFailure,
do_create_environment,
do_create_atat_admin_user,
dispatch_create_environment, dispatch_create_environment,
dispatch_create_atat_admin_user, dispatch_create_atat_admin_user,
create_environment, dispatch_provision_portfolio,
dispatch_provision_user, dispatch_provision_user,
create_environment,
do_provision_user, do_provision_user,
do_provision_portfolio,
do_create_environment,
do_create_atat_admin_user,
) )
from atst.models.utils import claim_for_update from atst.models.utils import claim_for_update
from atst.domain.exceptions import ClaimFailedException from atst.domain.exceptions import ClaimFailedException
@ -22,9 +26,10 @@ from tests.factories import (
EnvironmentFactory, EnvironmentFactory,
EnvironmentRoleFactory, EnvironmentRoleFactory,
PortfolioFactory, PortfolioFactory,
PortfolioStateMachineFactory,
ApplicationRoleFactory, ApplicationRoleFactory,
) )
from atst.models import EnvironmentRole, ApplicationRoleStatus from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus
@pytest.fixture(autouse=True, scope="function") @pytest.fixture(autouse=True, scope="function")
@ -32,6 +37,12 @@ def csp():
return Mock(wraps=MockCloudProvider({}, with_delay=False, with_failure=False)) 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): def test_environment_job_failure(celery_app, celery_worker):
@celery_app.task(bind=True, base=RecordEnvironmentFailure) @celery_app.task(bind=True, base=RecordEnvironmentFailure)
def _fail_hard(self, environment_id=None): 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): def test_dispatch_provision_user(csp, session, celery_app, celery_worker, monkeypatch):
# Given that I have four environment roles: # Given that I have four environment roles:
# (A) one of which has a completed status # (A) one of which has a completed status
# (B) one of which has an environment that has not been provisioned # (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_role = EnvironmentRoleFactory.create(
environment=provisioned_environment, environment=provisioned_environment,
status=EnvironmentRole.Status.PENDING, status=EnvironmentRole.Status.PENDING,
role="my_role", role="ADMIN",
) )
# When I call the user provisoning task # When I call the user provisoning task
@ -302,7 +314,33 @@ def test_do_provision_user(csp, session):
session.refresh(environment_role) session.refresh(environment_role)
# I expect that the CSP create_or_update_user method will be called # I expect that the CSP create_or_update_user method will be called
csp.create_or_update_user.assert_called_once_with( 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 # I expect that the EnvironmentRole now has a csp_user_id
assert environment_role.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: 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. 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. 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 defense_component_label: Select DoD component(s) funding your Portfolio
file_format_not_allowed: Only PDF or PNG files can be uploaded. file_format_not_allowed: Only PDF or PNG files can be uploaded.
number_description: Task order number (13 digits) number_description: Task order number (13 digits)

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -176,7 +176,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -188,7 +188,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -490,12 +490,12 @@ Imported from: AT-AT CI - New App Step 1-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
@ -595,12 +595,12 @@ Imported from: AT-AT CI - New App Step 1-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -111,7 +111,7 @@
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -182,7 +182,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -291,12 +291,12 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Portfolio Settings--> <!--Imported from: AT-AT CI - Portfolio Settings-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=td.name</td> <td>css=th.table-cell--third</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <td>assertElementPresent</td>
<td>css=td.name</td> <td>css=th.table-cell--third</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -313,7 +313,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=button.usa-button.usa-button-primary.usa-button-big</td> <td>css=button.usa-button.usa-button-primary.usa-button-big</td>
<td>Save</td> <td>Save Changes</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -336,30 +336,14 @@ Imported from: AT-AT CI - New Portfolio-->
<td></td> <td></td>
<td></td> <td></td>
</tr> </tr>
<!--Imported from: AT-AT CI - Portfolio Settings-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=input.usa-button.usa-button-primary</td> <td>css=a.usa-button.usa-button-secondary.add-new-button</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></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
@ -369,13 +353,13 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</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>*Invite new portfolio member*</td> <td>*Add Manager*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -459,13 +443,13 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</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>*Assign member permissions*</td> <td>*Set Portfolio Permissions*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -474,12 +458,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_app_mgmt</td> <td>css=#perms_app_mgmt-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=#permission_sets-perms_app_mgmt</td> <td>css=#perms_app_mgmt-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -489,12 +473,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td> <td>css=#perms_funding-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td> <td>css=#perms_funding-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -504,12 +488,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_funding</td> <td>css=#perms_reporting-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=#permission_sets-perms_funding</td> <td>css=#perms_reporting-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -519,57 +503,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_funding > option:nth-of-type(1)</td> <td>css=#perms_portfolio_mgmt-None</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></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#permission_sets-perms_portfolio_mgmt</td> <td>css=#perms_portfolio_mgmt-None</td>
<td>edit_portfolio_admin</td> <td>edit_portfolio_admin</td>
</tr> </tr>
<tr> <tr>
@ -579,21 +518,6 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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>css=input[type="submit"].action-group__action</td>
<td></td> <td></td>
</tr> </tr>
@ -609,27 +533,27 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.name</td> <td>css=table.atat-table > tbody > tr > td > span.label.label--success.label--below</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></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*You have successfully invited Brandon Buchannan to the portfolio.*</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -219,12 +219,12 @@ Imported from: AT-AT CI - login-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=td.name</td> <td>css=th.table-cell--third</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <td>assertElementPresent</td>
<td>css=td.name</td> <td>css=th.table-cell--third</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -240,7 +240,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=button.usa-button.usa-button-primary.usa-button-big</td> <td>css=button.usa-button.usa-button-primary.usa-button-big</td>
<td>Save</td> <td>Save Changes</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -257,21 +257,6 @@ Imported from: AT-AT CI - login-->
<td>css=button.usa-button.usa-button-primary</td> <td>css=button.usa-button.usa-button-primary</td>
<td>*Update*</td> <td>*Update*</td>
</tr> </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> </tbody>
</table> </table>
</body> </body>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -609,12 +609,12 @@ Ghost Inspector flow control because button tends to take a little while to beco
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$100,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -624,15 +624,13 @@ Ghost Inspector flow control because button tends to take a little while to beco
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</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>*October 01, 2019 <td>*October 01, 2019 - June 30, 2020*</td>
-
June 30, 2020*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -641,12 +639,12 @@ Ghost Inspector flow control because button tends to take a little while to beco
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*Days Remaining*</td>
</tr> </tr>
<tr> <tr>
@ -662,7 +660,7 @@ Ghost Inspector flow control because button tends to take a little while to beco
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=h3.h5</td> <td>css=h3.h5</td>
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td> <td>*IDIQ CLIN 0002*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -219,12 +219,12 @@ Imported from: AT-AT CI - login-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$0.00*</td>
</tr> </tr>
<tr> <tr>
@ -234,12 +234,12 @@ Imported from: AT-AT CI - login-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*0 days*</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -646,12 +646,12 @@ Imported from: AT-AT CI - Create New TO-->
<!--Imported from: AT-AT CI - Reports - Basics--> <!--Imported from: AT-AT CI - Reports - Basics-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$100,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -662,15 +662,13 @@ Imported from: AT-AT CI - Create New TO-->
<!--Imported from: AT-AT CI - Reports - Basics--> <!--Imported from: AT-AT CI - Reports - Basics-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</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>*October 01, 2019 <td>*October 01, 2019 - June 30, 2020*</td>
-
June 30, 2020*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -680,12 +678,12 @@ Imported from: AT-AT CI - Create New TO-->
<!--Imported from: AT-AT CI - Reports - Basics--> <!--Imported from: AT-AT CI - Reports - Basics-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*Days Remaining*</td>
</tr> </tr>
<tr> <tr>
@ -702,7 +700,7 @@ Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=h3.h5</td> <td>css=h3.h5</td>
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td> <td>*IDIQ CLIN 0002*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -646,12 +646,12 @@ Imported from: AT-AT CI - Create New TO-->
<!--Imported from: AT-AT CI - Reports - Basics--> <!--Imported from: AT-AT CI - Reports - Basics-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$100,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -662,15 +662,13 @@ Imported from: AT-AT CI - Create New TO-->
<!--Imported from: AT-AT CI - Reports - Basics--> <!--Imported from: AT-AT CI - Reports - Basics-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</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>*October 01, 2019 <td>*October 01, 2019 - June 30, 2020*</td>
-
June 30, 2020*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -680,12 +678,12 @@ Imported from: AT-AT CI - Create New TO-->
<!--Imported from: AT-AT CI - Reports - Basics--> <!--Imported from: AT-AT CI - Reports - Basics-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*Days Remaining*</td>
</tr> </tr>
<tr> <tr>
@ -702,7 +700,7 @@ Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=h3.h5</td> <td>css=h3.h5</td>
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td> <td>*IDIQ CLIN 0002*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -828,7 +826,7 @@ Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Task order details*</td> <td>*Task Order #*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -837,12 +835,12 @@ Imported from: AT-AT CI - Create New TO-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$100,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -852,29 +850,14 @@ Imported from: AT-AT CI - Create New TO-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$800,000.00*</td>
</tr> </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> </tbody>
</table> </table>
</body> </body>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -1387,12 +1387,12 @@ Ghost Inspector flow control because button tends to take a little while to beco
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$100,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -1402,15 +1402,13 @@ Ghost Inspector flow control because button tends to take a little while to beco
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</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>*October 01, 2019 <td>*October 01, 2019 - June 30, 2020*</td>
-
June 30, 2020*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -1425,7 +1423,7 @@ Ghost Inspector flow control because button tends to take a little while to beco
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=h3.h5</td> <td>css=h3.h5</td>
<td>*IDIQ CLIN 0002 Classified IaaS/PaaS*</td> <td>*IDIQ CLIN 0002*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -626,12 +626,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*Brandon*</td>
</tr> </tr>
<tr> <tr>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -186,7 +186,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -117,7 +117,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -123,7 +123,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -129,7 +129,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -135,7 +135,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -135,7 +135,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -727,12 +727,27 @@ Imported from: AT-AT CI - TO Step 2-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$100,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -742,12 +757,12 @@ Imported from: AT-AT CI - TO Step 2-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$800,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -757,13 +772,13 @@ Imported from: AT-AT CI - TO Step 2-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <td>verifyText</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> <td>*$0.00*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -141,7 +141,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -769,12 +769,28 @@ Imported from: AT-AT CI - TO Step 3-->
<!--Imported from: AT-AT CI - TO Step 4--> <!--Imported from: AT-AT CI - TO Step 4-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$100,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -785,12 +801,12 @@ Imported from: AT-AT CI - TO Step 3-->
<!--Imported from: AT-AT CI - TO Step 4--> <!--Imported from: AT-AT CI - TO Step 4-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <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> <td>*$800,000.00*</td>
</tr> </tr>
<tr> <tr>
@ -801,13 +817,13 @@ Imported from: AT-AT CI - TO Step 3-->
<!--Imported from: AT-AT CI - TO Step 4--> <!--Imported from: AT-AT CI - TO Step 4-->
<tr> <tr>
<td>waitForElementPresent</td> <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> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <td>verifyText</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> <td>*$0.00*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -998,7 +1014,7 @@ Imported from: AT-AT CI - TO Step 3-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.usa-alert-text</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>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">