diff --git a/internal/protocol/gopher/gopher_test.go b/internal/protocol/gopher/gopher_test.go index 3167333..bf8af24 100644 --- a/internal/protocol/gopher/gopher_test.go +++ b/internal/protocol/gopher/gopher_test.go @@ -494,3 +494,103 @@ func TestGopherResumeIgnoredWhenWriterSet(t *testing.T) { t.Error("Output file should not be created when Writer is set") } } + +// TestGopherResumeInformation verifies that resume machinery works for the +// informational (type i) response path, which is also routed through +// downloadText. Informational lines are passed through verbatim (no +// metadata strip), so the resume offset matches the raw line length +// including trailing CR/LF. +func TestGopherResumeInformation(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + port := listener.Addr().(*net.TCPAddr).Port + + cancelReady := make(chan struct{}) + connReady := make(chan struct{}) + go func() { + for i := 0; i < 2; i++ { + conn, lerr := listener.Accept() + if lerr != nil { + return + } + r := bufio.NewReader(conn) + r.ReadString('\n') + if i == 0 { + // First half: send two info lines, then wait + // for the client to cancel before closing. + conn.Write([]byte("iFirst line.\r\n")) + conn.Write([]byte("iSecond line.\r\n")) + <-cancelReady + conn.Close() + connReady <- struct{}{} + } else { + // Second half: full body. + conn.Write([]byte("iFirst line.\r\n")) + conn.Write([]byte("iSecond line.\r\n")) + conn.Write([]byte(".\r\n")) + conn.Close() + } + } + }() + + tmp := t.TempDir() + outPath := tmp + "/info.txt" + + proto := NewProtocol() + u, _ := url.Parse("gopher://127.0.0.1:" + strconv.Itoa(port) + "/info") + + progress := make(chan int64, 1) + ctx1, cancel1 := context.WithCancel(context.Background()) + errCh := make(chan error, 1) + go func() { + _, dlErr := proto.Download(ctx1, &core.DownloadRequest{ + URL: u, + Output: outPath, + ProgressCallback: func(current, total int64, speed float64) { + select { + case progress <- current: + default: + } + }, + }) + errCh <- dlErr + }() + <-progress + cancel1() + cancelReady <- struct{}{} + <-connReady + if dlErr := <-errCh; dlErr == nil { + t.Fatal("expected error from cancelled context") + } + + // Resume should append the missing bytes. For informational lines + // we do not strip metadata, so the output preserves the server CR/LF. + result, resErr := proto.Download(context.Background(), &core.DownloadRequest{ + URL: u, + Output: outPath, + Resume: true, + }) + if resErr != nil { + t.Fatalf("resume download failed: %v", resErr) + } + if !result.Resumed { + t.Error("expected Resumed=true") + } + if result.ContentType != "text/plain" { + t.Errorf("ContentType = %q, want text/plain", result.ContentType) + } + + data, _ := os.ReadFile(outPath) + // Informational lines are passed through verbatim: the 'i' item-type + // prefix and CR/LF framing from the server survive into the output. + want := "iFirst line.\r\niSecond line.\r\n" + if string(data) != want { + t.Errorf("content = %q, want %q", string(data), want) + } + if output.FileExists(outPath + output.ResumeMetaFileSuffix) { + t.Error("expected .goget.meta to be deleted after successful resume") + } +}