Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions SecVF.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
CC1111CC11111111CC111CC1 /* VMConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1111DD11111111DD111DD1 /* VMConfigurationTests.swift */; };
SPKT002SPKT002SPKT002002 /* SparklineViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = SPKT001SPKT001SPKT001001 /* SparklineViewTests.swift */; };
TUIT002TUIT002TUIT002002 /* TacticalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = TUIT001TUIT001TUIT001001 /* TacticalUITests.swift */; };
VCCT002VCCT002VCCT002002 /* VMCardCellAndCycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = VCCT001VCCT001VCCT001001 /* VMCardCellAndCycleTests.swift */; };
SBFT002SBFT002SBFT002002 /* SidebarFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = SBFT001SBFT001SBFT001001 /* SidebarFilterTests.swift */; };
NPT002NPT002NPT002002 /* NetworkPeersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = NPT001NPT001NPT001001 /* NetworkPeersTests.swift */; };
PFPT002PFPT002PFPT002002 /* PacketFilterPresetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PFPT001PFPT001PFPT001001 /* PacketFilterPresetsTests.swift */; };
Expand Down Expand Up @@ -73,6 +74,7 @@
THBT002THBT002THBT002002 /* TacticalHoverButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = THBT001THBT001THBT001001 /* TacticalHoverButton.swift */; };
TTRV002TTRV002TTRV002002 /* TacticalTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = TTRV001TTRV001TTRV001001 /* TacticalTableRowView.swift */; };
TESV002TESV002TESV002002 /* TacticalEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = TESV001TESV001TESV001001 /* TacticalEmptyStateView.swift */; };
VMCV002VMCV002VMCV002002 /* VMCardCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = VMCV001VMCV001VMCV001001 /* VMCardCellView.swift */; };
ICMW002ICMW002ICMW002002 /* ISOCacheManagerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ICMW001ICMW001ICMW001001 /* ISOCacheManagerWindow.swift */; };
SSWC002SSWC002SSWC002002 /* SwitchStatisticsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = SSWC001SSWC001SSWC001001 /* SwitchStatisticsWindowController.swift */; };
TSSC002TSSC002TSSC002002 /* TacticalSidebarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = TSSC001TSSC001TSSC001001 /* TacticalSidebarSection.swift */; };
Expand Down Expand Up @@ -129,6 +131,7 @@
DD1111DD11111111DD111DD1 /* VMConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigurationTests.swift; sourceTree = "<group>"; };
SPKT001SPKT001SPKT001001 /* SparklineViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparklineViewTests.swift; sourceTree = "<group>"; };
TUIT001TUIT001TUIT001001 /* TacticalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalUITests.swift; sourceTree = "<group>"; };
VCCT001VCCT001VCCT001001 /* VMCardCellAndCycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMCardCellAndCycleTests.swift; sourceTree = "<group>"; };
SBFT001SBFT001SBFT001001 /* SidebarFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarFilterTests.swift; sourceTree = "<group>"; };
NPT001NPT001NPT001001 /* NetworkPeersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkPeersTests.swift; sourceTree = "<group>"; };
PFPT001PFPT001PFPT001001 /* PacketFilterPresetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketFilterPresetsTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -169,6 +172,7 @@
THBT001THBT001THBT001001 /* TacticalHoverButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalHoverButton.swift; sourceTree = "<group>"; };
TTRV001TTRV001TTRV001001 /* TacticalTableRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalTableRowView.swift; sourceTree = "<group>"; };
TESV001TESV001TESV001001 /* TacticalEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalEmptyStateView.swift; sourceTree = "<group>"; };
VMCV001VMCV001VMCV001001 /* VMCardCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMCardCellView.swift; sourceTree = "<group>"; };
ICMW001ICMW001ICMW001001 /* ISOCacheManagerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ISOCacheManagerWindow.swift; sourceTree = "<group>"; };
SSWC001SSWC001SSWC001001 /* SwitchStatisticsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchStatisticsWindowController.swift; sourceTree = "<group>"; };
TSSC001TSSC001TSSC001001 /* TacticalSidebarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TacticalSidebarSection.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -250,6 +254,7 @@
THBT001THBT001THBT001001 /* TacticalHoverButton.swift */,
TTRV001TTRV001TTRV001001 /* TacticalTableRowView.swift */,
TESV001TESV001TESV001001 /* TacticalEmptyStateView.swift */,
VMCV001VMCV001VMCV001001 /* VMCardCellView.swift */,
ICMW001ICMW001ICMW001001 /* ISOCacheManagerWindow.swift */,
SSWC001SSWC001SSWC001001 /* SwitchStatisticsWindowController.swift */,
TSSC001TSSC001TSSC001001 /* TacticalSidebarSection.swift */,
Expand Down Expand Up @@ -307,6 +312,7 @@
DD1111DD11111111DD111DD1 /* VMConfigurationTests.swift */,
SPKT001SPKT001SPKT001001 /* SparklineViewTests.swift */,
TUIT001TUIT001TUIT001001 /* TacticalUITests.swift */,
VCCT001VCCT001VCCT001001 /* VMCardCellAndCycleTests.swift */,
SBFT001SBFT001SBFT001001 /* SidebarFilterTests.swift */,
NPT001NPT001NPT001001 /* NetworkPeersTests.swift */,
PFPT001PFPT001PFPT001001 /* PacketFilterPresetsTests.swift */,
Expand Down Expand Up @@ -475,6 +481,7 @@
THBT002THBT002THBT002002 /* TacticalHoverButton.swift in Sources */,
TTRV002TTRV002TTRV002002 /* TacticalTableRowView.swift in Sources */,
TESV002TESV002TESV002002 /* TacticalEmptyStateView.swift in Sources */,
VMCV002VMCV002VMCV002002 /* VMCardCellView.swift in Sources */,
ICMW002ICMW002ICMW002002 /* ISOCacheManagerWindow.swift in Sources */,
SSWC002SSWC002SSWC002002 /* SwitchStatisticsWindowController.swift in Sources */,
TSSC002TSSC002TSSC002002 /* TacticalSidebarSection.swift in Sources */,
Expand Down Expand Up @@ -511,6 +518,7 @@
CC1111CC11111111CC111CC1 /* VMConfigurationTests.swift in Sources */,
SPKT002SPKT002SPKT002002 /* SparklineViewTests.swift in Sources */,
TUIT002TUIT002TUIT002002 /* TacticalUITests.swift in Sources */,
VCCT002VCCT002VCCT002002 /* VMCardCellAndCycleTests.swift in Sources */,
SBFT002SBFT002SBFT002002 /* SidebarFilterTests.swift in Sources */,
NPT002NPT002NPT002002 /* NetworkPeersTests.swift in Sources */,
PFPT002PFPT002PFPT002002 /* PacketFilterPresetsTests.swift in Sources */,
Expand Down
164 changes: 164 additions & 0 deletions SecVF/Tests/VMCardCellAndCycleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// VMCardCellAndCycleTests.swift
// SecVFTests
//
// Tests for the multi-line VM card cell and the network-mode cycle.
// Cycle logic is the priority — the cell view tests just verify the
// card's public configuration surface (full UI rendering is covered
// visually).
//

