Create API token when signing in
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
@@ -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?
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ interface AuthenticationProvider {
|
|||||||
|
|
||||||
suspend fun getAuthHeader(): String?
|
suspend fun getAuthHeader(): String?
|
||||||
|
|
||||||
|
suspend fun logout()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user