This commit is contained in:
khodowany
2026-09-30 18:23:25 -05:00
parent 896de203b3
commit 2a1aa2c67a
40 changed files with 1197 additions and 610 deletions
+134
View File
@@ -0,0 +1,134 @@
#include <boost/test/unit_test.hpp>
#include <array>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <future>
#include <memory>
#include <string>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/json/object.hpp>
#include <openssl/sha.h>
#include <vamp_pico/http_client.hpp>
#include <vamp_zero/control.hpp>
namespace {
struct artwork_fixture {
std::filesystem::path root{std::filesystem::temp_directory_path() / ("vamp-title-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))};
std::string bytes = std::string(vamp_protocol::title_image_size, static_cast<std::char_t>(0xa3));
std::string identifier{};
artwork_fixture() {
bytes.replace(0U, 8U, "VPTI\x28\x01\x40\x00", 8U);
auto digest = std::array<std::uchar_t, SHA256_DIGEST_LENGTH>{};
SHA256(reinterpret_cast<std::uchar_t const *>(bytes.data()), bytes.size(), digest.data());
for (auto byte : digest) {
identifier.push_back("0123456789abcdef"[byte >> 4U]);
identifier.push_back("0123456789abcdef"[byte & 15U]);
}
std::filesystem::create_directories(root / "pico/titles");
write(bytes);
}
~artwork_fixture() {
auto error = std::error_code{};
std::filesystem::remove_all(root, error);
}
auto path() const -> std::filesystem::path {
return root / "pico/titles" / (identifier + ".mask");
}
auto write(std::string const &value) -> void {
auto stream = std::ofstream{path(), std::ios::binary};
stream.write(value.data(), static_cast<std::streamsize>(value.size()));
}
auto config() const -> vamp_zero::config {
auto value = vamp_zero::config{};
value.runtime.assets_path = root.string();
return value;
}
auto state() const -> vamp_zero::app_state {
auto catalog = std::make_shared<vamp_zero::catalog>();
catalog->parts.push_back({.id = "part", .title = "Title", .part = "Acoustic", .total_pages = 10000U, .title_image = identifier});
auto value = vamp_zero::app_state{};
value.catalog = catalog;
value.mode = vamp_zero::app_mode::PERFORMANCE_SCORE;
value.active_part_id = "part";
value.page_index = 9999U;
return value;
}
};
auto call(ligo::service_router const &router, std::string method, std::string route, boost::json::value payload = {})
-> std::expected<ligo::service_response, modulus::error> {
auto io = boost::asio::io_context{};
auto future = boost::asio::co_spawn(io, router.call_response({.method = std::move(method), .route = std::move(route)}, std::move(payload)), boost::asio::use_future);
io.run();
return future.get();
}
}
BOOST_FIXTURE_TEST_CASE(control_title_route_serves_registered_binary_and_remote_identity, artwork_fixture) {
auto control = vamp_zero::control_state{config(), state()};
auto router = vamp_zero::make_router(control);
auto status = call(router, "GET", vamp_protocol::remote_display_path);
BOOST_REQUIRE(status);
auto remote = vamp_pico::remote_display_state{};
BOOST_REQUIRE(vamp_pico::parse_remote_display_body(status->body, remote));
BOOST_TEST(std::string{remote.title_image.data()} == identifier);
BOOST_TEST(remote.page == 10000U);
BOOST_TEST(remote.pages == 10000U);
auto response = call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}});
BOOST_REQUIRE(response);
BOOST_TEST(response->content_type == "application/octet-stream");
BOOST_TEST(response->body == bytes);
auto validated = vamp_zero::validated_title_path(root.string(), identifier);
BOOST_REQUIRE(validated);
BOOST_TEST(*validated == path().string());
BOOST_CHECK(!call(router, "GET", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}}));
}
BOOST_FIXTURE_TEST_CASE(control_title_route_rejects_arbitrary_unregistered_and_corrupt_assets, artwork_fixture) {
auto control = vamp_zero::control_state{config(), state()};
auto router = vamp_zero::make_router(control);
for (auto const &id : {std::string{"../title"}, std::string(64U, 'f'), std::string(64U, 'A'), identifier + "a"}) {
BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", id}}));
}
BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}, {"path", path().string()}}));
BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", 1}}));
BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path));
auto empty = vamp_zero::control_state{config(), {}};
BOOST_CHECK(!call(vamp_zero::make_router(empty), "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}}));
for (auto const &corrupt : {bytes.substr(1U), bytes + "x", std::string(bytes.size(), 'x')}) {
write(corrupt);
BOOST_CHECK(!vamp_zero::validated_title_path(root.string(), identifier));
BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}}));
}
auto modified = bytes;
modified.back() ^= 1;
write(modified);
BOOST_CHECK(!vamp_zero::validated_title_path(root.string(), identifier));
BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}}));
}
BOOST_FIXTURE_TEST_CASE(control_title_validation_rejects_symlink_outside_title_directory, artwork_fixture) {
auto other = root / "other.mask";
std::filesystem::rename(path(), other);
std::filesystem::create_symlink(other, path());
BOOST_CHECK(!vamp_zero::validated_title_path(root.string(), identifier));
}
BOOST_FIXTURE_TEST_CASE(control_has_no_browser_remote_or_catalog_dump, artwork_fixture) {
auto control = vamp_zero::control_state{config(), {}};
auto router = vamp_zero::make_router(control);
BOOST_CHECK(!call(router, "GET", "/"));
BOOST_CHECK(!call(router, "GET", "/api/status"));
BOOST_CHECK(!call(router, "GET", "/assets/PetalumaScript.otf"));
BOOST_CHECK(!call(router, "GET", "/assets/other.otf"));
BOOST_REQUIRE(call(router, "GET", "/api/health"));
BOOST_REQUIRE(call(router, "GET", vamp_protocol::remote_display_path));
}