Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Run tests
on:
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Unit Tests
run: |
chmod +x gradlew
./gradlew test --stacktrace
22 changes: 22 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ android {
}
}

packaging {
resources {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
kotlinOptions {
freeCompilerArgs += listOf(
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
)
}
}

namespace = "com.nativeapptemplate.nativeapptemplatefree"
}

Expand Down Expand Up @@ -138,6 +154,12 @@ dependencies {
ksp(libs.hilt.compiler)

debugImplementation(libs.androidx.compose.ui.tooling)

testImplementation(libs.androidx.navigation.testing)
testImplementation(libs.hilt.android.testing)
testImplementation(libs.kotlin.test)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.robolectric)
}


16 changes: 16 additions & 0 deletions app/src/main/assets/logged_in_shopkeeper.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"data": {
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1A",
"type": "shopkeeper_sign_in",
"attributes": {
"account_id": "2140BC6B-1830-45EE-96A4-B4ED5F53AC11",
"personal_account_id": "2140BC6B-1830-45EE-96A4-B4ED5F53AC11",
"account_owner_id": "5712F2DF-DFC7-A3AA-66BC-191203654A1A",
"account_name": "Account1",
"email": "[email protected]",
"name": "John Smith",
"time_zone": "Tokyo",
"uid": "[email protected]"
}
}
}
40 changes: 40 additions & 0 deletions app/src/main/assets/permissions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"data": [
{
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1A",
"type": "permission",
"attributes": {
"name": "update shops",
"tag": "update_shops",
"created_at": "2024-07-01T15:30:35.000Z",
"updated_at": "2024-07-01T15:30:35.000Z"
}
},
{
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1B",
"type": "permission",
"attributes": {
"name": "update organizations",
"tag": "update_organizations",
"created_at": "2024-07-01T15:30:35.000Z",
"updated_at": "2024-07-01T15:30:35.000Z"
}
},
{
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C",
"type": "permission",
"attributes": {
"name": "invitation",
"tag": "invitation",
"created_at": "2024-07-01T15:30:35.000Z",
"updated_at": "2024-07-01T15:30:35.000Z"
}
}
],
"meta": {
"android_app_version": 1,
"should_update_privacy": false,
"should_update_terms": false,
"shop_limit_count": 99
}
}
15 changes: 15 additions & 0 deletions app/src/main/assets/shop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"data": {
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C",
"type": "shop",
"attributes": {
"name": "Shop1",
"description": "This is a Shop1",
"time_zone": "Tokyo"
},
"meta": {
"limit_count": 96,
"created_shops_count": 3
}
}
}
43 changes: 43 additions & 0 deletions app/src/main/assets/shops.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"data": [
{
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1C",
"type": "shop",
"attributes": {
"name": "Shop1",
"description": "This is a Shop1",
"time_zone": "Tokyo"
},
"meta": {
"limit_count": 96,
"created_shops_count": 3
}
},
{
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1D",
"type": "shop",
"attributes": {
"name": "Shop2",
"description": "This is a Shop2",
"time_zone": "Tokyo"
},
"meta": {
"limit_count": 96,
"created_shops_count": 3
}
},
{
"id": "5712F2DF-DFC7-A3AA-66BC-191203654A1E",
"type": "shop",
"attributes": {
"name": "Shop3",
"description": "This is a Shop3",
"time_zone": "Tokyo"
},
"meta": {
"limit_count": 96,
"created_shops_count": 3
}
}
]
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
@file:Suppress("ktlint:standard:max-line-length")

