@@ -67,6 +67,14 @@ async def post(self):
6767 return self .postdata
6868
6969
70+ class FakeGetRequest :
71+ """A minimal aiohttp-request stand-in exposing only the query string the handler reads."""
72+
73+ def __init__ (self , query ):
74+ """Store the query parameters this request will hand back."""
75+ self .query = query
76+
77+
7078def _load_yaml (path = "apps.yaml" ):
7179 """Load a YAML file from the temporary working directory."""
7280 yaml = YAML ()
@@ -502,6 +510,111 @@ def run_web_apps_edit_tests(my_predbat):
502510 print (" ERROR: the rotated credential was not written" )
503511 failed += 1
504512
513+ # ---------------------------------------------------------------------
514+ # The page serving the mask meant clicking Edit on a credential filled the input with
515+ # "xxx": the real key could not be seen or amended, only retyped from scratch. The page
516+ # stays masked; Edit fetches the one real value through /apps_value instead.
517+ print ("Test: credential rows are flagged for reveal-on-edit and still served masked" )
518+ text = _render_apps_page (my_predbat , {"octopus_api_key" : "sk-top-level-credential-0000" , "chat" : {"providers" : {"openrouter" : {"api_key" : "sk-live-nested-credential-1234" , "model" : "some-model" }}}})
519+ if "sk-top-level-credential-0000" in text or "sk-live-nested-credential-1234" in text :
520+ print (" ERROR: a credential reached the rendered page in the clear" )
521+ failed += 1
522+ if not re .search (r'<tr id="row_\d+" data-arg-name="octopus_api_key" data-original-value="xxx" data-secret="1">' , text ):
523+ print (" ERROR: the top-level credential row should be masked and flagged data-secret" )
524+ failed += 1
525+ if not re .search (r"data-nested-path='chat\.providers\.openrouter\.api_key' data-nested-original='xxx' data-secret='1'" , text ):
526+ print (" ERROR: the nested credential row should be masked and flagged data-secret" )
527+ failed += 1
528+ if re .search (r"data-nested-path='chat\.providers\.openrouter\.model'[^>]*data-secret" , text ):
529+ print (" ERROR: a non-credential sibling must not be flagged data-secret" )
530+ failed += 1
531+
532+ print ("Test: /apps_value hands back the real value of the credential being edited" )
533+ web_interface = _reset_fixture (my_predbat )
534+ for path , expected in (("chat.providers.openrouter.api_key" , "sk-live-nested-credential-1234" ), ("forecast_solar[0].api_key" , "fs-live-credential-5678" ), ("forecast_solar[0].declination" , "30" )):
535+ result = json .loads (asyncio .run (web_interface .html_apps_value (FakeGetRequest ({"path" : path }))).text )
536+ if not result .get ("success" ) or result .get ("value" ) != expected :
537+ print (" ERROR: expected /apps_value for {} to return {}, got: {}" .format (path , expected , result ))
538+ failed += 1
539+
540+ print ("Test: /apps_value refuses a missing path or a whole structure" )
541+ for query in ({}, {"path" : "chat.providers.nope" }, {"path" : "chat.providers" }):
542+ result = json .loads (asyncio .run (web_interface .html_apps_value (FakeGetRequest (query ))).text )
543+ if result .get ("success" ) or "value" in result :
544+ print (" ERROR: expected /apps_value to refuse {}, got: {}" .format (query , result ))
545+ failed += 1
546+
547+ print ("Test: Edit reveals a flagged credential before opening the input" )
548+ for function , attribute in (("editValue" , "originalValue" ), ("editNestedValue" , "nestedOriginal" )):
549+ edit_src = apps_js [apps_js .index ("function {}(" .format (function )) :]
550+ edit_src = edit_src [: edit_src .index ("\n }\n " )]
551+ if "row.dataset.secret === '1'" not in edit_src or "revealSecretValue(row, '{}'" .format (attribute ) not in edit_src :
552+ print (" ERROR: expected {} to fetch the real value of a data-secret row, got:\n {}" .format (function , edit_src ))
553+ failed += 1
554+ reveal_src = apps_js [apps_js .index ("async function revealSecretValue(" ) :]
555+ reveal_src = reveal_src [: reveal_src .index ("\n }\n " )]
556+ if "fetch('./apps_value?path=' + encodeURIComponent(path))" not in reveal_src or "delete row.dataset.secret" not in reveal_src :
557+ print (" ERROR: expected revealSecretValue to fetch /apps_value and replace the masked original, got:\n {}" .format (reveal_src ))
558+ failed += 1
559+
560+ # A revealed credential is the first value that routinely holds quotes, braces or angle
561+ # brackets to reach the editor - the mask "xxx" never did (#5243 review)
562+ print ("Test: a credential holding quote, brace and angle-bracket characters round-trips through reveal and save" )
563+ awkward = 'pa"ss{word<x>&{0}'
564+ web_interface = _reset_fixture (my_predbat )
565+ web_interface .args ["chat" ]["providers" ]["openrouter" ]["api_key" ] = awkward
566+ path = "chat.providers.openrouter.api_key"
567+ result = json .loads (asyncio .run (web_interface .html_apps_value (FakeGetRequest ({"path" : path }))).text )
568+ if not result .get ("success" ) or result .get ("value" ) != awkward :
569+ print (" ERROR: expected /apps_value to return the brace-bearing credential verbatim, got: {}" .format (result ))
570+ failed += 1
571+ else :
572+ result = _post_changes (web_interface , {path : {"rowId" : 1001 , "originalValue" : awkward , "newValue" : awkward + "2" , "type" : "string" , "isNested" : True , "path" : path }})
573+ if not result .get ("success" ) or _load_yaml ()["pred_bat" ]["chat" ]["providers" ]["openrouter" ]["api_key" ] != awkward + "2" :
574+ print (" ERROR: the edited quote-bearing credential was not written intact, got: {}" .format (result ))
575+ failed += 1
576+
577+ print ("Test: a digit-only credential saved as a revealed row keeps its leading zeros" )
578+ web_interface = _reset_fixture (my_predbat )
579+ path = "forecast_solar[0].api_key"
580+ result = _post_changes (web_interface , {path : {"rowId" : 1001 , "originalValue" : "0042" , "newValue" : "00430" , "type" : "string" , "isNested" : True , "path" : path }})
581+ if not result .get ("success" ) or str (_load_yaml ()["pred_bat" ]["forecast_solar" ][0 ]["api_key" ]) != "00430" :
582+ print (" ERROR: expected the credential to be written as the string 00430, got: {} / {}" .format (result , _load_yaml ()["pred_bat" ]["forecast_solar" ][0 ]["api_key" ]))
583+ failed += 1
584+
585+ print ("Test: /apps_value refuses a negative index into an empty list rather than raising" )
586+ web_interface = _reset_fixture (my_predbat )
587+ web_interface .args ["forecast_solar" ] = []
588+ result = json .loads (asyncio .run (web_interface .html_apps_value (FakeGetRequest ({"path" : "forecast_solar[-1].api_key" }))).text )
589+ if result .get ("success" ) or "value" in result :
590+ print (" ERROR: expected a refusal for an index into an empty list, got: {}" .format (result ))
591+ failed += 1
592+
593+ print ("Test: the editor sets a revealed value as a property and treats it as plain text" )
594+ for function in ("editValue" , "editNestedValue" ):
595+ edit_src = apps_js [apps_js .index ("function {}(" .format (function )) :]
596+ edit_src = edit_src [: edit_src .index ("\n }\n " )]
597+ if 'value="${currentValue}"' in edit_src or "input.value = currentValue" not in edit_src :
598+ print (" ERROR: expected {} to assign input.value rather than interpolate the value into markup, got:\n {}" .format (function , edit_src ))
599+ failed += 1
600+ if "row.dataset.revealed !== '1' && currentValue" not in edit_src :
601+ print (" ERROR: expected {} to skip the entity dropdown for a revealed credential, got:\n {}" .format (function , edit_src ))
602+ failed += 1
603+ for function in ("saveValue" , "saveNestedValue" ):
604+ save_src = apps_js [apps_js .index ("function {}(" .format (function )) :]
605+ save_src = save_src [: save_src .index ("\n }\n " )]
606+ if "row.dataset.revealed === '1' ? 'string'" not in save_src :
607+ print (" ERROR: expected {} to save a revealed credential as a string, got:\n {}" .format (function , save_src ))
608+ failed += 1
609+ if "row.dataset.revealed = '1'" not in reveal_src :
610+ print (" ERROR: expected revealSecretValue to mark the row as revealed" )
611+ failed += 1
612+ display_src = apps_js [apps_js .index ("function getDisplayValueEntity(" ) :]
613+ display_src = display_src [: display_src .index ("\n }\n " )]
614+ if display_src .count ("escapeHtml(" ) != 2 :
615+ print (" ERROR: expected getDisplayValueEntity to escape both of its returns, got:\n {}" .format (display_src ))
616+ failed += 1
617+
505618 finally :
506619 os .chdir (original_dir )
507620 shutil .rmtree (temp_dir , ignore_errors = True )
0 commit comments