Skip to content

Commit cac9fc2

Browse files
committed
Add some more testing
1 parent 2dd6030 commit cac9fc2

8 files changed

Lines changed: 333 additions & 62 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ jobs:
2222
run: |
2323
cd tests/build
2424
make -j$(nproc)
25-
./coffeeshop_tests
25+
./cupstore_tests
File renamed without changes.

build_clean_move.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# change these please, i only hardcoded them for me personally
5+
REPO=~/Repositories/modmanager
6+
SD_PATH=/run/media/tim/HOMIE/wiiu/apps/coffeeshop
7+
BUILD_DIR=$REPO/build
8+
9+
echo "=== CLEAN BUILD DEPLOY ==="
10+
11+
if [ ! -d "$SD_PATH" ]; then
12+
echo "ERROR: SD card not mounted at $SD_PATH"
13+
exit 1
14+
fi
15+
16+
echo "Deleting old logs on SD..."
17+
rm -f "$SD_PATH/app.log"
18+
rm -f "$SD_PATH/early.log"
19+
20+
echo "Nuking build dir..."
21+
rm -rf "$BUILD_DIR"
22+
mkdir -p "$BUILD_DIR"
23+
24+
echo "Running CMake..."
25+
cd "$BUILD_DIR"
26+
/opt/devkitpro/portlibs/wiiu/bin/powerpc-eabi-cmake .. -DBUILD_MODE=hw
27+
28+
echo "Building..."
29+
make -j$(nproc)
30+
31+
echo "Deploying to SD..."
32+
cp wiiu_mod_store.wuhb "$SD_PATH/"
33+
34+
echo "=== DONE ==="

src/net/RepoManager.cpp

Lines changed: 70 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -122,64 +122,81 @@ void RepoManager::fetch(const std::string& url) {
122122
}
123123

