250 lines
12 KiB
Swift
250 lines
12 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
|
|
@Suite(.serialized)
|
|
struct GeminiOAuthRecoveryAPITests {
|
|
@Test
|
|
func `explicit oauth2 js path overrides installed gemini cli`() async throws {
|
|
let env = try GeminiTestEnvironment()
|
|
defer { env.cleanup() }
|
|
try env.writeCredentials(
|
|
accessToken: "old-token",
|
|
refreshToken: "refresh-token",
|
|
expiry: Date().addingTimeInterval(-3600),
|
|
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"))
|
|
|
|
let oauthURL = env.homeURL.appendingPathComponent("oauth2.js")
|
|
try """
|
|
const OAUTH_CLIENT_ID = 'path-client-id';
|
|
const OAUTH_CLIENT_SECRET = 'path-client-secret';
|
|
""".write(to: oauthURL, atomically: true, encoding: .utf8)
|
|
|
|
let binURL = try env.writeFakeGeminiCLI()
|
|
let oauthEnv = GeminiOAuthConfig.EnvironmentValues(oauth2JSPath: oauthURL.path)
|
|
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
|
|
guard let url = request.url, let host = url.host else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
switch host {
|
|
case "oauth2.googleapis.com":
|
|
let body = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
|
guard body.contains("client_id=path-client-id") else {
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
|
|
}
|
|
let json = GeminiAPITestHelpers.jsonData([
|
|
"access_token": "new-token",
|
|
"expires_in": 3600,
|
|
"id_token": GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
|
|
])
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
|
|
case "cloudresourcemanager.googleapis.com":
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.jsonData(["projects": []]))
|
|
case "cloudcode-pa.googleapis.com":
|
|
if url.path == "/v1internal:loadCodeAssist" {
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.loadCodeAssistStandardTierResponse())
|
|
}
|
|
if url.path != "/v1internal:retrieveUserQuota" {
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
|
|
}
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.sampleQuotaResponse())
|
|
default:
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
|
|
}
|
|
}
|
|
|
|
let probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
|
|
let snapshot = try await BinaryLocator.$geminiBinaryPathOverrideForTesting.withValue(binURL.path) {
|
|
try await GeminiOAuthConfig.$environmentOverride.withValue(oauthEnv) {
|
|
try await probe.fetch()
|
|
}
|
|
}
|
|
#expect(snapshot.accountPlan == "Paid")
|
|
}
|
|
|
|
@Test
|
|
func `prefers environment oauth client over installed gemini cli`() async throws {
|
|
let env = try GeminiTestEnvironment()
|
|
defer { env.cleanup() }
|
|
try env.writeCredentials(
|
|
accessToken: "old-token",
|
|
refreshToken: "refresh-token",
|
|
expiry: Date().addingTimeInterval(-3600),
|
|
idToken: nil)
|
|
|
|
let binURL = try env.writeFakeGeminiCLI()
|
|
let oauthEnv = GeminiOAuthConfig.EnvironmentValues(
|
|
clientID: "env-client-id",
|
|
clientSecret: "env-client-secret")
|
|
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
|
|
guard let url = request.url, let host = url.host else {
|
|
throw URLError(.badURL)
|
|
}
|
|
switch host {
|
|
case "oauth2.googleapis.com":
|
|
let body = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
|
guard body.contains("client_id=env-client-id"),
|
|
body.contains("client_secret=env-client-secret")
|
|
else {
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
|
|
}
|
|
let json = GeminiAPITestHelpers.jsonData([
|
|
"access_token": "new-token",
|
|
"expires_in": 3600,
|
|
])
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
|
|
case "cloudresourcemanager.googleapis.com":
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.jsonData(["projects": []]))
|
|
case "cloudcode-pa.googleapis.com":
|
|
if url.path == "/v1internal:loadCodeAssist" {
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.loadCodeAssistFreeTierResponse())
|
|
}
|
|
if url.path != "/v1internal:retrieveUserQuota" {
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
|
|
}
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.sampleFlashQuotaResponse())
|
|
default:
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
|
|
}
|
|
}
|
|
|
|
let probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
|
|
_ = try await BinaryLocator.$geminiBinaryPathOverrideForTesting.withValue(binURL.path) {
|
|
try await GeminiOAuthConfig.$environmentOverride.withValue(oauthEnv) {
|
|
try await probe.fetch()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `refreshes via known Homebrew Cellar libexec path without gemini binary`() async throws {
|
|
let env = try GeminiTestEnvironment()
|
|
defer { env.cleanup() }
|
|
try env.writeCredentials(
|
|
accessToken: "old-token",
|
|
refreshToken: "refresh-token",
|
|
expiry: Date().addingTimeInterval(-3600),
|
|
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"))
|
|
|
|
// Resolvable gemini binary exists but omits OAuth config; Cellar package root
|
|
// under a synthetic Homebrew prefix holds the credentials instead.
|
|
let binURL = try env.writeFakeGeminiCLI(includeOAuth: false, layout: .npmNested)
|
|
let homebrewPrefix = env.homeURL.appendingPathComponent("homebrew-prefix")
|
|
try Self.plantHomebrewCellarGeminiPackage(
|
|
under: homebrewPrefix,
|
|
clientID: "cellar-client-id",
|
|
clientSecret: "cellar-client-secret")
|
|
|
|
let clearOAuthEnv = GeminiOAuthConfig.EnvironmentValues()
|
|
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
|
|
guard let url = request.url, let host = url.host else {
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
switch host {
|
|
case "oauth2.googleapis.com":
|
|
let body = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
|
guard body.contains("client_id=cellar-client-id") else {
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
|
|
}
|
|
let json = GeminiAPITestHelpers.jsonData([
|
|
"access_token": "new-token",
|
|
"expires_in": 3600,
|
|
"id_token": GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
|
|
])
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
|
|
case "cloudresourcemanager.googleapis.com":
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.jsonData(["projects": []]))
|
|
case "cloudcode-pa.googleapis.com":
|
|
guard request.value(forHTTPHeaderField: "Authorization") == "Bearer new-token" else {
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
|
|
}
|
|
if url.path == "/v1internal:loadCodeAssist" {
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.loadCodeAssistStandardTierResponse())
|
|
}
|
|
if url.path != "/v1internal:retrieveUserQuota" {
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
|
|
}
|
|
return GeminiAPITestHelpers.response(
|
|
url: url.absoluteString,
|
|
status: 200,
|
|
body: GeminiAPITestHelpers.sampleQuotaResponse())
|
|
default:
|
|
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
|
|
}
|
|
}
|
|
|
|
let probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
|
|
let snapshot = try await BinaryLocator.$geminiBinaryPathOverrideForTesting.withValue(binURL.path) {
|
|
try await GeminiOAuthConfig.$environmentOverride.withValue(clearOAuthEnv) {
|
|
try await GeminiStatusProbe.$knownInstallPrefixesForTesting.withValue([homebrewPrefix.path]) {
|
|
try await probe.fetch()
|
|
}
|
|
}
|
|
}
|
|
#expect(snapshot.accountPlan == "Paid")
|
|
}
|
|
|
|
private static func plantHomebrewCellarGeminiPackage(
|
|
under prefix: URL,
|
|
clientID: String,
|
|
clientSecret: String) throws
|
|
{
|
|
let packageRoot = prefix
|
|
.appendingPathComponent("Cellar")
|
|
.appendingPathComponent("gemini-cli")
|
|
.appendingPathComponent("0.41.2")
|
|
.appendingPathComponent("libexec")
|
|
.appendingPathComponent("lib")
|
|
.appendingPathComponent("node_modules")
|
|
.appendingPathComponent("@google")
|
|
.appendingPathComponent("gemini-cli")
|
|
let bundleDir = packageRoot.appendingPathComponent("bundle")
|
|
try FileManager.default.createDirectory(at: bundleDir, withIntermediateDirectories: true)
|
|
try """
|
|
{
|
|
"name": "@google/gemini-cli"
|
|
}
|
|
""".write(
|
|
to: packageRoot.appendingPathComponent("package.json"),
|
|
atomically: true,
|
|
encoding: .utf8)
|
|
try "#!/usr/bin/env node\nawait import('./chunk-OAUTH.js');\n".write(
|
|
to: bundleDir.appendingPathComponent("gemini.js"),
|
|
atomically: true,
|
|
encoding: .utf8)
|
|
try """
|
|
var OAUTH_CLIENT_ID = "\(clientID)";
|
|
var OAUTH_CLIENT_SECRET = "\(clientSecret)";
|
|
""".write(
|
|
to: bundleDir.appendingPathComponent("chunk-OAUTH.js"),
|
|
atomically: true,
|
|
encoding: .utf8)
|
|
}
|
|
}
|