Skip to content

Commit f9d168f

Browse files
dadachiclaude
andcommitted
Add unit tests for utils, network, and pre-push hook
- Add 34 new unit tests across 4 test files covering previously untested code - RequestHelperTest (6 tests): header generation with and without auth tokens - DateTimeFormatterUtilityTest (4 tests): locale-safe formatter assertions - DateUtilityTest (10 tests): ZonedDateTime/String extensions and timezone conversion - UtilityTest (14 tests): email validation, alphanumeric checks, scanUri - Add scripts/pre-push hook running spotlessCheck, lintDebug, and tests - Skip pre-push checks on branch deletions (zero-hash detection) - Document pre-push hook in CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 52a739c commit f9d168f

6 files changed

Lines changed: 344 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ MVVM layered architecture following [Android Modern App Architecture](https://de
5656
- Demo data: `demo/` package contains `DemoAssetManager` and `Demo*Repository` classes that load JSON fixtures from `app/src/test/assets/`.
5757
- JVM toolchain: Java 17.
5858

59+
## Pre-push Hook
60+
61+
A `scripts/pre-push` script runs Spotless, lint, and tests before each push. Install it with:
62+
```bash
63+
cp scripts/pre-push .git/hooks/pre-push
64+
```
65+
5966
## Connecting to Local API
6067

6168
In `app/build.gradle.kts`, swap the debug `buildConfigField` values:
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.network
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Assert.assertFalse
5+
import org.junit.Assert.assertNotNull
6+
import org.junit.Assert.assertTrue
7+
import org.junit.Test
8+
9+
class RequestHelperTest {
10+
11+
@Test
12+
fun getHeaders_withoutAuth_containsBaseHeaders() {
13+
val helper = RequestHelper()
14+
val headers = helper.getHeaders()
15+
16+
assertEquals("android", headers["source"])
17+
assertEquals("application/vnd.api+json; charset=utf-8", headers["Accept"])
18+
assertEquals("application/json", headers["Content-Type"])
19+
}
20+
21+
@Test
22+
fun getHeaders_withoutAuth_doesNotContainAuthHeaders() {
23+
val helper = RequestHelper()
24+
val headers = helper.getHeaders()
25+
26+
assertFalse(headers.containsKey("access-token"))
27+
assertFalse(headers.containsKey("token-type"))
28+
assertFalse(headers.containsKey("client"))
29+
assertFalse(headers.containsKey("expiry"))
30+
assertFalse(headers.containsKey("uid"))
31+
}
32+
33+
@Test
34+
fun getHeaders_withAuth_containsAuthHeaders() {
35+
val helper = RequestHelper(
36+
apiAuthToken = "test-token",
37+
client = "test-client",
38+
expiry = "12345",
39+
uid = "test@example.com",
40+
)
41+
val headers = helper.getHeaders()
42+
43+
assertEquals("test-token", headers["access-token"])
44+
assertEquals("Bearer ", headers["token-type"])
45+
assertEquals("test-client", headers["client"])
46+
assertEquals("12345", headers["expiry"])
47+
assertEquals("test@example.com", headers["uid"])
48+
}
49+
50+
@Test
51+
fun getHeaders_withAuth_stillContainsBaseHeaders() {
52+
val helper = RequestHelper(
53+
apiAuthToken = "test-token",
54+
client = "test-client",
55+
expiry = "12345",
56+
uid = "test@example.com",
57+
)
58+
val headers = helper.getHeaders()
59+
60+
assertEquals("android", headers["source"])
61+
assertEquals("application/vnd.api+json; charset=utf-8", headers["Accept"])
62+
assertEquals("application/json", headers["Content-Type"])
63+
}
64+
65+
@Test
66+
fun getHeaders_withEmptyToken_doesNotContainAuthHeaders() {
67+
val helper = RequestHelper(
68+
apiAuthToken = "",
69+
client = "test-client",
70+
expiry = "12345",
71+
uid = "test@example.com",
72+
)
73+
val headers = helper.getHeaders()
74+
75+
assertFalse(headers.containsKey("access-token"))
76+
assertFalse(headers.containsKey("token-type"))
77+
}
78+
79+
@Test
80+
fun getHeaders_containsClientNameAndVersion() {
81+
val helper = RequestHelper()
82+
val headers = helper.getHeaders()
83+
84+
assertNotNull(headers["client-name"])
85+
assertNotNull(headers["client-version"])
86+
assertTrue(headers["client-name"]!!.isNotEmpty())
87+
assertTrue(headers["client-version"]!!.isNotEmpty())
88+
}
89+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.utils
2+
3+
import org.junit.Assert.assertNotNull
4+
import org.junit.Assert.assertTrue
5+
import org.junit.Test
6+
import java.time.LocalDateTime
7+
8+
class DateTimeFormatterUtilityTest {
9+
10+
private val testDateTime = LocalDateTime.of(2025, 3, 15, 14, 30, 0)
11+
12+
@Test
13+
fun cardDateFormatter_isNotNull() {
14+
assertNotNull(DateTimeFormatterUtility.cardDateFormatter())
15+
}
16+
17+
@Test
18+
fun cardDateFormatter_formatsDateWithMonthDayYear() {
19+
val formatted = testDateTime.format(DateTimeFormatterUtility.cardDateFormatter())
20+
// Locale-safe: verify it contains day and year regardless of month name locale
21+
assertTrue(formatted.contains("15"))
22+
assertTrue(formatted.contains("2025"))
23+
}
24+
25+
@Test
26+
fun cardTimeFormatter_isNotNull() {
27+
assertNotNull(DateTimeFormatterUtility.cardTimeFormatter())
28+
}
29+
30+
@Test
31+
fun cardTimeFormatter_formatsTimeAsHoursAndMinutes() {
32+
val formatted = testDateTime.format(DateTimeFormatterUtility.cardTimeFormatter())
33+
assertTrue(formatted.contains("14"))
34+
assertTrue(formatted.contains("30"))
35+
}
36+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.utils
2+
3+
import com.nativeapptemplate.nativeapptemplatefree.utils.DateUtility.cardDateString
4+
import com.nativeapptemplate.nativeapptemplatefree.utils.DateUtility.cardTimeString
5+
import org.junit.Assert.assertEquals
6+
import org.junit.Assert.assertTrue
7+
import org.junit.Test
8+
import java.time.ZoneId
9+
import java.time.ZonedDateTime
10+
11+
class DateUtilityTest {
12+
13+
private val testZonedDateTime = ZonedDateTime.of(
14+
2025,
15+
3,
16+
15,
17+
14,
18+
30,
19+
0,
20+
0,
21+
ZoneId.of("UTC"),
22+
)
23+
24+
// ZonedDateTime extension tests
25+
26+
@Test
27+
fun zonedDateTime_cardDateString_formatsCorrectly() {
28+
val result = testZonedDateTime.cardDateString()
29+
// Locale-safe: verify it contains day and year
30+
assertTrue(result.contains("15"))
31+
assertTrue(result.contains("2025"))
32+
}
33+
34+
@Test
35+
fun zonedDateTime_cardTimeString_formatsCorrectly() {
36+
assertEquals("14:30", testZonedDateTime.cardTimeString())
37+
}
38+
39+
// String extension tests with UTC zone
40+
41+
@Test
42+
fun string_cardDateString_formatsIsoStringWithUtcZone() {
43+
val isoString = "2025-03-15T14:30:00Z"
44+
val result = isoString.cardDateString(ZoneId.of("UTC"))
45+
assertTrue(result.contains("15"))
46+
assertTrue(result.contains("2025"))
47+
}
48+
49+
@Test
50+
fun string_cardTimeString_formatsIsoStringWithUtcZone() {
51+
val isoString = "2025-03-15T14:30:00Z"
52+
assertEquals("14:30", isoString.cardTimeString(ZoneId.of("UTC")))
53+
}
54+
55+
// Blank string tests
56+
57+
@Test
58+
fun string_cardDateString_returnsEmptyForBlankString() {
59+
assertEquals("", "".cardDateString())
60+
}
61+
62+
@Test
63+
fun string_cardTimeString_returnsEmptyForBlankString() {
64+
assertEquals("", "".cardTimeString())
65+
}
66+
67+
@Test
68+
fun string_cardDateString_returnsEmptyForWhitespaceString() {
69+
assertEquals("", " ".cardDateString())
70+
}
71+
72+
@Test
73+
fun string_cardTimeString_returnsEmptyForWhitespaceString() {
74+
assertEquals("", " ".cardTimeString())
75+
}
76+
77+
// Timezone conversion tests
78+
79+
@Test
80+
fun string_cardDateString_convertsTimezoneCorrectly() {
81+
// 2025-03-15T23:30:00Z in UTC is 2025-03-16 08:30 in Asia/Tokyo (+9)
82+
val isoString = "2025-03-15T23:30:00Z"
83+
val result = isoString.cardDateString(ZoneId.of("Asia/Tokyo"))
84+
assertTrue(result.contains("16"))
85+
assertTrue(result.contains("2025"))
86+
}
87+
88+
@Test
89+
fun string_cardTimeString_convertsTimezoneCorrectly() {
90+
// 2025-03-15T14:30:00Z in UTC is 23:30 in Asia/Tokyo (+9)
91+
val isoString = "2025-03-15T14:30:00Z"
92+
assertEquals("23:30", isoString.cardTimeString(ZoneId.of("Asia/Tokyo")))
93+
}
94+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.utils
2+
3+
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Assert.assertFalse
6+
import org.junit.Assert.assertTrue
7+
import org.junit.Test
8+
import org.junit.runner.RunWith
9+
import org.robolectric.RobolectricTestRunner
10+
11+
@RunWith(RobolectricTestRunner::class)
12+
class UtilityTest {
13+
14+
// validateEmail tests
15+
16+
@Test
17+
fun validateEmail_validEmail_returnsTrue() {
18+
assertTrue("test@example.com".validateEmail())
19+
}
20+
21+
@Test
22+
fun validateEmail_emptyString_returnsFalse() {
23+
assertFalse("".validateEmail())
24+
}
25+
26+
@Test
27+
fun validateEmail_noAtSign_returnsFalse() {
28+
assertFalse("testexample.com".validateEmail())
29+
}
30+
31+
@Test
32+
fun validateEmail_noDomain_returnsFalse() {
33+
assertFalse("test@".validateEmail())
34+
}
35+
36+
// isAlphanumeric tests
37+
38+
@Test
39+
fun isAlphanumeric_alphanumericText_returnsTrue() {
40+
assertTrue(Utility.isAlphanumeric("abc123"))
41+
}
42+
43+
@Test
44+
fun isAlphanumeric_lettersOnly_returnsTrue() {
45+
assertTrue(Utility.isAlphanumeric("abcdef"))
46+
}
47+
48+
@Test
49+
fun isAlphanumeric_numbersOnly_returnsTrue() {
50+
assertTrue(Utility.isAlphanumeric("123456"))
51+
}
52+
53+
@Test
54+
fun isAlphanumeric_specialChars_returnsFalse() {
55+
assertFalse(Utility.isAlphanumeric("abc!@#"))
56+
}
57+
58+
@Test
59+
fun isAlphanumeric_null_returnsFalse() {
60+
assertFalse(Utility.isAlphanumeric(null))
61+
}
62+
63+
@Test
64+
fun isAlphanumeric_blank_returnsFalse() {
65+
assertFalse(Utility.isAlphanumeric(""))
66+
}
67+
68+
// scanUri tests
69+
70+
@Test
71+
fun scanUri_serverType_usesScanPath() {
72+
val uri = Utility.scanUri("test-id", "server")
73+
assertTrue(uri.toString().contains("/scan?"))
74+
}
75+
76+
@Test
77+
fun scanUri_customerType_usesScanCustomerPath() {
78+
val uri = Utility.scanUri("test-id", "customer")
79+
assertTrue(uri.toString().contains("/scan_customer?"))
80+
}
81+
82+
@Test
83+
fun scanUri_containsItemTagId() {
84+
val uri = Utility.scanUri("test-id-123", "server")
85+
assertEquals("test-id-123", uri.getQueryParameter("item_tag_id"))
86+
}
87+
88+
@Test
89+
fun scanUri_containsType() {
90+
val uri = Utility.scanUri("test-id", "server")
91+
assertEquals("server", uri.getQueryParameter("type"))
92+
}
93+
}

scripts/pre-push

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env bash
2+
3+
# Git pre-push hook – runs local CI checks before pushing
4+
5+
set -e
6+
7+
# Skip checks for branch deletions
8+
while read local_ref local_sha remote_ref remote_sha; do
9+
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
10+
exit 0
11+
fi
12+
done
13+
14+
echo "==> Running local CI checks before push..."
15+
16+
echo "--- Spotless check ---"
17+
./gradlew spotlessCheck --stacktrace || exit 1
18+
19+
echo "--- Lint ---"
20+
./gradlew lintDebug --stacktrace || exit 1
21+
22+
echo "--- Tests ---"
23+
./gradlew test --stacktrace || exit 1
24+
25+
echo "==> All checks passed!"

0 commit comments

Comments
 (0)