Files
rust-xmpp/tests/muc_state_native.rs
T
wt 7f60661eef - MUC сохраняет согласованный журнал конфигурации, affiliations, nick,
ролей,
  членства и ограниченной истории без MAM. Изменение фиксируется до
  публикации
  ответа; при ошибке записи состояние комнаты откатывается.
- Crash recovery использует последнее сохранённое поколение после
  SIGKILL,
  включая временные комнаты. Heartbeat обновляет живое членство;
  ожидающие записи сохраняют исходный deadline через повторные
  перезапуски.
  `muc_recovery_grace_period`: по умолчанию 300 секунд, пределы
  1..604800.
- Подтверждённые выходы, смена nick, ролей и политик комнаты не
  заменяются
  старой записью восстановления. Native config/state остаются экспортом
  для
  Prosody; согласованное восстановление Rust использует собственный
  sidecar.
- Исправлены MUC probe, неизвестные типы presence, вход без MUC `<x/>`,
  ошибки
  доставки и статус 333. Обычные presence extensions сохраняются в живом
  обмене; поддельная MUC identity фильтруется. `subject` вместе с `body`
  либо
  `thread` не изменяет тему комнаты.
- Подключены `muc_max_rooms`, `restrict_room_creation`, политики
  регистрации
  и зарегистрированного nick, `muc_tombstone_expiry`; добавлены
  встроенные
  административные ad-hoc команды уничтожения комнат и изменения
  affiliation.
- Hub сохраняет resumable XEP-0198 сессии, счётчики и очередь. Resume
  после
  перезапуска доступен на новом TLS/SASL-транспорте через legacy SASL и
  SASL2.
  Действующие приватные SM-записи восстанавливают логические сессии и
  MUC
  до открытия listeners: offline-доставка попадает в очередь,
  синтетический
  join и история повторно не отправляются. SM resume не требует
  дополнительной
  available presence; без SM восстановление MUC требует новой
  аутентификации
  точного полного JID и available presence либо явного MUC-входа.
  Входящий handled-счётчик обновляется после обработки. Сокеты TCP/TLS
  не сохраняются; неопределённая сетевая отправка допускает повторную
  доставку.
- Известные встроенные MUC-возможности Prosody отображаются на
  Rust-реализацию.
  Произвольные Lua/community-модули не исполняются; неизвестные явно
  включённые
  MUC-модули отклоняются при запуске. Общие и host `modules_disabled`
  учитываются
  для vCard и occupant-id.

Порядок восстановления, ограничения хранения и отката на Prosody:
[docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).

Проверка: 293 Rust tests, Clippy/fmt/release, полный сетевой прогон, 500
TLS
MUC+SM ресурсов с SIGKILL/resume и native reference — успешно.
Подробности: [docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md).
2026-10-20 17:34:43 +07:00

142 lines
5.6 KiB
Rust

