diff --git a/.github/workflows/android_unit_tests.yaml b/.github/workflows/android_unit_tests.yaml index 1ff80014f6a7..9dabddd4745e 100644 --- a/.github/workflows/android_unit_tests.yaml +++ b/.github/workflows/android_unit_tests.yaml @@ -43,7 +43,7 @@ jobs: # Tasks are listed per package rather than using the root testDebugUnitTest # task, which would also build the Dart sources of the aggregate test app. # Add a task here when a package gains an android/src/test directory. - run: cd tests/android && ./gradlew :firebase_crashlytics:testDebugUnitTest + run: cd tests/android && ./gradlew :firebase_crashlytics:testDebugUnitTest :cloud_functions:testDebugUnitTest - name: 'Upload test reports' if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a diff --git a/packages/cloud_functions/cloud_functions/android/build.gradle b/packages/cloud_functions/cloud_functions/android/build.gradle index 5cc439d6c2b2..d6dcebcbf0a5 100644 --- a/packages/cloud_functions/cloud_functions/android/build.gradle +++ b/packages/cloud_functions/cloud_functions/android/build.gradle @@ -78,6 +78,11 @@ android { implementation 'com.google.firebase:firebase-functions' implementation 'androidx.annotation:annotation:1.7.0' implementation 'org.reactivestreams:reactive-streams:1.0.4' + testImplementation 'junit:junit:4.13.2' + } + + testOptions { + unitTests.returnDefaultValues = true } } diff --git a/packages/cloud_functions/cloud_functions/android/src/main/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPlugin.kt b/packages/cloud_functions/cloud_functions/android/src/main/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPlugin.kt index f2aff1556e6d..a70556caf16a 100644 --- a/packages/cloud_functions/cloud_functions/android/src/main/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPlugin.kt +++ b/packages/cloud_functions/cloud_functions/android/src/main/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPlugin.kt @@ -136,7 +136,7 @@ class FlutterFirebaseFunctionsPlugin : FlutterPlugin, FlutterFirebasePlugin, Clo } } - details["code"] = code.replace("_", "-").lowercase(Locale.getDefault()) + details["code"] = mapFunctionsErrorCode(code) details["message"] = message if (additionalData != null) { @@ -164,6 +164,11 @@ class FlutterFirebaseFunctionsPlugin : FlutterPlugin, FlutterFirebasePlugin, Clo companion object { private const val METHOD_CHANNEL_NAME = "plugins.flutter.io/firebase_functions" + + // Locale.ROOT: Turkish/Azerbaijani map 'I' → 'ı' under Locale.getDefault(). + internal fun mapFunctionsErrorCode(code: String): String { + return code.replace("_", "-").lowercase(Locale.ROOT) + } } override fun call(arguments: Map, callback: (Result) -> Unit) { diff --git a/packages/cloud_functions/cloud_functions/android/src/test/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPluginTest.kt b/packages/cloud_functions/cloud_functions/android/src/test/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPluginTest.kt new file mode 100644 index 000000000000..dc3306864872 --- /dev/null +++ b/packages/cloud_functions/cloud_functions/android/src/test/kotlin/io/flutter/plugins/firebase/functions/FlutterFirebaseFunctionsPluginTest.kt @@ -0,0 +1,72 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package io.flutter.plugins.firebase.functions + +import java.util.Locale +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FlutterFirebaseFunctionsPluginTest { + private lateinit var originalLocale: Locale + + @Before + fun setUp() { + originalLocale = Locale.getDefault() + } + + @After + fun tearDown() { + Locale.setDefault(originalLocale) + } + + @Test + fun mapFunctionsErrorCode_usesRootLocaleUnderTurkishDefault() { + Locale.setDefault(Locale("tr", "TR")) + + // Canary: the JVM's Turkish locale really does map 'I' to 'ı'. + assertEquals( + "faıled-precondıtıon", + "FAILED_PRECONDITION".replace("_", "-").lowercase(Locale.getDefault())) + + assertMappedCodes() + } + + @Test + fun mapFunctionsErrorCode_usesRootLocaleUnderAzerbaijaniDefault() { + Locale.setDefault(Locale("az", "AZ")) + + assertEquals( + "faıled-precondıtıon", + "FAILED_PRECONDITION".replace("_", "-").lowercase(Locale.getDefault())) + + assertMappedCodes() + } + + private fun assertMappedCodes() { + val expected = + mapOf( + "FAILED_PRECONDITION" to "failed-precondition", + "INVALID_ARGUMENT" to "invalid-argument", + "PERMISSION_DENIED" to "permission-denied", + "UNAUTHENTICATED" to "unauthenticated", + "INTERNAL" to "internal", + "UNAVAILABLE" to "unavailable", + "DEADLINE_EXCEEDED" to "deadline-exceeded", + "ALREADY_EXISTS" to "already-exists", + "UNIMPLEMENTED" to "unimplemented", + "UNKNOWN" to "unknown", + "NOT_FOUND" to "not-found", + "ABORTED" to "aborted", + ) + + for ((enumName, canonical) in expected) { + assertEquals(canonical, FlutterFirebaseFunctionsPlugin.mapFunctionsErrorCode(enumName)) + } + } +}