projects request and test

This commit is contained in:
user5 2025-05-27 12:07:58 +03:00
parent 7e8fbd3063
commit c0de6f6606
12 changed files with 182 additions and 34 deletions

View File

@ -13,15 +13,20 @@ object ApiFactory {
fun provideRepository(dataStoreRepository: DataStoreRepository): ApiRepository { fun provideRepository(dataStoreRepository: DataStoreRepository): ApiRepository {
val json = Json { ignoreUnknownKeys = true } val json = Json { ignoreUnknownKeys = true }
val client = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.addInterceptor(AuthInterceptor(dataStoreRepository))
.build()
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl("https://api.matule.ru/api/") .baseUrl("https://api.matule.ru/api/")
.addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply { .client(client)
level = HttpLoggingInterceptor.Level.BODY
}).build())
.build() .build()
return BaseApiRepository(retrofit.create<ApiService>(), dataStoreRepository) return BaseApiRepository(retrofit.create<ApiService>(), dataStoreRepository, Handle.Base())
} }
} }

View File

@ -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.AuthUserResponseDto
import com.example.api.core.data.dto.RegisterUserDto 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.SignInUserDto
import com.example.api.core.data.dto.UserResponseDto import com.example.api.core.data.dto.UserResponseDto
import retrofit2.http.Body import retrofit2.http.Body
@ -23,7 +23,7 @@ internal interface ApiService {
suspend fun auth(@Body signInUserDto: SignInUserDto): AuthUserResponseDto suspend fun auth(@Body signInUserDto: SignInUserDto): AuthUserResponseDto
@GET("collections/project/records") @GET("collections/project/records")
suspend fun salesAndProjects(): SalesAndActionsDto suspend fun projects(): ProjectsDto
// @GET("collections/products/records") // @GET("collections/products/records")
// suspend fun products(): // suspend fun products():

View File

@ -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())
}
}

View File

