import Foundation import Testing @testable import gitbay /// The trust-surface invariants from the v1 security sweep (#2). These /// pin behavior so a refactor cannot quietly loosen it. struct SecuritySweepTests { // MARK: - Transport @Test func theDefaultSessionStoresNothing() { let configuration = GitbayClient.makeEphemeralSession().configuration // No disk cache: private repo content must never land in Cache.db. #expect(configuration.urlCache == nil) // No cookies accepted or sent — the API is bearer-token only. #expect(configuration.httpCookieAcceptPolicy == .never) #expect(configuration.httpShouldSetCookies == false) // Ephemeral: no credential or cookie storage backed by disk. #expect(configuration.httpCookieStorage?.cookies?.isEmpty ?? true) } @Test func theTokenOnlyTravelsToTheInstanceHost() async throws { let box = StubProtocol.box() let client = GitbayClient( instance: try GitbayInstance(url: "https://gitbay.org"), token: "gb_secret", session: box.session() ) box.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"username":"cmc"},"exit_code":0}"#)) nonisolated struct Who: Decodable, Sendable { let username: String } _ = try await client.read(["whoami"], as: Who.self) let seen = try #require(box.seen.first) #expect(seen.url.host() == "gitbay.org") #expect(seen.headers["Authorization"] == "Bearer gb_secret") // And no cookie header ever accompanies it. #expect(seen.headers["Cookie"] == nil) } @Test func redirectsOffTheInstanceAreRefusedByHostCheck() throws { let instance = try GitbayInstance(url: "https://gitbay.org") // The exact checks RedirectGuard and perform() rely on. #expect(!instance.isOwn(URL(string: "https://evil.example/api/v1/read")!)) #expect(!instance.isOwn(URL(string: "http://gitbay.org/api/v1/read")!)) // downgrade #expect(!instance.isOwn(URL(string: "https://gitbay.org.evil.example/x")!)) // suffix trick #expect(!instance.isOwn(URL(string: "https://gitbay.org:8443/x")!)) // port swap #expect(instance.isOwn(URL(string: "https://GITBAY.ORG/api/v1/cmd")!)) } @Test func plaintextHTTPIsRefusedForAnythingButLoopback() { #expect(throws: GitbayInstance.InvalidURL.self) { _ = try GitbayInstance(url: "http://forge.example") } #expect(throws: GitbayInstance.InvalidURL.self) { // A LAN address is not loopback; ATS would block it too. _ = try GitbayInstance(url: "http://192.168.1.10") } #expect((try? GitbayInstance(url: "http://localhost:3000")) != nil) #expect((try? GitbayInstance(url: "http://127.0.0.1:3000")) != nil) } // MARK: - Storage @Test func nothingTokenShapedReachesUserDefaults() async throws { let defaults = try #require(UserDefaults(suiteName: "security.\(UUID().uuidString)")) let store = MemoryTokenStore() let box = StubProtocol.box() box.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"username":"cmc"},"exit_code":0}"#)) let session = await SessionStore(store: store, defaults: defaults) { instance, token in GitbayClient(instance: instance, token: token, session: box.session()) } try await session.signIn(instanceURL: "gitbay.org", token: "gb_secret_value") // The only thing persisted outside the token store is the account // id, and no persisted value contains the token. let persisted = defaults.persistentDomain(forName: defaults.description) ?? [:] _ = persisted for (key, value) in defaults.dictionaryRepresentation() { if let text = value as? String { #expect(!text.contains("gb_secret_value"), "token leaked into UserDefaults key \(key)") } } #expect(store.token(for: "https://gitbay.org#cmc") == "gb_secret_value") } @Test func theCacheKeyIsAOneWayDigestOfTheToken() throws { // Two clients with different tokens must not share ETag cache // keys, and the key must not contain the token itself. let store = ETagStore() _ = store let key = ETagStore.key(account: "0a1b2c3d4e5f6a7b", argv: ["repo", "list"]) #expect(!key.contains("gb_")) #expect(key.contains("repo")) } }