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
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ MVVM layered architecture following [Android Modern App Architecture](https://de
- Demo data: `demo/` package contains `DemoAssetManager` and `Demo*Repository` classes that load JSON fixtures from `app/src/test/assets/`.
- JVM toolchain: Java 17.

## Pre-push Hook

A `scripts/pre-push` script runs Spotless, lint, and tests before each push. Install it with:
```bash
cp scripts/pre-push .git/hooks/pre-push
```

## Connecting to Local API

In `app/build.gradle.kts`, swap the debug `buildConfigField` values:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.nativeapptemplate.nativeapptemplatefree.network

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test

class RequestHelperTest {

@Test
fun getHeaders_withoutAuth_containsBaseHeaders() {
val helper = RequestHelper()
val headers = helper.getHeaders()

assertEquals("android", headers["source"])
assertEquals("application/vnd.api+json; charset=utf-8", headers["Accept"])
assertEquals("application/json", headers["Content-Type"])
}

@Test
fun getHeaders_withoutAuth_doesNotContainAuthHeaders() {
val helper = RequestHelper()
val headers = helper.getHeaders()

assertFalse(headers.containsKey("access-token"))
assertFalse(headers.containsKey("token-type"))
assertFalse(headers.containsKey("client"))
assertFalse(headers.containsKey("expiry"))
assertFalse(headers.containsKey("uid"))
}

@Test
fun getHeaders_withAuth_containsAuthHeaders() {
val helper = RequestHelper(
apiAuthToken = "test-token",
client = "test-client",
expiry = "12345",
uid = "test@example.com",
)
val headers = helper.getHeaders()

assertEquals("test-token", headers["access-token"])
assertEquals("Bearer ", headers["token-type"])
assertEquals("test-client", headers["client"])
assertEquals("12345", headers["expiry"])
assertEquals("test@example.com", headers["uid"])
}

@Test
fun getHeaders_withAuth_stillContainsBaseHeaders() {
val helper = RequestHelper(
apiAuthToken = "test-token",
client = "test-client",
expiry = "12345",
uid = "test@example.com",
)
val headers = helper.getHeaders()

assertEquals("android", headers["source"])
assertEquals("application/vnd.api+json; charset=utf-8", headers["Accept"])
assertEquals("application/json", headers["Content-Type"])
}

@Test
fun getHeaders_withEmptyToken_doesNotContainAuthHeaders() {
val helper = RequestHelper(
apiAuthToken = "",
client = "test-client",
expiry = "12345",
uid = "test@example.com",
)
val headers = helper.getHeaders()

assertFalse(headers.containsKey("access-token"))
assertFalse(headers.containsKey("token-type"))
}

@Test
fun getHeaders_containsClientNameAndVersion() {
val helper = RequestHelper()
val headers = helper.getHeaders()

assertNotNull(headers["client-name"])
assertNotNull(headers["client-version"])
assertTrue(headers["client-name"]!!.isNotEmpty())
assertTrue(headers["client-version"]!!.isNotEmpty())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.nativeapptemplate.nativeapptemplatefree.utils

import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDateTime

class DateTimeFormatterUtilityTest {

private val testDateTime = LocalDateTime.of(2025, 3, 15, 14, 30, 0)

@Test
fun cardDateFormatter_isNotNull() {
assertNotNull(DateTimeFormatterUtility.cardDateFormatter())
}

@Test
fun cardDateFormatter_formatsDateWithMonthDayYear() {
val formatted = testDateTime.format(DateTimeFormatterUtility.cardDateFormatter())
// Locale-safe: verify it contains day and year regardless of month name locale
assertTrue(formatted.contains("15"))
assertTrue(formatted.contains("2025"))
}

@Test
fun cardTimeFormatter_isNotNull() {
assertNotNull(DateTimeFormatterUtility.cardTimeFormatter())
}

@Test
fun cardTimeFormatter_formatsTimeAsHoursAndMinutes() {
val formatted = testDateTime.format(DateTimeFormatterUtility.cardTimeFormatter())
assertTrue(formatted.contains("14"))
assertTrue(formatted.contains("30"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.nativeapptemplate.nativeapptemplatefree.utils

import com.nativeapptemplate.nativeapptemplatefree.utils.DateUtility.cardDateString
import com.nativeapptemplate.nativeapptemplatefree.utils.DateUtility.cardTimeString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.ZoneId
import java.time.ZonedDateTime

class DateUtilityTest {

private val testZonedDateTime = ZonedDateTime.of(
2025,
3,
15,
14,
30,
0,
0,
ZoneId.of("UTC"),
)

// ZonedDateTime extension tests

@Test
fun zonedDateTime_cardDateString_formatsCorrectly() {
val result = testZonedDateTime.cardDateString()
// Locale-safe: verify it contains day and year
assertTrue(result.contains("15"))
assertTrue(result.contains("2025"))
}

@Test
fun zonedDateTime_cardTimeString_formatsCorrectly() {
assertEquals("14:30", testZonedDateTime.cardTimeString())
}

// String extension tests with UTC zone

@Test
fun string_cardDateString_formatsIsoStringWithUtcZone() {
val isoString = "2025-03-15T14:30:00Z"
val result = isoString.cardDateString(ZoneId.of("UTC"))
assertTrue(result.contains("15"))
assertTrue(result.contains("2025"))
}

@Test
fun string_cardTimeString_formatsIsoStringWithUtcZone() {
val isoString = "2025-03-15T14:30:00Z"
assertEquals("14:30", isoString.cardTimeString(ZoneId.of("UTC")))
}

// Blank string tests

@Test
fun string_cardDateString_returnsEmptyForBlankString() {
assertEquals("", "".cardDateString())
}

@Test
fun string_cardTimeString_returnsEmptyForBlankString() {
assertEquals("", "".cardTimeString())
}

@Test
fun string_cardDateString_returnsEmptyForWhitespaceString() {
assertEquals("", " ".cardDateString())
}

@Test
fun string_cardTimeString_returnsEmptyForWhitespaceString() {
assertEquals("", " ".cardTimeString())
}

// Timezone conversion tests

@Test
fun string_cardDateString_convertsTimezoneCorrectly() {
// 2025-03-15T23:30:00Z in UTC is 2025-03-16 08:30 in Asia/Tokyo (+9)
val isoString = "2025-03-15T23:30:00Z"
val result = isoString.cardDateString(ZoneId.of("Asia/Tokyo"))
assertTrue(result.contains("16"))
assertTrue(result.contains("2025"))
}

@Test
fun string_cardTimeString_convertsTimezoneCorrectly() {
// 2025-03-15T14:30:00Z in UTC is 23:30 in Asia/Tokyo (+9)
val isoString = "2025-03-15T14:30:00Z"
assertEquals("23:30", isoString.cardTimeString(ZoneId.of("Asia/Tokyo")))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.nativeapptemplate.nativeapptemplatefree.utils

import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class UtilityTest {

// validateEmail tests

@Test
fun validateEmail_validEmail_returnsTrue() {
assertTrue("test@example.com".validateEmail())
}

@Test
fun validateEmail_emptyString_returnsFalse() {
assertFalse("".validateEmail())
}

@Test
fun validateEmail_noAtSign_returnsFalse() {
assertFalse("testexample.com".validateEmail())
}

@Test
fun validateEmail_noDomain_returnsFalse() {
assertFalse("test@".validateEmail())
}

// isAlphanumeric tests

@Test
fun isAlphanumeric_alphanumericText_returnsTrue() {
assertTrue(Utility.isAlphanumeric("abc123"))
}

@Test
fun isAlphanumeric_lettersOnly_returnsTrue() {
assertTrue(Utility.isAlphanumeric("abcdef"))
}

@Test
fun isAlphanumeric_numbersOnly_returnsTrue() {
assertTrue(Utility.isAlphanumeric("123456"))
}

@Test
fun isAlphanumeric_specialChars_returnsFalse() {
assertFalse(Utility.isAlphanumeric("abc!@#"))
}

@Test
fun isAlphanumeric_null_returnsFalse() {
assertFalse(Utility.isAlphanumeric(null))
}

@Test
fun isAlphanumeric_blank_returnsFalse() {
assertFalse(Utility.isAlphanumeric(""))
}

// scanUri tests

@Test
fun scanUri_serverType_usesScanPath() {
val uri = Utility.scanUri("test-id", "server")
assertTrue(uri.toString().contains("/scan?"))
}

@Test
fun scanUri_customerType_usesScanCustomerPath() {
val uri = Utility.scanUri("test-id", "customer")
assertTrue(uri.toString().contains("/scan_customer?"))
}

@Test
fun scanUri_containsItemTagId() {
val uri = Utility.scanUri("test-id-123", "server")
assertEquals("test-id-123", uri.getQueryParameter("item_tag_id"))
}

@Test
fun scanUri_containsType() {
val uri = Utility.scanUri("test-id", "server")
assertEquals("server", uri.getQueryParameter("type"))
}
}
25 changes: 25 additions & 0 deletions scripts/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

# Git pre-push hook – runs local CI checks before pushing

set -e

# Skip checks for branch deletions
while read local_ref local_sha remote_ref remote_sha; do
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
exit 0
fi
done

echo "==> Running local CI checks before push..."

echo "--- Spotless check ---"
./gradlew spotlessCheck --stacktrace || exit 1

echo "--- Lint ---"
./gradlew lintDebug --stacktrace || exit 1

echo "--- Tests ---"
./gradlew test --stacktrace || exit 1

echo "==> All checks passed!"
Loading