@ -4,6 +4,7 @@ import com.example.api.core.domain.ApiRepository
import com.example.api.core.domain.AuthUserResponse import com.example.api.core.domain.AuthUserResponse
import com.example.api.core.domain.FetchResult import com.example.api.core.domain.FetchResult
import com.example.api.core.domain.RegisterUser import com.example.api.core.domain.RegisterUser
import com.example.api.core.domain.Projects
import retrofit2.HttpException import retrofit2.HttpException
import java.net.UnknownHostException import java.net.UnknownHostException
@ -12,18 +13,15 @@ import java.net.UnknownHostException
* Дата создания: 27.05.2025 * Дата создания: 27.05.2025
* */ * */
internal class BaseApiRepository( internal interface Handle {
private val service: ApiService, suspend fun <T> handle(action: suspend () -> T): FetchResult<T>
private val dataStoreRepository: DataStoreRepository
) : RemoteMapper(), ApiRepository {
override suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse> { class Base : Handle {
override suspend fun <T> handle(action: suspend () -> T): FetchResult<T> {
return try { return try {
service.register(registerUser.toRegisterUserDto()) val result = action.invoke()
val response = service.auth(registerUser.toSignInUserDto())
dataStoreRepository.saveToken(response.token)
FetchResult.Success(response.toUserResponse()) FetchResult.Success(result)
} catch (e: UnknownHostException) { } catch (e: UnknownHostException) {
FetchResult.Error(null, "no internet connection") FetchResult.Error(null, "no internet connection")
} catch (e: HttpException) { } catch (e: HttpException) {
@ -32,4 +30,25 @@ internal class BaseApiRepository(
FetchResult.Error(null, e.message!!) FetchResult.Error(null, e.message!!)
} }
} }
}
}
internal class BaseApiRepository(
private val service: ApiService,
private val dataStoreRepository: DataStoreRepository,
private val handle: Handle,
) : RemoteMapper(), ApiRepository {
override suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse> = handle.handle {
service.register(registerUser.toRegisterUserDto())
val response = service.auth(registerUser.toSignInUserDto())
dataStoreRepository.saveToken(response.token)
response.toUserResponse()
}
override suspend fun projects(): FetchResult<Projects> = handle.handle {
service.projects().toDomain()
}
} }

View File

@ -16,7 +16,7 @@ interface DataStoreRepository {
suspend fun saveToken(token: String) suspend fun saveToken(token: String)
fun token(): Flow<String> fun token(): Flow<String?>
class Base(private val preferences: DataStore<Preferences>) : DataStoreRepository { class Base(private val preferences: DataStore<Preferences>) : DataStoreRepository {
override suspend fun saveToken(token: String) { override suspend fun saveToken(token: String) {
@ -25,9 +25,9 @@ interface DataStoreRepository {
} }
} }
override fun token(): Flow<String> { override fun token(): Flow<String?> {
return preferences.data.map { preferences -> return preferences.data.map { preferences ->
preferences[TOKEN_KEY] ?: "" preferences[TOKEN_KEY]
} }
} }
} }

View File

@ -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.AuthUserResponseDto
import com.example.api.core.data.dto.RegisterUserDto 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.SignInUserDto
import com.example.api.core.data.dto.SignInUserResponseDto 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.AuthUserResponse
import com.example.api.core.domain.RegisterUser 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.SignInUserResponse
import com.example.api.core.domain.UserResponse
/** /**
* Автор: Манякин Дмитрий (user5) * Автор: Манякин Дмитрий (user5)
@ -55,4 +57,32 @@ internal abstract class RemoteMapper {
verified = verified 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
)
}
} }

View File

@ -3,10 +3,10 @@ package com.example.api.core.data.dto
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
internal data class SalesAndActionsDto( internal data class ProjectsDto(
val page: Int, val page: Int,
val perPage: Int, val perPage: Int,
val totalPage: Int, val totalPages: Int,
val totalItems: Int, val totalItems: Int,
val items: List<SaleItemDto> val items: List<SaleItemDto>
) )

View File

@ -8,4 +8,6 @@ package com.example.api.core.domain
interface ApiRepository { interface ApiRepository {
suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse> suspend fun auth(registerUser: RegisterUser): FetchResult<AuthUserResponse>
suspend fun projects(): FetchResult<Projects>
} }

View File

@ -1,6 +1,6 @@
package com.example.api.core.domain package com.example.api.core.domain
internal data class SalesAndActions( data class Projects(
val page: Int, val page: Int,
val perPage: Int, val perPage: Int,
val totalPage: Int, val totalPage: Int,

View File

@ -126,6 +126,49 @@ class ServiceTest {
assertEquals("/collections/users/auth-with-password", request.path) 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 @After
fun teardown() { fun teardown() {
mockWebServer.shutdown() mockWebServer.shutdown()

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.example.api.core.domain.ApiRepository import com.example.api.core.domain.ApiRepository
import com.example.api.core.domain.AuthUserResponse import com.example.api.core.domain.AuthUserResponse
import com.example.api.core.domain.RegisterUser import com.example.api.core.domain.RegisterUser
import com.example.api.core.domain.Projects
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -24,10 +25,15 @@ class MainViewModel @Inject constructor(
private val apiRepository: ApiRepository private val apiRepository: ApiRepository
) : ViewModel() { ) : ViewModel() {
private val _loadResultUiState = private val _authUiState =
MutableStateFlow<FetchResultUiState<AuthUserResponse>>(FetchResultUiState.Initial()) MutableStateFlow<FetchResultUiState<AuthUserResponse>>(FetchResultUiState.Initial())
val loadResultUiState: StateFlow<FetchResultUiState<AuthUserResponse>> val authUiState: StateFlow<FetchResultUiState<AuthUserResponse>>
get() = _loadResultUiState.asStateFlow() get() = _authUiState.asStateFlow()
private val _ProjectsUiState =
MutableStateFlow<FetchResultUiState<Projects>>(FetchResultUiState.Initial())
val projectsUiState: StateFlow<FetchResultUiState<Projects>>
get() = _ProjectsUiState.asStateFlow()
fun auth(email: String, password: String) { fun auth(email: String, password: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -40,7 +46,17 @@ class MainViewModel @Inject constructor(
) )
withContext(Dispatchers.Main) { 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())
} }
} }
} }

View File

@ -23,11 +23,12 @@ fun TestScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: MainViewModel = hiltViewModel() 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) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column { Column {
Button(onClick = { viewModel.auth("someemael@te3st1.com", "12345678") }) { Button(onClick = { viewModel.auth("someemael@te3s11t1.com", "12345678") }) {
Text("register") Text("register")
} }
@ -36,6 +37,16 @@ fun TestScreen(
onError = { _, message -> Text(text = message) }, onError = { _, message -> Text(text = message) },
onLoading = { CircularProgressIndicator() } 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() }
)
} }
} }
} }