Files
netbird/client/internal/filedrop/progress_test.go
Zoltan Papp 090f9d8e80 [client] Report file drop progress, and stop a withdrawn transfer
The receiver reported progress once per file, after the whole body had
been staged: spool.Write drains its reader before returning, so a large
file sat at nothing until it jumped to done. It now stages through a
reader that reports as the bytes land, the same way the sender already
did, and both sides thin their reports to one per percent and one per
200ms so a fast transfer cannot flood the history or the UI.

Progress also becomes an event of its own. It was left out because a
report per byte would have been unusable; thinned, it costs a handful of
events a second and spares every UI a poll. The daemon's event bridge
ignores the new kind, so nothing is published where a notification would
be noise.

Withdrawing consent mid-transfer did nothing. Cancel routed through
Decide, which only moves an offer out of Pending, so an accepted offer
kept its decision, kept its spool, and kept being uploaded into: the
receiver's list went quiet while the sender ran to completion. Revoke
takes an offer back whatever it has already answered, the staging reader
gives up as soon as consent is gone, and the sender stops rather than
retrying a refusal three times over. A transfer withdrawn this way reads
as declined on both ends, which is what it is, rather than as a failure.
2026-08-18 14:41:11 +02:00

109 lines
3.0 KiB
Go

package filedrop
import (
"bytes"
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProgressReader_ReportsEveryPercentAtMost(t *testing.T) {
var reports []int64
// Read one byte at a time out of a 1000 byte payload: without thinning that
// would be 1000 reports for 100 percent worth of progress.
reader := &progressReader{
r: bytes.NewReader(make([]byte, 1000)),
total: 1000,
report: func(sent int64) { reports = append(reports, sent) },
}
buf := make([]byte, 1)
for {
_, err := reader.Read(buf)
if err == io.EOF {
break
}
require.NoError(t, err)
}
assert.LessOrEqual(t, len(reports), 100, "One report per percent at most")
assert.Equal(t, int64(1000), reports[len(reports)-1], "The finished item must be reported")
}
func TestProgressReader_HoldsBackFasterThanTheInterval(t *testing.T) {
reader := &progressReader{total: 1000, report: func(int64) {}}
start := time.Now()
reader.sent = 10
require.True(t, reader.due(start), "The first percent goes through")
reader.sent = 20
assert.False(t, reader.due(start.Add(progressInterval/2)),
"A percent arriving inside the interval waits")
assert.True(t, reader.due(start.Add(progressInterval)),
"The same percent goes through once the interval has passed")
}
func TestProgressReader_ReportsTheLastByteWhateverTheInterval(t *testing.T) {
reader := &progressReader{total: 1000, report: func(int64) {}}
start := time.Now()
reader.sent = 500
require.True(t, reader.due(start))
reader.sent = 1000
assert.True(t, reader.due(start.Add(time.Millisecond)),
"A finished item is never held back by the interval")
}
func TestProgressReader_ReportsEveryReadWithoutASize(t *testing.T) {
// A payload of unknown size has no percent to thin against, so every read
// is worth reporting rather than none.
reader := &progressReader{total: 0, report: func(int64) {}}
reader.sent = 1
assert.True(t, reader.due(time.Now()))
}
func TestAcceptedReader_StopsOnceTheOfferIsWithdrawn(t *testing.T) {
accepted := true
reader := &acceptedReader{
r: bytes.NewReader(make([]byte, 1024)),
accepted: func() bool { return accepted },
// Backdated so the very first read checks rather than waiting out the
// interval.
last: time.Now().Add(-progressInterval),
}
buf := make([]byte, 128)
n, err := reader.Read(buf)
require.NoError(t, err)
assert.Equal(t, 128, n, "An accepted offer reads normally")
accepted = false
reader.last = time.Now().Add(-progressInterval)
_, err = reader.Read(buf)
assert.ErrorIs(t, err, ErrNotAccepted, "A withdrawn offer stops the copy")
}
func TestAcceptedReader_ChecksNoMoreOftenThanTheInterval(t *testing.T) {
checks := 0
reader := &acceptedReader{
r: bytes.NewReader(make([]byte, 4096)),
accepted: func() bool { checks++; return true },
}
buf := make([]byte, 1)
for range 32 {
_, err := reader.Read(buf)
require.NoError(t, err)
}
assert.LessOrEqual(t, checks, 1, "The offer store is not locked once per read")
}