Create API token when signing in

This commit is contained in:
Kirill Kamakin
2022-12-11 11:57:13 +01:00
parent a560db8bb6
commit c99f9fea88
21 changed files with 130 additions and 124 deletions

View File

@@ -5,4 +5,6 @@ interface AuthDataSource {
* Tries to acquire authentication token using the provided credentials * Tries to acquire authentication token using the provided credentials
*/ */
suspend fun authenticate(username: String, password: String): String suspend fun authenticate(username: String, password: String): String
suspend fun createApiToken(name: String): String
} }

View File

@@ -10,9 +10,5 @@ interface AuthRepo {
suspend fun getAuthHeader(): String? suspend fun getAuthHeader(): String?
suspend fun requireAuthHeader(): String
suspend fun logout() suspend fun logout()
suspend fun invalidateAuthHeader()
} }

View File

@@ -9,12 +9,4 @@ interface AuthStorage {
suspend fun setAuthHeader(authHeader: String?) suspend fun setAuthHeader(authHeader: String?)
suspend fun getAuthHeader(): String? suspend fun getAuthHeader(): String?
suspend fun setEmail(email: String?)
suspend fun getEmail(): String?
suspend fun setPassword(password: String?)
suspend fun getPassword(): String?
} }

View File

@@ -4,7 +4,9 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.baseurl.ServerVersion import gq.kirmanak.mealient.data.baseurl.ServerVersion
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 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.MealieDataSourceV1
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -15,14 +17,20 @@ class AuthDataSourceImpl @Inject constructor(
private val v1Source: MealieDataSourceV1, private val v1Source: MealieDataSourceV1,
) : AuthDataSource { ) : AuthDataSource {
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
private suspend fun getUrl(): String = serverInfoRepo.requireUrl()
override suspend fun authenticate( override suspend fun authenticate(
username: String, username: String,
password: String, password: String,
): String { ): String = when (getVersion()) {
val baseUrl = serverInfoRepo.requireUrl() ServerVersion.V0 -> v0Source.authenticate(getUrl(), username, password)
return when (serverInfoRepo.getVersion()) { ServerVersion.V1 -> v1Source.authenticate(getUrl(), username, password)
ServerVersion.V0 -> v0Source.authenticate(baseUrl, username, password) }
ServerVersion.V1 -> v1Source.authenticate(baseUrl, 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
} }
} }

View File