package com.nativeapptemplate.nativeapptemplatefree.ui.settings

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
Expand Down Expand Up @@ -109,7 +106,7 @@ fun DarkModeSettingsDialog(

// [ColumnScope] is used for using the [ColumnScope.AnimatedVisibility] extension overload composable.
@Composable
private fun ColumnScope.SettingsPanel(
private fun SettingsPanel(
settings: UserEditableSettings,
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nativeapptemplate.nativeapptemplatefree.BuildConfig
import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.R
import com.nativeapptemplate.nativeapptemplatefree.model.DarkThemeConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.nativeapptemplate.nativeapptemplatefree.datastore

import com.nativeapptemplate.nativeapptemplatefree.UserPreferences
import com.nativeapptemplate.nativeapptemplatefree.datastoreTest.InMemoryDataStore
import com.nativeapptemplate.nativeapptemplatefree.model.Attributes
import com.nativeapptemplate.nativeapptemplatefree.model.Data
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test

class NatPreferencesDataSourceTest {
private val testScope = TestScope(UnconfinedTestDispatcher())

private lateinit var subject: NatPreferencesDataSource

@Before
fun setup() {
subject = NatPreferencesDataSource(InMemoryDataStore(UserPreferences.getDefaultInstance()))
}

@Test
fun isLoggedIn_isFalseByDefault() = testScope.runTest {
assertFalse(subject.userData.first().isLoggedIn)
}

@Test
fun isLoggedIn_whenSettingShopkeeper_becomesTrue() = testScope.runTest {
assertFalse(subject.isLoggedIn().first())

subject.setShopkeeper(testInputLoggedInShopkeeper)

assertTrue(subject.isLoggedIn().first())
}
}

private const val LOGGED_IN_SHOPKEEPER_TYPE = "shopkeeper_sign_in"
private const val LOGGED_IN_SHOPKEEPER_ID = "5712F2DF-DFC7-A3AA-66BC-191203654A1A"
private const val LOGGED_IN_SHOPKEEPER_ACCOUNT_ID = "2140BC6B-1830-45EE-96A4-B4ED5F53AC11"
private const val LOGGED_IN_SHOPKEEPER_PERSONAL_ACCOUNT_ID = "2140BC6B-1830-45EE-96A4-B4ED5F53AC11"
private const val LOGGED_IN_SHOPKEEPER_ACCOUNT_OWNER_ID = "5712F2DF-DFC7-A3AA-66BC-191203654A1A"
private const val LOGGED_IN_SHOPKEEPER_ACCOUNT_NAME = "Account1"
private const val LOGGED_IN_SHOPKEEPER_EMAIL = "[email protected]"
private const val LOGGED_IN_SHOPKEEPER_NAME = "John Smith"
private const val LOGGED_IN_SHOPKEEPER_TIME_ZONE = "Tokyo"
private const val LOGGED_IN_SHOPKEEPER_TOKEN = "[email protected]"
private const val LOGGED_IN_SHOPKEEPER_CLIENT = "Vd6GFW-9DaZrU2pzFd-Asa"
private const val LOGGED_IN_SHOPKEEPER_UID = "[email protected]"
private const val LOGGED_IN_SHOPKEEPER_EXPIRY = "1713165114"

private val testInputLoggedInShopkeeperData =
Data(
id = LOGGED_IN_SHOPKEEPER_ID,
type = LOGGED_IN_SHOPKEEPER_TYPE,
attributes = Attributes(
accountId = LOGGED_IN_SHOPKEEPER_ACCOUNT_ID,
personalAccountId = LOGGED_IN_SHOPKEEPER_PERSONAL_ACCOUNT_ID,
accountOwnerId = LOGGED_IN_SHOPKEEPER_ACCOUNT_OWNER_ID,
accountName = LOGGED_IN_SHOPKEEPER_ACCOUNT_NAME,
email = LOGGED_IN_SHOPKEEPER_EMAIL,
name = LOGGED_IN_SHOPKEEPER_NAME,
timeZone = LOGGED_IN_SHOPKEEPER_TIME_ZONE,
token = LOGGED_IN_SHOPKEEPER_TOKEN,
client = LOGGED_IN_SHOPKEEPER_CLIENT,
uid = LOGGED_IN_SHOPKEEPER_UID,
expiry = LOGGED_IN_SHOPKEEPER_EXPIRY,
)
)

private val testInputLoggedInShopkeeper = LoggedInShopkeeper(
datum = testInputLoggedInShopkeeperData,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.nativeapptemplate.nativeapptemplatefree.datastore

import androidx.datastore.core.CorruptionException
import com.nativeapptemplate.nativeapptemplatefree.userPreferences
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream

class UserPreferencesSerializerTest {
private val userPreferencesSerializer = UserPreferencesSerializer()

@Test
fun defaultUserPreferences_isEmpty() {
kotlin.test.assertEquals(
userPreferences {
// Default value
},
userPreferencesSerializer.defaultValue,
)
}

@Test
fun writingAndReadingUserPreferences_outputsCorrectValue() = runTest {
val expectedUserPreferences = userPreferences {
isLoggedIn = true
}

val outputStream = ByteArrayOutputStream()

expectedUserPreferences.writeTo(outputStream)

val inputStream = ByteArrayInputStream(outputStream.toByteArray())

val actualUserPreferences = userPreferencesSerializer.readFrom(inputStream)

kotlin.test.assertEquals(
expectedUserPreferences,
actualUserPreferences,
)
}

@Test(expected = CorruptionException::class)
fun readingInvalidUserPreferences_throwsCorruptionException() = runTest {
userPreferencesSerializer.readFrom(ByteArrayInputStream(byteArrayOf(0)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nativeapptemplate.nativeapptemplatefree.datastoreTest

import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.updateAndGet

class InMemoryDataStore<T>(initialValue: T) : DataStore<T> {
override val data = MutableStateFlow(initialValue)
override suspend fun updateData(
transform: suspend (it: T) -> T,
) = data.updateAndGet { transform(it) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.nativeapptemplate.nativeapptemplatefree.datastoreTest

import androidx.datastore.core.DataStore
import com.nativeapptemplate.nativeapptemplatefree.UserPreferences
import com.nativeapptemplate.nativeapptemplatefree.datastore.UserPreferencesSerializer
import com.nativeapptemplate.nativeapptemplatefree.di.modules.DataStoreModule
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [DataStoreModule::class],
)
internal object TestDataStoreModule {
@Provides
@Singleton
fun providesUserPreferencesDataStore(
serializer: UserPreferencesSerializer,
): DataStore<UserPreferences> = InMemoryDataStore(serializer.defaultValue)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.nativeapptemplate.nativeapptemplatefree.demo

import android.content.Context
import java.io.InputStream

fun interface DemoAssetManager {
fun open(context : Context, fileName: String): InputStream
}
Loading