ZaStoGram_desktop/Telegram/SourceFiles/tests/test_e2e_cloud_inbound.cpp
2026-08-01 07:51:49 +03:00

349 lines
9.4 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "e2e_cloud/core/envelope_codec.h"
#include "e2e_cloud/protocol/inbound_envelope_processor.h"
#include <cstdio>
#include <optional>
namespace {
using namespace E2ECloud;
template <typename Id>
[[nodiscard]] Id FilledId(std::uint8_t value) {
auto result = Id();
result.bytes.fill(value);
return result;
}
[[nodiscard]] int Fail(const char *message) {
std::fprintf(stderr, "%s\n", message);
return 1;
}
[[nodiscard]] TransportEnvelope MakeEnvelope() {
return {
.conversationId = FilledId<ConversationId>(1),
.objectKind = ObjectKind::MlsApplication,
.senderAccountId = FilledId<AccountId>(2),
.senderClientId = FilledId<ClientId>(3),
.telegramPeerIdBinding = 42,
.epochOrGeneration = 7,
.objectId = FilledId<ObjectId>(4),
.payloadHash = FilledId<Digest>(5),
.payload = QByteArray("payload"),
.authenticationData = QByteArray("authentication"),
};
}
class TestAuthenticator final : public InboundEnvelopeAuthenticator {
public:
[[nodiscard]] bool authenticate(
const TransportEnvelope &) const override {
++calls;
return accept;
}
mutable int calls = 0;
bool accept = true;
};
class TestJournal final : public InboundEnvelopeJournal {
public:
[[nodiscard]] InboundJournalLookup lookup(
ConversationId conversationId,
ObjectId objectId,
Digest payloadHash) const override {
++lookupCalls;
if (storageError) {
return InboundJournalLookup::StorageError;
} else if (!entry) {
return InboundJournalLookup::Missing;
} else if (entry->conversationId != conversationId
|| entry->objectId != objectId) {
return InboundJournalLookup::Missing;
} else if (entry->payloadHash != payloadHash) {
return InboundJournalLookup::ObjectIdConflict;
}
return entry->accepted
? InboundJournalLookup::Accepted
: InboundJournalLookup::Pending;
}
bool begin(const TransportEnvelope &envelope) override {
++beginCalls;
if (failBegin) {
return false;
}
entry = Entry{
.conversationId = envelope.conversationId,
.objectId = envelope.objectId,
.payloadHash = envelope.payloadHash,
.accepted = false,
};
return true;
}
bool accept(
ConversationId conversationId,
ObjectId objectId) override {
++acceptCalls;
if (failAccept
|| !entry
|| entry->conversationId != conversationId
|| entry->objectId != objectId) {
return false;
}
entry->accepted = true;
return true;
}
bool abort(
ConversationId conversationId,
ObjectId objectId) override {
++abortCalls;
if (failAbort
|| !entry
|| entry->conversationId != conversationId
|| entry->objectId != objectId) {
return false;
}
entry.reset();
return true;
}
struct Entry {
ConversationId conversationId;
ObjectId objectId;
Digest payloadHash;
bool accepted = false;
};
std::optional<Entry> entry;
mutable int lookupCalls = 0;
int beginCalls = 0;
int acceptCalls = 0;
int abortCalls = 0;
bool storageError = false;
bool failBegin = false;
bool failAccept = false;
bool failAbort = false;
};
class TestApplier final : public InboundEnvelopeApplier {
public:
[[nodiscard]] InboundApplyResult apply(
const TransportEnvelope &envelope) override {
++calls;
lastEnvelope = envelope;
return result;
}
[[nodiscard]] InboundRecoveryResult recover(
const TransportEnvelope &) const override {
++recoverCalls;
return recovery;
}
InboundApplyResult result = InboundApplyResult::Applied;
InboundRecoveryResult recovery = InboundRecoveryResult::Unknown;
int calls = 0;
mutable int recoverCalls = 0;
std::optional<TransportEnvelope> lastEnvelope;
};
struct Fixture {
Fixture()
: encoded(*codec.encode(envelope))
, processor(
envelope.conversationId,
envelope.telegramPeerIdBinding,
codec,
authenticator,
journal,
applier) {
}
EnvelopeCodecV1 codec;
TransportEnvelope envelope = MakeEnvelope();
EncodedEnvelope encoded;
TestAuthenticator authenticator;
TestJournal journal;
TestApplier applier;
InboundEnvelopeProcessor processor;
};
[[nodiscard]] int ScenarioAcceptedThenDeduplicated() {
auto fixture = Fixture();
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::Accepted
|| fixture.applier.calls != 1
|| !fixture.journal.entry
|| !fixture.journal.entry->accepted
|| fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::Duplicate
|| fixture.applier.calls != 1
|| fixture.authenticator.calls != 2) {
return Fail("authenticated inbound object was not deduplicated");
}
return 0;
}
[[nodiscard]] int ScenarioRejectsBeforeJournalMutation() {
auto fixture = Fixture();
fixture.authenticator.accept = false;
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::AuthenticationFailed
|| fixture.journal.lookupCalls
|| fixture.journal.beginCalls
|| fixture.applier.calls) {
return Fail("unauthenticated inbound object reached the journal");
}
auto wrongCarrier = fixture.envelope;
wrongCarrier.telegramPeerIdBinding = 99;
const auto encoded = fixture.codec.encode(wrongCarrier);
if (!encoded
|| fixture.processor.process(encoded->bytes)
!= InboundProcessResult::WrongCarrier
|| fixture.authenticator.calls != 1) {
return Fail("wrong carrier binding reached authentication");
}
return 0;
}
[[nodiscard]] int ScenarioDetectsAuthenticatedObjectIdConflict() {
auto fixture = Fixture();
fixture.journal.entry = TestJournal::Entry{
.conversationId = fixture.envelope.conversationId,
.objectId = fixture.envelope.objectId,
.payloadHash = FilledId<Digest>(9),
.accepted = true,
};
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::ObjectIdConflict
|| fixture.authenticator.calls != 1
|| fixture.applier.calls) {
return Fail("authenticated object identifier conflict was accepted");
}
return 0;
}
[[nodiscard]] int ScenarioPendingRequiresRecovery() {
auto fixture = Fixture();
fixture.journal.entry = TestJournal::Entry{
.conversationId = fixture.envelope.conversationId,
.objectId = fixture.envelope.objectId,
.payloadHash = fixture.envelope.payloadHash,
.accepted = false,
};
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::RecoveryRequired
|| fixture.applier.calls
|| fixture.applier.recoverCalls != 1) {
return Fail("uncertain post-crash apply state was replayed blindly");
}
return 0;
}
[[nodiscard]] int ScenarioPendingNotAppliedRetriesSafely() {
auto fixture = Fixture();
fixture.journal.entry = TestJournal::Entry{
.conversationId = fixture.envelope.conversationId,
.objectId = fixture.envelope.objectId,
.payloadHash = fixture.envelope.payloadHash,
.accepted = false,
};
fixture.applier.recovery = InboundRecoveryResult::NotApplied;
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::Accepted
|| fixture.applier.recoverCalls != 1
|| fixture.applier.calls != 1
|| fixture.journal.abortCalls != 1
|| fixture.journal.beginCalls != 1
|| fixture.journal.acceptCalls != 1
|| !fixture.journal.entry
|| !fixture.journal.entry->accepted) {
return Fail("known pre-apply crash did not retry the inbound object");
}
return 0;
}
[[nodiscard]] int ScenarioPendingAppliedCompletesJournal() {
auto fixture = Fixture();
fixture.journal.entry = TestJournal::Entry{
.conversationId = fixture.envelope.conversationId,
.objectId = fixture.envelope.objectId,
.payloadHash = fixture.envelope.payloadHash,
.accepted = false,
};
fixture.applier.recovery = InboundRecoveryResult::Applied;
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::Accepted
|| fixture.applier.recoverCalls != 1
|| fixture.applier.calls
|| fixture.journal.acceptCalls != 1
|| !fixture.journal.entry
|| !fixture.journal.entry->accepted) {
return Fail("known post-apply crash replayed the inbound object");
}
return 0;
}
[[nodiscard]] int ScenarioDeferredApplyCanRetry() {
auto fixture = Fixture();
fixture.applier.result = InboundApplyResult::Deferred;
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::Deferred
|| fixture.journal.entry
|| fixture.journal.abortCalls != 1) {
return Fail("deferred inbound object remained permanently pending");
}
fixture.applier.result = InboundApplyResult::Applied;
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::Accepted) {
return Fail("deferred inbound object could not be retried");
}
return 0;
}
[[nodiscard]] int ScenarioCommitFailureFailsClosed() {
auto fixture = Fixture();
fixture.journal.failAccept = true;
if (fixture.processor.process(fixture.encoded.bytes)
!= InboundProcessResult::RecoveryRequired
|| !fixture.journal.entry
|| fixture.journal.entry->accepted
|| fixture.applier.calls != 1) {
return Fail("journal commit failure lost uncertain apply state");
}
return 0;
}
} // namespace
int main(int, char *[]) {
for (const auto scenario : {
ScenarioAcceptedThenDeduplicated,
ScenarioRejectsBeforeJournalMutation,
ScenarioDetectsAuthenticatedObjectIdConflict,
ScenarioPendingRequiresRecovery,
ScenarioPendingNotAppliedRetriesSafely,
ScenarioPendingAppliedCompletesJournal,
ScenarioDeferredApplyCanRetry,
ScenarioCommitFailureFailsClosed,
}) {
if (const auto result = scenario()) {
return result;
}
}
return 0;
}