Replace anacrolix/upnp with huin/goupnp + custom NAT-PMP (RFC 6886) implementation. NAT-PMP is tried first (faster, more compatible with TP-Link routers), with UPnP-IGD SOAP as fallback. Gateway detection reads /proc/net/route for accuracy. Includes unit tests with mock NAT-PMP server and permanent e2e tests (build tag manual).
177 lines
4.4 KiB
Go
177 lines
4.4 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// parseRangeStart
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestParseRangeStart(t *testing.T) {
|
|
tests := []struct {
|
|
header string
|
|
want int64
|
|
}{
|
|
{"bytes=0-", 0},
|
|
{"bytes=1024-", 1024},
|
|
{"bytes=5000-9999", 5000},
|
|
{"bytes=1048576-", 1048576},
|
|
{"", -1},
|
|
{"invalid", -1},
|
|
{"bytes=", -1},
|
|
{"bytes=-500", -1},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
got := parseRangeStart(tc.header)
|
|
if got != tc.want {
|
|
t.Errorf("parseRangeStart(%q) = %d, want %d", tc.header, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// StreamServer.EstimatedProgress
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestEstimatedProgress_NoFile(t *testing.T) {
|
|
ss := &StreamServer{}
|
|
pos, dur := ss.EstimatedProgress()
|
|
if pos != 0 || dur != 0 {
|
|
t.Errorf("expected (0, 0), got (%d, %d)", pos, dur)
|
|
}
|
|
}
|
|
|
|
func TestEstimatedProgress_HalfWay(t *testing.T) {
|
|
ss := &StreamServer{totalFileSize: 1000}
|
|
ss.maxByteOffset.Store(500)
|
|
|
|
pos, dur := ss.EstimatedProgress()
|
|
if pos != 50 || dur != 100 {
|
|
t.Errorf("expected (50, 100), got (%d, %d)", pos, dur)
|
|
}
|
|
}
|
|
|
|
func TestEstimatedProgress_CapsAt100(t *testing.T) {
|
|
ss := &StreamServer{totalFileSize: 1000}
|
|
ss.maxByteOffset.Store(1500)
|
|
|
|
pos, dur := ss.EstimatedProgress()
|
|
if pos != 100 || dur != 100 {
|
|
t.Errorf("expected (100, 100), got (%d, %d)", pos, dur)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// maxByteOffset only increases (simulated Range tracking)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestMaxByteOffsetNeverRegresses(t *testing.T) {
|
|
ss := &StreamServer{totalFileSize: 10000}
|
|
|
|
offsets := []int64{0, 2000, 5000, 3000, 8000, 4000}
|
|
for _, off := range offsets {
|
|
for {
|
|
cur := ss.maxByteOffset.Load()
|
|
if off <= cur || ss.maxByteOffset.CompareAndSwap(cur, off) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if ss.maxByteOffset.Load() != 8000 {
|
|
t.Errorf("expected 8000, got %d", ss.maxByteOffset.Load())
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End-to-end: real HTTP server with Range requests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestStreamServerRangeTracking(t *testing.T) {
|
|
// Create temp file (10 KB)
|
|
tmpFile := t.TempDir() + "/test.mp4"
|
|
data := make([]byte, 10240)
|
|
for i := range data {
|
|
data[i] = byte(i % 256)
|
|
}
|
|
if err := os.WriteFile(tmpFile, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
srv := NewStreamServerFromDisk(tmpFile, 0)
|
|
srv.disableUPnP = true
|
|
ctx := context.Background()
|
|
url, err := srv.Start(ctx)
|
|
if err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
defer srv.Shutdown(ctx)
|
|
|
|
// 1. Non-range GET — maxByteOffset stays 0
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if srv.maxByteOffset.Load() != 0 {
|
|
t.Errorf("non-range: expected 0, got %d", srv.maxByteOffset.Load())
|
|
}
|
|
|
|
// 2. Range: bytes=5000- → offset 5000
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.Header.Set("Range", "bytes=5000-")
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Range GET: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusPartialContent {
|
|
t.Errorf("expected 206, got %d", resp.StatusCode)
|
|
}
|
|
if srv.maxByteOffset.Load() != 5000 {
|
|
t.Errorf("expected 5000, got %d", srv.maxByteOffset.Load())
|
|
}
|
|
|
|
// 3. Higher offset
|
|
req, _ = http.NewRequest("GET", url, nil)
|
|
req.Header.Set("Range", "bytes=8000-")
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Range GET 2: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if srv.maxByteOffset.Load() != 8000 {
|
|
t.Errorf("expected 8000, got %d", srv.maxByteOffset.Load())
|
|
}
|
|
|
|
// 4. Lower offset does NOT regress
|
|
req, _ = http.NewRequest("GET", url, nil)
|
|
req.Header.Set("Range", "bytes=2000-")
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Range GET 3: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if srv.maxByteOffset.Load() != 8000 {
|
|
t.Errorf("expected still 8000, got %d", srv.maxByteOffset.Load())
|
|
}
|
|
|
|
// 5. Verify progress estimate
|
|
pos, dur := srv.EstimatedProgress()
|
|
// 8000/10240 = 78.1% → 78
|
|
if pos < 78 || pos > 79 {
|
|
t.Errorf("expected pos ~78, got %d", pos)
|
|
}
|
|
if dur != 100 {
|
|
t.Errorf("expected dur=100, got %d", dur)
|
|
}
|
|
}
|