Public Access
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
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#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();
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "json_scan.h"
|
||||
|
||||
using namespace roro::release;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
struct Seen {
|
||||
std::vector<std::string> paths;
|
||||
std::map<std::string, std::string> values;
|
||||
std::map<std::string, bool> strings, cut;
|
||||
};
|
||||
|
||||
static Seen scan(const std::string& json, size_t piece, size_t max = 300, bool* failed = nullptr, bool* done = nullptr) {
|
||||
Seen seen;
|
||||
JsonScanner s(
|
||||
[&](const std::string& path, const std::string& value, bool isString, bool truncated) {
|
||||
seen.paths.push_back(path);
|
||||
seen.values[path] = value;
|
||||
seen.strings[path] = isString;
|
||||
seen.cut[path] = truncated;
|
||||
},
|
||||
max);
|
||||
for (size_t at = 0; at < json.size(); at += piece) s.feed(json.data() + at, std::min(piece, json.size() - at));
|
||||
if (failed) *failed = s.failed();
|
||||
if (done) *done = s.done();
|
||||
return seen;
|
||||
}
|
||||
|
||||
void test_paths_and_kinds() {
|
||||
std::string json = R"({"tag_name":"v0.10.0","draft":false,"size":1885712,"ratio":-1.5e3,"none":null,
|
||||
"assets":[{"name":"a.ota","size":12},{"name":"b.bin","size":3,"tags":[]},{"name":"c","nested":{"deep":[true,"x"]}}],
|
||||
"empty":{},"last":"end"})";
|
||||
for (size_t piece : {size_t(1), size_t(3), size_t(7), json.size()}) {
|
||||
bool failed, done;
|
||||
Seen s = scan(json, piece, 300, &failed, &done);
|
||||
TEST_ASSERT_FALSE(failed);
|
||||
TEST_ASSERT_TRUE(done);
|
||||
TEST_ASSERT_EQUAL_STRING("v0.10.0", s.values["tag_name"].c_str());
|
||||
TEST_ASSERT_TRUE(s.strings["tag_name"]);
|
||||
TEST_ASSERT_EQUAL_STRING("false", s.values["draft"].c_str());
|
||||
TEST_ASSERT_FALSE(s.strings["draft"]);
|
||||
TEST_ASSERT_EQUAL_STRING("1885712", s.values["size"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("-1.5e3", s.values["ratio"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("null", s.values["none"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("a.ota", s.values["assets[0].name"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("3", s.values["assets[1].size"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("true", s.values["assets[2].nested.deep[0]"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("x", s.values["assets[2].nested.deep[1]"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("end", s.values["last"].c_str());
|
||||
TEST_ASSERT_EQUAL_size_t(0, s.values.count("assets[1].tags")); // empty containers report nothing
|
||||
TEST_ASSERT_EQUAL_size_t(0, s.values.count("empty"));
|
||||
}
|
||||
}
|
||||
|
||||
void test_top_level_array_and_scalars() {
|
||||
Seen s = scan(R"([{"a":1},{"a":2},"x"])", 4);
|
||||
TEST_ASSERT_EQUAL_STRING("2", s.values["[1].a"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("x", s.values["[2]"].c_str());
|
||||
bool done;
|
||||
s = scan("42", 1, 300, nullptr, &done); // a number ends only when something follows it
|
||||
TEST_ASSERT_FALSE(done);
|
||||
s = scan("42 ", 1, 300, nullptr, &done);
|
||||
TEST_ASSERT_TRUE(done);
|
||||
TEST_ASSERT_EQUAL_STRING("42", s.values[""].c_str());
|
||||
}
|
||||
|
||||
void test_escapes_and_unicode() {
|
||||
std::string json = R"({"s":"line1\nline2\t\"q\" \\ \/ caf\u00e9 \u20ac \ud83d\ude00 end"})";
|
||||
for (size_t piece : {size_t(1), size_t(5), json.size()}) {
|
||||
Seen s = scan(json, piece);
|
||||
TEST_ASSERT_EQUAL_STRING("line1\nline2\t\"q\" \\ / caf\xC3\xA9 \xE2\x82\xAC \xF0\x9F\x98\x80 end", s.values["s"].c_str());
|
||||
}
|
||||
std::string key = R"({"a\"b":1})";
|
||||
TEST_ASSERT_EQUAL_STRING("1", scan(key, 2).values["a\"b"].c_str());
|
||||
}
|
||||
|
||||
void test_long_values_are_cut_on_a_character() {
|
||||
std::string json = "{\"body\":\"" + std::string(20, 'a') + "\xC3\xA9\xC3\xA9\"}"; // 24 bytes
|
||||
Seen s = scan(json, 3, 21); // room for 21: the é would be cut
|
||||
TEST_ASSERT_EQUAL_STRING(std::string(20, 'a').c_str(), s.values["body"].c_str());
|
||||
TEST_ASSERT_TRUE(s.cut["body"]);
|
||||
s = scan(json, 3, 22);
|
||||
TEST_ASSERT_EQUAL_size_t(22, s.values["body"].size());
|
||||
s = scan(json, 3, 100);
|
||||
TEST_ASSERT_FALSE(s.cut["body"]);
|
||||
// What follows a cut value is still read.
|
||||
s = scan(R"({"a":"0123456789","b":"ok"})", 2, 4);
|
||||
TEST_ASSERT_EQUAL_STRING("0123", s.values["a"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("ok", s.values["b"].c_str());
|
||||
}
|
||||
|
||||
void test_what_it_can_not_follow() {
|
||||
bool failed;
|
||||
scan("{\"a\" 1}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("{\"a\":1,,}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("[1,2}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("<html>not json</html>", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("{\"a\":\"\\u12G4\"}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
bool done;
|
||||
scan("{\"a\":[1,2", 2, 300, &failed, &done); // cut short: not an error, not done
|
||||
TEST_ASSERT_FALSE(failed);
|
||||
TEST_ASSERT_FALSE(done);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_paths_and_kinds);
|
||||
RUN_TEST(test_top_level_array_and_scalars);
|
||||
RUN_TEST(test_escapes_and_unicode);
|
||||
RUN_TEST(test_long_values_are_cut_on_a_character);
|
||||
RUN_TEST(test_what_it_can_not_follow);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":18,"tag_name":"v0.10.0","target_commitish":"","name":"roro9stack v0.10.0","body":"v0.10.0: the Notes App (plain text notes in /notes, an editor that saves by itself, up to 16 KB); e in the Storage App's text viewer; keys sent through the Debug Console are no longer swallowed now and then\n\n\n## Files\n\n- `roro9stack-v0.10.0.ota`: the signed Update File. Copy it to `/updates` on the SD card and install it from Settings > Firmware or the Storage App, or push it over Wi-Fi with `scripts/ota_push.py`.\n- `roro9stack-v0.10.0-factory.bin`: the whole flash image, for a first install over USB at offset 0.\n- `roro9stack-v0.10.0.elf.gz`: the symbols, to decode a crash report from this build.\n- `SHA256SUMS`: checksums of the three.\n\n## Changes since v0.9.0\n\n- F1 plan: Notes ships as v0.10.0\n- Notes: plain text notes on the SD card, with an editor that saves by itself (#19)\n- A key sent through the Debug Console could turn the screen \"off\" for a tick\n- F1 plan: the Notes design round (Q141-Q150)\n","url":"https://git.twis.la/api/v1/repos/twisla/roro9stack/releases/18","html_url":"https://git.twis.la/twisla/roro9stack/releases/tag/v0.10.0","tarball_url":"https://git.twis.la/twisla/roro9stack/archive/v0.10.0.tar.gz","zipball_url":"https://git.twis.la/twisla/roro9stack/archive/v0.10.0.zip","upload_url":"https://git.twis.la/api/v1/repos/twisla/roro9stack/releases/18/assets","draft":false,"prerelease":false,"created_at":"2026-10-06T08:11:31Z","published_at":"2026-10-06T08:11:31Z","author":{"id":1,"login":"twisla","login_name":"","source_id":0,"full_name":"Cl\u00e9ment Martin","email":"1+twisla@noreply.git.twis.la","avatar_url":"https://git.twis.la/avatars/db0ad25f6a366bc836e69290433c81ec","html_url":"https://git.twis.la/twisla","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2021-08-17T07:22:50Z","restricted":false,"active":false,"prohibit_login":false,"location":"Earth","website":"https://git.twis.la","description":"Owner of this nice gitea instance.","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"twisla"},"assets":[{"id":2,"name":"SHA256SUMS","size":278,"download_count":2,"created_at":"2026-10-06T10:24:35Z","uuid":"b72c1ea7-af35-4563-917a-9ab56a649219","browser_download_url":"https://git.twis.la/twisla/roro9stack/releases/download/v0.10.0/SHA256SUMS"},{"id":3,"name":"roro9stack-v0.10.0-factory.bin","size":1951088,"download_count":1,"created_at":"2026-10-06T10:24:35Z","uuid":"75163ae2-f495-4d5d-9883-ff773321c688","browser_download_url":"https://git.twis.la/twisla/roro9stack/releases/download/v0.10.0/roro9stack-v0.10.0-factory.bin"},{"id":4,"name":"roro9stack-v0.10.0.elf.gz","size":14345572,"download_count":0,"created_at":"2026-10-06T10:24:35Z","uuid":"8b04e7fb-fee0-46bd-8be4-3bd67a8f8804","browser_download_url":"https://git.twis.la/twisla/roro9stack/releases/download/v0.10.0/roro9stack-v0.10.0.elf.gz"},{"id":5,"name":"roro9stack-v0.10.0.ota","size":1885712,"download_count":2,"created_at":"2026-10-06T10:24:36Z","uuid":"4b827c6e-a57a-4436-b6e5-b8947e66d5ae","browser_download_url":"https://git.twis.la/twisla/roro9stack/releases/download/v0.10.0/roro9stack-v0.10.0.ota"}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,146 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "update_check.h"
|
||||
|
||||
using namespace roro::release;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static std::string file(const char* name) {
|
||||
std::string path = std::string("test/test_release/fixtures/") + name;
|
||||
FILE* f = std::fopen(path.c_str(), "rb");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(f, path.c_str());
|
||||
std::string s;
|
||||
char buf[4096];
|
||||
size_t n;
|
||||
while ((n = std::fread(buf, 1, sizeof buf, f)) > 0) s.append(buf, n);
|
||||
std::fclose(f);
|
||||
return s;
|
||||
}
|
||||
|
||||
static std::vector<Release> read(const std::string& json, size_t piece, size_t max = 10, bool* complete = nullptr) {
|
||||
ReleaseReader r(max);
|
||||
for (size_t at = 0; at < json.size(); at += piece) r.feed(json.data() + at, std::min(piece, json.size() - at));
|
||||
r.end();
|
||||
TEST_ASSERT_TRUE(r.ok());
|
||||
if (complete) *complete = r.complete();
|
||||
return r.releases();
|
||||
}
|
||||
|
||||
// The answers of git.twis.la, as they were on 2026-10-06.
|
||||
void test_latest_as_the_server_sends_it() {
|
||||
std::string json = file("latest.json");
|
||||
for (size_t piece : {size_t(1), size_t(17), size_t(1000), json.size()}) {
|
||||
bool complete;
|
||||
auto releases = read(json, piece, 10, &complete);
|
||||
TEST_ASSERT_TRUE(complete);
|
||||
TEST_ASSERT_EQUAL_size_t(1, releases.size());
|
||||
const Release& r = releases[0];
|
||||
TEST_ASSERT_EQUAL_STRING("v0.10.0", r.tag.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("2026-10-06", r.date().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("https://git.twis.la/twisla/roro9stack/releases/download/v0.10.0/roro9stack-v0.10.0.ota", r.otaUrl.c_str());
|
||||
TEST_ASSERT_EQUAL_UINT32(1885712, r.otaSize);
|
||||
TEST_ASSERT_TRUE(r.notes.rfind("v0.10.0: the Notes App", 0) == 0);
|
||||
TEST_ASSERT_TRUE(r.notes.find("\n\n") == std::string::npos); // the tag's message only
|
||||
TEST_ASSERT_TRUE(r.usable());
|
||||
TEST_ASSERT_TRUE(trustedAssetUrl(r.otaUrl));
|
||||
}
|
||||
}
|
||||
|
||||
void test_a_list() {
|
||||
std::string json = file("list3.json");
|
||||
for (size_t piece : {size_t(1), size_t(64), json.size()}) {
|
||||
auto releases = read(json, piece);
|
||||
TEST_ASSERT_EQUAL_size_t(3, releases.size());
|
||||
TEST_ASSERT_EQUAL_STRING("v0.10.0", releases[0].tag.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("v0.9.0", releases[1].tag.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("v0.8.1", releases[2].tag.c_str());
|
||||
for (auto& r : releases) {
|
||||
TEST_ASSERT_TRUE(r.usable());
|
||||
TEST_ASSERT_TRUE(r.otaUrl.find("/" + r.tag + "/roro9stack-" + r.tag + ".ota") != std::string::npos);
|
||||
TEST_ASSERT_TRUE(r.otaSize > 1000000);
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_size_t(2, read(json, 100, 2).size()); // no more than asked for
|
||||
}
|
||||
|
||||
void test_what_makes_a_release_usable() {
|
||||
auto releases = read(R"([
|
||||
{"tag_name":"v1.0.0","draft":true,"prerelease":false,"assets":[{"name":"roro9stack-v1.0.0.ota","size":5,"browser_download_url":"https://git.twis.la/twisla/roro9stack/releases/download/v1.0.0/roro9stack-v1.0.0.ota"}]},
|
||||
{"tag_name":"v0.9.0-rc1","draft":false,"prerelease":true,"assets":[{"name":"roro9stack-v0.9.0-rc1.ota","size":5,"browser_download_url":"https://git.twis.la/twisla/roro9stack/releases/download/v0.9.0-rc1/roro9stack-v0.9.0-rc1.ota"}]},
|
||||
{"tag_name":"v0.8.0","draft":false,"prerelease":false,"assets":[{"size":9,"browser_download_url":"https://git.twis.la/x/SHA256SUMS","name":"SHA256SUMS"}]},
|
||||
{"assets":[{"browser_download_url":"https://git.twis.la/twisla/roro9stack/releases/download/v0.7.0/roro9stack-v0.7.0.ota","size":7,"name":"roro9stack-v0.7.0.ota"},{"name":"x.bin","size":1,"browser_download_url":"https://git.twis.la/x.bin"}],"draft":false,"tag_name":"v0.7.0","prerelease":false}
|
||||
])", 5);
|
||||
TEST_ASSERT_EQUAL_size_t(4, releases.size());
|
||||
TEST_ASSERT_FALSE(releases[0].usable()); // a draft
|
||||
TEST_ASSERT_FALSE(releases[1].usable()); // a pre-release
|
||||
TEST_ASSERT_FALSE(releases[2].usable()); // nothing to install in it
|
||||
TEST_ASSERT_TRUE(releases[3].usable()); // the fields in any order
|
||||
TEST_ASSERT_EQUAL_UINT32(7, releases[3].otaSize);
|
||||
}
|
||||
|
||||
void test_notes() {
|
||||
TEST_ASSERT_EQUAL_STRING("one two", firstParagraph(" one two \n\nsecond", false).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("one\ntwo", firstParagraph("one\ntwo\r\n\r\nthree", false).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("only", firstParagraph("only", false).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("cut...", firstParagraph("cut", true).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("whole", firstParagraph("whole\n\nrest", true).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("", firstParagraph("\n\n", false).c_str());
|
||||
}
|
||||
|
||||
void test_which_releases_are_updates() {
|
||||
Release r;
|
||||
r.tag = "v0.11.0";
|
||||
r.otaUrl = "https://git.twis.la/twisla/roro9stack/releases/download/v0.11.0/roro9stack-v0.11.0.ota";
|
||||
TEST_ASSERT_TRUE(isNewer(r, "v0.10.0"));
|
||||
TEST_ASSERT_TRUE(isNewer(r, "v0.10.0-14-gabc1234-dirty+debug")); // between releases: the next one is still newer
|
||||
TEST_ASSERT_FALSE(isNewer(r, "v0.11.0"));
|
||||
TEST_ASSERT_FALSE(isNewer(r, "v0.11.0-3-gabc1234")); // a build after the release isn't older than it
|
||||
TEST_ASSERT_FALSE(isNewer(r, "v0.12.0"));
|
||||
TEST_ASSERT_FALSE(isNewer(r, "unknown")); // a build that doesn't know its version isn't offered anything
|
||||
r.tag = "v0.10.1";
|
||||
TEST_ASSERT_TRUE(isNewer(r, "v0.10.0"));
|
||||
r.tag = "v0.9.9";
|
||||
TEST_ASSERT_FALSE(isNewer(r, "v0.10.0"));
|
||||
r.tag = "v0.11.0";
|
||||
r.draft = true;
|
||||
TEST_ASSERT_FALSE(isNewer(r, "v0.10.0"));
|
||||
r.draft = false;
|
||||
|
||||
TEST_ASSERT_TRUE(shouldAnnounce(r, "v0.10.0", "", ""));
|
||||
TEST_ASSERT_FALSE(shouldAnnounce(r, "v0.10.0", "v0.11.0", "")); // it rolled back here before
|
||||
TEST_ASSERT_FALSE(shouldAnnounce(r, "v0.10.0", "", "v0.11.0")); // already said so
|
||||
TEST_ASSERT_TRUE(shouldAnnounce(r, "v0.10.0", "v0.10.5", "v0.10.9"));
|
||||
TEST_ASSERT_TRUE(isDebugBuild("v0.10.0-3-gabc+debug"));
|
||||
TEST_ASSERT_FALSE(isDebugBuild("v0.10.0"));
|
||||
}
|
||||
|
||||
void test_only_the_projects_downloads_are_fetched() {
|
||||
const char* good = "https://git.twis.la/twisla/roro9stack/releases/download/v0.11.0/roro9stack-v0.11.0.ota";
|
||||
TEST_ASSERT_TRUE(trustedAssetUrl(good));
|
||||
TEST_ASSERT_TRUE(trustedAssetUrl("https://GIT.twis.la/twisla/roro9stack/releases/download/v1/a.ota"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("http://git.twis.la/twisla/roro9stack/releases/download/v1/a.ota"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("https://git.twis.la:8443/twisla/roro9stack/releases/download/v1/a.ota"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("https://evil.example/twisla/roro9stack/releases/download/v1/a.ota"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("https://git.twis.la.evil.example/twisla/roro9stack/releases/download/v1/a.ota"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("https://git.twis.la@evil.example/twisla/roro9stack/releases/download/v1/a.ota"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("https://git.twis.la/other/repo/releases/download/v1/a.ota"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("https://git.twis.la/twisla/roro9stack/releases/download/../../../x"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl("https://git.twis.la/twisla/roro9stack/releases/download/v1/a.ota?x=1"));
|
||||
TEST_ASSERT_FALSE(trustedAssetUrl(""));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_latest_as_the_server_sends_it);
|
||||
RUN_TEST(test_a_list);
|
||||
RUN_TEST(test_what_makes_a_release_usable);
|
||||
RUN_TEST(test_notes);
|
||||
RUN_TEST(test_which_releases_are_updates);
|
||||
RUN_TEST(test_only_the_projects_downloads_are_fetched);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user