Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85627882
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
70 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment