From c0de6f66067a66d60124ac5190c1366ed9b930bd Mon Sep 17 00:00:00 2001 From: user5 Date: Tue, 27 May 2025 12:07:58 +0300 Subject: [PATCH] projects request and test --- .../example/api/core/data/core/ApiFactory.kt | 13 +++-- .../example/api/core/data/core/ApiService.kt | 4 +- .../api/core/data/core/AuthInterceptor.kt | 22 +++++++++ .../api/core/data/core/BaseApiRepository.kt | 47 +++++++++++++------ .../api/core/data/core/DataStoreRepository.kt | 6 +-- .../api/core/data/core/RemoteMapper.kt | 34 +++++++++++++- .../{SalesAndActionsDto.kt => ProjectsDto.kt} | 4 +- .../example/api/core/domain/ApiRepository.kt | 2 + .../{SalesAndActions.kt => Projects.kt} | 2 +- .../java/com/example/api/core/ServiceTest.kt | 43 +++++++++++++++++ .../java/com/example/api/MainViewModel.kt | 24 ++++++++-- .../main/java/com/example/api/TestScreen.kt | 15 +++++- 12 files changed, 182 insertions(+), 34 deletions(-) create mode 100644 api-core/src/main/java/com/example/api/core/data/core/AuthInterceptor.kt rename api-core/src/main/java/com/example/api/core/data/dto/{SalesAndActionsDto.kt => ProjectsDto.kt} (75%) rename api-core/src/main/java/com/example/api/core/domain/{SalesAndActions.kt => Projects.kt} (81%) diff --git a/api-core/src/main/java/com/example/api/core/data/core/ApiFactory.kt b/api-core/src/main/java/com/example/api/core/data/core/ApiFactory.kt index 6424735..be64095 100644 --- a/api-core/src/main/java/com/example/api/core/data/core/ApiFactory.kt +++ b/api-core/src/main/java/com/example/api/core/data/core/ApiFactory.kt @@ -13,15 +13,20 @@ object ApiFactory { fun provideRepository(dataStoreRepository: DataStoreRepository): ApiRepository { val json = Json { ignoreUnknownKeys = true } + val client = OkHttpClient.Builder() + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + .addInterceptor(AuthInterceptor(dataStoreRepository)) + .build() + val retrofit = Retrofit.Builder() .baseUrl("https://api.matule.ru/api/") .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) - .client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - }).build()) + .client(client) .build() - return BaseApiRepository(retrofit.create(), dataStoreRepository) + return BaseApiRepository(retrofit.create(), dataStoreRepository, Handle.Base()) } } \ No newline at end of file diff --git a/api-core/src/main/java/com/example/api/core/data/core/ApiService.kt b/api-core/src/main/java/com/example/api/core/data/core/ApiService.kt index fa2a6e8..212d4b3 100644 --- a/api-core/src/main/java/com/example/api/core/data/core/ApiService.kt +++ b/api-core/src/main/java/com/example/api/core/data/core/ApiService.kt @@ -2,7 +2,7 @@ package com.example.api.core.data.core import com.example.api.core.data.dto.AuthUserResponseDto import com.example.api.core.data.dto.RegisterUserDto -import com.example.api.core.data.dto.SalesAndActionsDto +import com.example.api.core.data.dto.ProjectsDto import com.example.api.core.data.dto.SignInUserDto import com.example.api.core.data.dto.UserResponseDto import retrofit2.http.Body @@ -23,7 +23,7 @@ internal interface ApiService { suspend fun auth(@Body signInUserDto: SignInUserDto): AuthUserResponseDto @GET("collections/project/records") - suspend fun salesAndProjects(): SalesAndActionsDto + suspend fun projects(): ProjectsDto // @GET("collections/products/records") // suspend fun products(): diff --git a/api-core/src/main/java/com/example/api/core/data/core/AuthInterceptor.kt b/api-core/src/main/java/com/example/api/core/data/core/AuthInterceptor.kt new file mode 100644 index 0000000..576e6aa --- /dev/null +++ b/api-core/src/main/java/com/example/api/core/data/core/AuthInterceptor.kt @@ -0,0 +1,22 @@ +package com.example.api.core.data.core + +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 27.05.2025 + * */ + +class AuthInterceptor(private val dataStoreRepository: DataStoreRepository) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val token = runBlocking { dataStoreRepository.token().first() } + val request = chain.request().newBuilder().apply { + token?.let { addHeader("Authorization", "Bearer $token") } + } + + return chain.proceed(request.build()) + } +} \ No newline at end of file diff --git a/api-core/src/main/java/com/example/api/core/data/core/BaseApiRepository.kt b/api-core/src/main/java/com/example/api/core/data/core/BaseApiRepository.kt index 63f22d9..4670fc8 100644 --- a/api-core/src/main/java/com/example/api/core/data/core/BaseApiRepository.kt +++ b/api-core/src/main/java/com/example/api/core/data/core/BaseApiRepository.kt @@ -4,6 +4,7 @@ import com.example.api.core.domain.ApiRepository import com.example.api.core.domain.AuthUserResponse import com.example.api.core.domain.FetchResult import com.example.api.core.domain.RegisterUser +import com.example.api.core.domain.Projects import retrofit2.HttpException import java.net.UnknownHostException @@ -12,24 +13,42 @@ import java.net.UnknownHostException * Дата создания: 27.05.2025 * */ +internal interface Handle { + suspend fun handle(action: suspend () -> T): FetchResult + + class Base : Handle { + override suspend fun handle(action: suspend () -> T): FetchResult { + return try { + val result = action.invoke() + + FetchResult.Success(result) + } catch (e: UnknownHostException) { + FetchResult.Error(null, "no internet connection") + } catch (e: HttpException) { + FetchResult.Error(null, e.message()) + } catch (e: Exception) { + FetchResult.Error(null, e.message!!) + } + } + + } +} + internal class BaseApiRepository( private val service: ApiService, - private val dataStoreRepository: DataStoreRepository + private val dataStoreRepository: DataStoreRepository, + private val handle: Handle, ) : RemoteMapper(), ApiRepository { - override suspend fun auth(registerUser: RegisterUser): FetchResult { - return try { - service.register(registerUser.toRegisterUserDto()) - val response = service.auth(registerUser.toSignInUserDto()) - dataStoreRepository.saveToken(response.token) + override suspend fun auth(registerUser: RegisterUser): FetchResult = handle.handle { + service.register(registerUser.toRegisterUserDto()) + val response = service.auth(registerUser.toSignInUserDto()) + dataStoreRepository.saveToken(response.token) + response.toUserResponse() + } - FetchResult.Success(response.toUserResponse()) - } catch (e: UnknownHostException) { - FetchResult.Error(null, "no internet connection") - } catch (e: HttpException) { - FetchResult.Error(null, e.message()) - } catch (e: Exception) { - FetchResult.Error(null, e.message!!) - } + + override suspend fun projects(): FetchResult = handle.handle { + service.projects().toDomain() } } \ No newline at end of file diff --git a/api-core/src/main/java/com/example/api/core/data/core/DataStoreRepository.kt b/api-core/src/main/java/com/example/api/core/data/core/DataStoreRepository.kt index 9dbfdc4..5d920ed 100644 --- a/api-core/src/main/java/com/example/api/core/data/core/DataStoreRepository.kt +++ b/api-core/src/main/java/com/example/api/core/data/core/DataStoreRepository.kt @@ -16,7 +16,7 @@ interface DataStoreRepository { suspend fun saveToken(token: String) - fun token(): Flow + fun token(): Flow class Base(private val preferences: DataStore) : DataStoreRepository { override suspend fun saveToken(token: String) { @@ -25,9 +25,9 @@ interface DataStoreRepository { } } - override fun token(): Flow { + override fun token(): Flow { return preferences.data.map { preferences -> - preferences[TOKEN_KEY] ?: "" + preferences[TOKEN_KEY] } } } diff --git a/api-core/src/main/java/com/example/api/core/data/core/RemoteMapper.kt b/api-core/src/main/java/com/example/api/core/data/core/RemoteMapper.kt index 23a27a9..00c4407 100644 --- a/api-core/src/main/java/com/example/api/core/data/core/RemoteMapper.kt +++ b/api-core/src/main/java/com/example/api/core/data/core/RemoteMapper.kt @@ -2,13 +2,15 @@ package com.example.api.core.data.core import com.example.api.core.data.dto.AuthUserResponseDto import com.example.api.core.data.dto.RegisterUserDto +import com.example.api.core.data.dto.SaleItemDto +import com.example.api.core.data.dto.ProjectsDto import com.example.api.core.data.dto.SignInUserDto import com.example.api.core.data.dto.SignInUserResponseDto -import com.example.api.core.data.dto.UserResponseDto import com.example.api.core.domain.AuthUserResponse import com.example.api.core.domain.RegisterUser +import com.example.api.core.domain.SaleItem +import com.example.api.core.domain.Projects import com.example.api.core.domain.SignInUserResponse -import com.example.api.core.domain.UserResponse /** * Автор: Манякин Дмитрий (user5) @@ -55,4 +57,32 @@ internal abstract class RemoteMapper { verified = verified ) } + + protected fun ProjectsDto.toDomain(): Projects { + return Projects( + page = page, + perPage = perPage, + totalPage = totalPages, + totalItems = totalItems, + items = items.map { it.toSaleItem() } + ) + } + + protected fun SaleItemDto.toSaleItem(): SaleItem { + return SaleItem( + id = id, + collectionId = collectionId, + collectionName = collectionName, + created = created, + updated = updated, + title = title, + dateStart = dateStart, + dateEnd = dateEnd, + gender = gender, + descriptionSource = descriptionSource, + category = category, + image = image, + userId = userId + ) + } } \ No newline at end of file diff --git a/api-core/src/main/java/com/example/api/core/data/dto/SalesAndActionsDto.kt b/api-core/src/main/java/com/example/api/core/data/dto/ProjectsDto.kt similarity index 75% rename from api-core/src/main/java/com/example/api/core/data/dto/SalesAndActionsDto.kt rename to api-core/src/main/java/com/example/api/core/data/dto/ProjectsDto.kt index 66741cc..9be1381 100644 --- a/api-core/src/main/java/com/example/api/core/data/dto/SalesAndActionsDto.kt +++ b/api-core/src/main/java/com/example/api/core/data/dto/ProjectsDto.kt @@ -3,10 +3,10 @@ package com.example.api.core.data.dto import kotlinx.serialization.Serializable @Serializable -internal data class SalesAndActionsDto( +internal data class ProjectsDto( val page: Int, val perPage: Int, - val totalPage: Int, + val totalPages: Int, val totalItems: Int, val items: List ) diff --git a/api-core/src/main/java/com/example/api/core/domain/ApiRepository.kt b/api-core/src/main/java/com/example/api/core/domain/ApiRepository.kt index 45c10a7..dbd34c7 100644 --- a/api-core/src/main/java/com/example/api/core/domain/ApiRepository.kt +++ b/api-core/src/main/java/com/example/api/core/domain/ApiRepository.kt @@ -8,4 +8,6 @@ package com.example.api.core.domain interface ApiRepository { suspend fun auth(registerUser: RegisterUser): FetchResult + + suspend fun projects(): FetchResult } \ No newline at end of file diff --git a/api-core/src/main/java/com/example/api/core/domain/SalesAndActions.kt b/api-core/src/main/java/com/example/api/core/domain/Projects.kt similarity index 81% rename from api-core/src/main/java/com/example/api/core/domain/SalesAndActions.kt rename to api-core/src/main/java/com/example/api/core/domain/Projects.kt index ac1e13f..3e47dd7 100644 --- a/api-core/src/main/java/com/example/api/core/domain/SalesAndActions.kt +++ b/api-core/src/main/java/com/example/api/core/domain/Projects.kt @@ -1,6 +1,6 @@ package com.example.api.core.domain -internal data class SalesAndActions( +data class Projects( val page: Int, val perPage: Int, val totalPage: Int, diff --git a/api-core/src/test/java/com/example/api/core/ServiceTest.kt b/api-core/src/test/java/com/example/api/core/ServiceTest.kt index 987584e..b4c9463 100644 --- a/api-core/src/test/java/com/example/api/core/ServiceTest.kt +++ b/api-core/src/test/java/com/example/api/core/ServiceTest.kt @@ -126,6 +126,49 @@ class ServiceTest { assertEquals("/collections/users/auth-with-password", request.path) } + @Test + fun test_project_list() = runBlocking { + val response = """ + { + "items": [ + { + "category": "е", + "collectionId": "pbc_3202395908", + "collectionName": "project", + "created": "2025-05-26 14:47:25.523Z", + "dateEnd": "2025-05-24 12:00:00.000Z", + "dateStart": "2025-05-15 12:00:00.000Z", + "description_source": "е", + "gender": "е", + "id": "7m35pcmbjr86kh4", + "image": "", + "title": "е", + "typeProject": "е", + "updated": "2025-05-26 14:47:25.523Z", + "user_id": "" + } + ], + "page": 1, + "perPage": 30, + "totalItems": 1, + "totalPages": 1 + } + """.trimIndent() + + mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(response)) + + val responseResult = service.projects() + + val request = mockWebServer.takeRequest() + + assertEquals(1, responseResult.page) + assertEquals(1, responseResult.totalItems) + assertEquals(1, responseResult.items.size) + + assertEquals("GET", request.method) + assertEquals("/collections/project/records", request.path) + } + @After fun teardown() { mockWebServer.shutdown() diff --git a/app/src/main/java/com/example/api/MainViewModel.kt b/app/src/main/java/com/example/api/MainViewModel.kt index 73d7fc0..f221c2b 100644 --- a/app/src/main/java/com/example/api/MainViewModel.kt +++ b/app/src/main/java/com/example/api/MainViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.example.api.core.domain.ApiRepository import com.example.api.core.domain.AuthUserResponse import com.example.api.core.domain.RegisterUser +import com.example.api.core.domain.Projects import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -24,10 +25,15 @@ class MainViewModel @Inject constructor( private val apiRepository: ApiRepository ) : ViewModel() { - private val _loadResultUiState = + private val _authUiState = MutableStateFlow>(FetchResultUiState.Initial()) - val loadResultUiState: StateFlow> - get() = _loadResultUiState.asStateFlow() + val authUiState: StateFlow> + get() = _authUiState.asStateFlow() + + private val _ProjectsUiState = + MutableStateFlow>(FetchResultUiState.Initial()) + val projectsUiState: StateFlow> + get() = _ProjectsUiState.asStateFlow() fun auth(email: String, password: String) { viewModelScope.launch(Dispatchers.IO) { @@ -40,7 +46,17 @@ class MainViewModel @Inject constructor( ) withContext(Dispatchers.Main) { - _loadResultUiState.value = result.map(FetchResultMapper()) + _authUiState.value = result.map(FetchResultMapper()) + } + } + } + + fun salesAndProjects() { + viewModelScope.launch(Dispatchers.IO) { + val result = apiRepository.projects() + + withContext(Dispatchers.Main) { + _ProjectsUiState.value = result.map(FetchResultMapper()) } } } diff --git a/app/src/main/java/com/example/api/TestScreen.kt b/app/src/main/java/com/example/api/TestScreen.kt index 51834fa..dd2f458 100644 --- a/app/src/main/java/com/example/api/TestScreen.kt +++ b/app/src/main/java/com/example/api/TestScreen.kt @@ -23,11 +23,12 @@ fun TestScreen( modifier: Modifier = Modifier, viewModel: MainViewModel = hiltViewModel() ) { - val state by viewModel.loadResultUiState.collectAsState() + val state by viewModel.authUiState.collectAsState() + val state2 by viewModel.projectsUiState.collectAsState() Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column { - Button(onClick = { viewModel.auth("someemael@te3st1.com", "12345678") }) { + Button(onClick = { viewModel.auth("someemael@te3s11t1.com", "12345678") }) { Text("register") } @@ -36,6 +37,16 @@ fun TestScreen( onError = { _, message -> Text(text = message) }, onLoading = { CircularProgressIndicator() } ) + + Button(onClick = { viewModel.salesAndProjects() }) { + Text("sales and projects") + } + + state2.Show( + onSuccess = { Text(text = it.items.size.toString()) }, + onError = { _, message -> Text(text = message) }, + onLoading = { CircularProgressIndicator() } + ) } } } \ No newline at end of file