124124
std::optional<Game> RepoManager::parseGameFromJson(const std::string& json) {
125-
auto jg = nlohmann::json::parse(json);
126-
if (!jg.contains("name") || (!jg.contains("titleIds") && !jg.contains("title_ids")) || !jg.contains("mods")) {
127-
LOG_WARN("RepoManager: skipping game - missing required fields");
128-
return std::nullopt;
129-
}
130-
131-
Game game;
132-
game.name = jg["name"].get<std::string>();
133-
game.icon = jg.value("icon", "");
134-
auto tids = jg.contains("titleIds") ? jg["titleIds"] : jg["title_ids"];
135-
for (auto& tid : tids) game.titleIds.push_back(tid.get<std::string>());
136-
137-
for (auto& jm : jg["mods"]) {
138-
if (!jm.contains("id") || !jm.contains("name") ||
139-
!jm.contains("version") || !jm.contains("download")) {
140-
LOG_WARN("RepoManager: skipping mod - missing required fields");
141-
continue;
125+
try {
126+
auto jg = nlohmann::json::parse(json);
127+
if (!jg.contains("name") || (!jg.contains("titleIds") && !jg.contains("title_ids")) || !jg.contains("mods")) {
128+
LOG_WARN("RepoManager: skipping game - missing required fields");
129+
return std::nullopt;
142130
}
143-
Mod mod;
144-
mod.id = jm["id"].get<std::string>();
145-
bool validId = !mod.id.empty() && std::all_of(mod.id.begin(), mod.id.end(),
146-
[](char c){ return isalnum(c) || c == '-' || c == '_'; });
147-
if (!validId) { LOG_WARN("RepoManager: skipping mod with invalid id: %s", mod.id.c_str()); continue; }
148-
149-
mod.name = jm["name"].get<std::string>();
150-
mod.version = jm["version"].get<std::string>();
151-
mod.download = jm["download"].get<std::string>();
152-
mod.author = jm.value("author", "Unknown");
153-
mod.description = jm.value("description", "");
154-
mod.type = jm.value("type", "mod");
155-
mod.thumbnail = jm.value("thumbnail", "");
156-
157-
if (jm.contains("includes") && jm["includes"].is_array())
158-
for (auto& s : jm["includes"]) mod.includes.push_back(s.get<std::string>());
159-
if (jm.contains("screenshots") && jm["screenshots"].is_array())
160-
for (auto& s : jm["screenshots"]) mod.screenshots.push_back(s.get<std::string>());
161-
162-
mod.releaseDate = jm.value("releaseDate", "");
163-
mod.changelog = jm.value("changelog", "");
164-
mod.license = jm.value("license", "");
165-
mod.fileSize = jm.value("fileSize", uint64_t(0));
166-
167-
if (jm.contains("requirements") && jm["requirements"].is_array())
168-
for (auto& s : jm["requirements"]) mod.requirements.push_back(s.get<std::string>());
169-
if (jm.contains("tags") && jm["tags"].is_array())
170-
for (auto& s : jm["tags"]) mod.tags.push_back(s.get<std::string>());
171-
172-
if (!RepoManager::validateUrl(mod.download)) {
173-
LOG_WARN("RepoManager: skipping mod '%s' - invalid download URL", mod.id.c_str());
174-
continue;
131+
Game game;
132+
game.name = jg["name"].get<std::string>();
133+
game.icon = jg.value("icon", "");
134+
135+
// titleIds validation
136+
auto tids = jg.contains("titleIds") ? jg["titleIds"] : jg["title_ids"];
137+
if (!tids.is_array() || tids.empty()) {
138+
LOG_WARN("RepoManager: skipping game - titleIds must be non-empty array");
139+
return std::nullopt;
175140
}
176-
game.mods.push_back(std::move(mod));
141+
for (auto& tid : tids) game.titleIds.push_back(tid.get<std::string>());
142+
143+
// mods validation
144+
if (!jg["mods"].is_array() || jg["mods"].empty()) {
145+
LOG_WARN("RepoManager: skipping game - mods must be non-empty array");
146+
return std::nullopt;
147+
}
148+
149+
for (auto& jm : jg["mods"]) {
150+
if (!jm.contains("id") || !jm.contains("name") ||
151+
!jm.contains("version") || !jm.contains("download")) {
152+
LOG_WARN("RepoManager: skipping mod - missing required fields");
153+
continue;
154+
}
155+
Mod mod;
156+
mod.id = jm["id"].get<std::string>();
157+
bool validId = !mod.id.empty() && std::all_of(mod.id.begin(), mod.id.end(),
158+
[](char c){ return isalnum(c) || c == '-' || c == '_'; });
159+
if (!validId) { LOG_WARN("RepoManager: skipping mod with invalid id: %s", mod.id.c_str()); continue; }
160+
mod.name = jm["name"].get<std::string>();
161+
mod.version = jm["version"].get<std::string>();
162+
mod.download = jm["download"].get<std::string>();
163+
mod.author = jm.value("author", "Unknown");
164+
mod.description = jm.value("description", "");
165+
mod.type = jm.value("type", "mod");
166+
mod.thumbnail = jm.value("thumbnail", "");
167+
if (jm.contains("includes") && jm["includes"].is_array())
168+
for (auto& s : jm["includes"]) mod.includes.push_back(s.get<std::string>());
169+
if (jm.contains("screenshots") && jm["screenshots"].is_array())
170+
for (auto& s : jm["screenshots"]) mod.screenshots.push_back(s.get<std::string>());
171+
mod.releaseDate = jm.value("releaseDate", "");
172+
mod.changelog = jm.value("changelog", "");
173+
mod.license = jm.value("license", "");
174+
175+
// fileSize with type check and bounds validation
176+
if (jm.contains("fileSize") && jm["fileSize"].is_number()) {
177+
int64_t size = jm["fileSize"].get<int64_t>();
178+
mod.fileSize = (size > 0) ? static_cast<uint64_t>(size) : 0;
179+
}
180+
181+
if (jm.contains("requirements") && jm["requirements"].is_array())
182+
for (auto& s : jm["requirements"]) mod.requirements.push_back(s.get<std::string>());
183+
if (jm.contains("tags") && jm["tags"].is_array())
184+
for (auto& s : jm["tags"]) mod.tags.push_back(s.get<std::string>());
185+
if (!RepoManager::validateUrl(mod.download)) {
186+
LOG_WARN("RepoManager: skipping mod '%s' - invalid download URL", mod.id.c_str());
187+
continue;
188+
}
189+
game.mods.push_back(std::move(mod));
190+
}
191+
if (game.mods.empty()) return std::nullopt;
192+
return game;
193+
} catch (const nlohmann::json::exception& e) {
194+
LOG_WARN("RepoManager: JSON parse error: %s", e.what());
195+
return std::nullopt;
177196
}
178-
179-
if (game.mods.empty()) return std::nullopt;
180-
return game;
181197
}
182198

199+
183200
void RepoManager::parseGame(const std::string& json) {
184201
try {
185202
auto game = parseGameFromJson(json);

tests/CMakeLists.txt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,38 @@ cmake_minimum_required(VERSION 3.14)
22
project(cupstore_tests CXX)
33
set(CMAKE_CXX_STANDARD 17)
44
set(CMAKE_CXX_STANDARD_REQUIRED ON)
5-
65
include(FetchContent)
76
FetchContent_Declare(
87
Catch2
98
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
109
GIT_TAG v3.5.2
1110
)
1211
FetchContent_MakeAvailable(Catch2)
13-
1412
# Source files under test (pure logic only - no WUT/SDL/curl)
1513
set(SRC ${CMAKE_SOURCE_DIR}/../src)
1614
set(LIBS ${CMAKE_SOURCE_DIR}/../libs)
17-
1815
add_executable(cupstore_tests
1916
test_conflict_checker.cpp
2017
test_repo_manager.cpp
2118
test_install_checker.cpp
19+
test_url_resolver.cpp
20+
test_mod_id_validation.cpp
2221
stubs.cpp
23-
2422
${SRC}/mods/ConflictChecker.cpp
2523
${SRC}/net/RepoManager.cpp
2624
${SRC}/mods/InstallChecker.cpp
2725
${SRC}/mods/InstalledScanner.cpp
2826
)
29-
3027
target_include_directories(cupstore_tests PRIVATE
3128
${SRC}
3229
${LIBS}
3330
)
34-
3531
target_link_libraries(cupstore_tests PRIVATE Catch2::Catch2WithMain)
36-
3732
# Stub out filesystem paths for tests
3833
target_compile_definitions(cupstore_tests PRIVATE
3934
CUPSTORE_TESTS=1
4035
BUILD_HW=0
4136
)
42-
4337
include(CTest)
4438
include(Catch)
4539
catch_discover_tests(cupstore_tests)

tests/test_mod_id_validation.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <algorithm>
3+
#include <string>
4+
5+
// Copy the validation logic for testing
6+
static bool isValidModId(const std::string& id) {
7+
if (id.empty()) return false;
8+
return std::all_of(id.begin(), id.end(),
9+
[](char c){ return isalnum(c) || c == '-' || c == '_'; });
10+
}
11+
12+
TEST_CASE("Mod ID - valid alphanumeric", "[modid]") {
13+
REQUIRE(isValidModId("mymod123"));
14+
REQUIRE(isValidModId("UPPERCASE"));
15+
REQUIRE(isValidModId("123numbers"));
16+
}
17+
18+
TEST_CASE("Mod ID - valid with dashes", "[modid]") {
19+
REQUIRE(isValidModId("my-mod"));
20+
REQUIRE(isValidModId("some-long-mod-name"));
21+
REQUIRE(isValidModId("-leading-dash"));
22+
REQUIRE(isValidModId("trailing-dash-"));
23+
}
24+
25+
TEST_CASE("Mod ID - valid with underscores", "[modid]") {
26+
REQUIRE(isValidModId("my_mod"));
27+
REQUIRE(isValidModId("some_long_mod_name"));
28+
REQUIRE(isValidModId("_leading_underscore"));
29+
REQUIRE(isValidModId("trailing_underscore_"));
30+
}
31+
32+
TEST_CASE("Mod ID - mixed valid characters", "[modid]") {
33+
REQUIRE(isValidModId("mod-123_test"));
34+
REQUIRE(isValidModId("a1-b2_c3"));
35+
}
36+
37+
TEST_CASE("Mod ID - invalid spaces", "[modid]") {
38+
REQUIRE_FALSE(isValidModId("my mod"));
39+
REQUIRE_FALSE(isValidModId("mod name with spaces"));
40+
}
41+
42+
TEST_CASE("Mod ID - invalid special chars", "[modid]") {
43+
REQUIRE_FALSE(isValidModId("mod!"));
44+
REQUIRE_FALSE(isValidModId("mod@name"));
45+
REQUIRE_FALSE(isValidModId("mod#test"));
46+
REQUIRE_FALSE(isValidModId("mod$"));
47+
REQUIRE_FALSE(isValidModId("mod%"));
48+
REQUIRE_FALSE(isValidModId("mod.test"));
49+
REQUIRE_FALSE(isValidModId("mod/test"));
50+
REQUIRE_FALSE(isValidModId("mod\\test"));
51+
}
52+
53+
TEST_CASE("Mod ID - empty string", "[modid]") {
54+
REQUIRE_FALSE(isValidModId(""));
55+
}
56+
57+
TEST_CASE("Mod ID - single character", "[modid]") {
58+
REQUIRE(isValidModId("a"));
59+
REQUIRE(isValidModId("1"));
60+
REQUIRE(isValidModId("-"));
61+
REQUIRE(isValidModId("_"));
62+
}

0 commit comments

Comments
 (0)