@@ -4,7 +4,6 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.datasource.AuthenticationProvider import gq.kirmanak.mealient.datasource.AuthenticationProvider
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -24,34 +23,20 @@ class AuthRepoImpl @Inject constructor(
override suspend fun authenticate(email: String, password: String) { override suspend fun authenticate(email: String, password: String) {
logger.v { "authenticate() called with: email = $email, password = $password" } logger.v { "authenticate() called with: email = $email, password = $password" }
val token = authDataSource.authenticate(email, password) val token = authDataSource.authenticate(email, password)
val header = AUTH_HEADER_FORMAT.format(token) authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(token))
authStorage.setAuthHeader(header) val apiToken = authDataSource.createApiToken(API_TOKEN_NAME)
authStorage.setEmail(email) authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(apiToken))
authStorage.setPassword(password)
} }
override suspend fun getAuthHeader(): String? = authStorage.getAuthHeader() 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() { override suspend fun logout() {
logger.v { "logout() called" } logger.v { "logout() called" }
authStorage.setEmail(null)
authStorage.setPassword(null)
authStorage.setAuthHeader(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 { companion object {
private const val AUTH_HEADER_FORMAT = "Bearer %s" private const val AUTH_HEADER_FORMAT = "Bearer %s"
private const val API_TOKEN_NAME = "Mealient"
} }
} }

View File

@@ -32,14 +32,6 @@ class AuthStorageImpl @Inject constructor(
override suspend fun getAuthHeader(): String? = getString(AUTH_HEADER_KEY) 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( private suspend fun putString(
key: String, key: String,
value: String? value: String?
@@ -57,11 +49,5 @@ class AuthStorageImpl @Inject constructor(
companion object { companion object {
@VisibleForTesting @VisibleForTesting
const val AUTH_HEADER_KEY = "authHeader" const val AUTH_HEADER_KEY = "authHeader"
@VisibleForTesting
const val EMAIL_KEY = "email"
@VisibleForTesting
const val PASSWORD_KEY = "password"
} }
} }

View File

@@ -6,6 +6,8 @@ import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel 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_AUTH_HEADER
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
@@ -51,13 +53,15 @@ class AuthRepoImplTest : BaseUnitTest() {
@Test @Test
fun `when authenticate successfully then saves to storage`() = runTest { fun `when authenticate successfully then saves to storage`() = runTest {
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 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 { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { dataSource.createApiToken(any()) } returns TEST_API_TOKEN
subject.authenticate(TEST_USERNAME, TEST_PASSWORD) subject.authenticate(TEST_USERNAME, TEST_PASSWORD)
coVerifyAll { coVerify {
dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD))
storage.setAuthHeader(TEST_AUTH_HEADER) storage.setAuthHeader(TEST_AUTH_HEADER)
storage.setEmail(TEST_USERNAME) dataSource.createApiToken(eq("Mealient"))
storage.setPassword(TEST_PASSWORD) storage.setAuthHeader(TEST_API_AUTH_HEADER)
} }
confirmVerified(storage) confirmVerified(storage)
} }
@@ -71,50 +75,9 @@ class AuthRepoImplTest : BaseUnitTest() {
} }
@Test @Test
fun `when logout then removes email, password and header`() = runTest { fun `when logout expect header removal`() = runTest {
subject.logout() subject.logout()
coVerifyAll { coVerify { storage.setAuthHeader(null) }
storage.setEmail(null)
storage.setPassword(null)
storage.setAuthHeader(null)
}
confirmVerified(storage) 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) }
}
} }

View File

@@ -8,12 +8,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.data.auth.AuthStorage 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.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.logging.Logger
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER 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.FakeLogger
import gq.kirmanak.mealient.test.HiltRobolectricTest import gq.kirmanak.mealient.test.HiltRobolectricTest
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
@@ -55,16 +51,4 @@ class AuthStorageImplTest : HiltRobolectricTest() {
fun `when authHeader is observed then sends null if nothing saved`() = runTest { fun `when authHeader is observed then sends null if nothing saved`() = runTest {
assertThat(subject.authHeaderFlow.first()).isEqualTo(null) 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)
}
} }

View File

@@ -8,6 +8,8 @@ object AuthImplTestData {
const val TEST_BASE_URL = "https://example.com/" const val TEST_BASE_URL = "https://example.com/"
const val TEST_TOKEN = "TEST_TOKEN" const val TEST_TOKEN = "TEST_TOKEN"
const val TEST_AUTH_HEADER = "Bearer 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" const val TEST_VERSION = "v0.5.6"
val TEST_SERVER_VERSION_V0 = ServerVersion.V0 val TEST_SERVER_VERSION_V0 = ServerVersion.V0
val TEST_SERVER_VERSION_V1 = ServerVersion.V1 val TEST_SERVER_VERSION_V1 = ServerVersion.V1

View File

@@ -4,4 +4,6 @@ interface AuthenticationProvider {
suspend fun getAuthHeader(): String? suspend fun getAuthHeader(): String?
suspend fun logout()
} }

View File

