988 lines
36 KiB
C++
988 lines
36 KiB
C++
#include "MockAssetAccessor.h"
|
|
#include "MockAssetRequest.h"
|
|
#include "MockAssetResponse.h"
|
|
#include "MockTaskProcessor.h"
|
|
#include "ResponseCacheControl.h"
|
|
|
|
#include <CesiumAsync/AsyncSystem.h>
|
|
#include <CesiumAsync/CacheItem.h>
|
|
#include <CesiumAsync/CachingAssetAccessor.h>
|
|
#include <CesiumAsync/HttpHeaders.h>
|
|
#include <CesiumAsync/IAssetAccessor.h>
|
|
#include <CesiumAsync/IAssetRequest.h>
|
|
#include <CesiumAsync/IAssetResponse.h>
|
|
#include <CesiumAsync/ICacheDatabase.h>
|
|
|
|
#include <doctest/doctest.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <ctime>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace CesiumAsync;
|
|
|
|
namespace {
|
|
|
|
class MockStoreCacheDatabase : public ICacheDatabase {
|
|
public:
|
|
struct StoreRequestParameters {
|
|
std::string key;
|
|
std::time_t expiryTime;
|
|
std::string url;
|
|
std::string requestMethod;
|
|
HttpHeaders requestHeaders;
|
|
uint16_t statusCode;
|
|
HttpHeaders responseHeaders;
|
|
std::vector<std::byte> responseData;
|
|
};
|
|
|
|
MockStoreCacheDatabase()
|
|
: getEntryCall{false},
|
|
storeResponseCall{false},
|
|
pruneCall{false},
|
|
clearAllCall{false} {}
|
|
|
|
virtual std::optional<CacheItem>
|
|
getEntry(const std::string& /*key*/) const override {
|
|
this->getEntryCall = true;
|
|
return this->cacheItem;
|
|
}
|
|
|
|
virtual bool storeEntry(
|
|
const std::string& key,
|
|
std::time_t expiryTime,
|
|
const std::string& url,
|
|
const std::string& requestMethod,
|
|
const HttpHeaders& requestHeaders,
|
|
uint16_t statusCode,
|
|
const HttpHeaders& responseHeaders,
|
|
const std::span<const std::byte>& responseData) override {
|
|
this->storeRequestParam = StoreRequestParameters{
|
|
key,
|
|
expiryTime,
|
|
url,
|
|
requestMethod,
|
|
requestHeaders,
|
|
statusCode,
|
|
responseHeaders,
|
|
std::vector<std::byte>(responseData.begin(), responseData.end())};
|
|
this->storeResponseCall = true;
|
|
return true;
|
|
}
|
|
|
|
virtual bool prune() override {
|
|
this->pruneCall = true;
|
|
return true;
|
|
}
|
|
|
|
virtual bool clearAll() override {
|
|
this->clearAllCall = true;
|
|
return true;
|
|
}
|
|
|
|
mutable bool getEntryCall;
|
|
bool storeResponseCall;
|
|
bool pruneCall;
|
|
bool clearAllCall;
|
|
|
|
std::optional<StoreRequestParameters> storeRequestParam;
|
|
std::optional<CacheItem> cacheItem;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
bool runResponseCacheTest(
|
|
int statusCode,
|
|
const std::string& methodStr,
|
|
const HttpHeaders& httpHeaders) {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(statusCode),
|
|
"app/json",
|
|
httpHeaders,
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
methodStr,
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
|
|
return mockCacheDatabase->storeResponseCall;
|
|
}
|
|
|
|
TEST_CASE("Test the condition of caching the request") {
|
|
SUBCASE("Cache request") {
|
|
SUBCASE("GET request, has max-age, cacheable status code") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Control", "must-revalidate, max-age=100"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, has Expires header, cacheable status code") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "app/json"},
|
|
{"Expires", "Wed, 21 Oct 5020 07:28:00 GMT"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, max-age 0, old Expires header") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
// Similar to Google Photorealistic 3D Tiles, root request
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Cache-Control", "private, max-age=0, must-revalidate"},
|
|
{"ETag", "deadbeef"},
|
|
{"Expires", "Mon, 01 Jan 1990 00:00:00 GMT"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, max-age 0, stale-while-revalidate") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
// Similar to Google Photorealistic 3D Tiles, tile request
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Cache-Control",
|
|
"private, max-age=0, stale-while-revalidate=86400"},
|
|
{"ETag", "deadbeef"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, no-cache with Etag") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Cache-Control", "no-cache"},
|
|
{"ETag", "deadbeef"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, no-cache with Last-Modified") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Cache-Control", "no-cache"},
|
|
{"Last-Modified", "Mon, 01 Jan 1990 00:00:00 GMT"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, just Last-Modified") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Last-Modified", "Mon, 01 Jan 1990 00:00:00 GMT"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, just Etag") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"ETag", "deadbeef"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE(
|
|
"GET Request, Expires header is less than current, but has an ETag") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"ETag", "deadbeef"},
|
|
{"Expires", "Wed, 21 Oct 2010 07:28:00 GMT"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, Expires header is less than current, but has a "
|
|
"Last-Modified") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Last-Modified", "Mon, 01 Jan 1990 00:00:00 GMT"},
|
|
{"Expires", "Wed, 21 Oct 2010 07:28:00 GMT"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, max-age is zero, but has an ETag") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"ETag", "deadbeef"},
|
|
{"Cache-Control", "max-age=0"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
|
|
SUBCASE("GET Request, max-age is zero, but has a Last-Modified") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Last-Modified", "Mon, 01 Jan 1990 00:00:00 GMT"},
|
|
{"Cache-Control", "max-age=0"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == true);
|
|
}
|
|
}
|
|
}
|
|
|
|
SUBCASE("No cache condition") {
|
|
SUBCASE("No store for response that doesn't have GET method") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Control",
|
|
"must-revalidate, max-age=100, public, private"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"POST",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == false);
|
|
}
|
|
|
|
SUBCASE("No store for response that has no cacheable status code") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(404),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Control",
|
|
"must-revalidate, public, private, max-age=100"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == false);
|
|
}
|
|
|
|
SUBCASE(
|
|
"No store for response that has No-Store in the cache-control header") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Control", "no-store"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == false);
|
|
}
|
|
|
|
SUBCASE(
|
|
"No store for response that has No-Cache in the cache-control header") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Control", "must-revalidate, no-cache"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
|
|
//
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
|
//
|
|
// The no-cache response directive indicates that the response can be
|
|
// stored in caches, but the response must be validated with the origin
|
|
// server before each reuse, even when the cache is disconnected from the
|
|
// origin server.
|
|
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == false);
|
|
}
|
|
|
|
SUBCASE(
|
|
"No store for response that has no Cache-Control and Expires header") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{{"Content-Type", "app/json"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == false);
|
|
}
|
|
|
|
SUBCASE("No store if Expires header is less than the current") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Expires", "Wed, 21 Oct 2010 07:28:00 GMT"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == false);
|
|
}
|
|
|
|
SUBCASE("No store if max-age=0 and response has no ETag or Last-Modified") {
|
|
std::vector<int> statusCodes{200, 202, 203, 204, 205, 304};
|
|
|
|
for (auto& statusCode : statusCodes) {
|
|
HttpHeaders headers = {
|
|
{"Content-Type", "application/json"},
|
|
{"Cache-Control", "max-age=0"}};
|
|
|
|
bool responseCached = runResponseCacheTest(statusCode, "GET", headers);
|
|
CHECK(responseCached == false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Test calculation of expiry time for the cached response") {
|
|
SUBCASE("Response has max-age cache control") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Control", "must-revalidate, private, max-age=400"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == true);
|
|
REQUIRE(
|
|
mockCacheDatabase->storeRequestParam->expiryTime - std::time(nullptr) ==
|
|
400);
|
|
}
|
|
|
|
SUBCASE("Response has Expires header") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Expires", "Wed, 21 Oct 2037 07:28:00 GMT"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
std::unique_ptr<MockStoreCacheDatabase> ownedMockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
MockStoreCacheDatabase* mockCacheDatabase = ownedMockCacheDatabase.get();
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(ownedMockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.wait();
|
|
REQUIRE(mockCacheDatabase->storeResponseCall == true);
|
|
REQUIRE(mockCacheDatabase->storeRequestParam->expiryTime == 2139722880);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Test serving cache item") {
|
|
SUBCASE("Cache item doesn't exist") {
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Response-Header", "Response-Value"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{{"Request-Header", "Request-Value"}},
|
|
std::move(mockResponse));
|
|
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::make_unique<MockStoreCacheDatabase>());
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
// test that the response is from the server
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(asyncSystem, "test.com", std::vector<IAssetAccessor::THeader>{})
|
|
.thenImmediately(
|
|
[](const std::shared_ptr<IAssetRequest>& completedRequest) {
|
|
REQUIRE(completedRequest != nullptr);
|
|
REQUIRE(completedRequest->url() == "test.com");
|
|
REQUIRE(
|
|
completedRequest->headers() ==
|
|
HttpHeaders{{"Request-Header", "Request-Value"}});
|
|
REQUIRE(completedRequest->method() == "GET");
|
|
|
|
const IAssetResponse* response = completedRequest->response();
|
|
REQUIRE(response != nullptr);
|
|
REQUIRE(
|
|
response->headers().at("Response-Header") ==
|
|
"Response-Value");
|
|
REQUIRE(response->statusCode() == 200);
|
|
REQUIRE(response->contentType() == "app/json");
|
|
REQUIRE(response->data().empty());
|
|
REQUIRE(!ResponseCacheControl::parseFromResponseHeaders(
|
|
response->headers())
|
|
.has_value());
|
|
})
|
|
.wait();
|
|
}
|
|
|
|
SUBCASE("Successfully retrieve cache item") {
|
|
// create mock request and mock response. They are intended to be different
|
|
// from the cache content so that we can verify the response in the callback
|
|
// comes from the cache
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Response-Header", "Response-Value"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
// mock fresh cache item
|
|
std::unique_ptr<MockStoreCacheDatabase> mockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
std::time_t currentTime = std::time(nullptr);
|
|
CacheRequest cacheRequest(
|
|
HttpHeaders{{"Cache-Request-Header", "Cache-Request-Value"}},
|
|
"GET",
|
|
"cache.com");
|
|
CacheResponse cacheResponse(
|
|
static_cast<uint16_t>(200),
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Response-Header", "Cache-Response-Value"},
|
|
{"Cache-Control", "max-age=100, private"}},
|
|
std::vector<std::byte>());
|
|
CacheItem cacheItem(
|
|
currentTime + 100,
|
|
std::move(cacheRequest),
|
|
std::move(cacheResponse));
|
|
mockCacheDatabase->cacheItem = cacheItem;
|
|
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(mockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
// test that the response is from the cache
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(
|
|
asyncSystem,
|
|
"test.com",
|
|
std::vector<IAssetAccessor::THeader>{
|
|
{"Some-Request-Header", "The Value"}})
|
|
.thenImmediately(
|
|
[](const std::shared_ptr<IAssetRequest>& completedRequest) {
|
|
REQUIRE(completedRequest != nullptr);
|
|
REQUIRE(completedRequest->method() == "GET");
|
|
|
|
// URL and Headers should match the original request, even if
|
|
// that's different from what's in the cache.
|
|
REQUIRE(completedRequest->url() == "test.com");
|
|
REQUIRE(
|
|
completedRequest->headers() ==
|
|
HttpHeaders{{"Some-Request-Header", "The Value"}});
|
|
|
|
const IAssetResponse* response = completedRequest->response();
|
|
REQUIRE(response != nullptr);
|
|
REQUIRE(
|
|
response->headers().at("Cache-Response-Header") ==
|
|
"Cache-Response-Value");
|
|
REQUIRE(response->statusCode() == 200);
|
|
REQUIRE(response->contentType() == "app/json");
|
|
REQUIRE(response->data().empty());
|
|
|
|
std::optional<ResponseCacheControl> cacheControl =
|
|
ResponseCacheControl::parseFromResponseHeaders(
|
|
response->headers());
|
|
|
|
REQUIRE(cacheControl.has_value());
|
|
REQUIRE(cacheControl->mustRevalidate() == false);
|
|
REQUIRE(cacheControl->noCache() == false);
|
|
REQUIRE(cacheControl->noStore() == false);
|
|
REQUIRE(cacheControl->noTransform() == false);
|
|
REQUIRE(cacheControl->accessControlPublic() == false);
|
|
REQUIRE(cacheControl->accessControlPrivate() == true);
|
|
REQUIRE(cacheControl->proxyRevalidate() == false);
|
|
REQUIRE(cacheControl->maxAgeExists() == true);
|
|
REQUIRE(cacheControl->maxAgeValue() == 100);
|
|
REQUIRE(cacheControl->sharedMaxAgeExists() == false);
|
|
})
|
|
.wait();
|
|
}
|
|
|
|
SUBCASE("Retrieve outdated cache item and cache control mandates "
|
|
"revalidation before using it") {
|
|
// Mock 304 response
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(304),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Revalidation-Response-Header", "Revalidation-Response-Value"},
|
|
{"Cache-Control", "max-age=300, must-revalidate, private"},
|
|
{"ETag", "1"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
// mock cache item
|
|
std::unique_ptr<MockStoreCacheDatabase> mockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
std::time_t currentTime = std::time(nullptr);
|
|
CacheRequest cacheRequest(
|
|
HttpHeaders{{"Cache-Request-Header", "Cache-Request-Value"}},
|
|
"GET",
|
|
"cache.com");
|
|
CacheResponse cacheResponse(
|
|
static_cast<uint16_t>(200),
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Response-Header", "Cache-Response-Value"},
|
|
{"Cache-Control", "max-age=100, private"},
|
|
{"ETag", "1"}},
|
|
std::vector<std::byte>());
|
|
CacheItem cacheItem(
|
|
currentTime - 100,
|
|
std::move(cacheRequest),
|
|
std::move(cacheResponse));
|
|
mockCacheDatabase->cacheItem = cacheItem;
|
|
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::make_unique<MockAssetAccessor>(mockRequest),
|
|
std::move(mockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
// test that the response is from the cache and it should update the header
|
|
// and cache control coming from the validation response
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(
|
|
asyncSystem,
|
|
"test.com",
|
|
std::vector<IAssetAccessor::THeader>{
|
|
{"Some-Request-Header", "The Value"}})
|
|
.thenImmediately(
|
|
[](const std::shared_ptr<IAssetRequest>& completedRequest) {
|
|
REQUIRE(completedRequest != nullptr);
|
|
REQUIRE(completedRequest->method() == "GET");
|
|
|
|
// URL and Headers should match the original request, even if
|
|
// that's different from what's in the cache.
|
|
REQUIRE(completedRequest->url() == "test.com");
|
|
REQUIRE(
|
|
completedRequest->headers() ==
|
|
HttpHeaders{{"Some-Request-Header", "The Value"}});
|
|
|
|
// check response header is updated
|
|
const IAssetResponse* response = completedRequest->response();
|
|
REQUIRE(response != nullptr);
|
|
REQUIRE(
|
|
response->headers().at("Revalidation-Response-Header") ==
|
|
"Revalidation-Response-Value");
|
|
REQUIRE(
|
|
response->headers().at("Cache-Response-Header") ==
|
|
"Cache-Response-Value");
|
|
REQUIRE(response->statusCode() == 200);
|
|
REQUIRE(response->contentType() == "app/json");
|
|
REQUIRE(response->data().empty());
|
|
|
|
// check cache control is updated
|
|
std::optional<ResponseCacheControl> cacheControl =
|
|
ResponseCacheControl::parseFromResponseHeaders(
|
|
response->headers());
|
|
REQUIRE(cacheControl.has_value());
|
|
REQUIRE(cacheControl->mustRevalidate() == true);
|
|
REQUIRE(cacheControl->noCache() == false);
|
|
REQUIRE(cacheControl->noStore() == false);
|
|
REQUIRE(cacheControl->noTransform() == false);
|
|
REQUIRE(cacheControl->accessControlPublic() == false);
|
|
REQUIRE(cacheControl->accessControlPrivate() == true);
|
|
REQUIRE(cacheControl->proxyRevalidate() == false);
|
|
REQUIRE(cacheControl->maxAgeExists() == true);
|
|
REQUIRE(cacheControl->maxAgeValue() == 300);
|
|
REQUIRE(cacheControl->sharedMaxAgeExists() == false);
|
|
})
|
|
.wait();
|
|
}
|
|
|
|
SUBCASE("Cache should serve validation response from the server directly if "
|
|
"it is not 304") {
|
|
// Mock 200 response
|
|
std::unique_ptr<IAssetResponse> mockResponse =
|
|
std::make_unique<MockAssetResponse>(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Revalidation-Response-Header", "Revalidation-Response-Value"},
|
|
{"ETag", "1"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<IAssetRequest> mockRequest =
|
|
std::make_shared<MockAssetRequest>(
|
|
"GET",
|
|
"test.com",
|
|
HttpHeaders{},
|
|
std::move(mockResponse));
|
|
|
|
// mock cache item
|
|
std::unique_ptr<MockStoreCacheDatabase> mockCacheDatabase =
|
|
std::make_unique<MockStoreCacheDatabase>();
|
|
std::time_t currentTime = std::time(nullptr);
|
|
CacheRequest cacheRequest(
|
|
HttpHeaders{{"Cache-Request-Header", "Cache-Request-Value"}},
|
|
"GET",
|
|
"cache.com");
|
|
CacheResponse cacheResponse(
|
|
static_cast<uint16_t>(200),
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Cache-Response-Header", "Cache-Response-Value"},
|
|
{"Cache-Control", "max-age=100, private"},
|
|
{"ETag", "1"}},
|
|
std::vector<std::byte>());
|
|
CacheItem cacheItem(
|
|
currentTime - 100,
|
|
std::move(cacheRequest),
|
|
std::move(cacheResponse));
|
|
mockCacheDatabase->cacheItem = cacheItem;
|
|
|
|
std::unique_ptr<MockAssetAccessor> pAssetAccessor =
|
|
std::make_unique<MockAssetAccessor>(mockRequest);
|
|
|
|
pAssetAccessor->responsesByUrl["test.com"] = MockAssetResponse(
|
|
static_cast<uint16_t>(200),
|
|
"app/json",
|
|
HttpHeaders{
|
|
{"Content-Type", "app/json"},
|
|
{"Revalidation-Response-Header", "Revalidation-Response-Value"},
|
|
{"ETag", "1"}},
|
|
std::vector<std::byte>());
|
|
|
|
std::shared_ptr<CachingAssetAccessor> cacheAssetAccessor =
|
|
std::make_shared<CachingAssetAccessor>(
|
|
spdlog::default_logger(),
|
|
std::move(pAssetAccessor),
|
|
std::move(mockCacheDatabase));
|
|
std::shared_ptr<MockTaskProcessor> mockTaskProcessor =
|
|
std::make_shared<MockTaskProcessor>();
|
|
|
|
// test that the response is from the server directly
|
|
AsyncSystem asyncSystem(mockTaskProcessor);
|
|
cacheAssetAccessor
|
|
->get(
|
|
asyncSystem,
|
|
"test.com",
|
|
std::vector<IAssetAccessor::THeader>{
|
|
{"Some-Request-Header", "The Value"}})
|
|
.thenImmediately(
|
|
[](const std::shared_ptr<IAssetRequest>& completedRequest) {
|
|
REQUIRE(completedRequest != nullptr);
|
|
REQUIRE(completedRequest->url() == "test.com");
|
|
REQUIRE(completedRequest->method() == "GET");
|
|
REQUIRE(
|
|
completedRequest->headers() ==
|
|
HttpHeaders{{"Some-Request-Header", "The Value"}});
|
|
|
|
const IAssetResponse* response = completedRequest->response();
|
|
REQUIRE(response != nullptr);
|
|
|
|
REQUIRE(
|
|
response->headers().at("Revalidation-Response-Header") ==
|
|
"Revalidation-Response-Value");
|
|
REQUIRE(response->statusCode() == 200);
|
|
REQUIRE(response->contentType() == "app/json");
|
|
REQUIRE(response->data().empty());
|
|
REQUIRE(!ResponseCacheControl::parseFromResponseHeaders(
|
|
response->headers())
|
|
.has_value());
|
|
})
|
|
.wait();
|
|
}
|
|
}
|