Asked in review: "does the latest code have the PIN input field?" It did.
## Hiding was weaker than it looked
pin_input, form_pin and the reveal toggle were all present, and the default
build hid them at runtime in on_after_new. Three problems:
1. The DSL declared the control visible and Rust hid it afterwards, so
anything re-applying the UI definition — a hot reload, a re-instantiated
sheet — brought it back. Defect B11 already names this class.
2. Hidden is not absent. The TextInput stayed in the widget tree, and a
hidden input can still be focused or filled programmatically.
3. form_pin was compiled into every build, so any path reaching it could
populate it.
Item 0.3 asks for removal, not concealment.
## The field no longer exists without `demo`
form_pin, pin_visible, the eye-toggle handler, the text-input handler, both
PIN checks, the request construction and try_build_ussd_request are all
#[cfg(feature = "demo")]. A default build has no field to write to.
The DSL now declares visible: false on the PIN input and its reveal button,
so hidden is the default state rather than a runtime correction; demo
unhides them on init. That closes the reload path in (1).
One runtime call remains as belt-and-braces: if a hot-reloaded definition
surfaces the input, the non-demo arm wipes what was typed. It has no
form_pin to clear, because there isn't one.
## Proving absence rather than asserting it
A grep for form_pin proves nothing — it passes just as happily against a
field still present behind a runtime if. tools/check-no-pin-capture.sh is a
compile probe: it references the field outside any cfg block and requires
the default build to fail with "no field `form_pin`" while demo succeeds.
The script restores the file on every exit path.
Verified both directions: passes on current code, exits 1 when the field is
re-exposed un-gated.
## Validation
cargo check -p {nigig-pay-ui,nigig-pay,nigig-mpesa,nigig-core} pass
cargo check -p {nigig-pay,nigig-mpesa} --features demo pass
nigig-pay-ui: cargo test --lib pass (61)
domain / storage / platform / mpesa harness pass
no-PIN-capture guard: verified to fail on a re-exposed field pass
## What 0.3 still leaves open
The Java-side KEY_PIN scrubbing was already done, so 0.3 is complete for the
default product. A demo build still captures a PIN by design — that path
exists for authorised device testing and is covered by the unresolved
Play-policy decision in ADR 0007. If that decision goes against the USSD
rail, the demo path and its PIN capture are deleted with it.
270 lines
12 KiB
YAML
270 lines
12 KiB
YAML
name: Payment domain, storage, platform and UI
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- 'crates/nigig-pay-domain/**'
|
|
- 'crates/nigig-pay-storage/**'
|
|
- 'crates/nigig-pay-platform/**'
|
|
- 'crates/robius-ussd/**'
|
|
- 'crates/apps/nigig-pay-ui/**'
|
|
- 'tools/makepad-native-libs.sh'
|
|
- 'tools/test-rust-clean.sh'
|
|
- 'tools/test-mpesa-store-clean.sh'
|
|
- 'crates/nigig-core/src/persistence/mpesa/**'
|
|
- 'rust-toolchain.toml'
|
|
- 'deny.toml'
|
|
- '.forgejo/workflows/pay-domain.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'crates/nigig-pay-domain/**'
|
|
- 'crates/nigig-pay-storage/**'
|
|
- 'crates/nigig-pay-platform/**'
|
|
- 'crates/robius-ussd/**'
|
|
- 'crates/apps/nigig-pay-ui/**'
|
|
- 'tools/makepad-native-libs.sh'
|
|
- 'tools/test-rust-clean.sh'
|
|
- 'tools/test-mpesa-store-clean.sh'
|
|
- 'crates/nigig-core/src/persistence/mpesa/**'
|
|
- 'rust-toolchain.toml'
|
|
- 'deny.toml'
|
|
- '.forgejo/workflows/pay-domain.yml'
|
|
|
|
jobs:
|
|
isolated-payment-tests:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# Each step installs its own throwaway toolchain at the version declared
|
|
# in rust-toolchain.toml, runs tests with --locked against the checked-in
|
|
# lockfile, then checks formatting, Clippy (-D warnings) and benchmarks.
|
|
- name: Domain tests, fmt, clippy and benches
|
|
run: TEST_TARGET=domain ./tools/test-rust-clean.sh
|
|
|
|
# The storage target additionally exercises the opt-in SQLCipher
|
|
# encryption-at-rest path.
|
|
- name: Storage tests, fmt, clippy and SQLCipher
|
|
run: TEST_TARGET=storage ./tools/test-rust-clean.sh
|
|
|
|
# The platform crate is the seam between the domain and Android. This
|
|
# target additionally compiles the optional USSD adapter for the host
|
|
# and asserts that the mock gateway cannot be built into a release.
|
|
- name: Platform gateway tests, fmt, clippy and mock-in-release guard
|
|
run: TEST_TARGET=platform ./tools/test-rust-clean.sh
|
|
|
|
# nigig-core is not a workspace member (it needs git-hosted Makepad and
|
|
# Robius crates), but its parser/classifier/store logic is pure and can
|
|
# be verified standalone. This covers defects B1 and B4.
|
|
- name: M-Pesa parser, classifier and store tests
|
|
run: ./tools/test-mpesa-store-clean.sh
|
|
|
|
- name: Reject whitespace errors
|
|
run: git diff --check
|
|
|
|
# Build governance: lockfiles must be present and current. A lockfile
|
|
# that changes during CI means the committed one was stale.
|
|
- name: Lockfiles must be committed and unchanged
|
|
run: |
|
|
set -e
|
|
for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do
|
|
test -f "crates/$crate/Cargo.lock" \
|
|
|| { echo "missing crates/$crate/Cargo.lock"; exit 1; }
|
|
done
|
|
git diff --exit-code -- '**/Cargo.lock'
|
|
|
|
# Dependency audit, licence and source checks (review item 1.2).
|
|
# Scoped to the crates whose dependency graph fully resolves standalone.
|
|
- name: Dependency audit, licences, bans and sources
|
|
run: |
|
|
set -e
|
|
version=0.18.6
|
|
url="https://github.com/EmbarkStudios/cargo-deny/releases/download"
|
|
curl -sSLf -o /tmp/cargo-deny.tar.gz \
|
|
"$url/$version/cargo-deny-$version-x86_64-unknown-linux-musl.tar.gz"
|
|
tar xzf /tmp/cargo-deny.tar.gz -C /tmp
|
|
install -m 0755 \
|
|
"/tmp/cargo-deny-$version-x86_64-unknown-linux-musl/cargo-deny" \
|
|
/usr/local/bin/cargo-deny
|
|
for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do
|
|
echo "== cargo-deny: $crate =="
|
|
cargo-deny --manifest-path "crates/$crate/Cargo.toml" \
|
|
--config deny.toml check
|
|
done
|
|
|
|
# Boundary enforcement (review item 1.4): the payment domain and storage
|
|
# crates must never depend on the UI framework. This checks real
|
|
# dependencies and imports, not prose in doc comments.
|
|
- name: Payment crates must not depend on Makepad
|
|
run: |
|
|
set -e
|
|
for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do
|
|
if grep -n -i 'makepad' "crates/$crate/Cargo.toml"; then
|
|
echo "Makepad dependency declared in $crate"
|
|
exit 1
|
|
fi
|
|
if grep -rn --include='*.rs' -E '^[[:space:]]*(use|extern crate)[[:space:]]+makepad' \
|
|
"crates/$crate"; then
|
|
echo "Makepad imported in $crate"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo 'no Makepad dependency in payment domain, storage or platform'
|
|
|
|
# Review item 5.1: JNI and platform SDK calls belong in the platform
|
|
# crate's adapters. The domain and storage crates must stay portable.
|
|
- name: Domain and storage must not reach the platform SDK
|
|
run: |
|
|
set -e
|
|
for crate in nigig-pay-domain nigig-pay-storage; do
|
|
if grep -rn --include='*.rs' -E '^[[:space:]]*use[[:space:]]+(jni|robius_ussd|robius_sms|robius_fingerprinting)' \
|
|
"crates/$crate"; then
|
|
echo "platform SDK imported in $crate"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo 'no platform SDK imports in payment domain or storage'
|
|
|
|
# Review item 5.4 / 0.5. The guard is a compile_error!, so the check is
|
|
# that the build genuinely fails. A passing build here means a release
|
|
# artifact could fabricate payment confirmations.
|
|
- name: Mock gateway must not compile into a release build
|
|
run: |
|
|
set -e
|
|
if cargo build --manifest-path crates/nigig-pay-platform/Cargo.toml \
|
|
--locked --release --features mock >/dev/null 2>&1; then
|
|
echo 'the mock gateway compiled into a release build'
|
|
exit 1
|
|
fi
|
|
echo 'mock gateway is correctly refused in release builds'
|
|
|
|
# Review item 7.3: the payment UI crate's tests must run in CI rather than
|
|
# being skipped. They were previously unrunnable here because the Makepad
|
|
# link step needs native libraries; `tools/makepad-native-libs.sh` now
|
|
# installs exactly the set required (including ALSA/PulseAudio/OpenSSL,
|
|
# which are needed to *link* a test binary even though `cargo check`
|
|
# succeeds without them).
|
|
payment-ui-tests:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install Makepad native libraries
|
|
run: ./tools/makepad-native-libs.sh --install
|
|
|
|
- name: Install the declared toolchain
|
|
run: |
|
|
set -e
|
|
version="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
rust-toolchain.toml | head -n 1)"
|
|
curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init
|
|
chmod 700 /tmp/rustup-init
|
|
/tmp/rustup-init -y --profile minimal --default-toolchain "$version" --no-modify-path
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
# The pure payment crates are gated far more strictly elsewhere in this
|
|
# workflow. Here the goal is that the widget-layer logic — fee
|
|
# breakdowns, CSV parsing, money formatting — is actually exercised.
|
|
- name: Payment UI unit tests
|
|
run: cargo test -p nigig-pay-ui --lib
|
|
|
|
# Review item 6.1 / Phase 6 exit criterion. A settlement tick may only
|
|
# follow a provider confirmation, so the payment status vocabulary is
|
|
# owned by `nigig_pay_domain::FlowStage` and must not be hand-written
|
|
# beside an untrusted signal. This catches the specific regression that
|
|
# shipped before: "✓ SMS received" and "✓ N/N sent".
|
|
- name: Payment status must not claim settlement from untrusted signals
|
|
run: |
|
|
set -e
|
|
sheet=crates/apps/nigig-pay-ui/src/shared_pay_sheet.rs
|
|
if grep -n '✓.*\(SMS\|sent\|Sent\|received\)' "$sheet"; then
|
|
echo 'a settlement tick is being claimed from an untrusted signal'
|
|
exit 1
|
|
fi
|
|
for marker in ObservedEvidence NeedsReconciliation; do
|
|
if grep -n "PayFlowEvent::$marker" -A 6 "$sheet" | grep -q '✓'; then
|
|
echo "PayFlowEvent::$marker renders a settlement tick"
|
|
exit 1
|
|
fi
|
|
done
|
|
echo 'no settlement claims from untrusted signals'
|
|
|
|
# Review items 6.6 / U7. Batch progress must be derived from
|
|
# `PaymentBatch`, which records one outcome per item, rather than from a
|
|
# bare counter incremented at each match arm. The unbounded version let
|
|
# a single item with two terminal USSD events report "3/2".
|
|
- name: Batch progress must not be an unbounded counter
|
|
run: |
|
|
set -e
|
|
handler=crates/apps/nigig-pay-ui/src/pay_flow/pay_flow_handler.rs
|
|
if grep -n '^[^/]*bulk_done[[:space:]]*+=' "$handler"; then
|
|
echo 'bulk_done is being incremented directly; use settle_bulk_item()'
|
|
exit 1
|
|
fi
|
|
echo 'batch progress is derived, not counted'
|
|
|
|
# Review defect S2, the fail-open biometric. On Android
|
|
# `robius_fingerprinting::authenticate` returns Ok(()) as soon as the
|
|
# prompt is *shown*, not when the user answers. Dispatch must therefore
|
|
# be gated on an AuthorizationAttempt grant, never on that early Ok.
|
|
- name: USSD dispatch must be gated on an authorization grant
|
|
run: |
|
|
set -e
|
|
handler=crates/apps/nigig-pay-ui/src/pay_flow/pay_flow_handler.rs
|
|
if ! grep -q 'attempt.consume()' "$handler"; then
|
|
echo 'dispatch_ussd no longer consumes an authorization grant'
|
|
exit 1
|
|
fi
|
|
if ! grep -q 'AuthorizationSignal::Succeeded' "$handler"; then
|
|
echo 'no explicit success signal feeds the authorization attempt'
|
|
exit 1
|
|
fi
|
|
echo 'dispatch is gated on an explicit authorization grant'
|
|
|
|
# The demo gate must be reachable by the obvious command. Without a
|
|
# forwarding feature on the app crates, `cargo run -p nigig-pay
|
|
# --features demo` fails with "does not contain this feature", and the
|
|
# only way in is the non-obvious `--features nigig-pay-ui/demo`. That
|
|
# made the intended tracker-only message look like a broken build.
|
|
- name: The demo feature must be reachable from the app crates
|
|
run: |
|
|
set -e
|
|
for app in nigig-pay nigig-mpesa; do
|
|
grep -q 'demo = \["nigig-pay-ui/demo"\]' "crates/apps/$app/Cargo.toml" \
|
|
|| { echo "$app does not forward the demo feature"; exit 1; }
|
|
done
|
|
cargo check -p nigig-pay --features demo
|
|
cargo check -p nigig-mpesa --features demo
|
|
echo 'demo feature is reachable from both app crates'
|
|
|
|
# A gated feature must explain the product boundary, not the binary.
|
|
# "unavailable in this build" was reported as a bug three times: it
|
|
# reads like something is broken and gives the user no next step.
|
|
# Checks displayed strings only, so the explanatory comments stay legal.
|
|
- name: Gated features must not report themselves as build errors
|
|
run: |
|
|
set -e
|
|
if grep -rn --include='*.rs' 'set_status(.*unavailable in this build' crates/; then
|
|
echo 'a user-facing string describes the build instead of the product'
|
|
exit 1
|
|
fi
|
|
echo 'gated features explain the product boundary'
|
|
|
|
# Review item 0.3: remove custom PIN capture from the default product.
|
|
# `form_pin` must not exist unless `demo` is enabled, so a stale UI
|
|
# definition, a hot reload, or a merely-hidden widget cannot populate
|
|
# it. Asserted by compiling a reference to the field and requiring the
|
|
# default build to reject it.
|
|
- name: The default build must not contain a PIN field
|
|
run: ./tools/check-no-pin-capture.sh
|
|
|
|
# Phase 4's draw_walk fixes live in these two crates. Compiling them is
|
|
# what makes that work verifiable rather than merely written.
|
|
- name: Payment app crates must compile
|
|
run: |
|
|
set -e
|
|
cargo check -p nigig-pay-ui
|
|
cargo check -p nigig-pay
|
|
cargo check -p nigig-mpesa
|