fix(test/endpoints): add pytest coverage for all Microdot routes

This commit is contained in:
pi
2026-03-26 00:39:41 +13:00
parent 63235c7822
commit fed312a397
9 changed files with 996 additions and 123 deletions

View File

@@ -15,6 +15,7 @@ adafruit-ampy = "*"
microdot = "*"
[dev-packages]
pytest = "*"
[requires]
python_version = "3.12"
@@ -25,3 +26,4 @@ watch = "python -m watchfiles 'python tests/web.py' src tests"
install = "pipenv install"
run = "sh -c 'cd src && python main.py'"
dev = "watchfiles \"sh -c 'cd src && python main.py'\" src"
help-pdf = "sh scripts/build_help_pdf.sh"

303
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "12b64c3bf5857d958f790f2416072408e2244631242ba2598210d89df330e184"
"sha256": "6cec0fe6dec67c9177363a558131f333153b6caa47e1ddeca303cb0d19954cf8"
},
"pipfile-spec": 6,
"requires": {
@@ -26,19 +26,19 @@
},
"anyio": {
"hashes": [
"sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703",
"sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"
"sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708",
"sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"
],
"markers": "python_version >= '3.9'",
"version": "==4.12.1"
"markers": "python_version >= '3.10'",
"version": "==4.13.0"
},
"attrs": {
"hashes": [
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
"sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
"sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309",
"sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"
],
"markers": "python_version >= '3.9'",
"version": "==25.4.0"
"version": "==26.1.0"
},
"bitarray": {
"hashes": [
@@ -257,122 +257,138 @@
},
"charset-normalizer": {
"hashes": [
"sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4",
"sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66",
"sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54",
"sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05",
"sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765",
"sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064",
"sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819",
"sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e",
"sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412",
"sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc",
"sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e",
"sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281",
"sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af",
"sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2",
"sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe",
"sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8",
"sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262",
"sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac",
"sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85",
"sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c",
"sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf",
"sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139",
"sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770",
"sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d",
"sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918",
"sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3",
"sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7",
"sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39",
"sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d",
"sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990",
"sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765",
"sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1",
"sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa",
"sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659",
"sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d",
"sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9",
"sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9",
"sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2",
"sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d",
"sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475",
"sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c",
"sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81",
"sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67",
"sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99",
"sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5",
"sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694",
"sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf",
"sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca",
"sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c",
"sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c",
"sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636",
"sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f",
"sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02",
"sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497",
"sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f",
"sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2",
"sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d",
"sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873",
"sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a",
"sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e",
"sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1",
"sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123",
"sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550",
"sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc",
"sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36",
"sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644",
"sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4",
"sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0",
"sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e",
"sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f",
"sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4",
"sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98",
"sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294",
"sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22",
"sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23",
"sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8",
"sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2",
"sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362",
"sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242",
"sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4",
"sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95",
"sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d",
"sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94",
"sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6",
"sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2",
"sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4",
"sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8",
"sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e",
"sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a",
"sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce",
"sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969",
"sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f",
"sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923",
"sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6",
"sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee",
"sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6",
"sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467",
"sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f",
"sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193",
"sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7",
"sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9",
"sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95",
"sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763",
"sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7",
"sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98",
"sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60",
"sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade",
"sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c",
"sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2",
"sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f",
"sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a",
"sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947",
"sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3"
"sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e",
"sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c",
"sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5",
"sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815",
"sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f",
"sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0",
"sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484",
"sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407",
"sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6",
"sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8",
"sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264",
"sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815",
"sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2",
"sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4",
"sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579",
"sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f",
"sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa",
"sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95",
"sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab",
"sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297",
"sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a",
"sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e",
"sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84",
"sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8",
"sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0",
"sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9",
"sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f",
"sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1",
"sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843",
"sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565",
"sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7",
"sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c",
"sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b",
"sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7",
"sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687",
"sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9",
"sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14",
"sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89",
"sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f",
"sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0",
"sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9",
"sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a",
"sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389",
"sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0",
"sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30",
"sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd",
"sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e",
"sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9",
"sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc",
"sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532",
"sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d",
"sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae",
"sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2",
"sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64",
"sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f",
"sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557",
"sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e",
"sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff",
"sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398",
"sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db",
"sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a",
"sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43",
"sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597",
"sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c",
"sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e",
"sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2",
"sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54",
"sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e",
"sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4",
"sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4",
"sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7",
"sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6",
"sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5",
"sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194",
"sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69",
"sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f",
"sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316",
"sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e",
"sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73",
"sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8",
"sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923",
"sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88",
"sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f",
"sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21",
"sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4",
"sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6",
"sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc",
"sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2",
"sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866",
"sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021",
"sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2",
"sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d",
"sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8",
"sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de",
"sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237",
"sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4",
"sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778",
"sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb",
"sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc",
"sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602",
"sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4",
"sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f",
"sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5",
"sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611",
"sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8",
"sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf",
"sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d",
"sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b",
"sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db",
"sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e",
"sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077",
"sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd",
"sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef",
"sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e",
"sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8",
"sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe",
"sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058",
"sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17",
"sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833",
"sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421",
"sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550",
"sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff",
"sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2",
"sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc",
"sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982",
"sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d",
"sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed",
"sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104",
"sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"
],
"markers": "python_version >= '3.7'",
"version": "==3.4.5"
"version": "==3.4.6"
},
"click": {
"hashes": [
@@ -897,5 +913,46 @@
"version": "==1.3.2"
}
},
"develop": {}
"develop": {
"iniconfig": {
"hashes": [
"sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730",
"sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"
],
"markers": "python_version >= '3.10'",
"version": "==2.3.0"
},
"packaging": {
"hashes": [
"sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4",
"sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"
],
"markers": "python_version >= '3.8'",
"version": "==26.0"
},
"pluggy": {
"hashes": [
"sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3",
"sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"
],
"markers": "python_version >= '3.9'",
"version": "==1.6.0"
},
"pygments": {
"hashes": [
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
],
"markers": "python_version >= '3.8'",
"version": "==2.19.2"
},
"pytest": {
"hashes": [
"sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b",
"sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"
],
"index": "pypi",
"version": "==9.0.2"
}
}
}

