mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
fix: review fixes for the restore fix
- A restore of the repeaters alone saves them before the reboot, instead of leaving that to somfy.loop(). - e2e: a refused restore leaves no upload behind, tested on its own. - e2e: auth() sits next to upload(), whose file part is now file=. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d45ba59c1
commit
d968f7e70c
3 changed files with 25 additions and 13 deletions
|
|
@ -608,7 +608,9 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename,
|
|||
else {
|
||||
this->file.seek(this->file.position() + this->header.netRecordSize, SeekSet);
|
||||
}
|
||||
if(opts.shades) s->commit();
|
||||
// The repeaters are saved in shades.cfg too. Left to somfy.loop(), they
|
||||
// race the reboot.
|
||||
if(opts.shades || opts.repeaters) s->commit();
|
||||
if(opts.transceiver)
|
||||
{
|
||||
// Read into a copy so the radio task never transmits on a half restored config.
|
||||
|
|
|
|||
|
|
@ -8,17 +8,13 @@ import pytest
|
|||
|
||||
from proto_client import run_sync
|
||||
from test_commands import is_ok, op, synced_model
|
||||
from test_routes import http, upload
|
||||
from test_routes import auth, http, restore_without_a_file, upload
|
||||
from test_settings import desc, settings, uptime, wait_for_restart
|
||||
|
||||
# Saved for the round trip and put back by the restore. .invalid never resolves.
|
||||
CHANGED_NTP_SERVER = "restore.e2e.invalid"
|
||||
|
||||
|
||||
def auth(token):
|
||||
return {"apikey": token} if token else {}
|
||||
|
||||
|
||||
def test_restore_reads_the_options_sent_as_a_form_field(host, token):
|
||||
"""The options come as the form field "data", next to the file.
|
||||
|
||||
|
|
@ -32,6 +28,15 @@ def test_restore_reads_the_options_sent_as_a_form_field(host, token):
|
|||
assert json.loads(body)["desc"] == "JSON parse error"
|
||||
|
||||
|
||||
def test_a_refused_restore_leaves_no_upload_behind(host, token):
|
||||
"""A later /restore without a file must not find the refused one's upload."""
|
||||
status, _, _ = upload(host, "/restore", auth(token), fields={"data": "{"})
|
||||
assert status == 500
|
||||
status, reason = restore_without_a_file(host, token)
|
||||
assert status == 400
|
||||
assert "didn't complete" in reason, reason
|
||||
|
||||
|
||||
def test_a_restore_brings_back_the_parts_that_were_ticked(request, host, token, connect, schema):
|
||||
if not request.config.getoption("--reboot"):
|
||||
pytest.skip("restores a backup and reboots the device: pass --reboot")
|
||||
|
|
@ -48,7 +53,7 @@ def test_a_restore_brings_back_the_parts_that_were_ticked(request, host, token,
|
|||
assert is_ok(schema, reply), desc(schema, reply)
|
||||
try:
|
||||
options = json.dumps({"shades": False, "settings": True})
|
||||
status, _, body = upload(host, "/restore", auth(token), content=backup, fields={"data": options})
|
||||
status, _, body = upload(host, "/restore", auth(token), file=backup, fields={"data": options})
|
||||
assert status == 200, body
|
||||
wait_for_restart(host, token, schema, uptime_before)
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -38,18 +38,23 @@ def http(host, path, method="GET", body=None, headers=None):
|
|||
return e.code, e.headers, e.read()
|
||||
|
||||
|
||||
def upload(host, path, headers=None, content=b"\0" * 256, fields=None):
|
||||
def auth(token):
|
||||
"""The header that carries the token, if there is one."""
|
||||
return {"apikey": token} if token else {}
|
||||
|
||||
|
||||
def upload(host, path, headers=None, file=b"\0" * 256, fields=None):
|
||||
"""Posts a form as the web UI does: the file, then the fields.
|
||||
|
||||
The file is by default a small one that isn't a backup or an image, and
|
||||
content=None leaves it out.
|
||||
file=None leaves it out.
|
||||
"""
|
||||
boundary = "e2e-upload"
|
||||
parts = []
|
||||
if content is not None:
|
||||
if file is not None:
|
||||
parts.append(
|
||||
f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"e2e.bin\"\r\n"
|
||||
"Content-Type: application/octet-stream\r\n\r\n".encode() + content)
|
||||
"Content-Type: application/octet-stream\r\n\r\n".encode() + file)
|
||||
for name, value in (fields or {}).items():
|
||||
parts.append(f"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}".encode())
|
||||
body = b"\r\n".join(parts) + f"\r\n--{boundary}--\r\n".encode()
|
||||
|
|
@ -95,7 +100,7 @@ def test_secured_refuses_a_web_asset_name_below_a_route(host, secured, method, p
|
|||
def test_secured_accepts_the_token_on_a_route(host, secured, token):
|
||||
if not secured:
|
||||
pytest.skip("the device's security is None")
|
||||
status, _, _ = http(host, "/backup", headers={"apikey": token})
|
||||
status, _, _ = http(host, "/backup", headers=auth(token))
|
||||
assert status == 200
|
||||
|
||||
|
||||
|
|
@ -109,7 +114,7 @@ def test_secured_refuses_an_upload_without_a_token(host, secured, path):
|
|||
|
||||
|
||||
def restore_without_a_file(host, token):
|
||||
status, _, body = upload(host, "/restore", {"apikey": token}, content=None, fields={"e2e": "1"})
|
||||
status, _, body = upload(host, "/restore", auth(token), file=None, fields={"e2e": "1"})
|
||||
return status, json.loads(body or b"{}").get("desc", "")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue