feat(cli): upgrade command, rich status, and version cache
- Replace `upgrade` stub with real command (alias for `self-update`)
- Also register `update` as alias: `unarr update` works too
- Rewrite `status` to show full config, disk usage, daemon state, and
update availability with colored sections
- Add version check cache (1h TTL) so `status` is instant on repeat runs
- Guard against division by zero on empty filesystems
- Guard against negative durations from clock skew
- Guard against stale PID via heartbeat recency check (2 min)
- Add comprehensive test coverage across agent, engine, upgrade, usenet,
arr, library, mediaserver, and UI packages
- Improve Makefile coverage target to exclude cmd/ glue code
- Fix stream handler resource cleanup and ffprobe error handling
2026-03-31 22:05:43 +02:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestExpandHome(t *testing.T) {
|
|
|
|
|
home, _ := os.UserHomeDir()
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
input string
|
|
|
|
|
want string
|
|
|
|
|
}{
|
|
|
|
|
{"~/Documents", home + "/Documents"},
|
|
|
|
|
{"~/", home},
|
|
|
|
|
{"/absolute/path", "/absolute/path"},
|
|
|
|
|
{"relative/path", "relative/path"},
|
|
|
|
|
{"", ""},
|
|
|
|
|
{"~notexpanded", "~notexpanded"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
|
|
|
got := expandHome(tt.input)
|
|
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("expandHome(%q) = %q, want %q", tt.input, got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix(security): CORS allowlist, URL scheme guard, state perms, ZIP slip, mirror docs
Phase 3 security audit follow-up. Medium and low-severity hardenings
plus a deferred-work plan for the cross-repo stream-token rollout.
Stream server CORS: replace the wildcard Access-Control-Allow-Origin
with an allowlist that echoes back only torrentclaw.com,
app.torrentclaw.com, the local Next dev port (3030 — matches the web
repo package.json) and any extras the operator adds via the new
downloads.cors_extra_origins TOML key. A Vary: Origin header is now
emitted whenever the request carries an Origin header so an
intermediate cache cannot serve a stale ACAO to a different origin.
URL scheme guard: openBrowser and OpenPlayer refuse any URL that is
not http(s). Combined with passing the URL after "--" wherever the
launched helper supports it (open, mpv, vlc, cvlc), this stops a
leading "-" from being parsed as a switch by the spawned process.
State file permissions: WriteState now writes 0o600 so the agent ID,
PID and counters cannot be enumerated by another local user on a
shared host. Matches the existing config file mode.
ZIP slip defense-in-depth: extractZip extracts the safety check into
safeZipPath, which canonicalises the entry name (normalising
backslashes to "/"), rejects "..", "../" prefix and "/../" interior
components, and verifies the final destination stays inside destDir
before opening any file.
Mirror fallback: documented the design for multi-provider
mirrors.json hosting in the comment block on DefaultStaticFallbackURLs
and added a follow-up note about signing it with the same ed25519
release key. The list is kept at one provider until the second host
is provisioned and added to torrentclaw-web's STATIC_FALLBACKS.
Deferred work: a new plan document Docs/plans/security-stream-token.md
covers the per-task stream token (Phase 2.2 of the original audit)
which requires coordinated web + CLI work and ships separately.
2026-05-15 18:48:59 +02:00
|
|
|
func TestIsSafeBrowserURL(t *testing.T) {
|
|
|
|
|
good := []string{
|
|
|
|
|
"http://localhost:3000",
|
|
|
|
|
"https://torrentclaw.com/some/path?q=1",
|
|
|
|
|
}
|
|
|
|
|
bad := []string{
|
|
|
|
|
"--help",
|
|
|
|
|
"-version",
|
|
|
|
|
"file:///etc/passwd",
|
|
|
|
|
"javascript:alert(1)",
|
|
|
|
|
"data:text/html,foo",
|
|
|
|
|
"ftp://example.com",
|
|
|
|
|
"",
|
|
|
|
|
}
|
|
|
|
|
for _, u := range good {
|
|
|
|
|
if !isSafeBrowserURL(u) {
|
|
|
|
|
t.Errorf("isSafeBrowserURL(%q) = false, want true", u)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, u := range bad {
|
|
|
|
|
if isSafeBrowserURL(u) {
|
|
|
|
|
t.Errorf("isSafeBrowserURL(%q) = true, want false", u)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(cli): upgrade command, rich status, and version cache
- Replace `upgrade` stub with real command (alias for `self-update`)
- Also register `update` as alias: `unarr update` works too
- Rewrite `status` to show full config, disk usage, daemon state, and
update availability with colored sections
- Add version check cache (1h TTL) so `status` is instant on repeat runs
- Guard against division by zero on empty filesystems
- Guard against negative durations from clock skew
- Guard against stale PID via heartbeat recency check (2 min)
- Add comprehensive test coverage across agent, engine, upgrade, usenet,
arr, library, mediaserver, and UI packages
- Improve Makefile coverage target to exclude cmd/ glue code
- Fix stream handler resource cleanup and ffprobe error handling
2026-03-31 22:05:43 +02:00
|
|
|
func TestDefaultDownloadDir(t *testing.T) {
|
|
|
|
|
dir := defaultDownloadDir()
|
|
|
|
|
if dir == "" {
|
|
|
|
|
t.Error("defaultDownloadDir() returned empty string")
|
|
|
|
|
}
|
|
|
|
|
home, _ := os.UserHomeDir()
|
|
|
|
|
if !strings.HasPrefix(dir, home) {
|
|
|
|
|
t.Errorf("defaultDownloadDir() = %q, expected to start with home dir %q", dir, home)
|
|
|
|
|
}
|
|
|
|
|
}
|