/
smychkov
/
SStorage
Обзор
Документация
Войти
/
smychkov
/
SStorage
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/test_http.cpp
491 строка
18 KB
Андрей
feat: top N keys + scan stream (range/top/all) with pagination
14 июл 2026, 12:56
14 июл 2026, 12:56
56c6264
Код
Авторство
О чём код?
//============================================================================ // Интеграционные тесты HTTP API //============================================================================ // Поднимаем реальный сервер на случайном порту, делаем HTTP-запросы через // BSD-сокеты, проверяем ответы. // Контракт S3: docs/contracts/S3-http-scan-modes.md //============================================================================ #include "../src/server/http_handler.hpp" #include "../src/core/database.hpp" #include <arpa/inet.h> #include <cerrno> #include <chrono> #include <cstdio> #include <cstdlib> #include <filesystem> #include <iostream> #include <netinet/in.h> #include <string> #include <sys/socket.h> #include <sys/stat.h> #include <sys/time.h> #include <thread> #include <unistd.h> using namespace sstorage; static int g_passed = 0; static int g_failed = 0; #define CHECK(cond) do { \ if (cond) { ++g_passed; } \ else { ++g_failed; std::cerr << "FAIL: " #cond " at line " << __LINE__ << "\n"; } \ } while (0) //============================================================================ // Минимальный HTTP-клиент на сокетах //============================================================================ // Отправляет запрос, читает ответ до конца. Возвращает (status, body). //============================================================================ static std::pair<int, std::string> httpRequest(uint16_t port, const std::string& method, const std::string& path, const std::string& body = "") { int sock = ::socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return {-1, ""}; sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(port); ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); if (::connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) { ::close(sock); return {-1, ""}; } // Таймаут recv — чтобы не зависнуть при падении сервера struct timeval tv; tv.tv_sec = 5; tv.tv_usec = 0; ::setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); std::string req = method + " " + path + " HTTP/1.1\r\n"; req += "Host: 127.0.0.1\r\n"; req += "Content-Length: " + std::to_string(body.size()) + "\r\n"; req += "Connection: close\r\n\r\n"; req += body; ::send(sock, req.data(), req.size(), 0); std::string response; char buf[4096]; while (true) { ssize_t n = ::recv(sock, buf, sizeof(buf), 0); if (n <= 0) break; response.append(buf, n); } ::close(sock); // Разбор статуса int status = 0; auto spacePos = response.find(' '); if (spacePos != std::string::npos) { try { status = std::stoi(response.substr(spacePos + 1, 3)); } catch (...) {} } // Body после \r\n\r\n std::string respBody; auto bodyPos = response.find("\r\n\r\n"); if (bodyPos != std::string::npos) { respBody = response.substr(bodyPos + 4); } return {status, respBody}; } static std::string makeTempDir(const std::string& prefix) { std::string p = "/tmp/sstorage_http_" + prefix + "_" + std::to_string(::getpid()) + "_" + std::to_string(rand()); ::mkdir(p.c_str(), 0755); return p; } static void cleanupDir(const std::string& dir) { std::error_code ec; std::filesystem::remove_all(dir, ec); } // Извлечение nextCursor из JSON scan-ответа static std::string extractNextCursor(const std::string& body) { const std::string marker = "\"nextCursor\":\""; auto pos = body.find(marker); if (pos == std::string::npos) return "__NOT_FOUND__"; pos += marker.size(); auto end = body.find('"', pos); if (end == std::string::npos) return "__MALFORMED__"; return body.substr(pos, end - pos); } // Проверка: ключ присутствует в JSON items static bool hasKey(const std::string& body, const std::string& key) { return body.find("\"key\":\"" + key + "\"") != std::string::npos; } int main() { //======================================================================== // Phase 1: Базовые API-тесты (health, put/get, delete, stats, flush) //======================================================================== { auto dir = makeTempDir("basic"); Config cfg; cfg.setDataDirectory(dir); Database db(cfg); CHECK(db.open()); uint16_t port = 18080 + (rand() % 1000); HttpHandler handler(db, port); CHECK(handler.start()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 1. /health { auto [status, body] = httpRequest(port, "GET", "/health"); CHECK(status == 200); CHECK(body.find("\"status\":\"ok\"") != std::string::npos); } // 2. PUT /kv/key + GET /kv/key { auto [put_s, _] = httpRequest(port, "PUT", "/kv/hello", "world"); CHECK(put_s == 204); auto [get_s, body] = httpRequest(port, "GET", "/kv/hello"); CHECK(get_s == 200); CHECK(body == "world"); } // 3. GET несуществующего → 404 { auto [status, _] = httpRequest(port, "GET", "/kv/nonexistent"); CHECK(status == 404); } // 4. DELETE /kv/key { httpRequest(port, "PUT", "/kv/todelete", "v"); auto [del_s, _] = httpRequest(port, "DELETE", "/kv/todelete"); CHECK(del_s == 204); auto [get_s, __] = httpRequest(port, "GET", "/kv/todelete"); CHECK(get_s == 404); } // 6. GET /stats { auto [status, body] = httpRequest(port, "GET", "/stats"); CHECK(status == 200); CHECK(body.find("\"memtable_size\"") != std::string::npos); } // 7. POST /admin/flush { auto [status, _] = httpRequest(port, "POST", "/admin/flush"); CHECK(status == 204); } // 8. Неизвестный endpoint → 404 { auto [status, _] = httpRequest(port, "GET", "/unknown"); CHECK(status == 404); } // 9. Method not allowed { auto [status, _] = httpRequest(port, "PATCH", "/kv/x"); CHECK(status == 405); } handler.stop(); db.close(); cleanupDir(dir); } //======================================================================== // Phase 2: Scan §9.1–§9.5 (k00..k09, чистая БД) //======================================================================== { auto dir = makeTempDir("scan"); Config cfg; cfg.setDataDirectory(dir); Database db(cfg); CHECK(db.open()); uint16_t port = 19080 + (rand() % 1000); HttpHandler handler(db, port); CHECK(handler.start()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Наполнение: k00..k09 (10 ключей) for (int i = 0; i < 10; ++i) { char key[8], val[8]; std::snprintf(key, sizeof(key), "k%02d", i); std::snprintf(val, sizeof(val), "v%d", i); httpRequest(port, "PUT", std::string("/kv/") + key, val); } // === §9.1 Top mode === // top=2 → [(k00,v0),(k01,v1)], count=2, nextCursor="" { auto [status, body] = httpRequest(port, "GET", "/scan?top=2"); CHECK(status == 200); CHECK(body.find("\"items\"") != std::string::npos); CHECK(body.find("\"count\":2") != std::string::npos); CHECK(hasKey(body, "k00")); CHECK(hasKey(body, "k01")); CHECK(!hasKey(body, "k02")); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); CHECK(body.find("\"key\":\"k00\"") < body.find("\"key\":\"k01\"")); } // top=0 → [], count=0, nextCursor="" { auto [status, body] = httpRequest(port, "GET", "/scan?top=0"); CHECK(status == 200); CHECK(body.find("\"count\":0") != std::string::npos); CHECK(body.find("\"items\":[]") != std::string::npos); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } // top=100 → все 10 (100 > 10: конец данных), nextCursor="" { auto [status, body] = httpRequest(port, "GET", "/scan?top=100"); CHECK(status == 200); CHECK(body.find("\"count\":10") != std::string::npos); CHECK(hasKey(body, "k09")); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } // top=abc → N=0, [], count=0 { auto [status, body] = httpRequest(port, "GET", "/scan?top=abc"); CHECK(status == 200); CHECK(body.find("\"count\":0") != std::string::npos); CHECK(body.find("\"items\":[]") != std::string::npos); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } // top=-5 → N=0, [], count=0 { auto [status, body] = httpRequest(port, "GET", "/scan?top=-5"); CHECK(status == 200); CHECK(body.find("\"count\":0") != std::string::npos); CHECK(body.find("\"items\":[]") != std::string::npos); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } // top= (пустое) → N=0, [], count=0 { auto [status, body] = httpRequest(port, "GET", "/scan?top="); CHECK(status == 200); CHECK(body.find("\"count\":0") != std::string::npos); CHECK(body.find("\"items\":[]") != std::string::npos); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } // === §9.2 Пагинация «все» ?limit=4 (k00..k09, 3 страницы 4+4+2) === { std::string nc1, nc2; // Шаг 1: /scan?limit=4 → k00..k03, nextCursor непустой { auto [status, body] = httpRequest(port, "GET", "/scan?limit=4"); CHECK(status == 200); CHECK(body.find("\"count\":4") != std::string::npos); CHECK(hasKey(body, "k00")); CHECK(hasKey(body, "k03")); CHECK(!hasKey(body, "k04")); nc1 = extractNextCursor(body); CHECK(!nc1.empty()); CHECK(nc1 == "6b3033"); } // Шаг 2: cursor → k04..k07, nextCursor непустой { auto [status, body] = httpRequest(port, "GET", "/scan?limit=4&cursor=" + nc1); CHECK(status == 200); CHECK(body.find("\"count\":4") != std::string::npos); CHECK(hasKey(body, "k04")); CHECK(hasKey(body, "k07")); CHECK(!hasKey(body, "k03")); nc2 = extractNextCursor(body); CHECK(!nc2.empty()); } // Шаг 3: cursor → k08,k09, nextCursor="" { auto [status, body] = httpRequest(port, "GET", "/scan?limit=4&cursor=" + nc2); CHECK(status == 200); CHECK(body.find("\"count\":2") != std::string::npos); CHECK(hasKey(body, "k08")); CHECK(hasKey(body, "k09")); CHECK(!hasKey(body, "k07")); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } } // === §9.3 Дефолтная страница /scan (10 < 50 → одна страница) === { auto [status, body] = httpRequest(port, "GET", "/scan"); CHECK(status == 200); CHECK(body.find("\"count\":10") != std::string::npos); CHECK(hasKey(body, "k00")); CHECK(hasKey(body, "k09")); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } // === §9.4 Диапазон (from/to + курсор) === { // from=k02&to=k05 → k02..k05, count=4, nextCursor="" { auto [status, body] = httpRequest(port, "GET", "/scan?from=k02&to=k05"); CHECK(status == 200); CHECK(body.find("\"count\":4") != std::string::npos); CHECK(hasKey(body, "k02")); CHECK(hasKey(body, "k05")); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } std::string nc; // from=k02&to=k05&limit=2 → k02,k03, nextCursor непустой { auto [status, body] = httpRequest(port, "GET", "/scan?from=k02&to=k05&limit=2"); CHECK(status == 200); CHECK(body.find("\"count\":2") != std::string::npos); CHECK(hasKey(body, "k02")); CHECK(hasKey(body, "k03")); nc = extractNextCursor(body); CHECK(!nc.empty()); } // cursor → k04,k05, nextCursor="" { auto [status, body] = httpRequest(port, "GET", "/scan?from=k02&to=k05&limit=2&cursor=" + nc); CHECK(status == 200); CHECK(body.find("\"count\":2") != std::string::npos); CHECK(hasKey(body, "k04")); CHECK(hasKey(body, "k05")); CHECK(!hasKey(body, "k03")); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } } // === §9.5 Пустой результат === { // from=z0&to=z9 → items=[], count=0 { auto [status, body] = httpRequest(port, "GET", "/scan?from=z0&to=z9"); CHECK(status == 200); CHECK(body.find("\"count\":0") != std::string::npos); CHECK(body.find("\"items\":[]") != std::string::npos); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } // top=0 → items=[], count=0 { auto [status, body] = httpRequest(port, "GET", "/scan?top=0"); CHECK(status == 200); CHECK(body.find("\"count\":0") != std::string::npos); CHECK(body.find("\"items\":[]") != std::string::npos); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } } // 5. /scan — проверка нового JSON-формата (§5: items/count/nextCursor) { httpRequest(port, "PUT", "/kv/a", "1"); httpRequest(port, "PUT", "/kv/b", "2"); httpRequest(port, "PUT", "/kv/c", "3"); auto [status, body] = httpRequest(port, "GET", "/scan?from=a&to=c"); CHECK(status == 200); CHECK(body.find("\"count\":") != std::string::npos); CHECK(body.find("\"items\"") != std::string::npos); CHECK(body.find("\"nextCursor\"") != std::string::npos); CHECK(body.find("\"rows\"") == std::string::npos); } handler.stop(); db.close(); cleanupDir(dir); } //======================================================================== // Phase 3: §9.1a Пагинация top (200 ключей, отдельная БД) //======================================================================== { auto dir = makeTempDir("scan200"); Config cfg; cfg.setDataDirectory(dir); Database db(cfg); CHECK(db.open()); uint16_t port = 20080 + (rand() % 1000); HttpHandler handler(db, port); CHECK(handler.start()); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Наполнение: k000..k199 (200 ключей, фикс. ширина 3 цифры) for (int i = 0; i < 200; ++i) { char key[8], val[16]; std::snprintf(key, sizeof(key), "k%03d", i); std::snprintf(val, sizeof(val), "v%d", i); httpRequest(port, "PUT", std::string("/kv/") + key, val); } std::string nc1, nc2; // Шаг 1: /scan?top=120 → k000..k049 (50), nextCursor непустой { auto [status, body] = httpRequest(port, "GET", "/scan?top=120"); CHECK(status == 200); CHECK(body.find("\"count\":50") != std::string::npos); CHECK(hasKey(body, "k000")); CHECK(hasKey(body, "k049")); nc1 = extractNextCursor(body); CHECK(!nc1.empty()); } // Шаг 2: cursor → k050..k099 (50), nextCursor непустой { auto [status, body] = httpRequest(port, "GET", "/scan?top=120&cursor=" + nc1); CHECK(status == 200); CHECK(body.find("\"count\":50") != std::string::npos); CHECK(hasKey(body, "k050")); CHECK(hasKey(body, "k099")); CHECK(!hasKey(body, "k049")); nc2 = extractNextCursor(body); CHECK(!nc2.empty()); } // Шаг 3: cursor → k100..k119 (20), nextCursor="" (N=120 достигнут) { auto [status, body] = httpRequest(port, "GET", "/scan?top=120&cursor=" + nc2); CHECK(status == 200); CHECK(body.find("\"count\":20") != std::string::npos); CHECK(hasKey(body, "k100")); CHECK(hasKey(body, "k119")); CHECK(!hasKey(body, "k099")); CHECK(body.find("\"nextCursor\":\"\"") != std::string::npos); } handler.stop(); db.close(); cleanupDir(dir); } std::cout << "test_http: passed=" << g_passed << " failed=" << g_failed << "\n"; return g_failed == 0 ? 0 : 1; }