docs: rewrite PLAN.md and update CODEBASE.md, README.md

- Full rewrite of PLAN.md to reflect current architecture (27 REST + 3 WS endpoints,
  embedded frontend, automation, scheduler, health check, GoReleaser CI)
- Added health.go, mods.go, scheduler.go, robfig/cron dep to CODEBASE.md
- Added Gitea Actions CI/CD section to CODEBASE.md
- Added conventional commits to code style section
- Added missing API routes and embed/ to README.md
This commit is contained in:
MrFastwind
2026-07-23 20:14:56 +02:00
parent 914e0fbe48
commit d55200886b
15 changed files with 1765 additions and 67 deletions
@@ -0,0 +1,179 @@
package services
import (
"strings"
"testing"
"arma3-web-server/internal/models"
)
func TestParseModlistHTML_SingleMod(t *testing.T) {
html := `<html><body><table>
<tr data-type="ModContainer">
<td data-type="DisplayName">CBA_A3</td>
<td><span class="from-steam">Steam</span></td>
<td><a href="https://steamcommunity.com/sharedfiles/filedetails/?id=450814997">link</a></td>
</tr>
</table></body></html>`
_, mods, err := ParseModlistHTML(strings.NewReader(html))
if err != nil {
t.Fatalf("ParseModlistHTML() error = %v", err)
}
if len(mods) != 1 {
t.Fatalf("got %d mods, want 1", len(mods))
}
if mods[0].ID != "450814997" {
t.Errorf("mods[0].ID = %q, want %q", mods[0].ID, "450814997")
}
if mods[0].Name != "CBA_A3" {
t.Errorf("mods[0].Name = %q, want %q", mods[0].Name, "CBA_A3")
}
if !mods[0].Enabled {
t.Error("mods[0].Enabled should be true")
}
}
func TestParseModlistHTML_MultipleMods(t *testing.T) {
html := `<html><body><table>
<tr data-type="ModContainer">
<td data-type="DisplayName">CBA_A3</td>
<td><span>Steam</span></td>
<td><a href="https://steamcommunity.com/sharedfiles/filedetails/?id=450814997">link</a></td>
</tr>
<tr data-type="ModContainer">
<td data-type="DisplayName">ACE3</td>
<td><span>Steam</span></td>
<td><a href="https://steamcommunity.com/sharedfiles/filedetails/?id=463939057">link</a></td>
</tr>
</table></body></html>`
_, mods, err := ParseModlistHTML(strings.NewReader(html))
if err != nil {
t.Fatalf("ParseModlistHTML() error = %v", err)
}
if len(mods) != 2 {
t.Fatalf("got %d mods, want 2", len(mods))
}
if mods[0].Name != "CBA_A3" || mods[1].Name != "ACE3" {
t.Errorf("mods = [%q, %q], want [CBA_A3, ACE3]", mods[0].Name, mods[1].Name)
}
}
func TestParseModlistHTML_Deduplication(t *testing.T) {
html := `<html><body><table>
<tr data-type="ModContainer">
<td data-type="DisplayName">CBA_A3</td>
<td><span>Steam</span></td>
<td><a href="https://steamcommunity.com/sharedfiles/filedetails/?id=450814997">link</a></td>
</tr>
<tr data-type="ModContainer">
<td data-type="DisplayName">CBA_A3</td>
<td><span>Steam</span></td>
<td><a href="https://steamcommunity.com/sharedfiles/filedetails/?id=450814997">link</a></td>
</tr>
</table></body></html>`
_, mods, _ := ParseModlistHTML(strings.NewReader(html))
if len(mods) != 1 {
t.Errorf("got %d mods, want 1 (deduplication)", len(mods))
}
}
func TestParseModlistHTML_EmptyTable(t *testing.T) {
html := `<html><body><table></table></body></html>`
_, mods, err := ParseModlistHTML(strings.NewReader(html))
if err != nil {
t.Fatalf("ParseModlistHTML() error = %v", err)
}
if len(mods) != 0 {
t.Errorf("got %d mods, want 0", len(mods))
}
}
func TestParseModlistHTML_InvalidHTML(t *testing.T) {
// Go's html parser is lenient and returns empty results for garbage input
_, mods, err := ParseModlistHTML(strings.NewReader("not html at all <>"))
if err != nil {
t.Fatalf("ParseModlistHTML() should not error on lenient parser, got: %v", err)
}
if len(mods) != 0 {
t.Errorf("ParseModlistHTML() returned %d mods for garbage input, want 0", len(mods))
}
}
func TestRenderModlistHTML(t *testing.T) {
mods := []models.ModEntry{
{ID: "450814997", Name: "CBA_A3", Enabled: true},
{ID: "463939057", Name: "ACE3", Enabled: true},
}
output, err := RenderModlistHTML("Test", mods)
if err != nil {
t.Fatalf("RenderModlistHTML() error = %v", err)
}
if !strings.Contains(output, "450814997") {
t.Error("output should contain mod ID 450814997")
}
if !strings.Contains(output, "CBA_A3") {
t.Error("output should contain mod name CBA_A3")
}
if !strings.Contains(output, "463939057") {
t.Error("output should contain mod ID 463939057")
}
if !strings.Contains(output, "<?xml") {
t.Error("output should start with XML declaration")
}
}
func TestRenderModlistHTML_SkipsEmptyIDs(t *testing.T) {
mods := []models.ModEntry{
{ID: "", Name: "No ID", Enabled: true},
{ID: "450814997", Name: "CBA_A3", Enabled: true},
}
output, err := RenderModlistHTML("Test", mods)
if err != nil {
t.Fatalf("RenderModlistHTML() error = %v", err)
}
if strings.Contains(output, "No ID") {
t.Error("output should not contain mod with empty ID")
}
}
func TestFileNameToModlistName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"my_modlist.html", "my modlist"},
{"LibUkraine Final (3).html", "LibUkraine Final (3)"},
{"test-file-name.html", "test file name"},
{"simple.html", "simple"},
}
for _, tt := range tests {
got := FileNameToModlistName(tt.input)
if got != tt.want {
t.Errorf("FileNameToModlistName(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestCleanModName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"CBA_A3", "CBA_A3"},
{"mod/name", "mod_name"},
{"mod\\name", "mod_name"},
{" spaces ", "spaces"},
}
for _, tt := range tests {
got := cleanModName(tt.input)
if got != tt.want {
t.Errorf("cleanModName(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}