//! Native freeze(true)/restore differential coverage for MUC occupant checkpoints.
use rust_xmpp::{
muc::Muc,
muc_recovery::{OccupantSnapshot, load_room, save_room},
};
const DOMAIN: &str = "rooms.example.org";
const ROOM: &str = "room@rooms.example.org";
#[test]
#[ignore = "requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA"]
fn actual_prosody_restores_rust_checkpoint_and_rust_reads_native_freeze() {
let root = tempfile::tempdir().unwrap();
// Let the real room implementation create its normal configuration/index.
let mut muc = Muc::new(root.path().into(), DOMAIN.into());
muc.set_component_options(serde_json::json!({"muc_room_locking":false}), false);
muc.set_defaults(serde_json::json!({"persistent":true,"whois":"moderators"}));
let join = roxmltree::Document::parse(
"<presence to='room@rooms.example.org/Alice'><x xmlns='http://jabber.org/protocol/muc'/><status>Rust desktop</status></presence>",
)
.unwrap();
muc.handle("alice@example.org/desktop", join.root_element())
.unwrap()
.unwrap();
drop(muc);
let snapshots = vec![
OccupantSnapshot {
jid: "alice@example.org/desktop".into(),
nick: "Alice".into(),
role: "moderator".into(),
presence_payload: "<show>away</show><status>Rust desktop</status>".into(),
},
OccupantSnapshot {
jid: "alice@example.org/mobile".into(),
nick: "Alice".into(),
role: "moderator".into(),
presence_payload: "<status>Rust mobile</status><priority>3</priority>".into(),
},
OccupantSnapshot {
jid: "bob@example.org/native".into(),
nick: "Bob".into(),
role: "participant".into(),
presence_payload: "<status>Rust Bob</status>".into(),
},
];
save_room(root.path(), DOMAIN, ROOM, &snapshots).unwrap();
let bootstrap = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/scripts/prosody_compat/oracle.lua"
))
.unwrap();
let bootstrap = bootstrap.split("if phase == 'probe' then").next().unwrap();
let script = format!(
"{bootstrap}\n{}",
r#"
config.set('rooms.example.org','component_module','muc');
assert(require('prosody.core.hostmanager').activate('rooms.example.org'));
local muc=assert(mm.get_module('rooms.example.org','muc'));
local room=assert(muc.get_room_from_jid('room@rooms.example.org'));
assert(room:get_persistent());
local alice=assert(room:get_occupant_by_nick('room@rooms.example.org/Alice'));
assert(alice.bare_jid=='alice@example.org' and alice.role=='moderator');
local sessions=0;
for jid,presence in alice:each_session() do
sessions=sessions+1;
assert(presence.attr.from=='room@rooms.example.org/Alice');
if jid=='alice@example.org/desktop' then
assert(presence:get_child_text('show')=='away');
assert(presence:get_child_text('status')=='Rust desktop');
elseif jid=='alice@example.org/mobile' then
assert(presence:get_child_text('status')=='Rust mobile');
assert(presence:get_child_text('priority')=='3');
else error('unexpected Alice session '..jid); end
end
assert(sessions==2,'native restore lost a resource');
local bob=assert(room:get_occupant_by_nick('room@rooms.example.org/Bob'));
assert(bob.bare_jid=='bob@example.org' and bob.role=='participant');
assert(bob:get_presence():get_child_text('status')=='Rust Bob');
-- Change the native occupant and serialize through upstream freeze(true).
bob.role='visitor';
bob:set_session('bob@example.org/native',st.presence({xmlns='jabber:client'})
:tag('show'):text('dnd'):up():tag('status'):text('Native Bob & ready'):up());
room:save_occupant(bob);
local frozen,state=room:freeze(true);
assert(state['room@rooms.example.org/Alice'].role=='moderator');
assert(state['room@rooms.example.org/Bob'].role=='visitor');
assert(state['alice@example.org/desktop'].name=='presence');
assert(state['alice@example.org/mobile'].name=='presence');
assert(room:save(nil,true));
print('NATIVE_MUC_STATE_ROUNDTRIP_OK');
"#
);
let script_path = root.path().join("native-muc-state.lua");
std::fs::write(&script_path, script).unwrap();
let output = std::process::Command::new(
std::env::var("PROSODY_REFERENCE_LUA").unwrap_or("lua5.4".into()),
)
.arg(&script_path)
.arg(std::env::var("PROSODY_REFERENCE_SOURCE").unwrap())
.arg(root.path())
.arg("muc-state")
.output()
.unwrap();
assert!(
output.status.success(),
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("NATIVE_MUC_STATE_ROUNDTRIP_OK"));
let recovered = load_room(root.path(), DOMAIN, ROOM).unwrap();
assert_eq!(recovered.len(), 3, "{recovered:?}");
for original in &snapshots[..2] {
assert_eq!(
recovered.iter().find(|o| o.jid == original.jid),
Some(original),
"native freeze must preserve both Alice sessions"
);
}
let bob = recovered
.iter()
.find(|o| o.jid == "bob@example.org/native")
.unwrap();
assert_eq!(bob.nick, "Bob");
assert_eq!(bob.role, "visitor");
let wrapped = format!("<presence>{}</presence>", bob.presence_payload);
let payload = roxmltree::Document::parse(&wrapped).unwrap();
let text = |name: &str| {
payload
.root_element()
.children()
.find(|n| n.has_tag_name(name))
.and_then(|n| n.text())
};
assert_eq!(text("show"), Some("dnd"));
assert_eq!(text("status"), Some("Native Bob & ready"));
}