import XCTest
@testable import SecVF

@MainActor
final class VMCardCellAndCycleTests: XCTestCase {

// MARK: - Network-mode cycle (pure logic)

func testCycleNATGoesToVirtualIsolated() {
let nat = VirtualNetworkConfig(mode: .nat, routerVMId: nil, isRouter: false)
let next = VMManager.nextNetworkConfig(after: nat)
XCTAssertEqual(next.mode, .virtual)
XCTAssertFalse(next.isRouter)
XCTAssertNil(next.routerVMId)
}

func testCycleVirtualIsolatedGoesToRouter() {
let virt = VirtualNetworkConfig(mode: .virtual, routerVMId: nil, isRouter: false)
let next = VMManager.nextNetworkConfig(after: virt)
XCTAssertEqual(next.mode, .virtual)
XCTAssertTrue(next.isRouter)
XCTAssertNil(next.routerVMId)
}

func testCycleRouterGoesToNAT() {
let router = VirtualNetworkConfig(mode: .virtual, routerVMId: nil, isRouter: true)
let next = VMManager.nextNetworkConfig(after: router)
XCTAssertEqual(next.mode, .nat)
XCTAssertFalse(next.isRouter)
XCTAssertNil(next.routerVMId)
}

func testCycleVirtualGuestDropsToNATCleanly() {
// A virtual-mode VM with a routerVMId set (guest of someone)
// shouldn't drag its stale guest-of relationship through the
// cycle. The cycle drops it to NAT and clears the routerVMId.
let guest = VirtualNetworkConfig(mode: .virtual,
routerVMId: UUID(),
isRouter: false)
let next = VMManager.nextNetworkConfig(after: guest)
XCTAssertEqual(next.mode, .nat)
XCTAssertNil(next.routerVMId)
XCTAssertFalse(next.isRouter)
}

func testCycleIsCircular() {
// Four cycles from NAT returns to NAT-equivalent state. Note:
// a guest VM short-circuits to NAT in one step, so this test
// walks the canonical NAT → Virtual → Router → NAT path.
var cfg = VirtualNetworkConfig(mode: .nat, routerVMId: nil, isRouter: false)
let initial = cfg
cfg = VMManager.nextNetworkConfig(after: cfg) // → Virtual
cfg = VMManager.nextNetworkConfig(after: cfg) // → Router
cfg = VMManager.nextNetworkConfig(after: cfg) // → NAT
XCTAssertEqual(cfg.mode, initial.mode)
XCTAssertEqual(cfg.isRouter, initial.isRouter)
XCTAssertEqual(cfg.routerVMId, initial.routerVMId)
}

// MARK: - Network chip rendering

func testNetworkChipShowsNATForNATMode() {
let cfg = VirtualNetworkConfig(mode: .nat, routerVMId: nil, isRouter: false)
let (text, _) = VMCardCellView.networkChipSpec(for: cfg)
XCTAssertTrue(text.contains("NAT"),
"NAT chip must mention NAT in its label, got: \(text)")
}

func testNetworkChipShowsRouterForRouterMode() {
let cfg = VirtualNetworkConfig(mode: .virtual, routerVMId: nil, isRouter: true)
let (text, _) = VMCardCellView.networkChipSpec(for: cfg)
XCTAssertTrue(text.uppercased().contains("ROUTER"),
"Router chip must mention ROUTER, got: \(text)")
}

func testNetworkChipShowsGuestForGuestMode() {
let cfg = VirtualNetworkConfig(mode: .virtual,
routerVMId: UUID(),
isRouter: false)
let (text, _) = VMCardCellView.networkChipSpec(for: cfg)
XCTAssertTrue(text.uppercased().contains("GUEST"),
"Guest chip must mention GUEST, got: \(text)")
}

func testNetworkChipShowsIsolatedForVirtualNoRouter() {
let cfg = VirtualNetworkConfig(mode: .virtual, routerVMId: nil, isRouter: false)
let (text, _) = VMCardCellView.networkChipSpec(for: cfg)
XCTAssertTrue(text.uppercased().contains("ISOLATED"),
"Isolated chip must mention ISOLATED, got: \(text)")
}

// MARK: - Status pill spec

func testStatusPillTextMatchesEachStatus() {
let cases: [(VMStatus, String)] = [
(.running, "RUNNING"),
(.starting, "STARTING"),
(.stopping, "STOPPING"),
(.stopped, "STOPPED"),
]
for (status, expected) in cases {
let (_, text, _) = VMCardCellView.statusPillSpec(for: status)
XCTAssertEqual(text, expected,
"Status \(status) should pill-render as \(expected), got \(text)")
}
}

// MARK: - Formatters

func testFormatPacketCountUsesGroupingSeparator() {
XCTAssertEqual(VMCardCellView.formatPacketCount(0), "0")
XCTAssertEqual(VMCardCellView.formatPacketCount(42), "42")
XCTAssertEqual(VMCardCellView.formatPacketCount(1_234), "1,234")
XCTAssertEqual(VMCardCellView.formatPacketCount(1_234_567),"1,234,567")
}

func testFormatBpsCrossesUnitThresholds() {
XCTAssertTrue(VMCardCellView.formatBps(500).contains("B/s"),
"Sub-kB rates show as raw B/s")
XCTAssertTrue(VMCardCellView.formatBps(2_048).contains("kB/s"),
"2 kB ≥ 1024 should render as kB/s")
XCTAssertTrue(VMCardCellView.formatBps(5_000_000).contains("MB/s"),
"5 MB ≥ 1 MB should render as MB/s")
XCTAssertTrue(VMCardCellView.formatBps(3_000_000_000).contains("GB/s"),
"3 GB ≥ 1 GB should render as GB/s")
}

// MARK: - Cell construction sanity

func testVMCardCellInitialFrameIsRowHeight() {
let cell = VMCardCellView(frame: NSRect(x: 0, y: 0, width: 400,
height: VMCardCellView.rowHeight))
XCTAssertEqual(cell.frame.height, VMCardCellView.rowHeight)
XCTAssertEqual(cell.identifier, VMCardCellView.identifier)
}

func testVMCardCellConfigureSurvivesAllStatuses() {
// Survival-only — fully painting the cell requires a graphics
// context that we don't have in a unit test. Verifying the
// configure() method doesn't crash for any status keeps the
// happy-path bound for table reload integrity.
let cell = VMCardCellView(frame: NSRect(x: 0, y: 0, width: 400,
height: VMCardCellView.rowHeight))
var vm = VMConfiguration(name: "X", bundlePath: "/tmp/X.bundle/")
for status in [VMStatus.running, .starting, .stopping, .stopped] {
vm.status = status
cell.configure(with: vm,
liveDownBps: 100.0,
liveUpBps: 50.0,
trafficSamples: [1, 2, 3],
packetCount: 42)
}
}
}
Loading
Loading