@@ -12,21 +12,30 @@ import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
// TODO has to be interceptor, otherwise only public recipes are visible
class MealieAuthenticator @Inject constructor( class MealieAuthenticator @Inject constructor(
private val authenticationProvider: Provider<AuthenticationProvider>, private val authenticationProviderProvider: Provider<AuthenticationProvider>,
) : Authenticator { ) : Authenticator {
private val authenticationProvider: AuthenticationProvider
get() = authenticationProviderProvider.get()
override fun authenticate(route: Route?, response: Response): Request? { override fun authenticate(route: Route?, response: Response): Request? {
val supportsBearer = response.challenges().any { it.scheme == BEARER_SCHEME } val supportsBearer = response.challenges().any { it.scheme == BEARER_SCHEME }
val request = response.request val request = response.request
return if (supportsBearer && request.header(HEADER_NAME) == null) { return when {
getAuthHeader()?.let { request.copyWithHeader(HEADER_NAME, it) } request.header(HEADER_NAME) != null -> {
} else { logout()
null // Either Bearer is not supported or we've already tried to authenticate 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 { companion object {
@VisibleForTesting @VisibleForTesting

View File

@@ -1,6 +1,7 @@
package gq.kirmanak.mealient.datasource.v0 package gq.kirmanak.mealient.datasource.v0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 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.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0 import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
@@ -41,4 +42,9 @@ interface MealieDataSourceV0 {
baseUrl: String, baseUrl: String,
request: ParseRecipeURLRequestV0, request: ParseRecipeURLRequestV0,
): String ): String
suspend fun createApiToken(
baseUrl: String,
request: CreateApiTokenRequestV0,
): String
} }

View File

@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
import gq.kirmanak.mealient.datasource.decode import gq.kirmanak.mealient.datasource.decode
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 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.ErrorDetailV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
@@ -86,7 +87,15 @@ class MealieDataSourceV0Impl @Inject constructor(
): String = networkRequestWrapper.makeCallAndHandleUnauthorized( ): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) }, block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) },
logMethod = { "parseRecipeFromURL" }, 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" }
) )
} }

View File

@@ -41,4 +41,10 @@ interface MealieServiceV0 {
@Url url: String, @Url url: String,
@Body request: ParseRecipeURLRequestV0, @Body request: ParseRecipeURLRequestV0,
): String ): String
@POST
suspend fun createApiToken(
@Url url: String,
@Body request: CreateApiTokenRequestV0,
): String
} }

View File

@@ -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,
)

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.datasource.v1 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.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
@@ -48,4 +50,9 @@ interface MealieDataSourceV1 {
baseUrl: String, baseUrl: String,
request: ParseRecipeURLRequestV1, request: ParseRecipeURLRequestV1,
): String ): String
suspend fun createApiToken(
baseUrl: String,
request: CreateApiTokenRequestV1,
): CreateApiTokenResponseV1
} }

View File

@@ -3,6 +3,8 @@ package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
import gq.kirmanak.mealient.datasource.decode 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.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1 import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 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) }, block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) },
logMethod = { "parseRecipeFromURL" }, logMethod = { "parseRecipeFromURL" },
logParameters = { "baseUrl = $baseUrl, request = $request" } 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" }
)
} }

View File

@@ -47,4 +47,10 @@ interface MealieServiceV1 {
@Url url: String, @Url url: String,
@Body request: ParseRecipeURLRequestV1, @Body request: ParseRecipeURLRequestV1,
): String ): String
@POST
suspend fun createApiToken(
@Url url: String,
@Body request: CreateApiTokenRequestV1,
): CreateApiTokenResponseV1
} }

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -17,7 +17,7 @@ class MealieAuthenticatorTest : BaseUnitTest() {
private lateinit var subject: MealieAuthenticator private lateinit var subject: MealieAuthenticator
@MockK @MockK(relaxUnitFun = true)
lateinit var authenticationProvider: AuthenticationProvider lateinit var authenticationProvider: AuthenticationProvider
@Before @Before
@@ -48,11 +48,18 @@ class MealieAuthenticatorTest : BaseUnitTest() {
} }
@Test @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") val response = buildResponse(authHeader = "token")
assertThat(subject.authenticate(null, response)).isNull() 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 @Test
fun `when auth header exists expect authenticate to return request`() { fun `when auth header exists expect authenticate to return request`() {
coEvery { authenticationProvider.getAuthHeader() } returns "token" coEvery { authenticationProvider.getAuthHeader() } returns "token"