4
pytest.ini Normal file
View File

@@ -0,0 +1,4 @@
[pytest]
testpaths = tests
python_files = test_endpoints_pytest.py

182
tests/async_tcp_server.py Normal file
View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# Standalone async TCP server (stdlib only). Multiple simultaneous clients.
# No watchdog: runs on a full host (e.g. Raspberry Pi); ESP32 clients may use WDT.
# For RTT latency, clients may send lines like ``rtt 12345`` (ticks); they are echoed back.
#
# Run from anywhere (default: all IPv4 interfaces, port 9000):
# python3 async_tcp_server.py
# python3 async_tcp_server.py --port 9000
# Localhost only:
# python3 async_tcp_server.py --host 127.0.0.1
#
# Or from this directory:
# chmod +x async_tcp_server.py && ./async_tcp_server.py
import argparse
import asyncio
import time
class _ClientRegistry:
"""Track writers and broadcast newline-terminated lines to all clients."""
def __init__(self) -> None:
self._writers: set[asyncio.StreamWriter] = set()
def add(self, writer: asyncio.StreamWriter) -> None:
self._writers.add(writer)
def remove(self, writer: asyncio.StreamWriter) -> None:
self._writers.discard(writer)
def count(self) -> int:
return len(self._writers)
async def broadcast_line(self, line: str) -> None:
data = (line.rstrip("\r\n") + "\n").encode("utf-8")
for writer in list(self._writers):
try:
writer.write(data)
await writer.drain()
except Exception as e:
print(f"[tcp] broadcast failed, dropping client: {e}")
self._writers.discard(writer)
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
async def _periodic_broadcast(
registry: _ClientRegistry,
interval_sec: float,
message: str,
) -> None:
while True:
await asyncio.sleep(interval_sec)
if registry.count() == 0:
continue
line = message.format(t=time.time())
print(f"[tcp] broadcast to {registry.count()} client(s): {line!r}")
await registry.broadcast_line(line)
async def _handle_client(
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
registry: _ClientRegistry,
) -> None:
peer = writer.get_extra_info("peername")
print(f"[tcp] connected: {peer}")
registry.add(writer)
try:
while not reader.at_eof():
data = await reader.readline()
if not data:
break
message = data.decode("utf-8", errors="replace").rstrip("\r\n")
# Echo newline-delimited lines (simple test harness behaviour).
# Clients may send ``rtt <ticks>`` for round-trip timing; echo unchanged.
t0 = time.perf_counter()
writer.write((message + "\n").encode("utf-8"))
await writer.drain()
if message.startswith("rtt "):
server_ms = (time.perf_counter() - t0) * 1000.0
print(
f"[tcp] echoed rtt from {peer} "
f"(host write+drain ~{server_ms:.2f} ms)"
)
finally:
registry.remove(writer)
writer.close()
await writer.wait_closed()
print(f"[tcp] disconnected: {peer}")
def _make_client_handler(registry: _ClientRegistry):
async def _handler(
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
) -> None:
await _handle_client(reader, writer, registry)
return _handler
async def _run(
host: str,
port: int,
broadcast_interval: float | None,
broadcast_message: str,
) -> None:
registry = _ClientRegistry()
handler = _make_client_handler(registry)
server = await asyncio.start_server(handler, host, port)
print(f"[tcp] listening on {host}:{port} (Ctrl+C to stop)")
if broadcast_interval is not None and broadcast_interval > 0:
print(
f"[tcp] periodic broadcast every {broadcast_interval}s "
f"(use {{t}} in --message for unix time)"
)
async with server:
tasks = []
if broadcast_interval is not None and broadcast_interval > 0:
tasks.append(
asyncio.create_task(
_periodic_broadcast(registry, broadcast_interval, broadcast_message),
name="broadcast",
)
)
try:
if tasks:
await asyncio.gather(server.serve_forever(), *tasks)
else:
await server.serve_forever()
finally:
for t in tasks:
t.cancel()
for t in tasks:
try:
await t
except asyncio.CancelledError:
pass
def main() -> None:
parser = argparse.ArgumentParser(
description="Standalone asyncio TCP server (multiple connections).",
)
parser.add_argument(
"--host",
default="0.0.0.0",
help="bind address (default: all IPv4 interfaces)",
)
parser.add_argument("--port", type=int, default=9000, help="bind port")
parser.add_argument(
"--interval",
type=float,
default=5.0,
metavar="SEC",
help="seconds between broadcast lines to all clients (default: 5)",
)
parser.add_argument(
"--message",
default="ping {t:.0f}",
help='broadcast line (newline added); use "{t}" for time.time() (default: %(default)s)',
)
parser.add_argument(
"--no-broadcast",
action="store_true",
help="disable periodic broadcast (echo-only)",
)
args = parser.parse_args()
interval = None if args.no_broadcast else args.interval
try:
asyncio.run(_run(args.host, args.port, interval, args.message))
except KeyboardInterrupt:
print("\n[tcp] stopped")
if __name__ == "__main__":
main()

