Skip to content
Open
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
1 change: 1 addition & 0 deletions openai-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ kotlin {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation(libs.coroutines.test)
implementation(libs.ktor.client.mock)
}
}
val jvmMain by getting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ internal fun createHttpClient(config: OpenAIConfig): HttpClient {
install(Auth) {
bearer {
loadTokens {
BearerTokens(accessToken = config.token, refreshToken = "")
BearerTokens(accessToken = config.token, refreshToken = null)
}
// In the event of a 401, do NOT clear the token; just return the old token - OpenAI tokens do not have a refresh mechanism
refreshTokens {
oldTokens
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.aallam.openai.client.misc

import com.aallam.openai.client.OpenAIConfig
import com.aallam.openai.client.internal.createHttpClient
import io.ktor.client.engine.mock.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

/**
* Tests for HttpClient authentication behavior, specifically token handling
* when receiving 401 responses.
*/
class TestHttpClientAuth {

/**
* Verifies that the authentication token is NOT cleared when a 401 response
* is received from the API. This test ensures that the refreshTokens callback
* returns the old tokens instead of null (which would clear the token).
*
* To verify the test is working correctly:
* 1. Run the test - it should pass
* 2. Comment out the refreshTokens block in HttpClient.kt
* 3. Run the test again - it should fail
*/
@Test
fun testTokenNotClearedOn401() = runTest {
val testToken = "test-token-12345"
var requestCount = 0
val capturedAuthHeaders = mutableListOf<String?>()

// Create a mock engine that simulates:
// 1. First request: Returns 401 (triggers token refresh)
// 2. Second request: Should still have the token (not cleared)
val mockEngine = MockEngine { request ->
requestCount++
val authHeader = request.headers[HttpHeaders.Authorization]
capturedAuthHeaders.add(authHeader)

when (requestCount) {
1 -> {
// First request: return 401 to trigger refresh
respond(
content = """{"error": {"message": "Invalid token", "type": "invalid_request_error"}}""",
status = HttpStatusCode.Unauthorized,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
2 -> {
// Second request: should succeed with same token
respond(
content = """{"data": []}""",
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}
else -> error("Unexpected request count: $requestCount")
}
}

val config = OpenAIConfig(token = testToken, engine = mockEngine)
val httpClient = createHttpClient(config)

try {
// Make a request that will trigger 401 and then retry
httpClient.get("/test")

// Verify we made 2 requests (initial + retry after 401)
assertEquals(2, requestCount, "Should have made 2 requests (initial + retry)")

// Verify first request had the token
assertNotNull(capturedAuthHeaders[0], "First request should have Authorization header")
assertEquals("Bearer $testToken", capturedAuthHeaders[0])

// Verify second request STILL has the token (not cleared)
assertNotNull(capturedAuthHeaders[1], "Second request should still have Authorization header")
assertEquals("Bearer $testToken", capturedAuthHeaders[1],
"Token should NOT be cleared after 401 response")
} finally {
httpClient.close()
}
}
}