package notify import ( "context" "errors" "os" "path/filepath" "testing" "time" ) func TestFileSendSessionProgress(t *testing.T) { session := &fileSendSession{ fileID: "file-1", path: "/tmp/demo.bin", name: "demo.bin", size: 200, checksum: "sum", startedAt: time.Unix(100, 0), updatedAt: time.Unix(100, 0), } metaEvent := session.onMetaSent(time.Unix(100, 0)) if got, want := metaEvent.Kind, EnvelopeFileMeta; got != want { t.Fatalf("meta kind mismatch: got %v want %v", got, want) } if got, want := metaEvent.Total, int64(200); got != want { t.Fatalf("meta total mismatch: got %d want %d", got, want) } if got, want := metaEvent.Received, int64(0); got != want { t.Fatalf("meta received mismatch: got %d want %d", got, want) } chunkEvent, err := session.onChunkSent(0, 80, time.Unix(104, 0)) if err != nil { t.Fatalf("onChunkSent failed: %v", err) } if got, want := chunkEvent.Received, int64(80); got != want { t.Fatalf("chunk received mismatch: got %d want %d", got, want) } if got, want := chunkEvent.Percent, 40.0; got != want { t.Fatalf("chunk percent mismatch: got %v want %v", got, want) } if got, want := chunkEvent.Duration, 4*time.Second; got != want { t.Fatalf("chunk duration mismatch: got %v want %v", got, want) } if got, want := chunkEvent.StepDuration, 4*time.Second; got != want { t.Fatalf("chunk step duration mismatch: got %v want %v", got, want) } if got, want := chunkEvent.InstantRateBPS, 20.0; got != want { t.Fatalf("chunk instant rate mismatch: got %v want %v", got, want) } secondChunkEvent, err := session.onChunkSent(80, 120, time.Unix(108, 0)) if err != nil { t.Fatalf("second onChunkSent failed: %v", err) } if got, want := secondChunkEvent.Received, int64(200); got != want { t.Fatalf("second chunk received mismatch: got %d want %d", got, want) } if got, want := secondChunkEvent.Percent, 100.0; got != want { t.Fatalf("second chunk percent mismatch: got %v want %v", got, want) } if got, want := secondChunkEvent.RateBPS, 25.0; got != want { t.Fatalf("second chunk rate mismatch: got %v want %v", got, want) } if got, want := secondChunkEvent.StepDuration, 4*time.Second; got != want { t.Fatalf("second chunk step duration mismatch: got %v want %v", got, want) } if got, want := secondChunkEvent.InstantRateBPS, 30.0; got != want { t.Fatalf("second chunk instant rate mismatch: got %v want %v", got, want) } endEvent := session.onEndSent(time.Unix(110, 0)) if !endEvent.Done { t.Fatal("end event should be done") } if got, want := endEvent.Received, int64(200); got != want { t.Fatalf("end received mismatch: got %d want %d", got, want) } if got, want := endEvent.Percent, 100.0; got != want { t.Fatalf("end percent mismatch: got %v want %v", got, want) } if got, want := endEvent.Duration, 10*time.Second; got != want { t.Fatalf("end duration mismatch: got %v want %v", got, want) } if got, want := endEvent.StepDuration, 2*time.Second; got != want { t.Fatalf("end step duration mismatch: got %v want %v", got, want) } if got, want := endEvent.RateBPS, 20.0; got != want { t.Fatalf("end rate mismatch: got %v want %v", got, want) } if got, want := endEvent.InstantRateBPS, 0.0; got != want { t.Fatalf("end instant rate mismatch: got %v want %v", got, want) } } func TestSendFileWithHooksLogsLocalProgress(t *testing.T) { dir := t.TempDir() filePath := filepath.Join(dir, "demo.txt") data := []byte("hello notify send progress") if err := os.WriteFile(filePath, data, 0o644); err != nil { t.Fatalf("WriteFile failed: %v", err) } var sentKinds []EnvelopeKind var events []FileEvent err := sendFileWithHooks(context.Background(), filePath, fileSendHooks{ sendReliable: func(ctx context.Context, env Envelope) error { sentKinds = append(sentKinds, env.Kind) return nil }, sendAbort: func(fileID string, stage string, offset int64, cause error) error { t.Fatalf("unexpected abort: fileID=%s stage=%s offset=%d err=%v", fileID, stage, offset, cause) return nil }, publishEvent: func(event FileEvent) { events = append(events, event) }, }) if err != nil { t.Fatalf("sendFileWithHooks failed: %v", err) } if got, want := len(sentKinds), 3; got != want { t.Fatalf("sent kinds count mismatch: got %d want %d", got, want) } if sentKinds[0] != EnvelopeFileMeta || sentKinds[1] != EnvelopeFileChunk || sentKinds[2] != EnvelopeFileEnd { t.Fatalf("unexpected sent kinds: %v", sentKinds) } if got, want := len(events), 3; got != want { t.Fatalf("event count mismatch: got %d want %d", got, want) } if events[0].Kind != EnvelopeFileMeta || events[1].Kind != EnvelopeFileChunk || events[2].Kind != EnvelopeFileEnd { t.Fatalf("unexpected event kinds: %+v", []EnvelopeKind{events[0].Kind, events[1].Kind, events[2].Kind}) } if got, want := events[1].Received, int64(len(data)); got != want { t.Fatalf("chunk received mismatch: got %d want %d", got, want) } if !events[2].Done { t.Fatal("end event should be done") } if got, want := events[2].Received, int64(len(data)); got != want { t.Fatalf("end received mismatch: got %d want %d", got, want) } if got, want := events[2].Path, filePath; got != want { t.Fatalf("end path mismatch: got %q want %q", got, want) } if events[0].Packet.FileID == "" { t.Fatal("fileID should not be empty") } if events[0].Packet.FileID != events[1].Packet.FileID || events[1].Packet.FileID != events[2].Packet.FileID { t.Fatalf("fileID should stay stable across events: %+v", events) } } func TestSendFileWithHooksAbortOnChunkFailure(t *testing.T) { dir := t.TempDir() filePath := filepath.Join(dir, "demo.txt") data := []byte("hello notify send failure") if err := os.WriteFile(filePath, data, 0o644); err != nil { t.Fatalf("WriteFile failed: %v", err) } wantErr := errors.New("chunk ack timeout") var abortFileID string var abortStage string var abortOffset int64 var abortCause error var events []FileEvent err := sendFileWithHooks(context.Background(), filePath, fileSendHooks{ sendReliable: func(ctx context.Context, env Envelope) error { if env.Kind == EnvelopeFileChunk { return wantErr } return nil }, sendAbort: func(fileID string, stage string, offset int64, cause error) error { abortFileID = fileID abortStage = stage abortOffset = offset abortCause = cause return nil }, publishEvent: func(event FileEvent) { events = append(events, event) }, }) if !errors.Is(err, wantErr) { t.Fatalf("sendFileWithHooks error mismatch: got %v want %v", err, wantErr) } if abortFileID == "" { t.Fatal("abort should capture fileID") } if got, want := abortStage, "chunk"; got != want { t.Fatalf("abort stage mismatch: got %q want %q", got, want) } if got, want := abortOffset, int64(0); got != want { t.Fatalf("abort offset mismatch: got %d want %d", got, want) } if !errors.Is(abortCause, wantErr) { t.Fatalf("abort cause mismatch: got %v want %v", abortCause, wantErr) } if got, want := len(events), 2; got != want { t.Fatalf("event count mismatch: got %d want %d", got, want) } if got, want := events[0].Kind, EnvelopeFileMeta; got != want { t.Fatalf("first event kind mismatch: got %v want %v", got, want) } if got, want := events[1].Kind, EnvelopeFileAbort; got != want { t.Fatalf("abort event kind mismatch: got %v want %v", got, want) } if got, want := events[1].Packet.Stage, "chunk"; got != want { t.Fatalf("abort packet stage mismatch: got %q want %q", got, want) } if got, want := events[1].Received, int64(0); got != want { t.Fatalf("abort received mismatch: got %d want %d", got, want) } if !errors.Is(events[1].Err, wantErr) { t.Fatalf("abort event error mismatch: got %v want %v", events[1].Err, wantErr) } }