diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index 5d3e90bb0..345885c70 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -108,22 +108,42 @@ public function getFiles(): array [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Permission.kt', - 'template' => '/android/library/src/main/java/io/appwrite/Permission.kt.twig', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/Permission.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/test/java/{{ sdk.namespace | caseSlash }}/PermissionTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/PermissionTest.kt.twig', ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Role.kt', - 'template' => '/android/library/src/main/java/io/appwrite/Role.kt.twig', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/Role.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/test/java/{{ sdk.namespace | caseSlash }}/RoleTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/RoleTest.kt.twig', ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/ID.kt', - 'template' => '/android/library/src/main/java/io/appwrite/ID.kt.twig', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/ID.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/test/java/{{ sdk.namespace | caseSlash }}/IDTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/IDTest.kt.twig', ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Query.kt', - 'template' => '/android/library/src/main/java/io/appwrite/Query.kt.twig', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/test/java/{{ sdk.namespace | caseSlash }}/QueryTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig', ], [ 'scope' => 'default', @@ -175,6 +195,11 @@ public function getFiles(): array 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/KeepAliveService.kt', 'template' => '/android/library/src/main/java/io/appwrite/KeepAliveService.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Response.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/Response.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/views/CallbackActivity.kt', @@ -225,6 +250,16 @@ public function getFiles(): array 'destination' => '/library/src/main/AndroidManifest.xml', 'template' => '/android/library/src/main/AndroidManifest.xml.twig', ], + [ + 'scope' => 'service', + 'destination' => '/library/src/test/java/{{ sdk.namespace | caseSlash }}/services/{{ service.name | caseUcfirst }}ServiceTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/services/ServiceTest.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/library/src/test/java/{{ sdk.namespace | caseSlash }}/cookies/CookiesTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/cookies/CookiesTest.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/library/build.gradle', @@ -385,7 +420,12 @@ public function getFiles(): array [ 'scope' => 'definition', 'destination' => 'library/src/main/java/io/appwrite/models/{{ definition.name | caseUcfirst }}.kt', - 'template' => '/android/library/src/main/java/io/appwrite/models/Model.kt.twig', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/Model.kt.twig', + ], + [ + 'scope' => 'definition', + 'destination' => 'library/src/test/java/io/appwrite/models/{{ definition.name | caseUcfirst }}Test.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/models/ModelTest.kt.twig', ], ]; } diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index a7a4c5aea..d30ddfff6 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -353,25 +353,50 @@ public function getFiles(): array 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Permission.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/Permission.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/src/test/kotlin/{{ sdk.namespace | caseSlash }}/PermissionTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/PermissionTest.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Role.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/Role.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/src/test/kotlin/{{ sdk.namespace | caseSlash }}/RoleTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/RoleTest.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/ID.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/ID.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/src/test/kotlin/{{ sdk.namespace | caseSlash }}/IDTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/IDTest.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Query.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/src/test/kotlin/{{ sdk.namespace | caseSlash }}/QueryTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Response.kt', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/Response.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/coroutines/Callback.kt', - 'template' => '/android/library/src/main/java/io/appwrite/coroutines/Callback.kt.twig', + 'template' => '/kotlin/src/main/kotlin/io/appwrite/coroutines/Callback.kt.twig', ], [ 'scope' => 'default', @@ -404,6 +429,11 @@ public function getFiles(): array 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/services/{{service.name | caseUcfirst}}.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig', ], + [ + 'scope' => 'service', + 'destination' => '/src/test/kotlin/{{ sdk.namespace | caseSlash }}/services/{{service.name | caseUcfirst}}ServiceTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/services/ServiceTest.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/InputFile.kt', @@ -419,6 +449,11 @@ public function getFiles(): array 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/models/{{ definition.name | caseUcfirst }}.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/models/Model.kt.twig', ], + [ + 'scope' => 'definition', + 'destination' => '/src/test/kotlin/{{ sdk.namespace | caseSlash }}/models/{{ definition.name | caseUcfirst }}Test.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/models/ModelTest.kt.twig', + ], ]; } @@ -443,7 +478,7 @@ public function getFilters(): array protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T'): string { if ($method['type'] === 'webAuth') { - return 'Bool'; + return 'Boolean'; } if ($method['type'] === 'location') { return 'ByteArray'; diff --git a/templates/android/gradle/wrapper/gradle-wrapper.properties b/templates/android/gradle/wrapper/gradle-wrapper.properties index 85e684fc9..c87e29eb6 100644 --- a/templates/android/gradle/wrapper/gradle-wrapper.properties +++ b/templates/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jun 01 15:55:54 IST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/templates/android/library/build.gradle.twig b/templates/android/library/build.gradle.twig index 9d809012f..e3d4c9696 100644 --- a/templates/android/library/build.gradle.twig +++ b/templates/android/library/build.gradle.twig @@ -34,6 +34,10 @@ android { consumerProguardFiles("consumer-rules.pro") } + testOptions { + unitTests.includeAndroidResources = true + } + buildTypes { release { minifyEnabled false @@ -70,11 +74,18 @@ dependencies { implementation("androidx.activity:activity-ktx:1.6.1") implementation("androidx.browser:browser:1.4.0") - testImplementation 'junit:junit:4.13.2' + testImplementation "org.jetbrains.kotlin:kotlin-test" + testImplementation "io.mockk:mockk:1.13.7" testImplementation "androidx.test.ext:junit-ktx:1.1.5" testImplementation "androidx.test:core-ktx:1.5.0" - testImplementation "org.robolectric:robolectric:4.5.1" - testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1") + testImplementation 'io.github.ivanshafran:shared-preferences-mock:1.2.4' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" + testImplementation "androidx.arch.core:core-testing:2.2.0" +} + +tasks.withType(Test).configureEach { + useJUnit() + jvmArgs('-XX:+StartAttachListener') } apply from: "${rootProject.projectDir}/scripts/publish-module.gradle" \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index 210ef4fbe..400fe8719 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -37,6 +37,7 @@ import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume +import okhttp3.Response as Okhttp3Response class Client @JvmOverloads constructor( context: Context, @@ -266,7 +267,10 @@ class Client @JvmOverloads constructor( .get() .build() - return awaitResponse(request, responseType, converter) + val response = awaitResponse(request, responseType) + + @Suppress("UNCHECKED_CAST") + return converter?.invoke(response.data) ?: response.data as T } val body = if (MultipartBody.FORM.toString() == headers["content-type"]) { @@ -303,7 +307,10 @@ class Client @JvmOverloads constructor( .method(method, body) .build() - return awaitResponse(request, responseType, converter) + val response = awaitResponse(request, responseType) + + @Suppress("UNCHECKED_CAST") + return converter?.invoke(response.data) ?: response.data as T } /** @@ -441,11 +448,10 @@ class Client @JvmOverloads constructor( * @return [T] */ @Throws({{ spec.title | caseUcfirst }}Exception::class) - private suspend fun awaitResponse( + internal suspend fun awaitResponse( request: Request, responseType: Class, - converter: ((Any) -> T)? = null - ) = suspendCancellableCoroutine { + ) = suspendCancellableCoroutine> { http.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { if (it.isCancelled) { @@ -454,8 +460,7 @@ class Client @JvmOverloads constructor( it.cancel(e) } - @Suppress("UNCHECKED_CAST") - override fun onResponse(call: Call, response: Response) { + override fun onResponse(call: Call, response: Okhttp3Response) { if (!response.isSuccessful) { val body = response.body!! .charStream() @@ -481,19 +486,19 @@ class Client @JvmOverloads constructor( } when { responseType == Boolean::class.java -> { - it.resume(true as T) + it.resume(Response(true)) return } responseType == ByteArray::class.java -> { - it.resume(response.body!! + val data = response.body!! .byteStream() .buffered() - .use(BufferedInputStream::readBytes) as T - ) + .use(BufferedInputStream::readBytes) + it.resume(Response(data)) return } response.body == null -> { - it.resume(true as T) + it.resume(Response(true)) return } } @@ -502,7 +507,7 @@ class Client @JvmOverloads constructor( .buffered() .use(BufferedReader::readText) if (body.isEmpty()) { - it.resume(true as T) + it.resume(Response(true)) return } val map = gson.fromJson( @@ -510,7 +515,7 @@ class Client @JvmOverloads constructor( object : TypeToken(){}.type ) it.resume( - converter?.invoke(map) ?: map as T + Response(map) ) } }) diff --git a/templates/android/library/src/main/java/io/appwrite/ID.kt.twig b/templates/android/library/src/main/java/io/appwrite/ID.kt.twig deleted file mode 100644 index 3ac3c98b0..000000000 --- a/templates/android/library/src/main/java/io/appwrite/ID.kt.twig +++ /dev/null @@ -1,10 +0,0 @@ -package {{ sdk.namespace | caseDot }} - -class ID { - companion object { - fun custom(id: String): String - = id - fun unique(): String - = "unique()" - } -} \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/Permission.kt.twig b/templates/android/library/src/main/java/io/appwrite/Permission.kt.twig deleted file mode 100644 index 33e5b741c..000000000 --- a/templates/android/library/src/main/java/io/appwrite/Permission.kt.twig +++ /dev/null @@ -1,21 +0,0 @@ -package {{ sdk.namespace | caseDot }} - -class Permission { - companion object { - fun read(role: String): String { - return "read(\"${role}\")" - } - fun write(role: String): String { - return "write(\"${role}\")" - } - fun create(role: String): String { - return "create(\"${role}\")" - } - fun update(role: String): String { - return "update(\"${role}\")" - } - fun delete(role: String): String { - return "delete(\"${role}\")" - } - } -} \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/Query.kt.twig b/templates/android/library/src/main/java/io/appwrite/Query.kt.twig deleted file mode 100644 index 7f4fbcd81..000000000 --- a/templates/android/library/src/main/java/io/appwrite/Query.kt.twig +++ /dev/null @@ -1,60 +0,0 @@ -package {{ sdk.namespace | caseDot }} - -class Query { - companion object { - fun equal(attribute: String, value: Any) = addQuery(attribute, "equal", value) - - fun notEqual(attribute: String, value: Any) = Query.addQuery(attribute, "notEqual", value) - - fun lessThan(attribute: String, value: Any) = Query.addQuery(attribute, "lessThan", value) - - fun lessThanEqual(attribute: String, value: Any) = Query.addQuery(attribute, "lessThanEqual", value) - - fun greaterThan(attribute: String, value: Any) = Query.addQuery(attribute, "greaterThan", value) - - fun greaterThanEqual(attribute: String, value: Any) = Query.addQuery(attribute, "greaterThanEqual", value) - - fun search(attribute: String, value: String) = Query.addQuery(attribute, "search", value) - - fun isNull(attribute: String) = "isNull(\"${attribute}\")" - - fun isNotNull(attribute: String) = "isNotNull(\"${attribute}\")" - - fun between(attribute: String, start: Int, end: Int) = "between(\"${attribute}\", ${start}, ${end})" - - fun between(attribute: String, start: Double, end: Double) = "between(\"${attribute}\", ${start}, ${end})" - - fun between(attribute: String, start: String, end: String) = "between(\"${attribute}\", \"${start}\", \"${end}\")" - - fun startsWith(attribute: String, value: String) = Query.addQuery(attribute, "startsWith", value) - - fun endsWith(attribute: String, value: String) = Query.addQuery(attribute, "endsWith", value) - - fun select(attributes: List) = "select([${attributes.joinToString(",") { "\"$it\"" }}])" - - fun orderAsc(attribute: String) = "orderAsc(\"${attribute}\")" - - fun orderDesc(attribute: String) = "orderDesc(\"${attribute}\")" - - fun cursorBefore(documentId: String) = "cursorBefore(\"${documentId}\")" - - fun cursorAfter(documentId: String) = "cursorAfter(\"${documentId}\")" - - fun limit(limit: Int) = "limit(${limit})" - - fun offset(offset: Int) = "offset(${offset})" - - private fun addQuery(attribute: String, method: String, value: Any): String { - return when (value) { - is List<*> -> "${method}(\"${attribute}\", [${value.map{it -> parseValues(it!!)}.joinToString(",")}])" - else -> "${method}(\"${attribute}\", [${Query.parseValues(value)}])" - } - } - private fun parseValues(value: Any): String { - return when (value) { - is String -> "\"${value}\"" - else -> "${value}" - } - } - } -} diff --git a/templates/android/library/src/main/java/io/appwrite/Role.kt.twig b/templates/android/library/src/main/java/io/appwrite/Role.kt.twig deleted file mode 100644 index 2e4de9861..000000000 --- a/templates/android/library/src/main/java/io/appwrite/Role.kt.twig +++ /dev/null @@ -1,72 +0,0 @@ -package {{ sdk.namespace | caseDot }} - -/** - * Helper class to generate role strings for [Permission]. - */ -class Role { - companion object { - - /** - * Grants access to anyone. - * - * This includes authenticated and unauthenticated users. - */ - fun any(): String = "any" - - /** - * Grants access to a specific user by user ID. - * - * You can optionally pass verified or unverified for - * [status] to target specific types of users. - */ - fun user(id: String, status: String = ""): String = if(status.isEmpty()) { - "user:$id" - } else { - "user:$id/$status" - } - - /** - * Grants access to any authenticated or anonymous user. - * - * You can optionally pass verified or unverified for - * [status] to target specific types of users. - */ - fun users(status: String = ""): String = if(status.isEmpty()) { - "users" - } else { - "users/$status" - } - - /** - * Grants access to any guest user without a session. - * - * Authenticated users don't have access to this role. - */ - fun guests(): String = "guests" - - /** - * Grants access to a team by team ID. - * - * You can optionally pass a role for [role] to target - * team members with the specified role. - */ - fun team(id: String, role: String = ""): String = if(role.isEmpty()) { - "team:$id" - } else { - "team:$id/$role" - } - - /** - * Grants access to a specific member of a team. - * - * When the member is removed from the team, they will - * no longer have access. - */ - fun member(id: String): String = "member:$id" - - /** - * Grants access to a user with the specified label. - */ - fun label(name: String): String = "label:$name" - } -} \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/models/Model.kt.twig b/templates/android/library/src/main/java/io/appwrite/models/Model.kt.twig index 6b049555c..9ea5280b6 100644 --- a/templates/android/library/src/main/java/io/appwrite/models/Model.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/models/Model.kt.twig @@ -1,7 +1,7 @@ package {{ sdk.namespace | caseDot }}.models import com.google.gson.annotations.SerializedName -import io.appwrite.extensions.jsonCast +import {{ sdk.namespace | caseDot }}.extensions.jsonCast /** * {{ definition.description }} diff --git a/templates/dart/test/services/service_test.dart.twig b/templates/dart/test/services/service_test.dart.twig index b5997121a..a31114258 100644 --- a/templates/dart/test/services/service_test.dart.twig +++ b/templates/dart/test/services/service_test.dart.twig @@ -101,7 +101,8 @@ void main() { {%- if method.type == 'location' ~%} expect(response, isA()); - {%~ endif ~%}{%~ if method.responseModel and method.responseModel != 'any' ~%} + {%~ endif ~%} + {%~ if method.responseModel and method.responseModel != 'any' ~%} expect(response, isA()); {%~ endif ~%} }); diff --git a/templates/kotlin/build.gradle.twig b/templates/kotlin/build.gradle.twig index 9ae0c32e8..8803473cb 100644 --- a/templates/kotlin/build.gradle.twig +++ b/templates/kotlin/build.gradle.twig @@ -36,7 +36,9 @@ dependencies { implementation("com.squareup.okhttp3:logging-interceptor") implementation("com.google.code.gson:gson:2.9.0") + testImplementation 'io.mockk:mockk:1.13.7' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" } test { diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 7181175ca..4dbc61337 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -2,6 +2,7 @@ package {{ sdk.namespace | caseDot }} import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken +import {{ sdk.namespace | caseDot }}.Response import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception import {{ sdk.namespace | caseDot }}.extensions.fromJson import {{ sdk.namespace | caseDot }}.json.PreciseNumberAdapter @@ -17,7 +18,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.logging.HttpLoggingInterceptor import java.io.BufferedInputStream import java.io.BufferedReader import java.io.File @@ -32,6 +32,7 @@ import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume +import okhttp3.Response as Okhttp3Response class Client @JvmOverloads constructor( var endPoint: String = "{{spec.endpoint}}", @@ -67,6 +68,7 @@ class Client @JvmOverloads constructor( "x-sdk-platform" to "{{ sdk.platform }}", "x-sdk-language" to "{{ language.name | caseLower }}", "x-sdk-version" to "{{ sdk.version }}"{% if spec.global.defaultHeaders | length > 0 %},{% endif %} + {% for key,header in spec.global.defaultHeaders %} "{{ key | caseLower }}" to "{{ header }}"{% if not loop.last %},{% endif %} {% endfor %} @@ -225,7 +227,10 @@ class Client @JvmOverloads constructor( .get() .build() - return awaitResponse(request, responseType, converter) + val response = awaitResponse(request, responseType) + + @Suppress("UNCHECKED_CAST") + return converter?.invoke(response.data) ?: response.data as T } val body = if (MultipartBody.FORM.toString() == headers["content-type"]) { @@ -262,7 +267,10 @@ class Client @JvmOverloads constructor( .method(method, body) .build() - return awaitResponse(request, responseType, converter) + val response = awaitResponse(request, responseType) + + @Suppress("UNCHECKED_CAST") + return converter?.invoke(response.data) ?: response.data as T } /** @@ -400,11 +408,10 @@ class Client @JvmOverloads constructor( * @return [T] */ @Throws({{ spec.title | caseUcfirst }}Exception::class) - private suspend fun awaitResponse( + internal suspend fun awaitResponse( request: Request, responseType: Class, - converter: ((Any) -> T)? = null - ) = suspendCancellableCoroutine { + ) = suspendCancellableCoroutine> { http.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { if (it.isCancelled) { @@ -413,23 +420,22 @@ class Client @JvmOverloads constructor( it.cancel(e) } - @Suppress("UNCHECKED_CAST") - override fun onResponse(call: Call, response: Response) { + override fun onResponse(call: Call, response: Okhttp3Response) { if (!response.isSuccessful) { val body = response.body!! .charStream() .buffered() .use(BufferedReader::readText) - + val error = if (response.headers["content-type"]?.contains("application/json") == true) { val map = gson.fromJson>( body, object : TypeToken>(){}.type ) {{ spec.title | caseUcfirst }}Exception( - map["message"] as? String ?: "", + map["message"] as? String ?: "", (map["code"] as Number).toInt(), - map["type"] as? String ?: "", + map["type"] as? String ?: "", body ) } else { @@ -440,19 +446,19 @@ class Client @JvmOverloads constructor( } when { responseType == Boolean::class.java -> { - it.resume(true as T) + it.resume(Response(true)) return } responseType == ByteArray::class.java -> { - it.resume(response.body!! + val data = response.body!! .byteStream() .buffered() - .use(BufferedInputStream::readBytes) as T - ) + .use(BufferedInputStream::readBytes) + it.resume(Response(data)) return } response.body == null -> { - it.resume(true as T) + it.resume(Response(true)) return } } @@ -461,7 +467,7 @@ class Client @JvmOverloads constructor( .buffered() .use(BufferedReader::readText) if (body.isEmpty()) { - it.resume(true as T) + it.resume(Response(true)) return } val map = gson.fromJson>( @@ -469,7 +475,7 @@ class Client @JvmOverloads constructor( object : TypeToken>(){}.type ) it.resume( - converter?.invoke(map) ?: map as T + Response(map) ) } }) diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Response.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Response.kt.twig new file mode 100644 index 000000000..b261a9520 --- /dev/null +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Response.kt.twig @@ -0,0 +1,7 @@ +package {{ sdk.namespace | caseDot }} + +import {{ sdk.namespace | caseDot }}.extensions.toJson + +class Response(val data: T) { + override fun toString(): String = if (data is Map<*, *>) toJson() else data.toString() +} diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig index 5fb0f8069..ddf44fe5c 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig @@ -36,9 +36,6 @@ class {{ service.name | caseUcfirst }} : Service { {%~ endif %} @Throws({{ spec.title | caseUcfirst }}Exception::class) suspend fun {% if method.responseModel | hasGenericType(spec) %}{{ '' | raw }} {% endif %}{{ method.name | caseCamel }}( - {%~ if method.type == "webAuth" %} - activity: ComponentActivity, - {%~ endif %} {%~ for parameter in method.parameters.all %} {{ parameter.name | caseCamel }}: {{ parameter | typeName }}{%~ if not parameter.required or parameter.nullable %}? = null{% endif %}, {%~ endfor %} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/IDTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/IDTest.kt.twig new file mode 100644 index 000000000..82fa5c8c3 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/IDTest.kt.twig @@ -0,0 +1,12 @@ +package {{ sdk.namespace | caseDot }} + +import org.junit.Test +import kotlin.test.assertEquals + +class IDTest { + @Test + fun testUnique() = assertEquals("unique()", ID.unique()) + + @Test + fun testCustom() = assertEquals("custom", ID.custom("custom")) +} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/PermissionTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/PermissionTest.kt.twig new file mode 100644 index 000000000..b4a5417c2 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/PermissionTest.kt.twig @@ -0,0 +1,21 @@ +package {{ sdk.namespace | caseDot }} + +import org.junit.Test +import kotlin.test.assertEquals + +class PermissionTest { + @Test + fun readReturnsRead() = assertEquals("read(\"any\")", Permission.read(Role.any())) + + @Test + fun writeReturnsWrite() = assertEquals("write(\"any\")", Permission.write(Role.any())) + + @Test + fun createReturnsCreate() = assertEquals("create(\"any\")", Permission.create(Role.any())) + + @Test + fun updateReturnsUpdate() = assertEquals("update(\"any\")", Permission.update(Role.any())) + + @Test + fun deleteReturnsDelete() = assertEquals("delete(\"any\")", Permission.delete(Role.any())) +} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig new file mode 100644 index 000000000..347cb53f9 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig @@ -0,0 +1,189 @@ +package {{ sdk.namespace | caseDot }} + +import org.junit.Test +import kotlin.test.assertEquals + +internal class BasicFilterQueryTest( + val description: String, + val value: Any, + val expectedValues: String +) + +class QueryTest { + private val tests = listOf( + BasicFilterQueryTest( + "with a string", + "s", + "[\"s\"]" + ), + BasicFilterQueryTest( + "with an integer", + 1, + "[1]" + ), + BasicFilterQueryTest( + "with a double", + 1.2, + "[1.2]" + ), + BasicFilterQueryTest( + "with a whole number double", + 1.0, + "[1.0]" + ), + BasicFilterQueryTest( + "with a bool", + false, + "[false]" + ), + BasicFilterQueryTest( + "with a list", + listOf("a", "b", "c"), + "[\"a\",\"b\",\"c\"]" + ), + ) + + @Test + fun basicFilterEqual() { + for (t in tests) { + assertEquals( + "equal(\"attr\", ${t.expectedValues})", + Query.equal("attr", t.value), + t.description + ) + } + } + + @Test + fun basicFilterNotEqual() { + for (t in tests) { + assertEquals( + "notEqual(\"attr\", ${t.expectedValues})", + Query.notEqual("attr", t.value), + t.description + ) + } + } + + @Test + fun basicFilterLessThan() { + for (t in tests) { + assertEquals( + "lessThan(\"attr\", ${t.expectedValues})", + Query.lessThan("attr", t.value), + t.description + ) + } + } + + @Test + fun basicFilterLessThanEqual() { + for (t in tests) { + assertEquals( + "lessThanEqual(\"attr\", ${t.expectedValues})", + Query.lessThanEqual("attr", t.value), + t.description + ) + } + } + + @Test + fun basicFilterGreaterThan() { + for (t in tests) { + assertEquals( + "greaterThan(\"attr\", ${t.expectedValues})", + Query.greaterThan("attr", t.value), + t.description + ) + } + } + + @Test + fun basicFilterGreaterThanEqual() { + for (t in tests) { + assertEquals( + "greaterThanEqual(\"attr\", ${t.expectedValues})", + Query.greaterThanEqual("attr", t.value), + t.description + ) + } + } + + @Test + fun searchReturnsSearch() = assertEquals( + "search(\"attr\", [\"keyword1 keyword2\"])", + Query.search("attr", "keyword1 keyword2") + ) + + @Test + fun isNullReturnsIsNull() = assertEquals( + "isNull(\"attr\")", + Query.isNull("attr") + ) + + @Test + fun isNotNullReturnsIsNotNull() = assertEquals( + "isNotNull(\"attr\")", + Query.isNotNull("attr") + ) + + @Test + fun betweenWithIntegers() = assertEquals( + "between(\"attr\", 1, 2)", + Query.between("attr", 1, 2) + ) + + @Test + fun betweenWithDoubles() = assertEquals( + "between(\"attr\", 1.0, 2.0)", + Query.between("attr", 1.0, 2.0) + ) + + @Test + fun betweenWithStrings() = assertEquals( + "between(\"attr\", \"a\", \"z\")", + Query.between("attr", "a", "z") + ) + + @Test + fun selectReturnsSelect() = assertEquals( + "select([\"attr1\",\"attr2\"])", + Query.select(listOf("attr1", "attr2")) + ) + + @Test + fun orderAscReturnsOrderAsc() = assertEquals( + "orderAsc(\"attr\")", + Query.orderAsc("attr") + ) + + @Test + fun orderDescReturnsOrderDesc() = assertEquals( + "orderDesc(\"attr\")", + Query.orderDesc("attr") + ) + + @Test + fun cursorBeforeReturnsCursorBefore() = assertEquals( + "cursorBefore(\"attr\")", + Query.cursorBefore("attr") + ) + + @Test + fun cursorAfterReturnsCursorAfter() = assertEquals( + "cursorAfter(\"attr\")", + Query.cursorAfter("attr") + ) + + @Test + fun limitReturnsLimit() = assertEquals( + "limit(1)", + Query.limit(1) + ) + + @Test + fun offsetReturnsOffset() = assertEquals( + "offset(1)", + Query.offset(1) + ) +} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/RoleTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/RoleTest.kt.twig new file mode 100644 index 000000000..1ab1125b2 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/RoleTest.kt.twig @@ -0,0 +1,66 @@ +package {{ sdk.namespace | caseDot }} + +import org.junit.Test +import kotlin.test.assertEquals + +class RoleTest { + @Test + fun anyReturnsAny() = assertEquals( + "any", + Role.any() + ) + + @Test + fun userWithoutStatus() = assertEquals( + "user:custom", + Role.user("custom") + ) + + @Test + fun userWithStatus() = assertEquals( + "user:custom/verified", + Role.user("custom", "verified") + ) + + @Test + fun usersWithoutStatus() = assertEquals( + "users", + Role.users() + ) + + @Test + fun usersWithStatus() = assertEquals( + "users/verified", + Role.users("verified") + ) + + @Test + fun guestsReturnsGuests() = assertEquals( + "guests", + Role.guests() + ) + + @Test + fun teamWithoutRole() = assertEquals( + "team:custom", + Role.team("custom") + ) + + @Test + fun teamWithRole() = assertEquals( + "team:custom/owner", + Role.team("custom", "owner") + ) + + @Test + fun memberReturnsMember() = assertEquals( + "member:custom", + Role.member("custom") + ) + + @Test + fun labelReturnsLabel() = assertEquals( + "label:admin", + Role.label("admin") + ) +} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/cookies/CookiesTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/cookies/CookiesTest.kt.twig new file mode 100644 index 000000000..99dc6aa99 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/cookies/CookiesTest.kt.twig @@ -0,0 +1,17 @@ +package {{ sdk.namespace | caseDot }}.cookies + +import java.net.HttpCookie +import org.junit.Test +import kotlin.test.assertEquals + +class CookiesTest { + @Test + fun cookiesToString() { + val httpCookie = HttpCookie("name", "value").apply { + domain = "localhost" + } + val internalCookie = InternalCookie(httpCookie) + + assertEquals("name=value; domain=localhost", internalCookie.toHttpCookie().toSetCookieString()) + } +} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/models/ModelTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/models/ModelTest.kt.twig new file mode 100644 index 000000000..ba91778d3 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/models/ModelTest.kt.twig @@ -0,0 +1,28 @@ +package {{ sdk.namespace | caseDot }}.models + +import org.junit.Test +import kotlin.test.assertEquals + +class {{definition.name | caseUcfirst}}Test { + + @Test + fun method() { + val model = {{ definition.name | caseUcfirst | overrideIdentifier }}{% if definition.name | hasGenericType(spec) %}{% endif %}( +{% for property in definition.properties %} + {{ property.name | escapeKeyword | removeDollarSign }} = {% if property.type == 'array' %}listOf(){% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}Preferences(data = mapOf()){% elseif property.type == 'object' %}mapOf(){% elseif property.type == 'string' %}"{{property['x-example'] | escapeDollarSign}}"{% elseif property.type == 'boolean' %}true{% elseif property.type == 'number' %}{{property['x-example'] | number_format(1)}}{% else %}{{property['x-example']}}{% endif %}, +{% endfor %} +{% if definition.additionalProperties %} + data = mapOf(), +{% endif %} + ) + val map = model.toMap() + val result = {{ definition.name | caseUcfirst | overrideIdentifier }}.from(map{% if definition.name | hasGenericType(spec) %}, Any::class.java{% endif %}) + +{% for property in definition.properties %} + assertEquals( + {% if property.type == 'array' %}listOf(){% elseif property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}mapOf("data" to mapOf()){% elseif property.type == 'object' %}mapOf(){% elseif property.type == 'string' %}"{{property['x-example'] | escapeDollarSign}}"{% elseif property.type == 'boolean' %}true{% elseif property.type == 'number' %}{{property['x-example'] | number_format(1)}}{% else %}{{property['x-example']}}{% endif %}, + result.{{ property.name | escapeKeyword | removeDollarSign }}{% if property.type == 'object' and (property.sub_schema == 'prefs' or property.sub_schema == 'preferences') %}.data{% endif %} + ) +{% endfor %} + } +} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/services/ServiceTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/services/ServiceTest.kt.twig new file mode 100644 index 000000000..88cce9094 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/services/ServiceTest.kt.twig @@ -0,0 +1,113 @@ +package {{ sdk.namespace | caseDot }}.services + +import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.Response +import {{ sdk.namespace | caseDot }}.models.InputFile +import io.mockk.coEvery +import io.mockk.mockkObject +import io.mockk.spyk +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertIs + +{% if language.name == 'Android' %} +import android.content.Context +import android.content.pm.PackageInfo +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.test.core.app.ApplicationProvider +import {{ sdk.namespace | caseDot }}.WebAuthComponent +import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.every + +{% endif %} +class {{service.name | caseUcfirst}}ServiceTest { +{% if language.name == 'Android' %} + @get:Rule + val rule = InstantTaskExecutorRule() +{% endif %} + + private lateinit var client: Client + private lateinit var {{service.name | caseCamel}}: {{service.name | caseUcfirst}} + + @Before + fun setup() { + {% if language.name == 'Android' %} + val contextMock = mockk() + val prefs = SPMockBuilder().createSharedPreferences() + val packageInfo = PackageInfo() + packageInfo.versionName = "" + + every { contextMock.packageName } returns "io.appwrite" + every { contextMock.packageManager.getPackageInfo("io.appwrite", 0) } returns packageInfo + every { contextMock.getSharedPreferences(any(), any()) } returns prefs + {% endif %} + + client = spyk(Client({% if language.name == 'Android' %}contextMock{% endif %})) + {{service.name | caseCamel}} = {{service.name | caseUcfirst}}(client) + } + +{% for method in service.methods %} + @Test + fun method{{method.name | caseUcfirst}}() { + {%- if method.type == 'webAuth' -%} + {%- elseif method.type == 'location' -%} + val data = byteArrayOf() + {%- else -%} + {%~ if method.responseModel and method.responseModel != 'any' ~%} + val data = mapOf( + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + "{{property.name | escapeDollarSign}}" to {% if property.type == 'object' %}mapOf(){% elseif property.type == 'array' %}listOf(){% elseif property.type == 'string' %}"{{property.example | escapeDollarSign}}"{% elseif property.type == 'boolean' %}true{% else %}{{property.example}}{% endif %},{%~ endfor ~%}{% set break = true %}{%- else -%}{% set continue = true %}{%- endif -%}{%~ endfor -%} + + ) + {%~ else ~%} + val data = ""; + {%- endif -%} + {% endif %} + + {%~ if method.type == 'webAuth' ~%} + {%~ if language.name == 'Android' ~%} + mockkStatic(Uri::class) + val uriMock = mockk() + every { uriMock.getQueryParameter("key") } returns "example" + every { uriMock.getQueryParameter("secret") } returns "example" + every { uriMock.host } returns "HOSTNAME" + every { Uri.parse(any()) } returns uriMock + + mockkObject(WebAuthComponent.Companion) + coEvery { WebAuthComponent.Companion.authenticate(any(), any(), any(), any()) } answers { + arg<((Result) -> Unit)?>(3)?.invoke(Result.success("http://localhost/oauth?key=example&secret=example")) + } + {%~ else ~%} + coEvery { + client.awaitResponse(any(), any()) + } returns Response(true) + {%~ endif ~%} + {%~ else ~%} + coEvery { + client.awaitResponse(any(), any()) + } returns Response(data) + {%~ endif ~%} + + runTest { + val response = {{service.name | caseCamel}}.{{method.name | caseCamel}}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {{parameter.name | escapeKeyword | caseCamel}} = {% if parameter.type == 'object' %}mapOf(){% elseif parameter.type == 'array' %}listOf(){% elseif parameter.type == 'file' %}InputFile.fromBytes(byteArrayOf(), mimeType = "image/png"){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}"{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}"{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%},{%~ endfor ~%} + {%~ if method.type == "webAuth" and language.name == 'Android' %} + activity = ComponentActivity(), + {%~ endif %} + ) + {%- if method.type == 'location' ~%} + assertIs(response) + {%~ endif ~%} + {%~ if method.responseModel and method.responseModel != 'any' ~%} + assertIs<{{ sdk.namespace | caseDot }}.models.{{method.responseModel | caseUcfirst | overrideIdentifier}}{% if method.responseModel | hasGenericType(spec) %}<*>{% endif %}>(response) + {%~ endif ~%} + } + } +{% endfor %} +} diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index b8b608f5a..b105e0b5c 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -28,16 +28,19 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.robolectric.annotation.Config import java.io.File import java.io.IOException import java.nio.file.Files import java.nio.file.Paths +import io.mockk.mockk +import io.mockk.every +import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder +import android.content.Context +import android.content.pm.PackageInfo + data class TestPayload(val response: String) -@Config(manifest=Config.NONE) -@RunWith(AndroidJUnit4::class) class ServiceTest { private val filename: String = "result.txt" @@ -59,7 +62,16 @@ class ServiceTest { @Test @Throws(IOException::class) fun test() { - val client = Client(ApplicationProvider.getApplicationContext()) + val contextMock = mockk() + val prefs = SPMockBuilder().createSharedPreferences() + val packageInfo = PackageInfo() + packageInfo.versionName = "" + + every { contextMock.packageName } returns "io.appwrite" + every { contextMock.packageManager.getPackageInfo("io.appwrite", 0) } returns packageInfo + every { contextMock.getSharedPreferences(any(), any()) } returns prefs + + val client = Client(contextMock) .setEndpointRealtime("wss://demo.appwrite.io/v1") .setProject("console") .addHeader("Origin", "http://localhost")