/** @file Native unit tests for ReleaseUpdaterCore (version + release/asset parsing). */ #include #include "ReleaseUpdaterCore.hpp" #include static void test_version_parse_basic(void) { RuVersion v; TEST_ASSERT_TRUE(ruVersionParse("v1.2.3", v)); TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(3, v.patch); TEST_ASSERT_TRUE(ruVersionParse("1.2.3", v)); TEST_ASSERT_EQUAL_INT(3, v.patch); TEST_ASSERT_TRUE(ruVersionParse("v10.0.1", v)); TEST_ASSERT_EQUAL_INT(10, v.major); } static void test_version_parse_short(void) { RuVersion v; TEST_ASSERT_TRUE(ruVersionParse("1.2", v)); TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(0, v.patch); TEST_ASSERT_TRUE(ruVersionParse("v1", v)); TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(0, v.minor); TEST_ASSERT_EQUAL_INT(0, v.patch); } static void test_version_parse_reject(void) { RuVersion v; TEST_ASSERT_FALSE(ruVersionParse("", v)); TEST_ASSERT_FALSE(ruVersionParse("x", v)); TEST_ASSERT_FALSE(ruVersionParse("1.x.0", v)); TEST_ASSERT_FALSE(ruVersionParse("v.1.2", v)); } static void test_version_parse_suffix_tolerated(void) { RuVersion v; // git-describe style build suffix after patch is ignored. TEST_ASSERT_TRUE(ruVersionParse("v1.2.3-5-gabcdef", v)); TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(3, v.patch); // dotted trailing content after patch is also ignored (treated as build suffix). TEST_ASSERT_TRUE(ruVersionParse("1.2.3.99", v)); TEST_ASSERT_EQUAL_INT(1, v.major); TEST_ASSERT_EQUAL_INT(2, v.minor); TEST_ASSERT_EQUAL_INT(3, v.patch); } static void test_version_compare(void) { RuVersion a{1,2,3}, b{1,2,3}; TEST_ASSERT_EQUAL_INT(0, ruVersionCompare(a, b)); TEST_ASSERT_EQUAL_INT(1, ruVersionCompare(RuVersion{1,3,0}, RuVersion{1,2,9})); TEST_ASSERT_EQUAL_INT(-1, ruVersionCompare(RuVersion{1,2,3}, RuVersion{2,0,0})); TEST_ASSERT_EQUAL_INT(1, ruVersionCompare(RuVersion{1,2,4}, RuVersion{1,2,3})); } // Minimal GitHub/Gitea "releases/latest" shape (both share tag_name + assets[].{name,browser_download_url}). static const char* SAMPLE_JSON = "{\"tag_name\":\"v1.4.0\",\"assets\":[" "{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/sc.bin\"}," "{\"name\":\"DIYStickyNotes-JC-jc3248w535-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/jc.bin\"}" "]}"; static void test_parse_selects_sc01(void) { RuRelease r; TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON, "sc01plus", r)); TEST_ASSERT_EQUAL_STRING("v1.4.0", r.tag); TEST_ASSERT_EQUAL_INT(1, r.version.major); TEST_ASSERT_EQUAL_INT(4, r.version.minor); TEST_ASSERT_EQUAL_STRING("https://h/a/sc.bin", r.assetUrl); } static void test_parse_selects_jc_not_sc(void) { RuRelease r; // jc token must NOT cross-match the sc01 asset, and sc01 token must NOT match the jc asset. TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON, "jc3248w535", r)); TEST_ASSERT_EQUAL_STRING("https://h/a/jc.bin", r.assetUrl); } static void test_parse_no_matching_asset(void) { RuRelease r; TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON, "nosuchboard", r)); // parse ok... TEST_ASSERT_EQUAL_STRING("", r.assetUrl); // ...but no asset } // A same-token asset with a non-.bin extension listed BEFORE the real firmware must be skipped // (locks the `&& ruEndsWithBin(name)` filter against a regression). static const char* SAMPLE_JSON_NONBIN_FIRST = "{\"tag_name\":\"v1.4.0\",\"assets\":[" "{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.sha256\",\"browser_download_url\":\"https://h/a/sc.sha256\"}," "{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/sc.bin\"}" "]}"; static void test_parse_skips_non_bin_asset(void) { RuRelease r; TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON_NONBIN_FIRST, "sc01plus", r)); TEST_ASSERT_EQUAL_STRING("https://h/a/sc.bin", r.assetUrl); // .sha256 skipped, real .bin chosen } // A board-token ".bin" asset that is NOT the app image (missing the "-firmware-" segment, e.g. a // LittleFS/factory/combined image) listed BEFORE the real firmware asset must be skipped (locks the // "{token}-firmware-" needle against a regression that could flash a non-app binary to the app // partition and brick the device). static const char* SAMPLE_JSON_NONFIRMWARE_FIRST = "{\"tag_name\":\"v1.4.0\",\"assets\":[" "{\"name\":\"DIYStickyNotes-sc01plus-littlefs-1.4.0.bin\",\"browser_download_url\":\"https://h/a/lfs.bin\"}," "{\"name\":\"DIYStickyNotes-sc01plus-firmware-1.4.0.bin\",\"browser_download_url\":\"https://h/a/sc.bin\"}" "]}"; static void test_parse_skips_non_firmware_bin_asset(void) { RuRelease r; TEST_ASSERT_TRUE(ruParseLatestRelease(SAMPLE_JSON_NONFIRMWARE_FIRST, "sc01plus", r)); TEST_ASSERT_EQUAL_STRING("https://h/a/sc.bin", r.assetUrl); // littlefs .bin skipped, real firmware chosen } static void test_parse_malformed(void) { RuRelease r; TEST_ASSERT_FALSE(ruParseLatestRelease("not json", "sc01plus", r)); TEST_ASSERT_FALSE(ruParseLatestRelease("{\"assets\":[]}", "sc01plus", r)); // missing tag_name TEST_ASSERT_FALSE(ruParseLatestRelease("{\"tag_name\":\"\",\"assets\":[]}", "sc01plus", r)); // empty tag_name } int main(int, char**) { UNITY_BEGIN(); RUN_TEST(test_version_parse_basic); RUN_TEST(test_version_parse_short); RUN_TEST(test_version_parse_reject); RUN_TEST(test_version_parse_suffix_tolerated); RUN_TEST(test_version_compare); RUN_TEST(test_parse_selects_sc01); RUN_TEST(test_parse_selects_jc_not_sc); RUN_TEST(test_parse_no_matching_asset); RUN_TEST(test_parse_skips_non_bin_asset); RUN_TEST(test_parse_skips_non_firmware_bin_asset); RUN_TEST(test_parse_malformed); return UNITY_END(); }