Files
roro9stack/test/test_http/test_http.cpp
T
twislaandClaude Opus 5.5 468dc0dc0d Updates: when an answer is the rest of the same file, and how long to try
For carrying on a download that broke (issue #54): the head's ETag and
Content-Range are read, a 206 is checked to be the rest of the same
file, and a policy says how many tries a download is worth. Host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
2026-10-11 18:26:39 +02:00

214 lines
9.0 KiB
C++

#include <unity.h>
#include <string>
#include "http_head.h"
#include "resume.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_part_of_a_file() {
std::string head = "HTTP/1.1 206 Partial Content\r\nContent-Range: bytes 1000-2510383/2510384\r\n"
"ETag: \"ac948b3f\"\r\nContent-Length: 2509384\r\n\r\n";
HttpHeadParser p;
p.feed(head.data(), head.size());
TEST_ASSERT_EQUAL_INT(206, p.head().status);
TEST_ASSERT_EQUAL_INT(1000, p.head().rangeFrom);
TEST_ASSERT_EQUAL_INT(2510383, p.head().rangeTo);
TEST_ASSERT_EQUAL_INT(2510384, p.head().rangeTotal);
TEST_ASSERT_EQUAL_STRING("\"ac948b3f\"", p.head().etag.c_str());
// A range that makes no sense is no range.
for (const char* bad : {"bytes */2510384", "bytes 5-2/10", "bytes 0-10/10", "items 0-1/2", ""}) {
std::string h = std::string("HTTP/1.1 206 Partial Content\r\nContent-Range: ") + bad + "\r\n\r\n";
HttpHeadParser q;
q.feed(h.data(), h.size());
TEST_ASSERT_EQUAL_INT(-1, q.head().rangeFrom);
}
}
static HttpHead part(long from, long total, const char* etag = "\"abc\"") {
HttpHead h;
h.status = 206;
h.rangeFrom = from;
h.rangeTo = total - 1;
h.rangeTotal = total;
h.contentLength = total - from;
h.etag = etag;
return h;
}
void test_the_rest_of_the_same_file_is_taken() {
TEST_ASSERT_EQUAL_STRING("", resumeRefusal(part(1000, 5000), 1000, 5000, "\"abc\"").c_str());
TEST_ASSERT_EQUAL_STRING("", resumeRefusal(part(1000, 5000, ""), 1000, 5000, "\"abc\"").c_str()); // no ETag this time
TEST_ASSERT_EQUAL_STRING("", resumeRefusal(part(1000, 5000), 1000, 5000, "").c_str()); // none the first time
}
void test_anything_else_is_refused() {
HttpHead whole;
whole.status = 200; // If-Range did not match: the file is another one now
whole.contentLength = 5000;
TEST_ASSERT_EQUAL_STRING("The file changed on the server", resumeRefusal(whole, 1000, 5000, "\"abc\"").c_str());
HttpHead gone;
gone.status = 404;
TEST_ASSERT_EQUAL_STRING("The server answered 404", resumeRefusal(gone, 1000, 5000, "").c_str());
TEST_ASSERT_FALSE(resumeRefusal(part(0, 5000), 1000, 5000, "\"abc\"").empty()); // from the start
TEST_ASSERT_FALSE(resumeRefusal(part(1000, 6000), 1000, 5000, "\"abc\"").empty()); // another size
TEST_ASSERT_FALSE(resumeRefusal(part(1000, 5000, "\"xyz\""), 1000, 5000, "\"abc\"").empty());
HttpHead shortPart = part(1000, 5000);
shortPart.rangeTo = 2000; // not up to the end
TEST_ASSERT_FALSE(resumeRefusal(shortPart, 1000, 5000, "\"abc\"").empty());
HttpHead chunked = part(1000, 5000);
chunked.chunked = true;
TEST_ASSERT_FALSE(resumeRefusal(chunked, 1000, 5000, "\"abc\"").empty());
}
void test_only_a_strong_etag_goes_in_if_range() {
TEST_ASSERT_EQUAL_STRING("\"abc\"", ifRangeFor("\"abc\"").c_str());
TEST_ASSERT_EQUAL_STRING("", ifRangeFor("W/\"abc\"").c_str());
TEST_ASSERT_EQUAL_STRING("", ifRangeFor("").c_str());
}
void test_how_long_a_download_is_tried() {
ResumePolicy poor; // a poor link: each try brings some more
for (int i = 0; i < ResumePolicy::kMaxResumes; i++) TEST_ASSERT_TRUE(poor.again(1000 * (i + 1)));
TEST_ASSERT_FALSE(poor.again(1000000));
TEST_ASSERT_EQUAL_INT(ResumePolicy::kMaxResumes, poor.resumes());
ResumePolicy dead; // the link is gone: nothing more comes
TEST_ASSERT_TRUE(dead.again(5000));
TEST_ASSERT_TRUE(dead.again(5000));
TEST_ASSERT_TRUE(dead.again(5000));
TEST_ASSERT_FALSE(dead.again(5000));
ResumePolicy fromNothing; // not even a first byte
TEST_ASSERT_TRUE(fromNothing.again(0));
TEST_ASSERT_TRUE(fromNothing.again(0));
TEST_ASSERT_FALSE(fromNothing.again(0));
ResumePolicy recovering; // stuck twice, then bytes again: the count starts over
recovering.again(5000);
recovering.again(5000);
recovering.again(5000);
TEST_ASSERT_TRUE(recovering.again(9000));
TEST_ASSERT_TRUE(recovering.again(9000));
}
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_part_of_a_file);
RUN_TEST(test_the_rest_of_the_same_file_is_taken);
RUN_TEST(test_anything_else_is_refused);
RUN_TEST(test_only_a_strong_etag_goes_in_if_range);
RUN_TEST(test_how_long_a_download_is_tried);
RUN_TEST(test_a_head_that_is_not_http);
RUN_TEST(test_chunked_bodies);
RUN_TEST(test_urls);
return UNITY_END();
}