3362 lines
129 KiB
Go
3362 lines
129 KiB
Go
package helpers
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
||
)
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// parsePartSize / formatByteSize
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageParsePartSize(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
want int64
|
||
wantErr bool
|
||
}{
|
||
{"16MB", 16 << 20, false},
|
||
{"16M", 16 << 20, false},
|
||
{"1GB", 1 << 30, false},
|
||
{"1g", 1 << 30, false},
|
||
{"1024KB", 1 << 20, false},
|
||
{"33554432", 32 << 20, false}, // 纯数字按字节
|
||
{" 8mb ", 8 << 20, false},
|
||
{"", 0, true},
|
||
{"abc", 0, true},
|
||
{"-4MB", 0, true},
|
||
{"0", 0, true},
|
||
{"512KB", 0, true}, // 低于 1MB 下限
|
||
{"2048MB", 0, true}, // 高于 1GB 上限
|
||
{"1.5MB", 0, true}, // 不支持小数
|
||
}
|
||
for _, c := range cases {
|
||
got, err := parsePartSize(c.in)
|
||
if c.wantErr {
|
||
if err == nil {
|
||
t.Errorf("parsePartSize(%q) 应报错, got %d", c.in, got)
|
||
}
|
||
continue
|
||
}
|
||
if err != nil {
|
||
t.Errorf("parsePartSize(%q) unexpected error: %v", c.in, err)
|
||
continue
|
||
}
|
||
if got != c.want {
|
||
t.Errorf("parsePartSize(%q) = %d, want %d", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatByteSize(t *testing.T) {
|
||
cases := map[int64]string{
|
||
16 << 20: "16MB",
|
||
1 << 30: "1GB",
|
||
4 << 10: "4KB",
|
||
123: "123B",
|
||
}
|
||
for in, want := range cases {
|
||
if got := formatByteSize(in); got != want {
|
||
t.Errorf("formatByteSize(%d) = %q, want %q", in, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// splitDownloadParts
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageSplitDownloadParts(t *testing.T) {
|
||
// 整除
|
||
parts := splitDownloadParts(32, 16)
|
||
if len(parts) != 2 || parts[0].offset != 0 || parts[0].length != 16 || parts[1].offset != 16 || parts[1].length != 16 {
|
||
t.Errorf("整除切分错误: %+v", parts)
|
||
}
|
||
// 有余量:末片较短
|
||
parts = splitDownloadParts(33, 16)
|
||
if len(parts) != 3 || parts[2].offset != 32 || parts[2].length != 1 {
|
||
t.Errorf("余量切分错误: %+v", parts)
|
||
}
|
||
// 单片
|
||
parts = splitDownloadParts(10, 16)
|
||
if len(parts) != 1 || parts[0].length != 10 {
|
||
t.Errorf("单片切分错误: %+v", parts)
|
||
}
|
||
// 非法输入
|
||
if splitDownloadParts(0, 16) != nil || splitDownloadParts(16, 0) != nil {
|
||
t.Error("非法输入应返回 nil")
|
||
}
|
||
// 覆盖完整性
|
||
parts = splitDownloadParts(100, 7)
|
||
var sum int64
|
||
for i, p := range parts {
|
||
if p.index != i {
|
||
t.Errorf("index 不连续: %+v", p)
|
||
}
|
||
sum += p.length
|
||
}
|
||
if sum != 100 {
|
||
t.Errorf("分片总长 %d != 100", sum)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// checkpoint 读写与恢复
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageCheckpointSaveLoadRoundtrip(t *testing.T) {
|
||
dir := t.TempDir()
|
||
metaPath := filepath.Join(dir, "f.bin.dwspart.meta")
|
||
fp := driveDownloadFingerprint("test-node-1", 0, 100, "")
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion,
|
||
Fingerprint: fp,
|
||
TotalSize: 100,
|
||
PartSize: 30,
|
||
Completed: []bool{true, false, true, false},
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatalf("save: %v", err)
|
||
}
|
||
got := loadDriveDownloadCheckpoint(metaPath, fp, 100, 30, 4)
|
||
if got == nil {
|
||
t.Fatal("roundtrip 应成功加载")
|
||
}
|
||
if !got.Completed[0] || got.Completed[1] || !got.Completed[2] {
|
||
t.Errorf("Completed 位图不符: %v", got.Completed)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageCheckpointLoadRejectsMismatch(t *testing.T) {
|
||
dir := t.TempDir()
|
||
metaPath := filepath.Join(dir, "f.meta")
|
||
fp := driveDownloadFingerprint("test-node-2", 0, 100, "")
|
||
cp := &driveDownloadCheckpoint{Version: driveCheckpointVersion, Fingerprint: fp, TotalSize: 100, PartSize: 30, Completed: make([]bool, 4)}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatalf("save: %v", err)
|
||
}
|
||
if loadDriveDownloadCheckpoint(metaPath, "other-fp", 100, 30, 4) != nil {
|
||
t.Error("指纹不符应作废")
|
||
}
|
||
if loadDriveDownloadCheckpoint(metaPath, fp, 200, 30, 4) != nil {
|
||
t.Error("总长不符应作废")
|
||
}
|
||
if loadDriveDownloadCheckpoint(metaPath, fp, 100, 40, 4) != nil {
|
||
t.Error("分片大小不符应作废")
|
||
}
|
||
if loadDriveDownloadCheckpoint(metaPath, fp, 100, 30, 5) != nil {
|
||
t.Error("分片数不符应作废")
|
||
}
|
||
if loadDriveDownloadCheckpoint(filepath.Join(dir, "missing"), fp, 100, 30, 4) != nil {
|
||
t.Error("文件缺失应返回 nil")
|
||
}
|
||
// 损坏 JSON
|
||
if err := os.WriteFile(metaPath, []byte("{bad"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if loadDriveDownloadCheckpoint(metaPath, fp, 100, 30, 4) != nil {
|
||
t.Error("损坏 JSON 应返回 nil")
|
||
}
|
||
}
|
||
|
||
// 指纹基于 nodeID + version + totalSize,不同 nodeID / version / size 产生不同指纹。
|
||
func TestCrossPlatformCoverageFingerprintNodeIDBased(t *testing.T) {
|
||
a := driveDownloadFingerprint("node-aaa", 0, 500, "")
|
||
b := driveDownloadFingerprint("node-aaa", 0, 500, "")
|
||
if a != b {
|
||
t.Error("相同参数应产生相同指纹")
|
||
}
|
||
c := driveDownloadFingerprint("node-bbb", 0, 500, "")
|
||
if a == c {
|
||
t.Error("不同 nodeID 应产生不同指纹")
|
||
}
|
||
d := driveDownloadFingerprint("node-aaa", 0, 501, "")
|
||
if a == d {
|
||
t.Error("不同 totalSize 应产生不同指纹")
|
||
}
|
||
e := driveDownloadFingerprint("node-aaa", 2, 500, "")
|
||
if a == e {
|
||
t.Error("不同 version 应产生不同指纹")
|
||
}
|
||
}
|
||
|
||
// checkpoint 指纹碰撞防护:相同输出路径、相同大小、不同 nodeID 不复用 checkpoint。
|
||
func TestCrossPlatformCoverageCheckpointNotReusedDifferentNodeID(t *testing.T) {
|
||
dir := t.TempDir()
|
||
metaPath := filepath.Join(dir, "f.meta")
|
||
const totalSize int64 = 1000
|
||
const partSize int64 = 300
|
||
partCount := int((totalSize + partSize - 1) / partSize)
|
||
|
||
// 用 nodeA 写入一个 checkpoint
|
||
fpA := driveDownloadFingerprint("node-A", 0, totalSize, "")
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion,
|
||
Fingerprint: fpA,
|
||
TotalSize: totalSize,
|
||
PartSize: partSize,
|
||
Completed: make([]bool, partCount),
|
||
}
|
||
cp.Completed[0] = true
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 用 nodeB 尝试加载,应返回 nil(不复用)
|
||
fpB := driveDownloadFingerprint("node-B", 0, totalSize, "")
|
||
if loadDriveDownloadCheckpoint(metaPath, fpB, totalSize, partSize, partCount) != nil {
|
||
t.Error("不同 nodeID 同大小不应复用 checkpoint")
|
||
}
|
||
}
|
||
|
||
// checkpoint 指纹碰撞防护:相同 nodeID、不同大小不复用 checkpoint。
|
||
func TestCrossPlatformCoverageCheckpointNotReusedDifferentSize(t *testing.T) {
|
||
dir := t.TempDir()
|
||
metaPath := filepath.Join(dir, "f.meta")
|
||
const partSize int64 = 300
|
||
|
||
// 用 size=1000 写入 checkpoint
|
||
fp1000 := driveDownloadFingerprint("same-node", 0, 1000, "")
|
||
pc := int((int64(1000) + partSize - 1) / partSize)
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion,
|
||
Fingerprint: fp1000,
|
||
TotalSize: 1000,
|
||
PartSize: partSize,
|
||
Completed: make([]bool, pc),
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 用 size=2000 尝试加载,应返回 nil
|
||
fp2000 := driveDownloadFingerprint("same-node", 0, 2000, "")
|
||
pc2 := int((int64(2000) + partSize - 1) / partSize)
|
||
if loadDriveDownloadCheckpoint(metaPath, fp2000, 2000, partSize, pc2) != nil {
|
||
t.Error("相同 nodeID 不同大小不应复用 checkpoint")
|
||
}
|
||
}
|
||
|
||
// checkpoint 正常续传:相同 nodeID + 相同大小复用 checkpoint。
|
||
func TestCrossPlatformCoverageCheckpointReusedSameNodeIDAndSize(t *testing.T) {
|
||
dir := t.TempDir()
|
||
metaPath := filepath.Join(dir, "f.meta")
|
||
const totalSize int64 = 1000
|
||
const partSize int64 = 300
|
||
partCount := int((totalSize + partSize - 1) / partSize)
|
||
|
||
fp := driveDownloadFingerprint("resume-node", 0, totalSize, "")
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion,
|
||
Fingerprint: fp,
|
||
TotalSize: totalSize,
|
||
PartSize: partSize,
|
||
Completed: make([]bool, partCount),
|
||
}
|
||
cp.Completed[0] = true
|
||
cp.Completed[2] = true
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 用相同 nodeID + 相同大小加载,应成功复用
|
||
got := loadDriveDownloadCheckpoint(metaPath, fp, totalSize, partSize, partCount)
|
||
if got == nil {
|
||
t.Fatal("相同 nodeID + 相同大小应复用 checkpoint")
|
||
}
|
||
if !got.Completed[0] || got.Completed[1] || !got.Completed[2] || got.Completed[3] {
|
||
t.Errorf("Completed 位图不符: %v", got.Completed)
|
||
}
|
||
}
|
||
|
||
// checkpoint 指纹碰撞防护:同 nodeID、同大小、version=0 但不同 resourceURL 不复用 checkpoint。
|
||
// 模拟最新版下载场景:文件被相同大小内容覆盖后 URL path 变化,旧 checkpoint 应作废。
|
||
func TestCrossPlatformCoverageCheckpointNotReusedDifferentResourceURL(t *testing.T) {
|
||
dir := t.TempDir()
|
||
metaPath := filepath.Join(dir, "f.meta")
|
||
const totalSize int64 = 1000
|
||
const partSize int64 = 300
|
||
partCount := int((totalSize + partSize - 1) / partSize)
|
||
|
||
// 模拟首次下载时的资源 URL(旧存储位置)
|
||
oldURL := "https://storage.example.com/v1/files/abc123/content?token=xxx"
|
||
fpOld := driveDownloadFingerprint("same-node", 0, totalSize, oldURL)
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion,
|
||
Fingerprint: fpOld,
|
||
TotalSize: totalSize,
|
||
PartSize: partSize,
|
||
Completed: make([]bool, partCount),
|
||
}
|
||
cp.Completed[0] = true
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 文件被覆盖后获得新的资源 URL(新存储位置,path 不同)
|
||
newURL := "https://storage.example.com/v1/files/def456/content?token=yyy"
|
||
fpNew := driveDownloadFingerprint("same-node", 0, totalSize, newURL)
|
||
|
||
// 新旧指纹应不同
|
||
if fpOld == fpNew {
|
||
t.Fatal("不同 resourceURL 应产生不同指纹")
|
||
}
|
||
|
||
// 用新指纹加载旧 checkpoint 应返回 nil(不复用)
|
||
if loadDriveDownloadCheckpoint(metaPath, fpNew, totalSize, partSize, partCount) != nil {
|
||
t.Error("同 nodeID、同大小、version=0 但不同 resourceURL 不应复用 checkpoint")
|
||
}
|
||
}
|
||
|
||
// 验证 resourceURL 为空时的安全降级:不影响其他字段的指纹计算。
|
||
func TestCrossPlatformCoverageFingerprintEmptyResourceURLFallback(t *testing.T) {
|
||
// 空 URL 应产生确定性指纹
|
||
a := driveDownloadFingerprint("node-x", 0, 500, "")
|
||
b := driveDownloadFingerprint("node-x", 0, 500, "")
|
||
if a != b {
|
||
t.Error("空 resourceURL 时相同参数应产生相同指纹")
|
||
}
|
||
|
||
// 空 URL 与有 URL 的指纹应不同
|
||
c := driveDownloadFingerprint("node-x", 0, 500, "https://example.com/path/to/file")
|
||
if a == c {
|
||
t.Error("空 URL 与有 URL 应产生不同指纹")
|
||
}
|
||
|
||
// version=0 时,仅 query 不同的 URL 应产生不同指纹(中心协议区分实际版本)
|
||
d := driveDownloadFingerprint("node-x", 0, 500, "https://example.com/path/to/file?token=aaa")
|
||
e := driveDownloadFingerprint("node-x", 0, 500, "https://example.com/path/to/file?token=bbb")
|
||
if d == e {
|
||
t.Error("version=0 时仅 query 不同应产生不同指纹")
|
||
}
|
||
|
||
// version>0 时,仅 query 不同的 URL 应产生相同指纹(只取 path)
|
||
f := driveDownloadFingerprint("node-x", 3, 500, "https://example.com/path/to/file?token=aaa")
|
||
g := driveDownloadFingerprint("node-x", 3, 500, "https://example.com/path/to/file?token=bbb")
|
||
if f != g {
|
||
t.Error("version>0 时仅 query 不同应产生相同指纹(只取 path)")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// parseContentRangeTotal / parseDownloadFileSize / parseDriveUploadType
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageParseContentRangeTotal(t *testing.T) {
|
||
if n, err := parseContentRangeTotal("bytes 0-0/12345"); err != nil || n != 12345 {
|
||
t.Errorf("got %d, %v", n, err)
|
||
}
|
||
for _, bad := range []string{"", "bytes 0-0/*", "bytes 0-0/", "bytes 0-0/abc", "12345"} {
|
||
if _, err := parseContentRangeTotal(bad); err == nil {
|
||
t.Errorf("%q 应报错", bad)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageParseContentRange(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
wantStart, wantEnd, wantTotal int64
|
||
wantErr bool
|
||
}{
|
||
{"bytes 0-1048575/104857600", 0, 1048575, 104857600, false},
|
||
{"bytes 500-999/2000", 500, 999, 2000, false},
|
||
{"bytes 0-0/1", 0, 0, 1, false},
|
||
{"bytes 100-199/*", 100, 199, -1, false}, // total 未知
|
||
{"", 0, 0, 0, true}, // 空串
|
||
{"0-100/200", 0, 0, 0, true}, // 无 "bytes " 前缀
|
||
{"bytes 0-100", 0, 0, 0, true}, // 缺少 /total
|
||
{"bytes abc-100/200", 0, 0, 0, true}, // start 非数字
|
||
{"bytes 0-abc/200", 0, 0, 0, true}, // end 非数字
|
||
{"bytes 0-100/abc", 0, 0, 0, true}, // total 非数字
|
||
{"bytes 100-50/200", 0, 0, 0, true}, // end < start
|
||
}
|
||
for _, c := range cases {
|
||
start, end, total, err := parseContentRange(c.in)
|
||
if c.wantErr {
|
||
if err == nil {
|
||
t.Errorf("parseContentRange(%q) 应报错", c.in)
|
||
}
|
||
continue
|
||
}
|
||
if err != nil {
|
||
t.Errorf("parseContentRange(%q) unexpected error: %v", c.in, err)
|
||
continue
|
||
}
|
||
if start != c.wantStart || end != c.wantEnd || total != c.wantTotal {
|
||
t.Errorf("parseContentRange(%q) = (%d,%d,%d), want (%d,%d,%d)",
|
||
c.in, start, end, total, c.wantStart, c.wantEnd, c.wantTotal)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageParseDownloadFileSize(t *testing.T) {
|
||
if n := parseDownloadFileSize(`{"result":{"fileSize":1048576,"downloadUrl":"u"}}`); n != 1048576 {
|
||
t.Errorf("number: got %d", n)
|
||
}
|
||
if n := parseDownloadFileSize(`{"fileSize":"2048"}`); n != 2048 {
|
||
t.Errorf("string: got %d", n)
|
||
}
|
||
if n := parseDownloadFileSize(`{"result":{}}`); n != 0 {
|
||
t.Errorf("missing: got %d", n)
|
||
}
|
||
if n := parseDownloadFileSize("not json"); n != 0 {
|
||
t.Errorf("invalid: got %d", n)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageParseDownloadFileVersion(t *testing.T) {
|
||
// 数值类型
|
||
if n := parseDownloadFileVersion(`{"result":{"version":3,"downloadUrl":"u"}}`); n != 3 {
|
||
t.Errorf("number: got %d, want 3", n)
|
||
}
|
||
// 字符串类型
|
||
if n := parseDownloadFileVersion(`{"version":"7"}`); n != 7 {
|
||
t.Errorf("string: got %d, want 7", n)
|
||
}
|
||
// 缺失字段
|
||
if n := parseDownloadFileVersion(`{"result":{}}`); n != 0 {
|
||
t.Errorf("missing: got %d, want 0", n)
|
||
}
|
||
// 非法 JSON
|
||
if n := parseDownloadFileVersion("not json"); n != 0 {
|
||
t.Errorf("invalid json: got %d, want 0", n)
|
||
}
|
||
// 无 result 包裹的数值
|
||
if n := parseDownloadFileVersion(`{"version":12}`); n != 12 {
|
||
t.Errorf("top-level number: got %d, want 12", n)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageParseDriveUploadType(t *testing.T) {
|
||
if got := parseDriveUploadType(`{"result":{"uploadType":"httpToCenterWithToken","resourceUrl":"u"}}`); got != uploadTypeCenterToken {
|
||
t.Errorf("got %q", got)
|
||
}
|
||
if got := parseDriveUploadType(`{"resourceUrl":"u"}`); got != "" {
|
||
t.Errorf("存量返回无 uploadType 应为空串, got %q", got)
|
||
}
|
||
if got := parseDriveUploadType("not json"); got != "" {
|
||
t.Errorf("invalid json 应为空串, got %q", got)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// isAuthStatusError / decorateUploadSizeError
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageIsAuthStatusError(t *testing.T) {
|
||
if !isAuthStatusError(&httpStatusError{StatusCode: 401}) || !isAuthStatusError(&httpStatusError{StatusCode: 403}) {
|
||
t.Error("typed 401/403 应命中")
|
||
}
|
||
if isAuthStatusError(&httpStatusError{StatusCode: 500}) {
|
||
t.Error("typed 500 不应命中")
|
||
}
|
||
if !isAuthStatusError(fmt.Errorf("OSS upload failed: %w", &httpStatusError{StatusCode: 403, Body: "x"})) {
|
||
t.Error("包装后的 typed 错误应命中")
|
||
}
|
||
if !isAuthStatusError(fmt.Errorf("HTTP 401: expired")) {
|
||
t.Error("字符串形态应命中(测试注入兼容)")
|
||
}
|
||
if isAuthStatusError(nil) || isAuthStatusError(fmt.Errorf("HTTP 404: not found")) {
|
||
t.Error("nil/404 不应命中")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDecorateUploadSizeError(t *testing.T) {
|
||
// 413 → 补充可读提示
|
||
err := decorateUploadSizeError(&httpStatusError{StatusCode: 413, Body: "too large"}, "")
|
||
if !strings.Contains(err.Error(), "提示") {
|
||
t.Errorf("413 应补充提示: %v", err)
|
||
}
|
||
// 中心协议 + 超限语义 body
|
||
err = decorateUploadSizeError(&httpStatusError{StatusCode: 400, Body: "file size exceed limit"}, uploadTypeCenterToken)
|
||
if !strings.Contains(err.Error(), "提示") {
|
||
t.Errorf("中心协议超限应补充提示: %v", err)
|
||
}
|
||
// 普通错误原样返回
|
||
orig := &httpStatusError{StatusCode: 500, Body: "internal"}
|
||
if got := decorateUploadSizeError(orig, uploadTypeCenterToken); got.Error() != orig.Error() {
|
||
t.Errorf("普通错误不应装饰: %v", got)
|
||
}
|
||
nonHTTP := fmt.Errorf("dial timeout")
|
||
if got := decorateUploadSizeError(nonHTTP, ""); got != nonHTTP {
|
||
t.Errorf("非 HTTP 错误不应装饰: %v", got)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// driveTransferDownload:整流 / 分片 / 回退 / 断点续传
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
// rangeTestServer 支持 Range 的测试服务端。
|
||
func rangeTestServer(t *testing.T, content []byte, requireToken string, tokenGen *atomic.Int32) *httptest.Server {
|
||
t.Helper()
|
||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if requireToken != "" {
|
||
want := requireToken
|
||
if tokenGen != nil {
|
||
want = fmt.Sprintf("%s-%d", requireToken, tokenGen.Load())
|
||
}
|
||
if r.Header.Get("dentry-token") != want {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "token expired")
|
||
return
|
||
}
|
||
}
|
||
rng := r.Header.Get("Range")
|
||
if rng == "" {
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write(content)
|
||
return
|
||
}
|
||
var start, end int64
|
||
if _, err := fmt.Sscanf(rng, "bytes=%d-%d", &start, &end); err != nil || start > end || start >= int64(len(content)) {
|
||
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
|
||
return
|
||
}
|
||
if end >= int64(len(content)) {
|
||
end = int64(len(content)) - 1
|
||
}
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
}
|
||
|
||
func makeTestContent(n int) []byte {
|
||
buf := make([]byte, n)
|
||
for i := range buf {
|
||
buf[i] = byte(i % 251)
|
||
}
|
||
return buf
|
||
}
|
||
|
||
func verifyFile(t *testing.T, path string, want []byte) {
|
||
t.Helper()
|
||
got, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatalf("读取产物失败: %v", err)
|
||
}
|
||
if len(got) != len(want) {
|
||
t.Fatalf("产物长度 %d != %d", len(got), len(want))
|
||
}
|
||
for i := range got {
|
||
if got[i] != want[i] {
|
||
t.Fatalf("产物内容第 %d 字节不符", i)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 小分片选项:partSize 用引擎内部值绕过 flag 校验(单测直接构造 options)。
|
||
func smallPartOpts(partSize int64, parallel int) driveDownloadOptions {
|
||
return driveDownloadOptions{partSize: partSize, parallel: parallel, resume: true}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferDownload_RangedParts(t *testing.T) {
|
||
content := makeTestContent(1000)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
dest := filepath.Join(t.TempDir(), "out.bin")
|
||
|
||
// knownSize=1000 ≥ 2×300 → 分片
|
||
opts := smallPartOpts(300, 3)
|
||
opts.knownSize = 1000
|
||
if err := driveTransferDownload(context.Background(), nil, srv.URL, nil, dest, opts); err != nil {
|
||
t.Fatalf("分片下载失败: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
if _, err := os.Stat(dest + drivePartFileSuffix); !os.IsNotExist(err) {
|
||
t.Error("完成后应清理 .dwspart")
|
||
}
|
||
if _, err := os.Stat(dest + drivePartMetaSuffix); !os.IsNotExist(err) {
|
||
t.Error("完成后应清理 checkpoint")
|
||
}
|
||
}
|
||
|
||
// knownSize 小于阈值 → 直接整流(走可注入 httpGetFile)。
|
||
func TestCrossPlatformCoverageDriveTransferDownload_SmallFileSingleStream(t *testing.T) {
|
||
var calls atomic.Int32
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
calls.Add(1)
|
||
return os.WriteFile(destPath, []byte("small"), 0o644)
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
|
||
dest := filepath.Join(t.TempDir(), "small.bin")
|
||
opts := smallPartOpts(1<<20, 4)
|
||
opts.knownSize = 100 // < 2MB 阈值
|
||
if err := driveTransferDownload(context.Background(), nil, "https://x.example.com/f", nil, dest, opts); err != nil {
|
||
t.Fatalf("整流下载失败: %v", err)
|
||
}
|
||
if calls.Load() != 1 {
|
||
t.Errorf("应恰好调用一次 httpGetFile, got %d", calls.Load())
|
||
}
|
||
verifyFile(t, dest, []byte("small"))
|
||
}
|
||
|
||
// 整流 401 → 重取凭证重试一次。
|
||
func TestCrossPlatformCoverageDownloadSingleWithAuthRetry(t *testing.T) {
|
||
var calls atomic.Int32
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
if calls.Add(1) == 1 {
|
||
return &httpStatusError{StatusCode: 401, Body: "expired"}
|
||
}
|
||
if url != "https://new.example.com/f" || headers["dentry-token"] != "new-token" {
|
||
return fmt.Errorf("重试应使用新凭证, got url=%s headers=%v", url, headers)
|
||
}
|
||
return os.WriteFile(destPath, []byte("ok"), 0o644)
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
|
||
fetched := false
|
||
fetch := func(ctx context.Context) (string, map[string]string, int, error) {
|
||
fetched = true
|
||
return "https://new.example.com/f", map[string]string{"dentry-token": "new-token"}, 0, nil
|
||
}
|
||
dest := filepath.Join(t.TempDir(), "auth.bin")
|
||
if err := downloadSingleWithAuthRetry(context.Background(), fetch, "https://old.example.com/f", nil, dest); err != nil {
|
||
t.Fatalf("401 重试后应成功: %v", err)
|
||
}
|
||
if !fetched || calls.Load() != 2 {
|
||
t.Errorf("fetched=%v calls=%d", fetched, calls.Load())
|
||
}
|
||
verifyFile(t, dest, []byte("ok"))
|
||
}
|
||
|
||
// 整流重取凭证后仍失败 → 走既有错误路径(返回错误)。
|
||
func TestCrossPlatformCoverageDownloadSingleWithAuthRetry_StillFails(t *testing.T) {
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
return &httpStatusError{StatusCode: 403, Body: "denied"}
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
fetch := func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "https://new.example.com/f", nil, 0, nil
|
||
}
|
||
err := downloadSingleWithAuthRetry(context.Background(), fetch, "https://old.example.com/f", nil, filepath.Join(t.TempDir(), "x"))
|
||
if err == nil || !isAuthStatusError(err) {
|
||
t.Fatalf("应返回原始鉴权错误: %v", err)
|
||
}
|
||
}
|
||
|
||
// 服务端不支持 Range(探测返回 200)→ 自动回退整流。
|
||
func TestCrossPlatformCoverageDriveTransferDownload_FallbackWhenNoRangeSupport(t *testing.T) {
|
||
content := makeTestContent(900)
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// 忽略 Range 头,始终 200 全量
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write(content)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "fallback.bin")
|
||
opts := smallPartOpts(300, 4)
|
||
opts.knownSize = 900 // ≥ 阈值,进入探测
|
||
if err := driveTransferDownload(context.Background(), nil, srv.URL, nil, dest, opts); err != nil {
|
||
t.Fatalf("回退整流失败: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
}
|
||
|
||
// Content-Range 校验:正常匹配、区间错位、header 缺失
|
||
func TestCrossPlatformCoverageFetchRangeInto_ContentRangeValidation(t *testing.T) {
|
||
content := makeTestContent(1000)
|
||
|
||
// 正常情况:Content-Range 区间匹配
|
||
t.Run("match", func(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "cr-match-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(1000); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 1, offset: 300, length: 300}
|
||
if err := fetchRangeInto(context.Background(), srv.URL, nil, f, part, 0); err != nil {
|
||
t.Fatalf("正常匹配不应报错: %v", err)
|
||
}
|
||
})
|
||
|
||
// Content-Range 区间错位(start 不匹配)→ 返回错误
|
||
t.Run("mismatch_start", func(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// 请求 300-599,但返回假装的 0-299
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes 0-299/%d", len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[0:300])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "cr-mismatch-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(1000); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 1, offset: 300, length: 300}
|
||
err = fetchRangeInto(context.Background(), srv.URL, nil, f, part, 0)
|
||
if err == nil {
|
||
t.Fatal("Content-Range 错位应返回错误")
|
||
}
|
||
if !strings.Contains(err.Error(), "不匹配") {
|
||
t.Errorf("错误信息应包含'不匹配': %v", err)
|
||
}
|
||
})
|
||
|
||
// Content-Range header 缺失 → 强制报错,拒绝无法验证偏移一致性的分片
|
||
t.Run("missing_header", func(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
// 不设置 Content-Range header
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "cr-missing-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(1000); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 0, offset: 0, length: 300}
|
||
err = fetchRangeInto(context.Background(), srv.URL, nil, f, part, 0)
|
||
if err == nil {
|
||
t.Fatal("Content-Range 缺失应返回错误")
|
||
}
|
||
if !strings.Contains(err.Error(), "缺少 Content-Range") {
|
||
t.Errorf("错误信息应包含'缺少 Content-Range': %v", err)
|
||
}
|
||
})
|
||
|
||
// Content-Range 格式异常 → 返回错误
|
||
t.Run("malformed_header", func(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "invalid-format")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[0:300])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "cr-malformed-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(1000); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 0, offset: 0, length: 300}
|
||
err = fetchRangeInto(context.Background(), srv.URL, nil, f, part, 0)
|
||
if err == nil {
|
||
t.Fatal("Content-Range 格式异常应返回错误")
|
||
}
|
||
if !strings.Contains(err.Error(), "解析失败") {
|
||
t.Errorf("错误信息应包含'解析失败': %v", err)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 分片过程 401 → single-flight 刷新凭证后续传(不重下已完成分片)。
|
||
func TestCrossPlatformCoverageDriveTransferDownload_AuthRefreshDuringParts(t *testing.T) {
|
||
content := makeTestContent(1200)
|
||
var tokenGen atomic.Int32 // 服务端当前有效 token 代数
|
||
srv := rangeTestServer(t, content, "tok", &tokenGen)
|
||
defer srv.Close()
|
||
|
||
var fetchCalls atomic.Int32
|
||
fetch := func(ctx context.Context) (string, map[string]string, int, error) {
|
||
fetchCalls.Add(1)
|
||
return srv.URL, map[string]string{"dentry-token": fmt.Sprintf("tok-%d", tokenGen.Load())}, 5, nil
|
||
}
|
||
dest := filepath.Join(t.TempDir(), "auth-parts.bin")
|
||
opts := smallPartOpts(300, 2)
|
||
opts.knownSize = 1200
|
||
opts.version = 5
|
||
|
||
// 初始凭证是第 0 代;探测后服务端轮换到第 1 代,分片请求将收到 401
|
||
initial := map[string]string{"dentry-token": "tok-0"}
|
||
go func() {
|
||
// 探测完成前不轮换:用探测本身消耗第 0 代,随后轮换
|
||
tokenGen.Store(1)
|
||
}()
|
||
if err := driveTransferDownload(context.Background(), fetch, srv.URL, initial, dest, opts); err != nil {
|
||
t.Fatalf("凭证刷新续传失败: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
}
|
||
|
||
// 断点续传:模拟部分分片完成后中断,重跑跳过已完成分片。
|
||
func TestCrossPlatformCoverageDriveTransferDownload_ResumeSkipsCompletedParts(t *testing.T) {
|
||
content := makeTestContent(1000)
|
||
partSize := int64(300)
|
||
dest := filepath.Join(t.TempDir(), "resume.bin")
|
||
partPath := dest + drivePartFileSuffix
|
||
metaPath := dest + drivePartMetaSuffix
|
||
|
||
// 预置:分片 0、2 已完成(写入正确数据),1、3 未完成
|
||
pre := make([]byte, 1000)
|
||
copy(pre[0:300], content[0:300])
|
||
copy(pre[600:900], content[600:900])
|
||
if err := os.WriteFile(partPath, pre, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const resumeNodeID = "resume-test-node"
|
||
|
||
// 服务端记录收到的 Range 区间
|
||
var mu struct {
|
||
ranges []string
|
||
}
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
rng := r.Header.Get("Range")
|
||
mu.ranges = append(mu.ranges, rng)
|
||
var start, end int64
|
||
fmt.Sscanf(rng, "bytes=%d-%d", &start, &end)
|
||
if end >= int64(len(content)) {
|
||
end = int64(len(content)) - 1
|
||
}
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion,
|
||
Fingerprint: driveDownloadFingerprint(resumeNodeID, 0, 1000, srv.URL),
|
||
TotalSize: 1000,
|
||
PartSize: partSize,
|
||
Completed: []bool{true, false, true, false},
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
opts := smallPartOpts(partSize, 1) // 串行便于断言
|
||
opts.knownSize = 1000
|
||
opts.nodeID = resumeNodeID
|
||
if err := driveTransferDownload(context.Background(), nil, srv.URL, nil, dest, opts); err != nil {
|
||
t.Fatalf("续传失败: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
|
||
// 探测请求(bytes=0-0) + 分片1(300-599) + 分片3(900-999),不应重下分片0/2
|
||
for _, rng := range mu.ranges {
|
||
if rng == fmt.Sprintf("bytes=%d-%d", 0, partSize-1) || rng == fmt.Sprintf("bytes=%d-%d", 2*partSize, 3*partSize-1) {
|
||
t.Errorf("已完成分片被重下: %s", rng)
|
||
}
|
||
}
|
||
}
|
||
|
||
// --no-resume:清理历史断点从头下载,且过程中不写 checkpoint。
|
||
func TestCrossPlatformCoverageDriveTransferDownload_NoResume(t *testing.T) {
|
||
content := makeTestContent(1000)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "noresume.bin")
|
||
// 预置一个陈旧 checkpoint(若未清理会因 Completed 全 true 跳过下载产出错误内容)
|
||
stale := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion,
|
||
Fingerprint: driveDownloadFingerprint("stale-node", 0, 1000, ""),
|
||
TotalSize: 1000,
|
||
PartSize: 300,
|
||
Completed: []bool{true, true, true, true},
|
||
}
|
||
if err := os.WriteFile(dest+drivePartFileSuffix, make([]byte, 1000), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := stale.save(dest + drivePartMetaSuffix); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
opts := driveDownloadOptions{partSize: 300, parallel: 2, resume: false, knownSize: 1000}
|
||
if err := driveTransferDownload(context.Background(), nil, srv.URL, nil, dest, opts); err != nil {
|
||
t.Fatalf("no-resume 下载失败: %v", err)
|
||
}
|
||
verifyFile(t, dest, content) // 内容正确说明确实重下而非采信陈旧 checkpoint
|
||
if _, err := os.Stat(dest + drivePartMetaSuffix); !os.IsNotExist(err) {
|
||
t.Error("no-resume 不应遗留 checkpoint")
|
||
}
|
||
}
|
||
|
||
// 分片失败重试后成功(指数退避路径)。
|
||
func TestCrossPlatformCoverageDownloadOnePart_RetryOnTransientError(t *testing.T) {
|
||
content := makeTestContent(600)
|
||
var hits atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if hits.Add(1) <= 2 { // 前两次 500
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
return
|
||
}
|
||
rng := r.Header.Get("Range")
|
||
var start, end int64
|
||
fmt.Sscanf(rng, "bytes=%d-%d", &start, &end)
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
dir := t.TempDir()
|
||
f, err := os.Create(filepath.Join(dir, "part.tmp"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
if err := downloadOnePart(context.Background(), creds, f, driveDownloadPart{index: 0, offset: 0, length: 600}, 0); err != nil {
|
||
t.Fatalf("瞬时错误重试后应成功: %v", err)
|
||
}
|
||
if hits.Load() != 3 {
|
||
t.Errorf("应请求 3 次, got %d", hits.Load())
|
||
}
|
||
}
|
||
|
||
// 分片持续失败超过重试上限 → 返回错误。
|
||
func TestCrossPlatformCoverageDownloadOnePart_ExhaustsRetries(t *testing.T) {
|
||
var hits atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
hits.Add(1)
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.Create(filepath.Join(t.TempDir(), "part.tmp"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
err = downloadOnePart(context.Background(), creds, f, driveDownloadPart{index: 0, offset: 0, length: 10}, 0)
|
||
if err == nil {
|
||
t.Fatal("超过重试上限应失败")
|
||
}
|
||
if hits.Load() != int32(driveDownloadPartRetries)+1 {
|
||
t.Errorf("应请求 %d 次, got %d", driveDownloadPartRetries+1, hits.Load())
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// driveUploadPut:中心协议 PUT + 401 重取凭证重试
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveUploadPut_Success(t *testing.T) {
|
||
var gotURL string
|
||
var gotHeaders map[string]string
|
||
SetHTTPPutFile(func(ctx context.Context, url string, headers map[string]string, filePath string, fileSize int64) error {
|
||
gotURL, gotHeaders = url, headers
|
||
return nil
|
||
})
|
||
defer SetHTTPPutFile(nil)
|
||
|
||
cred := `{"result":{"uploadType":"httpToCenterWithToken","resourceUrl":"https://c.example.com/attachment/token/upload/single?file_size=5&spaceId=s1&upload_key=k1","uploadId":"k1","headers":{"dentry-token":"tk"}}}`
|
||
uploadID, err := driveUploadPut(context.Background(), cred, nil, "/tmp/f.bin", 5)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if uploadID != "k1" {
|
||
t.Errorf("uploadID = %q", uploadID)
|
||
}
|
||
if !strings.Contains(gotURL, "upload_key=k1") {
|
||
t.Errorf("应使用服务端拼好的完整 URL: %q", gotURL)
|
||
}
|
||
if gotHeaders["dentry-token"] != "tk" {
|
||
t.Errorf("headers 应透传 dentry-token: %v", gotHeaders)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveUploadPut_AuthRetryWithNewCredential(t *testing.T) {
|
||
var calls atomic.Int32
|
||
SetHTTPPutFile(func(ctx context.Context, url string, headers map[string]string, filePath string, fileSize int64) error {
|
||
if calls.Add(1) == 1 {
|
||
return &httpStatusError{StatusCode: 401, Body: "token expired"}
|
||
}
|
||
if headers["dentry-token"] != "tk2" {
|
||
return fmt.Errorf("重试应使用新 token, got %v", headers)
|
||
}
|
||
return nil
|
||
})
|
||
defer SetHTTPPutFile(nil)
|
||
|
||
cred1 := `{"result":{"uploadType":"httpToCenterWithToken","resourceUrl":"https://c.example.com/u?upload_key=k1","uploadId":"k1","headers":{"dentry-token":"tk1"}}}`
|
||
cred2 := `{"result":{"uploadType":"httpToCenterWithToken","resourceUrl":"https://c.example.com/u?upload_key=k2","uploadId":"k2","headers":{"dentry-token":"tk2"}}}`
|
||
refetch := func(ctx context.Context) (string, error) { return cred2, nil }
|
||
|
||
uploadID, err := driveUploadPut(context.Background(), cred1, refetch, "/tmp/f.bin", 5)
|
||
if err != nil {
|
||
t.Fatalf("401 重试后应成功: %v", err)
|
||
}
|
||
if uploadID != "k2" {
|
||
t.Errorf("应返回新凭证的 uploadId, got %q", uploadID)
|
||
}
|
||
if calls.Load() != 2 {
|
||
t.Errorf("PUT 应调用 2 次, got %d", calls.Load())
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveUploadPut_AuthRetryStillFails(t *testing.T) {
|
||
SetHTTPPutFile(func(ctx context.Context, url string, headers map[string]string, filePath string, fileSize int64) error {
|
||
return &httpStatusError{StatusCode: 403, Body: "denied"}
|
||
})
|
||
defer SetHTTPPutFile(nil)
|
||
|
||
cred := `{"result":{"resourceUrl":"https://c.example.com/u","uploadId":"k1"}}`
|
||
refetch := func(ctx context.Context) (string, error) { return cred, nil }
|
||
if _, err := driveUploadPut(context.Background(), cred, refetch, "/tmp/f.bin", 5); err == nil {
|
||
t.Fatal("重试仍失败应返回错误")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveUploadPut_SizeLimitHint(t *testing.T) {
|
||
SetHTTPPutFile(func(ctx context.Context, url string, headers map[string]string, filePath string, fileSize int64) error {
|
||
return &httpStatusError{StatusCode: 413, Body: "request entity too large"}
|
||
})
|
||
defer SetHTTPPutFile(nil)
|
||
|
||
cred := `{"result":{"uploadType":"httpToCenterWithToken","resourceUrl":"https://c.example.com/u","uploadId":"k1","headers":{"dentry-token":"tk"}}}`
|
||
_, err := driveUploadPut(context.Background(), cred, nil, "/tmp/f.bin", 5)
|
||
if err == nil || !strings.Contains(err.Error(), "提示") {
|
||
t.Fatalf("超限错误应含可读提示: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// parseDriveDownloadInfo:中心协议 headers 透传(新增行为)
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageParseDriveDownloadInfo_CenterProtocolHeaders(t *testing.T) {
|
||
text := `{"result":{"downloadType":"httpToCenterWithToken","downloadUrl":"https://c.example.com/attachment/token/mdown?k=v","headers":{"dentry-token":"tk"},"fileName":"a.bin"},"success":true}`
|
||
url, headers, err := parseDriveDownloadInfo(text)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if url != "https://c.example.com/attachment/token/mdown?k=v" {
|
||
t.Errorf("url = %q", url)
|
||
}
|
||
if headers["dentry-token"] != "tk" {
|
||
t.Errorf("中心协议应透传 dentry-token, got %v", headers)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// driveCredentialState:single-flight 刷新
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveCredentialStateSingleFlightRefresh(t *testing.T) {
|
||
var fetches atomic.Int32
|
||
cs := &driveCredentialState{
|
||
url: "u0",
|
||
initialVersion: 5,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
n := fetches.Add(1)
|
||
return fmt.Sprintf("u%d", n), nil, 5, nil
|
||
},
|
||
}
|
||
_, _, gen0 := cs.current()
|
||
// 两个并发分片都持第 0 代请求刷新:只应真正 fetch 一次
|
||
if err := cs.refresh(context.Background(), gen0); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := cs.refresh(context.Background(), gen0); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if fetches.Load() != 1 {
|
||
t.Errorf("同代刷新应 single-flight, fetch 次数 = %d", fetches.Load())
|
||
}
|
||
url, _, gen1 := cs.current()
|
||
if url != "u1" || gen1 != gen0+1 {
|
||
t.Errorf("刷新后 url=%q gen=%d", url, gen1)
|
||
}
|
||
// 持新代再刷新 → 再 fetch 一次
|
||
if err := cs.refresh(context.Background(), gen1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if fetches.Load() != 2 {
|
||
t.Errorf("新代刷新应真正执行, fetch 次数 = %d", fetches.Load())
|
||
}
|
||
}
|
||
|
||
// refresh 无 fetcher → 返回错误
|
||
func TestCrossPlatformCoverageDriveCredentialStateRefreshNilFetch(t *testing.T) {
|
||
cs := &driveCredentialState{url: "u0", fetch: nil}
|
||
_, _, gen := cs.current()
|
||
err := cs.refresh(context.Background(), gen)
|
||
if err == nil || !strings.Contains(err.Error(), "无法自动刷新") {
|
||
t.Errorf("fetch==nil 应报错, got %v", err)
|
||
}
|
||
}
|
||
|
||
// refresh fetcher 返回 error → 传播给调用方
|
||
func TestCrossPlatformCoverageDriveCredentialStateRefreshFetchError(t *testing.T) {
|
||
cs := &driveCredentialState{
|
||
url: "u0",
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "", nil, 0, fmt.Errorf("network")
|
||
},
|
||
}
|
||
_, _, gen := cs.current()
|
||
err := cs.refresh(context.Background(), gen)
|
||
if err == nil || !strings.Contains(err.Error(), "network") {
|
||
t.Errorf("fetch 报错应传播, got %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// parsePartSize 补充分支
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageParsePartSize_ExtraSuffixes(t *testing.T) {
|
||
// "K" 后缀
|
||
if got, err := parsePartSize("1024K"); err != nil || got != 1<<20 {
|
||
t.Errorf("1024K: got %d, err=%v", got, err)
|
||
}
|
||
// 裸 "B" 后缀(去 B 后当纯数字)
|
||
if got, err := parsePartSize("16777216B"); err != nil || got != 16<<20 {
|
||
t.Errorf("16777216B: got %d, err=%v", got, err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// parseContentRange 补充:负 start
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageParseContentRange_NegativeStart(t *testing.T) {
|
||
_, _, _, err := parseContentRange("bytes -1-100/200")
|
||
if err == nil {
|
||
t.Error("负 start 应报错")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// likelySizeLimitBody 补充中文关键词和 "size"+"over"/"size"+"limit"
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageLikelySizeLimitBody(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
want bool
|
||
}{
|
||
{"文件超限", true},
|
||
{"超出配额", true},
|
||
{"容量不足", true},
|
||
{"file size over limit", true},
|
||
{"Size Limit Exceeded", true},
|
||
{"no error", false},
|
||
{"", false},
|
||
}
|
||
for _, c := range cases {
|
||
if got := likelySizeLimitBody(c.in); got != c.want {
|
||
t.Errorf("likelySizeLimitBody(%q) = %v, want %v", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// driveTransferDownload 默认 opts 和 probe 错误路径
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
// opts.partSize/parallel 为 0 时使用默认值
|
||
func TestCrossPlatformCoverageDriveTransferDownload_DefaultOpts(t *testing.T) {
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
return os.WriteFile(destPath, []byte("ok"), 0o644)
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
|
||
dest := filepath.Join(t.TempDir(), "default.bin")
|
||
// knownSize < 2*defaultPartSize → 整流; partSize=0, parallel=0 用默认值
|
||
opts := driveDownloadOptions{knownSize: 100}
|
||
if err := driveTransferDownload(context.Background(), nil, "https://x.example.com/f", nil, dest, opts); err != nil {
|
||
t.Fatalf("默认 opts 应正常: %v", err)
|
||
}
|
||
}
|
||
|
||
// probeRangeSupport 返回 error → driveTransferDownload 传播
|
||
func TestCrossPlatformCoverageDriveTransferDownload_ProbeError(t *testing.T) {
|
||
// 使用无效 URL 触发 probeRangeSupport 错误
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 2, knownSize: 100}
|
||
err := driveTransferDownload(context.Background(), nil, "://invalid-url", nil, filepath.Join(t.TempDir(), "x"), opts)
|
||
if err == nil {
|
||
t.Fatal("无效 URL 的 probe 应失败")
|
||
}
|
||
}
|
||
|
||
// probeRangeSupport 返回 totalSize 小于阈值 → 回退整流
|
||
func TestCrossPlatformCoverageDriveTransferDownload_ProbeTotalBelowThreshold(t *testing.T) {
|
||
// 服务端返回 206 但 Content-Range 中的 total 很小
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-0/5") // total=5 < 2*partSize=20
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write([]byte{0})
|
||
}))
|
||
defer srv.Close()
|
||
|
||
var called atomic.Int32
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
called.Add(1)
|
||
return os.WriteFile(destPath, []byte("small"), 0o644)
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
|
||
dest := filepath.Join(t.TempDir(), "small-probe.bin")
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 2, knownSize: 100}
|
||
if err := driveTransferDownload(context.Background(), nil, srv.URL, nil, dest, opts); err != nil {
|
||
t.Fatalf("totalSize < threshold 应回退整流: %v", err)
|
||
}
|
||
if called.Load() == 0 {
|
||
t.Error("应走整流路径(httpGetFile)")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// downloadSingleWithAuthRetry: fetch==nil、fetch 报错
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDownloadSingleWithAuthRetry_NilFetch(t *testing.T) {
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
return &httpStatusError{StatusCode: 401, Body: "expired"}
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
// fetch==nil → 不重试,直接返回原始错误
|
||
err := downloadSingleWithAuthRetry(context.Background(), nil, "https://old.example.com/f", nil, filepath.Join(t.TempDir(), "x"))
|
||
if err == nil || !isAuthStatusError(err) {
|
||
t.Fatalf("fetch==nil 应直接返回鉴权错误: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDownloadSingleWithAuthRetry_FetchError(t *testing.T) {
|
||
var calls atomic.Int32
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
calls.Add(1)
|
||
return &httpStatusError{StatusCode: 401, Body: "expired"}
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
// fetch 返回 error → 返回原始 httpGetFile 错误(非 fetch 错误)
|
||
fetch := func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "", nil, 0, fmt.Errorf("fetch failed")
|
||
}
|
||
err := downloadSingleWithAuthRetry(context.Background(), fetch, "https://old.example.com/f", nil, filepath.Join(t.TempDir(), "x"))
|
||
if err == nil || !isAuthStatusError(err) {
|
||
t.Fatalf("fetch 报错应返回原始鉴权错误: %v", err)
|
||
}
|
||
if calls.Load() != 1 {
|
||
t.Errorf("httpGetFile 应只调用一次, got %d", calls.Load())
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// probeRangeSupport: 401 刷新失败、default 非 2xx、循环耗尽
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageProbeRangeSupport_AuthRefreshFail(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{
|
||
url: srv.URL,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "", nil, 0, fmt.Errorf("refresh-fail")
|
||
},
|
||
}
|
||
_, _, err := probeRangeSupport(context.Background(), creds)
|
||
if err == nil || !strings.Contains(err.Error(), "重新获取下载凭证失败") {
|
||
t.Fatalf("401+refresh 失败应报凭证错误: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageProbeRangeSupport_NonRetryableError(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusNotFound)
|
||
fmt.Fprint(w, "not found")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
_, _, err := probeRangeSupport(context.Background(), creds)
|
||
if err == nil {
|
||
t.Fatal("404 应返回错误")
|
||
}
|
||
var se *httpStatusError
|
||
if !errors.As(err, &se) || se.StatusCode != 404 {
|
||
t.Errorf("应返回 httpStatusError(404): %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageProbeRangeSupport_AuthExhausted(t *testing.T) {
|
||
// 第一次 403 → refresh 成功 → 第二次仍 403 → attempt>0 返回 "下载凭证刷新后仍鉴权失败"
|
||
var hits atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
hits.Add(1)
|
||
w.WriteHeader(http.StatusForbidden)
|
||
fmt.Fprint(w, "denied")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{
|
||
url: srv.URL,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) { return srv.URL, nil, 0, nil },
|
||
}
|
||
_, _, err := probeRangeSupport(context.Background(), creds)
|
||
if err == nil {
|
||
t.Fatal("持续 403 应报错")
|
||
}
|
||
if !strings.Contains(err.Error(), "下载凭证刷新后仍鉴权失败") {
|
||
t.Fatalf("应返回凭证刷新失败错误: %v", err)
|
||
}
|
||
if hits.Load() != 2 {
|
||
t.Errorf("应请求 2 次, got %d", hits.Load())
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageProbeRangeSupport_InvalidURL(t *testing.T) {
|
||
creds := &driveCredentialState{url: "://bad"}
|
||
_, _, err := probeRangeSupport(context.Background(), creds)
|
||
if err == nil {
|
||
t.Fatal("无效 URL 应报错")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageProbeRangeSupport_NetworkError(t *testing.T) {
|
||
// 使用已关闭的服务端模拟网络错误
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||
srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
_, _, err := probeRangeSupport(context.Background(), creds)
|
||
if err == nil {
|
||
t.Fatal("网络错误应传播")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageProbeRangeSupport_ContentRangeParseError(t *testing.T) {
|
||
// 返回 206 但 Content-Range 无法解析 total → 返回 0, nil, nil
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-0/*") // total 未知
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write([]byte{0})
|
||
}))
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
total, resp, err := probeRangeSupport(context.Background(), creds)
|
||
if err != nil {
|
||
t.Fatalf("Content-Range 异常不应报 error: %v", err)
|
||
}
|
||
if resp != nil {
|
||
t.Error("不应返回 resp")
|
||
}
|
||
if total != 0 {
|
||
t.Errorf("total 应为 0, got %d", total)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// writeStreamToFile 错误路径
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageWriteStreamToFile_CreateError(t *testing.T) {
|
||
// 写入不存在目录下的文件 → os.Create 失败
|
||
err := writeStreamToFile(strings.NewReader("data"), "/no-such-dir/sub/file.bin")
|
||
if err == nil {
|
||
t.Fatal("不存在路径应失败")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageWriteStreamToFile_CopyError(t *testing.T) {
|
||
dest := filepath.Join(t.TempDir(), "copy-err.bin")
|
||
errReader := &errReaderHelper{err: fmt.Errorf("read broken")}
|
||
err := writeStreamToFile(errReader, dest)
|
||
if err == nil || !strings.Contains(err.Error(), "read broken") {
|
||
t.Fatalf("io.Copy 错误应传播: %v", err)
|
||
}
|
||
}
|
||
|
||
type errReaderHelper struct{ err error }
|
||
|
||
func (r *errReaderHelper) Read(p []byte) (int, error) { return 0, r.err }
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// checkpoint save 错误路径
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageCheckpointSave_WriteError(t *testing.T) {
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion, Fingerprint: "fp", TotalSize: 100, PartSize: 30, Completed: []bool{true},
|
||
}
|
||
// 写入不存在的目录 → 失败
|
||
err := cp.save("/no-such-dir/sub/meta.json")
|
||
if err == nil {
|
||
t.Fatal("不存在路径应失败")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// downloadRangedParts: 文件操作失败、上下文取消、logf 分支
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDownloadRangedParts_OpenFileError(t *testing.T) {
|
||
creds := &driveCredentialState{url: "http://x.example.com"}
|
||
// destPath 指向不存在的目录 → OpenFile 失败
|
||
dest := "/no-such-dir/sub/out.bin"
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 1, resume: false}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil || !strings.Contains(err.Error(), "分片临时文件失败") {
|
||
t.Fatalf("OpenFile 失败应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDownloadRangedParts_ContextCancelDuringDispatch(t *testing.T) {
|
||
started := make(chan struct{})
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
select {
|
||
case started <- struct{}{}:
|
||
default:
|
||
}
|
||
// 阻塞直到请求上下文被取消
|
||
<-r.Context().Done()
|
||
}))
|
||
defer srv.Close()
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
defer cancel()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "cancel-dispatch.bin")
|
||
// 多分片、parallel=1 → worker 卡在第一个请求,dispatch 第二个时 select 触发 ctx.Done
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 1, resume: false, knownSize: 100}
|
||
|
||
go func() {
|
||
<-started // 确保第一个 HTTP 请求已发出
|
||
cancel()
|
||
}()
|
||
|
||
err := downloadRangedParts(ctx, creds, dest, 100, opts)
|
||
if err == nil {
|
||
t.Fatal("context cancel 应返回错误")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDownloadRangedParts_LogfBranches(t *testing.T) {
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
var logs []string
|
||
dest := filepath.Join(t.TempDir(), "logf.bin")
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
opts := driveDownloadOptions{
|
||
partSize: 30, parallel: 2, resume: true, knownSize: 100,
|
||
logf: func(format string, args ...any) {
|
||
logs = append(logs, fmt.Sprintf(format, args...))
|
||
},
|
||
}
|
||
if err := downloadRangedParts(context.Background(), creds, dest, 100, opts); err != nil {
|
||
t.Fatalf("logf 测试下载失败: %v", err)
|
||
}
|
||
if len(logs) == 0 {
|
||
t.Error("应有日志输出")
|
||
}
|
||
// 应包含"分片下载"字样(全新下载)
|
||
found := false
|
||
for _, l := range logs {
|
||
if strings.Contains(l, "分片下载") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("全新下载应有'分片下载'日志, got %v", logs)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDownloadRangedParts_ResumeLogf(t *testing.T) {
|
||
// 模拟断点续传场景的 logf 输出
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "resume-logf.bin")
|
||
partPath := dest + drivePartFileSuffix
|
||
metaPath := dest + drivePartMetaSuffix
|
||
|
||
// 预置分片数据文件和 checkpoint(分片 0 已完成)
|
||
pre := make([]byte, 100)
|
||
copy(pre[0:30], content[0:30])
|
||
if err := os.WriteFile(partPath, pre, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fp := driveDownloadFingerprint("", 0, 100, srv.URL)
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion, Fingerprint: fp,
|
||
TotalSize: 100, PartSize: 30, Completed: []bool{true, false, false, false},
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
var logs []string
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
opts := driveDownloadOptions{
|
||
partSize: 30, parallel: 2, resume: true, knownSize: 100,
|
||
logf: func(format string, args ...any) {
|
||
logs = append(logs, fmt.Sprintf(format, args...))
|
||
},
|
||
}
|
||
if err := downloadRangedParts(context.Background(), creds, dest, 100, opts); err != nil {
|
||
t.Fatalf("续传 logf 失败: %v", err)
|
||
}
|
||
// 应包含"断点续传"字样
|
||
found := false
|
||
for _, l := range logs {
|
||
if strings.Contains(l, "断点续传") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("续传应有'断点续传'日志, got %v", logs)
|
||
}
|
||
}
|
||
|
||
// checkpoint 存在但分片文件缺失 → 重新从头下载
|
||
func TestCrossPlatformCoverageDownloadRangedParts_CheckpointPartFileMissing(t *testing.T) {
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "cp-missing.bin")
|
||
metaPath := dest + drivePartMetaSuffix
|
||
|
||
// 只有 checkpoint 没有 partPath → cp 作废
|
||
fp := driveDownloadFingerprint("", 0, 100, srv.URL)
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion, Fingerprint: fp,
|
||
TotalSize: 100, PartSize: 30, Completed: []bool{true, true, true, true},
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 2, resume: true, knownSize: 100}
|
||
if err := downloadRangedParts(context.Background(), creds, dest, 100, opts); err != nil {
|
||
t.Fatalf("checkpoint 无分片文件应从头下载: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
}
|
||
|
||
// checkpoint 存在但分片文件大小不符 → 重新从头下载
|
||
func TestCrossPlatformCoverageDownloadRangedParts_CheckpointPartFileSizeMismatch(t *testing.T) {
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "cp-size.bin")
|
||
partPath := dest + drivePartFileSuffix
|
||
metaPath := dest + drivePartMetaSuffix
|
||
|
||
// partPath 大小错误(50 != 100)
|
||
if err := os.WriteFile(partPath, make([]byte, 50), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fp := driveDownloadFingerprint("", 0, 100, srv.URL)
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion, Fingerprint: fp,
|
||
TotalSize: 100, PartSize: 30, Completed: []bool{true, false, false, false},
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 2, resume: true, knownSize: 100}
|
||
if err := downloadRangedParts(context.Background(), creds, dest, 100, opts); err != nil {
|
||
t.Fatalf("分片大小不符应从头下载: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// downloadOnePart: context 取消、auth retry 失败
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDownloadOnePart_ContextCanceled(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.Create(filepath.Join(t.TempDir(), "ctx-cancel.tmp"))
|
||
defer f.Close()
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
cancel() // 立即取消
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
err := downloadOnePart(ctx, creds, f, driveDownloadPart{index: 0, offset: 0, length: 10}, 0)
|
||
if err == nil {
|
||
t.Fatal("context 已取消应返回错误")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDownloadOnePart_AuthRefreshFail(t *testing.T) {
|
||
var hits atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
hits.Add(1)
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.Create(filepath.Join(t.TempDir(), "auth-fail.tmp"))
|
||
defer f.Close()
|
||
|
||
creds := &driveCredentialState{
|
||
url: srv.URL,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "", nil, 0, fmt.Errorf("refresh-fail")
|
||
},
|
||
}
|
||
err := downloadOnePart(context.Background(), creds, f, driveDownloadPart{index: 0, offset: 0, length: 10}, 0)
|
||
if err == nil || !strings.Contains(err.Error(), "重新获取下载凭证失败") {
|
||
t.Fatalf("auth refresh 失败应报凭证错误: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// fetchRangeInto: 网络错误、io.Copy 错误、短读
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageFetchRangeInto_InvalidURL(t *testing.T) {
|
||
f, _ := os.Create(filepath.Join(t.TempDir(), "inv.tmp"))
|
||
defer f.Close()
|
||
err := fetchRangeInto(context.Background(), "://bad", nil, f, driveDownloadPart{offset: 0, length: 10}, 0)
|
||
if err == nil {
|
||
t.Fatal("无效 URL 应报错")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFetchRangeInto_NetworkError(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||
srv.Close() // 关闭模拟网络错误
|
||
|
||
f, _ := os.Create(filepath.Join(t.TempDir(), "net.tmp"))
|
||
defer f.Close()
|
||
err := fetchRangeInto(context.Background(), srv.URL, nil, f, driveDownloadPart{offset: 0, length: 10}, 0)
|
||
if err == nil {
|
||
t.Fatal("网络错误应传播")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFetchRangeInto_ShortRead(t *testing.T) {
|
||
// 服务端只返回 5 字节但分片期望 10 字节 → 短读错误
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-9/100")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write([]byte("short")) // 只有 5 字节
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.CreateTemp(t.TempDir(), "short-*")
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
err := fetchRangeInto(context.Background(), srv.URL, nil, f, driveDownloadPart{offset: 0, length: 10}, 0)
|
||
if err == nil || !strings.Contains(err.Error(), "分片长度不符") {
|
||
t.Fatalf("短读应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFetchRangeInto_CopyError(t *testing.T) {
|
||
// 服务端在部分传输后关闭连接 → io.Copy 报错
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-99/100")
|
||
w.Header().Set("Content-Length", "100")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
// 只写入一部分然后 panic 来中断连接
|
||
_, _ = w.Write([]byte("partial"))
|
||
panic(http.ErrAbortHandler)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.CreateTemp(t.TempDir(), "copy-err-*")
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
err := fetchRangeInto(context.Background(), srv.URL, nil, f, driveDownloadPart{offset: 0, length: 100}, 0)
|
||
// 应有 error(io.Copy 失败或短读)
|
||
if err == nil {
|
||
t.Fatal("连接中断应报错")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// parseDriveDownloadInfo: 各种 fallback 路径
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageParseDriveDownloadInfo_InvalidJSON(t *testing.T) {
|
||
_, _, err := parseDriveDownloadInfo("not json")
|
||
if err == nil || !strings.Contains(err.Error(), "解析") {
|
||
t.Fatalf("非法 JSON 应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageParseDriveDownloadInfo_ResourceUrlFallback(t *testing.T) {
|
||
// downloadUrl 缺失,走 resourceUrl fallback
|
||
text := `{"result":{"resourceUrl":"https://oss.example.com/signed"}}`
|
||
url, headers, err := parseDriveDownloadInfo(text)
|
||
if err != nil {
|
||
t.Fatalf("resourceUrl fallback 应成功: %v", err)
|
||
}
|
||
if url != "https://oss.example.com/signed" {
|
||
t.Errorf("url = %q", url)
|
||
}
|
||
if len(headers) != 0 {
|
||
t.Errorf("无 headers 应为空 map: %v", headers)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageParseDriveDownloadInfo_ResourceUrlsFallback(t *testing.T) {
|
||
// downloadUrl 和 resourceUrl 都缺失,走 resourceUrls 数组 fallback
|
||
text := `{"result":{"resourceUrls":[{"url":"https://cdn.example.com/file","headers":{"x-custom":"val"}}]}}`
|
||
url, headers, err := parseDriveDownloadInfo(text)
|
||
if err != nil {
|
||
t.Fatalf("resourceUrls fallback 应成功: %v", err)
|
||
}
|
||
if url != "https://cdn.example.com/file" {
|
||
t.Errorf("url = %q", url)
|
||
}
|
||
if headers["x-custom"] != "val" {
|
||
t.Errorf("headers 应包含 x-custom: %v", headers)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageParseDriveDownloadInfo_EmptyURL(t *testing.T) {
|
||
// 所有 URL 字段都为空
|
||
text := `{"result":{"fileName":"a.bin"}}`
|
||
_, _, err := parseDriveDownloadInfo(text)
|
||
if err == nil || !strings.Contains(err.Error(), "downloadUrl 为空") {
|
||
t.Fatalf("所有 URL 缺失应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// P1-1/P1-2 补充测试:中心协议解析与指纹唯一性
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
// TestCrossPlatformCoverageParseDriveDownloadInfo_ResourceUrlsPerURLHeaders
|
||
// 验证 resourceUrls[].headers 中的 dentry-token 被正确透传到最终 headers。
|
||
func TestCrossPlatformCoverageParseDriveDownloadInfo_ResourceUrlsPerURLHeaders(t *testing.T) {
|
||
text := `{"result":{"resourceUrls":[{"url":"https://center.example.com/download/path","headers":{"dentry-token":"dt-abc123","x-oss-security":"sig"}}]}}`
|
||
url, headers, err := parseDriveDownloadInfo(text)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if url != "https://center.example.com/download/path" {
|
||
t.Errorf("url = %q, want center URL", url)
|
||
}
|
||
if headers["dentry-token"] != "dt-abc123" {
|
||
t.Errorf("dentry-token 未正确透传: got %v", headers)
|
||
}
|
||
if headers["x-oss-security"] != "sig" {
|
||
t.Errorf("x-oss-security 未正确透传: got %v", headers)
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageParseDriveDownloadInfo_TopLevelAndPerURLHeadersMerge
|
||
// 验证顶层 headers 与 resourceUrls[].headers 合并(per-URL 覆盖顶层同名 key)。
|
||
func TestCrossPlatformCoverageParseDriveDownloadInfo_TopLevelAndPerURLHeadersMerge(t *testing.T) {
|
||
text := `{"result":{"headers":{"x-top":"top-val","dentry-token":"old"},"resourceUrls":[{"url":"https://center.example.com/dl","headers":{"dentry-token":"new"}}]}}`
|
||
_, headers, err := parseDriveDownloadInfo(text)
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
// 顶层 headers 应被读入
|
||
if headers["x-top"] != "top-val" {
|
||
t.Errorf("顶层 x-top 应保留: got %v", headers)
|
||
}
|
||
// per-URL headers 覆盖同名 key
|
||
if headers["dentry-token"] != "new" {
|
||
t.Errorf("per-URL dentry-token 应覆盖顶层: got %q", headers["dentry-token"])
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageFingerprintVersion0DifferentURLs
|
||
// 验证 version=0 时不同完整 URL(含 query)产生不同指纹。
|
||
func TestCrossPlatformCoverageFingerprintVersion0DifferentURLs(t *testing.T) {
|
||
urlA := "https://center.example.com/download/file?sign=aaa&expire=100"
|
||
urlB := "https://center.example.com/download/file?sign=bbb&expire=200"
|
||
urlC := "https://center.example.com/download/other-file?sign=aaa&expire=100"
|
||
|
||
fpA := driveDownloadFingerprint("node-1", 0, 1024, urlA)
|
||
fpB := driveDownloadFingerprint("node-1", 0, 1024, urlB)
|
||
fpC := driveDownloadFingerprint("node-1", 0, 1024, urlC)
|
||
|
||
// 同 path 不同 query → 不同指纹
|
||
if fpA == fpB {
|
||
t.Error("version=0: 同 path 不同 query 应产生不同指纹")
|
||
}
|
||
// 不同 path → 不同指纹
|
||
if fpA == fpC {
|
||
t.Error("version=0: 不同 path 应产生不同指纹")
|
||
}
|
||
// 相同 URL → 相同指纹
|
||
fpA2 := driveDownloadFingerprint("node-1", 0, 1024, urlA)
|
||
if fpA != fpA2 {
|
||
t.Error("version=0: 相同 URL 应产生相同指纹")
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageFingerprintVersionedStableAcrossResign
|
||
// 验证 version>0 时重签名(仅 query 变化)不影响指纹。
|
||
func TestCrossPlatformCoverageFingerprintVersionedStableAcrossResign(t *testing.T) {
|
||
urlOld := "https://oss.example.com/files/abc/content?Signature=old&Expires=1"
|
||
urlNew := "https://oss.example.com/files/abc/content?Signature=new&Expires=2"
|
||
|
||
fpOld := driveDownloadFingerprint("node-2", 5, 2048, urlOld)
|
||
fpNew := driveDownloadFingerprint("node-2", 5, 2048, urlNew)
|
||
|
||
if fpOld != fpNew {
|
||
t.Error("version>0: 重签名不应改变指纹(只取 path)")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// driveTransferDownload: 整合 fetch 在 totalSize<threshold 路径
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveTransferDownload_ProbeTotalBelowThreshold_WithFetchNil(t *testing.T) {
|
||
// 服务端返回 206 + total < threshold → 回退整流且 fetch lambda 中 fetch==nil
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-0/5")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write([]byte{0})
|
||
}))
|
||
defer srv.Close()
|
||
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
return os.WriteFile(destPath, []byte("tiny"), 0o644)
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
|
||
dest := filepath.Join(t.TempDir(), "nil-fetch-threshold.bin")
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 2, knownSize: 100}
|
||
// fetch=nil: 当 totalSize<threshold 整流时,401/403 不会再重取凭证
|
||
if err := driveTransferDownload(context.Background(), nil, srv.URL, nil, dest, opts); err != nil {
|
||
t.Fatalf("fetch==nil totalSize<threshold 应正常整流: %v", err)
|
||
}
|
||
}
|
||
|
||
// 测试 unused import io
|
||
func TestCrossPlatformCoverageFetchRangeInto_HeadersPassThrough(t *testing.T) {
|
||
content := makeTestContent(100)
|
||
var gotHeaders http.Header
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
gotHeaders = r.Header
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.CreateTemp(t.TempDir(), "hdr-*")
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
headers := map[string]string{"dentry-token": "tk123", "x-custom": "val"}
|
||
err := fetchRangeInto(context.Background(), srv.URL, headers, f, driveDownloadPart{offset: 0, length: 50}, 0)
|
||
if err != nil {
|
||
t.Fatalf("headers 透传测试失败: %v", err)
|
||
}
|
||
if gotHeaders.Get("dentry-token") != "tk123" || gotHeaders.Get("x-custom") != "val" {
|
||
t.Errorf("headers 应透传: %v", gotHeaders)
|
||
}
|
||
}
|
||
|
||
// 确保 io 和 errors 包的使用
|
||
var _ = io.Discard
|
||
var _ = errors.New
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// downloadOnePart: auth retry 成功后 continue、backoff 期间 ctx 取消
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDownloadOnePart_AuthRefreshSuccess(t *testing.T) {
|
||
// 第一次 401 → refresh 成功 → 第二次成功
|
||
content := makeTestContent(100)
|
||
var tokenGen atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Header.Get("dentry-token") != fmt.Sprintf("tok-%d", tokenGen.Load()) {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
return
|
||
}
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
if end >= int64(len(content)) {
|
||
end = int64(len(content)) - 1
|
||
}
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
// 初始 token 无效(gen=0),refresh 后更新为 gen=1
|
||
tokenGen.Store(1)
|
||
creds := &driveCredentialState{
|
||
url: srv.URL,
|
||
headers: map[string]string{"dentry-token": "tok-0"},
|
||
initialVersion: 3,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return srv.URL, map[string]string{"dentry-token": fmt.Sprintf("tok-%d", tokenGen.Load())}, 3, nil
|
||
},
|
||
}
|
||
|
||
f, _ := os.CreateTemp(t.TempDir(), "auth-success-*")
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
err := downloadOnePart(context.Background(), creds, f, driveDownloadPart{index: 0, offset: 0, length: 50}, 0)
|
||
if err != nil {
|
||
t.Fatalf("auth refresh 成功后应下载成功: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDownloadOnePart_ContextCancelDuringBackoff(t *testing.T) {
|
||
// 模拟:第一次非 auth 错误(500)→ 进入退避等待(500ms)→ ctx 超时取消
|
||
// 用短 context timeout 确保在退避期间触发 ctx.Done
|
||
responded := make(chan struct{})
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
fmt.Fprint(w, "server error")
|
||
select {
|
||
case responded <- struct{}{}:
|
||
default:
|
||
}
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.Create(filepath.Join(t.TempDir(), "backoff-cancel.tmp"))
|
||
defer f.Close()
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
defer cancel()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
|
||
go func() {
|
||
<-responded // 等待第一次 HTTP 响应完成
|
||
// 小延迟确保 client 已收到响应并进入 backoff select
|
||
cancel()
|
||
}()
|
||
|
||
err := downloadOnePart(ctx, creds, f, driveDownloadPart{index: 0, offset: 0, length: 10}, 0)
|
||
if err == nil {
|
||
t.Fatal("退避期间 ctx 取消应返回错误")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// downloadRangedParts: Truncate 失败、f.Sync 失败、workers 边界
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDownloadRangedParts_WorkersCapToRemaining(t *testing.T) {
|
||
// 预置 checkpoint 让大部分分片已完成 → remaining < parallel → workers 被限制
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "cap-workers.bin")
|
||
partPath := dest + drivePartFileSuffix
|
||
metaPath := dest + drivePartMetaSuffix
|
||
|
||
// 4 个分片中只剩 1 个未完成 → workers=min(4,1)=1
|
||
pre := make([]byte, 100)
|
||
copy(pre[0:30], content[0:30])
|
||
copy(pre[30:60], content[30:60])
|
||
copy(pre[60:90], content[60:90])
|
||
if err := os.WriteFile(partPath, pre, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fp := driveDownloadFingerprint("", 0, 100, srv.URL)
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion, Fingerprint: fp,
|
||
TotalSize: 100, PartSize: 30, Completed: []bool{true, true, true, false},
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 4, resume: true, knownSize: 100}
|
||
if err := downloadRangedParts(context.Background(), creds, dest, 100, opts); err != nil {
|
||
t.Fatalf("workers cap 测试失败: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
}
|
||
|
||
// 所有分片已完成(remaining==0)→ workers clamped to 1、无分片下载、直接完成
|
||
func TestCrossPlatformCoverageDownloadRangedParts_AllCompleted(t *testing.T) {
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "all-done.bin")
|
||
partPath := dest + drivePartFileSuffix
|
||
metaPath := dest + drivePartMetaSuffix
|
||
|
||
// 预置完整的分片文件和全部完成的 checkpoint
|
||
if err := os.WriteFile(partPath, content, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fp := driveDownloadFingerprint("", 0, 100, srv.URL)
|
||
cp := &driveDownloadCheckpoint{
|
||
Version: driveCheckpointVersion, Fingerprint: fp,
|
||
TotalSize: 100, PartSize: 30, Completed: []bool{true, true, true, true},
|
||
}
|
||
if err := cp.save(metaPath); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 4, resume: true, knownSize: 100}
|
||
if err := downloadRangedParts(context.Background(), creds, dest, 100, opts); err != nil {
|
||
t.Fatalf("全部完成应直接成功: %v", err)
|
||
}
|
||
verifyFile(t, dest, content)
|
||
}
|
||
|
||
// driveTransferDownload 中 totalSize<threshold 路径触发 401 重取凭证(fetch!=nil)
|
||
func TestCrossPlatformCoverageDriveTransferDownload_ProbeTotalBelowThreshold_FetchCalled(t *testing.T) {
|
||
// 服务端返回 206 + total < threshold
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-0/5")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write([]byte{0})
|
||
}))
|
||
defer srv.Close()
|
||
|
||
var getCalls atomic.Int32
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
if getCalls.Add(1) == 1 {
|
||
return &httpStatusError{StatusCode: 401, Body: "expired"}
|
||
}
|
||
return os.WriteFile(destPath, []byte("ok"), 0o644)
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
|
||
var fetchCalls atomic.Int32
|
||
fetch := func(ctx context.Context) (string, map[string]string, int, error) {
|
||
fetchCalls.Add(1)
|
||
return srv.URL, map[string]string{"x-tok": "new"}, 0, nil
|
||
}
|
||
|
||
dest := filepath.Join(t.TempDir(), "threshold-fetch.bin")
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 2, knownSize: 100}
|
||
if err := driveTransferDownload(context.Background(), fetch, srv.URL, nil, dest, opts); err != nil {
|
||
t.Fatalf("totalSize<threshold + 401 重取应成功: %v", err)
|
||
}
|
||
if fetchCalls.Load() == 0 {
|
||
t.Error("应调用 fetch 重取凭证")
|
||
}
|
||
}
|
||
|
||
// driveTransferDownload 中 totalSize<threshold 路径 fetch==nil 时 401 应直接报错
|
||
func TestCrossPlatformCoverageDriveTransferDownload_ProbeTotalBelowThreshold_FetchNilAuth(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-0/5")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write([]byte{0})
|
||
}))
|
||
defer srv.Close()
|
||
|
||
SetHTTPGetFile(func(ctx context.Context, url string, headers map[string]string, destPath string) error {
|
||
return &httpStatusError{StatusCode: 401, Body: "expired"}
|
||
})
|
||
defer SetHTTPGetFile(nil)
|
||
|
||
dest := filepath.Join(t.TempDir(), "nil-fetch-auth.bin")
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 2, knownSize: 100}
|
||
// fetch=nil → 401 时内部 lambda 中 fetch==nil 返回 "无法自动刷新" → downloadSingleWithAuthRetry 返回原始错误
|
||
err := driveTransferDownload(context.Background(), nil, srv.URL, nil, dest, opts)
|
||
if err == nil || !isAuthStatusError(err) {
|
||
t.Fatalf("fetch==nil + 401 应返回鉴权错误: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// Cross-platform coverage: OS operation hook injection tests
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveTransferSaveJsonMarshalFailure(t *testing.T) {
|
||
origMarshal := driveJsonMarshal
|
||
t.Cleanup(func() { driveJsonMarshal = origMarshal })
|
||
driveJsonMarshal = func(v any) ([]byte, error) { return nil, errors.New("injected marshal failure") }
|
||
|
||
cp := &driveDownloadCheckpoint{Version: 1, TotalSize: 100, PartSize: 10, Completed: []bool{true}}
|
||
err := cp.save(filepath.Join(t.TempDir(), "test.meta"))
|
||
if err == nil || !strings.Contains(err.Error(), "injected marshal failure") {
|
||
t.Fatalf("json.Marshal 失败应传播: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferSaveRenameFailure(t *testing.T) {
|
||
origRename := driveOsRename
|
||
t.Cleanup(func() { driveOsRename = origRename })
|
||
driveOsRename = func(string, string) error { return errors.New("injected rename failure") }
|
||
|
||
cp := &driveDownloadCheckpoint{Version: 1, TotalSize: 100, PartSize: 10, Completed: []bool{true}}
|
||
err := cp.save(filepath.Join(t.TempDir(), "test.meta"))
|
||
if err == nil || !strings.Contains(err.Error(), "injected rename failure") {
|
||
t.Fatalf("rename 失败应传播: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferTruncateFailure(t *testing.T) {
|
||
origTruncate := driveFileTruncate
|
||
t.Cleanup(func() { driveFileTruncate = origTruncate })
|
||
driveFileTruncate = func(f *os.File, size int64) error { return errors.New("injected truncate failure") }
|
||
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "truncate-fail.bin")
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 2, resume: false, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil || !strings.Contains(err.Error(), "预分配分片临时文件失败") {
|
||
t.Fatalf("Truncate 失败应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferSyncFailure(t *testing.T) {
|
||
origSync := driveFileSync
|
||
t.Cleanup(func() { driveFileSync = origSync })
|
||
driveFileSync = func(f *os.File) error { return errors.New("injected sync failure") }
|
||
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "sync-fail.bin")
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 2, resume: false, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil || !strings.Contains(err.Error(), "injected sync failure") {
|
||
t.Fatalf("Sync 失败应传播: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferStatFailure(t *testing.T) {
|
||
origStat := driveFileStat
|
||
t.Cleanup(func() { driveFileStat = origStat })
|
||
driveFileStat = func(f *os.File) (os.FileInfo, error) { return nil, errors.New("injected stat failure") }
|
||
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "stat-fail.bin")
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 2, resume: false, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil || !strings.Contains(err.Error(), "injected stat failure") {
|
||
t.Fatalf("Stat 失败应传播: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferSizeMismatch(t *testing.T) {
|
||
origStat := driveFileStat
|
||
t.Cleanup(func() { driveFileStat = origStat })
|
||
// 返回与 totalSize 不一致的文件信息
|
||
driveFileStat = func(f *os.File) (os.FileInfo, error) {
|
||
// 返回真实 stat,但我们在调用前先 truncate 文件为不同大小
|
||
return f.Stat()
|
||
}
|
||
// 注入 Sync 后将文件 truncate 为不同大小
|
||
origSync := driveFileSync
|
||
driveFileSync = func(f *os.File) error {
|
||
// Sync 成功后破坏文件大小
|
||
if err := origSync(f); err != nil {
|
||
return err
|
||
}
|
||
return f.Truncate(50) // 破坏大小,让 Stat 返回 50 != 100
|
||
}
|
||
t.Cleanup(func() { driveFileSync = origSync })
|
||
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "size-mismatch.bin")
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 2, resume: false, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil || !strings.Contains(err.Error(), "下载完成但文件长度不符") {
|
||
t.Fatalf("文件大小不符应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferFinalRenameFailure(t *testing.T) {
|
||
origRename := driveOsRename
|
||
t.Cleanup(func() { driveOsRename = origRename })
|
||
// 只在最终重命名时失败(checkpoint save 也用 driveOsRename,所以禁用 resume 跳过)
|
||
driveOsRename = func(src, dst string) error {
|
||
if strings.HasSuffix(src, drivePartFileSuffix) {
|
||
return errors.New("injected final rename failure")
|
||
}
|
||
return origRename(src, dst)
|
||
}
|
||
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "rename-fail.bin")
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 2, resume: false, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil || !strings.Contains(err.Error(), "重命名下载文件失败") {
|
||
t.Fatalf("最终 rename 失败应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferCheckpointSaveFailure(t *testing.T) {
|
||
origRename := driveOsRename
|
||
t.Cleanup(func() { driveOsRename = origRename })
|
||
// checkpoint save 的 rename 失败(.tmp → .dwspart.meta)
|
||
driveOsRename = func(src, dst string) error {
|
||
if strings.HasSuffix(dst, drivePartMetaSuffix) {
|
||
return errors.New("injected checkpoint rename failure")
|
||
}
|
||
return origRename(src, dst)
|
||
}
|
||
|
||
content := makeTestContent(100)
|
||
srv := rangeTestServer(t, content, "", nil)
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "cp-save-fail.bin")
|
||
// resume=true 才会在分片完成后调用 cp.save
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 1, resume: true, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil || !strings.Contains(err.Error(), "写入下载断点信息失败") {
|
||
t.Fatalf("checkpoint save 失败应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferProbeAuthExhausted(t *testing.T) {
|
||
// 服务端永远返回 401,刷新成功但第二次仍 401 → "下载凭证刷新后仍鉴权失败"
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "always-unauthorized")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{
|
||
url: srv.URL,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
// 刷新成功但服务端仍拒绝
|
||
return srv.URL, nil, 0, nil
|
||
},
|
||
}
|
||
_, _, err := probeRangeSupport(context.Background(), creds)
|
||
if err == nil || !strings.Contains(err.Error(), "下载凭证刷新后仍鉴权失败") {
|
||
t.Fatalf("连续 401 应报凭证刷新后失败: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferParseContentRangeNoDash(t *testing.T) {
|
||
// range 部分没有 dash → "非法 Content-Range 区间"
|
||
_, _, _, err := parseContentRange("bytes 12345/67890")
|
||
if err == nil || !strings.Contains(err.Error(), "非法 Content-Range 区间") {
|
||
t.Fatalf("无 dash 应报区间错误: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferDownloadOnePartBackoffCtxDone(t *testing.T) {
|
||
// 服务端返回 500,触发重试退避;在退避等待中取消 context
|
||
var hits atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
hits.Add(1)
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
fmt.Fprint(w, "server error")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.CreateTemp(t.TempDir(), "backoff-ctx-*")
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
// 第一次失败后,在退避 sleep 中取消
|
||
go func() {
|
||
for hits.Load() < 1 {
|
||
// spin-wait
|
||
}
|
||
cancel()
|
||
}()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
err := downloadOnePart(ctx, creds, f, driveDownloadPart{index: 0, offset: 0, length: 10}, 0)
|
||
if err == nil {
|
||
t.Fatal("退避中 ctx 取消应返回错误")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferFetchRangeIntoCopyError(t *testing.T) {
|
||
// 服务端返回 206 但中断 body → io.Copy 错误
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Range", "bytes 0-9/100")
|
||
w.Header().Set("Content-Length", "10")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
// 写入部分数据后立即关闭连接(触发 io.Copy 中 unexpected EOF)
|
||
hj, ok := w.(http.Hijacker)
|
||
if !ok {
|
||
// 回退:写入少于期望的数据量,触发短读而非 io.Copy err
|
||
_, _ = w.Write([]byte("par"))
|
||
return
|
||
}
|
||
conn, buf, _ := hj.Hijack()
|
||
_, _ = buf.WriteString("par")
|
||
_ = buf.Flush()
|
||
_ = conn.Close()
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.CreateTemp(t.TempDir(), "copy-err-*")
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
err := fetchRangeInto(context.Background(), srv.URL, nil, f, driveDownloadPart{offset: 0, length: 10}, 0)
|
||
// 应产生 io.Copy 错误或短读错误
|
||
if err == nil {
|
||
t.Fatal("中断的 body 应产生错误")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveTransferWorkerCtxAlreadyCancelled(t *testing.T) {
|
||
// 多 worker 场景:一个 worker 失败后 cancel,其他 worker 取到 part 时 runCtx 已取消
|
||
var reqCount atomic.Int32
|
||
blockFirst := make(chan struct{})
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := reqCount.Add(1)
|
||
if n == 1 {
|
||
// 第一个请求阻塞一下,让其他 worker 有机会拿到 part
|
||
<-blockFirst
|
||
}
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
fmt.Fprint(w, "error")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
// 在短暂延迟后释放第一个请求
|
||
go func() {
|
||
for reqCount.Load() < 2 {
|
||
// spin-wait 直到有第二个请求进来
|
||
}
|
||
close(blockFirst)
|
||
}()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "worker-ctx.bin")
|
||
// 大量分片,多 worker:确保某个 worker 失败后其他 worker 在取 part 时触发 runCtx.Err()
|
||
opts := driveDownloadOptions{partSize: 5, parallel: 4, resume: false, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil {
|
||
t.Fatal("分片失败应报错")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// drive_transfer.go 覆盖率补全:downloadOnePart select ctx.Done 分支
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveTransferDownloadOnePartSelectCtxDone(t *testing.T) {
|
||
// 目标:覆盖 downloadOnePart 中 select { case <-ctx.Done(): return err } 分支。
|
||
// 策略:server 返回 500(非 auth)→ 代码通过 ctx.Err() 检查(此时 ctx 未取消)
|
||
// → 进入 select 等待 backoff(500ms),此时 goroutine 延迟 50ms 后 cancel ctx
|
||
// → select 收到 ctx.Done() → 返回 err
|
||
|
||
var responded atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
responded.Add(1)
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
fmt.Fprint(w, "server error")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, _ := os.CreateTemp(t.TempDir(), "select-ctx-done-*")
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
defer cancel()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
|
||
// 等第一次 HTTP 响应返回,然后延迟 50ms 确保代码已通过 ctx.Err() 检查并进入 select
|
||
go func() {
|
||
for responded.Load() < 1 {
|
||
time.Sleep(time.Millisecond)
|
||
}
|
||
// 关键延迟:确保代码已经通过 line 676 的 ctx.Err() 检查(此时返回 nil)
|
||
// 并进入 select 等待 time.After(500ms)
|
||
time.Sleep(50 * time.Millisecond)
|
||
cancel()
|
||
}()
|
||
|
||
err := downloadOnePart(ctx, creds, f, driveDownloadPart{index: 0, offset: 0, length: 10}, 0)
|
||
if err == nil {
|
||
t.Fatal("select 中 ctx 取消应返回错误")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// drive_transfer.go 覆盖率补全:worker 取到 part 后 runCtx 已取消
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveTransferWorkerRunCtxAlreadyCancelled(t *testing.T) {
|
||
// 目标:覆盖 downloadRangedParts worker 中 runCtx.Err() != nil → return 分支。
|
||
// 策略:2 workers + 多个分片;server 第一次请求成功,第二次返回 500 触发 fail()+cancel;
|
||
// 第一个 worker 完成后循环取下一个 part,此时 runCtx 已 cancelled。
|
||
content := makeTestContent(100)
|
||
var reqCount atomic.Int32
|
||
gate := make(chan struct{}) // 控制第一个请求的时序
|
||
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := reqCount.Add(1)
|
||
if n == 1 {
|
||
// 第一个请求:正常返回分片数据
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
if end >= int64(len(content)) {
|
||
end = int64(len(content)) - 1
|
||
}
|
||
// 等待 gate 信号,确保第二个 worker 已拿到 part 并发起请求
|
||
<-gate
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
} else if n == 2 {
|
||
// 第二个请求:失败,触发 fail() 取消 runCtx
|
||
close(gate) // 释放第一个请求
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
fmt.Fprint(w, "error")
|
||
} else {
|
||
// 后续请求(第一个 worker 循环后用已取消的 ctx 发起)立即返回错误
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
fmt.Fprint(w, "cancelled")
|
||
}
|
||
}))
|
||
defer srv.Close()
|
||
|
||
// 释放 gate 以防第二个请求没按预期到达
|
||
go func() {
|
||
time.Sleep(2 * time.Second)
|
||
select {
|
||
case <-gate:
|
||
default:
|
||
close(gate)
|
||
}
|
||
}()
|
||
|
||
creds := &driveCredentialState{url: srv.URL}
|
||
dest := filepath.Join(t.TempDir(), "worker-runctx.bin")
|
||
// 10 分片 * 10 bytes = 100 bytes;parallel=2
|
||
opts := driveDownloadOptions{partSize: 10, parallel: 2, resume: false, knownSize: 100}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil {
|
||
t.Fatal("worker 失败应报错")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// drive.go 覆盖率补全:download 命令 logf 回调、fetchCred 回调、非 Canceled 错误路径
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveDownloadRangedPathLogfAndFetchCred(t *testing.T) {
|
||
// 目标:覆盖 drive.go download 命令中的:
|
||
// - dlOpts.logf 回调(line 501-503)
|
||
// - fetchCred 回调(line 540-545)
|
||
// - return err 非 Canceled 路径(line 552)
|
||
// 策略:
|
||
// MCP step1: 返回 download info 带 fileSize=100, resourceUrl 指向 test server
|
||
// Test server: probe 返回 401 → 触发 fetchCred
|
||
// MCP step2: fetchCred 调用 → 返回错误(覆盖 ferr != nil 分支 line 542-544)
|
||
// 由于 fetchCred 失败,probeRangeSupport 返回错误,driveTransferDownload 返回该错误
|
||
// 该错误不是 context.Canceled → 走 line 552 的 return err
|
||
|
||
// 保存并替换 driveRangeClient 以控制 probe 行为
|
||
origClient := driveRangeClient
|
||
t.Cleanup(func() { driveRangeClient = origClient })
|
||
|
||
driveRangeClient = &http.Client{
|
||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||
// 始终返回 401 触发凭证刷新
|
||
return &http.Response{
|
||
StatusCode: http.StatusUnauthorized,
|
||
Header: make(http.Header),
|
||
Body: io.NopCloser(strings.NewReader("token expired")),
|
||
}, nil
|
||
}),
|
||
}
|
||
|
||
dest := filepath.Join(t.TempDir(), "ranged-logf.bin")
|
||
// MCP responses: step1 返回带 fileSize 的下载信息(需 >= 2*partSize=2MB 才走 ranged 路径)
|
||
// step2 fetchCred 返回错误
|
||
mcpResp := `{"resourceUrl":"https://fake.invalid/file.bin","fileSize":3000000}`
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: mcpResp}, // step1: download_file 成功
|
||
{err: errors.New("refresh failed")}, // step2: fetchCred 调用失败
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download", "--node", "node-1", "--output", dest, "--part-size", "1MB", "--parallel", "1")
|
||
if err == nil {
|
||
t.Fatal("fetchCred 失败应导致错误")
|
||
}
|
||
// 确保不是 context.Canceled 错误(覆盖 line 552)
|
||
if errors.Is(err, context.Canceled) {
|
||
t.Fatal("错误不应是 context.Canceled")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveDownloadRangedFetchCredSuccess(t *testing.T) {
|
||
// 目标:覆盖 fetchCred 的成功路径(line 545: return parseDownloadInfo(t))
|
||
// 策略:probe 第一次 401 → fetchCred 成功返回新凭证 → probe 第二次成功 →
|
||
// 进入 ranged download → 完成下载
|
||
|
||
content := makeTestContent(100)
|
||
origClient := driveRangeClient
|
||
t.Cleanup(func() { driveRangeClient = origClient })
|
||
|
||
var probeAttempt atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := probeAttempt.Add(1)
|
||
if n == 1 {
|
||
// 第一次 probe → 401 触发 fetchCred
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
return
|
||
}
|
||
// 后续请求正常处理 range
|
||
var start, end int64
|
||
if _, err := fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end); err != nil {
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write(content)
|
||
return
|
||
}
|
||
if end >= int64(len(content)) {
|
||
end = int64(len(content)) - 1
|
||
}
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
driveRangeClient = srv.Client()
|
||
|
||
dest := filepath.Join(t.TempDir(), "ranged-fetchcred-ok.bin")
|
||
// MCP step1: download info with fileSize >= 2*partSize (2MB) to trigger ranged path
|
||
// MCP step2: fetchCred returns new valid info (same server URL)
|
||
mcpResp1 := fmt.Sprintf(`{"resourceUrl":"%s/file.bin","fileSize":3000000}`, srv.URL)
|
||
mcpResp2 := fmt.Sprintf(`{"resourceUrl":"%s/file.bin","fileSize":3000000}`, srv.URL)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: mcpResp1}, // step1: download_file
|
||
{text: mcpResp2}, // step2: fetchCred (refresh)
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download", "--node", "node-1", "--output", dest, "--part-size", "1MB", "--parallel", "1", "--no-resume")
|
||
if err != nil {
|
||
t.Fatalf("ranged download 应成功: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// drive.go 覆盖率补全:download-version 命令的对称路径
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveDownloadVersionRangedPathLogfAndFetchCred(t *testing.T) {
|
||
// 覆盖 download-version 中的 logf(line 595-597)、fetchCred(line 632-637)、
|
||
// 和 return err(line 644)
|
||
|
||
origClient := driveRangeClient
|
||
t.Cleanup(func() { driveRangeClient = origClient })
|
||
|
||
driveRangeClient = &http.Client{
|
||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||
return &http.Response{
|
||
StatusCode: http.StatusUnauthorized,
|
||
Header: make(http.Header),
|
||
Body: io.NopCloser(strings.NewReader("token expired")),
|
||
}, nil
|
||
}),
|
||
}
|
||
|
||
dest := filepath.Join(t.TempDir(), "version-ranged.bin")
|
||
mcpResp := `{"downloadUrl":"https://fake.invalid/file.bin","fileSize":3000000}`
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: mcpResp}, // step1: download_file_version
|
||
{err: errors.New("refresh failed")}, // step2: fetchCred 失败
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download-version", "--node", "node-1", "--version", "3", "--output", dest,
|
||
"--part-size", "1MB", "--parallel", "1")
|
||
if err == nil {
|
||
t.Fatal("download-version fetchCred 失败应报错")
|
||
}
|
||
if errors.Is(err, context.Canceled) {
|
||
t.Fatal("错误不应是 context.Canceled")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageDriveDownloadVersionRangedFetchCredSuccess(t *testing.T) {
|
||
// 覆盖 download-version 的 fetchCred 成功路径(line 637)
|
||
|
||
totalSize := int64(3000000)
|
||
content := makeTestContent(int(totalSize))
|
||
origClient := driveRangeClient
|
||
t.Cleanup(func() { driveRangeClient = origClient })
|
||
|
||
var probeAttempt atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := probeAttempt.Add(1)
|
||
if n == 1 {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
return
|
||
}
|
||
var start, end int64
|
||
if _, err := fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end); err != nil {
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write(content)
|
||
return
|
||
}
|
||
if end >= int64(len(content)) {
|
||
end = int64(len(content)) - 1
|
||
}
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
driveRangeClient = srv.Client()
|
||
|
||
dest := filepath.Join(t.TempDir(), "version-fetchcred-ok.bin")
|
||
mcpResp1 := fmt.Sprintf(`{"downloadUrl":"%s/file.bin","fileSize":3000000}`, srv.URL)
|
||
mcpResp2 := fmt.Sprintf(`{"downloadUrl":"%s/file.bin","fileSize":3000000}`, srv.URL)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: mcpResp1},
|
||
{text: mcpResp2},
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download-version", "--node", "node-1", "--version", "3", "--output", dest,
|
||
"--part-size", "1MB", "--parallel", "1", "--no-resume")
|
||
if err != nil {
|
||
t.Fatalf("download-version ranged 应成功: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// drive.go 覆盖率补全:uploadToDrive refetch lambda
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveUploadRefetchLambda(t *testing.T) {
|
||
// 目标:覆盖 drive.go uploadToDrive 中 refetch lambda(line 2063-2065)
|
||
// 策略:httpPutFile 第一次返回 401 → 触发 refetch → callMCPToolReturnTextOnServer
|
||
// → scriptedCaller step2 返回新凭证 → 第二次 PUT 成功
|
||
|
||
oldPut := httpPutFile
|
||
putCalls := 0
|
||
httpPutFile = func(ctx context.Context, url string, headers map[string]string, filePath string, fileSize int64) error {
|
||
putCalls++
|
||
if putCalls == 1 {
|
||
return &httpStatusError{StatusCode: 401, Body: "token expired"}
|
||
}
|
||
return nil
|
||
}
|
||
t.Cleanup(func() { httpPutFile = oldPut })
|
||
|
||
file := filepath.Join(t.TempDir(), "upload.txt")
|
||
_ = os.WriteFile(file, []byte("content"), 0o600)
|
||
|
||
// step1: get_upload_info 成功
|
||
// step2: refetch (第二次 get_upload_info) 成功
|
||
// step3: commit_upload 成功
|
||
payload1 := `{"uploadId":"u1","resourceUrl":"https://upload.invalid/put1"}`
|
||
payload2 := `{"uploadId":"u2","resourceUrl":"https://upload.invalid/put2"}`
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: payload1},
|
||
{text: payload2},
|
||
{text: `{}`},
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller, "upload", "--file", file, "--folder", "folder-1")
|
||
if err != nil {
|
||
t.Fatalf("upload with refetch 应成功: %v", err)
|
||
}
|
||
if putCalls != 2 {
|
||
t.Fatalf("PUT 应被调用 2 次(首次 401 + 重试),实际: %d", putCalls)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 覆盖率补全:drive.go:501-503 logf 闭包体(通过命令路径触发分片下载)
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveDownloadCmdLogfInvocation(t *testing.T) {
|
||
// 目标:覆盖 drive.go download 命令中 dlOpts.logf 闭包体(line 501-503)。
|
||
// 策略:通过 executeDriveEdge 走完整命令路径,MCP 返回 fileSize >= 2*partSize(2MB),
|
||
// server 正确支持 Range,使 driveTransferDownload 走入 downloadRangedParts,
|
||
// logf 在分片启动时被调用。
|
||
|
||
totalSize := 2200000 // > 2*1MB threshold
|
||
content := makeTestContent(totalSize)
|
||
|
||
origClient := driveRangeClient
|
||
t.Cleanup(func() { driveRangeClient = origClient })
|
||
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
rng := r.Header.Get("Range")
|
||
if rng == "" {
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write(content)
|
||
return
|
||
}
|
||
var start, end int64
|
||
if _, err := fmt.Sscanf(rng, "bytes=%d-%d", &start, &end); err != nil || start >= int64(len(content)) {
|
||
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
|
||
return
|
||
}
|
||
if end >= int64(len(content)) {
|
||
end = int64(len(content)) - 1
|
||
}
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
driveRangeClient = srv.Client()
|
||
|
||
dest := filepath.Join(t.TempDir(), "cmd-logf.bin")
|
||
mcpResp := fmt.Sprintf(`{"resourceUrl":"%s/file.bin","fileSize":%d}`, srv.URL, totalSize)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: mcpResp},
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download", "--node", "node-1", "--output", dest, "--part-size", "1MB", "--parallel", "2", "--no-resume")
|
||
if err != nil {
|
||
t.Fatalf("ranged download via cmd 应成功: %v", err)
|
||
}
|
||
// 验证文件内容正确
|
||
got, _ := os.ReadFile(dest)
|
||
if len(got) != totalSize {
|
||
t.Fatalf("产物大小 %d != %d", len(got), totalSize)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 覆盖率补全:drive.go:553 download 命令 cancel + --no-resume else 分支
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveDownloadCancelNoResume(t *testing.T) {
|
||
// 目标:覆盖 download 命令中 context.Canceled + noResume=true 的 else 分支(line 553)。
|
||
// 条件:driveTransferDownload 返回 context.Canceled + --no-resume 已设置。
|
||
// 由于 fileSize 很小(< 2*16MB),走 downloadSingleWithAuthRetry → httpGetFile。
|
||
// mock httpGetFile 返回 context.Canceled。
|
||
|
||
oldGet := httpGetFile
|
||
httpGetFile = func(_ context.Context, _ string, _ map[string]string, _ string) error {
|
||
return context.Canceled
|
||
}
|
||
t.Cleanup(func() { httpGetFile = oldGet })
|
||
|
||
dest := filepath.Join(t.TempDir(), "cancel-noresume.bin")
|
||
mcpResp := `{"resourceUrl":"https://fake.invalid/f.bin","fileSize":100}`
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: mcpResp},
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download", "--node", "node-1", "--output", dest, "--no-resume")
|
||
if !errors.Is(err, context.Canceled) {
|
||
t.Fatalf("应返回 context.Canceled, got: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 覆盖率补全:drive.go:652 download-version 命令 cancel + --no-resume else 分支
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveDownloadVersionCancelNoResume(t *testing.T) {
|
||
// 目标:覆盖 download-version 命令中 context.Canceled + noResume=true 的 else 分支(line 652)。
|
||
|
||
oldGet := httpGetFile
|
||
httpGetFile = func(_ context.Context, _ string, _ map[string]string, _ string) error {
|
||
return context.Canceled
|
||
}
|
||
t.Cleanup(func() { httpGetFile = oldGet })
|
||
|
||
dest := filepath.Join(t.TempDir(), "ver-cancel-noresume.bin")
|
||
mcpResp := `{"downloadUrl":"https://fake.invalid/f.bin","fileSize":100}`
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: mcpResp},
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download-version", "--node", "node-1", "--version", "3", "--output", dest, "--no-resume")
|
||
if !errors.Is(err, context.Canceled) {
|
||
t.Fatalf("应返回 context.Canceled, got: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 覆盖率补全:drive_transfer.go:604 worker goroutine ctx cancel early return
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
func TestCrossPlatformCoverageDriveTransferWorkerCtxCancelBeforeProcess(t *testing.T) {
|
||
// 目标:覆盖 downloadRangedParts worker 中 "if runCtx.Err() != nil { return }"。
|
||
// 通过结构化 seam 让 worker 在收到唯一分片后确定性观察到取消状态;
|
||
// 不再依赖微秒级 timeout 与 goroutine 调度概率。
|
||
var checks atomic.Int32
|
||
testseam.Swap(t, &driveWorkerContextErr, func(context.Context) error {
|
||
checks.Add(1)
|
||
return context.Canceled
|
||
})
|
||
var requests atomic.Int32
|
||
testseam.Swap(t, &driveRangeClient, &http.Client{
|
||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||
requests.Add(1)
|
||
return nil, errors.New("worker context guard did not stop the request")
|
||
}),
|
||
})
|
||
|
||
creds := &driveCredentialState{url: "http://127.0.0.1:1/fake"}
|
||
dest := filepath.Join(t.TempDir(), "worker-context-guard.bin")
|
||
opts := driveDownloadOptions{partSize: 1, parallel: 1, resume: false, knownSize: 1}
|
||
if err := downloadRangedParts(context.Background(), creds, dest, 1, opts); err != nil {
|
||
t.Fatalf("downloadRangedParts context guard: %v", err)
|
||
}
|
||
if checks.Load() != 1 {
|
||
t.Fatalf("worker context checks = %d, want 1", checks.Load())
|
||
}
|
||
if requests.Load() != 0 {
|
||
t.Fatalf("worker requests = %d, want 0", requests.Load())
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// driveCredentialState.refresh: 版本校验(P1 修复验证)
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
// 刷新后版本一致 → 正常继续
|
||
func TestCrossPlatformCoverageRefreshVersionMatch(t *testing.T) {
|
||
cs := &driveCredentialState{
|
||
url: "u0",
|
||
initialVersion: 5,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "u1", nil, 5, nil
|
||
},
|
||
}
|
||
_, _, gen := cs.current()
|
||
if err := cs.refresh(context.Background(), gen); err != nil {
|
||
t.Fatalf("版本一致应成功: %v", err)
|
||
}
|
||
url, _, _ := cs.current()
|
||
if url != "u1" {
|
||
t.Errorf("刷新后 URL 应更新, got %q", url)
|
||
}
|
||
}
|
||
|
||
// 刷新后版本变化 → 返回错误终止
|
||
func TestCrossPlatformCoverageRefreshVersionMismatch(t *testing.T) {
|
||
cs := &driveCredentialState{
|
||
url: "u0",
|
||
initialVersion: 5,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "u1", nil, 8, nil
|
||
},
|
||
}
|
||
_, _, gen := cs.current()
|
||
err := cs.refresh(context.Background(), gen)
|
||
if err == nil {
|
||
t.Fatal("版本变更应返回错误")
|
||
}
|
||
if !strings.Contains(err.Error(), "文件版本已变更") {
|
||
t.Fatalf("错误信息应包含版本变更提示, got: %v", err)
|
||
}
|
||
if !strings.Contains(err.Error(), "5") || !strings.Contains(err.Error(), "8") {
|
||
t.Fatalf("错误信息应包含新旧版本号, got: %v", err)
|
||
}
|
||
// URL 不应更新(拒绝了变更)
|
||
url, _, _ := cs.current()
|
||
if url != "u0" {
|
||
t.Errorf("版本变更时 URL 不应更新, got %q", url)
|
||
}
|
||
}
|
||
|
||
// initialVersion=0(旧 MCP 不返回 version)→ 激进策略:返回 sentinel error
|
||
func TestCrossPlatformCoverageRefreshVersionZeroInitial(t *testing.T) {
|
||
cs := &driveCredentialState{
|
||
url: "u0",
|
||
initialVersion: 0, // 未知初始版本
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "u1", nil, 99, nil // 刷新返回任意版本
|
||
},
|
||
}
|
||
_, _, gen := cs.current()
|
||
err := cs.refresh(context.Background(), gen)
|
||
if !errors.Is(err, errCredentialRefreshVersionUnknown) {
|
||
t.Fatalf("initialVersion=0 应返回 errCredentialRefreshVersionUnknown, got: %v", err)
|
||
}
|
||
// 凭证应已更新
|
||
url, _, _ := cs.current()
|
||
if url != "u1" {
|
||
t.Errorf("凭证应已更新, got url=%q", url)
|
||
}
|
||
}
|
||
|
||
// 刷新返回 version=0(MCP 不返回 version)→ 激进策略:返回 sentinel error
|
||
func TestCrossPlatformCoverageRefreshVersionZeroReturned(t *testing.T) {
|
||
cs := &driveCredentialState{
|
||
url: "u0",
|
||
initialVersion: 5,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "u1", nil, 0, nil // 刷新未返回版本
|
||
},
|
||
}
|
||
_, _, gen := cs.current()
|
||
err := cs.refresh(context.Background(), gen)
|
||
if !errors.Is(err, errCredentialRefreshVersionUnknown) {
|
||
t.Fatalf("version=0 应返回 errCredentialRefreshVersionUnknown, got: %v", err)
|
||
}
|
||
// 凭证应已更新
|
||
url, _, _ := cs.current()
|
||
if url != "u1" {
|
||
t.Errorf("凭证应已更新, got url=%q", url)
|
||
}
|
||
}
|
||
|
||
// 分片下载中 refresh 触发版本变更检测 → 下载终止
|
||
func TestCrossPlatformCoverageDownloadRangedParts_VersionMismatchAbort(t *testing.T) {
|
||
var calls atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := calls.Add(1)
|
||
if n == 1 {
|
||
// 第一次(probeRangeSupport 不经此路径)/首分片请求 → 401
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
return
|
||
}
|
||
// 不应走到这里
|
||
w.WriteHeader(http.StatusOK)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{
|
||
url: srv.URL,
|
||
initialVersion: 3,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
// 刷新时版本变了
|
||
return srv.URL, nil, 7, nil
|
||
},
|
||
}
|
||
dest := filepath.Join(t.TempDir(), "version-abort.bin")
|
||
opts := driveDownloadOptions{partSize: 50, parallel: 1, resume: false}
|
||
err := downloadRangedParts(context.Background(), creds, dest, 100, opts)
|
||
if err == nil {
|
||
t.Fatal("版本变更应导致下载失败")
|
||
}
|
||
if !strings.Contains(err.Error(), "文件版本已变更") {
|
||
t.Fatalf("应包含版本变更错误, got: %v", err)
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// fetchRangeInto: Content-Range total 校验(P1 修复验证)
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
// Content-Range total 与 expectedTotal 一致 → 正常通过
|
||
func TestCrossPlatformCoverageFetchRangeInto_ContentRangeTotalMatch(t *testing.T) {
|
||
content := makeTestContent(200)
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "total-match-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(200); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 0, offset: 0, length: 50}
|
||
if err := fetchRangeInto(context.Background(), srv.URL, nil, f, part, 200); err != nil {
|
||
t.Fatalf("total 一致不应报错: %v", err)
|
||
}
|
||
}
|
||
|
||
// Content-Range total 与 expectedTotal 不一致 → 返回错误
|
||
func TestCrossPlatformCoverageFetchRangeInto_ContentRangeTotalMismatch(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
// 服务端声称 total=500,但调用方期望 200
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/500", start, end))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
data := make([]byte, end-start+1)
|
||
_, _ = w.Write(data)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "total-mismatch-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(200); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 0, offset: 0, length: 50}
|
||
err = fetchRangeInto(context.Background(), srv.URL, nil, f, part, 200)
|
||
if err == nil {
|
||
t.Fatal("total 不匹配应返回错误")
|
||
}
|
||
if !strings.Contains(err.Error(), "总长不匹配") {
|
||
t.Fatalf("错误信息应包含'总长不匹配': %v", err)
|
||
}
|
||
if !strings.Contains(err.Error(), "500") || !strings.Contains(err.Error(), "200") {
|
||
t.Fatalf("错误信息应包含实际和期望值: %v", err)
|
||
}
|
||
}
|
||
|
||
// expectedTotal=0(调用方不知道总长)→ 跳过校验
|
||
func TestCrossPlatformCoverageFetchRangeInto_ContentRangeTotalSkipWhenZeroExpected(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/9999", start, end))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
data := make([]byte, end-start+1)
|
||
_, _ = w.Write(data)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "total-skip-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 0, offset: 0, length: 50}
|
||
// expectedTotal=0 → 不校验 total
|
||
if err := fetchRangeInto(context.Background(), srv.URL, nil, f, part, 0); err != nil {
|
||
t.Fatalf("expectedTotal=0 应跳过 total 校验: %v", err)
|
||
}
|
||
}
|
||
|
||
// Content-Range total=-1("*" 未知)→ crTotal<0 → 跳过校验
|
||
func TestCrossPlatformCoverageFetchRangeInto_ContentRangeTotalUnknown(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
// total="*" → parseContentRange 返回 total=-1
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/*", start, end))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
data := make([]byte, end-start+1)
|
||
_, _ = w.Write(data)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "total-unknown-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(100); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
part := driveDownloadPart{index: 0, offset: 0, length: 50}
|
||
// crTotal=-1 (不 > 0) → 跳过 total 校验
|
||
if err := fetchRangeInto(context.Background(), srv.URL, nil, f, part, 200); err != nil {
|
||
t.Fatalf("total=* 应跳过校验: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageFetchRangeIntoMissingContentRange 验证 fetchRangeInto 在
|
||
// 206 响应缺少 Content-Range 头时返回明确错误,拒绝无法验证偏移一致性的分片。
|
||
func TestCrossPlatformCoverageFetchRangeIntoMissingContentRange(t *testing.T) {
|
||
content := makeTestContent(1024)
|
||
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var start, end int64
|
||
fmt.Sscanf(r.Header.Get("Range"), "bytes=%d-%d", &start, &end)
|
||
// 返回 206 但不带 Content-Range
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[start : end+1])
|
||
}))
|
||
defer srv.Close()
|
||
|
||
f, err := os.CreateTemp(t.TempDir(), "missing-cr-*")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer f.Close()
|
||
if err := f.Truncate(1024); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
part := driveDownloadPart{index: 0, offset: 0, length: 512}
|
||
err = fetchRangeInto(context.Background(), srv.URL, nil, f, part, 1024)
|
||
if err == nil {
|
||
t.Fatal("应当返回错误:分片响应缺少 Content-Range")
|
||
}
|
||
if !strings.Contains(err.Error(), "缺少 Content-Range") {
|
||
t.Fatalf("错误信息应包含'缺少 Content-Range': %v", err)
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveDownloadFetchCredParseError 覆盖 drive.go download 命令
|
||
// fetchCred 闭包中 parseDriveDownloadInfo 返回 error 的路径(line 547-549)。
|
||
func TestCrossPlatformCoverageDriveDownloadFetchCredParseError(t *testing.T) {
|
||
origClient := driveRangeClient
|
||
t.Cleanup(func() { driveRangeClient = origClient })
|
||
|
||
driveRangeClient = &http.Client{
|
||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||
return &http.Response{
|
||
StatusCode: http.StatusUnauthorized,
|
||
Header: make(http.Header),
|
||
Body: io.NopCloser(strings.NewReader("expired")),
|
||
}, nil
|
||
}),
|
||
}
|
||
|
||
dest := filepath.Join(t.TempDir(), "parse-err.bin")
|
||
// step1: 正常返回(进入分片路径);step2: fetchCred 返回可解析但无 URL 的 JSON
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: `{"resourceUrl":"https://fake.invalid/f","fileSize":3000000}`},
|
||
{text: `{"result":{}}`}, // parseDriveDownloadInfo 会返回 "downloadUrl 为空" 错误
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download", "--node", "n1", "--output", dest, "--part-size", "1MB", "--parallel", "1")
|
||
if err == nil {
|
||
t.Fatal("parseDriveDownloadInfo 失败应导致错误")
|
||
}
|
||
if !strings.Contains(err.Error(), "downloadUrl") && !strings.Contains(err.Error(), "下载链接") {
|
||
t.Fatalf("错误应包含解析失败信息: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveDownloadVersionFetchCredParseError 覆盖 download-version
|
||
// fetchCred 闭包中 parseDriveDownloadInfo 返回 error 的路径(line 650-652)。
|
||
func TestCrossPlatformCoverageDriveDownloadVersionFetchCredParseError(t *testing.T) {
|
||
origClient := driveRangeClient
|
||
t.Cleanup(func() { driveRangeClient = origClient })
|
||
|
||
driveRangeClient = &http.Client{
|
||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||
return &http.Response{
|
||
StatusCode: http.StatusUnauthorized,
|
||
Header: make(http.Header),
|
||
Body: io.NopCloser(strings.NewReader("expired")),
|
||
}, nil
|
||
}),
|
||
}
|
||
|
||
dest := filepath.Join(t.TempDir(), "ver-parse-err.bin")
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: `{"resourceUrl":"https://fake.invalid/f","fileSize":3000000}`},
|
||
{text: `{"result":{}}`},
|
||
}}
|
||
|
||
err := executeDriveEdge(t, caller,
|
||
"download-version", "--node", "n1", "--version", "3", "--output", dest, "--part-size", "1MB", "--parallel", "1")
|
||
if err == nil {
|
||
t.Fatal("parseDriveDownloadInfo 失败应导致错误")
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 激进策略:version=0 时凭证刷新后清空分片从头下载
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
// TestCrossPlatformCoverageRefreshVersionBothZero 双方 version=0 → 返回 sentinel error,凭证已更新
|
||
func TestCrossPlatformCoverageRefreshVersionBothZero(t *testing.T) {
|
||
cs := &driveCredentialState{
|
||
url: "u0",
|
||
initialVersion: 0,
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return "u1", map[string]string{"k": "v"}, 0, nil
|
||
},
|
||
}
|
||
_, _, gen := cs.current()
|
||
err := cs.refresh(context.Background(), gen)
|
||
if !errors.Is(err, errCredentialRefreshVersionUnknown) {
|
||
t.Fatalf("双方 version=0 应返回 sentinel error, got: %v", err)
|
||
}
|
||
// 凭证应已更新
|
||
url, headers, newGen := cs.current()
|
||
if url != "u1" {
|
||
t.Errorf("URL 应已更新, got %q", url)
|
||
}
|
||
if headers["k"] != "v" {
|
||
t.Errorf("headers 应已更新, got %v", headers)
|
||
}
|
||
if newGen != gen+1 {
|
||
t.Errorf("gen 应递增, got %d (was %d)", newGen, gen)
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDownloadRangedParts_VersionUnknownCleansCheckpoint
|
||
// version=0 的凭证刷新导致分片下载中止,checkpoint 和临时文件被清理。
|
||
func TestCrossPlatformCoverageDownloadRangedParts_VersionUnknownCleansCheckpoint(t *testing.T) {
|
||
content := makeTestContent(100)
|
||
// 服务端:第一次请求正常(probe),后续分片请求返回 401 触发 refresh
|
||
var probeCount atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
rng := r.Header.Get("Range")
|
||
if rng == "bytes=0-0" && probeCount.Add(1) == 1 {
|
||
// probe 请求正常
|
||
w.Header().Set("Content-Range", fmt.Sprintf("bytes 0-0/%d", len(content)))
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write(content[0:1])
|
||
return
|
||
}
|
||
// 分片请求全部返回 401 触发 credential refresh
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
}))
|
||
defer srv.Close()
|
||
|
||
dest := filepath.Join(t.TempDir(), "version-unknown-clean.bin")
|
||
partPath := dest + drivePartFileSuffix
|
||
metaPath := dest + drivePartMetaSuffix
|
||
|
||
// fetch 返回 version=0(旧 MCP),initialVersion=0(opts.version 未设置)
|
||
fetch := func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return srv.URL, nil, 0, nil // version=0
|
||
}
|
||
opts := driveDownloadOptions{partSize: 30, parallel: 1, resume: true, knownSize: 100}
|
||
// initialVersion 来自 opts.version(默认 0)
|
||
|
||
err := driveTransferDownload(context.Background(), fetch, srv.URL, nil, dest, opts)
|
||
if err == nil {
|
||
t.Fatal("version=0 刷新应导致下载失败")
|
||
}
|
||
if !errors.Is(err, errCredentialRefreshVersionUnknown) {
|
||
t.Fatalf("应包含 sentinel error, got: %v", err)
|
||
}
|
||
// 验证 checkpoint 和临时文件被清理
|
||
if _, statErr := os.Stat(metaPath); !os.IsNotExist(statErr) {
|
||
t.Error("checkpoint 应被清理")
|
||
}
|
||
if _, statErr := os.Stat(partPath); !os.IsNotExist(statErr) {
|
||
t.Error("分片临时文件应被清理")
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageProbeRangeSupport_VersionUnknownNonFatal
|
||
// 探测阶段 version=0 的 refresh 不应阻断探测(无已完成分片需保护)。
|
||
func TestCrossPlatformCoverageProbeRangeSupport_VersionUnknownNonFatal(t *testing.T) {
|
||
var attempts atomic.Int32
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
n := attempts.Add(1)
|
||
if n == 1 {
|
||
// 第一次:401 触发 refresh
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
fmt.Fprint(w, "expired")
|
||
return
|
||
}
|
||
// 第二次(refresh 后重试):正常 206
|
||
w.Header().Set("Content-Range", "bytes 0-0/500")
|
||
w.WriteHeader(http.StatusPartialContent)
|
||
_, _ = w.Write([]byte{0})
|
||
}))
|
||
defer srv.Close()
|
||
|
||
creds := &driveCredentialState{
|
||
url: srv.URL,
|
||
initialVersion: 0, // version 未知
|
||
fetch: func(ctx context.Context) (string, map[string]string, int, error) {
|
||
return srv.URL, nil, 0, nil // 仍然未知
|
||
},
|
||
}
|
||
total, resp, err := probeRangeSupport(context.Background(), creds)
|
||
if err != nil {
|
||
t.Fatalf("探测阶段 version=0 不应报错: %v", err)
|
||
}
|
||
if resp != nil {
|
||
t.Error("不应返回全量 resp")
|
||
}
|
||
if total != 500 {
|
||
t.Errorf("应返回正确 total=500, got %d", total)
|
||
}
|
||
if attempts.Load() != 2 {
|
||
t.Errorf("应请求 2 次(401→refresh→retry 成功), got %d", attempts.Load())
|
||
}
|
||
}
|