Page MenuHomePhorge

No OneTemporary

Size
70 KB
Referenced Files
None
Subscribers
None
diff --git a/src/tests/client/account-data-test.cpp b/src/tests/client/account-data-test.cpp
index 49ba9fa..4b8f862 100644
--- a/src/tests/client/account-data-test.cpp
+++ b/src/tests/client/account-data-test.cpp
@@ -1,403 +1,396 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_test_macros.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <boost/asio.hpp>
#include <cprjobhandler.hpp>
#include <asio-promise-handler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <cursorutil.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include "client-test-util.hpp"
-
+#include "action-mock-utils.hpp"
#include "factory.hpp"
using namespace Kazv::Factory;
Event accountDataEvent = R"({
"type": "moe.kazv.mxc.kazv.some-event",
"content": {
"test": 1
}
})"_json;
TEST_CASE("Send set account data by room job", "[client][account-data]")
{
ClientModel loggedInModel = makeClient({});
auto [resModel, dontCareEffect] = ClientModel::update(
loggedInModel, SetAccountDataPerRoomAction{"!room:example.com", accountDataEvent});
assert1Job(resModel);
for1stJob(resModel, [] (const auto &job) {
REQUIRE(job.jobId() == "SetAccountDataPerRoom");
REQUIRE(job.url().find("/rooms/!room:example.com") != std::string::npos);
REQUIRE(job.url().find("/account_data/moe.kazv.mxc.kazv.some-event") != std::string::npos);
auto jsonBody = json::parse(std::get<BytesBody>(job.requestBody()));
REQUIRE(jsonBody == accountDataEvent.content().get());
});
}
TEST_CASE("Process account data by room response", "[client][account-data]")
{
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto store = createTestClientStore(ph);
WHEN("Success response")
{
auto succResponse = makeResponse("SetAccountDataPerRoom");
store.dispatch(ProcessResponseAction{succResponse})
.then([] (auto stat) {
REQUIRE(stat.success());
});
}
WHEN("Failed response")
{
auto failResponse = makeResponse("SetAccountDataPerRoom", withResponseJsonBody(R"({
"errcode": "M_FORBIDDEN",
"error": "Cannot add account data for other users."
})"_json));
failResponse.statusCode = 403;
store.dispatch(ProcessResponseAction{failResponse})
.then([] (auto stat) {
REQUIRE(!stat.success());
REQUIRE(stat.dataStr("error") == "Cannot add account data for other users.");
REQUIRE(stat.dataStr("errorCode") == "M_FORBIDDEN");
});
}
io.run();
}
TEST_CASE("Room::setAccountData()", "[client][account-data]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
ClientModel m = makeClient(
withRoom(makeRoom(
withRoomId("!room:example.com")
))
);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto setAccountDataCalled = false;
- auto mockContext = typename Client::ContextT([&ph, &setAccountDataCalled](const auto &action) {
- if (std::holds_alternative<SetAccountDataPerRoomAction>(action)) {
- setAccountDataCalled = true;
- auto a = std::get<SetAccountDataPerRoomAction>(action);
- REQUIRE(a.roomId == "!room:example.com");
- REQUIRE(a.accountDataEvent == accountDataEvent);
- return ph.createResolved(EffectStatus(true, json::object()));
- }
- throw std::runtime_error{"unhandled action"};
- }, ph, lager::deps<>{});
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataPerRoomAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!room:example.com");
r.setAccountData(accountDataEvent)
.then([&io](auto) {
io.stop();
});
io.run();
- REQUIRE(setAccountDataCalled);
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataPerRoomAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataPerRoomAction>()[0];
+ REQUIRE(a.roomId == "!room:example.com");
+ REQUIRE(a.accountDataEvent == accountDataEvent);
}
Event tagEvent = R"({
"type": "m.tag",
"content": {
"tags": {
"m.favourite": 0
}
}
})"_json;
TEST_CASE("Room::addOrSetTag()", "[client][account-data][tagging]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
ClientModel m = makeClient(
withRoom(makeRoom(
withRoomId("!room:example.com")
| withRoomAccountData({tagEvent})
))
);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto setAccountDataCalled = false;
auto expectedEvent = Event();
- auto mockContext = typename Client::ContextT([&ph, &expectedEvent, &setAccountDataCalled](const auto &action) {
- if (std::holds_alternative<SetAccountDataPerRoomAction>(action)) {
- setAccountDataCalled = true;
- auto a = std::get<SetAccountDataPerRoomAction>(action);
- REQUIRE(a.roomId == "!room:example.com");
- REQUIRE(a.accountDataEvent == expectedEvent);
- return ph.createResolved(EffectStatus(true, json::object()));
- }
- throw std::runtime_error{"unhandled action"};
- }, ph, lager::deps<>{});
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataPerRoomAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!room:example.com");
SECTION("adding new tag")
{
auto expectedJson = tagEvent.raw().get();
expectedJson["content"]["tags"]["u.xxx"] = {{"order", 0.2}};
expectedEvent = expectedJson;
r.addOrSetTag("u.xxx", 0.2)
.then([&io](auto) {
io.stop();
});
}
SECTION("adding new tag, no order")
{
auto expectedJson = tagEvent.raw().get();
expectedJson["content"]["tags"]["u.xxx"] = json::object();
expectedEvent = expectedJson;
r.addOrSetTag("u.xxx")
.then([&io](auto) {
io.stop();
});
}
SECTION("updating existing tag")
{
auto expectedJson = tagEvent.raw().get();
expectedJson["content"]["tags"]["m.favourite"] = {{"order", 0.5}};
expectedEvent = expectedJson;
r.addOrSetTag("m.favourite", 0.5)
.then([&io](auto) {
io.stop();
});
}
SECTION("updating existing tag, no order")
{
auto expectedJson = tagEvent.raw().get();
expectedJson["content"]["tags"]["m.favourite"] = json::object();
expectedEvent = expectedJson;
r.addOrSetTag("m.favourite")
.then([&io](auto) {
io.stop();
});
}
io.run();
- REQUIRE(setAccountDataCalled);
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataPerRoomAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataPerRoomAction>()[0];
+ REQUIRE(a.roomId == "!room:example.com");
+ REQUIRE(a.accountDataEvent == expectedEvent);
}
TEST_CASE("Room::removeTag()", "[client][account-data][tagging]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
ClientModel m = makeClient(
withRoom(makeRoom(
withRoomId("!room:example.com")
| withRoomAccountData({tagEvent})
))
);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto setAccountDataCalled = false;
auto expectedEvent = Event();
- auto mockContext = typename Client::ContextT([&ph, &expectedEvent, &setAccountDataCalled](const auto &action) {
- if (std::holds_alternative<SetAccountDataPerRoomAction>(action)) {
- setAccountDataCalled = true;
- auto a = std::get<SetAccountDataPerRoomAction>(action);
- REQUIRE(a.roomId == "!room:example.com");
- REQUIRE(a.accountDataEvent == expectedEvent);
- return ph.createResolved(EffectStatus(true, json::object()));
- }
- throw std::runtime_error{"unhandled action"};
- }, ph, lager::deps<>{});
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataPerRoomAction>()
+ );
+
+ auto mockContext = getMockContext(ph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!room:example.com");
SECTION("removing existing tag")
{
auto expectedJson = tagEvent.raw().get();
expectedJson["content"]["tags"] = json::object();
expectedEvent = expectedJson;
r.removeTag("m.favourite")
.then([&io](auto) {
io.stop();
});
}
SECTION("removing non-existent tag")
{
expectedEvent = tagEvent;
r.removeTag("u.xxx")
.then([&io](auto) {
io.stop();
});
}
io.run();
- REQUIRE(setAccountDataCalled);
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataPerRoomAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataPerRoomAction>()[0];
+ REQUIRE(a.roomId == "!room:example.com");
+ REQUIRE(a.accountDataEvent == expectedEvent);
}
TEST_CASE("Send set account data job", "[client][account-data]")
{
ClientModel loggedInModel = makeClient({});
auto [resModel, dontCareEffect] = ClientModel::update(
loggedInModel, SetAccountDataAction{accountDataEvent});
assert1Job(resModel);
for1stJob(resModel, [loggedInModel] (const auto &job) {
REQUIRE(job.jobId() == "SetAccountData");
REQUIRE(job.url().find("/user/" + loggedInModel.userId) != std::string::npos);
REQUIRE(job.url().find("/account_data/moe.kazv.mxc.kazv.some-event") != std::string::npos);
auto jsonBody = json::parse(std::get<BytesBody>(job.requestBody()));
REQUIRE(jsonBody == accountDataEvent.content().get());
});
}
TEST_CASE("Process account data response", "[client][account-data]")
{
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto store = createTestClientStore(ph);
WHEN("Success response")
{
auto succResponse = makeResponse("SetAccountData");
store.dispatch(ProcessResponseAction{succResponse})
.then([] (auto stat) {
REQUIRE(stat.success());
});
}
WHEN("Failed response")
{
auto failResponse = makeResponse("SetAccountData", withResponseJsonBody(R"({
"errcode": "M_FORBIDDEN",
"error": "Cannot add account data for other users."
})"_json));
failResponse.statusCode = 403;
store.dispatch(ProcessResponseAction{failResponse})
.then([] (auto stat) {
REQUIRE(!stat.success());
REQUIRE(stat.dataStr("error") == "Cannot add account data for other users.");
REQUIRE(stat.dataStr("errorCode") == "M_FORBIDDEN");
});
}
io.run();
}
TEST_CASE("Client::accountData()", "[client][account-data]")
{
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
ClientModel m = makeClient(
withAccountData({accountDataEvent})
);
auto store = createTestClientStoreFrom(m, ph);
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }), store, std::nullopt);
REQUIRE(client.accountData().make().get() == m.accountData);
}
TEST_CASE("Client::setAccountData()", "[client][account-data]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
ClientModel m = makeClient({});
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto setAccountDataCalled = false;
- auto mockContext = typename Client::ContextT([&ph, &setAccountDataCalled](const auto &action) {
- if (std::holds_alternative<SetAccountDataAction>(action)) {
- setAccountDataCalled = true;
- auto a = std::get<SetAccountDataAction>(action);
- REQUIRE(a.accountDataEvent == accountDataEvent);
- return ph.createResolved(EffectStatus(true, json::object()));
- }
- throw std::runtime_error{"unhandled action"};
- }, ph, lager::deps<>{});
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<SetAccountDataAction>()
+ );
+ auto mockContext = getMockContext(ph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
client.setAccountData(accountDataEvent)
.then([&io](auto) {
io.stop();
});
io.run();
- REQUIRE(setAccountDataCalled);
+ REQUIRE(dispatcher.template calledTimes<SetAccountDataAction>() == 1);
+ auto a = dispatcher.template of<SetAccountDataAction>()[0];
+ REQUIRE(a.accountDataEvent == accountDataEvent);
}
diff --git a/src/tests/client/action-mock-utils.hpp b/src/tests/client/action-mock-utils.hpp
new file mode 100644
index 0000000..18c017f
--- /dev/null
+++ b/src/tests/client/action-mock-utils.hpp
@@ -0,0 +1,242 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+
+#include <vector>
+#include <boost/hana.hpp>
+#include <boost/core/demangle.hpp>
+#include <zug/into_vector.hpp>
+#include <zug/transducer/map.hpp>
+#include <zug/transducer/filter.hpp>
+#include <store/store.hpp>
+#include <client/client.hpp>
+
+template<class Action>
+struct PassDownTag
+{};
+
+template<class Action>
+constexpr PassDownTag<Action> passDown()
+{
+ return {};
+};
+
+template<class Action, class DataT = Kazv::EffectStatus>
+struct ReturnResolvedTag
+{
+ DataT data;
+};
+
+template<class Action, class DataT = Kazv::EffectStatus>
+constexpr ReturnResolvedTag<Action, DataT> returnResolved(DataT data)
+{
+ return {data};
+}
+
+template<class Action, class DataT = Kazv::EffectStatus>
+constexpr ReturnResolvedTag<Action, DataT> returnEmpty()
+{
+ return {{}};
+}
+
+template<class SubAction, class FuncT>
+struct HandlerTag : public FuncT
+{
+ HandlerTag(FuncT f) : FuncT(std::move(f)) {}
+};
+
+template<class SubAction, class FuncT>
+constexpr auto makeHandler(FuncT f)
+{
+ return HandlerTag<SubAction, FuncT>(std::move(f));
+};
+
+template<class R, class F, class = int, class ...Args>
+struct IsExactInvocableHelper : public std::false_type
+{};
+
+template<class R, class F, class ...Args>
+struct IsExactInvocableHelper<
+ R,
+ F,
+ std::enable_if_t<std::is_same_v<std::invoke_result_t<F, Args...>, R>, int>,
+ Args...> : public std::true_type
+{};
+
+template<class R, class F, class ...Args>
+constexpr auto isExactInvocable = IsExactInvocableHelper<R, F, int, Args...>::value;
+
+static_assert(isExactInvocable<int, std::function<int(long long)>, long long>);
+
+template<class T>
+std::string getTypeName(const T &a)
+{
+ return boost::core::demangle(typeid(a).name());
+}
+
+template<class Variant>
+std::string getVariantTypeName(const Variant &v)
+{
+ return std::visit([](const auto &a) {
+ return getTypeName(a);
+ }, v);
+}
+
+template<class Promise>
+struct HandlerResult
+{
+ std::optional<Promise> retVal;
+};
+
+template<class PH, class Context, class Action = Kazv::Client::ActionT>
+struct MockDispatcher
+{
+ using ContextT = Context;
+ using ActionT = Action;
+ using PromiseT = typename ContextT::PromiseT;
+ using DataT = typename PromiseT::DataT;
+ using HandlerResultT = HandlerResult<PromiseT>;
+
+ struct Handler : public std::function<HandlerResultT(PH &, ContextT &, ActionT)>
+ {
+ using BaseT = std::function<HandlerResultT(PH &, ContextT &, ActionT)>;
+ using BaseT::BaseT;
+ using BaseT::operator();
+
+ template<class SubAction, class FuncT>
+ static BaseT make(FuncT func)
+ {
+ using Func = std::decay_t<FuncT>;
+ if constexpr (isExactInvocable<HandlerResultT, Func, PH &, ContextT &, SubAction>) {
+ return [func](PH &ph, ContextT &ctx, ActionT action) mutable -> HandlerResultT {
+ static_assert(boost::hana::is_valid([]() {
+ return boost::hana::type_c<
+ decltype(std::get<SubAction>(action))>;
+ })(),
+ "SubAction must be a variant alternative of ActionT");
+ if (std::holds_alternative<SubAction>(action)) {
+ return func(ph, ctx, std::get<SubAction>(action));
+ }
+ return {std::nullopt};
+ };
+ } else if constexpr (isExactInvocable<PromiseT, Func, PH &, ContextT &, SubAction>) {
+ return make<SubAction>([func](PH &ph, ContextT &ctx, SubAction subAction) mutable {
+ return HandlerResultT{func(ph, ctx, subAction)};
+ });
+ } else if constexpr (isExactInvocable<HandlerResultT, Func, SubAction>) {
+ return make<SubAction>([func](PH &, ContextT &, SubAction subAction) mutable {
+ return func(subAction);
+ });
+ } else if constexpr (isExactInvocable<PromiseT, Func, SubAction>) {
+ return make<SubAction>([func](PH &, ContextT &, SubAction subAction) mutable {
+ return func(subAction);
+ });
+ } else if constexpr (isExactInvocable<DataT, Func, SubAction>) {
+ return make<SubAction>([func](PH &ph, ContextT &, SubAction subAction) mutable {
+ return ph.createResolved(func(subAction));
+ });
+ } else if constexpr (isExactInvocable<void, Func, SubAction>) {
+ return make<SubAction>([func](PH &ph, ContextT &, SubAction subAction) mutable {
+ func(subAction);
+ return ph.createResolved({});
+ });
+ } else {
+ // This is a trick to avoid compilers reporting failure
+ // even when this branch is never executed for any Func
+ // https://stackoverflow.com/questions/38304847/how-does-a-failed-static-assert-work-in-an-if-constexpr-false-block
+ static_assert(!sizeof(Func), "Function is not convertible to an action handler");
+ return [func](PH &, ContextT &, ActionT) mutable -> HandlerResultT {
+ return {std::nullopt};
+ };
+ }
+ }
+
+ template<class SubAction>
+ Handler(PassDownTag<SubAction>)
+ : BaseT(make<SubAction>([](PH &, ContextT &ctx, SubAction action) mutable {
+ Kazv::kzo.client.dbg() << "PassDown" << std::endl;
+ return ctx.dispatch(action);
+ }))
+ {}
+
+ template<class SubAction>
+ Handler(ReturnResolvedTag<SubAction, DataT> tag)
+ : BaseT(make<SubAction>([data=tag.data](SubAction) mutable {
+ Kazv::kzo.client.dbg() << "ReturnResolved" << std::endl;
+ return data;
+ }))
+ {}
+
+ template<class SubAction, class FuncT>
+ Handler(HandlerTag<SubAction, FuncT> func)
+ : BaseT(make<SubAction>(func))
+ {}
+ };
+
+ PromiseT operator()(ActionT action)
+ {
+ actions->push_back(action);
+ std::string typeName = getVariantTypeName(action);
+ Kazv::kzo.client.dbg() << "Handling action " << typeName << std::endl;
+ for (auto &handler : handlers) {
+ auto res = handler(ph, ctx, action);
+ if (res.retVal.has_value()) {
+ return res.retVal.value();
+ }
+ }
+ throw std::runtime_error{"unhandled action: " + typeName};
+ }
+
+ template<class SubAction>
+ auto calledTimes()
+ {
+ return std::accumulate(actions->begin(), actions->end(),
+ 0,
+ [](auto acc, auto cur) {
+ return acc + (std::holds_alternative<SubAction>(cur) ? 1 : 0);
+ }
+ );
+ }
+
+ template<class SubAction>
+ std::vector<SubAction> of()
+ {
+ return zug::into_vector(
+ zug::filter([](const auto &a) { return std::holds_alternative<SubAction>(a); })
+ | zug::map([](const auto &a) { return std::get<SubAction>(a); }),
+ *actions
+ );
+ }
+
+ auto clear() { actions->clear(); }
+
+ PH &ph;
+ ContextT &ctx;
+ std::vector<Handler> handlers;
+ std::shared_ptr<std::vector<ActionT>> actions{std::make_shared<std::vector<ActionT>>()};
+};
+
+template<class PH, class ContextT, class ...Handlers>
+auto getMockDispatcher(PH &ph, ContextT &ctx, Handlers ...handlers)
+{
+ using ResType = MockDispatcher<PH, ContextT>;
+ return ResType{
+ ph,
+ ctx,
+ std::vector<typename ResType::Handler>{ {handlers...} }
+ };
+}
+
+template<
+ class ContextT = typename Kazv::Client::ContextT,
+ class ActionT = typename Kazv::Client::ActionT,
+ class PH,
+ class Func>
+auto getMockContext(PH &ph, Func &&func)
+{
+ return ContextT(std::forward<Func>(func), ph, lager::deps<>{});
+};
diff --git a/src/tests/client/create-room-test.cpp b/src/tests/client/create-room-test.cpp
index 43a0bd8..5e040e0 100644
--- a/src/tests/client/create-room-test.cpp
+++ b/src/tests/client/create-room-test.cpp
@@ -1,97 +1,84 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <catch2/catch_test_macros.hpp>
#include <boost/asio.hpp>
#include <asio-promise-handler.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <cursorutil.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include "client-test-util.hpp"
+#include "action-mock-utils.hpp"
#include "factory.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
-template<class Store, class Func>
-static auto getMockContext(SingleTypePromiseInterface<EffectStatus> &ph, Store &store, Func func)
-{
- return typename Client::ContextT([&ph, &store, func](const auto &action) {
- auto [cont, res] = func(action);
- if (!cont) {
- return ph.createResolved(res);
- }
- throw std::runtime_error{"unhandled action"};
- }, ph, lager::deps<>{});
-}
-
TEST_CASE("Client::createRoom()", "[client][create-room]")
{
ClientModel m = makeClient();
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> ph{AsioPromiseHandler{io.get_executor()}};
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto createRoomCalled = 0;
- std::optional<CreateRoomAction> action = std::nullopt;
- auto mockContext = getMockContext(ph, ctx, [&createRoomCalled, &action](const auto &a) {
- if (std::holds_alternative<CreateRoomAction>(a)) {
- ++createRoomCalled;
- action = std::get<CreateRoomAction>(a);
- return std::make_pair(/* cont = */ false, EffectStatus());
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
+ auto dispatcher = getMockDispatcher(
+ ph,
+ ctx,
+ returnEmpty<CreateRoomAction>()
+ );
+
+ auto mockContext = getMockContext(ph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
client.createRoom(
/* visibility = */ RoomVisibility::Private,
/* name = */ "some name",
/* alias = */ "alias",
/* invite = */ immer::array<std::string>{"@invited:example.com"},
/* isDirect = */ true,
/* allowFederate = */ true,
/* topic = */ "some topic",
/* powerLevelContentOverride = */ json::object({{"messages", 1}}),
/* preset = */ CreateRoomPreset::TrustedPrivateChat
).then([&io](auto) {
io.stop();
});
io.run();
- REQUIRE(createRoomCalled == 1);
- REQUIRE(action->visibility == RoomVisibility::Private);
- REQUIRE(action->name == "some name");
- REQUIRE(action->roomAliasName == "alias");
- REQUIRE(action->invite == immer::array<std::string>{"@invited:example.com"});
- REQUIRE(action->isDirect == true);
- REQUIRE(action->creationContent.get() == json::object({
+ REQUIRE(dispatcher.template calledTimes<CreateRoomAction>() == 1);
+ auto action = dispatcher.template of<CreateRoomAction>()[0];
+ REQUIRE(action.visibility == RoomVisibility::Private);
+ REQUIRE(action.name == "some name");
+ REQUIRE(action.roomAliasName == "alias");
+ REQUIRE(action.invite == immer::array<std::string>{"@invited:example.com"});
+ REQUIRE(action.isDirect == true);
+ REQUIRE(action.creationContent.get() == json::object({
{"m.federate", true},
}));
- REQUIRE(action->topic == "some topic");
- REQUIRE(action->preset == CreateRoomPreset::TrustedPrivateChat);
+ REQUIRE(action.topic == "some topic");
+ REQUIRE(action.preset == CreateRoomPreset::TrustedPrivateChat);
}
diff --git a/src/tests/client/redact-test.cpp b/src/tests/client/redact-test.cpp
index 9d10411..3343ce0 100644
--- a/src/tests/client/redact-test.cpp
+++ b/src/tests/client/redact-test.cpp
@@ -1,114 +1,101 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2021-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <catch2/catch_all.hpp>
#include <boost/asio.hpp>
#include <asio-promise-handler.hpp>
#include <cursorutil.hpp>
#include <sdk.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
-
+#include "action-mock-utils.hpp"
#include "client-test-util.hpp"
#include "factory.hpp"
using namespace Kazv::Factory;
-template<class Store, class Func>
-static auto getMockContext(SingleTypePromiseInterface<EffectStatus> &ph, Store &store, Func func)
-{
- return typename Client::ContextT([&ph, &store, func](const auto &action) {
- auto [cont, res] = func(action);
- if (!cont) {
- return ph.createResolved(res);
- }
-
- kzo.client.err() << "Unhandled action: index " << action.index();
- throw std::runtime_error{"unhandled action"};
- }, ph, lager::deps<>{});
-}
-
TEST_CASE("Redact a message", "[client][redact]")
{
ClientModel loggedInModel = makeClient({});
auto [resModel, dontCareEffect] = ClientModel::update(
loggedInModel, RedactEventAction{"!foo:tusooa.xyz", "$event-id", "some reason"});
assert1Job(resModel);
for1stJob(resModel, [] (const auto &job) {
REQUIRE(job.jobId() == "RedactEvent");
});
}
TEST_CASE("Redact a message without a reason", "[client][redact]")
{
ClientModel loggedInModel = makeClient({});
auto [resModel, dontCareEffect] = ClientModel::update(
loggedInModel, RedactEventAction{"!foo:tusooa.xyz", "$event-id", std::nullopt});
assert1Job(resModel);
for1stJob(resModel, [] (const auto &job) {
REQUIRE(job.jobId() == "RedactEvent");
REQUIRE(json::parse(std::get<BytesBody>(job.requestBody())) == json::object());
});
}
TEST_CASE("Room::redact()", "[client][redact]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m = makeClient(
withRoom(
makeRoom(withRoomId("!exampleroomid:example.com"))
)
);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto redactCalled = 0;
- auto redactAction = RedactEventAction{};
- auto mockContext = getMockContext(sgph, ctx, [&redactCalled, &redactAction](const auto &a) {
- if (std::holds_alternative<RedactEventAction>(a)) {
- ++redactCalled;
- redactAction = std::get<RedactEventAction>(a);
- return std::make_pair(/* cont = */ false, EffectStatus());
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
+
+ auto dispatcher = getMockDispatcher(
+ sgph,
+ ctx,
+ returnEmpty<RedactEventAction>()
+ );
+
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
r.redactEvent("$some-event", "some reason")
.then([&io](auto st) {
REQUIRE(st.success());
io.stop();
});
io.run();
- REQUIRE(redactCalled == 1);
+ REQUIRE(dispatcher.template calledTimes<RedactEventAction>() == 1);
+
+ auto redactAction = dispatcher.template of<RedactEventAction>()[0];
+
REQUIRE(redactAction.roomId == "!exampleroomid:example.com");
REQUIRE(redactAction.eventId == "$some-event");
REQUIRE(redactAction.reason == std::optional<std::string>("some reason"));
}
diff --git a/src/tests/client/room/local-echo-test.cpp b/src/tests/client/room/local-echo-test.cpp
index b5e42c8..52ff167 100644
--- a/src/tests/client/room/local-echo-test.cpp
+++ b/src/tests/client/room/local-echo-test.cpp
@@ -1,682 +1,587 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <tuple>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_predicate.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <asio-promise-handler.hpp>
#include <cursorutil.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include <crypto-util.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <debug.hpp>
#include <sdk.hpp>
#include "client-test-util.hpp"
+#include "action-mock-utils.hpp"
#include "factory.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
using Catch::Matchers::Predicate;
inline auto eventJson = json{
{"content", {
{"foo", "bar"},
}},
{"type", "m.room.message"},
};
-template<class Store, class Func>
-static auto getMockContext(SingleTypePromiseInterface<EffectStatus> &ph, Store &store, Func func)
+template<class Store, class ...Handlers>
+static auto makeDispatcher(SingleTypePromiseInterface<EffectStatus> &ph, Store &store, Handlers ...handlers)
{
- return typename Client::ContextT([&ph, &store, func](const auto &action) {
- static unsigned long long nextTxnId;
- kzo.client.dbg() << "dispatched: index " << action.index() << std::endl;
- auto [cont, res] = func(action);
- if (!cont) {
- return ph.createResolved(res);
- }
- if (std::holds_alternative<GetRoomStatesAction>(action)
- || std::holds_alternative<QueryKeysAction>(action)
- || std::holds_alternative<SendMessageAction>(action)) {
- return ph.createResolved(EffectStatus(true, json::object()));
- } else if (std::holds_alternative<SendMultipleToDeviceMessagesAction>(action)) {
- auto a = std::get<SendMultipleToDeviceMessagesAction>(action);
- return ph.createResolved(EffectStatus(true, json::object()));
- } else if (std::holds_alternative<ClaimKeysAction>(action)) {
- return ph.createResolved(EffectStatus(true, json::object({{"keyEvent", json::object({})}})));
- } else if (std::holds_alternative<PrepareForSharingRoomKeyAction>(action)) {
- auto a = std::get<PrepareForSharingRoomKeyAction>(action);
- auto txnId = std::to_string(nextTxnId++);
+ auto nextTxnId = std::make_shared<int>(0);
+ return getMockDispatcher(
+ ph,
+ store,
+ handlers...,
+ returnEmpty<GetRoomStatesAction>(),
+ returnEmpty<QueryKeysAction>(),
+ returnEmpty<SendMessageAction>(),
+ returnEmpty<SendMultipleToDeviceMessagesAction>(),
+ returnResolved<ClaimKeysAction>(EffectStatus(true,
+ json::object({{"keyEvent", json::object({})}}))),
+ makeHandler<PrepareForSharingRoomKeyAction>([nextTxnId]([[maybe_unused]] auto &ph, auto &store, const PrepareForSharingRoomKeyAction &a) {
+ auto txnId = std::to_string((*nextTxnId)++);
return store.dispatch(UpdateRoomAction{
a.roomId,
AddPendingRoomKeyAction{
makePendingRoomKeyEventV0(
txnId,
Event(json{{"type", "m.room.encrypted"}, {"content", {{"whatever", "ok"}}}}),
// Mocking the device list
{{"@foo:example.com", {"device1", "device2"}}}
)
}
}).then([txnId](auto &&) {
return EffectStatus{true, json::object({{"txnId", txnId}})};
});
- } else if (std::holds_alternative<SaveLocalEchoAction>(action)
- || std::holds_alternative<EncryptMegOlmEventAction>(action)
- || std::holds_alternative<RoomListAction>(action)
- || std::holds_alternative<UpdateLocalEchoStatusAction>(action)) {
- return store.dispatch(action);
- } else {
- kzo.client.err() << "Unhandled action: index " << action.index();
- throw std::runtime_error{"unhandled action"};
- }
- }, ph, lager::deps<>{});
+ }),
+ passDown<SaveLocalEchoAction>(),
+ passDown<EncryptMegOlmEventAction>(),
+ passDown<RoomListAction>(),
+ passDown<UpdateLocalEchoStatusAction>()
+ );
}
TEST_CASE("Local echo", "[client][room]")
{
RoomModel r;
auto next = RoomModel::update(r, AddLocalEchoAction{{"txnId1", Event(eventJson)}});
REQUIRE(!(next == r));
next = RoomModel::update(next, AddLocalEchoAction{{"txnId2", Event(eventJson)}});
REQUIRE(!(next == r));
REQUIRE(next.localEchoes.size() == 2);
REQUIRE_THAT(next.localEchoes[0], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId1";
}));
REQUIRE_THAT(next.localEchoes[1], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId2";
}));
}
TEST_CASE("Remove local echo", "[client][room]")
{
RoomModel r;
r.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"txnId1", Event(eventJson)},
{"txnId2", Event(eventJson)},
};
auto next = RoomModel::update(r, RemoveLocalEchoAction{"txnId1"});
REQUIRE(next.localEchoes.size() == 1);
REQUIRE_THAT(next.localEchoes[0], Predicate<LocalEchoDesc>([](const auto &desc) {
return desc.txnId == "txnId2";
}));
}
TEST_CASE("getLocalEchoByTxnId()", "[client][room]")
{
RoomModel r;
r.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"txnId1", Event(eventJson)},
{"txnId2", Event(eventJson)},
};
REQUIRE(r.getLocalEchoByTxnId("txnId1").value() == r.localEchoes[0]);
REQUIRE(!r.getLocalEchoByTxnId("txnId3").has_value());
}
TEST_CASE("Sending a message leaves a local echo", "[client][room]")
{
ClientModel m;
const auto roomId = "!foo:tusooa.xyz"s;
m.roomList.rooms = m.roomList.rooms.set(roomId, RoomModel{});
auto [next, dontCareEffect] = ClientModel::update(m, SendMessageAction{roomId, Event(eventJson)});
auto localEchoes = next.roomList.rooms[roomId].localEchoes;
REQUIRE(localEchoes.size() == 1);
assert1Job(next);
for1stJob(next, [localEchoes](const auto &job) {
REQUIRE(job.dataStr("txnId") == localEchoes[0].txnId);
});
}
TEST_CASE("Failed send changes the status of the local echo", "[client][room]")
{
ClientModel m;
const auto roomId = "!foo:tusooa.xyz"s;
m.roomList.rooms = m.roomList.rooms.set(roomId, RoomModel{});
auto [next, dontCareEffect] = ClientModel::update(m, SendMessageAction{roomId, Event(eventJson)});
auto resp = makeResponse(
"SendMessage",
withResponseDataKV("roomId", roomId)
| withResponseDataKV("txnId", next.roomList.rooms[roomId].localEchoes[0].txnId)
);
resp.statusCode = 500;
std::tie(next, dontCareEffect) = ClientModel::update(next, ProcessResponseAction{resp});
auto localEchoes = next.roomList.rooms[roomId].localEchoes;
REQUIRE(localEchoes.size() == 1);
REQUIRE(localEchoes[0].status == LocalEchoDesc::Failed);
}
TEST_CASE("Local echo with encrypted event", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto saveLocalEchoCalled = 0;
- auto sendMessageCalled = 0;
- auto txnId = std::string();
- auto sendMessageEvent = Event();
- auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &txnId, &sendMessageEvent](const auto &a) {
- if (std::holds_alternative<SaveLocalEchoAction>(a)) {
- ++saveLocalEchoCalled;
- } else if (std::holds_alternative<SendMessageAction>(a)) {
- ++sendMessageCalled;
- sendMessageEvent = std::get<SendMessageAction>(a).event;
- REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
- txnId = std::get<SendMessageAction>(a).txnId.value();
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
+ auto dispatcher = makeDispatcher(sgph, ctx);
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
- REQUIRE(saveLocalEchoCalled == 2);
- REQUIRE(sendMessageCalled == 1);
- REQUIRE(sendMessageEvent.encrypted());
- REQUIRE(sendMessageEvent.decrypted());
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 2);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 1);
+ auto sendMessageAction = dispatcher.template of<SendMessageAction>()[0];
+ REQUIRE(sendMessageAction.event.encrypted());
+ REQUIRE(sendMessageAction.event.decrypted());
REQUIRE(r.localEchoes().make().get().size() == 1);
- REQUIRE(r.localEchoes().make().get()[0].txnId == txnId);
+ REQUIRE(r.localEchoes().make().get()[0].txnId == sendMessageAction.txnId);
}
TEST_CASE("Local echo with encrypted event, loading room member failed", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto saveLocalEchoCalled = 0;
- auto sendMessageCalled = 0;
- auto megOlmEncryptCalled = 0;
- auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
- if (std::holds_alternative<GetRoomStatesAction>(a)) {
- auto err = json::object({{"errorCode", "400"}, {"error", "Cannot get room states"}});
- return std::make_pair(/* cont = */ false, EffectStatus(false, err));
- } else if (std::holds_alternative<SaveLocalEchoAction>(a)) {
- ++saveLocalEchoCalled;
- } else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
- ++megOlmEncryptCalled;
- } else if (std::holds_alternative<SendMessageAction>(a)) {
- ++sendMessageCalled;
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
-
+ auto dispatcher = makeDispatcher(sgph, ctx,
+ returnResolved<GetRoomStatesAction>(EffectStatus(false, json::object({{"errorCode", "400"}, {"error", "Cannot get room states"}})))
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](const auto &status) {
kzo.client.dbg() << "ended" << std::endl;
REQUIRE(!status.success());
io.stop();
});
io.run();
- REQUIRE(saveLocalEchoCalled == 1);
- REQUIRE(sendMessageCalled == 0);
- REQUIRE(megOlmEncryptCalled == 0);
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<EncryptMegOlmEventAction>() == 0);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].status == LocalEchoDesc::Failed);
}
TEST_CASE("Local echo with encrypted event, querying keys failed", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto saveLocalEchoCalled = 0;
- auto sendMessageCalled = 0;
- auto megOlmEncryptCalled = 0;
- auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
- if (std::holds_alternative<QueryKeysAction>(a)) {
- auto err = json::object({{"errorCode", "400"}, {"error", "Cannot get room states"}});
- return std::make_pair(/* cont = */ false, EffectStatus(false, err));
- } else if (std::holds_alternative<SaveLocalEchoAction>(a)) {
- ++saveLocalEchoCalled;
- } else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
- ++megOlmEncryptCalled;
- } else if (std::holds_alternative<SendMessageAction>(a)) {
- ++sendMessageCalled;
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
+ auto dispatcher = makeDispatcher(sgph, ctx,
+ returnResolved<QueryKeysAction>(EffectStatus(false, json::object({{"errorCode", "400"}, {"error", "Cannot get room states"}})))
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](const auto &status) {
kzo.client.dbg() << "ended" << std::endl;
REQUIRE(!status.success());
io.stop();
});
io.run();
- REQUIRE(saveLocalEchoCalled == 1);
- REQUIRE(sendMessageCalled == 0);
- REQUIRE(megOlmEncryptCalled == 0);
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<EncryptMegOlmEventAction>() == 0);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].status == LocalEchoDesc::Failed);
}
TEST_CASE("Encrypted room: Resend encrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
auto encryptedEvent = Event{json{
{"content", {{"foo", "bar"}}},
{"type", "m.room.encrypted"},
}};
auto decryptedJson = json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
};
auto event = encryptedEvent.setDecryptedJson(decryptedJson, Event::Decrypted);
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto saveLocalEchoCalled = 0;
- auto sendMessageCalled = 0;
- auto megOlmEncryptCalled = 0;
- auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
- if (std::holds_alternative<SaveLocalEchoAction>(a)) {
- ++saveLocalEchoCalled;
- } else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
- ++megOlmEncryptCalled;
- } else if (std::holds_alternative<SendMessageAction>(a)) {
- ++sendMessageCalled;
- REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
- REQUIRE(std::get<SendMessageAction>(a).txnId.value() == "some-txn-id");
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
+
+ auto dispatcher = makeDispatcher(sgph, ctx);
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
- REQUIRE(saveLocalEchoCalled == 0);
- REQUIRE(megOlmEncryptCalled == 0);
- REQUIRE(sendMessageCalled == 1);
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<EncryptMegOlmEventAction>() == 0);
REQUIRE(r.localEchoes().make().get().size() == 1);
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-txn-id");
+ auto a = dispatcher.template of<SendMessageAction>()[0];
+ REQUIRE(a.txnId.has_value());
+ REQUIRE(a.txnId.value() == "some-txn-id");
}
TEST_CASE("Encrypted room: Resend encrypted local echo AND failed key share event", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto saveLocalEchoCalled = 0;
- auto sendMessageCalled = 0;
- auto megOlmEncryptCalled = 0;
- auto sendToDeviceCalled = 0;
- auto olmEncryptCalled = 0;
auto resending = false;
-
- auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled,
- &sendMessageCalled, &megOlmEncryptCalled, &sendToDeviceCalled, &olmEncryptCalled, &resending](const auto &a) {
- if (std::holds_alternative<SendMultipleToDeviceMessagesAction>(a)) {
- auto sendKeyData = json{
- {"error", "Bad request"},
- {"errorCode", "400"},
- };
- ++sendToDeviceCalled;
+ auto dispatcher = makeDispatcher(sgph, ctx,
+ makeHandler<SendMultipleToDeviceMessagesAction>([&resending](auto &&ph, [[maybe_unused]] auto &&store, SendMultipleToDeviceMessagesAction) {
if (!resending) {
- return std::make_pair(/* cont = */ false, EffectStatus(/* succ = */ false, sendKeyData));
+ auto sendKeyData = json{
+ {"error", "Bad request"},
+ {"errorCode", "400"},
+ };
+ return HandlerResult<typename Client::PromiseT>{
+ ph.createResolved(EffectStatus(/* succ = */ false, sendKeyData))
+ };
}
- } else if (std::holds_alternative<SaveLocalEchoAction>(a)) {
- ++saveLocalEchoCalled;
- } else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
- ++megOlmEncryptCalled;
- } else if (std::holds_alternative<SendMessageAction>(a)) {
- ++sendMessageCalled;
- } else if (std::holds_alternative<PrepareForSharingRoomKeyAction>(a)) {
- ++olmEncryptCalled;
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
+ return HandlerResult<typename Client::PromiseT>{std::nullopt};
+ })
+ );
+
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.sendTextMessage("test")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
- REQUIRE(saveLocalEchoCalled == 2);
- REQUIRE(megOlmEncryptCalled == 1);
- REQUIRE(sendToDeviceCalled == 1);
- REQUIRE(olmEncryptCalled == 1);
- REQUIRE(sendMessageCalled == 0);
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 2);
+ REQUIRE(dispatcher.template calledTimes<EncryptMegOlmEventAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<SendMultipleToDeviceMessagesAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<PrepareForSharingRoomKeyAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 0);
+
REQUIRE(r.localEchoes().make().get().size() == 1);
auto savedEvent = r.localEchoes().make().get()[0];
auto txnId = savedEvent.txnId;
REQUIRE(savedEvent.event.encrypted());
- saveLocalEchoCalled = 0;
- megOlmEncryptCalled = 0;
- sendMessageCalled = 0;
- sendToDeviceCalled = 0;
- olmEncryptCalled = 0;
+ dispatcher.clear();
resending = true;
r.resendMessage(txnId)
.then([&io](auto) {
kzo.client.dbg() << "resent" << std::endl;
io.stop();
});
io.restart();
io.run();
- REQUIRE(saveLocalEchoCalled == 0);
- REQUIRE(megOlmEncryptCalled == 0);
- REQUIRE(sendMessageCalled == 1);
- REQUIRE(olmEncryptCalled == 0);
- REQUIRE(sendToDeviceCalled == 1);
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<EncryptMegOlmEventAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<PrepareForSharingRoomKeyAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<SendMultipleToDeviceMessagesAction>() == 1);
REQUIRE(r.pendingRoomKeyEvents().make().get().size() == 0);
}
TEST_CASE("Encrypted room: Resend unencrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = true;
room.roomId = "!exampleroomid:example.com";
auto event = Event{json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
}};
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
{"some-other-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto saveLocalEchoCalled = 0;
- auto sendMessageCalled = 0;
- auto megOlmEncryptCalled = 0;
- auto roomListActionCalled = 0;
- auto txnId = std::string();
- auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled, &roomListActionCalled, &txnId](const auto &a) {
- if (std::holds_alternative<SaveLocalEchoAction>(a)) {
- ++saveLocalEchoCalled;
- } else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
- ++megOlmEncryptCalled;
- } else if (std::holds_alternative<RoomListAction>(a)) {
- ++roomListActionCalled;
- } else if (std::holds_alternative<SendMessageAction>(a)) {
- ++sendMessageCalled;
- REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
- txnId = std::get<SendMessageAction>(a).txnId.value();
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
+ auto dispatcher = makeDispatcher(sgph, ctx);
+
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto st) {
REQUIRE(st.success());
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
- REQUIRE(saveLocalEchoCalled == 2);
- REQUIRE(roomListActionCalled == 2); // once for removing the existing local echo, once for removing pending key event
- REQUIRE(megOlmEncryptCalled == 1);
- REQUIRE(sendMessageCalled == 1);
+
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 2);
+ REQUIRE(dispatcher.template calledTimes<RoomListAction>() == 2); // once for removing the existing local echo, once for removing pending key event
+ REQUIRE(dispatcher.template calledTimes<EncryptMegOlmEventAction>() == 1);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 1);
+ auto sendMessageAction = dispatcher.template of<SendMessageAction>()[0];
+ REQUIRE(sendMessageAction.txnId.has_value());
REQUIRE(r.localEchoes().make().get().size() == 2);
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-other-txn-id");
- REQUIRE(r.localEchoes().make().get()[1].txnId == txnId);
+ REQUIRE(r.localEchoes().make().get()[1].txnId == sendMessageAction.txnId.value());
}
TEST_CASE("Unencrypted room: Resend unencrypted local echo", "[client][room]")
{
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
ClientModel m;
m.crypto = Crypto(RandomTag{}, genRandomData(Crypto::constructRandomSize()));
RoomModel room;
room.encrypted = false;
room.roomId = "!exampleroomid:example.com";
auto event = Event{json{
{"content", {{"dec-foo", "dec-bar"}}},
{"type", "m.room.message"},
}};
room.localEchoes = immer::flex_vector<LocalEchoDesc>{
{"some-txn-id", event, LocalEchoDesc::Failed},
};
m.roomList.rooms = m.roomList.rooms.set("!exampleroomid:example.com", room);
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto saveLocalEchoCalled = 0;
- auto sendMessageCalled = 0;
- auto megOlmEncryptCalled = 0;
- auto mockContext = getMockContext(sgph, ctx, [&saveLocalEchoCalled, &sendMessageCalled, &megOlmEncryptCalled](const auto &a) {
- if (std::holds_alternative<SaveLocalEchoAction>(a)) {
- ++saveLocalEchoCalled;
- } else if (std::holds_alternative<EncryptMegOlmEventAction>(a)) {
- ++megOlmEncryptCalled;
- } else if (std::holds_alternative<SendMessageAction>(a)) {
- ++sendMessageCalled;
- REQUIRE(std::get<SendMessageAction>(a).txnId.has_value());
- REQUIRE(std::get<SendMessageAction>(a).txnId.value() == "some-txn-id");
- }
- return std::make_pair(/* cont = */ true, EffectStatus());
- });
-
+ auto dispatcher = makeDispatcher(sgph, ctx);
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto r = client.room("!exampleroomid:example.com");
REQUIRE(!r.encrypted().make().get());
r.resendMessage("some-txn-id")
.then([&io](auto) {
kzo.client.dbg() << "ended" << std::endl;
io.stop();
});
io.run();
- REQUIRE(saveLocalEchoCalled == 0);
- REQUIRE(megOlmEncryptCalled == 0);
- REQUIRE(sendMessageCalled == 1);
+ REQUIRE(dispatcher.template calledTimes<SaveLocalEchoAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<EncryptMegOlmEventAction>() == 0);
+ REQUIRE(dispatcher.template calledTimes<SendMessageAction>() == 1);
REQUIRE(r.localEchoes().make().get().size() == 1);
+ auto a = dispatcher.template of<SendMessageAction>()[0];
+ REQUIRE(a.txnId.value() == "some-txn-id");
REQUIRE(r.localEchoes().make().get()[0].txnId == "some-txn-id");
}
TEST_CASE("makePendingRoomKeyEventV0()", "[client][room][local-echo]")
{
auto devices = immer::map<std::string, immer::flex_vector<std::string>>{
{"@foo:example.com", {"device1"}},
{"@bar:example.com", {"device2", "device3"}},
};
std::string txnId = "xxx";
Event event = json{{"type", "m.room.encrypted"}, {"content", {{"whatever", "ok"}}}};
auto e = makePendingRoomKeyEventV0(txnId, event, devices);
auto expected = immer::map<std::string, immer::map<std::string, Event>>{
{"@foo:example.com", {{"device1", event}}},
{"@bar:example.com", {{"device2", event}, {"device3", event}}},
};
REQUIRE(e.messages == expected);
}
diff --git a/src/tests/client/room/read-receipt-test.cpp b/src/tests/client/room/read-receipt-test.cpp
index 8d403f1..6770917 100644
--- a/src/tests/client/room/read-receipt-test.cpp
+++ b/src/tests/client/room/read-receipt-test.cpp
@@ -1,221 +1,217 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <boost/asio.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_range_equals.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <asio-promise-handler.hpp>
#include <room/room-model.hpp>
#include <sdk-model.hpp>
#include <client/client.hpp>
#include <client/actions/ephemeral.hpp>
#include <cprjobhandler.hpp>
#include <lagerstoreeventemitter.hpp>
#include <testfixtures/factory.hpp>
#include "client-test-util.hpp"
+#include "action-mock-utils.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
TEST_CASE("Adding an m.receipt ephemeral event", "[client][room][receipt]")
{
auto room = makeRoom();
auto receiptEvent = makeEvent(
withEventType("m.receipt")
// https://spec.matrix.org/v1.8/client-server-api/#events-5
| withEventContent(R"({
"$1435641916114394fHBLK:matrix.org": {
"m.read": {
"@rikj:jki.re": {
"ts": 1436451550453
}
},
"m.read.private": {
"@self:example.org": {
"ts": 1661384801651
}
}
}
})"_json));
room = RoomModel::update(room, AddEphemeralAction{{receiptEvent}});
auto receipt = room.readReceipts.at("@rikj:jki.re");
REQUIRE(
room.readReceipts.at("@rikj:jki.re")
==
ReadReceipt{"$1435641916114394fHBLK:matrix.org", 1436451550453}
);
REQUIRE(
room.eventReadUsers.at("$1435641916114394fHBLK:matrix.org")
==
immer::flex_vector<std::string>{"@rikj:jki.re", "@self:example.org"}
);
auto anotherReceiptEvent = makeEvent(
withEventType("m.receipt")
| withEventContent(json{
{"$123", {
{"m.read", {
{"@foo:example.com", {{"ts", 1796451550450}}}
}},
}},
}));
room = RoomModel::update(room, AddEphemeralAction{{anotherReceiptEvent}});
// the receipt that was there should still be there.
REQUIRE(
room.readReceipts.at("@rikj:jki.re")
==
ReadReceipt{"$1435641916114394fHBLK:matrix.org", 1436451550453}
);
REQUIRE(
room.readReceipts.at("@foo:example.com")
==
ReadReceipt{"$123", 1796451550450}
);
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto model = makeClient(withRoom(room));
auto store = createTestClientStoreFrom(model, ph);
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }),
store,
std::nullopt);
auto r = client.room(room.roomId);
auto readers1 = r.eventReaders(lager::make_constant<std::string>("$1435641916114394fHBLK:matrix.org")).make().get();
auto expected1 = immer::flex_vector<EventReader>{{"@rikj:jki.re", 1436451550453}, {"@self:example.org", 1661384801651}};
REQUIRE_THAT(readers1, Catch::Matchers::UnorderedRangeEquals(expected1));
auto readers2 = r.eventReaders(lager::make_constant<std::string>("$123")).make().get();
auto expected2 = immer::flex_vector<EventReader>{{"@foo:example.com", 1796451550450}};
REQUIRE(readers2 == expected2);
}
TEST_CASE("Update a receipt for some user", "[client][room][receipt]")
{
auto room = makeRoom();
auto receiptEvent = makeEvent(
withEventType("m.receipt")
// https://spec.matrix.org/v1.8/client-server-api/#events-5
| withEventContent(R"({
"$1435641916114394fHBLK:matrix.org": {
"m.read": {
"@rikj:jki.re": {
"ts": 1436451550453
}
}
}
})"_json));
room = RoomModel::update(room, AddEphemeralAction{{receiptEvent}});
auto anotherReceiptEvent = makeEvent(
withEventType("m.receipt")
| withEventContent(json{
{"$123", {
{"m.read", {
{"@rikj:jki.re", {{"ts", 1796451550450}}}
}},
}},
}));
room = RoomModel::update(room, AddEphemeralAction{{anotherReceiptEvent}});
REQUIRE(
room.readReceipts.at("@rikj:jki.re")
==
ReadReceipt{"$123", 1796451550450}
);
REQUIRE(room.eventReadUsers.count("$1435641916114394fHBLK:matrix.org") == 0);
REQUIRE(room.eventReadUsers.count("$123") == 1);
boost::asio::io_context io;
AsioPromiseHandler ph{io.get_executor()};
auto model = makeClient(withRoom(room));
auto store = createTestClientStoreFrom(model, ph);
auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }),
store,
std::nullopt);
auto r = client.room(room.roomId);
auto readers1 = r.eventReaders(lager::make_constant<std::string>("$1435641916114394fHBLK:matrix.org")).make().get();
auto expected1 = immer::flex_vector<EventReader>{};
REQUIRE(readers1 == expected1);
auto readers2 = r.eventReaders(lager::make_constant<std::string>("$123")).make().get();
auto expected2 = immer::flex_vector<EventReader>{{"@rikj:jki.re", 1796451550450}};
REQUIRE(readers2 == expected2);
}
TEST_CASE("Posting receipts", "[client][room][receipt]")
{
auto r = makeRoom();
auto m = makeClient(withRoom(r));
boost::asio::io_context io;
SingleTypePromiseInterface<EffectStatus> sgph{AsioPromiseHandler{io.get_executor()}};
auto jh = Kazv::CprJobHandler{io.get_executor()};
auto ee = Kazv::LagerStoreEventEmitter(lager::with_boost_asio_event_loop{io.get_executor()});
auto sdk = Kazv::makeSdk(
SdkModel{m},
jh,
ee,
Kazv::AsioPromiseHandler{io.get_executor()},
zug::identity
);
auto ctx = sdk.context();
- auto postReceiptCalled = 0;
- std::string postReceiptRoomId;
- std::string postReceiptEventId;
- auto mockContext = typename Client::ContextT([&sgph, &postReceiptCalled, &postReceiptRoomId, &postReceiptEventId](const auto &action) {
- if (std::holds_alternative<PostReceiptAction>(action)) {
- ++postReceiptCalled;
- postReceiptRoomId = std::get<PostReceiptAction>(action).roomId;
- postReceiptEventId = std::get<PostReceiptAction>(action).eventId;
- return sgph.createResolved(EffectStatus(true, json::object()));
- }
- throw std::runtime_error{"unhandled action"};
- }, sgph, lager::deps<>{});
+ auto dispatcher = getMockDispatcher(
+ sgph,
+ ctx,
+ returnEmpty<PostReceiptAction>()
+ );
+ auto mockContext = getMockContext(sgph, dispatcher);
auto client = Client(Client::InEventLoopTag{}, mockContext, sdk.context());
auto room = client.room(r.roomId);
room.postReceipt("$1")
.then([&io](auto) {
io.stop();
});
io.run();
- REQUIRE(postReceiptCalled == 1);
- REQUIRE(postReceiptRoomId == r.roomId);
- REQUIRE(postReceiptEventId == "$1");
+ REQUIRE(dispatcher.template calledTimes<PostReceiptAction>() == 1);
+ auto action = dispatcher.template of<PostReceiptAction>()[0];
+ REQUIRE(action.roomId == r.roomId);
+ REQUIRE(action.eventId == "$1");
}
TEST_CASE("PostReceiptAction", "[client][room][receipt]")
{
auto m = makeClient();
auto [next, _ignore] = updateClient(m, PostReceiptAction{"!someroom:example.com", "$someevent"});
assert1Job(next);
for1stJob(next, [](const BaseJob &job) {
REQUIRE(job.jobId() == "PostReceipt");
REQUIRE(job.url().find("rooms/!someroom:example.com/receipt/m.read/$someevent") != std::string::npos);
REQUIRE(json::parse(std::get<Bytes>(job.requestBody())) == json::object());
});
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 8, 6:30 AM (15 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1722945
Default Alt Text
(70 KB)

Event Timeline