12
tests/conftest.py Normal file
View File

@@ -0,0 +1,12 @@
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SRC_PATH = PROJECT_ROOT / "src"
LIB_PATH = PROJECT_ROOT / "lib"
for p in (str(SRC_PATH), str(LIB_PATH), str(PROJECT_ROOT)):
if p in sys.path:
sys.path.remove(p)
sys.path.insert(0, p)

View File

@@ -7,6 +7,15 @@ On Pi OS Lite (no desktop) these tests are skipped unless headless Chromium
and chromedriver are installed (e.g. chromium-browser chromium-chromedriver).
"""
import os
import pytest
if os.environ.get("LED_CONTROLLER_RUN_BROWSER_TESTS") != "1":
pytest.skip(
"Legacy device browser automation script; enable explicitly to run.",
allow_module_level=True,
)
import sys
import time
import requests

View File

@@ -4,6 +4,15 @@ Endpoint tests that mimic web browser requests.
Tests run against the device at 192.168.4.1
"""
import os
import pytest
if os.environ.get("LED_CONTROLLER_RUN_DEVICE_ENDPOINT_TESTS") != "1":
pytest.skip(
"Legacy device integration endpoint tests; enable explicitly to run.",
allow_module_level=True,
)
import requests
import json
import sys

View File

@@ -0,0 +1,594 @@
import asyncio
import builtins
import json
import os
import sys
import threading
import time
import uuid
from pathlib import Path
from typing import Any, Dict, Optional
import pytest
import requests
# Ensure imports resolve to the repo's `src/` + `lib/` code.
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SRC_PATH = PROJECT_ROOT / "src"
LIB_PATH = PROJECT_ROOT / "lib"
for p in (str(SRC_PATH), str(LIB_PATH), str(PROJECT_ROOT)):
if p in sys.path:
sys.path.remove(p)
sys.path.insert(0, p)
from microdot import Microdot, send_file # noqa: E402
from microdot.session import Session # noqa: E402
from microdot.websocket import with_websocket # noqa: E402
class DummySender:
def __init__(self):
self.sent: list[tuple[str, Optional[str]]] = []
async def send(self, data: Any, addr: Optional[str] = None):
if isinstance(data, (bytes, bytearray)):
data = bytes(data).decode(errors="ignore")
self.sent.append((data, addr))
return True
def _json(resp: requests.Response) -> Dict[str, Any]:
# Many endpoints already set Content-Type; but be tolerant for now.
return resp.json() # pragma: no cover
def _find_id_by_field(list_resp_json: Dict[str, Any], field: str, value: str) -> str:
for obj_id, data in list_resp_json.items():
if isinstance(data, dict) and data.get(field) == value:
return str(obj_id)
raise AssertionError(f"Could not find id for {field}={value!r}")
def _start_microdot_server(app: Microdot, host: str, port: int):
"""
Start Microdot server on a background thread.
Returns (thread, chosen_port).
"""
def runner():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(app.start_server(host=host, port=port))
finally:
try:
loop.close()
except Exception:
pass
thread = threading.Thread(target=runner, daemon=True)
thread.start()
# Poll until the socket is bound and app.server is available.
chosen_port = None
deadline = time.time() + 5.0
while time.time() < deadline:
server = getattr(app, "server", None)
if server and getattr(server, "sockets", None):
sockets = server.sockets or []
if sockets:
chosen_port = sockets[0].getsockname()[1]
break
time.sleep(0.05)
if chosen_port is None:
raise RuntimeError("Microdot server failed to start in time")
return thread, chosen_port
@pytest.fixture(scope="function")
def server(monkeypatch, tmp_path_factory):
"""
Start the Microdot app in-process and return a test client.
"""
tmp_root = tmp_path_factory.mktemp("endpoint-tests")
tmp_db_dir = tmp_root / "db"
tmp_settings_file = tmp_root / "settings.json"
# Be defensive: pytest runners can sometimes alter sys.path ordering.
for p in (str(SRC_PATH), str(LIB_PATH), str(PROJECT_ROOT)):
if p in sys.path:
sys.path.remove(p)
sys.path.insert(0, p)
# Patch Settings so endpoint tests never touch real `settings.json`.
import settings as settings_mod # noqa: E402
settings_mod.Settings.SETTINGS_FILE = str(tmp_settings_file)
# Patch the Model db directory so endpoint CRUD is isolated.
import models.model as model_mod # noqa: E402
monkeypatch.setattr(model_mod, "_db_dir", lambda: str(tmp_db_dir))
# Reset model singletons (controllers instantiate model classes at import time).
# Import the classes first so we can delete their `_instance` attribute if present.
import models.preset as models_preset # noqa: E402
import models.profile as models_profile # noqa: E402
import models.group as models_group # noqa: E402
import models.tab as models_tab # noqa: E402
import models.pallet as models_pallet # noqa: E402
import models.scene as models_scene # noqa: E402
import models.pattern as models_pattern # noqa: E402
import models.squence as models_sequence # noqa: E402
for cls in (
models_preset.Preset,
models_profile.Profile,
models_group.Group,
models_tab.Tab,
models_pallet.Palette,
models_scene.Scene,
models_pattern.Pattern,
models_sequence.Sequence,
):
if hasattr(cls, "_instance"):
delattr(cls, "_instance")
# Patch open() so pattern definitions work after we `chdir` into src/.
orig_open = builtins.open
def patched_open(file, *args, **kwargs):
if isinstance(file, str):
# Pattern controller loads definitions from a relative db/ path.
if file in {"db/pattern.json", "pattern.json", "/db/pattern.json"}:
file = str(PROJECT_ROOT / "db" / "pattern.json")
return orig_open(file, *args, **kwargs)
monkeypatch.setattr(builtins, "open", patched_open)
old_cwd = os.getcwd()
os.chdir(str(SRC_PATH))
dummy_sender = DummySender()
try:
# Ensure controllers are imported fresh after our patching.
for mod_name in (
"controllers.preset",
"controllers.profile",
"controllers.group",
"controllers.sequence",
"controllers.tab",
"controllers.palette",
"controllers.scene",
"controllers.pattern",
"controllers.settings",
):
sys.modules.pop(mod_name, None)
# Import controllers after patching db/settings/model singletons.
import controllers.preset as preset_ctl # noqa: E402
import controllers.profile as profile_ctl # noqa: E402
import controllers.group as group_ctl # noqa: E402
import controllers.sequence as sequence_ctl # noqa: E402
import controllers.tab as tab_ctl # noqa: E402
import controllers.palette as palette_ctl # noqa: E402
import controllers.scene as scene_ctl # noqa: E402
import controllers.pattern as pattern_ctl # noqa: E402
import controllers.settings as settings_ctl # noqa: E402
# Configure transport sender used by /presets/send.
from models.transport import set_sender # noqa: E402
set_sender(dummy_sender)
app = Microdot()
# Session secret key comes from settings (patched to tmp).
settings = settings_mod.Settings()
secret_key = settings.get(
"session_secret_key",
"led-controller-secret-key-change-in-production",
)
Session(app, secret_key=secret_key)
# Mount model controllers under their public prefixes.
app.mount(preset_ctl.controller, "/presets")
app.mount(profile_ctl.controller, "/profiles")
app.mount(group_ctl.controller, "/groups")
app.mount(sequence_ctl.controller, "/sequences")
app.mount(tab_ctl.controller, "/tabs")
app.mount(palette_ctl.controller, "/palettes")
app.mount(scene_ctl.controller, "/scenes")
app.mount(pattern_ctl.controller, "/patterns")
app.mount(settings_ctl.controller, "/settings")
@app.route("/")
def index(request):
return send_file("templates/index.html")
@app.route("/settings")
def settings_page(request):
return send_file("templates/settings.html")
@app.route("/favicon.ico")
def favicon(request):
return "", 204
@app.route("/static/<path:path>")
def static_handler(request, path):
if ".." in path:
return "Not found", 404
return send_file("static/" + path)
@app.route("/ws")
@with_websocket
async def ws(request, ws):
# Minimal websocket handler: forward raw JSON/text payloads to dummy sender.
while True:
data = await ws.receive()
if not data:
break
try:
parsed = json.loads(data)
addr = parsed.pop("to", None)
payload = json.dumps(parsed) if parsed else data
await dummy_sender.send(payload, addr=addr)
except Exception:
await dummy_sender.send(data)
thread, chosen_port = _start_microdot_server(app, host="127.0.0.1", port=0)
base_url = f"http://127.0.0.1:{chosen_port}"
client = requests.Session()
client.headers.update(
{
"User-Agent": "pytest/requests",
"Accept": "application/json",
}
)
yield {
"base_url": base_url,
"client": client,
"sender": dummy_sender,
"thread": thread,
"app": app,
}
finally:
# Stop server cleanly.
try:
app = locals().get("app")
if app is not None:
app.shutdown()
except Exception:
pass
# Give it a moment to close sockets.
time.sleep(0.1)
try:
thread = locals().get("thread")
if thread is not None:
thread.join(timeout=5)
except Exception:
pass
os.chdir(old_cwd)
def test_main_routes(server):
c: requests.Session = server["client"]
base_url: str = server["base_url"]
resp = c.get(f"{base_url}/")
assert resp.status_code == 200
assert "LED Controller" in resp.text
resp = c.get(f"{base_url}/favicon.ico")
assert resp.status_code == 204
resp = c.get(f"{base_url}/static/style.css")
assert resp.status_code == 200
resp = c.get(f"{base_url}/settings/page")
assert resp.status_code == 200
assert "LED Controller" in resp.text
resp = c.get(f"{base_url}/ws")
# WebSocket endpoints should reject non-upgraded HTTP requests.
assert resp.status_code != 200
assert resp.status_code in {400, 401, 403, 404, 405, 426}
def test_settings_controller(server):
c: requests.Session = server["client"]
base_url: str = server["base_url"]
resp = c.get(f"{base_url}/settings")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, dict)
assert "wifi_channel" in data
resp = c.get(f"{base_url}/settings/wifi/ap")
assert resp.status_code == 200
ap = resp.json()
assert "saved_ssid" in ap
assert "active" in ap
unique_ssid = f"pytest-ssid-{uuid.uuid4().hex[:8]}"
resp = c.post(
f"{base_url}/settings/wifi/ap",
json={"ssid": unique_ssid, "password": "secret", "channel": 1},
)
assert resp.status_code == 200
msg = resp.json()
assert msg["ssid"] == unique_ssid
assert msg["channel"] == 1
resp = c.post(
f"{base_url}/settings/wifi/ap",
json={"ssid": "bad-ssid", "password": "secret", "channel": 12},
)
assert resp.status_code == 400
resp = c.put(f"{base_url}/settings/settings", json={"wifi_channel": 11})
assert resp.status_code == 200
resp = c.put(f"{base_url}/settings/settings", json={"wifi_channel": 12})
assert resp.status_code == 400
def test_profiles_presets_tabs_endpoints(server):
c: requests.Session = server["client"]
base_url: str = server["base_url"]
sender: DummySender = server["sender"]
unique_profile_name = f"pytest-profile-{uuid.uuid4().hex[:8]}"
resp = c.post(f"{base_url}/profiles", json={"name": unique_profile_name})
assert resp.status_code == 201
created = resp.json()
assert isinstance(created, dict)
profile_id = next(iter(created.keys()))
resp = c.post(f"{base_url}/profiles/{profile_id}/apply")
assert resp.status_code == 200
resp = c.get(f"{base_url}/profiles/current")
assert resp.status_code == 200
current = resp.json()
assert str(current["id"]) == str(profile_id)
# Presets CRUD (scoped to current profile session).
resp = c.get(f"{base_url}/presets")
assert resp.status_code == 200
presets = resp.json()
assert isinstance(presets, dict)
assert presets # seeded presets should exist
first_preset_id = next(iter(presets.keys()))
resp = c.get(f"{base_url}/presets/{first_preset_id}")
assert resp.status_code == 200
assert resp.json() # dict
unique_preset_name = f"pytest-preset-{uuid.uuid4().hex[:8]}"
resp = c.post(
f"{base_url}/presets",
json={
"name": unique_preset_name,
"pattern": "on",
"colors": ["#ff0000"],
"brightness": 123,
"delay": 100,
},
)
assert resp.status_code == 201
created_preset = resp.json()
new_preset_id = next(iter(created_preset.keys()))
assert created_preset[new_preset_id]["profile_id"] == str(profile_id)
resp = c.put(
f"{base_url}/presets/{new_preset_id}",
json={"brightness": 77},
)
assert resp.status_code == 200
assert resp.json()["brightness"] == 77
sender.sent.clear()
resp = c.post(
f"{base_url}/presets/send",
json={"preset_ids": [new_preset_id], "save": False},
)
assert resp.status_code == 200
sent_result = resp.json()
assert sent_result["presets_sent"] >= 1
assert len(sender.sent) >= 1
resp = c.delete(f"{base_url}/presets/{new_preset_id}")
assert resp.status_code == 200
resp = c.get(f"{base_url}/presets/{new_preset_id}")
assert resp.status_code == 404
# Tabs CRUD (scoped to current profile session).
unique_tab_name = f"pytest-tab-{uuid.uuid4().hex[:8]}"
resp = c.post(
f"{base_url}/tabs",
json={"name": unique_tab_name, "names": ["1", "2"]},
)
assert resp.status_code == 201
created_tabs = resp.json()
tab_id = next(iter(created_tabs.keys()))
resp = c.get(f"{base_url}/tabs/{tab_id}")
assert resp.status_code == 200
assert resp.json()["name"] == unique_tab_name
resp = c.post(f"{base_url}/tabs/{tab_id}/set-current")
assert resp.status_code == 200
resp = c.get(f"{base_url}/tabs/current")
assert resp.status_code == 200
assert resp.json()["tab_id"] == str(tab_id)
resp = c.put(
f"{base_url}/tabs/{tab_id}",
json={"name": f"{unique_tab_name}-updated", "names": ["3"]},
)
assert resp.status_code == 200
assert resp.json()["names"] == ["3"]
resp = c.post(f"{base_url}/tabs/{tab_id}/clone", json={"name": "pytest-tab-clone"})
assert resp.status_code == 201
clone_payload = resp.json()
clone_id = next(iter(clone_payload.keys()))
resp = c.get(f"{base_url}/tabs/{clone_id}")
assert resp.status_code == 200
resp = c.delete(f"{base_url}/tabs/{clone_id}")
assert resp.status_code == 200
resp = c.delete(f"{base_url}/tabs/{tab_id}")
assert resp.status_code == 200
# Profile clone + update endpoints.
clone_name = f"pytest-profile-clone-{uuid.uuid4().hex[:8]}"
resp = c.post(f"{base_url}/profiles/{profile_id}/clone", json={"name": clone_name})
assert resp.status_code == 201
cloned = resp.json()
clone_profile_id = next(iter(cloned.keys()))
resp = c.post(f"{base_url}/profiles/{clone_profile_id}/apply")
assert resp.status_code == 200
resp = c.put(
f"{base_url}/profiles/current",
json={"name": f"{clone_name}-updated"},
)
assert resp.status_code == 200
resp = c.put(
f"{base_url}/profiles/{clone_profile_id}",
json={"name": f"{clone_name}-updated-2"},
)
assert resp.status_code == 200
resp = c.delete(f"{base_url}/profiles/{clone_profile_id}")
assert resp.status_code == 200
resp = c.delete(f"{base_url}/profiles/{profile_id}")
assert resp.status_code == 200
def test_groups_sequences_scenes_palettes_patterns_endpoints(server):
c: requests.Session = server["client"]
base_url: str = server["base_url"]
# Groups.
unique_group_name = f"pytest-group-{uuid.uuid4().hex[:8]}"
resp = c.post(f"{base_url}/groups", json={"name": unique_group_name})
assert resp.status_code == 201
groups_list = c.get(f"{base_url}/groups").json()
group_id = _find_id_by_field(groups_list, "name", unique_group_name)
resp = c.get(f"{base_url}/groups/{group_id}")
assert resp.status_code == 200
assert resp.json()["name"] == unique_group_name
resp = c.put(f"{base_url}/groups/{group_id}", json={"brightness": 10})
assert resp.status_code == 200
assert resp.json()["brightness"] == 10
resp = c.delete(f"{base_url}/groups/{group_id}")
assert resp.status_code == 200
# Sequences.
unique_seq_group_name = f"pytest-seq-group-{uuid.uuid4().hex[:8]}"
resp = c.post(
f"{base_url}/sequences",
json={"group_name": unique_seq_group_name, "presets": []},
)
assert resp.status_code == 201
sequences_list = c.get(f"{base_url}/sequences").json()
seq_id = _find_id_by_field(sequences_list, "group_name", unique_seq_group_name)
resp = c.get(f"{base_url}/sequences/{seq_id}")
assert resp.status_code == 200
resp = c.put(f"{base_url}/sequences/{seq_id}", json={"sequence_duration": 1234})
assert resp.status_code == 200
assert resp.json()["sequence_duration"] == 1234
resp = c.delete(f"{base_url}/sequences/{seq_id}")
assert resp.status_code == 200
# Scenes.
unique_scene_name = f"pytest-scene-{uuid.uuid4().hex[:8]}"
resp = c.post(f"{base_url}/scenes", json={"name": unique_scene_name})
assert resp.status_code == 201
scenes_list = c.get(f"{base_url}/scenes").json()
scene_id = _find_id_by_field(scenes_list, "name", unique_scene_name)
resp = c.get(f"{base_url}/scenes/{scene_id}")
assert resp.status_code == 200
resp = c.put(f"{base_url}/scenes/{scene_id}", json={"name": unique_scene_name + "-updated"})
assert resp.status_code == 200
assert resp.json()["name"].endswith("-updated")
resp = c.delete(f"{base_url}/scenes/{scene_id}")
assert resp.status_code == 200
# Palettes.
colors = ["#112233", "#445566"]
resp = c.post(f"{base_url}/palettes", json={"colors": colors})
assert resp.status_code == 201
palette_payload = resp.json()
palette_id = str(palette_payload["id"])
resp = c.get(f"{base_url}/palettes/{palette_id}")
assert resp.status_code == 200
assert resp.json()["id"] == palette_id
resp = c.put(f"{base_url}/palettes/{palette_id}", json={"colors": ["#000000"]})
assert resp.status_code == 200
assert resp.json()["colors"] == ["#000000"]
resp = c.delete(f"{base_url}/palettes/{palette_id}")
assert resp.status_code == 200
# Patterns.
resp = c.get(f"{base_url}/patterns/definitions")
assert resp.status_code == 200
definitions = resp.json()
assert isinstance(definitions, dict)
pattern_id = f"pytest_pattern_{uuid.uuid4().hex[:8]}"
resp = c.post(
f"{base_url}/patterns",
json={"name": pattern_id, "data": {"foo": "bar"}},
)
assert resp.status_code == 201
assert resp.json()["foo"] == "bar"
resp = c.get(f"{base_url}/patterns")
assert resp.status_code == 200
patterns_list = resp.json()
assert pattern_id in patterns_list
resp = c.get(f"{base_url}/patterns/{pattern_id}")
assert resp.status_code == 200
assert resp.json()["foo"] == "bar"
resp = c.put(f"{base_url}/patterns/{pattern_id}", json={"baz": 1})
assert resp.status_code == 200
assert resp.json()["baz"] == 1
resp = c.delete(f"{base_url}/patterns/{pattern_id}")
assert resp.status_code == 200

View File

@@ -1,3 +1,7 @@
import pytest
pytest.skip("Legacy manual server script (not a pytest suite).", allow_module_level=True)
from microdot import Microdot
from src.profile import profile_app