package download import ( "context" "os" "path/filepath" "strings" "testing" "time" ) // Where slskd writes a grab's files, and how collect finds them (#274). // slskd reads searchTimeout in whole seconds, from the last response. func TestSlskdSearchTimeoutIsInSeconds(t *testing.T) { t.Parallel() cases := []struct { wait time.Duration want int }{ {20 * time.Second, 18}, {200 * time.Millisecond, slskdMinSearchTimeout}, } for _, tc := range cases { s := &slskd{searchWait: tc.wait} got, ok := s.searchRequest("id", "text", 2)["searchTimeout"].(int) if !ok || got != tc.want { t.Errorf("wait %s: searchTimeout = %v, want %d seconds", tc.wait, got, tc.want) } } } func TestIsRenamedCopy(t *testing.T) { t.Parallel() cases := map[string]bool{ "01 A_638912345678901234.flac": true, "01 A.flac": false, "01 A_.flac": false, "01 A_v2.flac": false, "01 A_123.mp3": false, "01 AB_123.flac": false, } for name, want := range cases { if got := isRenamedCopy(name, "01 A", ".flac"); got != want { t.Errorf("isRenamedCopy(%q) = %v, want %v", name, got, want) } } } func succeeded(names ...string) [][]slskdTransfer { out := make([]slskdTransfer, 0, len(names)) for i, n := range names { out = append(out, slskdTransfer{ ID: "t" + itoa(i), Filename: n, State: "Completed, Succeeded", BytesTransferred: 500, }) } return [][]slskdTransfer{out} } // On a daemon with batches, each grab writes into a folder of its own, // collect reads from exactly there, and the folder is gone afterwards. func TestSlskdBatchGrabUsesItsOwnFolder(t *testing.T) { t.Parallel() stub := newSlskdStub(t) stub.batches = true stub.transfers = succeeded(`\s\Album\01 A.flac`, `\s\Album\02 B.flac`) s, downloads := newStubSlskd(t, stub) // A same-named file in the folder a per-user enqueue would use is // someone else's, and must not be touched. other := filepath.Join(downloads, "Album", "01 A.flac") if err := os.MkdirAll(filepath.Dir(other), 0o750); err != nil { t.Fatal(err) } if err := os.WriteFile(other, []byte("the user's"), 0o600); err != nil { t.Fatal(err) } dst := t.TempDir() got, err := s.Grab( context.Background(), slskdAlbum(t, stub, "peer", "01 A.flac", "02 B.flac"), dst, nil, ) if err != nil { t.Fatalf("Grab: %v", err) } if len(got.Files) != 2 { t.Fatalf("collected %d files, want 2", len(got.Files)) } stub.mu.Lock() bodies := append([]map[string]any(nil), stub.batchBodies...) stub.mu.Unlock() if len(bodies) != 1 { t.Fatalf("%d batches, want 1", len(bodies)) } if bodies[0]["username"] != "peer" { t.Errorf("batch username = %v", bodies[0]["username"]) } dest, _ := bodies[0]["options"].(map[string]any)["destination"].(string) if !strings.HasPrefix(dest, slskdDestRoot+"/") { t.Errorf("destination %q is not under %s", dest, slskdDestRoot) } if _, err := os.Stat(filepath.Join(downloads, filepath.FromSlash(dest))); !os.IsNotExist(err) { t.Errorf("batch folder left behind: %v", err) } if data, _ := os.ReadFile(other); string(data) != "the user's" { t.Errorf("the user's own file was taken or changed: %q", data) } } // A batch's files land flat in its destination, so a two-disc rip is // two batches, one per disc, or disc 2's "01" is renamed out of the way // of disc 1's. func TestSlskdBatchSplitsDiscs(t *testing.T) { t.Parallel() stub := newSlskdStub(t) stub.batches = true stub.transfers = succeeded(`\s\Wall\CD1\01 In.flac`, `\s\Wall\CD2\01 Hey You.flac`) stub.deliver("01 In.flac", "01 Hey You.flac") s, _ := newStubSlskd(t, stub) dst := t.TempDir() got, err := s.Grab(context.Background(), Candidate{ Files: []CandidateFile{ {Path: `\s\Wall\CD1\01 In.flac`, Size: 500, IsAudio: true}, {Path: `\s\Wall\CD2\01 Hey You.flac`, Size: 500, IsAudio: true}, }, Payload: map[string]string{"username": "peer"}, }, dst, nil) if err != nil { t.Fatalf("Grab: %v", err) } stub.mu.Lock() n := len(stub.batchBodies) stub.mu.Unlock() if n != 2 { t.Errorf("%d batches, want one per disc", n) } for _, want := range []string{ filepath.Join(dst, "CD1", "01 In.flac"), filepath.Join(dst, "CD2", "01 Hey You.flac"), } { found := false for _, f := range got.Files { found = found || f == want } if !found { t.Errorf("%s not collected; got %q", want, got.Files) } } } // An abandoned batch grab leaves nothing in slskd's folder either. func TestSlskdBatchFailureDiscardsItsFolder(t *testing.T) { t.Parallel() stub := newSlskdStub(t) stub.batches = true stub.transfers = [][]slskdTransfer{{ {ID: "t0", Filename: `\s\Album\01 A.flac`, State: "Completed, Errored"}, {ID: "t1", Filename: `\s\Album\02 B.flac`, State: "Completed, Errored"}, }} s, downloads := newStubSlskd(t, stub) // The stub delivers the file, as a partial slskd left behind would. if _, err := s.Grab( context.Background(), slskdAlbum(t, stub, "peer", "01 A.flac"), t.TempDir(), nil, ); err == nil { t.Fatal("Grab succeeded with every transfer failed") } entries, _ := os.ReadDir(filepath.Join(downloads, slskdDestRoot)) if len(entries) != 0 { t.Errorf("%d batch folders left behind", len(entries)) } } // An older daemon answers the batch endpoint with 400; the grab falls // back to the per-user enqueue, and later grabs do not ask again. func TestSlskdFallsBackWithoutBatches(t *testing.T) { t.Parallel() stub := newSlskdStub(t) stub.transfers = succeeded(`\s\Album\01 A.flac`, `\s\Album\02 B.flac`) s, _ := newStubSlskd(t, stub) for range 2 { stub.mu.Lock() stub.posted = false stub.pollCount = 0 stub.mu.Unlock() if _, err := s.Grab( context.Background(), slskdAlbum(t, stub, "peer", "01 A.flac"), t.TempDir(), nil, ); err != nil { t.Fatalf("Grab: %v", err) } } stub.mu.Lock() paths := append([]string(nil), stub.paths...) stub.mu.Unlock() batchCalls := 0 for _, p := range paths { if strings.HasSuffix(p, "/batches") { batchCalls++ } } if batchCalls != 1 { t.Errorf("asked for a batch %d times, want once", batchCalls) } } // Without batches, a file of the same name already in slskd's folder is // not this grab's: slskd wrote ours beside it as name_.ext, and // that is the one collected. func TestSlskdCollectsTheRenamedCopyNotTheOldFile(t *testing.T) { t.Parallel() stub := newSlskdStub(t) stub.transfers = succeeded(`\s\Album\01 A.flac`, `\s\Album\02 B.flac`) s, downloads := newStubSlskd(t, stub) old := filepath.Join(downloads, "Album", "01 A.flac") if err := os.MkdirAll(filepath.Dir(old), 0o750); err != nil { t.Fatal(err) } if err := os.WriteFile(old, []byte("left by an earlier attempt"), 0o600); err != nil { t.Fatal(err) } dst := t.TempDir() got, err := s.Grab( context.Background(), slskdAlbum(t, stub, "peer", "01 A.flac", "02 B.flac"), dst, nil, ) if err != nil { t.Fatalf("Grab: %v", err) } if len(got.Files) != 2 { t.Fatalf("collected %d files, want 2", len(got.Files)) } data, err := os.ReadFile(filepath.Join(dst, "01 A.flac")) if err != nil || string(data) != "audio" { t.Errorf("collected %q, want this grab's file", data) } if data, _ := os.ReadFile(old); string(data) != "left by an earlier attempt" { t.Error("the file that was already there was moved") } } // And when this grab's copy never arrived, the old one is not taken in // its place. func TestSlskdDoesNotCollectAFileThatWasAlreadyThere(t *testing.T) { t.Parallel() stub := newSlskdStub(t) stub.transfers = [][]slskdTransfer{ { {ID: "t0", Filename: `\s\Album\01 A.flac`, State: "Completed, Errored"}, { ID: "t1", Filename: `\s\Album\02 B.flac`, State: "Completed, Succeeded", BytesTransferred: 500, }, }, } s, downloads := newStubSlskd(t, stub) old := filepath.Join(downloads, "Album", "01 A.flac") if err := os.MkdirAll(filepath.Dir(old), 0o750); err != nil { t.Fatal(err) } if err := os.WriteFile(old, []byte("stale"), 0o600); err != nil { t.Fatal(err) } got, err := s.Grab( context.Background(), slskdAlbum(t, stub, "peer", "02 B.flac"), t.TempDir(), nil, ) if err != nil { t.Fatalf("Grab: %v", err) } if len(got.Files) != 1 || filepath.Base(got.Files[0]) != "02 B.flac" { t.Errorf("collected %q, want only 02 B.flac", got.Files) } }