diff --git a/build-logic/tools/build.gradle.kts b/build-logic/tools/build.gradle.kts index 745b6aff44..57c8906d8a 100644 --- a/build-logic/tools/build.gradle.kts +++ b/build-logic/tools/build.gradle.kts @@ -25,8 +25,9 @@ dependencies { implementation(gradleApi()) implementation(libs.dnsjava) - implementation(libs.square.okhttp3) + api(libs.square.okhttp3) testImplementation(testLibs.junit.junit) testImplementation(testLibs.mockk) + testImplementation(testLibs.square.mockwebserver) } diff --git a/build-logic/tools/src/main/java/org/signal/buildtools/SmartlingClient.kt b/build-logic/tools/src/main/java/org/signal/buildtools/SmartlingClient.kt index 5bb4a4632d..bda863346f 100644 --- a/build-logic/tools/src/main/java/org/signal/buildtools/SmartlingClient.kt +++ b/build-logic/tools/src/main/java/org/signal/buildtools/SmartlingClient.kt @@ -17,10 +17,12 @@ import java.util.concurrent.TimeUnit class SmartlingClient( private val userIdentifier: String, private val userSecret: String, - private val projectId: String + private val projectId: String, + private val baseUrl: String = "https://api.smartling.com", + client: OkHttpClient? = null ) { - private val client = OkHttpClient.Builder() + private val client = client ?: OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) @@ -41,7 +43,7 @@ class SmartlingClient( ).toString() val request = Request.Builder() - .url("https://api.smartling.com/auth-api/v2/authenticate") + .url("$baseUrl/auth-api/v2/authenticate") .post(jsonBody.toRequestBody("application/json".toMediaType())) .build() @@ -73,7 +75,7 @@ class SmartlingClient( .build() val request = Request.Builder() - .url("https://api.smartling.com/files-api/v2/projects/$projectId/file") + .url("$baseUrl/files-api/v2/projects/$projectId/file") .header("Authorization", "Bearer $authToken") .post(requestBody) .build() @@ -94,7 +96,7 @@ class SmartlingClient( @Suppress("UNCHECKED_CAST") fun getLocales(authToken: String, fileUri: String): List { val request = Request.Builder() - .url("https://api.smartling.com/files-api/v2/projects/$projectId/file/status?fileUri=$fileUri") + .url("$baseUrl/files-api/v2/projects/$projectId/file/status?fileUri=$fileUri") .header("Authorization", "Bearer $authToken") .get() .build() @@ -120,7 +122,7 @@ class SmartlingClient( */ fun downloadFile(authToken: String, fileUri: String, locale: String): String { val request = Request.Builder() - .url("https://api.smartling.com/files-api/v2/projects/$projectId/locales/$locale/file?fileUri=$fileUri") + .url("$baseUrl/files-api/v2/projects/$projectId/locales/$locale/file?fileUri=$fileUri") .header("Authorization", "Bearer $authToken") .get() .build() diff --git a/build-logic/tools/src/test/java/org/signal/buildtools/SmartlingClientTest.kt b/build-logic/tools/src/test/java/org/signal/buildtools/SmartlingClientTest.kt new file mode 100644 index 0000000000..a28c8680a4 --- /dev/null +++ b/build-logic/tools/src/test/java/org/signal/buildtools/SmartlingClientTest.kt @@ -0,0 +1,218 @@ +package org.signal.buildtools + +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import okhttp3.ExperimentalOkHttpApi +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.File + +@OptIn(ExperimentalOkHttpApi::class) +class SmartlingClientTest { + + private lateinit var server: MockWebServer + private lateinit var client: SmartlingClient + + @Before + fun setUp() { + server = MockWebServer() + server.start() + client = SmartlingClient( + userIdentifier = "test-user", + userSecret = "test-secret", + projectId = "test-project", + baseUrl = server.url("/").toString().removeSuffix("/") + ) + } + + @After + fun tearDown() { + server.shutdown() + } + + @Test + fun `authenticate returns access token on success`() { + server.enqueue( + MockResponse.Builder() + .code(200) + .body( + """ + { + "response": { + "data": { + "accessToken": "test-token-123" + } + } + } + """.trimIndent() + ) + .build() + ) + + val token = client.authenticate() + + assertEquals("test-token-123", token) + + val request = server.takeRequest() + assertEquals("POST", request.method) + assertEquals("/auth-api/v2/authenticate", request.path) + assertTrue(request.body.readUtf8().contains("test-user")) + } + + @Test(expected = SmartlingClient.SmartlingException::class) + fun `authenticate throws on HTTP error`() { + server.enqueue( + MockResponse.Builder() + .code(401) + .body("Unauthorized") + .build() + ) + + client.authenticate() + } + + @Test(expected = SmartlingClient.SmartlingException::class) + fun `authenticate throws on malformed response`() { + server.enqueue( + MockResponse.Builder() + .code(200) + .body("{}") + .build() + ) + + client.authenticate() + } + + @Test + fun `uploadFile returns response body on success`() { + server.enqueue( + MockResponse.Builder() + .code(200) + .body("""{"response": {"data": {"uploaded": true}}}""") + .build() + ) + + val tempFile = File.createTempFile("test-strings", ".xml").apply { + writeText("Test") + deleteOnExit() + } + + val response = client.uploadFile("auth-token", tempFile, "strings.xml") + + assertTrue(response.contains("uploaded")) + + val request = server.takeRequest() + assertEquals("POST", request.method) + assertEquals("/files-api/v2/projects/test-project/file", request.path) + assertEquals("Bearer auth-token", request.headers["Authorization"]) + } + + @Test(expected = SmartlingClient.SmartlingException::class) + fun `uploadFile throws on HTTP error`() { + server.enqueue( + MockResponse.Builder() + .code(500) + .body("Internal Server Error") + .build() + ) + + val tempFile = File.createTempFile("test-strings", ".xml").apply { + writeText("") + deleteOnExit() + } + + client.uploadFile("auth-token", tempFile, "strings.xml") + } + + @Test + fun `getLocales returns list of locale IDs`() { + server.enqueue( + MockResponse.Builder() + .code(200) + .body( + """ + { + "response": { + "data": { + "items": [ + {"localeId": "de"}, + {"localeId": "fr"}, + {"localeId": "es"} + ] + } + } + } + """.trimIndent() + ) + .build() + ) + + val locales = client.getLocales("auth-token", "strings.xml") + + assertEquals(listOf("de", "fr", "es"), locales) + + val request = server.takeRequest() + assertEquals("GET", request.method) + assertEquals("/files-api/v2/projects/test-project/file/status?fileUri=strings.xml", request.path) + assertEquals("Bearer auth-token", request.headers["Authorization"]) + } + + @Test(expected = SmartlingClient.SmartlingException::class) + fun `getLocales throws on HTTP error`() { + server.enqueue( + MockResponse.Builder() + .code(404) + .body("Not Found") + .build() + ) + + client.getLocales("auth-token", "strings.xml") + } + + @Test(expected = SmartlingClient.SmartlingException::class) + fun `getLocales throws on malformed response`() { + server.enqueue( + MockResponse.Builder() + .code(200) + .body("""{"response": {"data": {}}}""") + .build() + ) + + client.getLocales("auth-token", "strings.xml") + } + + @Test + fun `downloadFile returns file content`() { + val xmlContent = """Hallo""" + server.enqueue( + MockResponse.Builder() + .code(200) + .body(xmlContent) + .build() + ) + + val content = client.downloadFile("auth-token", "strings.xml", "de") + + assertEquals(xmlContent, content) + + val request = server.takeRequest() + assertEquals("GET", request.method) + assertEquals("/files-api/v2/projects/test-project/locales/de/file?fileUri=strings.xml", request.path) + assertEquals("Bearer auth-token", request.headers["Authorization"]) + } + + @Test(expected = SmartlingClient.SmartlingException::class) + fun `downloadFile throws on HTTP error`() { + server.enqueue( + MockResponse.Builder() + .code(500) + .body("Internal Server Error") + .build() + ) + + client.downloadFile("auth-token", "strings.xml", "de") + } +} diff --git a/gradle/test-libs.versions.toml b/gradle/test-libs.versions.toml index 004953242f..aa93ea8936 100644 --- a/gradle/test-libs.versions.toml +++ b/gradle/test-libs.versions.toml @@ -24,5 +24,6 @@ assertk = "com.willowtreeapps.assertk:assertk:0.28.1" mockk = "io.mockk:mockk:1.13.17" mockk-android = "io.mockk:mockk-android:1.13.17" +square-mockwebserver = "com.squareup.okhttp3:mockwebserver:5.0.0-alpha.16" conscrypt-openjdk-uber = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" diff-utils = "io.github.java-diff-utils:java-diff-utils:4.12"