yams/tests/unit/storage/s3_signer_catch2_test.cpp

169 lines
5.8 KiB
C++

// Catch2 tests for S3 request signing
// Migrated from GTest: s3_signer_test.cpp
#include <catch2/catch_test_macros.hpp>
#include <curl/curl.h>
#include <cstdlib>
#include <yams/storage/s3_signer.h>
#include <yams/storage/storage_backend.h>
using namespace yams::storage;
TEST_CASE("S3Signer includes optional headers in signature and list",
"[storage][s3][signer][catch2]") {
BackendConfig cfg;
cfg.region = "us-east-1";
cfg.credentials["access_key"] = "TESTACCESSKEY";
cfg.credentials["secret_key"] = "TESTSECRETKEY";
CURL* curl = curl_easy_init();
REQUIRE(curl != nullptr);
std::string url = "https://s3.amazonaws.com/test-bucket/test-object?uploads=";
std::string payloadStr = "<CompleteMultipartUpload/>";
auto payload = std::span<const std::byte>(reinterpret_cast<const std::byte*>(payloadStr.data()),
payloadStr.size());
std::vector<std::pair<std::string, std::string>> extra{
{"content-type", "application/xml"},
{"x-amz-server-side-encryption", "aws:kms"},
{"x-amz-storage-class", "STANDARD"},
};
auto res = S3Signer::signRequest(curl, cfg, "POST", url, payload, extra);
REQUIRE(res.has_value());
// Walk header list and collect
bool sawAuth = false, sawCT = false, sawSSE = false, sawSC = false;
for (auto* h = res.value(); h != nullptr; h = h->next) {
std::string line(h->data ? h->data : "");
if (line.rfind("Authorization:", 0) == 0) {
sawAuth = true;
// Signed headers should include content-type
REQUIRE(line.find("SignedHeaders=") != std::string::npos);
REQUIRE(line.find("content-type") != std::string::npos);
}
if (line.rfind("content-type:", 0) == 0 || line.rfind("Content-Type:", 0) == 0)
sawCT = true;
if (line.rfind("x-amz-server-side-encryption:", 0) == 0)
sawSSE = true;
if (line.rfind("x-amz-storage-class:", 0) == 0)
sawSC = true;
}
CHECK(sawAuth);
CHECK(sawCT);
CHECK(sawSSE);
CHECK(sawSC);
curl_slist_free_all(res.value());
curl_easy_cleanup(curl);
}
TEST_CASE("S3Signer trims credential and region whitespace", "[storage][s3][signer][catch2]") {
BackendConfig cfg;
cfg.region = " us-east-1\n";
cfg.credentials["access_key"] = " TESTACCESSKEY\t";
cfg.credentials["secret_key"] = "\nTESTSECRETKEY ";
CURL* curl = curl_easy_init();
REQUIRE(curl != nullptr);
std::string url = "https://s3.amazonaws.com/test-bucket/test-object";
std::string payloadStr = "payload";
auto payload = std::span<const std::byte>(reinterpret_cast<const std::byte*>(payloadStr.data()),
payloadStr.size());
auto res = S3Signer::signRequest(curl, cfg, "PUT", url, payload);
REQUIRE(res.has_value());
bool sawAuth = false;
for (auto* h = res.value(); h != nullptr; h = h->next) {
std::string line(h->data ? h->data : "");
if (line.rfind("Authorization:", 0) == 0) {
sawAuth = true;
CHECK(line.find("Credential=TESTACCESSKEY/") != std::string::npos);
CHECK(line.find("\n") == std::string::npos);
CHECK(line.find("\t") == std::string::npos);
}
}
CHECK(sawAuth);
curl_slist_free_all(res.value());
curl_easy_cleanup(curl);
}
TEST_CASE("S3Signer canonical request includes header separator newline",
"[storage][s3][signer][regression][catch2]") {
#if defined(_WIN32)
_putenv_s("YAMS_S3_SIGNER_FIXED_AMZ_DATE", "20250306T120000Z");
#else
setenv("YAMS_S3_SIGNER_FIXED_AMZ_DATE", "20250306T120000Z", 1);
#endif
BackendConfig cfg;
cfg.region = "us-east-1";
cfg.credentials["access_key"] = "TESTACCESSKEY";
cfg.credentials["secret_key"] = "TESTSECRETKEY";
CURL* curl = curl_easy_init();
REQUIRE(curl != nullptr);
std::string url = "https://bucket.example.com/test.txt";
std::string payloadStr = "hello";
auto payload = std::span<const std::byte>(reinterpret_cast<const std::byte*>(payloadStr.data()),
payloadStr.size());
auto res = S3Signer::signRequest(curl, cfg, "PUT", url, payload);
REQUIRE(res.has_value());
std::string authLine;
std::string dateLine;
for (auto* h = res.value(); h != nullptr; h = h->next) {
std::string line(h->data ? h->data : "");
if (line.rfind("Authorization:", 0) == 0) {
authLine = line;
}
if (line.rfind("x-amz-date:", 0) == 0) {
dateLine = line;
}
}
CHECK(dateLine == "x-amz-date: 20250306T120000Z");
CHECK(authLine.find("Credential=TESTACCESSKEY/20250306/us-east-1/s3/aws4_request") !=
std::string::npos);
CHECK(authLine.find(
"Signature=8611799d0306920216eb826af8f119522e26cb25ec5b6d3f1ea63958a45b8991") !=
std::string::npos);
curl_slist_free_all(res.value());
curl_easy_cleanup(curl);
#if defined(_WIN32)
_putenv_s("YAMS_S3_SIGNER_FIXED_AMZ_DATE", "");
#else
unsetenv("YAMS_S3_SIGNER_FIXED_AMZ_DATE");
#endif
}
TEST_CASE("S3Signer rejects Cloudflare bearer token shape for R2 access key",
"[storage][s3][signer][r2][catch2]") {
BackendConfig cfg;
cfg.region = "auto";
cfg.credentials["access_key"] = "cf-test-token-not-a-real-r2-access-key-0001";
cfg.credentials["secret_key"] = "dummy-secret-that-would-otherwisetrytosign";
CURL* curl = curl_easy_init();
REQUIRE(curl != nullptr);
std::string url =
"https://00000000000000000000000000000000.r2.cloudflarestorage.com/bucket/key";
auto res = S3Signer::signRequest(curl, cfg, "GET", url, {});
REQUIRE_FALSE(res.has_value());
CHECK(res.error().message.find("bearer token") != std::string::npos);
curl_easy_cleanup(curl);
}