Files
roro9stack/test/test_http/test_http.cpp
T
twislaandClaude Sonnet 5.5 b0e8226943 Updates from Gitea, step 1: the model (host-tested)
A streaming JSON scanner (a 33 KB list of releases costs a few hundred
bytes), the release reader built on it, HTTP response heads and chunked
bodies, URLs, and the decisions: which release is an update, whether to
announce it, and which download URLs the device takes. Tested against the
real answers of git.twis.la.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 15:11:58 +02:00

122 lines
4.9 KiB
C++

#include <unity.h>
#include <string>
#include "http_head.h"
using namespace roro::release;
void setUp() {}
void tearDown() {}
void test_a_head_in_pieces() {
std::string response =
"HTTP/1.1 200 OK\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\n"
"X-Other: a: b\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
for (size_t piece : {size_t(1), size_t(9), response.size()}) {
HttpHeadParser p;
size_t used = 0;
for (size_t at = 0; at < response.size() && !p.complete(); at += piece)
used += p.feed(response.data() + at, std::min(piece, response.size() - at));
TEST_ASSERT_TRUE(p.complete());
TEST_ASSERT_FALSE(p.failed());
TEST_ASSERT_EQUAL_INT(200, p.head().status);
TEST_ASSERT_TRUE(p.head().chunked);
TEST_ASSERT_EQUAL_STRING("application/json;charset=utf-8", p.head().contentType.c_str());
TEST_ASSERT_EQUAL(std::string("HTTP/1.1 200 OK\r\nContent-Type: application/json;charset=utf-8\r\nTransfer-Encoding: chunked\r\nX-Other: a: b\r\n\r\n").size(), used);
}
}
void test_a_download_head() {
std::string head = "HTTP/1.1 200 OK\r\ncontent-length: 1885712\r\nCONTENT-DISPOSITION: inline; filename=a.ota\r\n\r\n\x01\x02";
HttpHeadParser p;
size_t used = p.feed(head.data(), head.size());
TEST_ASSERT_EQUAL_size_t(head.size() - 2, used); // the body isn't touched
TEST_ASSERT_EQUAL_INT(200, p.head().status);
TEST_ASSERT_EQUAL_INT(1885712, p.head().contentLength);
TEST_ASSERT_FALSE(p.head().chunked);
std::string redirect = "HTTP/1.1 302 Found\r\nLocation: https://elsewhere.example/x\r\n\r\n";
HttpHeadParser r;
r.feed(redirect.data(), redirect.size());
TEST_ASSERT_EQUAL_INT(302, r.head().status);
TEST_ASSERT_EQUAL_STRING("https://elsewhere.example/x", r.head().location.c_str());
}
void test_a_head_that_is_not_http() {
HttpHeadParser p;
std::string junk = "\x16\x03\x01 not http at all\r\n\r\n";
p.feed(junk.data(), junk.size());
TEST_ASSERT_TRUE(p.failed());
HttpHeadParser big;
std::string endless = "HTTP/1.1 200 OK\r\n" + std::string(5000, 'x');
big.feed(endless.data(), endless.size());
TEST_ASSERT_TRUE(big.failed());
HttpHeadParser odd;
std::string status = "HTTP/1.1 999 What\r\n\r\n";
odd.feed(status.data(), status.size());
TEST_ASSERT_TRUE(odd.failed());
}
static std::string unchunk(const std::string& in, size_t piece, bool* done = nullptr, bool* failed = nullptr) {
ChunkedDecoder d;
std::string out;
for (size_t at = 0; at < in.size(); at += piece) {
size_t n = std::min(piece, in.size() - at);
std::string buf(n, '\0');
size_t w = d.decode(reinterpret_cast<const uint8_t*>(in.data() + at), n, reinterpret_cast<uint8_t*>(&buf[0]));
out.append(buf, 0, w);
}
if (done) *done = d.done();
if (failed) *failed = d.failed();
return out;
}
void test_chunked_bodies() {
std::string body = "5\r\nhello\r\n6;ext=1\r\n, worl\r\n1\r\nd\r\n0\r\nTrailer: x\r\n\r\n";
for (size_t piece : {size_t(1), size_t(2), size_t(5), body.size()}) {
bool done, failed;
TEST_ASSERT_EQUAL_STRING("hello, world", unchunk(body, piece, &done, &failed).c_str());
TEST_ASSERT_TRUE(done);
TEST_ASSERT_FALSE(failed);
}
bool done;
TEST_ASSERT_EQUAL_STRING("abc12345", unchunk("3\r\nabc\r\nA\r\n12345", 4, &done).c_str()); // cut short: what came is given
TEST_ASSERT_FALSE(done);
std::string hex = "1F\r\n" + std::string(31, 'x') + "\r\n0\r\n\r\n";
TEST_ASSERT_EQUAL_size_t(31, unchunk(hex, 3).size());
bool failed;
unchunk("zz\r\n", 1, nullptr, &failed);
TEST_ASSERT_TRUE(failed);
unchunk("3\r\nabcXX", 1, nullptr, &failed); // no CRLF after the data
TEST_ASSERT_TRUE(failed);
unchunk("FFFFFFFFFFFFFFFFFFFF\r\n", 1, nullptr, &failed);
TEST_ASSERT_TRUE(failed);
}
void test_urls() {
Url u = parseUrl("https://git.twis.la/api/v1/repos/x?limit=10");
TEST_ASSERT_TRUE(u.ok);
TEST_ASSERT_TRUE(u.https);
TEST_ASSERT_EQUAL_STRING("git.twis.la", u.host.c_str());
TEST_ASSERT_EQUAL_STRING("/api/v1/repos/x?limit=10", u.path.c_str());
TEST_ASSERT_EQUAL_INT(443, u.port);
u = parseUrl("http://Example.COM:8080");
TEST_ASSERT_TRUE(u.ok);
TEST_ASSERT_EQUAL_STRING("example.com", u.host.c_str());
TEST_ASSERT_EQUAL_STRING("/", u.path.c_str());
TEST_ASSERT_EQUAL_INT(8080, u.port);
for (const char* bad : {"", "git.twis.la/x", "ftp://a/b", "https:///x", "https://u:p@host/x", "https://host:0/x", "https://ho st/x", "https://host/a b",
"https://host:99999/x", "https://ho_st/x"})
TEST_ASSERT_FALSE_MESSAGE(parseUrl(bad).ok, bad);
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_a_head_in_pieces);
RUN_TEST(test_a_download_head);
RUN_TEST(test_a_head_that_is_not_http);
RUN_TEST(test_chunked_bodies);
RUN_TEST(test_urls);
return UNITY_END();
}