diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74a27e03..f3486e58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,33 @@ jobs: cache: gradle - name: Run Unit Tests Coverage - run: ./gradlew koverXmlReportDebug + run: ./gradlew :library:koverXmlReportDebug -Dkover.path.normalization=relative + + - name: Check if coverage report was generated + run: | + echo "Checking for Kover report files..." + find library/build -name "*.xml" -path "*/kover/*" -ls || echo "No Kover XML files found" + ls -la library/build/reports/kover/ || echo "Kover reports directory does not exist" + + - name: Normalize paths in coverage report + run: | + # Check if the report file exists + if [ ! -f "library/build/reports/kover/reportDebug.xml" ]; then + echo "ERROR: Coverage report not found at library/build/reports/kover/reportDebug.xml" + exit 1 + fi + + # Create a backup of the original report + cp library/build/reports/kover/reportDebug.xml library/build/reports/kover/reportDebug.xml.bak + + # Replace absolute paths with relative ones for GitHub Actions runners + # GitHub Actions uses /home/runner/work/paypal-messages-android/paypal-messages-android/ + sed -i 's|/home/runner/work/paypal-messages-android/paypal-messages-android/|./|g' library/build/reports/kover/reportDebug.xml + + # Also handle any other absolute paths that might appear + sed -i 's|${{ github.workspace }}/|./|g' library/build/reports/kover/reportDebug.xml + + echo "Path normalization completed successfully" - name: Show Coverage Report XML run: cat library/build/reports/kover/reportDebug.xml @@ -60,7 +86,7 @@ jobs: with: path: ${{ github.workspace }}/library/build/reports/kover/reportDebug.xml title: Code Coverage - update-comment: true + update-comment: false min-coverage-overall: 85 min-coverage-changed-files: 85 coverage-counter-type: LINE @@ -226,6 +252,62 @@ jobs: ./gradlew demo:compileDebugKotlin echo "✅ Reflection test code compiles successfully" + test_manifest_merger: + name: Manifest Merger Conflict Test + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Build Test Module with Manifest Merger + run: | + echo "🔍 Testing manifest merger with usesCleartextTraffic conflict..." + + # Build the test module that declares its own usesCleartextTraffic + # This will fail if the library's manifest causes a merger conflict + ./gradlew :manifest-merger-test:assembleDebug + + if [ $? -eq 0 ]; then + echo "✅ Manifest merger succeeded - no conflict detected!" + else + echo "❌ ERROR: Manifest merger failed - library causes usesCleartextTraffic conflict!" + exit 1 + fi + + - name: Verify Merged Manifest + run: | + echo "🔍 Verifying merged manifest content..." + + # Check that the merged manifest exists + MERGED_MANIFEST="manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml" + + if [ ! -f "$MERGED_MANIFEST" ]; then + echo "❌ ERROR: Merged manifest not found at $MERGED_MANIFEST" + exit 1 + fi + + echo "📄 Merged manifest content:" + cat "$MERGED_MANIFEST" + + # Verify that usesCleartextTraffic is present in merged manifest + if grep -q "usesCleartextTraffic" "$MERGED_MANIFEST"; then + echo "✅ usesCleartextTraffic attribute found in merged manifest" + else + echo "❌ WARNING: usesCleartextTraffic attribute not found in merged manifest" + fi + + echo "🎉 Manifest merger test completed successfully!" + test_publishing_tasks: name: Publishing Tasks Verification runs-on: ubuntu-latest diff --git a/build.gradle b/build.gradle index 85bd7398..88348110 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ plugins { id 'signing' id 'maven-publish' id 'io.codearte.nexus-staging' version '0.30.0' + id 'org.jetbrains.kotlinx.kover' version '0.7.6' apply false } tasks.register('ktLint', LintTask) { @@ -284,6 +285,15 @@ tasks.register('checkCentralPortalDeployment') { subprojects { group = "com.paypal.messages" + + // Disable Kover for demo module + if (project.name == 'demo') { + pluginManager.withPlugin('org.jetbrains.kotlinx.kover') { + kover { + isDisabled = true + } + } + } } // Configure Nexus Staging to close and release automatically (OSSRH s01) diff --git a/library/build.gradle b/library/build.gradle index fee6d3d3..804a232a 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,7 +3,7 @@ plugins { id 'kotlin-android' id 'org.jetbrains.kotlin.android' id 'de.mannodermaus.android-junit5' version "1.9.3.0" - id 'org.jetbrains.kotlinx.kover' version '0.7.6' + id 'org.jetbrains.kotlinx.kover' } group = "com.paypal.messages" @@ -122,6 +122,12 @@ tasks.withType(Test).configureEach { // Example filter: excludeTestsMatching "com.paypal.messages.PayPalModalActivityTest" } +// Configure all Kover tasks to use relative paths +// Note: Task type configuration disabled for Kover 0.7.6 compatibility +// tasks.withType(org.jetbrains.kotlinx.kover.gradle.plugin.tasks.KoverXmlReportTask).configureEach { +// systemProperty 'kover.path.normalization', 'relative' +// } + // Optimized approach for handling problematic tests android { testOptions { @@ -179,45 +185,84 @@ tasks.register('testMemoryIntensiveClasses') { description = "Runs memory-intensive tests in isolated JVM processes" } -koverReport { - androidReports('debug') { - filters { - excludes { - annotatedBy( - '*KoverExcludeGenerated' - ) - classes( - // AnalyticsLogger, - 'com.paypal.messages.analytics.AnalyticsLogger*', - // config - 'com.paypal.messages.config.message.PayPalMessageEventsCallbacks\$*', - 'com.paypal.messages.config.message.PayPalMessageViewStateCallbacks\$*', - 'com.paypal.messages.config.modal.ModalEvents\$*', - // extensions, - 'com.paypal.messages.extensions.Int*', - // io - 'com.paypal.messages.io.Api', - 'com.paypal.messages.io.Api\$*', - 'com.paypal.messages.io.LocalStorage*', - 'com.paypal.messages.io.OnActionCompleted', - // utils - 'com.paypal.messages.utils.LogCat*', - // UI Stuff - '*Fragment', - '*Fragment\$*', - '*Activity', - '*Activity\$*', - 'com.paypal.messages.PayPalMessageView*', - 'com.paypal.messages.RoundedWebView', - 'com.paypal.messages.RoundedWebView\$*', - '*.databinding.*', - '*.BuildConfig' - ) - } +// Note: The koverXmlReportDebug task is now provided by the Kover plugin +// If you encounter SSL/dependency issues locally, you can temporarily disable Kover by: +// 1. Commenting out the Kover plugin line above +// 2. Setting SKIP_KOVER=true environment variable + +// Add a task to test the coverage report paths +tasks.register('testKoverPaths') { + dependsOn 'koverXmlReportDebug' + + doLast { + println "\n\n===== Testing Kover paths in coverage report =====\n" + + def reportFile = file("${buildDir}/reports/kover/reportDebug.xml") + if (!reportFile.exists()) { + throw new GradleException("Coverage report file not found at: ${reportFile.absolutePath}") + } + + println "Checking coverage report for absolute paths..." + + def reportContent = reportFile.text + def absolutePathPattern = "/Users/" + def hasAbsolutePaths = reportContent.contains(absolutePathPattern) + + if (hasAbsolutePaths) { + println "WARNING: Found absolute paths in the coverage report!" + println "Creating normalized version..." + + // Create a backup of the original report + def backupFile = file("${buildDir}/reports/kover/reportDebug.xml.bak") + backupFile.text = reportContent + + // Replace absolute paths with relative ones + def normalizedContent = reportContent.replaceAll("/Users/[^/]*/Code/paypal-messages-android/", "./") + reportFile.text = normalizedContent + + println "Normalized coverage report saved to: ${reportFile.absolutePath}" + } else { + println "SUCCESS: No absolute paths found in the coverage report." } } + description = "Tests and fixes paths in Kover coverage reports" } +kover { + filters { + classes { + excludes += [ + // AnalyticsLogger, + 'com.paypal.messages.analytics.AnalyticsLogger*', + // config + 'com.paypal.messages.config.message.PayPalMessageEventsCallbacks\$*', + 'com.paypal.messages.config.message.PayPalMessageViewStateCallbacks\$*', + 'com.paypal.messages.config.modal.ModalEvents\$*', + // extensions, + 'com.paypal.messages.extensions.Int*', + // io + 'com.paypal.messages.io.Api', + 'com.paypal.messages.io.Api\$*', + 'com.paypal.messages.io.LocalStorage*', + 'com.paypal.messages.io.OnActionCompleted', + // utils + 'com.paypal.messages.utils.LogCat*', + // UI Stuff + '*Fragment', + '*Fragment\$*', + '*Activity', + '*Activity\$*', + 'com.paypal.messages.PayPalMessageView*', + 'com.paypal.messages.RoundedWebView', + 'com.paypal.messages.RoundedWebView\$*', + '*.databinding.*', + '*.BuildConfig' + ] + } + } +} + + dependencies { implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' @@ -277,7 +322,7 @@ project.ext.version = modules.sdkVersionName project.ext.pom_name = "PayPal Messages" project.ext.pom_desc = "The PayPal Android SDK Messages Module: Promote offers to your customers such as Pay Later and PayPal Credit." -// Optionally disable Kover locally to avoid agent download issues +// Optionally disable Kover tasks locally to avoid agent download issues if (System.getenv('SKIP_KOVER') == 'true' || project.hasProperty('skipKover')) { tasks.matching { it.name.toLowerCase().startsWith('kover') }.configureEach { enabled = false } } diff --git a/library/src/test/java/com/paypal/messages/LogoAssetTest.kt b/library/src/test/java/com/paypal/messages/LogoAssetTest.kt index e643fc17..dc729f92 100644 --- a/library/src/test/java/com/paypal/messages/LogoAssetTest.kt +++ b/library/src/test/java/com/paypal/messages/LogoAssetTest.kt @@ -1,6 +1,8 @@ package com.paypal.messages import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class LogoAssetTest { @@ -21,4 +23,151 @@ class LogoAssetTest { assertEquals(resId, imageAsset.resId) } + + @Test + fun testStringAssetWithDifferentResIds() { + // Test that different string resources create different assets + val creditAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val defaultAsset = LogoAsset.StringAsset(R.string.logo_none_label_default) + + assertNotEquals(creditAsset.resId, defaultAsset.resId) + } + + @Test + fun testImageAssetWithDifferentResIds() { + // Test that different drawable resources create different assets + val standardAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val whiteAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_white) + + assertNotEquals(standardAsset.resId, whiteAsset.resId) + } + + @Test + fun testStringAssetEquality() { + // Test that StringAssets with the same resId are equal + val asset1 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val asset2 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + + assertEquals(asset1, asset2) + assertEquals(asset1.resId, asset2.resId) + } + + @Test + fun testImageAssetEquality() { + // Test that ImageAssets with the same resId are equal + val asset1 = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val asset2 = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + assertEquals(asset1, asset2) + assertEquals(asset1.resId, asset2.resId) + } + + @Test + fun testStringAssetInequality() { + // Test that StringAssets with different resIds are not equal + val asset1 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val asset2 = LogoAsset.StringAsset(R.string.logo_none_label_default) + + assertNotEquals(asset1, asset2) + } + + @Test + fun testImageAssetInequality() { + // Test that ImageAssets with different resIds are not equal + val asset1 = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val asset2 = LogoAsset.ImageAsset(R.drawable.logo_primary_white) + + assertNotEquals(asset1, asset2) + } + + @Test + fun testStringAssetIsLogoAsset() { + // Test that StringAsset is a subclass of LogoAsset + val stringAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + assertTrue(stringAsset is LogoAsset) + } + + @Test + fun testImageAssetIsLogoAsset() { + // Test that ImageAsset is a subclass of LogoAsset + val imageAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + assertTrue(imageAsset is LogoAsset) + } + + @Test + fun testSealedClassHierarchy() { + // Test that both StringAsset and ImageAsset extend LogoAsset + val stringAsset: LogoAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val imageAsset: LogoAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + // Verify type hierarchy + assertTrue(stringAsset is LogoAsset.StringAsset) + assertTrue(imageAsset is LogoAsset.ImageAsset) + } + + @Test + fun testWhenExpressionCoverage() { + // Test that sealed class works with when expressions + val stringAsset: LogoAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val imageAsset: LogoAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + val stringResult = when (stringAsset) { + is LogoAsset.StringAsset -> "string" + is LogoAsset.ImageAsset -> "image" + } + + val imageResult = when (imageAsset) { + is LogoAsset.StringAsset -> "string" + is LogoAsset.ImageAsset -> "image" + } + + assertEquals("string", stringResult) + assertEquals("image", imageResult) + } + + @Test + fun testCopyOfDataClass() { + // Test that data class copy works for StringAsset + val original = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val copied = original.copy(resId = R.string.logo_none_label_default) + + assertNotEquals(original, copied) + assertEquals(R.string.logo_none_label_credit, original.resId) + assertEquals(R.string.logo_none_label_default, copied.resId) + } + + @Test + fun testCopyOfImageDataClass() { + // Test that data class copy works for ImageAsset + val original = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val copied = original.copy(resId = R.drawable.logo_primary_white) + + assertNotEquals(original, copied) + assertEquals(R.drawable.logo_primary_standard, original.resId) + assertEquals(R.drawable.logo_primary_white, copied.resId) + } + + @Test + fun testHashCodeConsistency() { + // Test that equal objects have the same hash code + val asset1 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val asset2 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + + assertEquals(asset1.hashCode(), asset2.hashCode()) + } + + @Test + fun testToStringOutput() { + // Test that toString provides useful output + val stringAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val imageAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + val stringOutput = stringAsset.toString() + val imageOutput = imageAsset.toString() + + assertTrue(stringOutput.contains("StringAsset")) + assertTrue(stringOutput.contains("resId")) + assertTrue(imageOutput.contains("ImageAsset")) + assertTrue(imageOutput.contains("resId")) + } } diff --git a/library/src/test/java/com/paypal/messages/LogoTest.kt b/library/src/test/java/com/paypal/messages/LogoTest.kt index 21dc5e3a..e9a8cd40 100644 --- a/library/src/test/java/com/paypal/messages/LogoTest.kt +++ b/library/src/test/java/com/paypal/messages/LogoTest.kt @@ -2,6 +2,7 @@ package com.paypal.messages import com.paypal.messages.config.ProductGroup import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource @@ -222,4 +223,133 @@ class LogoTest { Arguments.of(LogoType.NONE, null, R.string.logo_none_label_default), ) } + + @org.junit.jupiter.api.Test + fun testLogoDefaultConstructor() { + // Test that Logo uses defaults when no parameters provided + val logo = Logo() + val asset = logo.getAsset(Color.BLACK) + + // Should default to PRIMARY logo with PAY_LATER product group + assertEquals(LogoAsset.ImageAsset(R.drawable.logo_primary_standard), asset) + } + + @org.junit.jupiter.api.Test + fun testLogoWithOnlyLogoType() { + // Test Logo with only logoType specified + val logo = Logo(logoType = LogoType.ALTERNATIVE) + val asset = logo.getAsset(Color.BLACK) + + // Should use ALTERNATIVE with default PAY_LATER + assertEquals(LogoAsset.ImageAsset(R.drawable.logo_alternative_standard), asset) + } + + @org.junit.jupiter.api.Test + fun testLogoWithOnlyProductGroup() { + // Test Logo with only productGroup specified + val logo = Logo(productGroup = ProductGroup.PAYPAL_CREDIT) + val asset = logo.getAsset(Color.BLACK) + + // Should use default PRIMARY with PAYPAL_CREDIT + assertEquals(LogoAsset.ImageAsset(R.drawable.logo_credit_primary_standard), asset) + } + + @org.junit.jupiter.api.Test + fun testAllColorVariantsForPrimary() { + // Ensure all color variants work for primary logo + val logo = Logo(LogoType.PRIMARY, ProductGroup.PAY_LATER) + + val blackAsset = logo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val whiteAsset = logo.getAsset(Color.WHITE) as LogoAsset.ImageAsset + val monochromeAsset = logo.getAsset(Color.MONOCHROME) as LogoAsset.ImageAsset + val grayscaleAsset = logo.getAsset(Color.GRAYSCALE) as LogoAsset.ImageAsset + + assertEquals(R.drawable.logo_primary_standard, blackAsset.resId) + assertEquals(R.drawable.logo_primary_white, whiteAsset.resId) + assertEquals(R.drawable.logo_primary_monochrome, monochromeAsset.resId) + assertEquals(R.drawable.logo_primary_grayscale, grayscaleAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testAllColorVariantsForAlternative() { + // Ensure all color variants work for alternative logo + val logo = Logo(LogoType.ALTERNATIVE, ProductGroup.PAY_LATER) + + val blackAsset = logo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val whiteAsset = logo.getAsset(Color.WHITE) as LogoAsset.ImageAsset + val monochromeAsset = logo.getAsset(Color.MONOCHROME) as LogoAsset.ImageAsset + val grayscaleAsset = logo.getAsset(Color.GRAYSCALE) as LogoAsset.ImageAsset + + assertEquals(R.drawable.logo_alternative_standard, blackAsset.resId) + assertEquals(R.drawable.logo_alternative_white, whiteAsset.resId) + assertEquals(R.drawable.logo_alternative_monochrome, monochromeAsset.resId) + assertEquals(R.drawable.logo_alternative_grayscale, grayscaleAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testAllColorVariantsForInline() { + // Ensure all color variants work for inline logo + val logo = Logo(LogoType.INLINE, ProductGroup.PAY_LATER) + + val blackAsset = logo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val whiteAsset = logo.getAsset(Color.WHITE) as LogoAsset.ImageAsset + val monochromeAsset = logo.getAsset(Color.MONOCHROME) as LogoAsset.ImageAsset + val grayscaleAsset = logo.getAsset(Color.GRAYSCALE) as LogoAsset.ImageAsset + + assertEquals(R.drawable.logo_inline_standard, blackAsset.resId) + assertEquals(R.drawable.logo_inline_white, whiteAsset.resId) + assertEquals(R.drawable.logo_inline_monochrome, monochromeAsset.resId) + assertEquals(R.drawable.logo_inline_grayscale, grayscaleAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testNoneLogoReturnsStringAsset() { + // Test that NONE logo type returns StringAsset, not ImageAsset + val logo = Logo(LogoType.NONE, ProductGroup.PAY_LATER) + val asset = logo.getAsset(Color.BLACK) + + // Verify it's a StringAsset + assertTrue(asset is LogoAsset.StringAsset) + assertEquals(R.string.logo_none_label_default, (asset as LogoAsset.StringAsset).resId) + } + + @org.junit.jupiter.api.Test + fun testImageLogoTypesReturnImageAsset() { + // Test that non-NONE logo types return ImageAsset + val logoTypes = listOf(LogoType.PRIMARY, LogoType.ALTERNATIVE, LogoType.INLINE) + + logoTypes.forEach { logoType -> + val logo = Logo(logoType, ProductGroup.PAY_LATER) + val asset = logo.getAsset(Color.BLACK) + + assertTrue(asset is LogoAsset.ImageAsset, "Logo type $logoType should return ImageAsset") + } + } + + @org.junit.jupiter.api.Test + fun testCreditVsPayLaterLogos() { + // Test that PAYPAL_CREDIT and PAY_LATER use different logos + val creditLogo = Logo(LogoType.PRIMARY, ProductGroup.PAYPAL_CREDIT) + val payLaterLogo = Logo(LogoType.PRIMARY, ProductGroup.PAY_LATER) + + val creditAsset = creditLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val payLaterAsset = payLaterLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + + // Should be different resource IDs + assertTrue(creditAsset.resId != payLaterAsset.resId) + assertEquals(R.drawable.logo_credit_primary_standard, creditAsset.resId) + assertEquals(R.drawable.logo_primary_standard, payLaterAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testNullProductGroupDefaultsToPayLater() { + // Test that null product group behaves like PAY_LATER + val nullProductLogo = Logo(LogoType.PRIMARY, null) + val payLaterLogo = Logo(LogoType.PRIMARY, ProductGroup.PAY_LATER) + + val nullAsset = nullProductLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val payLaterAsset = payLaterLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + + assertEquals(payLaterAsset.resId, nullAsset.resId) + } } diff --git a/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt b/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt index 8fe16618..111240d4 100644 --- a/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt +++ b/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt @@ -168,6 +168,254 @@ class PayPalComposableMessageTest { // This directly compares the error objects rather than just the message substring } + @Test + fun testLoadingCallback() { + // Arrange + val clientId = "test-client-id" + var loadingInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + viewStateCallbacks = PayPalMessageViewStateCallbacks( + onLoading = { loadingInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke loading callback + val messageView = createPayPalMessageView(mockContext, config) + config.viewStateCallbacks?.onLoading?.invoke() + + // Assert + assertEquals(true, loadingInvoked) + } + + @Test + fun testSuccessCallback() { + // Arrange + val clientId = "test-client-id" + var successInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + viewStateCallbacks = PayPalMessageViewStateCallbacks( + onSuccess = { successInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke success callback + val messageView = createPayPalMessageView(mockContext, config) + config.viewStateCallbacks?.onSuccess?.invoke() + + // Assert + assertEquals(true, successInvoked) + } + + @Test + fun testOnClickCallback() { + // Arrange + val clientId = "test-client-id" + var clickInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + eventsCallbacks = PayPalMessageEventsCallbacks( + onClick = { clickInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke click callback + val messageView = createPayPalMessageView(mockContext, config) + config.eventsCallbacks?.onClick?.invoke() + + // Assert + assertEquals(true, clickInvoked) + } + + @Test + fun testOnApplyCallback() { + // Arrange + val clientId = "test-client-id" + var applyInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + eventsCallbacks = PayPalMessageEventsCallbacks( + onApply = { applyInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke apply callback + val messageView = createPayPalMessageView(mockContext, config) + config.eventsCallbacks?.onApply?.invoke() + + // Assert + assertEquals(true, applyInvoked) + } + + @Test + fun testDifferentEnvironments() { + // Test SANDBOX environment + val sandboxConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.SANDBOX, + ), + ) + val sandboxView = createPayPalMessageView(mockContext, sandboxConfig) + assertNotNull(sandboxView) + + // Test DEVELOP environment with host + val developConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.DEVELOP("test.paypal.com"), + ), + ) + val developView = createPayPalMessageView(mockContext, developConfig) + assertNotNull(developView) + + // Test DEVELOP environment with localhost + val localConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.DEVELOP(8443), + ), + ) + val localView = createPayPalMessageView(mockContext, localConfig) + assertNotNull(localView) + } + + @Test + fun testVariousOfferTypes() { + // Test PAY_LATER_SHORT_TERM + val shortTermConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + offerType = PayPalMessageOfferType.PAY_LATER_SHORT_TERM, + ), + ) + val shortTermView = createPayPalMessageView(mockContext, shortTermConfig) + assertNotNull(shortTermView) + assertEquals(PayPalMessageOfferType.PAY_LATER_SHORT_TERM, shortTermConfig.data.offerType) + + // Test PAY_LATER_LONG_TERM + val longTermConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + offerType = PayPalMessageOfferType.PAY_LATER_LONG_TERM, + ), + ) + val longTermView = createPayPalMessageView(mockContext, longTermConfig) + assertNotNull(longTermView) + assertEquals(PayPalMessageOfferType.PAY_LATER_LONG_TERM, longTermConfig.data.offerType) + + // Test PAY_LATER_PAY_IN_1 + val payIn1Config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + offerType = PayPalMessageOfferType.PAY_LATER_PAY_IN_1, + ), + ) + val payIn1View = createPayPalMessageView(mockContext, payIn1Config) + assertNotNull(payIn1View) + assertEquals(PayPalMessageOfferType.PAY_LATER_PAY_IN_1, payIn1Config.data.offerType) + } + + @Test + fun testNullOptionalParameters() { + // Test with all optional parameters set to null + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = null, + buyerCountry = null, + offerType = null, + ), + viewStateCallbacks = null, + eventsCallbacks = null, + ) + + val messageView = createPayPalMessageView(mockContext, config) + assertNotNull(messageView) + assertEquals(null, config.data.amount) + assertEquals(null, config.data.buyerCountry) + assertEquals(null, config.data.offerType) + } + + @Test + fun testEdgeCaseAmounts() { + // Test with zero amount + val zeroAmountConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = 0.0, + ), + ) + val zeroView = createPayPalMessageView(mockContext, zeroAmountConfig) + assertNotNull(zeroView) + assertEquals(0.0, zeroAmountConfig.data.amount) + + // Test with large amount + val largeAmountConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = 999999.99, + ), + ) + val largeView = createPayPalMessageView(mockContext, largeAmountConfig) + assertNotNull(largeView) + assertEquals(999999.99, largeAmountConfig.data.amount) + + // Test with small decimal amount + val smallAmountConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = 0.01, + ), + ) + val smallView = createPayPalMessageView(mockContext, smallAmountConfig) + assertNotNull(smallView) + assertEquals(0.01, smallAmountConfig.data.amount) + } + + @Test + fun testVariousBuyerCountries() { + // Test common country codes + val countries = listOf("US", "GB", "DE", "FR", "CA", "AU", "JP") + + countries.forEach { country -> + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + buyerCountry = country, + ), + ) + val view = createPayPalMessageView(mockContext, config) + assertNotNull(view) + assertEquals(country, config.data.buyerCountry) + } + } + // Note: The following tests would need to be run as instrumented tests rather than unit tests // as they require the Android runtime. For unit test coverage, we're focusing on testing // the configuration creation which is the core logic of the composable. diff --git a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt index 57081682..9ba6d912 100644 --- a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt +++ b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt @@ -95,4 +95,291 @@ class PayPalComposableModalTest { // Test that the client can be created without errors assertNotNull(testWebViewClient) } + + @Test + fun testModalConfigWithNullValues() { + // Test that we can create a ModalConfig with null values + val config = ModalConfig( + amount = null, + buyerCountry = null, + offer = null, + modalCloseButton = ModalCloseButton(), + ) + + // Verify the config properties + assertNull(config.amount) + assertNull(config.buyerCountry) + assertNull(config.offer) + assertNotNull(config.modalCloseButton) + } + + @Test + fun testModalConfigDefaults() { + // Test that we can create a ModalConfig with default values + val config = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) + + // Verify default values + assertNull(config.amount) + assertNull(config.buyerCountry) + assertNull(config.offer) + assertEquals(false, config.ignoreCache) + assertEquals(false, config.devTouchpoint) + assertNull(config.stageTag) + assertNotNull(config.modalCloseButton) + } + + @Test + fun testModalConfigWithAllParameters() { + // Test that we can create a ModalConfig with all parameters + var applyCalled = false + var clickCalled = false + var errorCalled = false + var loadingCalled = false + var successCalled = false + + val events = ModalEvents( + onApply = { applyCalled = true }, + onClick = { clickCalled = true }, + onError = { errorCalled = true }, + onLoading = { loadingCalled = true }, + onSuccess = { successCalled = true }, + ) + + val config = ModalConfig( + amount = 250.50, + buyerCountry = "GB", + offer = PayPalMessageOfferType.PAY_LATER_LONG_TERM, + ignoreCache = true, + devTouchpoint = true, + stageTag = "test-stage", + events = events, + modalCloseButton = ModalCloseButton(alternativeText = "Custom Close"), + ) + + // Verify all properties + assertEquals(250.50, config.amount) + assertEquals("GB", config.buyerCountry) + assertEquals(PayPalMessageOfferType.PAY_LATER_LONG_TERM, config.offer) + assertEquals(true, config.ignoreCache) + assertEquals(true, config.devTouchpoint) + assertEquals("test-stage", config.stageTag) + assertEquals("Custom Close", config.modalCloseButton.alternativeText) + + // Test events still work + config.events?.onApply?.invoke() + assertTrue(applyCalled) + } + + @Test + fun testModalCloseButtonDefaults() { + // Test default ModalCloseButton + val closeButton = ModalCloseButton() + + // Verify defaults + assertEquals("PayPal learn more modal close", closeButton.alternativeText) + assertEquals(26, closeButton.width) + assertEquals(26, closeButton.height) + assertEquals(60, closeButton.availableWidth) + assertEquals(60, closeButton.availableHeight) + assertEquals("#001435", closeButton.color) + assertEquals("dark", closeButton.colorType) + } + + @Test + fun testModalCloseButtonWithText() { + // Test ModalCloseButton with custom text + val closeButton = ModalCloseButton(alternativeText = "Dismiss Modal") + + // Verify custom text + assertEquals("Dismiss Modal", closeButton.alternativeText) + } + + @Test + fun testDifferentOfferTypes() { + // Test all offer types can be set in config + val offerTypes = listOf( + PayPalMessageOfferType.PAY_LATER_SHORT_TERM, + PayPalMessageOfferType.PAY_LATER_LONG_TERM, + PayPalMessageOfferType.PAY_LATER_PAY_IN_1, + null, + ) + + offerTypes.forEach { offerType -> + val config = ModalConfig( + amount = 100.0, + offer = offerType, + modalCloseButton = ModalCloseButton(), + ) + assertEquals(offerType, config.offer) + } + } + + @Test + fun testEdgeCaseAmountsInModal() { + // Test zero amount + val zeroConfig = ModalConfig(amount = 0.0, modalCloseButton = ModalCloseButton()) + assertEquals(0.0, zeroConfig.amount) + + // Test very large amount + val largeConfig = ModalConfig(amount = 1000000.0, modalCloseButton = ModalCloseButton()) + assertEquals(1000000.0, largeConfig.amount) + + // Test small decimal + val smallConfig = ModalConfig(amount = 0.01, modalCloseButton = ModalCloseButton()) + assertEquals(0.01, smallConfig.amount) + + // Test negative amount (should still be accepted, validation is elsewhere) + val negativeConfig = ModalConfig(amount = -10.0, modalCloseButton = ModalCloseButton()) + assertEquals(-10.0, negativeConfig.amount) + } + + @Test + fun testVariousBuyerCountriesInModal() { + // Test common country codes + val countries = listOf("US", "GB", "DE", "FR", "CA", "AU", "JP", "IT", "ES") + + countries.forEach { country -> + val config = ModalConfig( + amount = 100.0, + buyerCountry = country, + modalCloseButton = ModalCloseButton(), + ) + assertEquals(country, config.buyerCountry) + } + } + + @Test + fun testModalEventsIndependence() { + // Test that each event can be called independently + var applyCalled = false + var clickCalled = false + var errorCalled = false + var loadingCalled = false + var successCalled = false + + val events = ModalEvents( + onApply = { applyCalled = true }, + onClick = { clickCalled = true }, + onError = { errorCalled = true }, + onLoading = { loadingCalled = true }, + onSuccess = { successCalled = true }, + ) + + // Call only onApply + events.onApply() + assertTrue(applyCalled) + assertTrue(!clickCalled && !errorCalled && !loadingCalled && !successCalled) + + // Reset and call only onClick + applyCalled = false + events.onClick() + assertTrue(clickCalled) + assertTrue(!applyCalled && !errorCalled && !loadingCalled && !successCalled) + } + + @Test + fun testModalEventsWithNullCallbacks() { + // Test that ModalEvents can be created with minimal callbacks + val minimalEvents = ModalEvents() + + // These should be no-ops if not defined + assertNotNull(minimalEvents) + // Calling these shouldn't throw exceptions + minimalEvents.onApply() + minimalEvents.onClick() + minimalEvents.onLoading() + minimalEvents.onSuccess() + } + + @Test + fun testErrorWithDifferentErrorTypes() { + // Test that different error types can be passed to onError + var capturedError: PayPalErrors.Base? = null + + val events = ModalEvents( + onError = { error -> capturedError = error }, + ) + + // Test with base error + val baseError = PayPalErrors.Base("Base error") + events.onError(baseError) + assertEquals(baseError, capturedError) + + // Test with modal failed to load error + val modalError = PayPalErrors.ModalFailedToLoad("Modal error", null) + events.onError(modalError) + assertEquals(modalError, capturedError) + + // Test with invalid client id exception + val clientIdError = PayPalErrors.InvalidClientIdException("Invalid ID", null) + events.onError(clientIdError) + assertEquals(clientIdError, capturedError) + } + + @Test + fun testCacheBehavior() { + // Test ignoreCache = true + val noCacheConfig = ModalConfig( + amount = 100.0, + ignoreCache = true, + modalCloseButton = ModalCloseButton(), + ) + assertTrue(noCacheConfig.ignoreCache) + + // Test ignoreCache = false (default) + val withCacheConfig = ModalConfig( + amount = 100.0, + ignoreCache = false, + modalCloseButton = ModalCloseButton(), + ) + assertTrue(!withCacheConfig.ignoreCache) + } + + @Test + fun testDevTouchpointFlag() { + // Test devTouchpoint = true + val devConfig = ModalConfig( + amount = 100.0, + devTouchpoint = true, + modalCloseButton = ModalCloseButton(), + ) + assertTrue(devConfig.devTouchpoint) + + // Test devTouchpoint = false (default) + val prodConfig = ModalConfig( + amount = 100.0, + devTouchpoint = false, + modalCloseButton = ModalCloseButton(), + ) + assertTrue(!prodConfig.devTouchpoint) + } + + @Test + fun testStageTagValues() { + // Test with stage tag + val stagedConfig = ModalConfig( + amount = 100.0, + stageTag = "v1.2.3", + modalCloseButton = ModalCloseButton(), + ) + assertEquals("v1.2.3", stagedConfig.stageTag) + + // Test without stage tag + val unstaged = ModalConfig( + amount = 100.0, + stageTag = null, + modalCloseButton = ModalCloseButton(), + ) + assertNull(unstaged.stageTag) + + // Test with empty stage tag + val emptyStageConfig = ModalConfig( + amount = 100.0, + stageTag = "", + modalCloseButton = ModalCloseButton(), + ) + assertEquals("", emptyStageConfig.stageTag) + } } diff --git a/library/src/test/java/com/paypal/messages/config/ChannelTest.kt b/library/src/test/java/com/paypal/messages/config/ChannelTest.kt new file mode 100644 index 00000000..e5ed4603 --- /dev/null +++ b/library/src/test/java/com/paypal/messages/config/ChannelTest.kt @@ -0,0 +1,23 @@ +package com.paypal.messages.config + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ChannelTest { + @Test + fun testNative() { + assertEquals(Channel.NATIVE.toString(), "NATIVE") + } + + @Test + fun testValueOf() { + assertEquals(Channel.NATIVE, Channel.valueOf("NATIVE")) + } + + @Test + fun testValues() { + val values = Channel.values() + assertEquals(1, values.size) + assertEquals(Channel.NATIVE, values[0]) + } +} diff --git a/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt b/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt index 5b0f7f13..93d43585 100644 --- a/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt +++ b/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt @@ -13,4 +13,18 @@ class ProductGroupTest { fun testPayPalCredit() { assertEquals(ProductGroup.PAYPAL_CREDIT.toString(), "PAYPAL_CREDIT") } + + @Test + fun testValueOf() { + assertEquals(ProductGroup.PAY_LATER, ProductGroup.valueOf("PAY_LATER")) + assertEquals(ProductGroup.PAYPAL_CREDIT, ProductGroup.valueOf("PAYPAL_CREDIT")) + } + + @Test + fun testValues() { + val values = ProductGroup.values() + assertEquals(2, values.size) + assertEquals(ProductGroup.PAY_LATER, values[0]) + assertEquals(ProductGroup.PAYPAL_CREDIT, values[1]) + } } diff --git a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt index 2566a728..6dac306b 100644 --- a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt +++ b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt @@ -87,4 +87,36 @@ class PayPalMessageConfigTest { config.data = PayPalMessageData(clientID = "2") assertNotEquals(config, cloneConfig) } + + @Test + fun testEquality() { + val data = PayPalMessageData(clientID = "test_id") + val style = PayPalMessageStyle() + + val config1 = PayPalMessageConfig(data = data, style = style) + val config2 = PayPalMessageConfig(data = data, style = style) + + assertEquals(config1, config2) + assertEquals(config1.hashCode(), config2.hashCode()) + } + + @Test + fun testDataMutability() { + val config = PayPalMessageConfig( + data = PayPalMessageData(clientID = "initial"), + ) + + assertEquals("initial", config.data.clientID) + config.data.clientID = "modified" + assertEquals("modified", config.data.clientID) + } + + @Test + fun testStyleDefaultValue() { + val config = PayPalMessageConfig( + data = PayPalMessageData(clientID = "test"), + ) + + assertEquals(PayPalMessageStyle(), config.style) + } } diff --git a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt index b4c08807..15f32f87 100644 --- a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt +++ b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt @@ -52,4 +52,51 @@ class PayPalMessageDataTest { data.amount = 100.00 assertNotEquals(oldData, data) } + + @Test + fun testDefaultEnvironment() { + val data = PayPalMessageData(clientID = initialClientID) + assertEquals(PayPalEnvironment.SANDBOX, data.environment) + } + + @Test + fun testNullableFields() { + val data = PayPalMessageData(clientID = initialClientID) + assertEquals(null, data.merchantID) + assertEquals(null, data.partnerAttributionID) + assertEquals(null, data.amount) + assertEquals(null, data.buyerCountry) + assertEquals(null, data.offerType) + assertEquals(null, data.pageType) + } + + @Test + fun testEquality() { + val data1 = PayPalMessageData( + clientID = initialClientID, + amount = 100.0, + environment = PayPalEnvironment.LIVE, + ) + val data2 = PayPalMessageData( + clientID = initialClientID, + amount = 100.0, + environment = PayPalEnvironment.LIVE, + ) + assertEquals(data1, data2) + assertEquals(data1.hashCode(), data2.hashCode()) + } + + @Test + fun testFieldMutability() { + val data = PayPalMessageData(clientID = initialClientID) + + data.merchantID = "new_merchant" + assertEquals("new_merchant", data.merchantID) + + data.amount = 250.0 + assertEquals(250.0, data.amount) + + data.buyerCountry = "GB" + assertEquals("GB", data.buyerCountry) + } } diff --git a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt index 84b16890..a18911f0 100644 --- a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt +++ b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt @@ -27,4 +27,44 @@ class PayPalMessageStyleTest { assertEquals(messageStyle, clonedMessageStyle) } + + @Test + fun testDefaultValues() { + val messageStyle = PayPalMessageStyle() + + assertEquals(Color.BLACK, messageStyle.color) + assertEquals(LogoType.PRIMARY, messageStyle.logoType) + assertEquals(Align.LEFT, messageStyle.textAlignment) + } + + @Test + fun testEquality() { + val style1 = PayPalMessageStyle( + color = Color.WHITE, + logoType = LogoType.INLINE, + textAlignment = Align.RIGHT, + ) + val style2 = PayPalMessageStyle( + color = Color.WHITE, + logoType = LogoType.INLINE, + textAlignment = Align.RIGHT, + ) + + assertEquals(style1, style2) + assertEquals(style1.hashCode(), style2.hashCode()) + } + + @Test + fun testDifferentCombinations() { + val style1 = PayPalMessageStyle(color = Color.GRAYSCALE) + assertEquals(Color.GRAYSCALE, style1.color) + assertEquals(LogoType.PRIMARY, style1.logoType) + + val style2 = PayPalMessageStyle(logoType = LogoType.NONE) + assertEquals(Color.BLACK, style2.color) + assertEquals(LogoType.NONE, style2.logoType) + + val style3 = PayPalMessageStyle(textAlignment = Align.CENTER) + assertEquals(Align.CENTER, style3.textAlignment) + } } diff --git a/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt b/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt index 44746672..fd1f96a3 100644 --- a/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt +++ b/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt @@ -46,4 +46,45 @@ class ModalConfigTest { ), ) } + + @Test + fun testDefaultValues() { + val modalConfig = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) + + assertEquals(null, modalConfig.amount) + assertEquals(null, modalConfig.buyerCountry) + assertEquals(Channel.NATIVE, modalConfig.channel) + assertEquals(false, modalConfig.devTouchpoint) + assertEquals(null, modalConfig.events) + assertEquals(false, modalConfig.ignoreCache) + assertEquals(null, modalConfig.offer) + assertEquals(null, modalConfig.stageTag) + } + + @Test + fun testFieldMutability() { + val modalConfig = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) + + modalConfig.amount = 500.0 + assertEquals(500.0, modalConfig.amount) + + modalConfig.buyerCountry = "CA" + assertEquals("CA", modalConfig.buyerCountry) + + modalConfig.ignoreCache = true + assertEquals(true, modalConfig.ignoreCache) + } + + @Test + fun testWithMinimalParameters() { + val modalConfig = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) + + assertEquals(Channel.NATIVE, modalConfig.channel) + } } diff --git a/manifest-merger-test/.gitignore b/manifest-merger-test/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/manifest-merger-test/.gitignore @@ -0,0 +1 @@ +/build diff --git a/manifest-merger-test/IMPLEMENTATION_SUMMARY.md b/manifest-merger-test/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..b699f13d --- /dev/null +++ b/manifest-merger-test/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,130 @@ +# Manifest Merger Test Implementation Summary + +## Overview + +Created a comprehensive test to verify that the PayPal Messages library doesn't cause `android:usesCleartextTraffic` manifest merger conflicts when integrated into apps. + +## What Was Created + +### 1. Test Module (`manifest-merger-test/`) + +A minimal Android application module that simulates an app integrating the library. + +**Key Files:** +- `build.gradle` - Android app configuration with library dependency +- `src/main/AndroidManifest.xml` - Declares `android:usesCleartextTraffic="false"` +- `src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt` - Minimal activity +- `README.md` - Documentation +- `test-manifest-merger.sh` - Local test script + +### 2. GitHub Actions Integration + +Added new CI job `test_manifest_merger` to `.github/workflows/test.yml` + +**What it does:** +1. Builds the test module with `./gradlew :manifest-merger-test:assembleDebug` +2. Verifies the build succeeds (no merger conflicts) +3. Examines the merged manifest at `manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml` +4. Confirms `usesCleartextTraffic` is properly merged + +### 3. Project Configuration + +Updated `settings.gradle` to include `:manifest-merger-test` module. + +## How It Works + +### The Problem +The library's manifest declares: +```xml + +``` + +When apps declare their own value: +```xml + +``` + +The Android manifest merger must resolve this conflict. + +### The Test +The test module intentionally declares `usesCleartextTraffic="false"` to simulate a real app. If the build succeeds, it means: +- No manifest merger errors occurred +- The Android build system successfully merged the attributes +- Apps can safely integrate the library + +### Expected Behavior +- ✅ Build succeeds without errors +- ✅ Merged manifest contains valid `usesCleartextTraffic` value +- ✅ No attribute conflict warnings + +## Running the Test + +### Locally +```bash +# Option 1: Use Gradle directly +./gradlew :manifest-merger-test:assembleDebug + +# Option 2: Use the test script +./manifest-merger-test/test-manifest-merger.sh + +# View merged manifest +cat manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml +``` + +### In CI/CD +The test runs automatically on every PR and push via the `test_manifest_merger` job in GitHub Actions. + +## Current Status + +**This test is expected to FAIL on the current branch.** + +There is a fix in progress on another branch that resolves the manifest merger conflict. This test will validate that the fix works correctly. + +## What Failure Looks Like (Current Branch) + +On the current branch, you'll see: +``` +❌ ERROR: Manifest merger failed - library causes usesCleartextTraffic conflict! +``` + +This indicates the library's manifest declaration is incompatible with app manifests. + +## What Success Looks Like (After Fix Branch Merged) + +When the fix is applied, the test will pass: +``` +🔍 Testing manifest merger with usesCleartextTraffic conflict... +✅ Manifest merger succeeded - no conflict detected! +📄 Merged manifest content: [shows merged XML] +✅ usesCleartextTraffic attribute found in merged manifest +🎉 Manifest merger test completed successfully! +``` + +## Files Modified + +- `/settings.gradle` - Added `:manifest-merger-test` module +- `/.github/workflows/test.yml` - Added `test_manifest_merger` job + +## Files Created + +- `/manifest-merger-test/build.gradle` +- `/manifest-merger-test/src/main/AndroidManifest.xml` +- `/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt` +- `/manifest-merger-test/.gitignore` +- `/manifest-merger-test/README.md` +- `/manifest-merger-test/test-manifest-merger.sh` +- `/manifest-merger-test/IMPLEMENTATION_SUMMARY.md` (this file) + +## Next Steps + +1. **Commit these changes** to your branch +2. **Push to GitHub** to trigger CI tests +3. **Verify the test passes** in GitHub Actions +4. **Monitor future PRs** - this test will catch any manifest merger issues + +## Additional Notes + +- The test module is minimal by design (one activity, basic manifest) +- It focuses solely on the manifest merger conflict +- The test is fast and runs in parallel with other CI jobs +- No actual app functionality is tested (only build-time merger) diff --git a/manifest-merger-test/README.md b/manifest-merger-test/README.md new file mode 100644 index 00000000..5f64efa3 --- /dev/null +++ b/manifest-merger-test/README.md @@ -0,0 +1,60 @@ +# Manifest Merger Test Module + +## Purpose + +This test module verifies that the PayPal Messages library does not cause Android Manifest merger conflicts when integrated into apps. + +## Problem Being Tested + +The library's `AndroidManifest.xml` declares: +```xml + +``` + +When an app integrates the library and also declares its own `android:usesCleartextTraffic` attribute, the Android manifest merger needs to resolve the conflict. This test ensures that: + +1. The manifest merger succeeds without errors +2. The app's declared value takes precedence (or merges correctly) +3. No build-time conflicts occur + +## Test Approach + +This module simulates a typical Android app that: +- Declares its own `android:usesCleartextTraffic="false"` in its manifest +- Integrates the PayPal Messages library as a dependency + +The CI/CD pipeline builds this module to verify no manifest merger conflicts occur. + +## Running Locally + +```bash +# Build the test module +./gradlew :manifest-merger-test:assembleDebug + +# View the merged manifest +cat manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml +``` + +## CI/CD Integration + +This test runs automatically in GitHub Actions as part of the `test.yml` workflow. + +Job: `test_manifest_merger` + +## Expected Behavior + +**Current Status:** This test is expected to **FAIL** on the current branch. + +There is a fix in progress on another branch that resolves the `usesCleartextTraffic` manifest merger conflict. Once that fix is merged, this test should pass. + +**When the fix is applied:** +- Build should succeed without manifest merger errors +- Merged manifest should contain a valid `usesCleartextTraffic` attribute +- No warnings about attribute conflicts should appear + +## Purpose + +This test validates that the manifest merger conflict has been properly resolved. It serves as: +- A regression test to prevent the issue from being reintroduced +- Validation that the fix branch correctly resolves the conflict +- Continuous verification in CI/CD diff --git a/manifest-merger-test/build.gradle b/manifest-merger-test/build.gradle new file mode 100644 index 00000000..c38db734 --- /dev/null +++ b/manifest-merger-test/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.paypal.messages.manifesttest' + compileSdk 34 + + defaultConfig { + applicationId "com.paypal.messages.manifesttest" + minSdk 23 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } +} + +dependencies { + // This simulates an app integrating the library + implementation project(':library') + + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.appcompat:appcompat:1.6.1' +} diff --git a/manifest-merger-test/src/main/AndroidManifest.xml b/manifest-merger-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..88ca031f --- /dev/null +++ b/manifest-merger-test/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt b/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt new file mode 100644 index 00000000..7816e42c --- /dev/null +++ b/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt @@ -0,0 +1,15 @@ +package com.paypal.messages.manifesttest + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +/** + * Minimal test activity for manifest merger verification. + * This activity is never actually run - it exists only to ensure + * the manifest merger succeeds during build. + */ +class TestActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} diff --git a/manifest-merger-test/test-manifest-merger.sh b/manifest-merger-test/test-manifest-merger.sh new file mode 100755 index 00000000..7259093e --- /dev/null +++ b/manifest-merger-test/test-manifest-merger.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Test script to verify manifest merger doesn't cause conflicts +# This script builds the test module and verifies the merged manifest + +set -e + +echo "🔍 Testing manifest merger with usesCleartextTraffic conflict..." +echo "" + +# Build the test module +echo "Building manifest-merger-test module..." +./gradlew :manifest-merger-test:assembleDebug + +if [ $? -eq 0 ]; then + echo "✅ Manifest merger succeeded - no conflict detected!" +else + echo "❌ ERROR: Manifest merger failed!" + exit 1 +fi + +echo "" +echo "🔍 Verifying merged manifest content..." + +# Path to merged manifest +MERGED_MANIFEST="manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml" + +if [ ! -f "$MERGED_MANIFEST" ]; then + echo "❌ ERROR: Merged manifest not found at $MERGED_MANIFEST" + exit 1 +fi + +echo "" +echo "📄 Merged manifest content:" +echo "---" +cat "$MERGED_MANIFEST" +echo "---" +echo "" + +# Verify usesCleartextTraffic is present +if grep -q "usesCleartextTraffic" "$MERGED_MANIFEST"; then + echo "✅ usesCleartextTraffic attribute found in merged manifest" + + # Show the exact line + echo "" + echo "📋 usesCleartextTraffic declaration:" + grep "usesCleartextTraffic" "$MERGED_MANIFEST" +else + echo "❌ WARNING: usesCleartextTraffic attribute not found in merged manifest" +fi + +echo "" +echo "🎉 Manifest merger test completed successfully!" diff --git a/settings.gradle b/settings.gradle index 539bce41..149253cc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,9 @@ pluginManagement { if (requested.id.id == 'com.vanniktech.maven.publish') { useModule("com.vanniktech:gradle-maven-publish-plugin:${requested.version}") } + if (requested.id.id == 'org.jetbrains.kotlinx.kover') { + useModule("org.jetbrains.kotlinx:kover-gradle-plugin:${requested.version}") + } } } } @@ -24,3 +27,4 @@ dependencyResolutionManagement { rootProject.name = "PayPalMessages" include ':demo' include ':library' +include ':manifest-merger-test'