import Foundation import SessionCore import TestSupport import Testing import WireProtocol @testable import WebTerm /// C1 · The 401 terminal state end to end through the VM (B3 added /// `FailureReason.unauthorized`; this pins how the terminal presents it). /// /// Driven through a REAL `SessionEngine` over `TestSupport.FakeTransport`, so /// the assertion covers the whole path a wrong token actually takes: /// upgrade 401 → `TermTransportError.unauthorized` → engine terminal state → /// `SessionEvent.connection(.failed(.unauthorized))` → VM phase + copy. @MainActor @Suite("Terminal unauthorized (401)") struct TerminalUnauthorizedTests { @MainActor private struct Harness { let transport = FakeTransport() let clock = FakeClock() let engine: SessionEngine let viewModel: TerminalViewModel init() throws { let baseURL = try #require(URL(string: "http://192.168.1.5:3000")) let endpoint = try #require(HostEndpoint(baseURL: baseURL)) engine = SessionEngine( transport: transport, clock: clock, endpoint: endpoint, eventsSource: { _ in [] } ) viewModel = TerminalViewModel(engine: engine, events: engine.events) viewModel.start() } /// Upgrade rejected with 401 → `.connecting` + `.failed(.unauthorized)`. func openRejected() async { await transport.scriptConnectFailure(TermTransportError.unauthorized) await engine.open(sessionId: nil, cwd: nil) await viewModel.waitUntilProcessed(eventCount: 2) } } @Test("401 升级被拒 → 终态 failed(不是 reconnecting 转圈)") func unauthorizedIsTerminalNotRetried() async throws { let harness = try Harness() await harness.openRejected() guard case .failed = harness.viewModel.phase else { Issue.record("expected .failed, got \(harness.viewModel.phase)") return } #expect(harness.viewModel.banner == .none) // spinner cleared #expect(harness.viewModel.isReadOnly) #expect(await harness.transport.connectAttempts.count == 1) // no backoff loop } @Test("401 话术同时给出两条补救:令牌与 ALLOWED_ORIGINS(服务端两者都回 401)") func unauthorizedCopyOffersBothRemedies() async throws { let harness = try Harness() await harness.openRejected() guard case .failed(let message) = harness.viewModel.phase else { Issue.record("expected .failed, got \(harness.viewModel.phase)") return } #expect(message.contains("访问令牌")) #expect(message.contains("ALLOWED_ORIGINS")) #expect(message == TerminalViewModel.unauthorizedMessage) } @Test("401 横幅模型走 failed 分支(供 ReconnectBanner 渲染)") func unauthorizedRendersFailedBanner() async throws { let harness = try Harness() await harness.openRejected() #expect( harness.viewModel.bannerModel == .failed(message: TerminalViewModel.unauthorizedMessage) ) } @Test("401 终态下输入被丢弃(read-only,不再打向已死连接)") func unauthorizedDropsInput() async throws { let harness = try Harness() await harness.openRejected() harness.viewModel.sendInput("ls\r") #expect(harness.viewModel.droppedReadOnlyInputCount == 1) } }