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
This commit is contained in:
parent
01d62ffa13
commit
3e0f3a5a64
33 changed files with 7084 additions and 65 deletions
|
|
@ -267,3 +267,784 @@ func TestStripAngleBrackets(t *testing.T) {
|
|||
t.Errorf("MessageID not stripped: got %q", nzb.Files[0].Segments[0].MessageID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Malformed / edge-case XML inputs ---
|
||||
|
||||
func TestParse_CompletelyEmpty(t *testing.T) {
|
||||
_, err := Parse(strings.NewReader(""))
|
||||
if err == nil {
|
||||
t.Error("expected error for completely empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_OnlyWhitespace(t *testing.T) {
|
||||
_, err := Parse(strings.NewReader(" \n\t "))
|
||||
if err == nil {
|
||||
t.Error("expected error for whitespace-only input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_ValidXMLButNotNZB(t *testing.T) {
|
||||
_, err := Parse(strings.NewReader(`<?xml version="1.0"?><html><body>Hello</body></html>`))
|
||||
if err == nil {
|
||||
t.Error("expected error for non-NZB XML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_NZBWithNoSegments(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments></segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
_, err := Parse(strings.NewReader(xml))
|
||||
if err == nil {
|
||||
t.Error("expected error for file with no segments")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_SegmentWithEmptyMessageID(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1"> </segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
_, err := Parse(strings.NewReader(xml))
|
||||
if err == nil {
|
||||
t.Error("expected error: segment with empty/whitespace message ID should be skipped, leaving no valid files")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_MixedValidAndEmptySegments(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">valid@id</segment>
|
||||
<segment bytes="200" number="2"> </segment>
|
||||
<segment bytes="300" number="3">also-valid@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if len(nzb.Files[0].Segments) != 2 {
|
||||
t.Errorf("expected 2 valid segments, got %d", len(nzb.Files[0].Segments))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Metadata / Head parsing ---
|
||||
|
||||
func TestParse_MetaPassword(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<head>
|
||||
<meta type="password">s3cr3t</meta>
|
||||
<meta type="title">My Movie</meta>
|
||||
<meta type="category">Movies</meta>
|
||||
</head>
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if nzb.Password != "s3cr3t" {
|
||||
t.Errorf("Password: got %q, want %q", nzb.Password, "s3cr3t")
|
||||
}
|
||||
if nzb.Meta["title"] != "My Movie" {
|
||||
t.Errorf("Meta title: got %q", nzb.Meta["title"])
|
||||
}
|
||||
if nzb.Meta["category"] != "Movies" {
|
||||
t.Errorf("Meta category: got %q", nzb.Meta["category"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_MetaPasswordWithWhitespace(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<head>
|
||||
<meta type="password"> padded </meta>
|
||||
</head>
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if nzb.Password != "padded" {
|
||||
t.Errorf("Password should be trimmed: got %q", nzb.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_NoHead(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if nzb.Password != "" {
|
||||
t.Errorf("Password should be empty: got %q", nzb.Password)
|
||||
}
|
||||
if len(nzb.Meta) != 0 {
|
||||
t.Errorf("Meta should be empty: got %v", nzb.Meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_MetaWithEmptyType(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<head>
|
||||
<meta type="">ignored</meta>
|
||||
<meta type="name">kept</meta>
|
||||
</head>
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if _, ok := nzb.Meta[""]; ok {
|
||||
t.Error("empty-type meta should not be stored")
|
||||
}
|
||||
if nzb.Meta["name"] != "kept" {
|
||||
t.Errorf("Meta name: got %q", nzb.Meta["name"])
|
||||
}
|
||||
}
|
||||
|
||||
// --- Multiple files ---
|
||||
|
||||
func TestParse_MultipleFilesVariousTypes(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="bot" date="1700000000" subject=""movie.mkv" yEnc (1/100)">
|
||||
<groups><group>alt.binaries.movies</group></groups>
|
||||
<segments>
|
||||
<segment bytes="768000" number="1">mkv001@ex</segment>
|
||||
<segment bytes="768000" number="2">mkv002@ex</segment>
|
||||
</segments>
|
||||
</file>
|
||||
<file poster="bot" date="1700000000" subject=""movie.nfo" yEnc (1/1)">
|
||||
<groups><group>alt.binaries.movies</group></groups>
|
||||
<segments>
|
||||
<segment bytes="4096" number="1">nfo001@ex</segment>
|
||||
</segments>
|
||||
</file>
|
||||
<file poster="bot" date="1700000000" subject=""movie.par2" yEnc (1/1)">
|
||||
<groups><group>alt.binaries.movies</group></groups>
|
||||
<segments>
|
||||
<segment bytes="32768" number="1">par001@ex</segment>
|
||||
</segments>
|
||||
</file>
|
||||
<file poster="bot" date="1700000000" subject=""movie.vol0+1.par2" yEnc (1/1)">
|
||||
<groups><group>alt.binaries.movies</group></groups>
|
||||
<segments>
|
||||
<segment bytes="65536" number="1">parv001@ex</segment>
|
||||
</segments>
|
||||
</file>
|
||||
<file poster="bot" date="1700000000" subject=""sample.mkv" yEnc (1/1)">
|
||||
<groups><group>alt.binaries.movies</group></groups>
|
||||
<segments>
|
||||
<segment bytes="10000" number="1">sample001@ex</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
if len(nzb.Files) != 5 {
|
||||
t.Fatalf("expected 5 files, got %d", len(nzb.Files))
|
||||
}
|
||||
|
||||
// ContentFiles should exclude nfo, par2, par2 vol, and sample
|
||||
content := nzb.ContentFiles()
|
||||
if len(content) != 1 {
|
||||
t.Errorf("ContentFiles: got %d, want 1", len(content))
|
||||
}
|
||||
if len(content) > 0 && content[0].Filename() != "movie.mkv" {
|
||||
t.Errorf("ContentFiles[0]: got %q, want movie.mkv", content[0].Filename())
|
||||
}
|
||||
|
||||
// Par2Files
|
||||
par2 := nzb.Par2Files()
|
||||
if len(par2) != 2 {
|
||||
t.Errorf("Par2Files: got %d, want 2", len(par2))
|
||||
}
|
||||
|
||||
if !nzb.HasPar2() {
|
||||
t.Error("HasPar2 should be true")
|
||||
}
|
||||
if nzb.HasRars() {
|
||||
t.Error("HasRars should be false for this NZB")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Segment ordering / number parsing ---
|
||||
|
||||
func TestParse_SegmentNumberParsing(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="3">c@id</segment>
|
||||
<segment bytes="200" number="1">a@id</segment>
|
||||
<segment bytes="300" number="2">b@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
|
||||
segs := nzb.Files[0].Segments
|
||||
if len(segs) != 3 {
|
||||
t.Fatalf("expected 3 segments, got %d", len(segs))
|
||||
}
|
||||
|
||||
// Parse preserves order from XML; sorting is done by the downloader
|
||||
// Verify numbers are parsed correctly
|
||||
numbers := make(map[int]bool)
|
||||
for _, s := range segs {
|
||||
numbers[s.Number] = true
|
||||
}
|
||||
for _, want := range []int{1, 2, 3} {
|
||||
if !numbers[want] {
|
||||
t.Errorf("missing segment number %d", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_SegmentBytesZero(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="0" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if nzb.Files[0].Segments[0].Bytes != 0 {
|
||||
t.Errorf("expected 0 bytes, got %d", nzb.Files[0].Segments[0].Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_SegmentBytesNonNumeric(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="abc" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
// Non-numeric bytes should parse as 0
|
||||
if nzb.Files[0].Segments[0].Bytes != 0 {
|
||||
t.Errorf("non-numeric bytes should be 0, got %d", nzb.Files[0].Segments[0].Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// --- File helper methods ---
|
||||
|
||||
func TestFileTotalBytes(t *testing.T) {
|
||||
f := File{
|
||||
Segments: []Segment{
|
||||
{Bytes: 100}, {Bytes: 200}, {Bytes: 300},
|
||||
},
|
||||
}
|
||||
if got := f.TotalBytes(); got != 600 {
|
||||
t.Errorf("TotalBytes: got %d, want 600", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileTotalBytes_Empty(t *testing.T) {
|
||||
f := File{}
|
||||
if got := f.TotalBytes(); got != 0 {
|
||||
t.Errorf("TotalBytes of empty file: got %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExtension_Various(t *testing.T) {
|
||||
tests := []struct {
|
||||
subject string
|
||||
want string
|
||||
}{
|
||||
{`"file.MKV" yEnc`, ".mkv"},
|
||||
{`"file.RAR" yEnc`, ".rar"},
|
||||
{`"file.Par2" yEnc`, ".par2"},
|
||||
{`"noext" yEnc`, ""},
|
||||
{`"file.tar.gz" yEnc`, ".gz"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
f := File{Subject: tt.subject}
|
||||
if got := f.Extension(); got != tt.want {
|
||||
t.Errorf("Extension(%q) = %q, want %q", tt.subject, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- LargestFile edge cases ---
|
||||
|
||||
func TestLargestFile_EmptyNZB(t *testing.T) {
|
||||
nzb := &NZB{}
|
||||
if nzb.LargestFile() != nil {
|
||||
t.Error("LargestFile should return nil for empty NZB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLargestFile_SingleFile(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"only.bin"`, Segments: []Segment{{Bytes: 100}}},
|
||||
},
|
||||
}
|
||||
largest := nzb.LargestFile()
|
||||
if largest == nil {
|
||||
t.Fatal("LargestFile should not be nil")
|
||||
}
|
||||
if largest.Filename() != "only.bin" {
|
||||
t.Errorf("got %q", largest.Filename())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLargestFile_MultipleSameSize(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"a.bin"`, Segments: []Segment{{Bytes: 100}}},
|
||||
{Subject: `"b.bin"`, Segments: []Segment{{Bytes: 100}}},
|
||||
},
|
||||
}
|
||||
largest := nzb.LargestFile()
|
||||
if largest == nil {
|
||||
t.Fatal("LargestFile should not be nil")
|
||||
}
|
||||
// Should return the first one (stable)
|
||||
if largest.Filename() != "a.bin" {
|
||||
t.Errorf("got %q, expected first file for equal sizes", largest.Filename())
|
||||
}
|
||||
}
|
||||
|
||||
// --- IsObfuscated ---
|
||||
|
||||
func TestIsObfuscated_Normal(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"Movie.2024.1080p.BluRay.x264-GROUP.mkv"`},
|
||||
},
|
||||
}
|
||||
if nzb.IsObfuscated() {
|
||||
t.Error("normal filename should not be obfuscated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsObfuscated_HexName(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"a1b2c3d4e5f6a7b8c9d0e1f2.mkv"`},
|
||||
},
|
||||
}
|
||||
if !nzb.IsObfuscated() {
|
||||
t.Error("hex-like filename should be obfuscated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsObfuscated_EmptyFiles(t *testing.T) {
|
||||
nzb := &NZB{}
|
||||
if nzb.IsObfuscated() {
|
||||
t.Error("empty NZB should not be obfuscated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsObfuscated_ShortHex(t *testing.T) {
|
||||
// Short name (<=10 chars) should not trigger obfuscation
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"abcdef.mkv"`},
|
||||
},
|
||||
}
|
||||
if nzb.IsObfuscated() {
|
||||
t.Error("short hex-like name should not be obfuscated")
|
||||
}
|
||||
}
|
||||
|
||||
// --- isMetadataFile ---
|
||||
|
||||
func TestIsMetadataFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{"file.par2", true},
|
||||
{"file.nfo", true},
|
||||
{"file.sfv", true},
|
||||
{"file.nzb", true},
|
||||
{"file.txt", true},
|
||||
{"file.jpg", true},
|
||||
{"file.png", true},
|
||||
{"file.url", true},
|
||||
{"file.mkv", false},
|
||||
{"file.rar", false},
|
||||
{"file.avi", false},
|
||||
{"FILE.PAR2", true},
|
||||
{"FILE.NFO", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isMetadataFile(tt.name); got != tt.want {
|
||||
t.Errorf("isMetadataFile(%q) = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- isSampleFile ---
|
||||
|
||||
func TestIsSampleFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{"movie.sample.mkv", true},
|
||||
{"Sample.mkv", true},
|
||||
{"SAMPLE.avi", true},
|
||||
{"movie-sample-video.mkv", true},
|
||||
{"movie_sample.mkv", true},
|
||||
{"sample.mkv", true},
|
||||
{"resampled.mkv", false}, // "sample" is part of "resampled"
|
||||
{"movie.mkv", false},
|
||||
{"my.samples.zip", false}, // "sample" followed by 's' (alphanumeric)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isSampleFile(tt.name); got != tt.want {
|
||||
t.Errorf("isSampleFile(%q) = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- isHexLike ---
|
||||
|
||||
func TestIsHexLike(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{"abcdef0123456789", true},
|
||||
{"ABCDEF", true},
|
||||
{"Movie2024", false},
|
||||
{"aabbccdd", true},
|
||||
{"xyz_not_hex", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isHexLike(tt.input); got != tt.want {
|
||||
t.Errorf("isHexLike(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- sanitizeFilename ---
|
||||
|
||||
func TestSanitizeFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"simple name", "simple name"},
|
||||
{"name (1/50)", "name"},
|
||||
{"file yEnc (01/99)", "file"},
|
||||
{`path/with\special:chars*?`, `path_with_special_chars__`},
|
||||
{`"quoted" text`, `_quoted_ text`},
|
||||
{" spaces ", "spaces"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := sanitizeFilename(tt.input); got != tt.want {
|
||||
t.Errorf("sanitizeFilename(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Filename fallback ---
|
||||
|
||||
func TestFilename_Fallback_NoQuotes(t *testing.T) {
|
||||
f := File{Subject: "No quotes here yEnc (1/50)"}
|
||||
got := f.Filename()
|
||||
if got != "No quotes here" {
|
||||
t.Errorf("Filename fallback: got %q, want %q", got, "No quotes here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilename_EmptySubject(t *testing.T) {
|
||||
f := File{Subject: ""}
|
||||
got := f.Filename()
|
||||
if got != "" {
|
||||
t.Errorf("Filename empty subject: got %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// --- NZB aggregate methods on mixed content ---
|
||||
|
||||
func TestNZB_HasRars_NoRars(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"movie.mkv"`},
|
||||
{Subject: `"movie.par2"`},
|
||||
},
|
||||
}
|
||||
if nzb.HasRars() {
|
||||
t.Error("HasRars should be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNZB_HasPar2_NoPar2(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"movie.mkv"`},
|
||||
{Subject: `"movie.rar"`},
|
||||
},
|
||||
}
|
||||
if nzb.HasPar2() {
|
||||
t.Error("HasPar2 should be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNZB_TotalSegments_MultiFile(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Segments: []Segment{{}, {}, {}}},
|
||||
{Segments: []Segment{{}, {}}},
|
||||
},
|
||||
}
|
||||
if got := nzb.TotalSegments(); got != 5 {
|
||||
t.Errorf("TotalSegments: got %d, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNZB_TotalBytes_MultiFile(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Segments: []Segment{{Bytes: 100}, {Bytes: 200}}},
|
||||
{Segments: []Segment{{Bytes: 300}}},
|
||||
},
|
||||
}
|
||||
if got := nzb.TotalBytes(); got != 600 {
|
||||
t.Errorf("TotalBytes: got %d, want 600", got)
|
||||
}
|
||||
}
|
||||
|
||||
// --- isRarFile extended ---
|
||||
|
||||
func TestIsRarFile_Extended(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{"file.RAR", true}, // case insensitive
|
||||
{"file.Rar", true},
|
||||
{"file.s01", true},
|
||||
{"file.s99", true},
|
||||
{"file.002", true},
|
||||
{"file.999", true},
|
||||
{"file.r0", false}, // too short extension
|
||||
{"file.rXX", false}, // non-numeric
|
||||
{"file", false}, // no extension
|
||||
{"file.mp4", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isRarFile(tt.name); got != tt.want {
|
||||
t.Errorf("isRarFile(%q) = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parse with date edge cases ---
|
||||
|
||||
func TestParse_DateNonNumeric(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="not-a-number" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if nzb.Files[0].Date != 0 {
|
||||
t.Errorf("non-numeric date should be 0, got %d", nzb.Files[0].Date)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_DateEmpty(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="" subject=""test.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if nzb.Files[0].Date != 0 {
|
||||
t.Errorf("empty date should be 0, got %d", nzb.Files[0].Date)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parse: file with all segments having empty IDs should be excluded ---
|
||||
|
||||
func TestParse_AllEmptySegments(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""bad.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1"> </segment>
|
||||
<segment bytes="200" number="2"></segment>
|
||||
</segments>
|
||||
</file>
|
||||
<file poster="test" date="0" subject=""good.bin"">
|
||||
<groups><group>alt.test</group></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">valid@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if len(nzb.Files) != 1 {
|
||||
t.Fatalf("expected 1 valid file, got %d", len(nzb.Files))
|
||||
}
|
||||
if nzb.Files[0].Filename() != "good.bin" {
|
||||
t.Errorf("expected good.bin, got %q", nzb.Files[0].Filename())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Groups ---
|
||||
|
||||
func TestParse_NoGroups(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups></groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if len(nzb.Files[0].Groups) != 0 {
|
||||
t.Errorf("expected 0 groups, got %d", len(nzb.Files[0].Groups))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_MultipleGroups(t *testing.T) {
|
||||
xml := `<?xml version="1.0"?>
|
||||
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
|
||||
<file poster="test" date="0" subject=""test.bin"">
|
||||
<groups>
|
||||
<group>alt.binaries.movies</group>
|
||||
<group>alt.binaries.multimedia</group>
|
||||
<group>alt.binaries.hdtv</group>
|
||||
</groups>
|
||||
<segments>
|
||||
<segment bytes="100" number="1">seg@id</segment>
|
||||
</segments>
|
||||
</file>
|
||||
</nzb>`
|
||||
nzb, err := Parse(strings.NewReader(xml))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if len(nzb.Files[0].Groups) != 3 {
|
||||
t.Errorf("expected 3 groups, got %d", len(nzb.Files[0].Groups))
|
||||
}
|
||||
}
|
||||
|
||||
// --- ContentFiles with sample variations ---
|
||||
|
||||
func TestContentFiles_ExcludesSamples(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"movie.mkv"`, Segments: []Segment{{Bytes: 1000, MessageID: "a"}}},
|
||||
{Subject: `"movie.sample.mkv"`, Segments: []Segment{{Bytes: 100, MessageID: "b"}}},
|
||||
{Subject: `"Sample/preview.mkv"`, Segments: []Segment{{Bytes: 100, MessageID: "c"}}},
|
||||
},
|
||||
}
|
||||
content := nzb.ContentFiles()
|
||||
if len(content) != 1 {
|
||||
t.Errorf("ContentFiles should exclude samples: got %d, want 1", len(content))
|
||||
}
|
||||
}
|
||||
|
||||
// --- RarFiles with split naming ---
|
||||
|
||||
func TestRarFiles_SplitRars(t *testing.T) {
|
||||
nzb := &NZB{
|
||||
Files: []File{
|
||||
{Subject: `"movie.rar"`, Segments: []Segment{{MessageID: "a"}}},
|
||||
{Subject: `"movie.r00"`, Segments: []Segment{{MessageID: "b"}}},
|
||||
{Subject: `"movie.r01"`, Segments: []Segment{{MessageID: "c"}}},
|
||||
{Subject: `"movie.001"`, Segments: []Segment{{MessageID: "d"}}},
|
||||
{Subject: `"movie.002"`, Segments: []Segment{{MessageID: "e"}}},
|
||||
{Subject: `"movie.par2"`, Segments: []Segment{{MessageID: "f"}}},
|
||||
{Subject: `"movie.mkv"`, Segments: []Segment{{MessageID: "g"}}},
|
||||
},
|
||||
}
|
||||
rars := nzb.RarFiles()
|
||||
if len(rars) != 5 {
|
||||
t.Errorf("RarFiles: got %d, want 5", len(rars))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue