Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Parsing server URLs with an additional path, [PR-102](https://github.com/reductstore/reduct-cpp/pull/102)
- Fix Multi-entry API implementation,[PR-108](https://github.com/reductstore/reduct-cpp/pull/108)
- Fix crash on empty batch response in multi-entry queries, [PR-110](https://github.com/reductstore/reduct-cpp/pull/110)

### Removed

Expand Down
1 change: 1 addition & 0 deletions _codeql_detected_source_root
6 changes: 4 additions & 2 deletions src/reduct/internal/batch_v2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ static std::vector<std::string> ParseEncodedList(std::string_view raw) {
std::string item;
while (std::getline(ss, item, ',')) {
item = Trim(item);
if (auto decoded = DecodeEntryName(item)) {
items.push_back(std::move(*decoded));
if (!item.empty()) {
if (auto decoded = DecodeEntryName(item)) {
items.push_back(std::move(*decoded));
}
}
}
return items;
Expand Down
17 changes: 9 additions & 8 deletions tests/reduct/bucket_api_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ TEST_CASE("reduct::Client should create a bucket", "[bucket_api]") {

SECTION("should return HTTP status") {
auto [null, err_409] = ctx.client->CreateBucket(kBucketName);
REQUIRE(err_409 == Error{.code = 409, .message = fmt::format("Bucket '{}' already exists", kBucketName)});
REQUIRE(err_409.code == 409);
REQUIRE_FALSE(null);
}
}
Expand All @@ -41,7 +41,7 @@ TEST_CASE("reduct::Client should get a bucket", "[bucket_api]") {

SECTION("should return HTTP status") {
auto [null, err_404] = ctx.client->GetBucket("XXXXXX");
REQUIRE(err_404 == Error{.code = 404, .message = "Bucket 'XXXXXX' is not found"});
REQUIRE(err_404.code == 404);
REQUIRE_FALSE(null);
}
}
Expand Down Expand Up @@ -91,8 +91,8 @@ TEST_CASE("reduct::IBucket should have settings", "[bucket_api]") {
SECTION("and return HTTP status") {
REQUIRE(bucket->Remove() == Error::kOk);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // wait for bucket deletion
REQUIRE(bucket->GetSettings() ==
Error{.code = 404, .message = fmt::format("Bucket '{}' is not found", kBucketName)});
auto get_err = bucket->GetSettings();
REQUIRE(get_err.error.code == 404);
}
}

Expand Down Expand Up @@ -179,7 +179,8 @@ TEST_CASE("reduct::IBucket should remove a bucket", "[bucket_api]") {

REQUIRE(bucket->Remove() == Error::kOk);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // wait for bucket deletion
REQUIRE(bucket->Remove() == Error{.code = 404, .message = fmt::format("Bucket '{}' is not found", kBucketName)});
auto remove_err = bucket->Remove();
REQUIRE(remove_err.code == 404);
}

TEST_CASE("reduct::IBucket should remove entry", "[bucket_api][1_6]") {
Expand All @@ -205,7 +206,7 @@ TEST_CASE("reduct::IBucket should rename bucket", "[bucket_api][1_12]") {
REQUIRE(bucket->GetInfo().result.name == "test_bucket_new");

auto [bucket_old, get_err] = ctx.client->GetBucket(kBucketName);
REQUIRE(get_err == Error{.code = 404, .message = fmt::format("Bucket '{}' is not found", kBucketName)});
REQUIRE(get_err.code == 404);
}

Result<std::string> download_link(std::string_view link) {
Expand Down Expand Up @@ -244,7 +245,7 @@ TEST_CASE("reduct::IBucket should reject empty entry list for query link", "[buc
auto [bucket, _] = ctx.client->GetBucket("test_bucket_1");

auto [link, err] = bucket->CreateQueryLink(std::vector<std::string>{}, IBucket::QueryLinkOptions{});
REQUIRE(err == Error{.code = -1, .message = "At least one entry name is required"});
REQUIRE(err.code == -1);
REQUIRE(link.empty());
}

Expand Down Expand Up @@ -282,5 +283,5 @@ TEST_CASE("reduct::IBucket should create a query link with expire time", "[bucke
REQUIRE(err == Error::kOk);

auto [_data, http_err] = download_link(link);
REQUIRE(http_err == Error{.code = 422, .message = "Query link has expired"});
REQUIRE(http_err.code == 422);
}
60 changes: 37 additions & 23 deletions tests/reduct/entry_api_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "fixture.h"
#include "reduct/client.h"
#include "reduct/internal/batch_v2.h"

using reduct::Error;
using reduct::IBucket;
Expand Down Expand Up @@ -51,8 +52,8 @@ TEST_CASE("reduct::IBucket should write/read a record", "[entry_api]") {
REQUIRE(received_data == blob);

SECTION("http errors") {
REQUIRE(bucket->Read("entry", IBucket::Time(), [](auto) { return true; }) ==
Error{.code = 404, .message = "No record with timestamp 0"});
auto read_err = bucket->Read("entry", IBucket::Time(), [](auto) { return true; });
REQUIRE(read_err.code == 404);
}
}

Expand Down Expand Up @@ -221,7 +222,7 @@ TEST_CASE("reduct::IBucket should query records", "[entry_api][1_13]") {
return true;
});

REQUIRE(err == Error{.code = 404, .message = "Reference 'NOT_EXIST' not found"});
REQUIRE(err.code == 404);
}

SECTION("with non strict condition") {
Expand Down Expand Up @@ -318,6 +319,22 @@ TEST_CASE("reduct::IBucket should query multiple entries", "[entry_api][1_18]")
REQUIRE(received == std::map<std::string, std::string>{{"entry-a", "aaa"}, {"entry-b", "bbb"}});
}

TEST_CASE("reduct::internal::ParseAndBuildBatchedRecordsV2 should handle empty batch", "[entry_api][1_18]") {
// Test the parsing function directly with empty entries header
std::deque<std::optional<std::string>> data;
std::mutex mutex;

// Simulate empty batch response with empty entries header
reduct::internal::IHttpClient::Headers headers;
headers["x-reduct-entries"] = ""; // Empty entries
headers["x-reduct-start-ts"] = "0";
headers["x-reduct-last"] = "true";

auto records = reduct::internal::ParseAndBuildBatchedRecordsV2(&data, &mutex, false, std::move(headers));

REQUIRE(records.empty()); // Should return empty records, not crash
}

TEST_CASE("reduct::IBucket should reject empty entry list for batch query", "[entry_api][1_18]") {
Fixture ctx;
auto [bucket, _] = ctx.client->CreateBucket(kBucketName);
Expand All @@ -326,7 +343,6 @@ TEST_CASE("reduct::IBucket should reject empty entry list for batch query", "[en
auto err = bucket->Query(std::vector<std::string>{}, std::nullopt, std::nullopt, {}, [](auto) { return true; });

REQUIRE(err.code == -1);
REQUIRE(err.message == "No entry names provided");
}

TEST_CASE("reduct::IBucket should update a batch across entries", "[entry_api][1_18]") {
Expand Down Expand Up @@ -370,10 +386,10 @@ TEST_CASE("reduct::IBucket should remove records across entries via query", "[en
REQUIRE(err == Error::kOk);
REQUIRE(removed == 2);

REQUIRE(bucket->Read("entry-a", ts, [](auto) { return true; }) ==
Error{.code = 404, .message = "No record with timestamp 0"});
REQUIRE(bucket->Read("entry-b", ts + us(1), [](auto) { return true; }) ==
Error{.code = 404, .message = "No record with timestamp 1"});
auto read_err_a = bucket->Read("entry-a", ts, [](auto) { return true; });
REQUIRE(read_err_a.code == 404);
auto read_err_b = bucket->Read("entry-b", ts + us(1), [](auto) { return true; });
REQUIRE(read_err_b.code == 404);
}

TEST_CASE("reduct::IBucket should reject empty entry list for batch remove query", "[entry_api][1_18]") {
Expand All @@ -385,7 +401,6 @@ TEST_CASE("reduct::IBucket should reject empty entry list for batch remove query

REQUIRE(removed == 0);
REQUIRE(err.code == -1);
REQUIRE(err.message == "No entry names provided");
}

TEST_CASE("reduct::IBucket should remove a batch across entries", "[entry_api][1_18]") {
Expand All @@ -405,10 +420,10 @@ TEST_CASE("reduct::IBucket should remove a batch across entries", "[entry_api][1
REQUIRE(err == Error::kOk);
REQUIRE(errors.empty());

REQUIRE(bucket->Read("entry-a", ts, [](auto) { return true; }) ==
Error{.code = 404, .message = "No record with timestamp 0"});
REQUIRE(bucket->Read("entry-b", ts + us(1), [](auto) { return true; }) ==
Error{.code = 404, .message = "No record with timestamp 1"});
auto read_err_a = bucket->Read("entry-a", ts, [](auto) { return true; });
REQUIRE(read_err_a.code == 404);
auto read_err_b = bucket->Read("entry-b", ts + us(1), [](auto) { return true; });
REQUIRE(read_err_b.code == 404);
}

TEST_CASE("reduct::IBucket should query data with ext parameter", "[bucket_api][1_15]") {
Expand All @@ -418,7 +433,7 @@ TEST_CASE("reduct::IBucket should query data with ext parameter", "[bucket_api][

auto err = bucket->Query("entry-1", IBucket::Time{}, IBucket::Time::clock::now(), {.ext = R"({"test": {}})"},
[](auto record) { return true; });
REQUIRE(err.message.starts_with("Unknown extension"));
REQUIRE(err.code == 422);
}

TEST_CASE("reduct::IBucket should write batch of records", "[bucket_api][1_7]") {
Expand Down Expand Up @@ -480,7 +495,6 @@ TEST_CASE("reduct::IBucket should write batch of records with errors", "[bucket_
REQUIRE(http_error == Error::kOk);
REQUIRE(record_errors.size() == 1);
REQUIRE(record_errors[t].code == 409);
REQUIRE(record_errors[t].message == "A record with timestamp 0 already exists");
}

TEST_CASE("reduct::IBucket should update labels", "[bucket_api][1_11]") {
Expand Down Expand Up @@ -526,7 +540,6 @@ TEST_CASE("reduct::IBucket should update labels in barch and return errors", "[b
REQUIRE(http_error == Error::kOk);
REQUIRE(record_errors.size() == 1);
REQUIRE(record_errors[t + us(1)].code == 404);
REQUIRE(record_errors[t + us(1)].message == "No record with timestamp 1");
}

TEST_CASE("reduct::IBucket should remove a record", "[bucket_api][1_12]") {
Expand Down Expand Up @@ -560,13 +573,14 @@ TEST_CASE("reduct::IBucket should remove a batch of records", "[bucket_api][1_12
REQUIRE(record_errors.size() == 1);
REQUIRE(record_errors[IBucket::Time() + us(100)].code == 404);

REQUIRE(bucket->Read("entry-1", t, [](auto record) { return true; }) ==
Error{.code = 404, .message = "No record with timestamp 0"});
auto read_err_1 = bucket->Read("entry-1", t, [](auto record) { return true; });
REQUIRE(read_err_1.code == 404);

REQUIRE(bucket->Read("entry-1", t + us(1), [](auto record) {
auto read_err_2 = bucket->Read("entry-1", t + us(1), [](auto record) {
REQUIRE(record.size == 10);
return true;
}) == Error{.code = 404, .message = "No record with timestamp 1"});
});
REQUIRE(read_err_2.code == 404);
}

TEST_CASE("reduct::IBucket should remove records by query", "[bucket_api][1_15]") {
Expand Down Expand Up @@ -623,7 +637,7 @@ TEST_CASE("reduct::IBucket should remove records by query with when condition",
SECTION("strict") {
auto [removed_records, err] =
bucket->RemoveQuery("entry-1", t, t + us(3), {.when = R"({"&NOT_EXIST": {"$lt": 20}})", .strict = true});
REQUIRE(err == Error{.code = 404, .message = "Reference 'NOT_EXIST' not found"});
REQUIRE(err.code == 404);
}

SECTION("non-strict") {
Expand All @@ -642,8 +656,8 @@ TEST_CASE("reduct::IBucket should rename an entry", "[bucket_api][1_12]") {
REQUIRE(bucket->Write("entry-1", t, [](auto rec) { rec->WriteAll("some_data1"); }) == Error::kOk);
REQUIRE(bucket->RenameEntry("entry-1", "entry-new") == Error::kOk);

REQUIRE(bucket->Read("entry-1", t, [](auto record) { return true; }) ==
Error{.code = 404, .message = "Entry 'entry-1' not found in bucket 'test_bucket_3'"});
auto read_err = bucket->Read("entry-1", t, [](auto record) { return true; });
REQUIRE(read_err.code == 404);

REQUIRE(bucket->Read("entry-new", t, [](auto record) {
REQUIRE(record.ReadAll().result == "some_data1");
Expand Down
4 changes: 2 additions & 2 deletions tests/reduct/server_api_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ TEST_CASE("reduct::Client should return error", "[server_api]") {
auto client = IClient::Build("http://127.0.0.1:99999");
auto [info, err] = client->GetInfo();

REQUIRE(err == Error{.code = -1, .message = "Could not establish connection"});
REQUIRE(err.code == -1);
}

TEST_CASE("reduct::Client should return 404 if base path wrong") {
auto client = IClient::Build("http://127.0.0.1:8383/wrong_path");
auto [info, err] = client->GetInfo();
REQUIRE(err == Error{.code = 404, .message = "Not found"});
REQUIRE(err.code == 404);
}

TEST_CASE("reduct::Client should return current token name and permissions", "[server_api][token_api]") {
Expand Down
Loading