tests: cover subgroup file receipts and endpoint storage policy across restart

This commit is contained in:
evgeny committed 2026-11-03 13:39:32 +01:00
1 parent 45f4ca0f31
commit 576236daeb
2 files changed
+108 -3

No files matched your search

+48
View File
@@ -8,6 +8,8 @@
#include "dm/dm_core.h"
#include "dm/dm_crypto.h"
#include "dm/dm_history.h"
#include "dm/dm_media_receipt.h"
#include "media_async/media_crypto.h"
#include "../lib/debug_config.h"
#include "test_utils.h"
#include <assert.h>
@@ -95,6 +97,51 @@ static size_t send_body(int from, const char* conv, const char* text, uint8_t bo
memcpy(body, sqlite3_column_blob(st, 0), len); sqlite3_finalize(st); return len;
}
/* Четыре участника раскрывают один файл; политика M считает только устройства получателя. */
static void group_file(const char* conv, const char* root) {
uint8_t data[1024] = {7}, key[32], secret[32], restored[32];
struct dm_party parties[2];
assert(!dm_conversation_access(nodes[0], conv, parties, NULL));
memcpy(data + DM_MEDIA_DESC_SIZE, "DMKEY001", 8);
uint8_t* keys = data + DM_MEDIA_DESC_SIZE + 8;
uint64_t id = strtoull(conv, NULL, 10);
assert(!dm_file_keys_create(id, nodes[0]->node_id, data, parties, keys, key));
for (unsigned i = 0; i < 4; i++) {
assert(!dm_party_private(nodes[i], parties + i / 2, secret));
assert(!dm_file_keys_open(id, nodes[0]->node_id, data, parties, i / 2, secret, keys, restored));
assert(!memcmp(key, restored, sizeof(key)));
}
char path[1024], cipher[1024];
snprintf(path, sizeof(path), "%s/0/dm_media/07000000000000000000000000000000.file", root);
snprintf(cipher, sizeof(cipher), "%s/0/dm_media/07000000000000000000000000000000.cipher", root);
FILE* plain = fopen(path, "wb+"); FILE* encrypted = fopen(cipher, "wb"); assert(plain && encrypted);
assert(fwrite("payload", 1, 7, plain) == 7); rewind(plain); uint64_t size;
assert(!media_encrypt_stream(key, nodes[0]->node_id, data, plain, encrypted, &size, data + 24));
assert(!fclose(plain) && !fclose(encrypted)); memcpy(data + 16, &size, 8);
struct attachment_info info = {.kind = ATTACHMENT_FILE}; snprintf(info.name, sizeof(info.name), "group.bin");
int n = attachment_encode(&info, data + DM_MEDIA_DESC_SIZE + DM_MEDIA_KEYS_HEADER_SIZE, ATTACHMENT_WIRE_MAX); assert(n > 0);
assert(!dm_send(nodes[0], conv, "file", data, DM_MEDIA_DESC_SIZE + DM_MEDIA_KEYS_HEADER_SIZE + n));
sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(nodes[0]->topo_sqlite_db, "SELECT body FROM dm_files", -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW); uint8_t body[DM_BODY_MAX], r[DM_MEDIA_RECEIPT_SIZE], ack[DM_RECEIPT_SIZE];
size_t len = (size_t)sqlite3_column_bytes(st, 0); memcpy(body, sqlite3_column_blob(st, 0), len); sqlite3_finalize(st);
for (unsigned i = 1; i < 4; i++) assert(!dm_accept_message(nodes[i], body, len, ack));
struct dm_message_view view; assert(!dm_message_parse(body, len, &view));
assert(dm_media_user_action(nodes[2], conv, view.author, view.seq, 1) < 0); /* Само наличие сообщения — не открытие. */
assert(dm_media_receipt_make(nodes[1], body, len, 1, r) < 0); /* Свой узел не удаляет серверный файл. */
assert(dm_media_receipt_make(nodes[4], body, len, 0, r) < 0);
assert(!dm_media_retention_done(nodes[0], data, 0) && !dm_media_retention_done(nodes[0], data, 1));
assert(!dm_media_receipt_make(nodes[2], body, len, 0, r) && !dm_media_receipt_save(nodes[0], r, body, len));
assert(!dm_media_receipt_save(nodes[0], r, body, len)); /* Повтор не увеличивает число получателей. */
assert(!dm_media_retention_done(nodes[0], data, 0) && !dm_media_retention_done(nodes[0], data, 1));
assert(!dm_media_receipt_make(nodes[2], body, len, 1, r) && !dm_media_receipt_save(nodes[0], r, body, len));
assert(!dm_media_retention_done(nodes[0], data, 0) && dm_media_retention_done(nodes[0], data, 1) == 1);
assert(!dm_media_receipt_make(nodes[3], body, len, 0, r) && !dm_media_receipt_save(nodes[0], r, body, len));
assert(dm_media_retention_done(nodes[0], data, 0) == 1);
assert(scalar(0, "SELECT count(*) FROM dm_file_targets WHERE side=0 AND received IS NULL") == 1);
r[56] ^= 1; assert(dm_media_receipt_save(nodes[0], r, body, len) < 0);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_DM, DEBUG_LEVEL_DEBUG);
char root[512] = "/tmp/utun_dm_groups_XXXXXX"; assert(!test_mkdtemp(root));
@@ -131,6 +178,7 @@ int main(void) {
assert(view.mode == DM_MODE_DEVICE && view.seq == 2 && !dm_accept_message(nodes[4], reply, n, receipt));
assert(start(0, 1, DM_MODE_DEVICE, other) == dm_derive_conv_id(nodes[0]->node_id, nodes[1]->node_id));
n = send_body(0, other, "own device", reply); assert(!dm_accept_message(nodes[1], reply, n, receipt));
group_file(conv, root);
/* Выход атомарен: ошибка SQL не удаляет секреты и не закрывает беседу. */
assert(!sqlite3_exec(nodes[0]->topo_sqlite_db, "CREATE TEMP TRIGGER fail_close BEFORE UPDATE OF closed ON dm_conversations"
" BEGIN SELECT RAISE(ABORT,'injected closure failure'); END", NULL, NULL, NULL));
+60 -3
View File
@@ -524,6 +524,22 @@ static int send_fixture(struct media_test* t, enum attachment_kind kind, const c
return dm_send_file(t->nodes[A], t->conv, t->file, &info, 0, 0);
}
/* Перенести подписанную роль M на автора: личная копия и custody должны жить независимо. */
static int storage_on_author(struct media_test* t) {
for (int dst = 0; dst < NODES; dst++) for (int src = A; src <= M; src += M - A) {
uint8_t msg[512], join[64], adm[64]; uint64_t ts = ntp_time_get_seconds(t->nodes[dst]);
int n = member_sync_build_join_msg(t->ch_x, t->ch_ed, t->ids[src], t->keys[src].public_key, ts, msg, sizeof(msg));
if (n < 0 || sc_ed25519_sign(t->nodes[src]->my_ed25519_privkey, msg, (size_t)n, join)) return -1;
const char* tags = src == A ? "{\"storage\":\"yes\",\"ver\":\"2\"}" : "{\"storage\":\"no\",\"ver\":\"2\"}";
size_t size = strlen(tags); memcpy(msg, tags, size); memcpy(msg + size, &t->ids[src], 8);
if (sc_ed25519_sign(t->ch_priv, msg, size + 8, adm) ||
member_sync_put(t->nodes[dst], CHANNEL, t->ids[src], t->keys[src].public_key, t->ed[src], join, ts,
NULL, 0, NULL, tags, adm, 0, 0, NULL) < 0) return -1;
}
return chat_setting_set(t->nodes[A], "dm_media_delete_on_open", "1") ||
chat_setting_set(t->nodes[B], "storage_autoload", "0");
}
static void tick(void* arg) {
struct media_test* t = arg;
t->timer = NULL; t->ticks++; t->heartbeats++;
@@ -748,7 +764,10 @@ static void tick(void* arg) {
failure(t, "late EXPIRED changed verified file or revived custody"); return;
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM recovery test: exact original, signed receipt, S cleanup, late EXPIRED PASS");
t->phase++; uasync_stop(t->ua); return;
if (storage_on_author(t) || send_fixture(t, ATTACHMENT_FILE, "manual.bin")) {
failure(t, "colocated storage fixture"); return;
}
t->phase++; break;
}
if (ntp_time_get_seconds(b) >= t->recovery_start + 16) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM recovery test: timeout A_to_B=%d B_to_A=%d B_ready=%d B_receipt=%d",
@@ -757,6 +776,44 @@ static void tick(void* arg) {
failure(t, "available original cannot complete receive after EXPIRED"); return;
}
break;
case 12:
if (value(a, "SELECT count(*) FROM dm_custody WHERE ready=1 AND delete_on_open=1") == 1 &&
value(b, "SELECT count(*) FROM dm_files WHERE seq=4 AND ready=0 AND receipt IS NULL") == 1) {
if (snapshot(t, 4)) { failure(t, "manual file snapshot"); return; }
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM colocation: manual B remained pending, A stored independent ciphertext; restart A");
utun_instance_destroy(a); t->nodes[A] = NULL;
if (start_node(t, A)) { failure(t, "colocated A restart"); return; }
t->phase++;
}
break;
case 13:
if (live(b, t->ids[A]) && live(a, t->ids[B])) {
if (value(a, "SELECT count(*) FROM dm_custody WHERE ready=1 AND delete_on_open=1") != 1 ||
chat_setting_get_int(a, "dm_media_delete_on_open", -1) != 0 ||
dm_media_user_action(b, t->conv, t->ids[A], 4, 0)) { failure(t, "frozen policy/manual download"); return; }
t->phase++;
}
break;
case 14:
if (file_correct(t) && value(a, "SELECT count(*) FROM dm_files WHERE seq=4 AND receipt IS NOT NULL") == 1) {
if (value(a, "SELECT count(*) FROM dm_custody WHERE ready=1 AND delete_on_open=1") != 1) {
failure(t, "FILE_RECEIVED deleted delete-on-open custody"); return;
}
if (dm_media_user_action(b, t->conv, t->ids[A], 4, 1)) { failure(t, "user open acknowledgement"); return; }
t->phase++;
}
break;
case 15:
if (value(a, "SELECT count(*) FROM dm_custody") == 0 &&
value(s, "SELECT count(*) FROM dm_media_jobs WHERE seq=4 AND receipt IS NOT NULL AND body IS NULL") == 1) {
if (!file_correct(t) || value(a, "SELECT count(*) FROM dm_files WHERE seq=4 AND ready=1") != 1 ||
value(a, "SELECT count(*) FROM dm_custody_done WHERE delete_on_open=1") != 1) {
failure(t, "server deletion changed personal replica"); return;
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM colocation: frozen policy survived restart, manual download != opening, TAKEN retired only custody PASS");
t->phase++; uasync_stop(t->ua); return;
}
break;
}
if (t->failed) return;
if (t->ticks > 18000) { failure(t, "timeout"); return; }
@@ -815,6 +872,6 @@ int main(void) {
printf("DM media allocations: %zu->%zu\n", allocations, u_get_allocated_count());
if (allocations != u_get_allocated_count()) t.failed = 1;
printf("test_dm_media: phase=%u heartbeats=%u %s artifacts=%s\n", t.phase, t.heartbeats,
!t.failed && t.phase == 12 ? "PASS" : "FAIL", t.dir);
return t.failed || t.phase != 12;
!t.failed && t.phase == 16 ? "PASS" : "FAIL", t.dir);
return t.failed || t.phase != 16;
}