From c99f9fea881c60fe2d3c54386d57fa8343264ec8 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 11 Dec 2022 11:57:13 +0100 Subject: [PATCH] Create API token when signing in --- .../mealient/data/auth/AuthDataSource.kt | 2 + .../kirmanak/mealient/data/auth/AuthRepo.kt | 4 -- .../mealient/data/auth/AuthStorage.kt | 8 --- .../data/auth/impl/AuthDataSourceImpl.kt | 20 +++++-- .../mealient/data/auth/impl/AuthRepoImpl.kt | 23 ++------ .../data/auth/impl/AuthStorageImpl.kt | 14 ----- .../data/auth/impl/AuthRepoImplTest.kt | 57 ++++--------------- .../data/auth/impl/AuthStorageImplTest.kt | 16 ------ .../mealient/test/AuthImplTestData.kt | 2 + .../datasource/AuthenticationProvider.kt | 2 + .../datasource/impl/MealieAuthenticator.kt | 21 +++++-- .../datasource/v0/MealieDataSourceV0.kt | 6 ++ .../datasource/v0/MealieDataSourceV0Impl.kt | 11 +++- .../mealient/datasource/v0/MealieServiceV0.kt | 6 ++ .../v0/models/CreateApiTokenRequestV0.kt | 9 +++ .../datasource/v1/MealieDataSourceV1.kt | 7 +++ .../datasource/v1/MealieDataSourceV1Impl.kt | 11 +++- .../mealient/datasource/v1/MealieServiceV1.kt | 6 ++ .../v1/models/CreateApiTokenRequestV1.kt | 9 +++ .../v1/models/CreateApiTokenResponseV1.kt | 9 +++ .../datasource/MealieAuthenticatorTest.kt | 11 +++- 21 files changed, 130 insertions(+), 124 deletions(-) create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/CreateApiTokenRequestV0.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenRequestV1.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenResponseV1.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt index 576ccab..8ea9b20 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt @@ -5,4 +5,6 @@ interface AuthDataSource { * Tries to acquire authentication token using the provided credentials */ suspend fun authenticate(username: String, password: String): String + + suspend fun createApiToken(name: String): String } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt index 4e2b660..c07f800 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt @@ -10,9 +10,5 @@ interface AuthRepo { suspend fun getAuthHeader(): String? - suspend fun requireAuthHeader(): String - suspend fun logout() - - suspend fun invalidateAuthHeader() } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt index 8486e6a..faddce7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt @@ -9,12 +9,4 @@ interface AuthStorage { suspend fun setAuthHeader(authHeader: String?) suspend fun getAuthHeader(): String? - - suspend fun setEmail(email: String?) - - suspend fun getEmail(): String? - - suspend fun setPassword(password: String?) - - suspend fun getPassword(): String? } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index f42444b..8834372 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -4,7 +4,9 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerVersion import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 +import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 +import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1 import javax.inject.Inject import javax.inject.Singleton @@ -15,14 +17,20 @@ class AuthDataSourceImpl @Inject constructor( private val v1Source: MealieDataSourceV1, ) : AuthDataSource { + private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion() + + private suspend fun getUrl(): String = serverInfoRepo.requireUrl() + override suspend fun authenticate( username: String, password: String, - ): String { - val baseUrl = serverInfoRepo.requireUrl() - return when (serverInfoRepo.getVersion()) { - ServerVersion.V0 -> v0Source.authenticate(baseUrl, username, password) - ServerVersion.V1 -> v1Source.authenticate(baseUrl, username, password) - } + ): String = when (getVersion()) { + ServerVersion.V0 -> v0Source.authenticate(getUrl(), username, password) + ServerVersion.V1 -> v1Source.authenticate(getUrl(), username, password) + } + + override suspend fun createApiToken(name: String): String = when (getVersion()) { + ServerVersion.V0 -> v0Source.createApiToken(getUrl(), CreateApiTokenRequestV0(name)) + ServerVersion.V1 -> v1Source.createApiToken(getUrl(), CreateApiTokenRequestV1(name)).token } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt index 531c0b5..cb241cb 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt @@ -4,7 +4,6 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.datasource.AuthenticationProvider -import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -24,34 +23,20 @@ class AuthRepoImpl @Inject constructor( override suspend fun authenticate(email: String, password: String) { logger.v { "authenticate() called with: email = $email, password = $password" } val token = authDataSource.authenticate(email, password) - val header = AUTH_HEADER_FORMAT.format(token) - authStorage.setAuthHeader(header) - authStorage.setEmail(email) - authStorage.setPassword(password) + authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(token)) + val apiToken = authDataSource.createApiToken(API_TOKEN_NAME) + authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(apiToken)) } override suspend fun getAuthHeader(): String? = authStorage.getAuthHeader() - override suspend fun requireAuthHeader(): String = checkNotNull(getAuthHeader()) { - "Auth header is null when it was required" - } - override suspend fun logout() { logger.v { "logout() called" } - authStorage.setEmail(null) - authStorage.setPassword(null) authStorage.setAuthHeader(null) } - override suspend fun invalidateAuthHeader() { - logger.v { "invalidateAuthHeader() called" } - val email = authStorage.getEmail() ?: return - val password = authStorage.getPassword() ?: return - runCatchingExceptCancel { authenticate(email, password) } - .onFailure { logout() } // Clear all known values to avoid reusing them - } - companion object { private const val AUTH_HEADER_FORMAT = "Bearer %s" + private const val API_TOKEN_NAME = "Mealient" } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt index aa83e67..820e25f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt @@ -32,14 +32,6 @@ class AuthStorageImpl @Inject constructor( override suspend fun getAuthHeader(): String? = getString(AUTH_HEADER_KEY) - override suspend fun setEmail(email: String?) = putString(EMAIL_KEY, email) - - override suspend fun getEmail(): String? = getString(EMAIL_KEY) - - override suspend fun setPassword(password: String?) = putString(PASSWORD_KEY, password) - - override suspend fun getPassword(): String? = getString(PASSWORD_KEY) - private suspend fun putString( key: String, value: String? @@ -57,11 +49,5 @@ class AuthStorageImpl @Inject constructor( companion object { @VisibleForTesting const val AUTH_HEADER_KEY = "authHeader" - - @VisibleForTesting - const val EMAIL_KEY = "email" - - @VisibleForTesting - const val PASSWORD_KEY = "password" } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt index db98128..fa38ed2 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt @@ -6,6 +6,8 @@ import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.datasource.runCatchingExceptCancel +import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_AUTH_HEADER +import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_TOKEN import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD @@ -51,13 +53,15 @@ class AuthRepoImplTest : BaseUnitTest() { @Test fun `when authenticate successfully then saves to storage`() = runTest { coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 - coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN + coEvery { dataSource.authenticate(any(), any()) } returns TEST_TOKEN coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL + coEvery { dataSource.createApiToken(any()) } returns TEST_API_TOKEN subject.authenticate(TEST_USERNAME, TEST_PASSWORD) - coVerifyAll { + coVerify { + dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) storage.setAuthHeader(TEST_AUTH_HEADER) - storage.setEmail(TEST_USERNAME) - storage.setPassword(TEST_PASSWORD) + dataSource.createApiToken(eq("Mealient")) + storage.setAuthHeader(TEST_API_AUTH_HEADER) } confirmVerified(storage) } @@ -71,50 +75,9 @@ class AuthRepoImplTest : BaseUnitTest() { } @Test - fun `when logout then removes email, password and header`() = runTest { + fun `when logout expect header removal`() = runTest { subject.logout() - coVerifyAll { - storage.setEmail(null) - storage.setPassword(null) - storage.setAuthHeader(null) - } + coVerify { storage.setAuthHeader(null) } confirmVerified(storage) } - - @Test - fun `when invalidate then does not authenticate without email`() = runTest { - coEvery { storage.getEmail() } returns null - coEvery { storage.getPassword() } returns TEST_PASSWORD - subject.invalidateAuthHeader() - confirmVerified(dataSource) - } - - @Test - fun `when invalidate then does not authenticate without password`() = runTest { - coEvery { storage.getEmail() } returns TEST_USERNAME - coEvery { storage.getPassword() } returns null - subject.invalidateAuthHeader() - confirmVerified(dataSource) - } - - @Test - fun `when invalidate with credentials then calls authenticate`() = runTest { - coEvery { storage.getEmail() } returns TEST_USERNAME - coEvery { storage.getPassword() } returns TEST_PASSWORD - coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL - coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN - subject.invalidateAuthHeader() - coVerifyAll { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } - } - - @Test - fun `when invalidate with credentials and auth fails then clears email`() = runTest { - coEvery { storage.getEmail() } returns "invalid" - coEvery { storage.getPassword() } returns "" - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL - coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException() - subject.invalidateAuthHeader() - coVerify { storage.setEmail(null) } - } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt index ed8f816..5b0eab1 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt @@ -8,12 +8,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidTest import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_HEADER_KEY -import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.EMAIL_KEY -import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.PASSWORD_KEY import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME import gq.kirmanak.mealient.test.FakeLogger import gq.kirmanak.mealient.test.HiltRobolectricTest import io.mockk.MockKAnnotations @@ -55,16 +51,4 @@ class AuthStorageImplTest : HiltRobolectricTest() { fun `when authHeader is observed then sends null if nothing saved`() = runTest { assertThat(subject.authHeaderFlow.first()).isEqualTo(null) } - - @Test - fun `when setEmail then edits shared preferences`() = runTest { - subject.setEmail(TEST_USERNAME) - assertThat(sharedPreferences.getString(EMAIL_KEY, null)).isEqualTo(TEST_USERNAME) - } - - @Test - fun `when getPassword then reads shared preferences`() = runTest { - sharedPreferences.edit(commit = true) { putString(PASSWORD_KEY, TEST_PASSWORD) } - assertThat(subject.getPassword()).isEqualTo(TEST_PASSWORD) - } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt b/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt index 3e6da0d..a613a84 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt @@ -8,6 +8,8 @@ object AuthImplTestData { const val TEST_BASE_URL = "https://example.com/" const val TEST_TOKEN = "TEST_TOKEN" const val TEST_AUTH_HEADER = "Bearer TEST_TOKEN" + const val TEST_API_TOKEN = "TEST_API_TOKEN" + const val TEST_API_AUTH_HEADER = "Bearer TEST_API_TOKEN" const val TEST_VERSION = "v0.5.6" val TEST_SERVER_VERSION_V0 = ServerVersion.V0 val TEST_SERVER_VERSION_V1 = ServerVersion.V1 diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/AuthenticationProvider.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/AuthenticationProvider.kt index 7df0e88..e8d17a8 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/AuthenticationProvider.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/AuthenticationProvider.kt @@ -4,4 +4,6 @@ interface AuthenticationProvider { suspend fun getAuthHeader(): String? + suspend fun logout() + } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieAuthenticator.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieAuthenticator.kt index bf5df70..5fe4cf5 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieAuthenticator.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieAuthenticator.kt @@ -12,21 +12,30 @@ import javax.inject.Provider import javax.inject.Singleton @Singleton +// TODO has to be interceptor, otherwise only public recipes are visible class MealieAuthenticator @Inject constructor( - private val authenticationProvider: Provider, + private val authenticationProviderProvider: Provider, ) : Authenticator { + private val authenticationProvider: AuthenticationProvider + get() = authenticationProviderProvider.get() + override fun authenticate(route: Route?, response: Response): Request? { val supportsBearer = response.challenges().any { it.scheme == BEARER_SCHEME } val request = response.request - return if (supportsBearer && request.header(HEADER_NAME) == null) { - getAuthHeader()?.let { request.copyWithHeader(HEADER_NAME, it) } - } else { - null // Either Bearer is not supported or we've already tried to authenticate + return when { + request.header(HEADER_NAME) != null -> { + logout() + null + } + supportsBearer -> getAuthHeader()?.let { request.copyWithHeader(HEADER_NAME, it) } + else -> null } } - private fun getAuthHeader() = runBlocking { authenticationProvider.get().getAuthHeader() } + private fun getAuthHeader() = runBlocking { authenticationProvider.getAuthHeader() } + + private fun logout() = runBlocking { authenticationProvider.logout() } companion object { @VisibleForTesting diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt index 6a6abd2..92b70f6 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt @@ -1,6 +1,7 @@ package gq.kirmanak.mealient.datasource.v0 import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0 import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0 @@ -41,4 +42,9 @@ interface MealieDataSourceV0 { baseUrl: String, request: ParseRecipeURLRequestV0, ): String + + suspend fun createApiToken( + baseUrl: String, + request: CreateApiTokenRequestV0, + ): String } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt index f6939d7..473a3f4 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt @@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.datasource.NetworkRequestWrapper import gq.kirmanak.mealient.datasource.decode import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0 import gq.kirmanak.mealient.datasource.v0.models.ErrorDetailV0 import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 @@ -86,7 +87,15 @@ class MealieDataSourceV0Impl @Inject constructor( ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) }, logMethod = { "parseRecipeFromURL" }, - logParameters = { "baseUrl = $baseUrl, request = $request" } + logParameters = { "baseUrl = $baseUrl, request = $request" }, + ) + override suspend fun createApiToken( + baseUrl: String, + request: CreateApiTokenRequestV0, + ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) }, + logMethod = { "createApiToken" }, + logParameters = { "baseUrl = $baseUrl, request = $request" } ) } diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt index 579dada..79b8ef7 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt @@ -41,4 +41,10 @@ interface MealieServiceV0 { @Url url: String, @Body request: ParseRecipeURLRequestV0, ): String + + @POST + suspend fun createApiToken( + @Url url: String, + @Body request: CreateApiTokenRequestV0, + ): String } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/CreateApiTokenRequestV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/CreateApiTokenRequestV0.kt new file mode 100644 index 0000000..67315ec --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/CreateApiTokenRequestV0.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v0.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreateApiTokenRequestV0( + @SerialName("name") val name: String, +) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt index 9b6f665..5bc2b02 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.datasource.v1 +import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1 import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 @@ -48,4 +50,9 @@ interface MealieDataSourceV1 { baseUrl: String, request: ParseRecipeURLRequestV1, ): String + + suspend fun createApiToken( + baseUrl: String, + request: CreateApiTokenRequestV1, + ): CreateApiTokenResponseV1 } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt index 7bb2822..aa009d8 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt @@ -3,6 +3,8 @@ package gq.kirmanak.mealient.datasource.v1 import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.datasource.NetworkRequestWrapper import gq.kirmanak.mealient.datasource.decode +import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1 import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 @@ -98,8 +100,15 @@ class MealieDataSourceV1Impl @Inject constructor( block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) }, logMethod = { "parseRecipeFromURL" }, logParameters = { "baseUrl = $baseUrl, request = $request" } - ) + override suspend fun createApiToken( + baseUrl: String, + request: CreateApiTokenRequestV1 + ): CreateApiTokenResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) }, + logMethod = { "createApiToken" }, + logParameters = { "baseUrl = $baseUrl, request = $request" } + ) } diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt index 004ccf8..85271cf 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt @@ -47,4 +47,10 @@ interface MealieServiceV1 { @Url url: String, @Body request: ParseRecipeURLRequestV1, ): String + + @POST + suspend fun createApiToken( + @Url url: String, + @Body request: CreateApiTokenRequestV1, + ): CreateApiTokenResponseV1 } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenRequestV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenRequestV1.kt new file mode 100644 index 0000000..a50325e --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenRequestV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreateApiTokenRequestV1( + @SerialName("name") val name: String, +) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenResponseV1.kt new file mode 100644 index 0000000..6c10baa --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateApiTokenResponseV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreateApiTokenResponseV1( + @SerialName("token") val token: String, +) \ No newline at end of file diff --git a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieAuthenticatorTest.kt b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieAuthenticatorTest.kt index 41d8714..708d4c7 100644 --- a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieAuthenticatorTest.kt +++ b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieAuthenticatorTest.kt @@ -17,7 +17,7 @@ class MealieAuthenticatorTest : BaseUnitTest() { private lateinit var subject: MealieAuthenticator - @MockK + @MockK(relaxUnitFun = true) lateinit var authenticationProvider: AuthenticationProvider @Before @@ -48,11 +48,18 @@ class MealieAuthenticatorTest : BaseUnitTest() { } @Test - fun `when no auth header was set expect authenticate to return null`() { + fun `when an auth header was set expect authenticate to return null`() { val response = buildResponse(authHeader = "token") assertThat(subject.authenticate(null, response)).isNull() } + @Test + fun `when an auth header was set expect authenticate to logout`() { + val response = buildResponse(authHeader = "token") + subject.authenticate(null, response) + coVerify { authenticationProvider.logout() } + } + @Test fun `when auth header exists expect authenticate to return request`() { coEvery { authenticationProvider.getAuthHeader() } returns "token"