Update golangci-lint to v2.12.2 with canonical config (closes #60)
All checks were successful
check / check (push) Successful in 53s
All checks were successful
check / check (push) Successful in 53s
- Add canonical .golangci.yml (v2 schema, default: all, project thresholds for lll/funlen/cyclop/dupl) - Bump golangci-lint pins from v2.0.2 to v2.12.2 in Makefile (go install, new /v2 module path) and Dockerfile (tagged+digest Debian image pin) - Fix all lint findings surfaced by the new linter set across cmd/mfer, internal/bork, internal/cli, internal/log, and mfer: static sentinel errors (err113), context-aware HTTP and exec (noctx), guarded integer conversions and stricter permissions (gosec), named constants (mnd, goconst), function decomposition (funlen, cyclop, gocognit, nestif), declaration ordering (funcorder), t.Parallel/t.TempDir/t.Setenv adoption in tests (paralleltest, usetesting), protobuf getters (protogetter), plus formatting and style cleanups (wsl_v5, nlreturn, lll, revive, testifylint, and others) - Serialize CLI runs in tests behind a mutex so parallel tests do not cross-wire the process-global logger's captured output The decompositions are behavior-preserving. In particular: - REPO_POLICIES.md is untouched and stays byte-identical to the authoritative copy in the prompts repo - the mfer.manifest type stays unexported; whether to export it is an open owner design question (README question 13) - directories created by fetch keep mode 0755, because fetched trees are content meant to be readable by other uids - an absent MFFilePath.Mtime is handled explicitly and identically in freshen, list, and export rather than being read as the Unix epoch, which would classify every entry as changed and rewrite the manifest on every freshen - every user-visible error message renders byte-identically to what it did before, with the err113 sentinels wrapped mid-sentence where needed; the rendered strings are now pinned by tests Also fixes an argument-injection defect the lint pass surfaced: key IDs reach gpg as bare positional arguments, so a key ID beginning with "-" was parsed by gpg as an option. All positional arguments now follow an explicit "--" end-of-options marker. The symlink-escape gap in fetch's path handling, which sanitizePath does not and cannot address, is filed separately as #86.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
//nolint:testpackage // white-box tests exercise unexported internals
|
||||
package cli
|
||||
|
||||
import (
|
||||
@@ -16,13 +17,25 @@ import (
|
||||
"sneak.berlin/go/mfer/mfer"
|
||||
)
|
||||
|
||||
const (
|
||||
testFileTxt = "file.txt"
|
||||
testDirFile = "dir/file.txt"
|
||||
testIndexMF = "https://example.com/path/index.mf"
|
||||
|
||||
// Exactly what url.Parse renders, with no wrapper of our own.
|
||||
urlParseControlCharErr = `parse "http://example.com/\x7f": ` +
|
||||
`net/url: invalid control character in URL`
|
||||
)
|
||||
|
||||
func TestEncodeFilePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"file.txt", "file.txt"},
|
||||
{"dir/file.txt", "dir/file.txt"},
|
||||
{testFileTxt, testFileTxt},
|
||||
{testDirFile, testDirFile},
|
||||
{"my file.txt", "my%20file.txt"},
|
||||
{"dir/my file.txt", "dir/my%20file.txt"},
|
||||
{"file#1.txt", "file%231.txt"},
|
||||
@@ -33,6 +46,8 @@ func TestEncodeFilePath(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := encodeFilePath(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
@@ -40,23 +55,27 @@ func TestEncodeFilePath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSanitizePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Valid paths that should be accepted
|
||||
validTests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"file.txt", "file.txt"},
|
||||
{"dir/file.txt", "dir/file.txt"},
|
||||
{testFileTxt, testFileTxt},
|
||||
{testDirFile, testDirFile},
|
||||
{"dir/subdir/file.txt", "dir/subdir/file.txt"},
|
||||
{"./file.txt", "file.txt"},
|
||||
{"./dir/file.txt", "dir/file.txt"},
|
||||
{"dir/./file.txt", "dir/file.txt"},
|
||||
{"./file.txt", testFileTxt},
|
||||
{"./dir/file.txt", testDirFile},
|
||||
{"dir/./file.txt", testDirFile},
|
||||
}
|
||||
|
||||
for _, tt := range validTests {
|
||||
t.Run("valid:"+tt.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result, err := sanitizePath(tt.input)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -78,6 +97,8 @@ func TestSanitizePath(t *testing.T) {
|
||||
|
||||
for _, tt := range invalidTests {
|
||||
t.Run("invalid:"+tt.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := sanitizePath(tt.input)
|
||||
assert.Error(t, err, "expected error for path: %s", tt.input)
|
||||
})
|
||||
@@ -85,36 +106,115 @@ func TestSanitizePath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveManifestURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
// Already ends with .mf - use as-is
|
||||
{"https://example.com/path/index.mf", "https://example.com/path/index.mf"},
|
||||
{testIndexMF, testIndexMF},
|
||||
{"https://example.com/path/custom.mf", "https://example.com/path/custom.mf"},
|
||||
{"https://example.com/foo.mf", "https://example.com/foo.mf"},
|
||||
|
||||
// Directory with trailing slash - append index.mf
|
||||
{"https://example.com/path/", "https://example.com/path/index.mf"},
|
||||
{"https://example.com/path/", testIndexMF},
|
||||
{"https://example.com/", "https://example.com/index.mf"},
|
||||
|
||||
// Directory without trailing slash - add slash and index.mf
|
||||
{"https://example.com/path", "https://example.com/path/index.mf"},
|
||||
{"https://example.com/path", testIndexMF},
|
||||
{"https://example.com", "https://example.com/index.mf"},
|
||||
|
||||
// With query strings
|
||||
{"https://example.com/path?foo=bar", "https://example.com/path/index.mf?foo=bar"},
|
||||
{
|
||||
"https://example.com/path?foo=bar",
|
||||
"https://example.com/path/index.mf?foo=bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result, err := resolveManifestURL(tt.input)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// The sole caller wraps this error as "invalid URL: %w", so
|
||||
// resolveManifestURL must return url.Parse's error unadorned.
|
||||
t.Run("invalid:control character", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := resolveManifestURL("http://example.com/\x7f")
|
||||
require.ErrorContains(t, err, urlParseControlCharErr)
|
||||
assert.NotContains(t, err.Error(), "failed to parse URL")
|
||||
})
|
||||
}
|
||||
|
||||
// scanToManifest scans sourceFs and returns the serialized manifest bytes.
|
||||
func scanToManifest(t *testing.T, sourceFs afero.Fs) []byte {
|
||||
t.Helper()
|
||||
|
||||
s := mfer.NewScannerWithOptions(&mfer.ScannerOptions{Fs: sourceFs})
|
||||
require.NoError(t, s.EnumerateFS(sourceFs, "/", nil))
|
||||
|
||||
var manifestBuf bytes.Buffer
|
||||
|
||||
require.NoError(t, s.ToManifest(context.Background(), &manifestBuf, nil))
|
||||
|
||||
return manifestBuf.Bytes()
|
||||
}
|
||||
|
||||
// chdirTemp switches the working directory to a fresh temp dir for the
|
||||
// duration of the test and returns its path.
|
||||
func chdirTemp(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
destDir := t.TempDir()
|
||||
|
||||
origDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.Chdir(destDir))
|
||||
t.Cleanup(func() { _ = os.Chdir(origDir) })
|
||||
|
||||
return destDir
|
||||
}
|
||||
|
||||
// fetchTestHandler serves the manifest at /index.mf and the given files
|
||||
// at their paths.
|
||||
func fetchTestHandler(
|
||||
manifestData []byte, testFiles map[string][]byte,
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if path == "/index.mf" {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(manifestData)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Strip leading slash
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
content, exists := testFiles[path]
|
||||
if !exists {
|
||||
http.NotFound(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(content)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchFromHTTP(t *testing.T) {
|
||||
// Create source filesystem with test files
|
||||
sourceFs := afero.NewMemMapFs()
|
||||
@@ -134,51 +234,14 @@ func TestFetchFromHTTP(t *testing.T) {
|
||||
}
|
||||
|
||||
// Generate manifest using scanner
|
||||
opts := &mfer.ScannerOptions{
|
||||
Fs: sourceFs,
|
||||
}
|
||||
s := mfer.NewScannerWithOptions(opts)
|
||||
require.NoError(t, s.EnumerateFS(sourceFs, "/", nil))
|
||||
|
||||
var manifestBuf bytes.Buffer
|
||||
require.NoError(t, s.ToManifest(context.Background(), &manifestBuf, nil))
|
||||
manifestData := manifestBuf.Bytes()
|
||||
manifestData := scanToManifest(t, sourceFs)
|
||||
|
||||
// Create HTTP server that serves the source filesystem
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if path == "/index.mf" {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(manifestData)
|
||||
return
|
||||
}
|
||||
|
||||
// Strip leading slash
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
content, exists := testFiles[path]
|
||||
if !exists {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(content)
|
||||
}))
|
||||
server := httptest.NewServer(fetchTestHandler(manifestData, testFiles))
|
||||
defer server.Close()
|
||||
|
||||
// Create destination directory
|
||||
destDir, err := os.MkdirTemp("", "mfer-fetch-test-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(destDir) }()
|
||||
|
||||
// Change to dest directory for the test
|
||||
origDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chdir(destDir))
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
// Change to a fresh destination directory for the test
|
||||
destDir := chdirTemp(t)
|
||||
|
||||
// Parse the manifest to get file entries
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
|
||||
@@ -189,132 +252,125 @@ func TestFetchFromHTTP(t *testing.T) {
|
||||
|
||||
// Download each file using downloadFile
|
||||
progress := make(chan DownloadProgress, 10)
|
||||
|
||||
go func() {
|
||||
for range progress {
|
||||
// Drain progress channel
|
||||
for p := range progress {
|
||||
_ = p // drain progress channel
|
||||
}
|
||||
}()
|
||||
|
||||
baseURL := server.URL + "/"
|
||||
|
||||
for _, f := range files {
|
||||
localPath, err := sanitizePath(f.Path)
|
||||
localPath, err := sanitizePath(f.GetPath())
|
||||
require.NoError(t, err)
|
||||
|
||||
fileURL := baseURL + f.Path
|
||||
err = downloadFile(fileURL, localPath, f, progress)
|
||||
require.NoError(t, err, "failed to download %s", f.Path)
|
||||
fileURL := baseURL + f.GetPath()
|
||||
err = downloadFile(context.Background(), fileURL, localPath, f, progress)
|
||||
require.NoError(t, err, "failed to download %s", f.GetPath())
|
||||
}
|
||||
|
||||
close(progress)
|
||||
|
||||
// Verify downloaded files match originals
|
||||
for path, expectedContent := range testFiles {
|
||||
downloadedPath := filepath.Join(destDir, path)
|
||||
//nolint:gosec // test-controlled path
|
||||
downloadedContent, err := os.ReadFile(downloadedPath)
|
||||
require.NoError(t, err, "failed to read downloaded file %s", path)
|
||||
assert.Equal(t, expectedContent, downloadedContent, "content mismatch for %s", path)
|
||||
assert.Equal(t, expectedContent, downloadedContent,
|
||||
"content mismatch for %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchHashMismatch(t *testing.T) {
|
||||
// Create source filesystem with a test file
|
||||
sourceFs := afero.NewMemMapFs()
|
||||
originalContent := []byte("Original content")
|
||||
require.NoError(t, afero.WriteFile(sourceFs, "/file.txt", originalContent, 0o644))
|
||||
|
||||
// Generate manifest
|
||||
opts := &mfer.ScannerOptions{Fs: sourceFs}
|
||||
s := mfer.NewScannerWithOptions(opts)
|
||||
require.NoError(t, s.EnumerateFS(sourceFs, "/", nil))
|
||||
// Generate and parse manifest
|
||||
manifestData := scanToManifest(t, sourceFs)
|
||||
|
||||
var manifestBuf bytes.Buffer
|
||||
require.NoError(t, s.ToManifest(context.Background(), &manifestBuf, nil))
|
||||
|
||||
// Parse manifest
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestBuf.Bytes()))
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
|
||||
require.NoError(t, err)
|
||||
|
||||
files := manifest.Files()
|
||||
require.Len(t, files, 1)
|
||||
|
||||
// Create server that serves DIFFERENT content (to trigger hash mismatch)
|
||||
tamperedContent := []byte("Tampered content!")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(tamperedContent)
|
||||
}))
|
||||
|
||||
server := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(tamperedContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create temp directory
|
||||
destDir, err := os.MkdirTemp("", "mfer-fetch-hash-test-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(destDir) }()
|
||||
|
||||
origDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chdir(destDir))
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
// Work in a fresh temp directory
|
||||
chdirTemp(t)
|
||||
|
||||
// Try to download - should fail with hash mismatch
|
||||
err = downloadFile(server.URL+"/file.txt", "file.txt", files[0], nil)
|
||||
assert.Error(t, err)
|
||||
err = downloadFile(context.Background(),
|
||||
server.URL+"/file.txt", testFileTxt, files[0], nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "mismatch")
|
||||
|
||||
// Verify temp file was cleaned up
|
||||
_, err = os.Stat(".file.txt.tmp")
|
||||
assert.True(t, os.IsNotExist(err), "temp file should be cleaned up on hash mismatch")
|
||||
assert.True(t, os.IsNotExist(err),
|
||||
"temp file should be cleaned up on hash mismatch")
|
||||
|
||||
// Verify final file was not created
|
||||
_, err = os.Stat("file.txt")
|
||||
assert.True(t, os.IsNotExist(err), "final file should not exist on hash mismatch")
|
||||
_, err = os.Stat(testFileTxt)
|
||||
assert.True(t, os.IsNotExist(err),
|
||||
"final file should not exist on hash mismatch")
|
||||
}
|
||||
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchSizeMismatch(t *testing.T) {
|
||||
// Create source filesystem with a test file
|
||||
sourceFs := afero.NewMemMapFs()
|
||||
originalContent := []byte("Original content with specific size")
|
||||
require.NoError(t, afero.WriteFile(sourceFs, "/file.txt", originalContent, 0o644))
|
||||
|
||||
// Generate manifest
|
||||
opts := &mfer.ScannerOptions{Fs: sourceFs}
|
||||
s := mfer.NewScannerWithOptions(opts)
|
||||
require.NoError(t, s.EnumerateFS(sourceFs, "/", nil))
|
||||
// Generate and parse manifest
|
||||
manifestData := scanToManifest(t, sourceFs)
|
||||
|
||||
var manifestBuf bytes.Buffer
|
||||
require.NoError(t, s.ToManifest(context.Background(), &manifestBuf, nil))
|
||||
|
||||
// Parse manifest
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestBuf.Bytes()))
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
|
||||
require.NoError(t, err)
|
||||
|
||||
files := manifest.Files()
|
||||
require.Len(t, files, 1)
|
||||
|
||||
// Create server that serves content with wrong size
|
||||
wrongSizeContent := []byte("Short")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(wrongSizeContent)
|
||||
}))
|
||||
|
||||
server := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
_, _ = w.Write(wrongSizeContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create temp directory
|
||||
destDir, err := os.MkdirTemp("", "mfer-fetch-size-test-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(destDir) }()
|
||||
|
||||
origDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chdir(destDir))
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
// Work in a fresh temp directory
|
||||
chdirTemp(t)
|
||||
|
||||
// Try to download - should fail with size mismatch
|
||||
err = downloadFile(server.URL+"/file.txt", "file.txt", files[0], nil)
|
||||
assert.Error(t, err)
|
||||
err = downloadFile(context.Background(),
|
||||
server.URL+"/file.txt", testFileTxt, files[0], nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "size mismatch")
|
||||
|
||||
// Verify temp file was cleaned up
|
||||
_, err = os.Stat(".file.txt.tmp")
|
||||
assert.True(t, os.IsNotExist(err), "temp file should be cleaned up on size mismatch")
|
||||
assert.True(t, os.IsNotExist(err),
|
||||
"temp file should be cleaned up on size mismatch")
|
||||
}
|
||||
|
||||
//nolint:paralleltest // changes the process-global working directory
|
||||
func TestFetchProgress(t *testing.T) {
|
||||
// Create source filesystem with a larger test file
|
||||
sourceFs := afero.NewMemMapFs()
|
||||
@@ -322,53 +378,47 @@ func TestFetchProgress(t *testing.T) {
|
||||
content := bytes.Repeat([]byte("x"), 100*1024) // 100KB
|
||||
require.NoError(t, afero.WriteFile(sourceFs, "/large.txt", content, 0o644))
|
||||
|
||||
// Generate manifest
|
||||
opts := &mfer.ScannerOptions{Fs: sourceFs}
|
||||
s := mfer.NewScannerWithOptions(opts)
|
||||
require.NoError(t, s.EnumerateFS(sourceFs, "/", nil))
|
||||
// Generate and parse manifest
|
||||
manifestData := scanToManifest(t, sourceFs)
|
||||
|
||||
var manifestBuf bytes.Buffer
|
||||
require.NoError(t, s.ToManifest(context.Background(), &manifestBuf, nil))
|
||||
|
||||
// Parse manifest
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestBuf.Bytes()))
|
||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
|
||||
require.NoError(t, err)
|
||||
|
||||
files := manifest.Files()
|
||||
require.Len(t, files, 1)
|
||||
|
||||
// Create server that serves the content
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Length", "102400")
|
||||
// Write in chunks to allow progress reporting
|
||||
reader := bytes.NewReader(content)
|
||||
_, _ = io.Copy(w, reader)
|
||||
}))
|
||||
server := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Length", "102400")
|
||||
// Write in chunks to allow progress reporting
|
||||
reader := bytes.NewReader(content)
|
||||
_, _ = io.Copy(w, reader)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create temp directory
|
||||
destDir, err := os.MkdirTemp("", "mfer-fetch-progress-test-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(destDir) }()
|
||||
|
||||
origDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chdir(destDir))
|
||||
defer func() { _ = os.Chdir(origDir) }()
|
||||
// Work in a fresh temp directory
|
||||
chdirTemp(t)
|
||||
|
||||
// Set up progress channel and collect updates
|
||||
progress := make(chan DownloadProgress, 100)
|
||||
|
||||
var progressUpdates []DownloadProgress
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for p := range progress {
|
||||
progressUpdates = append(progressUpdates, p)
|
||||
}
|
||||
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Download
|
||||
err = downloadFile(server.URL+"/large.txt", "large.txt", files[0], progress)
|
||||
err = downloadFile(context.Background(),
|
||||
server.URL+"/large.txt", "large.txt", files[0], progress)
|
||||
close(progress)
|
||||
<-done
|
||||
|
||||
@@ -380,7 +430,8 @@ func TestFetchProgress(t *testing.T) {
|
||||
// Verify final progress shows complete
|
||||
if len(progressUpdates) > 0 {
|
||||
last := progressUpdates[len(progressUpdates)-1]
|
||||
assert.Equal(t, int64(len(content)), last.BytesRead, "final progress should show all bytes read")
|
||||
assert.Equal(t, int64(len(content)), last.BytesRead,
|
||||
"final progress should show all bytes read")
|
||||
assert.Equal(t, "large.txt", last.Path)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user