Files
MeshCore/test/test_room_login_integration.py
T
mikecarper 74bc91bf69 Improve room retention, moderation and local browser access
Add opt-in bounded chat history, persist explicit room ACL roles, enforce full-key bans and per-user quotas, and provide versioned information-board reads plus an ESP32 local room page. Preserve stored evidence on faults and withhold room delivery acknowledgments when a Companion cannot retain the message.

Exercise production handlers and filesystem fault recovery, pinned official-app JavaScript, real Chromium interactions, and representative ESP32, nRF52, RP2040 and STM32 builds. Add regression suites and bounded browser-workspace RAM reservations to CI.
2026-10-09 23:44:00 -07:00

801 lines
39 KiB
Python

#!/usr/bin/env python3
"""Execute the real room login/replay handlers against controlled storage faults."""
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
#include "filesystem.h"
#include <helpers/RoomAccessPolicy.h>
#include <helpers/RemoteCliReplyCache.h>
#include <helpers/RemoteCliRequest.h>
#include <helpers/ClientACLResponse.h>
#include <helpers/ClientPathPersistence.h>
#include <Packet.h>
#include <helpers/RoomLoginAuthorization.h>
#include <helpers/ClientLoginPersistence.h>
#include <helpers/LazyPersistence.h>
#include <helpers/LogicalMessageCache.h>
@CONSTANTS@
#define MESH_CLIENT_REPEATER_ONLY 0
namespace mesh {
struct Identity { uint8_t pub_key[PUB_KEY_SIZE]; };
struct Utils {
static void sha256(uint8_t* out, size_t n, const uint8_t* key, size_t key_n,
const uint8_t* text, size_t text_n) {
// Posting authorization and replay are tested here, not the hash primitive.
memset(out, 0, n);
for (size_t i = 0; i < key_n; ++i) out[i % n] ^= key[i];
for (size_t i = 0; i < text_n; ++i) out[i % n] ^= text[i];
}
};
@PACKET_CONSTRUCTOR@
@PACKET_PATH_CHECK@
}
@CLIENT_INFO@;
struct ReplayStorage {
uint8_t key[PUB_KEY_SIZE] = {};
bool found = false, read_ok = true, write_ok = true;
uint32_t ceiling = 0;
unsigned reads = 0, writes = 0;
};
static bool readClientLoginReplayCeiling(
ReplayStorage* fs, const uint8_t* key, uint32_t* ceiling, bool* found) {
++fs->reads;
if (!fs->read_ok) return false;
*found = fs->found && memcmp(fs->key, key, PUB_KEY_SIZE) == 0;
*ceiling = *found ? fs->ceiling : 0;
return true;
}
static bool writeClientLoginReplayCeiling(
ReplayStorage* fs, const uint8_t* key, uint32_t ceiling,
mesh::ClientLoginReplayReservationAction action) {
++fs->writes;
assert(action != mesh::ClientLoginReplayReservationAction::None);
if (!fs->write_ok) return false;
memcpy(fs->key, key, PUB_KEY_SIZE);
fs->ceiling = ceiling;
fs->found = true;
return true;
}
struct ClientACL {
ReplayStorage storage;
ReplayStorage* _fs = &storage;
bool login_replay_store_available = true, acl_load_complete = true;
bool authorizeLoginTimestamp(const uint8_t*, uint32_t, uint32_t, uint8_t);
};
@REPLAY_HANDLER@
struct FakeACL : ClientACL {
ClientInfo client{};
bool known = false, allocate_ok = true;
unsigned lookups = 0, authorizations = 0, allocations = 0;
uint8_t authorized_role = 255;
int getNumClients() const { return known ? 1 : 0; }
ClientInfo* getClientByIdx(int i) { assert(i == 0 && known); return &client; }
ClientInfo* getClient(const uint8_t* key, size_t length) {
++lookups;
assert(length == PUB_KEY_SIZE);
return known && memcmp(client.id.pub_key, key, length) == 0 ? &client : nullptr;
}
bool authorizeLoginTimestamp(const uint8_t* key, uint32_t timestamp,
uint32_t floor, uint8_t role) {
++authorizations;
authorized_role = role;
return ClientACL::authorizeLoginTimestamp(key, timestamp, floor, role);
}
ClientInfo* putClient(const mesh::Identity& sender, uint8_t permissions) {
++allocations;
assert(permissions == 0 && !known);
if (!allocate_ok) return nullptr;
client = ClientInfo{};
client.id = sender;
client.permissions = permissions;
client.out_path_len = OUT_PATH_UNKNOWN;
client.out_path_is_persistable = true;
known = true;
return &client;
}
};
struct FakeClock {
uint32_t now = 1000;
unsigned reads = 0, unique_reads = 0;
uint32_t getCurrentTime() { ++reads; return now; }
uint32_t getCurrentTimeUnique() { ++unique_reads; return now + unique_reads; }
};
struct FakeRng {
unsigned calls = 0;
void random(uint8_t* out, size_t length) {
++calls;
memset(out, 0xB5, length);
}
};
static uint32_t ticks = 0;
static uint32_t millis() { return ticks; }
struct MyMesh {
FakeACL acl;
MemoryFS policy_fs;
mesh::RoomAccessPolicy room_access;
MyMesh() { metadata_filesystem = &policy_fs; assert(room_access.load(&policy_fs)); }
int matching_peer_indexes[1] = {0};
mesh::RemoteCliReplyCache remote_cli_reply_cache;
mesh::LogicalMessageCache<ROOM_MESSAGE_CACHE_SIZE> recent_room_polls;
void serviceRoomQuotas();
static bool saveFilter(ClientInfo*);
unsigned path_acks = 0;
bool processAck(const uint8_t*) { ++path_acks; return true; }
uint8_t getUnsyncedCount(ClientInfo*) { return 2; }
int getExtraAckTransmitCount() { return 0; }
unsigned requests = 0;
uint8_t last_request_type = 0;
int handleRequest(ClientInfo*, uint32_t, uint8_t* payload, size_t, size_t) { ++requests; last_request_type = payload[0]; return 0; }
void handleCommand(uint32_t, char*, char* reply, int, uint8_t) { strcpy(reply, "OK"); }
mesh::Packet* createAck(uint32_t ack) { ++post_acks; response.payload_len = 4; memcpy(response.payload, &ack, 4); return &response; }
mesh::Packet* createMultiAck(uint32_t ack, uint8_t) { return createAck(ack); }
struct Prefs {
char password[32] = "admin";
char guest_password[32] = "guest";
bool allow_read_only = false;
} _prefs;
FakeClock clock;
FakeRng rng;
unsigned long millis_now = 20000, dirty_contacts_expiry = 0, next_push = 0;
uint8_t contacts_save_failures = 0, reply_data[32] = {};
unsigned creations = 0, direct_sends = 0, flood_sends = 0, path_returns = 0;
bool create_ok = true;
mesh::Packet response;
uint8_t response_secret[PUB_KEY_SIZE] = {}, response_key[PUB_KEY_SIZE] = {};
uint8_t returned_path[MAX_PATH_SIZE] = {}, direct_path[MAX_PATH_SIZE] = {};
uint8_t returned_path_len = 0, direct_path_len = 0, reply_hash_size = 0;
uint8_t captured_reply[32] = {};
size_t captured_reply_length = 0;
mesh::LogicalMessageCache<ROOM_MESSAGE_CACHE_SIZE> recent_room_posts;
unsigned posts = 0, post_acks = 0;
FakeClock* getRTCClock() { return &clock; }
FakeRng* getRNG() { return &rng; }
unsigned long futureMillis(unsigned long delay) { return millis_now + delay; }
mesh::Packet* createDatagram(uint8_t type, const mesh::Identity& sender,
const uint8_t* secret, const uint8_t* data, size_t len) {
++creations;
assert(type == PAYLOAD_TYPE_RESPONSE || type == PAYLOAD_TYPE_TXT_MSG);
assert(len <= sizeof(captured_reply));
memcpy(response_key, sender.pub_key, PUB_KEY_SIZE);
memcpy(response_secret, secret, PUB_KEY_SIZE);
memcpy(captured_reply, data, len);
captured_reply_length = len;
return create_ok ? &response : nullptr;
}
mesh::Packet* createPathReturn(const mesh::Identity& sender, const uint8_t* secret,
const uint8_t* path, uint8_t path_len, uint8_t type, const uint8_t* data, size_t len) {
++path_returns;
returned_path_len = path_len;
memcpy(returned_path, path, (path_len & 63) * ((path_len >> 6) + 1));
return createDatagram(type, sender, secret, data, len);
}
void sendDirect(mesh::Packet* packet, const uint8_t* path, uint8_t path_len,
unsigned long delay) {
assert(packet == &response && (delay == SERVER_RESPONSE_DELAY || delay == TXT_ACK_DELAY));
assert(mesh::Packet::isValidPathLen(path_len));
++direct_sends;
direct_path_len = path_len;
memcpy(direct_path, path, (path_len & 63) * ((path_len >> 6) + 1));
}
void sendFloodReply(mesh::Packet* packet, unsigned long delay, uint8_t hash_size) {
assert(packet == &response && (delay == SERVER_RESPONSE_DELAY || delay == TXT_ACK_DELAY));
++flood_sends;
reply_hash_size = hash_size;
}
bool store_ok = true;
bool addPost(ClientInfo*, const char*) { if (!store_ok) return false; ++posts; return true; }
void post(ClientInfo*, const char* text, uint32_t sender_timestamp) {
std::vector<uint8_t> data(5 + strlen(text) + 1);
memcpy(data.data(), &sender_timestamp, 4); data[4] = TXT_TYPE_PLAIN << 2;
memcpy(data.data() + 5, text, strlen(text));
mesh::Packet packet; packet.header = ROUTE_TYPE_DIRECT;
onPeerDataRecv(&packet, PAYLOAD_TYPE_TXT_MSG, 0, acl.client.shared_secret, data.data(), data.size() - 1);
}
void poll(uint32_t timestamp, uint32_t since = 0, bool flood = false) {
uint8_t data[10] = {}; memcpy(data, &timestamp, 4); data[4] = REQ_TYPE_KEEP_ALIVE;
memcpy(data + 5, &since, 4);
mesh::Packet packet; packet.header = flood ? ROUTE_TYPE_FLOOD : ROUTE_TYPE_DIRECT;
onPeerDataRecv(&packet, PAYLOAD_TYPE_REQ, 0, acl.client.shared_secret, data, 9);
}
void request(uint32_t timestamp, uint8_t subtype, uint8_t argument = 0) {
uint8_t data[8] = {}; memcpy(data, &timestamp, 4); data[4] = subtype;
data[5] = argument;
mesh::Packet packet; packet.header = ROUTE_TYPE_DIRECT;
onPeerDataRecv(&packet, PAYLOAD_TYPE_REQ, 0, acl.client.shared_secret, data, sizeof(data));
}
void onPeerDataRecv(mesh::Packet*, uint8_t, int, const uint8_t*, uint8_t*, size_t);
bool onPeerPathRecv(mesh::Packet*, int, const uint8_t*, uint8_t*, uint8_t, uint8_t, uint8_t*, uint8_t);
void onAnonDataRecv(mesh::Packet*, const uint8_t*, const mesh::Identity&,
uint8_t*, size_t);
};
@LOGIN_HANDLER@
@PEER_HANDLER@
@PATH_HANDLER@
@QUOTA_SERVICE@
@SAVE_FILTER@
static mesh::Identity identity(uint8_t n = 1) {
mesh::Identity sender{};
memset(sender.pub_key, n, sizeof(sender.pub_key));
return sender;
}
static void seedKnown(MyMesh& mesh, uint8_t permissions = PERM_ACL_ADMIN) {
auto& client = mesh.acl.client;
// Initialize padding too, so byte-for-byte rejected-session checks are stable.
memset(&client, 0, sizeof(client));
client.id = identity();
client.permissions = permissions;
client.last_timestamp = 99;
client.last_activity = 777;
client.out_path_len = 2;
client.out_path[0] = 0xA1;
client.out_path[1] = 0xB2;
client.out_path_is_persistable = false;
memset(client.shared_secret, 0x44, sizeof(client.shared_secret));
client.extra.room.sync_since = 70;
client.extra.room.last_post_timestamp = 800;
client.extra.room.pending_ack = 0x12345678;
client.extra.room.push_failures = 2;
client.extra.room.topic_seen_revision = 11;
client.extra.room.pending_topic_revision = 12;
client.extra.room.topic_failures = 2;
client.extra.room.post_quota_used = 9;
client.extra.room.poll_quota_used = 8;
mesh.acl.known = true;
}
static mesh::Packet incoming(bool flood = false) {
mesh::Packet packet;
packet.header = (PAYLOAD_TYPE_ANON_REQ << PH_TYPE_SHIFT)
| (flood ? ROUTE_TYPE_FLOOD : ROUTE_TYPE_DIRECT);
packet.path_len = 0x42; // Two 2-byte hashes, including non-default hash size.
packet.path[0] = 0x10;
packet.path[1] = 0x20;
packet.path[2] = 0x30;
packet.path[3] = 0x40;
return packet;
}
static void login(MyMesh& mesh, const char* password = "guest", uint32_t timestamp = 100,
uint32_t sync_since = 80, bool flood = false) {
// The real decrypt dispatcher provides one writable terminator byte.
const size_t password_len = strlen(password);
std::vector<uint8_t> data(8 + password_len + 1, 0xEE);
memcpy(data.data(), &timestamp, 4);
memcpy(data.data() + 4, &sync_since, 4);
memcpy(data.data() + 8, password, password_len);
uint8_t secret[PUB_KEY_SIZE];
memset(secret, 0x77, sizeof(secret));
auto packet = incoming(flood);
mesh.onAnonDataRecv(&packet, secret, identity(), data.data(), data.size() - 1);
if (mesh.room_access.allowsIdentity(identity().pub_key)) assert(data.back() == 0);
}
static void checkReply(const MyMesh& mesh, uint8_t permissions) {
assert(mesh.creations == 1 && mesh.captured_reply_length == 13);
uint32_t timestamp;
memcpy(&timestamp, mesh.captured_reply, sizeof(timestamp));
assert(timestamp == mesh.clock.now + 1);
assert(mesh.captured_reply[4] == RESP_SERVER_LOGIN_OK);
assert(mesh.captured_reply[5] == 0);
const uint8_t role = permissions & PERM_ACL_ROLE_MASK;
assert(mesh.captured_reply[6] == (role == PERM_ACL_ADMIN ? 1 : role == PERM_ACL_GUEST ? 2 : 0));
assert(mesh.captured_reply[7] == permissions);
assert(mesh.captured_reply[12] == FIRMWARE_VER_LEVEL);
for (unsigned i = 0; i < 4; ++i) assert(mesh.captured_reply[8 + i] == 0xB5);
for (unsigned i = 0; i < PUB_KEY_SIZE; ++i) {
assert(mesh.response_key[i] == 1 && mesh.response_secret[i] == 0x77);
}
}
static void checkRejectedSession(const MyMesh& mesh, const ClientInfo& before) {
assert(memcmp(&mesh.acl.client, &before, sizeof(before)) == 0);
assert(mesh.creations == 0 && mesh.direct_sends == 0 && mesh.flood_sends == 0);
assert(mesh.clock.reads == 0 && mesh.clock.unique_reads == 0 && mesh.rng.calls == 0);
assert(mesh.next_push == 0 && mesh.dirty_contacts_expiry == 0);
}
static void authorizationCases() {
struct Case { const char* password; bool reader; bool accepted; uint8_t role; };
const Case cases[] = {
{"", false, false, PERM_ACL_GUEST}, {"", true, true, PERM_ACL_GUEST},
{"wrong", false, false, PERM_ACL_GUEST}, {"wrong", true, true, PERM_ACL_GUEST},
{"guest", false, true, PERM_ACL_READ_WRITE}, {"guest", true, true, PERM_ACL_READ_WRITE},
{"admin", false, true, PERM_ACL_ADMIN}, {"admin", true, true, PERM_ACL_ADMIN},
};
for (const auto& c : cases) {
MyMesh mesh;
mesh._prefs.allow_read_only = c.reader;
login(mesh, c.password);
assert(mesh.acl.known == c.accepted);
assert(mesh.acl.authorizations == (c.accepted ? 1U : 0U));
assert(mesh.acl.allocations == (c.accepted ? 1U : 0U));
assert(mesh.acl.storage.reads == (c.accepted ? 1U : 0U));
assert(mesh.acl.storage.writes == (c.accepted && c.role != PERM_ACL_GUEST ? 1U : 0U));
if (c.accepted) {
assert(mesh.acl.client.permissions == c.role);
checkReply(mesh, c.role);
} else {
assert(!mesh.acl.storage.found && mesh.acl.storage.ceiling == 0);
assert(mesh.creations == 0 && mesh.dirty_contacts_expiry == 0);
}
}
for (bool reader : {false, true}) {
MyMesh mesh;
mesh._prefs.password[0] = mesh._prefs.guest_password[0] = 0;
mesh._prefs.allow_read_only = reader;
login(mesh, "");
assert(mesh.acl.known == reader);
if (reader) {
assert(mesh.acl.client.permissions == PERM_ACL_GUEST);
assert(mesh.acl.storage.writes == 0);
checkReply(mesh, PERM_ACL_GUEST);
} else assert(mesh.acl.authorizations == 0);
}
// Parser validation occurs before lookup, replay reservation, or allocation.
for (size_t length = 0; length < 8; ++length) {
MyMesh mesh;
auto packet = incoming();
std::vector<uint8_t> data(length + 1, 0xEE);
uint8_t secret[PUB_KEY_SIZE] = {};
mesh.onAnonDataRecv(&packet, secret, identity(), data.data(), length);
assert(data.back() == 0xEE && mesh.acl.lookups == 0);
assert(mesh.acl.authorizations == 0 && mesh.acl.allocations == 0);
}
MyMesh other_payload;
auto packet = incoming();
packet.header = (PAYLOAD_TYPE_RESPONSE << PH_TYPE_SHIFT) | ROUTE_TYPE_DIRECT;
uint8_t data[9] = {}, secret[PUB_KEY_SIZE] = {};
other_payload.onAnonDataRecv(&packet, secret, identity(), data, 8);
assert(other_payload.acl.lookups == 0 && other_payload.acl.authorizations == 0);
puts("real-handler credentials, disabled passwords, and minimum payload cases passed");
}
static void preservedRoles() {
const char* passwords[] = {"guest", "wrong", "", "old-admin"};
unsigned preserved = 0;
for (unsigned permissions = 0; permissions < 256; ++permissions) {
for (const auto* password : passwords) {
for (bool reader : {false, true}) {
MyMesh mesh;
seedKnown(mesh, permissions);
mesh._prefs.allow_read_only = reader;
mesh.acl.client.permissions_are_explicit = reader;
login(mesh, password);
assert(mesh.acl.client.permissions == permissions);
assert(mesh.acl.client.permissions_are_explicit == reader);
assert(mesh.acl.authorized_role == (permissions & PERM_ACL_ROLE_MASK));
assert(mesh.acl.allocations == 0 && mesh.acl.client.last_timestamp == 100);
checkReply(mesh, permissions);
// Repeat the exact accepted packet with a different public password.
const ClientInfo before = mesh.acl.client;
mesh.creations = mesh.direct_sends = mesh.flood_sends = 0;
mesh.clock.reads = mesh.clock.unique_reads = mesh.rng.calls = 0;
mesh.next_push = mesh.dirty_contacts_expiry = 0;
const unsigned writes_before = mesh.acl.storage.writes;
login(mesh, "guest", 100, 900, true);
checkRejectedSession(mesh, before);
assert(mesh.acl.storage.writes == writes_before);
++preserved;
}
}
// A valid explicit Admin credential replaces delegated labels 4/5, too.
MyMesh mesh;
seedKnown(mesh, permissions);
mesh.acl.client.permissions_are_explicit = true;
login(mesh, "admin");
const uint8_t expected = (permissions & ~PERM_ACL_ROLE_MASK) | PERM_ACL_ADMIN;
assert(mesh.acl.client.permissions == expected);
assert(mesh.acl.client.permissions_are_explicit);
checkReply(mesh, expected);
}
assert(preserved == 2048);
puts("2048 real-handler preserved roles and replay rejects plus 256 admin upgrades passed");
}
static void storageFailures() {
for (bool known : {false, true}) {
for (unsigned fault = 0; fault < 5; ++fault) {
MyMesh mesh;
if (known) seedKnown(mesh, PERM_ACL_FILTER_MGR | 0x80);
const ClientInfo before = mesh.acl.client;
switch (fault) {
case 0: mesh.acl._fs = nullptr; break;
case 1: mesh.acl.acl_load_complete = false; break;
case 2: mesh.acl.login_replay_store_available = false; break;
case 3: mesh.acl.storage.read_ok = false; break;
case 4: mesh.acl.storage.write_ok = false; break;
}
login(mesh, known ? "guest" : "admin");
checkRejectedSession(mesh, before);
assert(mesh.acl.known == known && mesh.acl.allocations == 0);
assert(mesh.acl.authorizations == 1);
assert(!mesh.acl.storage.found && mesh.acl.storage.ceiling == 0);
assert(mesh.acl.storage.writes == (fault == 4 ? 1U : 0U));
}
}
// Denial from disabled/incorrect credentials never reaches replay storage.
for (bool write_ok : {false, true}) {
MyMesh mesh;
mesh.acl.storage.write_ok = write_ok;
login(mesh, "wrong");
assert(mesh.acl.authorizations == 0 && mesh.acl.allocations == 0);
assert(mesh.acl.storage.reads == 0 && mesh.acl.storage.writes == 0);
}
// A historical privileged tombstone is still enforced on a public reader.
MyMesh tombstone;
tombstone._prefs.allow_read_only = true;
tombstone.acl.storage.found = true;
memcpy(tombstone.acl.storage.key, identity().pub_key, PUB_KEY_SIZE);
tombstone.acl.storage.ceiling = 160;
login(tombstone, "", 160);
assert(!tombstone.acl.known && tombstone.acl.allocations == 0);
assert(tombstone.acl.storage.ceiling == 160 && tombstone.acl.storage.writes == 0);
tombstone.acl.storage.write_ok = false;
login(tombstone, "", 161);
assert(!tombstone.acl.known && tombstone.acl.allocations == 0);
assert(tombstone.acl.storage.ceiling == 160 && tombstone.acl.storage.writes == 1);
// A live downgraded Guest cannot bypass its historical privileged record.
MyMesh known_reader;
seedKnown(known_reader, PERM_ACL_GUEST | 0x80);
known_reader.acl.storage.found = true;
memcpy(known_reader.acl.storage.key, identity().pub_key, PUB_KEY_SIZE);
known_reader.acl.storage.ceiling = 159;
known_reader.acl.storage.write_ok = false;
const ClientInfo reader_before = known_reader.acl.client;
login(known_reader, "guest", 160, 900, true);
checkRejectedSession(known_reader, reader_before);
assert(known_reader.acl.authorized_role == PERM_ACL_GUEST);
assert(known_reader.acl.allocations == 0 && known_reader.acl.storage.ceiling == 159);
assert(known_reader.acl.storage.writes == 1);
// Durable reservation happens before allocation; a full ACL cannot roll it back.
MyMesh full;
full.acl.allocate_ok = false;
login(full, "admin");
assert(!full.acl.known && full.acl.allocations == 1 && full.creations == 0);
assert(full.acl.storage.found && full.acl.storage.ceiling == 160);
login(full, "admin", 100);
assert(full.acl.allocations == 1 && full.acl.storage.writes == 1);
// Zero timestamps are never accepted even with correct credentials.
MyMesh zero;
login(zero, "admin", 0);
assert(!zero.acl.known && zero.acl.allocations == 0 && zero.acl.storage.writes == 0);
puts("real replay admission rejects failed storage, historical replay, and failed allocation passed");
}
static void sessionRefresh() {
for (bool flood : {false, true}) {
for (uint8_t route : {uint8_t(2), uint8_t(OUT_PATH_UNKNOWN), uint8_t(OUT_PATH_FORCE_FLOOD)}) {
MyMesh mesh;
seedKnown(mesh, PERM_ACL_REGION_MGR | 0x80);
mesh.acl.client.out_path_len = route;
login(mesh, "guest", 100, 81, flood);
const auto& client = mesh.acl.client;
assert(client.permissions == (PERM_ACL_REGION_MGR | 0x80));
assert(client.last_timestamp == 100 && client.last_activity == mesh.clock.now);
assert(client.extra.room.sync_since == 81 && client.extra.room.last_post_timestamp == 800);
assert(client.extra.room.pending_ack == 0 && client.extra.room.push_failures == 0);
assert(client.extra.room.topic_seen_revision == 0 && client.extra.room.pending_topic_revision == 0);
assert(client.extra.room.topic_failures == 0);
assert(client.extra.room.post_quota_used == 9 && client.extra.room.poll_quota_used == 8);
for (uint8_t value : client.shared_secret) assert(value == 0x77);
assert(mesh.next_push == mesh.millis_now + PUSH_NOTIFY_DELAY_MILLIS);
assert(mesh.dirty_contacts_expiry == mesh.millis_now + LAZY_CONTACTS_WRITE_DELAY);
assert(mesh.acl.allocations == 0 && mesh.clock.reads == 1 && mesh.clock.unique_reads == 1);
const uint8_t expected_route = flood && route != OUT_PATH_FORCE_FLOOD ? OUT_PATH_UNKNOWN : route;
assert(client.out_path_len == expected_route);
assert(client.out_path[0] == 0xA1 && client.out_path[1] == 0xB2);
if (flood) {
assert(mesh.path_returns == 1 && mesh.flood_sends == 1 && mesh.direct_sends == 0);
assert(mesh.returned_path_len == 0x42 && mesh.reply_hash_size == 2);
const uint8_t expected_path[] = {0x10, 0x20, 0x30, 0x40};
assert(memcmp(mesh.returned_path, expected_path, sizeof(expected_path)) == 0);
if (route != OUT_PATH_FORCE_FLOOD) assert(client.out_path_is_persistable);
} else if (route == 2) {
assert(mesh.path_returns == 0 && mesh.direct_sends == 1 && mesh.flood_sends == 0);
assert(mesh.direct_path_len == 2 && mesh.direct_path[0] == 0xA1 && mesh.direct_path[1] == 0xB2);
} else assert(mesh.path_returns == 0 && mesh.flood_sends == 1 && mesh.direct_sends == 0);
checkReply(mesh, client.permissions);
}
}
// A login refresh must preserve an existing persistence fault's backoff.
MyMesh backoff;
seedKnown(backoff);
backoff.dirty_contacts_expiry = 400000;
backoff.contacts_save_failures = 5;
login(backoff, "guest");
assert(backoff.dirty_contacts_expiry == 400000 && backoff.contacts_save_failures == 5);
// Activity alone is transient; an unchanged persisted session needs no write.
MyMesh unchanged;
seedKnown(unchanged);
memset(unchanged.acl.client.shared_secret, 0x77, PUB_KEY_SIZE);
login(unchanged, "guest", 100, 70);
assert(unchanged.acl.client.last_activity == unchanged.clock.now);
assert(unchanged.dirty_contacts_expiry == 0);
// Packet allocation failure cannot undo a successfully authenticated session.
MyMesh response_failure;
seedKnown(response_failure);
response_failure.create_ok = false;
login(response_failure, "guest");
assert(response_failure.acl.client.last_timestamp == 100);
assert(response_failure.creations == 1 && response_failure.direct_sends == 0 && response_failure.flood_sends == 0);
puts("real-handler successful reconnect refreshes cursor, activity, secret, topic state, and route passed");
}
static void readOnlyPosting() {
for (const char* password : {"", "wrong"}) {
MyMesh mesh;
mesh._prefs.allow_read_only = true;
login(mesh, password);
assert(mesh.acl.known && mesh.acl.client.permissions == PERM_ACL_GUEST);
mesh.post(&mesh.acl.client, "reader must not write", 1000);
assert(mesh.posts == 0 && mesh.post_acks == 0);
assert(mesh.acl.client.extra.room.last_post_timestamp == 0);
// Existing Guest is an ACL role too; a public password cannot upgrade it.
login(mesh, "guest", 101);
assert(mesh.acl.client.permissions == PERM_ACL_GUEST && mesh.acl.storage.writes == 0);
mesh.post(&mesh.acl.client, "still not writable", 1001);
assert(mesh.posts == 0 && mesh.post_acks == 0);
}
for (uint8_t role : {uint8_t(PERM_ACL_READ_ONLY), uint8_t(PERM_ACL_REGION_MGR), uint8_t(PERM_ACL_FILTER_MGR)}) {
MyMesh mesh;
seedKnown(mesh, role);
login(mesh, "guest");
mesh.post(&mesh.acl.client, "not a posting role", 1000);
assert(mesh.acl.client.permissions == role && mesh.posts == 0 && mesh.post_acks == 0);
}
for (const char* password : {"guest", "admin"}) {
MyMesh mesh;
login(mesh, password);
mesh.post(&mesh.acl.client, "writable", 1000);
mesh.post(&mesh.acl.client, "writable", 1000);
assert(mesh.posts == 1 && mesh.post_acks == 2);
}
puts("actual room post gate blocks logged-in readers/managers and accepts RW/Admin once passed");
}
static void moderationCases() {
for (bool known : {false, true}) for (const char* password : {"", "guest", "admin"}) {
MyMesh mesh;
if (known) seedKnown(mesh, PERM_ACL_ADMIN);
mesh._prefs.allow_read_only = true;
assert(mesh.room_access.addBan(&mesh.policy_fs, identity().pub_key) == mesh::RoomAccessPolicy::BanResult::Saved);
const ClientInfo before = mesh.acl.client;
login(mesh, password);
checkRejectedSession(mesh, before);
assert(mesh.acl.authorizations == 0 && mesh.acl.allocations == 0 && mesh.acl.lookups == 0);
assert(mesh.acl.storage.reads == 0 && mesh.acl.storage.writes == 0);
if (known) {
mesh.post(&mesh.acl.client, "banned", 1000);
mesh.poll(1000, 900);
assert(mesh.posts == 0 && mesh.post_acks == 0);
assert(memcmp(&mesh.acl.client, &before, sizeof(before)) == 0);
auto packet = incoming(); uint8_t path[] = {0x11, 0x22}, ack[4] = {};
assert(!mesh.onPeerPathRecv(&packet, 0, mesh.acl.client.shared_secret,
path, 2, PAYLOAD_TYPE_ACK, ack, sizeof(ack)));
assert(mesh.path_acks == 0 && memcmp(&mesh.acl.client, &before, sizeof(before)) == 0);
}
assert(mesh.room_access.removeBan(&mesh.policy_fs, identity().pub_key) == mesh::RoomAccessPolicy::BanResult::Saved);
login(mesh, password);
assert(mesh.acl.known && mesh.creations == 1);
}
// A 31-byte collision is a different identity; bans always use the full key.
MyMesh collision; auto other = identity(); other.pub_key[31] ^= 1;
assert(collision.room_access.addBan(&collision.policy_fs, other.pub_key) == mesh::RoomAccessPolicy::BanResult::Saved);
login(collision, "admin"); assert(collision.acl.known && collision.creations == 1);
// A bad policy image must not silently admit an Admin or public reader.
MyMesh unavailable;
unavailable.policy_fs.put(mesh::ROOM_ACCESS_PRIMARY_PATH, {1,2,3});
assert(!unavailable.room_access.load(&unavailable.policy_fs));
unavailable._prefs.allow_read_only = true;
login(unavailable, "admin");
assert(!unavailable.acl.known && unavailable.acl.lookups == 0 && unavailable.creations == 0);
puts("actual room gates deny full-key bans on login, posts, polls, and paths passed");
}
static void quotaCases() {
MyMesh mesh; ticks = 100; seedKnown(mesh, PERM_ACL_ADMIN);
assert(mesh.room_access.setRates(&mesh.policy_fs, 1, 1));
mesh.post(&mesh.acl.client, "one", 1000);
assert(mesh.posts == 1 && mesh.post_acks == 1 && mesh.acl.client.extra.room.post_quota_used == 1);
assert(mesh.acl.client.extra.room.poll_quota_used == 0);
login(mesh, "guest", 100);
assert(mesh.acl.client.extra.room.post_quota_used == 1); // Re-login never bypasses a posting quota.
mesh.post(&mesh.acl.client, "two", 1001);
assert(mesh.posts == 1 && mesh.post_acks == 1 && mesh.acl.client.extra.room.last_post_timestamp == 1000);
mesh.post(&mesh.acl.client, "one", 1000);
assert(mesh.posts == 1 && mesh.post_acks == 2 && mesh.acl.client.extra.room.post_quota_used == 1);
mesh.poll(1001, 50);
assert(mesh.post_acks == 3 && mesh.acl.client.extra.room.poll_quota_used == 1);
assert(mesh.acl.client.last_timestamp == 1001 && mesh.acl.client.extra.room.sync_since == 50);
mesh.acl.client.extra.room.pending_ack = 0x12345678;
mesh.acl.client.extra.room.pending_topic_revision = 55;
const auto poll_retry_before = mesh.acl.client;
mesh.poll(1001, 50); // Even equal-time retries cannot cancel a newer outstanding delivery.
assert(mesh.post_acks == 4 && mesh.acl.client.extra.room.poll_quota_used == 1);
assert(memcmp(&poll_retry_before, &mesh.acl.client, sizeof(poll_retry_before)) == 0);
mesh.poll(1002, 60);
assert(mesh.post_acks == 4 && mesh.acl.client.last_timestamp == 1001 && mesh.acl.client.extra.room.sync_since == 50);
login(mesh, "guest", 1002, 80);
assert(mesh.acl.client.extra.room.post_quota_used == 1 && mesh.acl.client.extra.room.poll_quota_used == 1);
mesh.poll(1001, 50); // Old exact retry ACK cannot rewind a refreshed/newer cursor.
assert(mesh.post_acks == 5 && mesh.acl.client.extra.room.sync_since == 80);
mesh.poll(1001, 99); // Changed content at an old timestamp isn't a cached retry.
assert(mesh.post_acks == 5 && mesh.acl.client.extra.room.sync_since == 80);
ticks = 60100;
mesh.post(&mesh.acl.client, "two", 1001);
assert(mesh.posts == 2 && mesh.post_acks == 6 && mesh.acl.client.extra.room.last_post_timestamp == 1001);
assert(mesh.acl.client.extra.room.post_quota_used == 1 && mesh.acl.client.extra.room.poll_quota_used == 0);
mesh.poll(1002, 60); // Earlier quota rejection must not have entered the retry cache.
assert(mesh.post_acks == 7 && mesh.acl.client.extra.room.poll_quota_used == 1);
assert(mesh.acl.client.extra.room.sync_since == 60);
ticks = 120100; mesh.store_ok = false;
const auto post_floor = mesh.acl.client.extra.room.last_post_timestamp;
const auto last_activity = mesh.acl.client.last_activity;
mesh.post(&mesh.acl.client, "storage-fails", 1003);
assert(mesh.posts == 2 && mesh.post_acks == 7 && mesh.acl.client.extra.room.post_quota_used == 0);
assert(mesh.acl.client.extra.room.last_post_timestamp == post_floor && mesh.acl.client.last_activity == last_activity);
mesh.store_ok = true; mesh.post(&mesh.acl.client, "storage-fails", 1003);
assert(mesh.posts == 3 && mesh.post_acks == 8 && mesh.acl.client.extra.room.post_quota_used == 1);
const auto before = mesh.acl.client;
mesh.poll(1004, 1000, true); // Flood polls do not consume allowance or refresh replay/session state.
assert(mesh.post_acks == 8 && memcmp(&before, &mesh.acl.client, sizeof(before)) == 0);
puts("actual room quotas survive relogin, exempt exact retries, and reject without replay or ACK passed");
}
static void allRequestQuotaCases() {
for (uint8_t subtype : {uint8_t(REQ_TYPE_GET_STATUS), uint8_t(REQ_TYPE_GET_TELEMETRY_DATA),
uint8_t(REQ_TYPE_GET_ACCESS_LIST), uint8_t(ROOM_BOARD_REQUEST_SUBTYPE)}) {
MyMesh mesh; ticks = 100; seedKnown(mesh, PERM_ACL_ADMIN);
assert(mesh.room_access.setRates(&mesh.policy_fs, 0, 1));
mesh.request(1000, subtype, 8);
assert(mesh.requests == 1 && mesh.last_request_type == subtype && mesh.acl.client.extra.room.poll_quota_used == 1);
assert(mesh.acl.client.last_timestamp == 1000);
mesh.request(1000, subtype, 8); // Exact accepted request is re-readable without a second charge.
assert(mesh.requests == 2 && mesh.acl.client.extra.room.poll_quota_used == 1);
const auto before_reject = mesh.acl.client;
mesh.request(1001, subtype, 8); // New timestamp consumes a new allowance and is denied.
mesh.request(1000, subtype, 9); // Changed payload cannot masquerade as an exact retry.
assert(mesh.requests == 2 && memcmp(&before_reject, &mesh.acl.client, sizeof(before_reject)) == 0);
ticks = 60100; mesh.request(1001, subtype, 8);
assert(mesh.requests == 3 && mesh.acl.client.last_timestamp == 1001 && mesh.acl.client.extra.room.poll_quota_used == 1);
mesh.request(1000, subtype, 8); // Older accepted read may repeat without lowering the session replay floor.
assert(mesh.requests == 4 && mesh.acl.client.last_timestamp == 1001 && mesh.acl.client.extra.room.poll_quota_used == 1);
mesh.request(1000, subtype, 9);
assert(mesh.requests == 4 && mesh.acl.client.last_timestamp == 1001);
}
puts("actual request quota covers status, telemetry, ACL, board, and exact payload retries passed");
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!strcmp(argv[1], "authorization")) authorizationCases();
else if (!strcmp(argv[1], "roles")) preservedRoles();
else if (!strcmp(argv[1], "storage")) storageFailures();
else if (!strcmp(argv[1], "refresh")) sessionRefresh();
else if (!strcmp(argv[1], "post")) readOnlyPosting();
else if (!strcmp(argv[1], "moderation")) moderationCases();
else if (!strcmp(argv[1], "quotas")) quotaCases();
else if (!strcmp(argv[1], "requests")) allRequestQuotaCases();
else assert(false);
}
'''
class RoomLoginIntegrationTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shutil.which("g++") or shutil.which("clang++")
if compiler is None:
raise AssertionError("a host C++ compiler is required")
room = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
room_header = (ROOT / "examples/simple_room_server/MyMesh.h").read_text()
acl_header = (ROOT / "src/helpers/ClientACL.h").read_text()
acl_source = (ROOT / "src/helpers/ClientACL.cpp").read_text()
packet = (ROOT / "src/Packet.cpp").read_text()
# Keep the executed bodies, ClientInfo layout, and protocol labels tied
# to production. Only filesystem operations and radio allocation are fake.
constants = "\n".join(line for line in acl_header.splitlines()
if line.startswith(("#define PERM_ACL_", "#define OUT_PATH_")))
for name in ("PUSH_NOTIFY_DELAY_MILLIS", "FIRMWARE_VER_LEVEL", "RESP_SERVER_LOGIN_OK",
"LAZY_CONTACTS_WRITE_DELAY", "ROOM_MESSAGE_CACHE_SIZE", "TXT_TYPE_PLAIN",
"TXT_TYPE_CLI_DATA", "TXT_TYPE_CLI_COMMAND", "TXT_ACK_DELAY", "REPLY_DELAY_MILLIS", "REQ_TYPE_KEEP_ALIVE", "REQ_TYPE_GET_STATUS", "REQ_TYPE_GET_TELEMETRY_DATA", "REQ_TYPE_GET_ACCESS_LIST"):
match = re.search(r"^\s*#define\s+" + name + r"\s+.*$", room, re.MULTILINE)
if match is None:
for path in (ROOT / "examples/simple_room_server/MyMesh.h", ROOT / "src/Mesh.h",
ROOT / "src/helpers/TxtDataHelpers.h", ROOT / "src/Packet.h"):
if path.exists():
match = re.search(r"^\s*#define\s+" + name + r"\s+.*$", path.read_text(), re.MULTILINE)
if match is not None:
break
if match is None:
raise AssertionError("production constant not found: " + name)
constants += "\n" + match.group()
board = (ROOT / "src/helpers/RoomBoardProtocol.h").read_text()
board_subtype = re.search(r"static constexpr uint8_t ROOM_BOARD_REQUEST_SUBTYPE = ([^;]+);", board)
if board_subtype is None:
raise AssertionError("production board request subtype not found")
constants += "\n#define ROOM_BOARD_REQUEST_SUBTYPE " + board_subtype[1]
delay = re.search(r"^\s*#define\s+SERVER_RESPONSE_DELAY\s+.*$", room_header, re.MULTILINE)
if delay is None:
raise AssertionError("production SERVER_RESPONSE_DELAY not found")
constants += "\n" + delay.group()
replacements = {
"@CONSTANTS@": constants,
"@PACKET_CONSTRUCTOR@": extract_braced(packet, "Packet::Packet()"),
"@PACKET_PATH_CHECK@": extract_braced(packet, "bool Packet::isValidPathLen("),
"@CLIENT_INFO@": extract_braced(acl_header, "struct ClientInfo {"),
"@REPLAY_HANDLER@": extract_braced(acl_source, "bool ClientACL::authorizeLoginTimestamp("),
"@PEER_HANDLER@": extract_braced(room, "void MyMesh::onPeerDataRecv("),
"@PATH_HANDLER@": extract_braced(room, "bool MyMesh::onPeerPathRecv("),
"@QUOTA_SERVICE@": extract_braced(room, "void MyMesh::serviceRoomQuotas("),
"@SAVE_FILTER@": extract_braced(room, "bool MyMesh::saveFilter("),
"@LOGIN_HANDLER@": extract_braced(room, "void MyMesh::onAnonDataRecv("),
}
generated = HARNESS
for marker, text in replacements.items():
generated = generated.replace(marker, text)
cls.directory = tempfile.TemporaryDirectory(prefix="room-login-integration-")
cls.addClassCleanup(cls.directory.cleanup)
work = Path(cls.directory.name)
source = work / "test.cpp"
source.write_text(generated)
cls.binary = work / "room-login"
command = [compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-I" + str(ROOT / "test/fixtures/room_history_store"), "-I" + str(ROOT / "src"),
str(source), "-o", str(cls.binary)]
if sys.platform.startswith("linux"):
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
if compiled.returncode != 0:
raise AssertionError(compiled.stdout + compiled.stderr)
def run_case(self, case, expected):
checked = subprocess.run([str(self.binary), case], capture_output=True, text=True, timeout=15)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn(expected, checked.stdout)
def test_unknown_blank_read_only_disabled_passwords_and_payload_bounds(self):
self.run_case("authorization", "real-handler credentials, disabled passwords, and minimum payload cases passed")
def test_non_demotion_all_roles_and_exact_replay_preserve_session(self):
self.run_case("roles", "2048 real-handler preserved roles and replay rejects plus 256 admin upgrades passed")
def test_auth_or_durable_replay_failure_precedes_acl_allocation_and_mutation(self):
self.run_case("storage", "real replay admission rejects failed storage, historical replay, and failed allocation passed")
def test_successful_relogin_refreshes_session_and_routes_without_resetting_post_floor(self):
self.run_case("refresh", "real-handler successful reconnect refreshes cursor, activity, secret, topic state, and route passed")
def test_all_room_request_types_consume_poll_budget_and_preserve_exact_retries(self):
self.run_case("requests", "actual request quota covers status, telemetry, ACL, board, and exact payload retries passed")
def test_full_identity_bans_apply_to_new_and_existing_sessions(self):
self.run_case("moderation", "actual room gates deny full-key bans on login, posts, polls, and paths passed")
def test_actual_post_and_poll_quotas_retries_and_relogin(self):
self.run_case("quotas", "actual room quotas survive relogin, exempt exact retries, and reject without replay or ACK passed")
def test_read_only_login_never_grants_post_permission(self):
self.run_case("post", "actual room post gate blocks logged-in readers/managers and accepts RW/Admin once passed")
if __name__ == "__main__":
unittest.main()