From f85dfe1dda973b3e34b932905c9ebcf822ae7d90 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 28 Sep 2023 22:32:27 -0400 Subject: [PATCH 01/20] AV-2434 Add setup for junit testing --- build.gradle.kts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 331e97ce..c984ce2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,6 +40,16 @@ dependencies { // Render dokka from the Java perspective dokkaPlugin("org.jetbrains.dokka:kotlin-as-java-plugin:1.8.20") + + // Test dependencies + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") + testImplementation("org.mockito:mockito-inline:4.11.0") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") +} + +tasks.test { + useJUnitPlatform() } tasks.processResources { From 2835ffdf263b431f6c35a476c5bafa623ab56c6b Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 28 Sep 2023 22:32:39 -0400 Subject: [PATCH 02/20] catch both IOException and JsonDataException --- src/main/kotlin/com/nylas/NylasClient.kt | 40 +++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/nylas/NylasClient.kt b/src/main/kotlin/com/nylas/NylasClient.kt index 68fe73b1..8ef674da 100644 --- a/src/main/kotlin/com/nylas/NylasClient.kt +++ b/src/main/kotlin/com/nylas/NylasClient.kt @@ -6,6 +6,7 @@ import com.nylas.interceptors.HttpLoggingInterceptor import com.nylas.models.* import com.nylas.resources.* import com.nylas.util.JsonHelper +import com.squareup.moshi.JsonDataException import okhttp3.* import okhttp3.Response import java.io.IOException @@ -273,14 +274,20 @@ class NylasClient( try { parsedError = JsonHelper.moshi().adapter(NylasOAuthError::class.java) .fromJson(responseBody) - } catch (e: IOException) { - throw NylasOAuthError( - error = "unknown", - errorDescription = "Unknown error received from the API: $responseBody", - errorUri = "unknown", - errorCode = "0", - statusCode = response.code(), - ) + } catch (ex: Exception) { + when (ex) { + is IOException, is JsonDataException -> { + throw NylasOAuthError( + error = "unknown", + errorDescription = "Unknown error received from the API: $responseBody", + errorUri = "unknown", + errorCode = "0", + statusCode = response.code(), + ) + } + + else -> throw ex + } } } else { try { @@ -290,12 +297,17 @@ class NylasClient( if (parsedError != null) { parsedError.requestId = errorResp?.requestId } - } catch (e: IOException) { - throw NylasApiError( - type = "unknown", - message = "Unknown error received from the API: $responseBody", - statusCode = response.code(), - ) + } catch (ex: Exception) { + when (ex) { + is IOException, is JsonDataException -> { + throw NylasApiError( + type = "unknown", + message = "Unknown error received from the API: $responseBody", + statusCode = response.code(), + ) + } + else -> throw ex + } } } From bffe95a3607fa8c0f692287345e7cd02a4f25cba Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 28 Sep 2023 22:33:47 -0400 Subject: [PATCH 03/20] AV-2435 Initial NylasClient commit --- src/test/kotlin/com/nylas/NylasClientTest.kt | 324 +++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 src/test/kotlin/com/nylas/NylasClientTest.kt diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt new file mode 100644 index 00000000..923374a2 --- /dev/null +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -0,0 +1,324 @@ +package com.nylas + +import com.nylas.models.NylasApiError +import com.nylas.models.NylasOAuthError +import com.nylas.models.NylasSdkTimeoutError +import com.nylas.util.JsonHelper +import okhttp3.* +import okio.Buffer +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever +import java.net.SocketTimeoutException +import kotlin.test.assertFailsWith + +class NylasClientTest { + + private lateinit var nylasClient: NylasClient + + @BeforeEach + fun setup() { + val httpClientBuilder = OkHttpClient.Builder() + nylasClient = NylasClient("testApiKey", httpClientBuilder) + } + + @Nested + inner class InitializationTests { + @Test + fun `builder uses the default url`() { + val client = NylasClient.Builder("testApiKey") + .build() + + val urlBuilder = client.newUrlBuilder() + val builtUrl = urlBuilder.build().toString() + + assert(builtUrl.startsWith("https://api.us.nylas.com/")) + } + + @Test + fun `builder sets correct apiUri`() { + val customApiUri = "https://custom-api.nylas.com/" + val client = NylasClient.Builder("testApiKey") + .apiUri(customApiUri) + .build() + + val urlBuilder = client.newUrlBuilder() + val builtUrl = urlBuilder.build().toString() + + assert(builtUrl.startsWith(customApiUri)) + } + + @Test + fun `builder sets correct httpClient`() { + val mockOkHttpClientBuilder: OkHttpClient.Builder = mock() + val mockOkHttpClient: OkHttpClient = mock() + whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) + whenever(mockOkHttpClientBuilder.build()).thenReturn(mockOkHttpClient) + + val client = NylasClient.Builder("testApiKey") + .httpClient(mockOkHttpClientBuilder) + .build() + + val field = NylasClient::class.java.getDeclaredField("httpClient") + field.isAccessible = true + + assertEquals(mockOkHttpClient, field.get(client)) + } + + @Test + fun `initializing the NylasClient with no optional fields should use correct defaults`() { + val client = NylasClient(apiKey = "testApiKey") + + val apiUriField = NylasClient::class.java.getDeclaredField("apiUri") + apiUriField.isAccessible = true + val apiUriSet = apiUriField.get(client) as HttpUrl + + assertEquals("https://api.us.nylas.com/", apiUriSet.toString()) + assertEquals("testApiKey", client.apiKey) + } + + @Test + fun `initializing the NylasClient with values sets them correctly`() { + val mockOkHttpClientBuilder: OkHttpClient.Builder = mock() + val mockOkHttpClient: OkHttpClient = mock() + whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) + whenever(mockOkHttpClientBuilder.build()).thenReturn(mockOkHttpClient) + + val client = NylasClient(apiKey = "testApiKey", httpClientBuilder = mockOkHttpClientBuilder, apiUri = "https://custom-api.nylas.com/") + + val httpClientField = NylasClient::class.java.getDeclaredField("httpClient") + httpClientField.isAccessible = true + val apiUriField = NylasClient::class.java.getDeclaredField("apiUri") + apiUriField.isAccessible = true + val apiUriSet = apiUriField.get(client) as HttpUrl + + assertEquals(mockOkHttpClient, httpClientField.get(client)) + assertEquals("https://custom-api.nylas.com/", apiUriSet.toString()) + assertEquals("testApiKey", client.apiKey) + } + } + + @Nested + inner class ResourcesTests { + @Test + fun `applications returns a valid Applications instance`() { + val result = nylasClient.applications() + assertNotNull(result) + } + + @Test + fun `auth returns a valid Auth instance`() { + val result = nylasClient.auth() + assertNotNull(result) + } + + @Test + fun `calendars returns a valid Calendars instance`() { + val result = nylasClient.calendars() + assertNotNull(result) + } + + @Test + fun `events returns a valid Events instance`() { + val result = nylasClient.events() + assertNotNull(result) + } + + @Test + fun `webhooks returns a valid Webhooks instance`() { + val result = nylasClient.webhooks() + assertNotNull(result) + } + } + + @Nested + inner class RequestTests { + private val mockHttpClient: OkHttpClient = mock(OkHttpClient::class.java) + private val mockCall: Call = mock(Call::class.java) + private val mockResponse: Response = mock(Response::class.java) + private val mockResponseBody: ResponseBody = mock(ResponseBody::class.java) + + @BeforeEach + fun setup() { + val mockOkHttpClientBuilder: OkHttpClient.Builder = mock() + whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) + whenever(mockOkHttpClientBuilder.build()).thenReturn(mockHttpClient) + whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + nylasClient = NylasClient("testApiKey", mockOkHttpClientBuilder) + } + + @Test + fun `should handle successful request`() { + val urlBuilder = nylasClient.newUrlBuilder() + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) + + val result = nylasClient.executeRequest>(urlBuilder, NylasClient.HttpMethod.GET, null, JsonHelper.mapTypeOf(String::class.java, String::class.java)) + + assertEquals("bar", result["foo"]) + } + + @Test + fun `should throw NylasOAuthError on error from oauth endpoints`() { + val tokenUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/token") + val revokeUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/revoke") + val oauthUrls = listOf(tokenUrlBuilder, revokeUrlBuilder) + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(false) + whenever(mockResponse.code()).thenReturn(401) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + whenever(mockResponseBody.string()).thenReturn("{ \"error\": \"internal_error\", \"error_code\": 500, \"error_description\": \"Internal error, contact administrator\", \"error_uri\": \"https://accounts.nylas.io/#tag/Event-Codes\", \"request_id\": \"eccc9c3f-7150-48e1-965e-4f89714ab51a\" }") + + for (urlBuilder in oauthUrls) { + val exception = assertFailsWith { + nylasClient.executeRequest(urlBuilder, NylasClient.HttpMethod.GET, null, String::class.java) + } + + assertEquals("internal_error", exception.error) + assertEquals("Internal error, contact administrator", exception.errorDescription) + assertEquals("https://accounts.nylas.io/#tag/Event-Codes", exception.errorUri) + assertEquals("500", exception.errorCode) + assertEquals(401, exception.statusCode) + } + } + + @Test + fun `should throw NylasOAuthError on error from oauth endpoints even if unexpected response from API`() { + val tokenUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/token") + val revokeUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/revoke") + val oauthUrls = listOf(tokenUrlBuilder, revokeUrlBuilder) + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(false) + whenever(mockResponse.code()).thenReturn(500) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + whenever(mockResponseBody.string()).thenReturn("not a json") + + for (urlBuilder in oauthUrls) { + val exception = assertFailsWith { + nylasClient.executeRequest(urlBuilder, NylasClient.HttpMethod.GET, null, String::class.java) + } + + assertEquals("unknown", exception.error) + assertEquals("Unknown error received from the API: not a json", exception.errorDescription) + assertEquals("unknown", exception.errorUri) + assertEquals("0", exception.errorCode) + assertEquals(500, exception.statusCode) + } + } + + @Test + fun `should throw NylasApiError on error from non-oauth endpoints`() { + val urlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/some/other/path") + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(false) + whenever(mockResponse.code()).thenReturn(400) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + whenever(mockResponseBody.string()).thenReturn("{ \"request_id\": \"4c2740b4-52a4-412e-bdee-49a6c6671b22\", \"error\": { \"type\": \"provider_error\", \"message\": \"Unauthorized\", \"provider_error\": { \"error\": \"Request had invalid authentication credentials.\" } } }") + + val exception = assertFailsWith { + nylasClient.executeRequest(urlBuilder, NylasClient.HttpMethod.GET, null, String::class.java) + } + + assertEquals("provider_error", exception.type) + assertEquals("Unauthorized", exception.message) + assertEquals("Request had invalid authentication credentials.", exception.providerError?.get("error")) + assertEquals("4c2740b4-52a4-412e-bdee-49a6c6671b22", exception.requestId) + assertEquals(400, exception.statusCode) + } + + @Test + fun `should throw NylasApiError on error from non-oauth endpoints even if unexpected response from API`() { + val urlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/some/other/path") + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(false) + whenever(mockResponse.code()).thenReturn(400) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + whenever(mockResponseBody.string()).thenReturn("not a json") + + val exception = assertFailsWith { + nylasClient.executeRequest(urlBuilder, NylasClient.HttpMethod.GET, null, String::class.java) + } + + assertEquals("unknown", exception.type) + assertEquals("Unknown error received from the API: not a json", exception.message) + assertEquals(400, exception.statusCode) + } + + @Test + fun `should handle socket timeout exception`() { + val urlBuilder = nylasClient.newUrlBuilder() + whenever(mockCall.execute()).thenThrow(SocketTimeoutException()) + + val exception = assertFailsWith { + nylasClient.executeRequest(urlBuilder, NylasClient.HttpMethod.GET, null, String::class.java) + } + + assertNotNull(exception) + } + + @Test + fun `should handle unexpected null response body`() { + val urlBuilder = nylasClient.newUrlBuilder() + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(null) + + val exception = assertFailsWith { + nylasClient.executeRequest(urlBuilder, NylasClient.HttpMethod.GET, null, String::class.java) + } + + assertEquals("Unexpected null response body", exception.message) + } + + // TODO::Should we handle this case? + /*@Test + fun `should handle failed deserialization of response body`() { + val urlBuilder = nylasClient.newUrlBuilder() + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("invalid json")) + + val exception = assertFailsWith { + nylasClient.executeRequest(urlBuilder, NylasClient.HttpMethod.GET, null, String::class.java) + } + + assertEquals("Failed to deserialize response body", exception.message) + }*/ + } + + @Nested + inner class EnumTests { + @Test + fun `HttpMethod should have correct number of values`() { + val expectedCount = 5 + assertEquals(expectedCount, NylasClient.HttpMethod.values().size) + } + + @Test + fun `HttpMethod should have correct values`() { + val expectedValues = listOf("GET", "PUT", "POST", "DELETE", "PATCH") + val actualValues = NylasClient.HttpMethod.values().map { it.name } + assertEquals(expectedValues, actualValues) + } + + @Test + fun `MediaType values should be correctly associated with their strings`() { + assertEquals("application/json", NylasClient.MediaType.APPLICATION_JSON.mediaType) + assertEquals("message/rfc822", NylasClient.MediaType.MESSAGE_RFC822.mediaType) + } + + @Test + fun `HttpHeaders values should be correctly associated with their strings`() { + assertEquals("Accept", NylasClient.HttpHeaders.ACCEPT.headerName) + assertEquals("Authorization", NylasClient.HttpHeaders.AUTHORIZATION.headerName) + assertEquals("Content-Type", NylasClient.HttpHeaders.CONTENT_TYPE.headerName) + } + } +} From 6016c53b51a29f369d917a91e74f3005e7dc7d78 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:18:11 -0500 Subject: [PATCH 04/20] added more tests for nylasclient --- src/test/kotlin/com/nylas/NylasClientTest.kt | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index 923374a2..31f397de 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -110,6 +110,12 @@ class NylasClientTest { assertNotNull(result) } + @Test + fun `attachments returns a valid Applications instance`() { + val result = nylasClient.attachments() + assertNotNull(result) + } + @Test fun `auth returns a valid Auth instance`() { val result = nylasClient.auth() @@ -122,12 +128,42 @@ class NylasClientTest { assertNotNull(result) } + @Test + fun `connectors returns a valid Calendars instance`() { + val result = nylasClient.connectors() + assertNotNull(result) + } + + @Test + fun `drafts returns a valid Calendars instance`() { + val result = nylasClient.drafts() + assertNotNull(result) + } + @Test fun `events returns a valid Events instance`() { val result = nylasClient.events() assertNotNull(result) } + @Test + fun `folders returns a valid Events instance`() { + val result = nylasClient.folders() + assertNotNull(result) + } + + @Test + fun `messages returns a valid Events instance`() { + val result = nylasClient.messages() + assertNotNull(result) + } + + @Test + fun `threads returns a valid Events instance`() { + val result = nylasClient.threads() + assertNotNull(result) + } + @Test fun `webhooks returns a valid Webhooks instance`() { val result = nylasClient.webhooks() @@ -291,6 +327,17 @@ class NylasClientTest { assertEquals("Failed to deserialize response body", exception.message) }*/ + + @Test + fun `should handle download request`() { + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + + val result = nylasClient.downloadResponse("test/path") + + assertEquals(mockResponseBody, result) + } } @Nested From 1a713e13810ee20b10b0dc28b6b7d1ab6eb8c9c6 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:48:26 -0500 Subject: [PATCH 05/20] add capturing to test case --- src/test/kotlin/com/nylas/NylasClientTest.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index 31f397de..9cc3795e 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -10,7 +10,11 @@ import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.whenever import java.net.SocketTimeoutException @@ -178,8 +182,12 @@ class NylasClientTest { private val mockResponse: Response = mock(Response::class.java) private val mockResponseBody: ResponseBody = mock(ResponseBody::class.java) + @Captor + private lateinit var requestCaptor: ArgumentCaptor + @BeforeEach fun setup() { + MockitoAnnotations.openMocks(this) val mockOkHttpClientBuilder: OkHttpClient.Builder = mock() whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) whenever(mockOkHttpClientBuilder.build()).thenReturn(mockHttpClient) @@ -196,8 +204,13 @@ class NylasClientTest { whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) val result = nylasClient.executeRequest>(urlBuilder, NylasClient.HttpMethod.GET, null, JsonHelper.mapTypeOf(String::class.java, String::class.java)) + // Capture the request passed to newCall + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value assertEquals("bar", result["foo"]) + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/") + assertEquals(capturedRequest.method(), "GET") } @Test From b2cea29ef92a7fc5c05ae454c90dc1f82eb8b220 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:54:58 -0500 Subject: [PATCH 06/20] add multipart test --- src/test/kotlin/com/nylas/NylasClientTest.kt | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index 9cc3795e..9f603d7e 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -348,8 +348,37 @@ class NylasClientTest { whenever(mockResponse.body()).thenReturn(mockResponseBody) val result = nylasClient.downloadResponse("test/path") + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value assertEquals(mockResponseBody, result) + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.method(), "GET") + } + + @Test + fun `should handle multipart request`() { + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) + + val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) + multipartBuilder.addFormDataPart("message", "abc123") + + nylasClient.executeFormRequest>( + "test/path", + NylasClient.HttpMethod.POST, + multipartBuilder.build(), + JsonHelper.mapTypeOf(String::class.java, String::class.java) + ) + + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.method(), "POST") + } + + @Test + fun `executeGet should set up the request with the correct params`() { + } } From 47253b446b358dc2ec69eee9d9c8795d4030b6f6 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:55:02 -0500 Subject: [PATCH 07/20] cleanup --- src/test/kotlin/com/nylas/NylasClientTest.kt | 24 +++----------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index 9f603d7e..12e4570b 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -192,19 +192,18 @@ class NylasClientTest { whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) whenever(mockOkHttpClientBuilder.build()).thenReturn(mockHttpClient) whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(mockResponseBody) nylasClient = NylasClient("testApiKey", mockOkHttpClientBuilder) } @Test fun `should handle successful request`() { val urlBuilder = nylasClient.newUrlBuilder() - whenever(mockCall.execute()).thenReturn(mockResponse) - whenever(mockResponse.isSuccessful).thenReturn(true) - whenever(mockResponse.body()).thenReturn(mockResponseBody) whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) val result = nylasClient.executeRequest>(urlBuilder, NylasClient.HttpMethod.GET, null, JsonHelper.mapTypeOf(String::class.java, String::class.java)) - // Capture the request passed to newCall verify(mockHttpClient).newCall(requestCaptor.capture()) val capturedRequest = requestCaptor.value @@ -218,10 +217,8 @@ class NylasClientTest { val tokenUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/token") val revokeUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/revoke") val oauthUrls = listOf(tokenUrlBuilder, revokeUrlBuilder) - whenever(mockCall.execute()).thenReturn(mockResponse) whenever(mockResponse.isSuccessful).thenReturn(false) whenever(mockResponse.code()).thenReturn(401) - whenever(mockResponse.body()).thenReturn(mockResponseBody) whenever(mockResponseBody.string()).thenReturn("{ \"error\": \"internal_error\", \"error_code\": 500, \"error_description\": \"Internal error, contact administrator\", \"error_uri\": \"https://accounts.nylas.io/#tag/Event-Codes\", \"request_id\": \"eccc9c3f-7150-48e1-965e-4f89714ab51a\" }") for (urlBuilder in oauthUrls) { @@ -242,10 +239,8 @@ class NylasClientTest { val tokenUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/token") val revokeUrlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/connect/revoke") val oauthUrls = listOf(tokenUrlBuilder, revokeUrlBuilder) - whenever(mockCall.execute()).thenReturn(mockResponse) whenever(mockResponse.isSuccessful).thenReturn(false) whenever(mockResponse.code()).thenReturn(500) - whenever(mockResponse.body()).thenReturn(mockResponseBody) whenever(mockResponseBody.string()).thenReturn("not a json") for (urlBuilder in oauthUrls) { @@ -264,10 +259,8 @@ class NylasClientTest { @Test fun `should throw NylasApiError on error from non-oauth endpoints`() { val urlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/some/other/path") - whenever(mockCall.execute()).thenReturn(mockResponse) whenever(mockResponse.isSuccessful).thenReturn(false) whenever(mockResponse.code()).thenReturn(400) - whenever(mockResponse.body()).thenReturn(mockResponseBody) whenever(mockResponseBody.string()).thenReturn("{ \"request_id\": \"4c2740b4-52a4-412e-bdee-49a6c6671b22\", \"error\": { \"type\": \"provider_error\", \"message\": \"Unauthorized\", \"provider_error\": { \"error\": \"Request had invalid authentication credentials.\" } } }") val exception = assertFailsWith { @@ -284,10 +277,8 @@ class NylasClientTest { @Test fun `should throw NylasApiError on error from non-oauth endpoints even if unexpected response from API`() { val urlBuilder = nylasClient.newUrlBuilder().addPathSegments("v3/some/other/path") - whenever(mockCall.execute()).thenReturn(mockResponse) whenever(mockResponse.isSuccessful).thenReturn(false) whenever(mockResponse.code()).thenReturn(400) - whenever(mockResponse.body()).thenReturn(mockResponseBody) whenever(mockResponseBody.string()).thenReturn("not a json") val exception = assertFailsWith { @@ -314,8 +305,6 @@ class NylasClientTest { @Test fun `should handle unexpected null response body`() { val urlBuilder = nylasClient.newUrlBuilder() - whenever(mockCall.execute()).thenReturn(mockResponse) - whenever(mockResponse.isSuccessful).thenReturn(true) whenever(mockResponse.body()).thenReturn(null) val exception = assertFailsWith { @@ -329,9 +318,6 @@ class NylasClientTest { /*@Test fun `should handle failed deserialization of response body`() { val urlBuilder = nylasClient.newUrlBuilder() - whenever(mockCall.execute()).thenReturn(mockResponse) - whenever(mockResponse.isSuccessful).thenReturn(true) - whenever(mockResponse.body()).thenReturn(mockResponseBody) whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("invalid json")) val exception = assertFailsWith { @@ -343,10 +329,6 @@ class NylasClientTest { @Test fun `should handle download request`() { - whenever(mockCall.execute()).thenReturn(mockResponse) - whenever(mockResponse.isSuccessful).thenReturn(true) - whenever(mockResponse.body()).thenReturn(mockResponseBody) - val result = nylasClient.downloadResponse("test/path") verify(mockHttpClient).newCall(requestCaptor.capture()) val capturedRequest = requestCaptor.value From 1094687e36aa18b868b096f813a6d604b8873cab Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:37:47 -0500 Subject: [PATCH 08/20] add more tests to the client --- src/test/kotlin/com/nylas/NylasClientTest.kt | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index 12e4570b..761be5f6 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -4,6 +4,7 @@ import com.nylas.models.NylasApiError import com.nylas.models.NylasOAuthError import com.nylas.models.NylasSdkTimeoutError import com.nylas.util.JsonHelper +import com.squareup.moshi.adapter import okhttp3.* import okio.Buffer import org.junit.jupiter.api.Assertions.* @@ -18,6 +19,7 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.whenever import java.net.SocketTimeoutException +import java.nio.charset.StandardCharsets import kotlin.test.assertFailsWith class NylasClientTest { @@ -360,7 +362,84 @@ class NylasClientTest { @Test fun `executeGet should set up the request with the correct params`() { + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) + nylasClient.executeGet>("test/path", JsonHelper.mapTypeOf(String::class.java, String::class.java)) + + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value + + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.method(), "GET") + } + + @Test + fun `executePut should set up the request with the correct params`() { + val putBody = "{ \"test\": \"updated\" }" + val type = JsonHelper.mapTypeOf(String::class.java, String::class.java) + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) + nylasClient.executePut>("test/path", type, putBody) + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value + val reqBody = capturedRequest.body() + val buffer = Buffer() + reqBody?.writeTo(buffer) + val requestBodyBuffer = buffer.readString(StandardCharsets.UTF_8) + + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.method(), "PUT") + assertEquals(requestBodyBuffer, putBody) + } + + @Test + fun `executePatch should set up the request with the correct params`() { + val patchBody = "{ \"test\": \"updated\" }" + val type = JsonHelper.mapTypeOf(String::class.java, String::class.java) + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) + nylasClient.executePatch>("test/path", type, patchBody) + + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value + val reqBody = capturedRequest.body() + val buffer = Buffer() + reqBody?.writeTo(buffer) + val requestBodyBuffer = buffer.readString(StandardCharsets.UTF_8) + + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.method(), "PATCH") + assertEquals(requestBodyBuffer, patchBody) + } + + @Test + fun `executePost should set up the request with the correct params`() { + val postBody = "{ \"test\": \"updated\" }" + val type = JsonHelper.mapTypeOf(String::class.java, String::class.java) + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) + nylasClient.executePost>("test/path", type, postBody) + + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value + val reqBody = capturedRequest.body() + val buffer = Buffer() + reqBody?.writeTo(buffer) + val requestBodyBuffer = buffer.readString(StandardCharsets.UTF_8) + + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.method(), "POST") + assertEquals(requestBodyBuffer, postBody) + } + + @Test + fun `executeDelete should set up the request with the correct params`() { + val type = JsonHelper.mapTypeOf(String::class.java, String::class.java) + whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) + nylasClient.executeDelete>("test/path", type) + + verify(mockHttpClient).newCall(requestCaptor.capture()) + val capturedRequest = requestCaptor.value + + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.method(), "DELETE") } } From ab617ba4c9ab4395b4d2388b21661c21e3839fd9 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 7 Dec 2023 19:00:18 -0500 Subject: [PATCH 09/20] cleanup --- src/test/kotlin/com/nylas/NylasClientTest.kt | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index 761be5f6..2d682dd0 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -4,7 +4,6 @@ import com.nylas.models.NylasApiError import com.nylas.models.NylasOAuthError import com.nylas.models.NylasSdkTimeoutError import com.nylas.util.JsonHelper -import com.squareup.moshi.adapter import okhttp3.* import okio.Buffer import org.junit.jupiter.api.Assertions.* @@ -381,10 +380,7 @@ class NylasClientTest { verify(mockHttpClient).newCall(requestCaptor.capture()) val capturedRequest = requestCaptor.value - val reqBody = capturedRequest.body() - val buffer = Buffer() - reqBody?.writeTo(buffer) - val requestBodyBuffer = buffer.readString(StandardCharsets.UTF_8) + val requestBodyBuffer = capturedRequest.body().asString() assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") assertEquals(capturedRequest.method(), "PUT") @@ -400,10 +396,7 @@ class NylasClientTest { verify(mockHttpClient).newCall(requestCaptor.capture()) val capturedRequest = requestCaptor.value - val reqBody = capturedRequest.body() - val buffer = Buffer() - reqBody?.writeTo(buffer) - val requestBodyBuffer = buffer.readString(StandardCharsets.UTF_8) + val requestBodyBuffer = capturedRequest.body().asString() assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") assertEquals(capturedRequest.method(), "PATCH") @@ -419,10 +412,7 @@ class NylasClientTest { verify(mockHttpClient).newCall(requestCaptor.capture()) val capturedRequest = requestCaptor.value - val reqBody = capturedRequest.body() - val buffer = Buffer() - reqBody?.writeTo(buffer) - val requestBodyBuffer = buffer.readString(StandardCharsets.UTF_8) + val requestBodyBuffer = capturedRequest.body().asString() assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") assertEquals(capturedRequest.method(), "POST") @@ -441,6 +431,16 @@ class NylasClientTest { assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") assertEquals(capturedRequest.method(), "DELETE") } + + /** + * Helper function to get the string value of a RequestBody + * @return String value of the RequestBody + */ + private fun RequestBody?.asString(): String { + val buffer = Buffer() + this?.writeTo(buffer) + return buffer.readString(StandardCharsets.UTF_8) + } } @Nested From ca2e459aaab961c52211e5c45357eba7ce0f4936 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Thu, 7 Dec 2023 19:22:05 -0500 Subject: [PATCH 10/20] Add query params to the test --- src/test/kotlin/com/nylas/NylasClientTest.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index 2d682dd0..5394f5a8 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -1,5 +1,6 @@ package com.nylas +import com.nylas.models.IQueryParams import com.nylas.models.NylasApiError import com.nylas.models.NylasOAuthError import com.nylas.models.NylasSdkTimeoutError @@ -361,13 +362,23 @@ class NylasClientTest { @Test fun `executeGet should set up the request with the correct params`() { + val mockQueryParams: IQueryParams = mock() + whenever(mockQueryParams.convertToMap()).thenReturn(mapOf( + "foo" to "bar", + "list" to listOf("a", "b", "c"), + "map" to mapOf("key1" to "value1", "key2" to "value2") + )) whenever(mockResponseBody.source()).thenReturn(Buffer().writeUtf8("{ \"foo\": \"bar\" }")) - nylasClient.executeGet>("test/path", JsonHelper.mapTypeOf(String::class.java, String::class.java)) + nylasClient.executeGet>( + "test/path", + JsonHelper.mapTypeOf(String::class.java, String::class.java), + mockQueryParams + ) verify(mockHttpClient).newCall(requestCaptor.capture()) val capturedRequest = requestCaptor.value - assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path") + assertEquals(capturedRequest.url().toString(), "https://api.us.nylas.com/test/path?foo=bar&list=a&list=b&list=c&map=key1%3Avalue1&map=key2%3Avalue2") assertEquals(capturedRequest.method(), "GET") } From 96950db73bd6ad5b9316265d6b9b6b906b2835d2 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Fri, 8 Dec 2023 22:57:56 -0500 Subject: [PATCH 11/20] AV-2436 Initial commit of Calendars --- .../com/nylas/resources/CalendarsTest.kt | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/test/kotlin/com/nylas/resources/CalendarsTest.kt diff --git a/src/test/kotlin/com/nylas/resources/CalendarsTest.kt b/src/test/kotlin/com/nylas/resources/CalendarsTest.kt new file mode 100644 index 00000000..8f716cad --- /dev/null +++ b/src/test/kotlin/com/nylas/resources/CalendarsTest.kt @@ -0,0 +1,205 @@ +package com.nylas.resources + +import com.nylas.NylasClient +import com.nylas.models.* +import com.nylas.models.Response +import com.nylas.util.JsonHelper +import com.squareup.moshi.Types +import okhttp3.* +import okio.Buffer +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.* +import java.lang.reflect.Type +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class CalendarsTest { + private val mockHttpClient: OkHttpClient = Mockito.mock(OkHttpClient::class.java) + private val mockCall: Call = Mockito.mock(Call::class.java) + private val mockResponse: okhttp3.Response = Mockito.mock(okhttp3.Response::class.java) + private val mockResponseBody: ResponseBody = Mockito.mock(ResponseBody::class.java) + private val mockOkHttpClientBuilder: OkHttpClient.Builder = Mockito.mock() + + @BeforeEach + fun setup() { + MockitoAnnotations.openMocks(this) + whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) + whenever(mockOkHttpClientBuilder.build()).thenReturn(mockHttpClient) + whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + } + + @Nested + inner class SerializationTests { + @Test + fun `Calendar serializes properly`() { + val adapter = JsonHelper.moshi().adapter(Calendar::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "grant_id": "abc-123-grant-id", + "description": "Description of my new calendar", + "hex_color": "#039BE5", + "hex_foreground_color": "#039BE5", + "id": "5d3qmne77v32r8l4phyuksl2x", + "is_owned_by_user": true, + "is_primary": true, + "location": "Los Angeles, CA", + "metadata": { + "your-key": "value" + }, + "name": "My New Calendar", + "object": "calendar", + "read_only": false, + "timezone": "America/Los_Angeles" + } + """.trimIndent() + ) + + val cal = adapter.fromJson(jsonBuffer)!! + assertIs(cal) + assertEquals("abc-123-grant-id", cal.grantId) + assertEquals("Description of my new calendar", cal.description) + assertEquals("#039BE5", cal.hexColor) + assertEquals("#039BE5", cal.hexForegroundColor) + assertEquals("5d3qmne77v32r8l4phyuksl2x", cal.id) + assertEquals(true, cal.isOwnedByUser) + assertEquals(true, cal.isPrimary) + assertEquals("Los Angeles, CA", cal.location) + assertEquals("My New Calendar", cal.name) + assertEquals(false, cal.readOnly) + assertEquals("America/Los_Angeles", cal.timezone) + } + } + + @Nested + inner class CrudTests { + private lateinit var grantId: String + private lateinit var mockNylasClient: NylasClient + private lateinit var calendars: Calendars + + @BeforeEach + fun setup() { + grantId = "abc-123-grant-id" + mockNylasClient = Mockito.mock(NylasClient::class.java) + calendars = Calendars(mockNylasClient) + } + + @Test + fun `listing calendars calls requests with the correct params`() { + calendars.list(grantId) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/calendars", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(ListResponse::class.java, Calendar::class.java), typeCaptor.firstValue) + } + + @Test + fun `finding a calendar calls requests with the correct params`() { + val calendarId = "calendar-123" + + calendars.find(grantId, calendarId) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/calendars/$calendarId", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Calendar::class.java), typeCaptor.firstValue) + } + + @Test + fun `creating a calendar calls requests with the correct params`() { + val adapter = JsonHelper.moshi().adapter(CreateCalendarRequest::class.java) + val createCalendarRequest = CreateCalendarRequest( + description = "Description of my new calendar", + location = "Los Angeles, CA", + metadata = mapOf("your-key" to "value"), + name = "My New Calendar", + timezone = "America/Los_Angeles" + ) + + calendars.create(grantId, createCalendarRequest) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/calendars", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Calendar::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(createCalendarRequest), requestBodyCaptor.firstValue) + } + + @Test + fun `updating a calendar calls requests with the correct params`() { + val calendarId = "calendar-123" + val adapter = JsonHelper.moshi().adapter(UpdateCalendarRequest::class.java) + val updateCalendarRequest = UpdateCalendarRequest( + description = "Description of my new calendar", + location = "Los Angeles, CA", + metadata = mapOf("your-key" to "value"), + name = "My New Calendar", + timezone = "America/Los_Angeles", + hexColor = "#039BE5", + hexForegroundColor = "#039BE5" + ) + + calendars.update(grantId, calendarId, updateCalendarRequest) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePut>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/calendars/$calendarId", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Calendar::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(updateCalendarRequest), requestBodyCaptor.firstValue) + } + + @Test + fun `destroying a calendar calls requests with the correct params`() { + val calendarId = "calendar-123" + + calendars.destroy(grantId, calendarId) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executeDelete>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/calendars/$calendarId", pathCaptor.firstValue) + assertEquals(DeleteResponse::class.java, typeCaptor.firstValue) + } + } +} \ No newline at end of file From f95fa0142e87a453a98837901290ec020f2a966b Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Fri, 8 Dec 2023 23:22:19 -0500 Subject: [PATCH 12/20] AV-2436 add remaining tests --- .../com/nylas/resources/CalendarsTest.kt | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/test/kotlin/com/nylas/resources/CalendarsTest.kt b/src/test/kotlin/com/nylas/resources/CalendarsTest.kt index 8f716cad..b966e161 100644 --- a/src/test/kotlin/com/nylas/resources/CalendarsTest.kt +++ b/src/test/kotlin/com/nylas/resources/CalendarsTest.kt @@ -202,4 +202,105 @@ class CalendarsTest { assertEquals(DeleteResponse::class.java, typeCaptor.firstValue) } } + + @Nested + inner class OtherMethodTests { + private lateinit var grantId: String + private lateinit var mockNylasClient: NylasClient + private lateinit var calendars: Calendars + + @BeforeEach + fun setup() { + mockNylasClient = Mockito.mock(NylasClient::class.java) + calendars = Calendars(mockNylasClient) + } + + @Test + fun `getting availability calls requests with the correct params`() { + val adapter = JsonHelper.moshi().adapter(GetAvailabilityRequest::class.java) + val getAvailabilityRequest = GetAvailabilityRequest( + startTime = 1620000000, + endTime = 1620000000, + participants = listOf( + AvailabilityParticipant( + email = "test@nylas.com", + calendarIds = listOf("calendar-123"), + openHours = listOf( + OpenHours( + days = listOf(0, 1, 2), + start = "09:00", + end = "17:00", + timezone = "America/Los_Angeles", + exdates = listOf("2021-05-03", "2021-05-04") + ) + ) + ) + ), + durationMinutes = 30, + intervalMinutes = 15, + roundTo30Minutes = true, + availabilityRules = AvailabilityRules( + availabilityMethod = AvailabilityMethod.MAX_AVAILABILITY, + buffer = MeetingBuffer( + before = 15, + after = 15 + ), + defaultOpenHours = listOf( + OpenHours( + days = listOf(0, 1, 2), + start = "09:00", + end = "17:00", + timezone = "America/Los_Angeles", + exdates = listOf("2021-05-03", "2021-05-04") + ) + ), + roundRobinEventId = "event-123" + ) + ) + + calendars.getAvailability(getAvailabilityRequest) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/calendars/availability", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, GetAvailabilityResponse::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(getAvailabilityRequest), requestBodyCaptor.firstValue) + } + + @Test + fun `getting free busy calls requests with the correct params`() { + val grantId = "abc-123-grant-id" + val adapter = JsonHelper.moshi().adapter(GetFreeBusyRequest::class.java) + val getFreeBusyRequest = GetFreeBusyRequest( + startTime = 1620000000, + endTime = 1620000000, + emails = listOf("test@nylas.com") + ) + + calendars.getFreeBusy(grantId, getFreeBusyRequest) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + val listOfFreeBusy = Types.newParameterizedType(List::class.java, GetFreeBusyResponse::class.java) + assertEquals("v3/grants/$grantId/calendars/free-busy", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, listOfFreeBusy), typeCaptor.firstValue) + assertEquals(adapter.toJson(getFreeBusyRequest), requestBodyCaptor.firstValue) + } + } } \ No newline at end of file From 264d29874d3fd1bb8f46d659b30fa23acc08c458 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:40:25 -0500 Subject: [PATCH 13/20] EvenVisibility -> EventVisibility --- src/main/kotlin/com/nylas/models/CreateEventRequest.kt | 6 +++--- src/main/kotlin/com/nylas/models/Event.kt | 2 +- .../nylas/models/{EvenVisibility.kt => EventVisibility.kt} | 2 +- src/main/kotlin/com/nylas/models/UpdateEventRequest.kt | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/main/kotlin/com/nylas/models/{EvenVisibility.kt => EventVisibility.kt} (88%) diff --git a/src/main/kotlin/com/nylas/models/CreateEventRequest.kt b/src/main/kotlin/com/nylas/models/CreateEventRequest.kt index 111d4568..f7ff15ae 100644 --- a/src/main/kotlin/com/nylas/models/CreateEventRequest.kt +++ b/src/main/kotlin/com/nylas/models/CreateEventRequest.kt @@ -84,7 +84,7 @@ data class CreateEventRequest( * Sets the visibility for the event. The calendar default will be used if this field is omitted. */ @Json(name = "visibility") - val visibility: EvenVisibility? = null, + val visibility: EventVisibility? = null, /** * Sets the maximum number of participants that may attend the event. */ @@ -436,7 +436,7 @@ data class CreateEventRequest( private var recurrence: List? = null private var calendarId: String? = null private var readOnly: Boolean? = null - private var visibility: EvenVisibility? = null + private var visibility: EventVisibility? = null private var capacity: Int? = null private var hideParticipant: Boolean? = null @@ -537,7 +537,7 @@ data class CreateEventRequest( * @param visibility The event visibility. * @return The builder. */ - fun visibility(visibility: EvenVisibility) = apply { this.visibility = visibility } + fun visibility(visibility: EventVisibility) = apply { this.visibility = visibility } /** * Set the maximum number of participants that may attend the event. diff --git a/src/main/kotlin/com/nylas/models/Event.kt b/src/main/kotlin/com/nylas/models/Event.kt index 75123389..d3b15fb4 100644 --- a/src/main/kotlin/com/nylas/models/Event.kt +++ b/src/main/kotlin/com/nylas/models/Event.kt @@ -123,7 +123,7 @@ data class Event( * Visibility of the event, if the event is private or public. */ @Json(name = "visibility") - val visibility: EvenVisibility? = null, + val visibility: EventVisibility? = null, /** * User who created the event. * Not supported for all providers. diff --git a/src/main/kotlin/com/nylas/models/EvenVisibility.kt b/src/main/kotlin/com/nylas/models/EventVisibility.kt similarity index 88% rename from src/main/kotlin/com/nylas/models/EvenVisibility.kt rename to src/main/kotlin/com/nylas/models/EventVisibility.kt index cd8c9e1c..276e0d2d 100644 --- a/src/main/kotlin/com/nylas/models/EvenVisibility.kt +++ b/src/main/kotlin/com/nylas/models/EventVisibility.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.Json /** * Enum representation of visibility of the event, if the event is private or public. */ -enum class EvenVisibility { +enum class EventVisibility { @Json(name = "public") PUBLIC, diff --git a/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt b/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt index 71eadfed..dbb22e83 100644 --- a/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt +++ b/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt @@ -84,7 +84,7 @@ data class UpdateEventRequest( * Sets the visibility for the event. The calendar default will be used if this field is omitted. */ @Json(name = "visibility") - val visibility: EvenVisibility? = null, + val visibility: EventVisibility? = null, /** * Sets the maximum number of participants that may attend the event. */ @@ -519,7 +519,7 @@ data class UpdateEventRequest( private var recurrence: List? = null private var calendarId: String? = null private var readOnly: Boolean? = null - private var visibility: EvenVisibility? = null + private var visibility: EventVisibility? = null private var capacity: Int? = null private var hideParticipant: Boolean? = null @@ -628,7 +628,7 @@ data class UpdateEventRequest( * @param visibility Sets the visibility for the event. The calendar default will be used if this field is omitted. * @return The builder. */ - fun visibility(visibility: EvenVisibility) = apply { this.visibility = visibility } + fun visibility(visibility: EventVisibility) = apply { this.visibility = visibility } /** * Update the capacity of the event. From 7ab2e6be05b03f0c733c989d456fdea4eb7ecd7c Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:30:41 -0500 Subject: [PATCH 14/20] AV-2437 Events Test suite --- .../com/nylas/models/CreateEventRequest.kt | 2 +- .../com/nylas/models/SendRsvpQueryParams.kt | 12 +- .../kotlin/com/nylas/resources/EventsTests.kt | 339 ++++++++++++++++++ 3 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 src/test/kotlin/com/nylas/resources/EventsTests.kt diff --git a/src/main/kotlin/com/nylas/models/CreateEventRequest.kt b/src/main/kotlin/com/nylas/models/CreateEventRequest.kt index f7ff15ae..9f72b17a 100644 --- a/src/main/kotlin/com/nylas/models/CreateEventRequest.kt +++ b/src/main/kotlin/com/nylas/models/CreateEventRequest.kt @@ -14,7 +14,7 @@ data class CreateEventRequest( * - [CreateEventRequest.When.Timespan] */ @Json(name = "when") - val whenObj: When = When.Time(0), + val whenObj: When, /** * Creates an event with the specified title. */ diff --git a/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt b/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt index 3d7f988c..377bbeb2 100644 --- a/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt @@ -7,22 +7,22 @@ import com.squareup.moshi.Json */ data class SendRsvpQueryParams( /** - * The RSVP status for the event. Must be yes, no, or maybe + * The ID of the calendar to create the event in. */ - @Json(name = "status") - val status: RsvpStatus, + @Json(name = "calendar_id") + val calendarId: String, ) : IQueryParams { /** * Builder for [SendRsvpQueryParams]. - * @param status The RSVP status for the event. Must be yes, no, or maybe + * @param calendarId The ID of the calendar to create the event in. */ - data class Builder(private val status: RsvpStatus) { + data class Builder(private val calendarId: String) { /** * Builds a [SendRsvpQueryParams] instance. * @return The [SendRsvpQueryParams] instance. */ - fun build() = SendRsvpQueryParams(status) + fun build() = SendRsvpQueryParams(calendarId) } } diff --git a/src/test/kotlin/com/nylas/resources/EventsTests.kt b/src/test/kotlin/com/nylas/resources/EventsTests.kt new file mode 100644 index 00000000..5816da9b --- /dev/null +++ b/src/test/kotlin/com/nylas/resources/EventsTests.kt @@ -0,0 +1,339 @@ +package com.nylas.resources + +import com.nylas.NylasClient +import com.nylas.models.* +import com.nylas.models.Response +import com.nylas.util.JsonHelper +import com.squareup.moshi.Types +import okhttp3.* +import okio.Buffer +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.* +import java.lang.reflect.Type +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class EventsTests { + private val mockHttpClient: OkHttpClient = Mockito.mock(OkHttpClient::class.java) + private val mockCall: Call = Mockito.mock(Call::class.java) + private val mockResponse: okhttp3.Response = Mockito.mock(okhttp3.Response::class.java) + private val mockResponseBody: ResponseBody = Mockito.mock(ResponseBody::class.java) + private val mockOkHttpClientBuilder: OkHttpClient.Builder = Mockito.mock() + + @BeforeEach + fun setup() { + MockitoAnnotations.openMocks(this) + whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) + whenever(mockOkHttpClientBuilder.build()).thenReturn(mockHttpClient) + whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + } + + @Nested + inner class SerializationTests { + @Test + fun `Event serializes properly`() { + val adapter = JsonHelper.moshi().adapter(Event::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "busy": true, + "calendar_id": "7d93zl2palhxqdy6e5qinsakt", + "conferencing": { + "provider": "Zoom Meeting", + "details": { + "meeting_code": "code-123456", + "password": "password-123456", + "url": "https://zoom.us/j/1234567890?pwd=1234567890" + } + }, + "created_at": 1661874192, + "description": "Description of my new calendar", + "hide_participants": false, + "grant_id": "41009df5-bf11-4c97-aa18-b285b5f2e386", + "html_link": "https://www.google.com/calendar/event?eid=bTMzcGJrNW4yYjk4bjk3OWE4Ef3feD2VuM29fMjAyMjA2MjdUMjIwMDAwWiBoYWxsYUBueWxhcy5jb20", + "id": "5d3qmne77v32r8l4phyuksl2x", + "location": "Roller Rink", + "metadata": { + "your_key": "your_value" + }, + "object": "event", + "organizer": { + "email": "organizer@example.com", + "name": "" + }, + "participants": [ + { + "comment": "Aristotle", + "email": "aristotle@example.com", + "name": "Aristotle", + "phone_number": "+1 23456778", + "status": "maybe" + } + ], + "read_only": false, + "reminders": { + "use_default": false, + "overrides": [ + { + "reminder_minutes": 10, + "reminder_method": "email" + } + ] + }, + "recurrence": [ + "RRULE:FREQ=WEEKLY;BYDAY=MO", + "EXDATE:20211011T000000Z" + ], + "status": "confirmed", + "title": "Birthday Party", + "updated_at": 1661874192, + "visibility": "private", + "when": { + "start_time": 1661874192, + "end_time": 1661877792, + "start_timezone": "America/New_York", + "end_timezone": "America/New_York", + "object": "timespan" + } + } + """.trimIndent() + ) + + val event = adapter.fromJson(jsonBuffer)!! + assertIs(event) + assertEquals(true, event.busy) + assertEquals("7d93zl2palhxqdy6e5qinsakt", event.calendarId) + assertIs(event.conferencing) + val conferencingDetails = event.conferencing as Conferencing.Details + assertEquals(ConferencingProvider.ZOOM_MEETING, conferencingDetails.provider) + assertEquals("code-123456", conferencingDetails.details.meetingCode) + assertEquals("password-123456", conferencingDetails.details.password) + assertEquals("https://zoom.us/j/1234567890?pwd=1234567890", conferencingDetails.details.url) + assertEquals(1661874192, event.createdAt) + assertEquals("Description of my new calendar", event.description) + assertEquals(false, event.hideParticipants) + assertEquals("41009df5-bf11-4c97-aa18-b285b5f2e386", event.grantId) + assertEquals("https://www.google.com/calendar/event?eid=bTMzcGJrNW4yYjk4bjk3OWE4Ef3feD2VuM29fMjAyMjA2MjdUMjIwMDAwWiBoYWxsYUBueWxhcy5jb20", event.htmlLink) + assertEquals("5d3qmne77v32r8l4phyuksl2x", event.id) + assertEquals("Roller Rink", event.location) + assertEquals(mapOf("your_key" to "your_value"), event.metadata) + assertEquals("event", event.getObject()) + assertEquals("organizer@example.com", event.organizer?.email) + assertEquals("", event.organizer?.name) + assertEquals(1, event.participants.size) + assertEquals("Aristotle", event.participants[0].comment) + assertEquals("aristotle@example.com", event.participants[0].email) + assertEquals("Aristotle", event.participants[0].name) + assertEquals("+1 23456778", event.participants[0].phoneNumber) + assertEquals(ParticipantStatus.MAYBE, event.participants[0].status) + assertEquals(false, event.readOnly) + assertEquals(2, event.recurrence?.size) + assertEquals("RRULE:FREQ=WEEKLY;BYDAY=MO", event.recurrence?.get(0)) + assertEquals("EXDATE:20211011T000000Z", event.recurrence?.get(1)) + assertEquals(EventStatus.CONFIRMED, event.status) + assertEquals("Birthday Party", event.title) + assertEquals(1661874192, event.updatedAt) + assertEquals(EventVisibility.PRIVATE, event.visibility) + assertIs(event.getWhen()) + val whenTimespan = event.getWhen() as When.Timespan + assertEquals(1661874192, whenTimespan.startTime) + assertEquals(1661877792, whenTimespan.endTime) + assertEquals("America/New_York", whenTimespan.startTimezone) + assertEquals("America/New_York", whenTimespan.endTimezone) + } + } + + @Nested + inner class CrudTests { + private lateinit var grantId: String + private lateinit var mockNylasClient: NylasClient + private lateinit var events: Events + + @BeforeEach + fun setup() { + grantId = "abc-123-grant-id" + mockNylasClient = Mockito.mock(NylasClient::class.java) + events = Events(mockNylasClient) + } + + @Test + fun `listing events calls requests with the correct params`() { + val listEventQueryParams = ListEventQueryParams( + calendarId = "calendar-id", + ) + + events.list(grantId, listEventQueryParams) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/events", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(ListResponse::class.java, Event::class.java), typeCaptor.firstValue) + } + + @Test + fun `finding a event calls requests with the correct params`() { + val eventId = "event-123" + val findEventQueryParams = FindEventQueryParams( + calendarId = "calendar-id", + ) + + events.find(grantId, eventId, findEventQueryParams) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/events/$eventId", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Event::class.java), typeCaptor.firstValue) + } + + @Test + fun `creating a event calls requests with the correct params`() { + val adapter = JsonHelper.moshi().adapter(CreateEventRequest::class.java) + val createEventRequest = CreateEventRequest( + whenObj = CreateEventRequest.When.Timespan( + startTime = 1620000000, + endTime = 1620000000, + startTimezone = "America/Los_Angeles", + endTimezone = "America/Los_Angeles" + ), + description = "Description of my new event", + location = "Los Angeles, CA", + metadata = mapOf("your-key" to "value"), + ) + val createEventQueryParams = CreateEventQueryParams( + calendarId = "calendar-id", + notifyParticipants = true, + ) + + events.create(grantId, createEventRequest, createEventQueryParams) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/events", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Event::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(createEventRequest), requestBodyCaptor.firstValue) + } + + @Test + fun `updating a event calls requests with the correct params`() { + val eventId = "event-123" + val adapter = JsonHelper.moshi().adapter(UpdateEventRequest::class.java) + val updateEventRequest = UpdateEventRequest( + description = "Description of my new event", + location = "Los Angeles, CA", + ) + val updateEventQueryParams = UpdateEventQueryParams( + calendarId = "calendar-id", + notifyParticipants = true, + ) + + events.update(grantId, eventId, updateEventRequest, updateEventQueryParams) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePut>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/events/$eventId", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Event::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(updateEventRequest), requestBodyCaptor.firstValue) + } + + @Test + fun `destroying a event calls requests with the correct params`() { + val eventId = "event-123" + val destroyEventQueryParams = DestroyEventQueryParams( + calendarId = "calendar-id", + notifyParticipants = true, + ) + + events.destroy(grantId, eventId, destroyEventQueryParams) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executeDelete>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/events/$eventId", pathCaptor.firstValue) + assertEquals(DeleteResponse::class.java, typeCaptor.firstValue) + } + } + + @Nested + inner class OtherMethodTests { + private lateinit var grantId: String + private lateinit var mockNylasClient: NylasClient + private lateinit var events: Events + + @BeforeEach + fun setup() { + grantId = "abc-123-grant-id" + mockNylasClient = Mockito.mock(NylasClient::class.java) + events = Events(mockNylasClient) + } + + @Test + fun `sending RSVP calls requests with the correct params`() { + val eventId = "event-123" + val adapter = JsonHelper.moshi().adapter(SendRsvpRequest::class.java) + val sendRsvpRequest = SendRsvpRequest( + status = RsvpStatus.YES, + ) + val sendRsvpQueryParams = SendRsvpQueryParams( + calendarId = "calendar-id", + ) + + events.sendRsvp(grantId, eventId, sendRsvpRequest, sendRsvpQueryParams) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/grants/$grantId/events/$eventId/send-rsvp", pathCaptor.firstValue) + assertEquals(DeleteResponse::class.java, typeCaptor.firstValue) + assertEquals(adapter.toJson(sendRsvpRequest), requestBodyCaptor.firstValue) + } + } +} \ No newline at end of file From 545ffdf24bfebbb8468c014dbba5527de9861a73 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:03:46 -0500 Subject: [PATCH 15/20] AV-2439 Add enum for Prompt --- src/main/kotlin/com/nylas/models/Prompt.kt | 14 ++++++++++++++ .../com/nylas/models/UrlForAuthenticationConfig.kt | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/com/nylas/models/Prompt.kt diff --git a/src/main/kotlin/com/nylas/models/Prompt.kt b/src/main/kotlin/com/nylas/models/Prompt.kt new file mode 100644 index 00000000..27929ef5 --- /dev/null +++ b/src/main/kotlin/com/nylas/models/Prompt.kt @@ -0,0 +1,14 @@ +package com.nylas.models + +import com.squareup.moshi.Json + +enum class Prompt { + @Json(name = "select_provider") + SELECT_PROVIDER, + @Json(name = "detect") + DETECT, + @Json(name = "select_provider,detect") + SELECT_PROVIDER_DETECT, + @Json(name = "detect,select_provider") + DETECT_SELECT_PROVIDER, +} \ No newline at end of file diff --git a/src/main/kotlin/com/nylas/models/UrlForAuthenticationConfig.kt b/src/main/kotlin/com/nylas/models/UrlForAuthenticationConfig.kt index 05be1b39..8b1be0ae 100644 --- a/src/main/kotlin/com/nylas/models/UrlForAuthenticationConfig.kt +++ b/src/main/kotlin/com/nylas/models/UrlForAuthenticationConfig.kt @@ -31,7 +31,7 @@ data class UrlForAuthenticationConfig( * The prompt parameter is used to force the consent screen to be displayed even if the user has already given consent to your application. */ @Json(name = "prompt") - val prompt: String? = null, + val prompt: Prompt? = null, /** * A space-delimited list of scopes that identify the resources that your application could access on the user's behalf. * If no scope is given, all of the default integration's scopes are used. @@ -66,7 +66,7 @@ data class UrlForAuthenticationConfig( ) { private var accessType: AccessType = AccessType.ONLINE private var provider: AuthProvider? = null - private var prompt: String? = null + private var prompt: Prompt? = null private var scope: List? = null private var includeGrantScopes: Boolean? = null private var state: String? = null @@ -92,7 +92,7 @@ data class UrlForAuthenticationConfig( * @param prompt The prompt parameter. * @return This builder. */ - fun prompt(prompt: String) = apply { this.prompt = prompt } + fun prompt(prompt: Prompt) = apply { this.prompt = prompt } /** * Set the scopes that identify the resources that your application could access on the user's behalf. From 77c010a30f6c6a15cf871ee26063a5f9c039c2cb Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:04:10 -0500 Subject: [PATCH 16/20] AV-2439 Fix scope encoding for OAuth URL --- src/main/kotlin/com/nylas/resources/Auth.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/nylas/resources/Auth.kt b/src/main/kotlin/com/nylas/resources/Auth.kt index 27792c4f..87ae6fce 100644 --- a/src/main/kotlin/com/nylas/resources/Auth.kt +++ b/src/main/kotlin/com/nylas/resources/Auth.kt @@ -165,7 +165,11 @@ class Auth(private val client: NylasClient) { .adapter(UrlForAuthenticationConfig::class.java) .toJson(config) JsonHelper.jsonToMap(json).forEach { (key, value) -> - url.addQueryParameter(key, value.toString()) + if(key == "scope") { + url.addQueryParameter("scope", config.scope?.joinToString(separator = " ")) + } else { + url.addQueryParameter(key, value.toString()) + } } return url From f17f92e3dadaa107a194ac9a7f5c88a882448548 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:05:32 -0500 Subject: [PATCH 17/20] AV-2439 Fix provider detect path --- src/main/kotlin/com/nylas/resources/Auth.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/nylas/resources/Auth.kt b/src/main/kotlin/com/nylas/resources/Auth.kt index 87ae6fce..40d2b9b8 100644 --- a/src/main/kotlin/com/nylas/resources/Auth.kt +++ b/src/main/kotlin/com/nylas/resources/Auth.kt @@ -136,7 +136,7 @@ class Auth(private val client: NylasClient) { */ @Throws(NylasApiError::class, NylasSdkTimeoutError::class) fun detectProvider(params: ProviderDetectParams): Response { - val path = "v3/grants/connect/providers/detect" + val path = "v3/providers/detect" val responseType = Types.newParameterizedType(Response::class.java, ProviderDetectResponse::class.java) return client.executePost(path, responseType, queryParams = params) From 816079a72d9f58ca9fdc099fc492ef9f1ff88d5f Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:05:45 -0500 Subject: [PATCH 18/20] AV-2439 Add Auth test suite --- .../kotlin/com/nylas/resources/AuthTests.kt | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/test/kotlin/com/nylas/resources/AuthTests.kt diff --git a/src/test/kotlin/com/nylas/resources/AuthTests.kt b/src/test/kotlin/com/nylas/resources/AuthTests.kt new file mode 100644 index 00000000..5d9a5da5 --- /dev/null +++ b/src/test/kotlin/com/nylas/resources/AuthTests.kt @@ -0,0 +1,245 @@ +package com.nylas.resources + +import com.nylas.NylasClient +import com.nylas.models.* +import com.nylas.util.JsonHelper +import com.squareup.moshi.Types +import okhttp3.Call +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.ResponseBody +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.lang.reflect.Type +import java.util.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class AuthTests { + private val mockHttpClient: OkHttpClient = Mockito.mock(OkHttpClient::class.java) + private val mockCall: Call = Mockito.mock(Call::class.java) + private val mockResponse: okhttp3.Response = Mockito.mock(okhttp3.Response::class.java) + private val mockResponseBody: ResponseBody = Mockito.mock(ResponseBody::class.java) + private val mockOkHttpClientBuilder: OkHttpClient.Builder = Mockito.mock() + private val baseUrl = "https://api.test.nylas.com" + private lateinit var grantId: String + private lateinit var mockNylasClient: NylasClient + private lateinit var auth: Auth + + @BeforeEach + fun setup() { + MockitoAnnotations.openMocks(this) + whenever(mockOkHttpClientBuilder.addInterceptor(any())).thenReturn(mockOkHttpClientBuilder) + whenever(mockOkHttpClientBuilder.build()).thenReturn(mockHttpClient) + whenever(mockHttpClient.newCall(any())).thenReturn(mockCall) + whenever(mockCall.execute()).thenReturn(mockResponse) + whenever(mockResponse.isSuccessful).thenReturn(true) + whenever(mockResponse.body()).thenReturn(mockResponseBody) + grantId = "abc-123-grant-id" + mockNylasClient = Mockito.mock(NylasClient::class.java) + whenever(mockNylasClient.newUrlBuilder()).thenReturn(HttpUrl.get(baseUrl).newBuilder()) + auth = Auth(mockNylasClient) + } + + @Nested + inner class OAuthTests { + private val config = UrlForAuthenticationConfig( + clientId = "abc-123", + redirectUri = "https://example.com/oauth/callback", + scope = listOf("email.read_only", "calendar", "contacts"), + loginHint = "test@gmail.com", + provider = AuthProvider.GOOGLE, + prompt = Prompt.SELECT_PROVIDER_DETECT, + state = "abc-123-state", + ) + + @Test + fun `urlForOAuth2 should return the correct URL`() { + val url = auth.urlForOAuth2(config) + + assert(url == "https://api.test.nylas.com/v3/connect/auth?client_id=abc-123&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&access_type=online&provider=google&prompt=select_provider%2Cdetect&scope=email.read_only%20calendar%20contacts&state=abc-123-state&login_hint=test%40gmail.com&response_type=code") + } + + @Test + fun `urlForOAuth2PKCE should return correct object with secret hashed and set`() { + val config = UrlForAuthenticationConfig( + clientId = "abc-123", + redirectUri = "https://example.com/oauth/callback", + scope = listOf("email.read_only", "calendar", "contacts"), + loginHint = "test@gmail.com", + provider = AuthProvider.GOOGLE, + prompt = Prompt.SELECT_PROVIDER_DETECT, + state = "abc-123-state", + ) + val mockUuid: UUID = Mockito.mock() + whenever(mockUuid.toString()).thenReturn("nylas") + val staticUuid = Mockito.mockStatic(UUID::class.java) + staticUuid.`when` { UUID.randomUUID() }.thenReturn(mockUuid) + + val pkceAuthURL = auth.urlForOAuth2PKCE(config) + + assert(pkceAuthURL.url == "https://api.test.nylas.com/v3/connect/auth?client_id=abc-123&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&access_type=online&provider=google&prompt=select_provider%2Cdetect&scope=email.read_only%20calendar%20contacts&state=abc-123-state&login_hint=test%40gmail.com&response_type=code&code_challenge_method=s256&code_challenge=ZTk2YmY2Njg2YTNjMzUxMGU5ZTkyN2RiNzA2OWNiMWNiYTliOTliMDIyZjQ5NDgzYTZjZTMyNzA4MDllNjhhMg") + assert(pkceAuthURL.secret == "nylas") + assert(pkceAuthURL.secretHash == "ZTk2YmY2Njg2YTNjMzUxMGU5ZTkyN2RiNzA2OWNiMWNiYTliOTliMDIyZjQ5NDgzYTZjZTMyNzA4MDllNjhhMg") + } + + @Test + fun `urlForAdminConsent should return the correct URL`() { + val url = auth.urlForAdminConsent(config, "abc-123-credential-id") + + assert(url == "https://api.test.nylas.com/v3/connect/auth?client_id=abc-123&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&access_type=online&provider=google&prompt=select_provider%2Cdetect&scope=email.read_only%20calendar%20contacts&state=abc-123-state&login_hint=test%40gmail.com&response_type=adminconsent&credential_id=abc-123-credential-id") + } + } + + @Nested + inner class TokenTests { + @Test + fun `exchangeCodeForToken should return the correct URL`() { + val adapter = JsonHelper.moshi().adapter(CodeExchangeRequest::class.java) + val request = CodeExchangeRequest( + redirectUri = "https://example.com/oauth/callback", + code = "abc-123-code", + clientId = "abc-123", + clientSecret = "abc-123-secret", + codeVerifier = "abc-123-verifier", + ) + + auth.exchangeCodeForToken(request) + + val json = adapter.toJson(request) + val jsonMap = JsonHelper.jsonToMap(json) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/connect/token", pathCaptor.firstValue) + assertEquals(CodeExchangeResponse::class.java, typeCaptor.firstValue) + assertEquals(json, requestBodyCaptor.firstValue) + assertEquals("authorization_code", jsonMap["grant_type"]) + } + + @Test + fun `refreshAccessToken should return the correct URL`() { + val adapter = JsonHelper.moshi().adapter(TokenExchangeRequest::class.java) + val request = TokenExchangeRequest( + redirectUri = "https://example.com/oauth/callback", + refreshToken = "abc-123-refresh-token", + clientId = "abc-123", + clientSecret = "abc-123-secret", + ) + + auth.refreshAccessToken(request) + + val json = adapter.toJson(request) + val jsonMap = JsonHelper.jsonToMap(json) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/connect/token", pathCaptor.firstValue) + assertEquals(CodeExchangeResponse::class.java, typeCaptor.firstValue) + assertEquals(json, requestBodyCaptor.firstValue) + assertEquals("refresh_token", jsonMap["grant_type"]) + } + } + + @Nested + inner class OtherAuthTests { + @Test + fun `customAuthentication calls requests with the correct params`() { + val adapter = JsonHelper.moshi().adapter(CreateGrantRequest::class.java) + val request = CreateGrantRequest( + provider = AuthProvider.GOOGLE, + settings = mapOf("email" to "test@nylas.com"), + state = "abc-123-state", + scopes = listOf("email.read_only", "calendar", "contacts"), + ) + + auth.customAuthentication(request) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/connect/custom", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, Grant::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(request), requestBodyCaptor.firstValue) + } + + @Test + fun `detectProvider calls requests with the correct params`() { + val request = ProviderDetectParams( + email = "test@nylas.com", + clientId = "abc-123", + allProviderTypes = true, + ) + + auth.detectProvider(request) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/providers/detect", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, ProviderDetectResponse::class.java), typeCaptor.firstValue) + assertEquals(request, queryParamCaptor.firstValue) + } + + @Test + fun `revoke calls requests with the correct params`() { + val token = "user-token" + + val res = auth.revoke(token) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture() + ) + + assertEquals("v3/connect/revoke?token=user-token", pathCaptor.firstValue) + assertNull(requestBodyCaptor.firstValue) + assertEquals(true, res) + } + } +} \ No newline at end of file From 83e31a0b2a09dab7c9d40397f9f59a8c0bcb41e9 Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:08:13 -0500 Subject: [PATCH 19/20] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 420b4ea8..3e6b61a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,14 @@ ### Added * Added support for sending drafts * Added support for the contacts API +* Added enum support for OAuth prompt ### Changed * Fixed issue with sending scheduled messages * Fixed incorrect PKCE code challenge generation +* Fixed provider detect endpoint path +* Fixed scope encoding for OAuth URL +* Fixed typo in 'EventVisibility' enum ## [2.0.0-beta.3] - Released 2023-12-18 From 5c03ff1daa6458311c97d147645047278e05a91e Mon Sep 17 00:00:00 2001 From: Mostafa Rashed <17770919+mrashed-dev@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:23:35 -0500 Subject: [PATCH 20/20] Add JSON deserialization tags to TrackingOptions --- src/main/kotlin/com/nylas/models/TrackingOptions.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/kotlin/com/nylas/models/TrackingOptions.kt b/src/main/kotlin/com/nylas/models/TrackingOptions.kt index b99cd4d5..aaaa3a17 100644 --- a/src/main/kotlin/com/nylas/models/TrackingOptions.kt +++ b/src/main/kotlin/com/nylas/models/TrackingOptions.kt @@ -1,5 +1,7 @@ package com.nylas.models +import com.squareup.moshi.Json + /** * Class representing the different tracking options for when a message is sent. */ @@ -7,17 +9,21 @@ data class TrackingOptions( /** * The label to apply to tracked messages. */ + @Json(name = "label") val label: String? = null, /** * Whether to track links. */ + @Json(name = "links") val links: Boolean? = null, /** * Whether to track opens. */ + @Json(name = "opens") val opens: Boolean? = null, /** * Whether to track thread replies. */ + @Json(name = "thread_replies") val threadReplies: Boolean? = null, )