|
| 1 | +@testable import CoderSDK |
| 2 | +import Foundation |
| 3 | +import Mocker |
| 4 | +import Testing |
| 5 | + |
| 6 | +@Suite(.timeLimit(.minutes(1))) |
| 7 | +struct UserAgentTests { |
| 8 | + // The pattern operators are expected to match against every Coder client. |
| 9 | + static let grammar = #"^coder-desktop(-core)?/[0-9]+\.[0-9]+\.[0-9]+ \(darwin/(amd64|arm64)\)$"# |
| 10 | + |
| 11 | + static let stubVersion = "0.8.4" |
| 12 | + static let stubServerVersion = "v2.18.2" |
| 13 | + |
| 14 | + @Test |
| 15 | + func appToken() { |
| 16 | + #expect( |
| 17 | + userAgent(component: .app, version: Self.stubVersion, arch: .arm64) |
| 18 | + == "coder-desktop/\(Self.stubVersion) (darwin/arm64)" |
| 19 | + ) |
| 20 | + } |
| 21 | + |
| 22 | + @Test |
| 23 | + func helperToken() { |
| 24 | + #expect( |
| 25 | + userAgent(component: .helper, version: Self.stubVersion, arch: .arm64) |
| 26 | + == "coder-desktop-core/\(Self.stubVersion) (darwin/arm64)" |
| 27 | + ) |
| 28 | + } |
| 29 | + |
| 30 | + // The CLI and the vpn-daemon report `amd64`, so an Intel build must too. |
| 31 | + @Test |
| 32 | + func intelReportsGoArchNotSwiftArch() { |
| 33 | + let ua = userAgent(component: .app, version: Self.stubVersion, arch: .amd64) |
| 34 | + #expect(ua == "coder-desktop/\(Self.stubVersion) (darwin/amd64)") |
| 35 | + #expect(!ua.contains("x86_64")) |
| 36 | + } |
| 37 | + |
| 38 | + @Test |
| 39 | + func matchesGrammar() throws { |
| 40 | + let regex = try Regex(Self.grammar) |
| 41 | + for component in [CoderComponent.app, .helper] { |
| 42 | + for arch in [GoArch.arm64, .amd64] { |
| 43 | + let ua = userAgent(component: component, version: Self.stubVersion, arch: arch) |
| 44 | + #expect(ua.contains(regex)) |
| 45 | + } |
| 46 | + // The version the running bundle actually reports. |
| 47 | + #expect(userAgent(component: component).contains(regex)) |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + @Test |
| 52 | + func missingVersionFallsBackToValidGrammar() throws { |
| 53 | + let regex = try Regex(Self.grammar) |
| 54 | + let ua = userAgent( |
| 55 | + component: .app, |
| 56 | + version: UserAgentDefaults.unknownVersion, |
| 57 | + arch: .arm64 |
| 58 | + ) |
| 59 | + #expect(ua == "coder-desktop/\(UserAgentDefaults.unknownVersion) (darwin/arm64)") |
| 60 | + #expect(ua.contains(regex)) |
| 61 | + } |
| 62 | + |
| 63 | + @Test |
| 64 | + func setOnRequest() async throws { |
| 65 | + // A distinct host per test: Mocker's registry is process-global and |
| 66 | + // suites run in parallel. |
| 67 | + let url = URL(string: "https://ua-default.example.com")! |
| 68 | + let client = Client(url: url, component: .helper) |
| 69 | + let sentUA = try await capturedUserAgent(client: client, url: url) |
| 70 | + |
| 71 | + #expect(try sentUA.contains(Regex(Self.grammar))) |
| 72 | + #expect(sentUA.hasPrefix("coder-desktop-core/")) |
| 73 | + } |
| 74 | + |
| 75 | + // A user-configured literal header must replace ours outright. Caller headers |
| 76 | + // are applied with `addValue`, which would otherwise comma-join the two. |
| 77 | + @Test |
| 78 | + func callerSuppliedUserAgentWins() async throws { |
| 79 | + let url = URL(string: "https://ua-override.example.com")! |
| 80 | + let custom = "my-own-agent/1.2.3" |
| 81 | + let client = Client( |
| 82 | + url: url, |
| 83 | + headers: [.init(name: "user-agent", value: custom)], |
| 84 | + component: .helper |
| 85 | + ) |
| 86 | + let sentUA = try await capturedUserAgent(client: client, url: url) |
| 87 | + |
| 88 | + #expect(sentUA == custom) |
| 89 | + #expect(!sentUA.contains("coder-desktop")) |
| 90 | + } |
| 91 | + |
| 92 | + /// Runs `buildInfo()` against a mock and returns the `User-Agent` it sent. |
| 93 | + private func capturedUserAgent(client: Client, url: URL) async throws -> String { |
| 94 | + var mock = try Mock( |
| 95 | + url: url.appending(path: "api/v2/buildinfo"), |
| 96 | + contentType: .json, |
| 97 | + statusCode: 200, |
| 98 | + data: [.get: CoderSDK.encoder.encode(BuildInfoResponse(version: Self.stubServerVersion))] |
| 99 | + ) |
| 100 | + var sentUA: String? |
| 101 | + mock.onRequestHandler = OnRequestHandler { req in |
| 102 | + sentUA = req.value(forHTTPHeaderField: Headers.userAgent) |
| 103 | + } |
| 104 | + mock.register() |
| 105 | + |
| 106 | + _ = try await client.buildInfo() |
| 107 | + return try #require(sentUA) |
| 108 | + } |
| 109 | +} |
0 commit comments