diff --git a/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java b/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java index b58cc9bc..af813916 100644 --- a/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java +++ b/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java @@ -56,7 +56,6 @@ public void onLoaded() { latch.countDown(); } }, - new TestHCaptchaStateListener(), webView ); }); diff --git a/compose/build.gradle b/compose/build.gradle index 8fe7d599..cfc3718e 100644 --- a/compose/build.gradle +++ b/compose/build.gradle @@ -2,14 +2,20 @@ plugins { id 'maven-publish' id 'com.android.library' id 'org.jetbrains.kotlin.android' + id "pmd" + id "jacoco" + id "checkstyle" + id "com.github.spotbugs" version "5.2.3" + id "org.owasp.dependencycheck" version "7.1.1" + id "org.sonarqube" version "3.4.0.2513" } android { namespace 'com.hcaptcha.compose' - compileSdk 33 + compileSdk 34 defaultConfig { - minSdk 16 + minSdk 23 // See https://developer.android.com/studio/publish/versioning // versionCode must be integer and be incremented by one for every new update @@ -57,11 +63,17 @@ android { dependencies { implementation project(':sdk') - implementation 'androidx.activity:activity-compose:1.8.2' - implementation 'androidx.compose.ui:ui-tooling:1.6.1' + implementation "androidx.activity:activity-compose:$compose_version" + implementation "androidx.compose.ui:ui-tooling:$compose_version" + implementation 'androidx.compose.material3:material3:1.2.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' + androidTestImplementation "androidx.compose.ui:ui-test-junit4-android:$compose_version" + androidTestImplementation "androidx.compose.ui:ui:$compose_version" + androidTestImplementation "androidx.activity:activity-ktx:$compose_version" + androidTestImplementation "androidx.compose.foundation:foundation-layout-android:$compose_version" } project.afterEvaluate { @@ -80,11 +92,11 @@ project.afterEvaluate { pom { name = 'Android Jetpack Compose SDK hCaptcha' description = 'This SDK provides a wrapper for hCaptcha and ready to use Jetpack Compose Component' - url = 'https://github.com/hCaptcha/hcaptcha-android-sdk' + url = 'https://github.com/hCaptcha/hcaptcha-jetpack-compose' licenses { license { name = 'MIT License' - url = 'https://github.com/hCaptcha/hcaptcha-android-sdk/blob/main/LICENSE' + url = 'https://github.com/hCaptcha/hcaptcha-jetpack-compose-sdk/blob/main/LICENSE' } } developers { @@ -105,4 +117,6 @@ project.afterEvaluate { } } } -} \ No newline at end of file +} + +apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle" \ No newline at end of file diff --git a/compose/src/androidTest/AndroidManifest.xml b/compose/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000..1a3fd523 --- /dev/null +++ b/compose/src/androidTest/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/compose/src/androidTest/java/com/hcaptcha/sdk/compose/ExampleInstrumentedTest.kt b/compose/src/androidTest/java/com/hcaptcha/sdk/compose/ExampleInstrumentedTest.kt deleted file mode 100644 index d2f4d684..00000000 --- a/compose/src/androidTest/java/com/hcaptcha/sdk/compose/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.hcaptcha.sdk.compose - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.hcaptcha.sdk.compose.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/compose/src/androidTest/java/com/hcaptcha/sdk/compose/HCaptchaComposeTest.kt b/compose/src/androidTest/java/com/hcaptcha/sdk/compose/HCaptchaComposeTest.kt new file mode 100644 index 00000000..66ab4d4a --- /dev/null +++ b/compose/src/androidTest/java/com/hcaptcha/sdk/compose/HCaptchaComposeTest.kt @@ -0,0 +1,77 @@ +package com.hcaptcha.sdk.compose + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.hcaptcha.sdk.HCaptchaCompose +import com.hcaptcha.sdk.HCaptchaConfig +import com.hcaptcha.sdk.HCaptchaError +import com.hcaptcha.sdk.HCaptchaResponse +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit + +@RunWith(AndroidJUnit4::class) +class HCaptchaComposeTest { + private val resultContentDescription = "HCaptchaResultString" + private val timeout = TimeUnit.SECONDS.toMillis(4) + + @get:Rule + val composeTestRule = createComposeRule() + + fun setContent(token: String = "10000000-ffff-ffff-ffff-000000000001") { + composeTestRule.setContent { + var text by remember { mutableStateOf("") } + Column { + Text(text = text, modifier = Modifier.semantics { contentDescription = resultContentDescription }) + + HCaptchaCompose(HCaptchaConfig + .builder() + .siteKey(token) + .build()) { result -> + when (result) { + is HCaptchaResponse.Success -> { + text = result.token + } + is HCaptchaResponse.Failure -> { + text = result.error.name + } + else -> {} + } + } + } + } + } + + @Test + fun validToken() { + setContent() + + runBlocking { delay(timeout) } + + composeTestRule.onNodeWithContentDescription(resultContentDescription) + .assertTextEquals("10000000-aaaa-bbbb-cccc-000000000001") + } + + @Test + fun invalidToken() { + setContent("bad-baad-token") + + runBlocking { delay(timeout) } + + composeTestRule.onNodeWithContentDescription(resultContentDescription) + .assertTextEquals(HCaptchaError.INVALID_DATA.name) + } +} \ No newline at end of file diff --git a/example-app/build.gradle b/example-app/build.gradle index 1ee2bec2..9a8a1883 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -16,7 +16,7 @@ android { namespace 'com.hcaptcha.example' defaultConfig { - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion intProp("exampleTargetSdkVersion", 34) versionCode 1 versionName "0.0.1" @@ -61,12 +61,12 @@ dependencies { implementation "com.google.android.flexbox:flexbox:3.0.0" implementation project(path: ':sdk') - implementation 'androidx.compose.ui:ui:1.6.1' - implementation 'androidx.compose.material3:material3:1.2.0' + implementation "androidx.compose.ui:ui:$compose_version" + implementation 'androidx.compose.material3:material3:1.2.1' implementation 'androidx.activity:activity-ktx:1.8.2' implementation 'androidx.activity:activity-compose:1.8.2' implementation project(path: ':compose') - implementation 'androidx.compose.foundation:foundation-layout-android:1.6.0' + implementation "androidx.compose.foundation:foundation-layout-android:$compose_version" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/example-app/src/main/java/com/hcaptcha/example/ComposeActivity.kt b/example-app/src/main/java/com/hcaptcha/example/ComposeActivity.kt index 83d2214a..73c05234 100644 --- a/example-app/src/main/java/com/hcaptcha/example/ComposeActivity.kt +++ b/example-app/src/main/java/com/hcaptcha/example/ComposeActivity.kt @@ -56,6 +56,7 @@ class ComposeActivity : ComponentActivity() { if (hCaptchaVisible) { HCaptchaCompose(HCaptchaConfig .builder() + .siteKey("10000000-ffff-ffff-ffff-000000000001") .build()) { result -> when (result) { is HCaptchaResponse.Success -> { diff --git a/gradle.properties b/gradle.properties index 19cfd6f6..2201abbc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,5 +20,5 @@ android.enableJetifier=true # To test more aggressive optimizations android.enableR8.fullMode=true # Kotline version -kotlin_version=1.9.0 -compose_version=1.5.2 \ No newline at end of file +kotlin_version=1.9.10 +compose_version=1.5.3 \ No newline at end of file diff --git a/gradle/shared/code-quality.gradle b/gradle/shared/code-quality.gradle new file mode 100644 index 00000000..efe77442 --- /dev/null +++ b/gradle/shared/code-quality.gradle @@ -0,0 +1,108 @@ +checkstyle { + toolVersion = '8.45.1' +} + +task checkstyle(type: Checkstyle) { + description 'Check code standard' + group 'verification' + configFile file("${rootDir}/gradle/config/checkstyle.xml") + source 'src' + include '**/*.java' + exclude '**/gen/**' + classpath = files() + ignoreFailures = false + maxWarnings = 0 +} + +pmd { + consoleOutput = true + toolVersion = "6.51.0" +} + +task pmd(type: Pmd) { + ruleSetFiles = files("${project.rootDir}/gradle/config/pmd.xml") + ignoreFailures = false + ruleSets = [] + source 'src' + include '**/*.java' + exclude '**/gen/**' + reports { + xml.required = false + xml.outputLocation = file("${project.buildDir}/reports/pmd/pmd.xml") + html.required = true + html.outputLocation = file("$project.buildDir/outputs/pmd/pmd.html") + } +} + +spotbugs { + ignoreFailures = false + showStackTraces = true + showProgress = false + reportLevel = 'high' + excludeFilter = file("${project.rootDir}/gradle/config/findbugs-exclude.xml") + onlyAnalyze = ['com.hcaptcha.sdk.*'] + projectName = name + release = version +} + +// enable html report +gradle.taskGraph.beforeTask { task -> + if (task.name.toLowerCase().contains('spotbugs')) { + task.reports { + html.enabled true + xml.enabled true + } + } +} + +// https://www.rallyhealth.com/coding/code-coverage-for-android-testing +task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + def coverageSourceDirs = [ + "src/main/java" + ] + def javaClasses = fileTree( + dir: "${project.buildDir}/intermediates/javac/debug/classes", + excludes: [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*' + ] + ) + + classDirectories.from files([javaClasses]) + additionalSourceDirs.from files(coverageSourceDirs) + sourceDirectories.from files(coverageSourceDirs) + executionData.from = "${project.buildDir}/jacoco/testDebugUnitTest.exec" + + reports { + xml.required = true + html.required = true + } +} + +check.dependsOn('checkstyle', 'pmd', 'jacocoUnitTestReport') + +sonarqube { + properties { + property "sonar.projectKey", "hCaptcha_hcaptcha-android-sdk" + property "sonar.organization", "hcaptcha" + property "sonar.host.url", "https://sonarcloud.io" + + property "sonar.language", "java" + property "sonar.sourceEncoding", "utf-8" + + property "sonar.sources", "src/main" + property "sonar.java.binaries", "${project.buildDir}/intermediates/javac/debug/classes" + property "sonar.tests", ["src/test/", "../test/src/androidTest/"] + + property "sonar.android.lint.report", "${project.buildDir}/outputs/lint-results.xml" + property "sonar.java.spotbugs.reportPaths", ["${project.buildDir}/reports/spotbugs/debug.xml", "${project.buildDir}/reports/spotbugs/release.xml"] + property "sonar.java.pmd.reportPaths", "${project.buildDir}/reports/pmd/pmd.xml" + property "sonar.java.checkstyle.reportPaths", "${project.buildDir}/reports/checkstyle/checkstyle.xml" + property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/jacocoUnitTestReport.xml" + } +} + +project.tasks["sonarqube"].dependsOn "check" + diff --git a/sdk/build.gradle b/sdk/build.gradle index 95229952..c8f9f4ca 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -1,9 +1,9 @@ plugins { id "com.android.library" + id "maven-publish" id "pmd" id "jacoco" id "checkstyle" - id "maven-publish" id "com.github.spotbugs" version "5.2.3" id "org.owasp.dependencycheck" version "7.1.1" id "org.sonarqube" version "3.4.0.2513" @@ -180,111 +180,4 @@ android.libraryVariants.all { variant -> variant.registerJavaGeneratingTask(generateTask, outputDir) } -checkstyle { - toolVersion = '8.45.1' -} - -task checkstyle(type: Checkstyle) { - description 'Check code standard' - group 'verification' - configFile file("${rootDir}/gradle/config/checkstyle.xml") - source 'src' - include '**/*.java' - exclude '**/gen/**' - classpath = files() - ignoreFailures = false - maxWarnings = 0 -} - -pmd { - consoleOutput = true - toolVersion = "6.51.0" -} - -task pmd(type: Pmd) { - ruleSetFiles = files("${project.rootDir}/gradle/config/pmd.xml") - ignoreFailures = false - ruleSets = [] - source 'src' - include '**/*.java' - exclude '**/gen/**' - reports { - xml.required = false - xml.outputLocation = file("${project.buildDir}/reports/pmd/pmd.xml") - html.required = true - html.outputLocation = file("$project.buildDir/outputs/pmd/pmd.html") - } -} - -spotbugs { - ignoreFailures = false - showStackTraces = true - showProgress = false - reportLevel = 'high' - excludeFilter = file("${project.rootDir}/gradle/config/findbugs-exclude.xml") - onlyAnalyze = ['com.hcaptcha.sdk.*'] - projectName = name - release = version -} - -// enable html report -gradle.taskGraph.beforeTask { task -> - if (task.name.toLowerCase().contains('spotbugs')) { - task.reports { - html.enabled true - xml.enabled true - } - } -} - -// https://www.rallyhealth.com/coding/code-coverage-for-android-testing -task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { - def coverageSourceDirs = [ - "src/main/java" - ] - def javaClasses = fileTree( - dir: "${project.buildDir}/intermediates/javac/debug/classes", - excludes: [ - '**/R.class', - '**/R$*.class', - '**/BuildConfig.*', - '**/Manifest*.*' - ] - ) - - classDirectories.from files([javaClasses]) - additionalSourceDirs.from files(coverageSourceDirs) - sourceDirectories.from files(coverageSourceDirs) - executionData.from = "${project.buildDir}/jacoco/testDebugUnitTest.exec" - - reports { - xml.required = true - html.required = true - } -} - -check.dependsOn('checkstyle', 'pmd', 'jacocoUnitTestReport') - -sonarqube { - properties { - property "sonar.projectKey", "hCaptcha_hcaptcha-android-sdk" - property "sonar.organization", "hcaptcha" - property "sonar.host.url", "https://sonarcloud.io" - - property "sonar.language", "java" - property "sonar.sourceEncoding", "utf-8" - - property "sonar.sources", "src/main" - property "sonar.java.binaries", "${project.buildDir}/intermediates/javac/debug/classes" - property "sonar.tests", ["src/test/", "../test/src/androidTest/"] - - property "sonar.android.lint.report", "${project.buildDir}/outputs/lint-results.xml" - property "sonar.java.spotbugs.reportPaths", ["${project.buildDir}/reports/spotbugs/debug.xml", "${project.buildDir}/reports/spotbugs/release.xml"] - property "sonar.java.pmd.reportPaths", "${project.buildDir}/reports/pmd/pmd.xml" - property "sonar.java.checkstyle.reportPaths", "${project.buildDir}/reports/checkstyle/checkstyle.xml" - property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/jacocoUnitTestReport.xml" - } -} - -project.tasks["sonarqube"].dependsOn "check" - +apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle" \ No newline at end of file diff --git a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java index dd7458b9..18b2c1f2 100644 --- a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java +++ b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java @@ -41,9 +41,6 @@ public class HCaptchaWebViewHelperTest { @Mock IHCaptchaVerifier captchaVerifier; - @Mock - HCaptchaStateListener stateListener; - @Mock HCaptchaWebView webView; @@ -62,7 +59,6 @@ public class HCaptchaWebViewHelperTest { public void init() { MockitoAnnotations.openMocks(this); androidLogMock = mockStatic(Log.class); - stateListener = mock(HCaptchaStateListener.class); webView = mock(HCaptchaWebView.class); webSettings = mock(WebSettings.class); htmlProvider = mock(IHCaptchaHtmlProvider.class); @@ -79,7 +75,7 @@ public void release() { @Test public void test_constructor() { new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier, - stateListener, webView); + webView); verify(webView).loadDataWithBaseURL(null, MOCK_HTML, "text/html", "UTF-8", null); verify(webView, times(2)).addJavascriptInterface(any(), anyString()); } @@ -87,7 +83,7 @@ public void test_constructor() { @Test public void test_destroy() { final HCaptchaWebViewHelper webViewHelper = new HCaptchaWebViewHelper(handler, context, config, - internalConfig, captchaVerifier, stateListener, webView); + internalConfig, captchaVerifier, webView); final ViewGroup viewParent = mock(ViewGroup.class, withSettings().extraInterfaces(ViewParent.class)); when(webView.getParent()).thenReturn(viewParent); webViewHelper.destroy(); @@ -98,7 +94,7 @@ public void test_destroy() { @Test public void test_destroy_webview_parent_null() { final HCaptchaWebViewHelper webViewHelper = new HCaptchaWebViewHelper(handler, context, config, - internalConfig, captchaVerifier, stateListener, webView); + internalConfig, captchaVerifier, webView); webViewHelper.destroy(); } @@ -106,8 +102,7 @@ public void test_destroy_webview_parent_null() { public void test_config_host_pased() { final String host = "https://my.awesome.host"; when(config.getHost()).thenReturn(host); - new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier, - stateListener, webView); + new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier, webView); verify(webView).loadDataWithBaseURL(host, MOCK_HTML, "text/html", "UTF-8", null); } } diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java index fb7b1017..c22d4bb3 100644 --- a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java +++ b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import android.app.Activity; import android.os.Handler; import android.os.Looper; @@ -43,17 +44,36 @@ public void testInsecureHttpRequestErrorHandling() throws Exception { final Handler handler = new Handler(Looper.getMainLooper()); final CountDownLatch failureLatch = new CountDownLatch(1); final HCaptchaConfig config = baseConfig.toBuilder().host("http://localhost").build(); - final IHCaptchaVerifier verifier = mock(IHCaptchaVerifier.class); - final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() { + final IHCaptchaVerifier verifier = new IHCaptchaVerifier() { @Override - void onSuccess(String token) { + public void onOpen() { failAsNonReachable(); } @Override - void onFailure(HCaptchaException e) { + public void onLoaded() { + failAsNonReachable(); + } + + @Override + public void startVerification(Activity activity) { + failAsNonReachable(); + } + + @Override + public void reset() { + failAsNonReachable(); + } + + @Override + public void onSuccess(String token) { + failAsNonReachable(); + } + + @Override + public void onFailure(HCaptchaException e) { assertEquals(HCaptchaError.INSECURE_HTTP_REQUEST_ERROR, e.getHCaptchaError()); assertEquals("Insecure resource http://localhost/favicon.ico requested", e.getMessage()); failureLatch.countDown(); @@ -64,7 +84,7 @@ void onFailure(HCaptchaException e) { scenario.onActivity(activity -> { HCaptchaWebView webView = new HCaptchaWebView(activity); final HCaptchaWebViewHelper helper = new HCaptchaWebViewHelper( - handler, activity, config, internalConfig, verifier, listener, webView); + handler, activity, config, internalConfig, verifier, webView); }); assertTrue(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));