mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
fix: update propagation node sync endpoints to use POST method and improve database log handling
This commit is contained in:
parent
57da414bcc
commit
8f381c8693
11 changed files with 107 additions and 25 deletions
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -204,8 +204,13 @@ def register_lxmf_routes(routes, app):
|
|||
# sync propagation node
|
||||
|
||||
# sync propagation node
|
||||
@routes.get("/api/v1/lxmf/propagation-node/sync")
|
||||
@routes.post("/api/v1/lxmf/propagation-node/sync")
|
||||
async def propagation_node_sync(request):
|
||||
from meshchatx.src.backend.demo_mode import demo_mode_block_response
|
||||
|
||||
blocked = demo_mode_block_response(app)
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
# ensure propagation node is configured before attempting to sync
|
||||
outbound_node = app.message_router.get_outbound_propagation_node()
|
||||
if outbound_node is None:
|
||||
|
|
@ -234,8 +239,13 @@ def register_lxmf_routes(routes, app):
|
|||
# stop syncing propagation node
|
||||
|
||||
# stop syncing propagation node
|
||||
@routes.get("/api/v1/lxmf/propagation-node/stop-sync")
|
||||
@routes.post("/api/v1/lxmf/propagation-node/stop-sync")
|
||||
async def propagation_node_stop_sync(request):
|
||||
from meshchatx.src.backend.demo_mode import demo_mode_block_response
|
||||
|
||||
blocked = demo_mode_block_response(app)
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
app.stop_propagation_node_sync()
|
||||
|
||||
return web.json_response(
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ class PersistentLogHandler(logging.Handler):
|
|||
|
||||
def set_database(self, database):
|
||||
with self.lock:
|
||||
if (
|
||||
self.database is not None
|
||||
and database is not None
|
||||
and self.database is not database
|
||||
and self.logs_buffer
|
||||
):
|
||||
self._flush_to_db()
|
||||
self.database = database
|
||||
|
||||
def emit(self, record):
|
||||
|
|
|
|||
|
|
@ -2109,7 +2109,7 @@ export default {
|
|||
// continue to sync
|
||||
}
|
||||
}
|
||||
await window.api.get("/api/v1/lxmf/propagation-node/sync");
|
||||
await window.api.post("/api/v1/lxmf/propagation-node/sync");
|
||||
} catch (e) {
|
||||
this.userInitiatedPropagationSync = false;
|
||||
const errorMessage =
|
||||
|
|
@ -2202,7 +2202,7 @@ export default {
|
|||
async stopSyncingPropagationNode() {
|
||||
const propagationSyncToastKey = "propagation-sync-status";
|
||||
try {
|
||||
await window.api.get("/api/v1/lxmf/propagation-node/stop-sync");
|
||||
await window.api.post("/api/v1/lxmf/propagation-node/stop-sync");
|
||||
} catch {
|
||||
// do nothing on error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3138,15 +3138,21 @@ export default {
|
|||
this.discovery.discoverable = this.parseBool(config.discoverable);
|
||||
}
|
||||
if (config.discovery_name) this.discovery.discovery_name = config.discovery_name;
|
||||
if (config.announce_interval) this.discovery.announce_interval = Number(config.announce_interval);
|
||||
if (config.announce_interval != null && config.announce_interval !== "") {
|
||||
this.discovery.announce_interval = Number(config.announce_interval);
|
||||
}
|
||||
if (config.reachable_on) this.discovery.reachable_on = config.reachable_on;
|
||||
if (config.discovery_stamp_value)
|
||||
this.discovery.discovery_stamp_value = Number(config.discovery_stamp_value);
|
||||
if (config.discovery_encrypt !== undefined)
|
||||
this.discovery.discovery_encrypt = this.parseBool(config.discovery_encrypt);
|
||||
if (config.publish_ifac !== undefined) this.discovery.publish_ifac = this.parseBool(config.publish_ifac);
|
||||
if (config.latitude) this.discovery.latitude = Number(config.latitude);
|
||||
if (config.longitude) this.discovery.longitude = Number(config.longitude);
|
||||
if (config.latitude != null && config.latitude !== "") {
|
||||
this.discovery.latitude = Number(config.latitude);
|
||||
}
|
||||
if (config.longitude != null && config.longitude !== "") {
|
||||
this.discovery.longitude = Number(config.longitude);
|
||||
}
|
||||
if (config.height) this.discovery.height = Number(config.height);
|
||||
if (config.location_cmd) this.discovery.location_cmd = String(config.location_cmd);
|
||||
|
||||
|
|
|
|||
|
|
@ -115,3 +115,32 @@ def test_log_cleanup(handler, db):
|
|||
|
||||
count = db.debug_logs.get_total_count()
|
||||
assert count <= 11 # 10 + the trigger log
|
||||
|
||||
|
||||
def test_set_database_flushes_buffered_logs_to_previous_database(tmp_path):
|
||||
db_a_file = tmp_path / "a.db"
|
||||
db_b_file = tmp_path / "b.db"
|
||||
db_a = Database(str(db_a_file))
|
||||
db_b = Database(str(db_b_file))
|
||||
db_a.initialize()
|
||||
db_b.initialize()
|
||||
|
||||
handler = PersistentLogHandler(database=db_a, flush_interval=3600)
|
||||
logger = logging.getLogger("identity_a_logger")
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
logger.info("identity A buffered log")
|
||||
assert len(handler.logs_buffer) == 1
|
||||
|
||||
handler.set_database(db_b)
|
||||
logger.info("identity B log")
|
||||
handler._flush_to_db()
|
||||
|
||||
logs_a = db_a.debug_logs.get_logs(limit=10)
|
||||
logs_b = db_b.debug_logs.get_logs(limit=10)
|
||||
assert any("identity A buffered log" in row["message"] for row in logs_a)
|
||||
assert all("identity A buffered log" not in row["message"] for row in logs_b)
|
||||
assert any("identity B log" in row["message"] for row in logs_b)
|
||||
|
||||
logger.removeHandler(handler)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ from tests.backend.demo_http_support import build_test_aio_app
|
|||
("POST", "/api/v1/identities/create"),
|
||||
("DELETE", "/api/v1/lxmf-messages/aa"),
|
||||
("PATCH", "/api/v1/server/security"),
|
||||
("POST", "/api/v1/lxmf/propagation-node/sync"),
|
||||
("POST", "/api/v1/lxmf/propagation-node/stop-sync"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ async def test_remote_propagation_sync_transitions_path_requested_to_complete(
|
|||
if request_count[key] >= 2:
|
||||
known_paths.add(key)
|
||||
|
||||
sync_handler = _route_handler(app, "/api/v1/lxmf/propagation-node/sync")
|
||||
sync_handler = _route_handler(app, "/api/v1/lxmf/propagation-node/sync", method="POST")
|
||||
status_handler = _route_handler(app, "/api/v1/lxmf/propagation-node/status")
|
||||
|
||||
with (
|
||||
|
|
@ -175,7 +175,7 @@ async def test_local_preferred_propagation_sync_completes_without_remote_lookup(
|
|||
local_hash = fake_router.propagation_destination.hash
|
||||
fake_router.set_outbound_propagation_node(local_hash)
|
||||
|
||||
sync_handler = _route_handler(app, "/api/v1/lxmf/propagation-node/sync")
|
||||
sync_handler = _route_handler(app, "/api/v1/lxmf/propagation-node/sync", method="POST")
|
||||
status_handler = _route_handler(app, "/api/v1/lxmf/propagation-node/status")
|
||||
|
||||
with (
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ async def test_lxmf_sync_endpoints(mock_app):
|
|||
# 2. Test sync initiation
|
||||
sync_handler = None
|
||||
for route in mock_app.get_routes():
|
||||
if route.path == "/api/v1/lxmf/propagation-node/sync" and route.method == "GET":
|
||||
if route.path == "/api/v1/lxmf/propagation-node/sync" and route.method == "POST":
|
||||
sync_handler = route.handler
|
||||
break
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ async def test_specific_node_hash_validation(mock_app):
|
|||
# Trigger sync
|
||||
sync_handler = None
|
||||
for route in mock_app.get_routes():
|
||||
if route.path == "/api/v1/lxmf/propagation-node/sync" and route.method == "GET":
|
||||
if route.path == "/api/v1/lxmf/propagation-node/sync" and route.method == "POST":
|
||||
sync_handler = route.handler
|
||||
break
|
||||
|
||||
|
|
|
|||
|
|
@ -278,4 +278,23 @@ target_port = 4242`;
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("imports announce_interval zero and equator coordinates from config", async () => {
|
||||
const wrapper = mountPage();
|
||||
|
||||
wrapper.vm.applyConfig({
|
||||
name: "ZeroInterval",
|
||||
type: "TCPClientInterface",
|
||||
target_host: "node.example",
|
||||
target_port: "4242",
|
||||
discoverable: "yes",
|
||||
announce_interval: 0,
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
});
|
||||
|
||||
expect(wrapper.vm.discovery.announce_interval).toBe(0);
|
||||
expect(wrapper.vm.discovery.latitude).toBe(0);
|
||||
expect(wrapper.vm.discovery.longitude).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,11 +106,13 @@ describe("App propagation sync", () => {
|
|||
});
|
||||
|
||||
it("shows detailed success toast with stored, confirmations and hidden counts", async () => {
|
||||
axiosMock.post.mockResolvedValue({ data: { message: "ok" } });
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
axiosMock.post.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/sync") {
|
||||
return Promise.resolve({ data: { message: "Sync is starting" } });
|
||||
}
|
||||
return Promise.resolve({ data: { message: "ok" } });
|
||||
});
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/status") {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
|
|
@ -138,17 +140,20 @@ describe("App propagation sync", () => {
|
|||
expect(ToastUtils.success).toHaveBeenCalledWith(
|
||||
"Sync complete. 8 messages received. (3 stored, 2 confirmations, 3 hidden)"
|
||||
);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/lxmf/propagation-node/sync");
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/destination/deadbeef/request-path");
|
||||
expect(ToastUtils.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("polls status while syncing and updates live loading toast", async () => {
|
||||
axiosMock.post.mockResolvedValue({ data: { message: "ok" } });
|
||||
let statusCalls = 0;
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
axiosMock.post.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/sync") {
|
||||
return Promise.resolve({ data: { message: "Sync is starting" } });
|
||||
}
|
||||
return Promise.resolve({ data: { message: "ok" } });
|
||||
});
|
||||
let statusCalls = 0;
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/status") {
|
||||
statusCalls += 1;
|
||||
if (statusCalls < 3) {
|
||||
|
|
@ -196,11 +201,13 @@ describe("App propagation sync", () => {
|
|||
});
|
||||
|
||||
it("uses translated status in error toast when sync ends in a failure state", async () => {
|
||||
axiosMock.post.mockResolvedValue({ data: { message: "ok" } });
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
axiosMock.post.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/sync") {
|
||||
return Promise.resolve({ data: { message: "Sync is starting" } });
|
||||
}
|
||||
return Promise.resolve({ data: { message: "ok" } });
|
||||
});
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/status") {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
|
|
@ -228,20 +235,22 @@ describe("App propagation sync", () => {
|
|||
});
|
||||
|
||||
it("still starts sync when request-path fails (stale CSRF / brief offline after background)", async () => {
|
||||
// Oracle: request-path is best-effort priming. The /sync GET already
|
||||
// Oracle: request-path is best-effort priming. The /sync POST already
|
||||
// requests a path server-side. A CSRF or network failure on the POST
|
||||
// must not abort the user-initiated sync (common after a backgrounded tab).
|
||||
axiosMock.post.mockRejectedValue(
|
||||
Object.assign(new Error("HTTP 403"), {
|
||||
response: { status: 403, data: { error: "Invalid or missing CSRF token" } },
|
||||
})
|
||||
);
|
||||
let syncCalled = false;
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
axiosMock.post.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/sync") {
|
||||
syncCalled = true;
|
||||
return Promise.resolve({ data: { message: "Sync is starting" } });
|
||||
}
|
||||
return Promise.reject(
|
||||
Object.assign(new Error("HTTP 403"), {
|
||||
response: { status: 403, data: { error: "Invalid or missing CSRF token" } },
|
||||
})
|
||||
);
|
||||
});
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/status") {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue