diff --git a/docs/modules/dev/pages/api.adoc b/docs/modules/dev/pages/api.adoc
index f32bdfbf..9e5228e9 100644
--- a/docs/modules/dev/pages/api.adoc
+++ b/docs/modules/dev/pages/api.adoc
@@ -2,7 +2,8 @@
= Wolf API
Wolf exposes a REST API that allows you to interact with the platform programmatically. +
-The API can be accessed only via UNIX sockets, you can control the exact path by setting the `WOLF_SOCKET_PATH` environment variable. If you want to access the socket from outside the container, you should mount the socket to the host machine, ex: `-e WOLF_SOCKET_PATH=/var/run/wolf/wolf.sock` and `-v /var/run/wolf:/var/run/wolf` will allow you to access the socket from the host machine at `/var/run/wolf/wolf.sock`.
+The API can be accessed only via UNIX sockets, you can control the exact path by setting the `WOLF_SOCKET_PATH` environment variable.
+If you want to access the socket from outside the container, you should mount the socket to the host machine, ex: `-e WOLF_SOCKET_PATH=/var/run/wolf/wolf.sock` and `-v /var/run/wolf:/var/run/wolf` will allow you to access the socket from the host machine at `/var/run/wolf/wolf.sock`.
You can test out the API using the `curl` command, for example, to get the OpenAPI specification you can run:
@@ -66,5 +67,16 @@ curl localhost:8080/api/v1/openapi-schema
type="application/json">
include::{includedir}/spec.json[]
+
+
++++
\ No newline at end of file
diff --git a/docs/modules/dev/partials/spec.json b/docs/modules/dev/partials/spec.json
index 68c16c63..8b0347ec 100644
--- a/docs/modules/dev/partials/spec.json
+++ b/docs/modules/dev/partials/spec.json
@@ -176,6 +176,65 @@
}
}
},
+ "/api/v1/clients/settings": {
+ "post": {
+ "summary": "Update client settings",
+ "description": "Update a client's settings including app state folder and client-specific settings",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__UpdateClientSettingsRequest"
+ }
+ }
+ },
+ "description": "",
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "400": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__GenericErrorResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__GenericErrorResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__GenericErrorResponse"
+ }
+ }
+ },
+ "description": ""
+ }
+ }
+ }
+ },
"/api/v1/pair/client": {
"post": {
"summary": "Pair a client",
@@ -448,6 +507,45 @@
}
}
}
+ },
+ "/api/v1/unpair/client": {
+ "post": {
+ "summary": "Unpair a client",
+ "description": "",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__UnpairClientRequest"
+ }
+ }
+ },
+ "description": "",
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/wolf__api__GenericErrorResponse"
+ }
+ }
+ },
+ "description": ""
+ }
+ }
+ }
}
},
"components": {
@@ -640,12 +738,16 @@
"type": "string"
},
"client_id": {
- "type": "integer"
+ "type": "string"
+ },
+ "settings": {
+ "$ref": "#/components/schemas/wolf__config__ClientSettings"
}
},
"required": [
"app_state_folder",
- "client_id"
+ "client_id",
+ "settings"
]
},
"wolf__api__PairedClientsResponse": {
@@ -666,13 +768,104 @@
"success"
]
},
+ "wolf__api__PartialClientSettings": {
+ "type": "object",
+ "properties": {
+ "controllers_override": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "XBOX",
+ "PS",
+ "NINTENDO",
+ "AUTO"
+ ]
+ }
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "h_scroll_acceleration": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "mouse_acceleration": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "run_gid": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "run_uid": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "v_scroll_acceleration": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": []
+ },
+ "wolf__api__PendingPairClient": {
+ "type": "object",
+ "properties": {
+ "client_ip": {
+ "type": "string",
+ "description": "The IP of the remote Moonlight client"
+ },
+ "pair_secret": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "client_ip",
+ "pair_secret"
+ ]
+ },
"wolf__api__PendingPairRequestsResponse": {
"type": "object",
"properties": {
"requests": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/wolf__api__PairRequest"
+ "$ref": "#/components/schemas/wolf__api__PendingPairClient"
}
},
"success": {
@@ -803,6 +996,54 @@
"session_id"
]
},
+ "wolf__api__UnpairClientRequest": {
+ "type": "object",
+ "properties": {
+ "client_id": {
+ "type": "string",
+ "description": "The client ID to unpair"
+ }
+ },
+ "required": [
+ "client_id"
+ ]
+ },
+ "wolf__api__UpdateClientSettingsRequest": {
+ "type": "object",
+ "properties": {
+ "app_state_folder": {
+ "description": "New app state folder path (optional)",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "client_id": {
+ "type": "string",
+ "description": "The client ID to identify the client (derived from certificate)"
+ },
+ "settings": {
+ "description": "Client settings to update (only specified fields will be updated)",
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/wolf__api__PartialClientSettings"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [
+ "app_state_folder",
+ "client_id",
+ "settings"
+ ]
+ },
"wolf__config__AppCMD__tagged": {
"type": "object",
"properties": {
@@ -1128,4 +1369,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp
index 1270e3a6..7aef1672 100644
--- a/src/moonlight-server/api/api.hpp
+++ b/src/moonlight-server/api/api.hpp
@@ -12,11 +12,20 @@ using namespace wolf::core;
void start_server(immer::box app_state);
+struct PendingPairClient {
+ std::string pair_secret;
+ rfl::Description<"The IP of the remote Moonlight client", std::string> client_ip;
+};
+
struct PairRequest {
std::string pair_secret;
rfl::Description<"The PIN created by the remote Moonlight client", std::string> pin;
};
+struct UnpairClientRequest {
+ rfl::Description<"The client ID to unpair", std::string> client_id;
+};
+
struct GenericSuccessResponse {
bool success = true;
};
@@ -28,12 +37,13 @@ struct GenericErrorResponse {
struct PendingPairRequestsResponse {
bool success = true;
- std::vector requests;
+ std::vector requests;
};
struct PairedClient {
- std::size_t client_id;
+ std::string client_id;
std::string app_state_folder;
+ config::ClientSettings settings = {};
};
struct PairedClientsResponse {
@@ -41,6 +51,22 @@ struct PairedClientsResponse {
std::vector clients;
};
+struct PartialClientSettings {
+ std::optional run_uid;
+ std::optional run_gid;
+ std::optional> controllers_override;
+ std::optional mouse_acceleration;
+ std::optional v_scroll_acceleration;
+ std::optional h_scroll_acceleration;
+};
+
+struct UpdateClientSettingsRequest {
+ rfl::Description<"The client ID to identify the client (derived from certificate)", std::string> client_id;
+ rfl::Description<"New app state folder path (optional)", std::optional> app_state_folder;
+ rfl::Description<"Client settings to update (only specified fields will be updated)", std::optional>
+ settings;
+};
+
struct AppListResponse {
bool success = true;
std::vector::ReflType> apps;
@@ -114,7 +140,7 @@ class UnixSocketServer {
void endpoint_Apps(const HTTPRequest &req, std::shared_ptr socket);
void endpoint_AddApp(const HTTPRequest &req, std::shared_ptr socket);
void endpoint_RemoveApp(const HTTPRequest &req, std::shared_ptr socket);
-
+ void endpoint_UnpairClient(const HTTPRequest &req, std::shared_ptr socket);
void endpoint_StreamSessions(const HTTPRequest &req, std::shared_ptr socket);
void endpoint_StreamSessionAdd(const HTTPRequest &req, std::shared_ptr socket);
void endpoint_StreamSessionStart(const HTTPRequest &req, std::shared_ptr socket);
@@ -124,6 +150,8 @@ class UnixSocketServer {
void endpoint_RunnerStart(const HTTPRequest &req, std::shared_ptr socket);
+ void endpoint_UpdateClientSettings(const HTTPRequest &req, std::shared_ptr socket);
+
void sse_broadcast(const std::string &payload);
void sse_keepalive(const boost::system::error_code &e);
diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp
index d9cb6b75..72a2363f 100644
--- a/src/moonlight-server/api/endpoints.cpp
+++ b/src/moonlight-server/api/endpoints.cpp
@@ -15,9 +15,9 @@ void UnixSocketServer::endpoint_Events(const HTTPRequest &req, std::shared_ptr socket) {
- auto requests = std::vector();
+ auto requests = std::vector();
for (auto [secret, pair_request] : *(state_->app_state)->pairing_atom->load()) {
- requests.push_back({.pair_secret = secret, .pin = pair_request->client_ip});
+ requests.push_back({.pair_secret = secret, .client_ip = pair_request->client_ip});
}
send_http(socket, 200, rfl::json::write(PendingPairRequestsResponse{.requests = requests}));
}
@@ -44,13 +44,41 @@ void UnixSocketServer::endpoint_Pair(const HTTPRequest &req, std::shared_ptr socket) {
auto res = PairedClientsResponse{.success = true};
auto clients = state_->app_state->config->paired_clients->load();
- for (const auto &client : clients.get()) {
- res.clients.push_back(
- PairedClient{.client_id = state::get_client_id(client), .app_state_folder = client->app_state_folder});
+ for (const config::PairedClient &client : clients.get()) {
+ res.clients.push_back(PairedClient{.client_id = std::to_string(state::get_client_id(client)),
+ .app_state_folder = client.app_state_folder,
+ .settings = client.settings});
}
send_http(socket, 200, rfl::json::write(res));
}
+void UnixSocketServer::endpoint_UnpairClient(const HTTPRequest &req, std::shared_ptr socket) {
+ try {
+ auto payload_result = rfl::json::read(req.body);
+ if (!payload_result) {
+ auto res = GenericErrorResponse{.error = "Invalid request format"};
+ send_http(socket, 400, rfl::json::write(res));
+ return;
+ }
+
+ const auto &payload = payload_result.value(); // Unwrap the Result
+ auto client = state::get_client_by_id(this->state_->app_state->config, payload.client_id.value());
+ if (!client) {
+ auto res = GenericErrorResponse{.error = "Client not found"};
+ send_http(socket, 404, rfl::json::write(res));
+ return;
+ }
+
+ state::unpair(this->state_->app_state->config, *client);
+
+ auto res = GenericSuccessResponse{.success = true};
+ send_http(socket, 200, rfl::json::write(res));
+ } catch (const std::exception &e) {
+ auto res = GenericErrorResponse{.error = e.what()};
+ send_http(socket, 500, rfl::json::write(res));
+ }
+}
+
void UnixSocketServer::endpoint_Apps(const HTTPRequest &req, std::shared_ptr socket) {
auto res = AppListResponse{.success = true};
auto apps = state_->app_state->config->apps->load();
@@ -125,7 +153,7 @@ void UnixSocketServer::endpoint_StreamSessionAdd(const HTTPRequest &req, std::sh
return;
}
- auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(ss.client_id));
+ auto client = state::get_client_by_id(this->state_->app_state->config, ss.client_id);
if (!client) {
logs::log(logs::warning, "[API] Invalid client_id: {}", ss.client_id);
auto res = GenericErrorResponse{.error = "Invalid client_id"};
@@ -279,4 +307,41 @@ void UnixSocketServer::endpoint_RunnerStart(const wolf::api::HTTPRequest &req, s
}
}
+void UnixSocketServer::endpoint_UpdateClientSettings(const HTTPRequest &req, std::shared_ptr socket) {
+ auto payload_result = rfl::json::read(req.body);
+ if (!payload_result) {
+ auto res = GenericErrorResponse{.error = "Invalid request format"};
+ send_http(socket, 400, rfl::json::write(res));
+ return;
+ }
+
+ const auto &payload = payload_result.value();
+ auto current_client = state::get_client_by_id(this->state_->app_state->config, payload.client_id.value());
+ if (!current_client) {
+ auto res = GenericErrorResponse{.error = "Client not found"};
+ send_http(socket, 404, rfl::json::write(res));
+ return;
+ }
+
+ // Edit only the settings that are being passed in the payload
+ auto current_settings = current_client->settings;
+ auto new_settings = payload.settings.get().value_or(PartialClientSettings{});
+ auto merged_client = config::PairedClient{
+ .client_cert = current_client->client_cert, // Immutable, changing this would mean a new client
+ .app_state_folder = payload.app_state_folder.get().value_or(current_client->app_state_folder),
+ .settings = config::ClientSettings{
+ .run_uid = new_settings.run_gid.value_or(current_settings.run_uid),
+ .run_gid = new_settings.run_gid.value_or(current_settings.run_gid),
+ .controllers_override = new_settings.controllers_override.value_or(current_settings.controllers_override),
+ .mouse_acceleration = new_settings.mouse_acceleration.value_or(current_settings.mouse_acceleration),
+ .v_scroll_acceleration = new_settings.v_scroll_acceleration.value_or(current_settings.v_scroll_acceleration),
+ .h_scroll_acceleration = new_settings.h_scroll_acceleration.value_or(current_settings.h_scroll_acceleration),
+ }};
+
+ update_client_settings(this->state_->app_state->config, std::stoull(payload.client_id.value()), merged_client);
+
+ auto res = GenericSuccessResponse{.success = true};
+ send_http(socket, 200, rfl::json::write(res));
+}
+
} // namespace wolf::api
\ No newline at end of file
diff --git a/src/moonlight-server/api/unix_socket_server.cpp b/src/moonlight-server/api/unix_socket_server.cpp
index f2e0c95e..ad0a48ab 100644
--- a/src/moonlight-server/api/unix_socket_server.cpp
+++ b/src/moonlight-server/api/unix_socket_server.cpp
@@ -56,6 +56,16 @@ UnixSocketServer::UnixSocketServer(boost::asio::io_context &io_context,
.handler = [this](auto req, auto socket) { endpoint_Pair(req, socket); },
});
+ state_->http.add(HTTPMethod::POST,
+ "/api/v1/unpair/client",
+ {
+ .summary = "Unpair a client",
+ .request_description = APIDescription{.json_schema = rfl::json::to_schema()},
+ .response_description = {{200, {.json_schema = rfl::json::to_schema()}},
+ {500, {.json_schema = rfl::json::to_schema()}}},
+ .handler = [this](auto req, auto socket) { endpoint_UnpairClient(req, socket); },
+ });
+
state_->http.add(HTTPMethod::GET,
"/api/v1/clients",
{
@@ -65,6 +75,21 @@ UnixSocketServer::UnixSocketServer(boost::asio::io_context &io_context,
.handler = [this](auto req, auto socket) { endpoint_PairedClients(req, socket); },
});
+ state_->http.add(HTTPMethod::POST,
+ "/api/v1/clients/settings",
+ {
+ .summary = "Update client settings",
+ .description = "Update a client's settings including app state folder and client-specific settings",
+ .request_description = APIDescription{.json_schema = rfl::json::to_schema()},
+ .response_description = {
+ {200, {.json_schema = rfl::json::to_schema()}},
+ {400, {.json_schema = rfl::json::to_schema()}},
+ {404, {.json_schema = rfl::json::to_schema()}},
+ {500, {.json_schema = rfl::json::to_schema()}}
+ },
+ .handler = [this](auto req, auto socket) { endpoint_UpdateClientSettings(req, socket); },
+ });
+
/**
* Apps API
*/
diff --git a/src/moonlight-server/rest/servers.cpp b/src/moonlight-server/rest/servers.cpp
index 8562262e..8ebe44e1 100644
--- a/src/moonlight-server/rest/servers.cpp
+++ b/src/moonlight-server/rest/servers.cpp
@@ -1,6 +1,7 @@
#include
#include
#include
+#include
#include
namespace HTTPServers {
@@ -10,6 +11,7 @@ namespace HTTPServers {
*/
constexpr char const *pin_html =
#include "html/pin.include.html"
+
;
namespace bt = boost::property_tree;
@@ -70,10 +72,19 @@ void startServer(HttpServer *server, const immer::box state, in
auto pair_handler = state->event_bus->register_handler>(
[pairing_atom](const immer::box pair_sig) {
- pairing_atom->update([&pair_sig](auto m) {
+ pairing_atom->update([&pair_sig](const immer::map> &m) {
auto secret = crypto::str_to_hex(crypto::random(8));
logs::log(logs::info, "Insert pin at http://{}:47989/pin/#{}", pair_sig->host_ip, secret);
- return m.set(secret, pair_sig);
+ // filter out any other (dangling) pair request from the same client
+ auto t_map = m.transient();
+ for (auto [key, value] : m) {
+ if (value->client_ip == pair_sig->client_ip) {
+ t_map.erase(key);
+ }
+ }
+ // insert the new pair request
+ t_map.set(secret, pair_sig);
+ return t_map.persistent();
});
});
diff --git a/src/moonlight-server/state/config.hpp b/src/moonlight-server/state/config.hpp
index 97d304bb..e1c31145 100644
--- a/src/moonlight-server/state/config.hpp
+++ b/src/moonlight-server/state/config.hpp
@@ -40,9 +40,11 @@ void unpair(const Config &cfg, const PairedClient &client);
*/
inline std::optional get_client_via_ssl(const Config &cfg, x509::x509_ptr client_cert) {
auto paired_clients = cfg.paired_clients->load();
- auto search_result =
- std::find_if(paired_clients->begin(), paired_clients->end(), [&client_cert](const PairedClient &pair_client) {
- auto paired_cert = x509::cert_from_string(pair_client.client_cert);
+ auto search_result = std::find_if(
+ paired_clients->begin(),
+ paired_clients->end(),
+ [&client_cert](const immer::box &pair_client) {
+ auto paired_cert = x509::cert_from_string(pair_client->client_cert);
auto verification_error = x509::verification_error(paired_cert, client_cert);
if (verification_error) {
logs::log(logs::trace, "X509 certificate verification error: {}", verification_error.value());
@@ -52,7 +54,7 @@ inline std::optional get_client_via_ssl(const Config &cfg, x509::x
}
});
if (search_result != paired_clients->end()) {
- return *search_result;
+ return **search_result;
} else {
return std::nullopt;
}
@@ -65,21 +67,31 @@ inline std::optional get_client_via_ssl(const Config &cfg, const s
return get_client_via_ssl(cfg, x509::cert_from_string(client_cert));
}
+/**
+ * Returns the client ID for a given client
+ */
inline std::size_t get_client_id(const PairedClient ¤t_client) {
return std::hash{}(current_client.client_cert);
}
-inline std::optional get_client_by_id(const Config &cfg, std::size_t client_id) {
+/**
+ * Returns the first PairedClient with the given client_id
+ */
+inline std::optional get_client_by_id(const Config &cfg, const std::string &client_id) {
auto paired_clients = cfg.paired_clients->load();
- auto search_result =
- std::find_if(paired_clients->begin(), paired_clients->end(), [client_id](const PairedClient &pair_client) {
- return get_client_id(pair_client) == client_id;
- });
+ auto client_id_num = std::stoull(client_id);
+
+ auto search_result = std::find_if(paired_clients->begin(),
+ paired_clients->end(),
+ [client_id_num](const immer::box &pair_client) -> bool {
+ auto id = get_client_id(*pair_client);
+ return id == client_id_num;
+ });
+
if (search_result != paired_clients->end()) {
- return *search_result;
- } else {
- return std::nullopt;
+ return **search_result;
}
+ return std::nullopt;
}
/**
@@ -138,4 +150,12 @@ static moonlight::control::pkts::CONTROLLER_TYPE get_controller_type(const Contr
}
return moonlight::control::pkts::CONTROLLER_TYPE::AUTO;
}
+
+std::optional get_client_by_id(const Config &cfg, const std::string &client_id);
+
+/**
+ * Replaces the specified client_id with the updated_client
+ * Side effects: will save back the configuration to disk
+ */
+void update_client_settings(const Config &cfg, std::size_t client_id, const PairedClient &updated_client);
} // namespace state
\ No newline at end of file
diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp
index fa9335e8..34921e99 100644
--- a/src/moonlight-server/state/configTOML.cpp
+++ b/src/moonlight-server/state/configTOML.cpp
@@ -305,4 +305,30 @@ void unpair(const Config &cfg, const PairedClient &client) {
rfl::toml::save(cfg.config_source, tml);
}
+void update_client_settings(const Config &cfg, std::size_t client_id, const PairedClient &updated_client) {
+
+ auto update_client_fn = [&](immer::box client) -> immer::box {
+ if (get_client_id(client) == client_id) {
+ return immer::box(updated_client);
+ }
+ return client;
+ };
+
+ cfg.paired_clients->update([&](const PairedClientList &paired_clients) {
+ return paired_clients | //
+ ranges::views::transform(update_client_fn) | //
+ ranges::to();
+ });
+
+ // Update the TOML file
+ auto tml = rfl::toml::load(cfg.config_source).value();
+
+ tml.paired_clients = tml.paired_clients | //
+ ranges::views::transform(update_client_fn) | //
+ ranges::to>();
+
+ // Save back to file
+ rfl::toml::save(cfg.config_source, tml);
+}
+
} // namespace state
\ No newline at end of file
diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp
index e7bf7014..ed4e08b4 100644
--- a/src/moonlight-server/state/data-structures.hpp
+++ b/src/moonlight-server/state/data-structures.hpp
@@ -225,5 +225,4 @@ const static immer::array DISPLAY_CONFIGURATIONS = {{
{.width = 7680, .height = 4320, .refreshRate = 90},
{.width = 7680, .height = 4320, .refreshRate = 60},
}};
-
} // namespace state
\ No newline at end of file
diff --git a/tests/testWolfAPI.cpp b/tests/testWolfAPI.cpp
index aacdd595..e7876eb5 100644
--- a/tests/testWolfAPI.cpp
+++ b/tests/testWolfAPI.cpp
@@ -3,10 +3,11 @@
#include
#include
#include
+#include
#include
-using Catch::Matchers::Equals;
using Catch::Matchers::ContainsSubstring;
+using Catch::Matchers::Equals;
using namespace wolf::api;
using curl_ptr = std::unique_ptr;
@@ -84,9 +85,14 @@ req(CURL *handle,
TEST_CASE("Pair APIs", "[API]") {
auto event_bus = std::make_shared();
auto running_sessions = std::make_shared>>();
- auto config = immer::box(state::load_or_default("config.test.toml", event_bus, running_sessions));
+ auto config = state::load_or_default("config.test.toml", event_bus, running_sessions);
+ { // Avoid overriding the test config file (shared across multiple tests)
+ config.config_source = "config.test.EDITED.toml";
+ auto tml = rfl::toml::load("config.test.toml").value();
+ rfl::toml::save(config.config_source, tml);
+ }
auto app_state = immer::box(state::AppState{
- .config = config,
+ .config = immer::box(config),
.pairing_cache = std::make_shared>>(),
.pairing_atom = std::make_shared>>>(),
.event_bus = event_bus,
@@ -110,8 +116,16 @@ TEST_CASE("Pair APIs", "[API]") {
response = req(curl.get(), HTTPMethod::GET, "http://localhost/api/v1/clients");
REQUIRE(response);
REQUIRE_THAT(response->second,
- Equals("{\"success\":true,\"clients\":[{\"client_id\":10594003729173467913,\"app_state_folder\":\"some/"
- "folder\"}]}"));
+ Equals("{\"success\":true,\"clients\":["
+ "{\"client_id\":\"10594003729173467913\","
+ "\"app_state_folder\":\"some/folder\","
+ "\"settings\":{"
+ "\"run_uid\":1234,"
+ "\"run_gid\":5678,"
+ "\"controllers_override\":[\"PS\"],"
+ "\"mouse_acceleration\":2.5,"
+ "\"v_scroll_acceleration\":1.5,"
+ "\"h_scroll_acceleration\":10.199999809265137}}]}"));
auto pair_promise = std::make_shared>();
@@ -125,7 +139,7 @@ TEST_CASE("Pair APIs", "[API]") {
response = req(curl.get(), HTTPMethod::GET, "http://localhost/api/v1/pair/pending");
REQUIRE(response);
REQUIRE_THAT(response->second,
- Equals("{\"success\":true,\"requests\":[{\"pair_secret\":\"secret\",\"pin\":\"1234\"}]}"));
+ Equals("{\"success\":true,\"requests\":[{\"pair_secret\":\"secret\",\"client_ip\":\"1234\"}]}"));
// Let's complete the pairing process
response = req(curl.get(),
@@ -135,6 +149,34 @@ TEST_CASE("Pair APIs", "[API]") {
REQUIRE(response);
REQUIRE_THAT(response->second, Equals("{\"success\":true}"));
REQUIRE(pair_promise->get_future().get() == "1234");
+
+ { // Test out changing client settings
+ REQUIRE_THAT(app_state->config.get().paired_clients->load().get()[0]->app_state_folder, Equals("some/folder"));
+ response = req(curl.get(),
+ HTTPMethod::POST,
+ "http://localhost/api/v1/clients/settings",
+ "{\"client_id\":\"10594003729173467913\",\"app_state_folder\":\"OVERRIDDEN\", \"settings\":{}}");
+ REQUIRE(response);
+ REQUIRE_THAT(response->second, Equals("{\"success\":true}"));
+ REQUIRE_THAT(app_state->config.get().paired_clients->load().get()[0]->app_state_folder, Equals("OVERRIDDEN"));
+
+ // Check back that we've correctly updated the config file
+ auto tml = rfl::toml::load(config.config_source).value();
+ REQUIRE(tml.paired_clients[0].app_state_folder == "OVERRIDDEN");
+ }
+
+ { // Test out unpairing
+ response = req(curl.get(),
+ HTTPMethod::POST,
+ "http://localhost/api/v1/unpair/client",
+ "{\"client_id\":\"10594003729173467913\"}");
+ REQUIRE(response);
+ REQUIRE_THAT(response->second, Equals("{\"success\":true}"));
+
+ // Check back that we've correctly updated the config file
+ auto tml = rfl::toml::load(config.config_source).value();
+ REQUIRE(tml.paired_clients.empty());
+ }
}
TEST_CASE("APPs APIs", "[API]") {