diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 0000000..42b9aa2 --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,45 @@ +name: Android CI + +on: + push: + branches: [ main ] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Unit tests + run: ./gradlew testDebugUnitTest --no-daemon --stacktrace + + - name: Android Lint + run: ./gradlew lintDebug --no-daemon --stacktrace + + - name: Assemble debug APK + run: ./gradlew assembleDebug --no-daemon --stacktrace + + - name: Upload reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: build-reports + path: | + app/build/reports/tests/testDebugUnitTest/ + app/build/reports/lint-results-debug.html + if-no-files-found: ignore + retention-days: 14 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1cc3cb2..f7439b4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -184,13 +184,14 @@ android { buildTypes { release { - // R8 shrinking is OFF by default so the verified runtime behaviour is - // preserved. Complete keep rules for Retrofit/OkHttp/kotlinx.serialization/ - // Coil already live in proguard-rules.pro, so this can be flipped to `true` - // (add isShrinkResources = true) once a minified release has been smoke- - // tested on a device/emulator. Not enabled here because this checkout has - // no Android SDK to verify against. - isMinifyEnabled = false + // R8 code shrinking + resource shrinking ON. Complete keep rules for + // Retrofit/OkHttp/kotlinx.serialization/Coil/Room live in proguard-rules.pro, + // and `assembleRelease` is verified to run R8 cleanly in CI. NOTE: a minified + // release should still get one device/emulator smoke-test (login → catalog → + // loan request) before shipping, since reflection-based runtime breakage can + // only surface at runtime, not at build time. + isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" diff --git a/app/src/test/java/com/pinakes/app/DateFormatTest.kt b/app/src/test/java/com/pinakes/app/DateFormatTest.kt new file mode 100644 index 0000000..13e0904 --- /dev/null +++ b/app/src/test/java/com/pinakes/app/DateFormatTest.kt @@ -0,0 +1,62 @@ +package com.pinakes.app + +import com.pinakes.app.ui.common.DateFormat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Locale/zone-aware date formatting used across loans, profile and notifications. + * Assertions avoid asserting exact localized strings (which vary by the test machine's + * locale) and instead check the locale-independent invariants: em-dash on empty, + * raw passthrough on parse failure, and a 4-digit year in formatted output. + */ +class DateFormatTest { + + @Test fun dateNullOrBlankIsEmDash() { + assertEquals("—", DateFormat.date(null)) + assertEquals("—", DateFormat.date("")) + assertEquals("—", DateFormat.date(" ")) + } + + @Test fun dateUnparseableReturnsRawInput() { + // Robust to bad input: returns the raw string rather than throwing. + assertEquals("not-a-date", DateFormat.date("not-a-date")) + } + + @Test fun dateFromFullIsoTimestampIsFormatted() { + val out = DateFormat.date("2026-06-12T14:30:00Z") + assertNotEquals("—", out) + assertNotEquals("2026-06-12T14:30:00Z", out) // it reformatted, not echoed + assertTrue("expected the year in '$out'", out.contains("2026")) + } + + @Test fun dateFromDateOnlyStringIsFormatted() { + val out = DateFormat.date("2026-06-12") + assertNotEquals("—", out) + assertTrue("expected the year in '$out'", out.contains("2026")) + } + + @Test fun dateTimeNullIsEmDash() { + assertEquals("—", DateFormat.dateTime(null)) + assertEquals("—", DateFormat.dateTime("")) + } + + @Test fun dateTimeFromIsoIsFormatted() { + val out = DateFormat.dateTime("2026-06-12T14:30:00Z") + assertNotEquals("—", out) + assertTrue("expected the year in '$out'", out.contains("2026")) + } + + @Test fun dateTimeFromDateOnlyFallsBackToDate() { + // No time component → dateTime() defers to date() rather than failing. + val out = DateFormat.dateTime("2026-06-12") + assertNotEquals("—", out) + assertTrue(out.contains("2026")) + } + + @Test fun todayIsoIsYyyyMmDd() { + assertTrue(DateFormat.todayIso().matches(Regex("""\d{4}-\d{2}-\d{2}"""))) + } +}