package notify import ( itransfer "b612.me/notify/internal/transfer" "testing" "time" ) func TestFileTransferStateObserveFeedsQuery(t *testing.T) { state := newFileTransferState() now := time.Unix(1100, 0) state.observe(fileTransferDirectionSend, FileEvent{ Kind: EnvelopeFileChunk, Packet: FilePacket{FileID: "state-active", Size: 32}, Received: 10, Total: 32, Time: now, }) state.observe(fileTransferDirectionReceive, FileEvent{ Kind: EnvelopeFileAbort, Packet: FilePacket{FileID: "state-failed", Size: 16, Stage: "chunk"}, Received: 6, Total: 16, Time: now.Add(time.Second), Err: errString("receive failed"), }) active := state.active() if got, want := len(active.Send), 1; got != want { t.Fatalf("active send count mismatch: got %d want %d", got, want) } if got, want := active.Send[0].FileID, "state-active"; got != want { t.Fatalf("active send fileID mismatch: got %q want %q", got, want) } failed := state.failed() if got, want := len(failed.Receive), 1; got != want { t.Fatalf("failed receive count mismatch: got %d want %d", got, want) } if got, want := failed.Receive[0].FileID, "state-failed"; got != want { t.Fatalf("failed receive fileID mismatch: got %q want %q", got, want) } if got, want := failed.Receive[0].Failed, true; got != want { t.Fatalf("failed receive flag mismatch: got %v want %v", got, want) } } func TestFileTransferStateLatestHelpers(t *testing.T) { state := newFileTransferState() now := time.Unix(1200, 0) serverClient := &ClientConn{ClientID: "client-a"} state.observe(fileTransferDirectionSend, FileEvent{ Kind: EnvelopeFileChunk, Packet: FilePacket{FileID: "state-shared", Size: 40}, Received: 15, Total: 40, Time: now, }) state.observe(fileTransferDirectionReceive, FileEvent{ ClientConn: serverClient, Kind: EnvelopeFileEnd, Packet: FilePacket{FileID: "state-shared", Size: 40}, Received: 40, Total: 40, Done: true, Time: now.Add(time.Second), }) summary, ok := state.latest(fileTransferDirectionSend, clientFileScope(), "state-shared") if !ok { t.Fatal("latest send summary should exist") } if got, want := summary.Received, int64(15); got != want { t.Fatalf("latest send received mismatch: got %d want %d", got, want) } group := state.latestByFileID("state-shared") if got, want := len(group.Send), 1; got != want { t.Fatalf("latest group send count mismatch: got %d want %d", got, want) } if got, want := len(group.Receive), 1; got != want { t.Fatalf("latest group receive count mismatch: got %d want %d", got, want) } if got, want := group.Receive[0].Scope, serverFileScope(serverClient); got != want { t.Fatalf("latest group receive scope mismatch: got %q want %q", got, want) } send := state.latestSendByFileID("state-shared") if got, want := len(send), 1; got != want { t.Fatalf("latest send list count mismatch: got %d want %d", got, want) } receive := state.latestReceiveByFileID("state-shared") if got, want := len(receive), 1; got != want { t.Fatalf("latest receive list count mismatch: got %d want %d", got, want) } if got, want := receive[0].Done, true; got != want { t.Fatalf("latest receive done mismatch: got %v want %v", got, want) } } func TestFileTransferStateObserveFeedsTransferRuntime(t *testing.T) { state := newFileTransferState() now := time.Unix(1300, 0) state.observe(fileTransferDirectionSend, FileEvent{ Kind: EnvelopeFileMeta, Packet: FilePacket{FileID: "kernel-send", Name: "demo.bin", Size: 8, Checksum: "sum-send"}, Path: "/tmp/demo.bin", Time: now, }) state.observe(fileTransferDirectionSend, FileEvent{ Kind: EnvelopeFileChunk, Packet: FilePacket{FileID: "kernel-send", Name: "demo.bin", Size: 8, Checksum: "sum-send"}, Received: 8, Time: now.Add(time.Second), }) state.observe(fileTransferDirectionSend, FileEvent{ Kind: EnvelopeFileEnd, Packet: FilePacket{FileID: "kernel-send", Name: "demo.bin", Size: 8, Checksum: "sum-send"}, Received: 8, Done: true, Time: now.Add(2 * time.Second), }) sendSnapshot, ok := state.runtimeSnapshot(fileTransferDirectionSend, clientFileScope(), "kernel-send") if !ok { t.Fatal("send snapshot should exist") } if got, want := sendSnapshot.State, itransfer.StateDone; got != want { t.Fatalf("send state = %v, want %v", got, want) } if got, want := sendSnapshot.Direction, itransfer.DirectionSend; got != want { t.Fatalf("send direction = %v, want %v", got, want) } if got, want := sendSnapshot.SentBytes, int64(8); got != want { t.Fatalf("send bytes = %d, want %d", got, want) } if got, want := sendSnapshot.AckedBytes, int64(8); got != want { t.Fatalf("send acked bytes = %d, want %d", got, want) } if got := sendSnapshot.Metadata["name"]; got != "demo.bin" { t.Fatalf("send metadata name = %q, want demo.bin", got) } state.observe(fileTransferDirectionReceive, FileEvent{ Kind: EnvelopeFileMeta, Packet: FilePacket{FileID: "kernel-recv", Name: "recv.bin", Size: 6, Checksum: "sum-recv"}, Time: now, }) state.observe(fileTransferDirectionReceive, FileEvent{ Kind: EnvelopeFileChunk, Packet: FilePacket{FileID: "kernel-recv", Name: "recv.bin", Size: 6, Checksum: "sum-recv"}, Received: 6, Time: now.Add(time.Second), }) state.observe(fileTransferDirectionReceive, FileEvent{ Kind: EnvelopeFileEnd, Packet: FilePacket{FileID: "kernel-recv", Name: "recv.bin", Size: 6, Checksum: "sum-recv"}, Received: 6, Done: true, Time: now.Add(2 * time.Second), }) recvSnapshot, ok := state.runtimeSnapshot(fileTransferDirectionReceive, clientFileScope(), "kernel-recv") if !ok { t.Fatal("receive snapshot should exist") } if got, want := recvSnapshot.State, itransfer.StateDone; got != want { t.Fatalf("receive state = %v, want %v", got, want) } if got, want := recvSnapshot.Direction, itransfer.DirectionReceive; got != want { t.Fatalf("receive direction = %v, want %v", got, want) } if got, want := recvSnapshot.ReceivedBytes, int64(6); got != want { t.Fatalf("receive bytes = %d, want %d", got, want) } } func TestFileTransferStateRuntimeResilienceStats(t *testing.T) { state := newFileTransferState() session := &fileSendSession{ fileID: "kernel-retry", path: "/tmp/retry.bin", name: "retry.bin", size: 5, checksum: "sum-retry", } state.startRuntimeSendSession(clientFileScope(), clientFileScope(), 0, session) state.recordRuntimeTimeout(fileTransferDirectionSend, clientFileScope(), session.fileID) state.recordRuntimeRetry(fileTransferDirectionSend, clientFileScope(), session.fileID) state.observe(fileTransferDirectionSend, FileEvent{ Kind: EnvelopeFileAbort, Packet: FilePacket{FileID: session.fileID, Name: session.name, Size: session.size, Checksum: session.checksum, Stage: "meta"}, Received: 0, Err: errString("ack timeout"), Time: time.Unix(1400, 0), }) snapshot, ok := state.runtimeSnapshot(fileTransferDirectionSend, clientFileScope(), session.fileID) if !ok { t.Fatal("runtime snapshot should exist") } if got, want := snapshot.TimeoutCount, 1; got != want { t.Fatalf("timeout count = %d, want %d", got, want) } if got, want := snapshot.RetryCount, 1; got != want { t.Fatalf("retry count = %d, want %d", got, want) } if got, want := snapshot.State, itransfer.StateAborted; got != want { t.Fatalf("state = %v, want %v", got, want) } if got, want := snapshot.LastError, "ack timeout"; got != want { t.Fatalf("last error = %q, want %q", got, want) } if got, want := snapshot.Stage, "meta"; got != want { t.Fatalf("stage = %q, want %q", got, want) } if got, want := snapshot.LastFailureStage, "meta"; got != want { t.Fatalf("last failure stage = %q, want %q", got, want) } } func TestFileTransferStateRuntimeSeparatesScopeAndDirection(t *testing.T) { state := newFileTransferState() now := time.Unix(1450, 0) serverClient := &ClientConn{ClientID: "client-b"} state.observe(fileTransferDirectionSend, FileEvent{ Kind: EnvelopeFileMeta, Packet: FilePacket{FileID: "shared-id", Name: "send.bin", Size: 4, Checksum: "sum-send"}, Time: now, }) state.observe(fileTransferDirectionReceive, FileEvent{ ClientConn: serverClient, Kind: EnvelopeFileMeta, Packet: FilePacket{FileID: "shared-id", Name: "recv.bin", Size: 6, Checksum: "sum-recv"}, Time: now.Add(time.Second), }) sendSnapshot, ok := state.runtimeSnapshot(fileTransferDirectionSend, clientFileScope(), "shared-id") if !ok { t.Fatal("send snapshot should exist") } if got, want := sendSnapshot.Direction, itransfer.DirectionSend; got != want { t.Fatalf("send direction = %v, want %v", got, want) } recvSnapshot, ok := state.runtimeSnapshot(fileTransferDirectionReceive, serverTransportScope(serverClient), "shared-id") if !ok { t.Fatal("receive snapshot should exist") } if got, want := recvSnapshot.Direction, itransfer.DirectionReceive; got != want { t.Fatalf("receive direction = %v, want %v", got, want) } } func TestFileTransferStateRuntimeSeparatesServerTransportGenerations(t *testing.T) { state := newFileTransferState() now := time.Unix(1500, 0) serverClient := &ClientConn{ClientID: "client-gen"} serverClient.markClientConnIdentityBound() serverClient.markClientConnStreamTransport() serverClient.markClientConnTransportAttached() state.observe(fileTransferDirectionReceive, FileEvent{ ClientConn: serverClient, Kind: EnvelopeFileMeta, Packet: FilePacket{FileID: "shared-transfer", Name: "recv-a.bin", Size: 4, Checksum: "sum-a"}, Time: now, }) firstScope := serverTransportScope(serverClient) firstSnapshot, ok := state.runtimeSnapshot(fileTransferDirectionReceive, firstScope, "shared-transfer") if !ok { t.Fatal("first generation snapshot should exist") } if got, want := firstSnapshot.Metadata[transferMetadataScopeKey], serverFileScope(serverClient); got != want { t.Fatalf("first generation public scope metadata = %q, want %q", got, want) } serverClient.markClientConnTransportDetached("read error", nil) serverClient.markClientConnTransportAttached() state.observe(fileTransferDirectionReceive, FileEvent{ ClientConn: serverClient, Kind: EnvelopeFileMeta, Packet: FilePacket{FileID: "shared-transfer", Name: "recv-b.bin", Size: 6, Checksum: "sum-b"}, Time: now.Add(time.Second), }) secondScope := serverTransportScope(serverClient) if secondScope == firstScope { t.Fatalf("runtime scope should change across transport generations: got %q", secondScope) } secondSnapshot, ok := state.runtimeSnapshot(fileTransferDirectionReceive, secondScope, "shared-transfer") if !ok { t.Fatal("second generation snapshot should exist") } if got, want := transferSnapshotRuntimeScope(secondSnapshot.Metadata), secondScope; got != want { t.Fatalf("second generation runtime scope metadata = %q, want %q", got, want) } if got, want := transferSnapshotTransportGeneration(secondSnapshot.Metadata), uint64(2); got != want { t.Fatalf("second generation transport generation metadata = %d, want %d", got, want) } } func TestFileTransferStateLatestByFileIDQueryResolvesTransportGeneration(t *testing.T) { state := newFileTransferState() now := time.Unix(1510, 0) serverClient := &ClientConn{ClientID: "client-query-gen"} serverClient.markClientConnIdentityBound() serverClient.markClientConnStreamTransport() serverClient.markClientConnTransportAttached() state.observe(fileTransferDirectionReceive, FileEvent{ ClientConn: serverClient, Kind: EnvelopeFileChunk, Packet: FilePacket{FileID: "shared", Size: 30}, Received: 6, Total: 30, Time: now, }) firstRuntimeScope := serverTransportScope(serverClient) logicalScope := serverFileScope(serverClient) serverClient.markClientConnTransportDetached("read error", nil) serverClient.markClientConnTransportAttached() state.observe(fileTransferDirectionReceive, FileEvent{ ClientConn: serverClient, Kind: EnvelopeFileChunk, Packet: FilePacket{FileID: "shared", Size: 30}, Received: 10, Total: 30, Time: now.Add(time.Second), }) secondRuntimeScope := serverTransportScope(serverClient) legacy := state.latestReceiveByFileID("shared") if got, want := len(legacy), 1; got != want { t.Fatalf("legacy receive count mismatch: got %d want %d", got, want) } gen1 := state.latestReceiveByFileIDQuery("shared", fileTransferSummaryQuery{ Scope: logicalScope, TransportGeneration: 1, MatchTransportGeneration: true, }) if got, want := len(gen1), 1; got != want { t.Fatalf("generation-1 receive count mismatch: got %d want %d", got, want) } if got, want := gen1[0].RuntimeScope, firstRuntimeScope; got != want { t.Fatalf("generation-1 runtime scope mismatch: got %q want %q", got, want) } gen2 := state.latestReceiveByFileIDQuery("shared", fileTransferSummaryQuery{ Scope: logicalScope, TransportGeneration: 2, MatchTransportGeneration: true, }) if got, want := len(gen2), 1; got != want { t.Fatalf("generation-2 receive count mismatch: got %d want %d", got, want) } if got, want := gen2[0].RuntimeScope, secondRuntimeScope; got != want { t.Fatalf("generation-2 runtime scope mismatch: got %q want %q", got, want) } }