Skip to content

Commit

Permalink
Merge pull request #115 from azrael8576/feat/fake-api-integration
Browse files Browse the repository at this point in the history
Integrate FakeRetrofitAtNetwork for API mock
  • Loading branch information
azrael8576 authored Feb 2, 2024
2 parents 426bd96 + f49cf55 commit 1de444b
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 6 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ UI 設計採用 [_Material 3 Design_](https://m3.material.io/) ,並以 Jetpack
> **Note:**[`ScheduleScreen`](https://github.com/azrael8576/amazing-talker/blob/main/feature/teacherschedule/src/main/java/com/wei/amazingtalker/feature/teacherschedule/schedule/ScheduleScreen.kt) 可以看到其搭配 Snap 動畫之使用範例。
## Build
該應用程序包含常用 `demoDebug``demoRelease` build variants。(`prod` variants 保留未來供生產環境所使用).
該應用程序包含常用 `debug``release` build variants。

目前所有版本連線**均為測試環境**
此外,該應用程序也使用了[Product Flavors](https://developer.android.com/studio/build/build-variants#product-flavors)來控制應用內容的載入來源。

- `demo` flavor 透過使用靜態本地數據,允許開發者立即建立應用並探索其使用者介面。
- `prod` flavor 則透過向後端伺服器發起真實網路請求,提供最新的內容。 目前,尚未公開後端服務。

對於正常開發,請使用該 `demoDebug` variant。對於 UI 性能測試,請使用該 `demoRelease` variant。

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.wei.amazingtalker.core.network.di

import com.wei.amazingtalker.core.network.AtNetworkDataSource
import com.wei.amazingtalker.core.network.fake.FakeRetrofitAtNetwork
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
internal interface FlavoredNetworkModule {
@Binds
fun binds(implementation: FakeRetrofitAtNetwork): AtNetworkDataSource
}
78 changes: 78 additions & 0 deletions core/network/src/main/assets/teacher_schedule.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"available": [
{
"start": "2024-02-05T11:30:00Z",
"end": "2024-02-05T12:00:00Z"
},
{
"start": "2024-02-05T13:00:00Z",
"end": "2024-02-05T20:30:00Z"
},
{
"start": "2024-02-06T11:30:00Z",
"end": "2024-02-06T12:00:00Z"
},
{
"start": "2024-02-06T14:00:00Z",
"end": "2024-02-06T17:00:00Z"
},
{
"start": "2024-02-06T18:00:00Z",
"end": "2024-02-06T20:30:00Z"
},
{
"start": "2024-02-07T12:30:00Z",
"end": "2024-02-07T17:00:00Z"
},
{
"start": "2024-02-07T18:00:00Z",
"end": "2024-02-07T20:30:00Z"
},
{
"start": "2024-02-08T12:00:00Z",
"end": "2024-02-08T13:00:00Z"
},
{
"start": "2024-02-08T14:00:00Z",
"end": "2024-02-08T20:00:00Z"
},
{
"start": "2024-02-09T12:00:00Z",
"end": "2024-02-09T15:00:00Z"
},
{
"start": "2024-02-10T04:30:00Z",
"end": "2024-02-10T08:30:00Z"
}
],
"booked": [
{
"start": "2024-02-05T12:00:00Z",
"end": "2024-02-05T13:00:00Z"
},
{
"start": "2024-02-06T12:00:00Z",
"end": "2024-02-06T14:00:00Z"
},
{
"start": "2024-02-06T17:00:00Z",
"end": "2024-02-06T18:00:00Z"
},
{
"start": "2024-02-07T11:30:00Z",
"end": "2024-02-07T12:30:00Z"
},
{
"start": "2024-02-07T17:00:00Z",
"end": "2024-02-07T18:00:00Z"
},
{
"start": "2024-02-08T13:00:00Z",
"end": "2024-02-08T14:00:00Z"
},
{
"start": "2024-02-09T11:00:00Z",
"end": "2024-02-09T12:00:00Z"
}
]
}
26 changes: 26 additions & 0 deletions core/network/src/main/java/JvmUnitTestFakeAssetManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import androidx.annotation.VisibleForTesting
import com.wei.amazingtalker.core.network.fake.FakeAssetManager
import java.io.File
import java.io.InputStream
import java.util.Properties

/**
* This class helps with loading Android `/assets` files, especially when running JVM unit tests.
* It must remain on the root package for an easier [Class.getResource] with relative paths.
* @see <a href="https://developer.android.com/reference/tools/gradle-api/7.3/com/android/build/api/dsl/UnitTestOptions">UnitTestOptions</a>
*/
@VisibleForTesting
internal object JvmUnitTestFakeAssetManager : FakeAssetManager {
private val config =
requireNotNull(javaClass.getResource("com/android/tools/test_config.properties")) {
"""
Missing Android resources properties file.
Did you forget to enable the feature in the gradle build file?
android.testOptions.unitTests.isIncludeAndroidResources = true
""".trimIndent()
}
private val properties = Properties().apply { config.openStream().use(::load) }
private val assets = File(properties["android_merged_assets"].toString())

override fun open(fileName: String): InputStream = File(assets, fileName).inputStream()
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.wei.amazingtalker.core.network.di

import android.content.Context
import com.wei.amazingtalker.core.network.BuildConfig
import com.wei.amazingtalker.core.network.fake.FakeAssetManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.Call
Expand All @@ -21,6 +24,12 @@ internal object NetworkModule {
ignoreUnknownKeys = true
}

@Provides
@Singleton
fun providesFakeAssetManager(
@ApplicationContext context: Context,
): FakeAssetManager = FakeAssetManager(context.assets::open)

@Provides
@Singleton
fun okHttpCallFactory(): Call.Factory =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.wei.amazingtalker.core.network.fake

import java.io.InputStream

fun interface FakeAssetManager {
fun open(fileName: String): InputStream
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.wei.amazingtalker.core.network.fake

import JvmUnitTestFakeAssetManager
import com.wei.amazingtalker.core.network.AtDispatchers
import com.wei.amazingtalker.core.network.AtNetworkDataSource
import com.wei.amazingtalker.core.network.Dispatcher
import com.wei.amazingtalker.core.network.model.NetworkTeacherSchedule
import com.wei.amazingtalker.core.network.model.NetworkTimeSlots
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.time.Duration
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import javax.inject.Inject

/**
* Mocks network data source for teacher availability, using static JSON assets for development and testing.
*/
class FakeRetrofitAtNetwork
@Inject
constructor(
@Dispatcher(AtDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
private val networkJson: Json,
private val assets: FakeAssetManager = JvmUnitTestFakeAssetManager,
) : AtNetworkDataSource {
/**
* Fetches mocked teacher availability based on a given start time, adjusting time slots dynamically.
*
* @param teacherName Name of the teacher (unused in mock).
* @param startedAt Start time to adjust availability slots.
* @return Adjusted {@link NetworkTeacherSchedule} with available and booked slots.
*/
@OptIn(ExperimentalSerializationApi::class)
override suspend fun getTeacherAvailability(
teacherName: String,
startedAt: String?,
): NetworkTeacherSchedule =
withContext(ioDispatcher) {
// Parse the input time, rounding down to the nearest hour if necessary
val inputTime = ZonedDateTime.parse(startedAt).withMinute(0).withSecond(0).withNano(0)
val baseTime = ZonedDateTime.parse("2024-02-04T16:00Z")

// Calculate the difference in hours between the input time and the base time
val hoursDiff = Duration.between(baseTime, inputTime).toHours()

// Decode the schedule from the JSON asset
val schedule: NetworkTeacherSchedule =
assets.open(TEACHER_SCHEDULE_ASSET).use { inputStream ->
networkJson.decodeFromStream(inputStream)
}

// Adjust the schedule times based on the calculated difference
adjustScheduleTimes(schedule, hoursDiff)
}

/**
* Adjusts the provided schedule's time slots based on the hour difference.
*/
private fun adjustScheduleTimes(
schedule: NetworkTeacherSchedule,
hoursDiff: Long,
): NetworkTeacherSchedule {
return schedule.copy(
available = adjustTimeSlots(schedule.available, hoursDiff),
booked = adjustTimeSlots(schedule.booked, hoursDiff),
)
}

/**
* Adjusts a list of time slots by the specified hours difference.
*/
private fun adjustTimeSlots(
slots: List<NetworkTimeSlots>,
hoursDiff: Long,
): List<NetworkTimeSlots> {
return slots.map { slot ->
slot.adjustByHours(hoursDiff)
}
}

/**
* Adjusts the start and end times of a NetworkTimeSlot by the given hours difference.
*/
private fun NetworkTimeSlots.adjustByHours(hoursDiff: Long): NetworkTimeSlots {
val formatter = DateTimeFormatter.ISO_ZONED_DATE_TIME
val adjustedStart =
ZonedDateTime.parse(this.startUtc, formatter).plusHours(hoursDiff).format(formatter)
val adjustedEnd =
ZonedDateTime.parse(this.endUtc, formatter).plusHours(hoursDiff).format(formatter)
return this.copy(startUtc = adjustedStart, endUtc = adjustedEnd)
}

companion object {
private const val TEACHER_SCHEDULE_ASSET = "teacher_schedule.json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

/**
* TODO 移動至 build variants 下產出資料夾
*/
@Module
@InstallIn(SingletonComponent::class)
internal interface FlavoredNetworkModule {
Expand Down
2 changes: 1 addition & 1 deletion secrets.defaults.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## This file provides default values to modules using the secrets-gradle-plugin. It is necessary
# because the secrets properties file is not under source control so CI builds will fail without
# default values.
BACKEND_URL="https://tw.amazingtalker.com/v1/guest/"
BACKEND_URL="http://example.com"

0 comments on commit 1de444b

Please sign in to comment.