Skip to content

Commit 356b610

Browse files
authored
Merge pull request #18 from approov/feature/use-mini-sdk
Feature/use mini sdk
2 parents 20b9aca + b53ce80 commit 356b610

13 files changed

Lines changed: 1342 additions & 30 deletions

File tree

.github/workflows/build_and_publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ jobs:
2323
run: git config --global --add safe.directory '*'
2424

2525
- name: Checkout Repository
26-
uses: actions/checkout@v4
26+
uses: actions/checkout@v5
2727

2828
- name: Set Up Java
29-
uses: actions/setup-java@v4
29+
uses: actions/setup-java@v5
3030
with:
3131
distribution: 'temurin' # Use Eclipse Temurin distribution
3232
java-version: '21' # Use Java 21 for Android builds
@@ -80,7 +80,7 @@ jobs:
8080
run: cd .maven && ./maven-publish.sh
8181

8282
- name: Upload Artifact
83-
uses: actions/upload-artifact@v4
83+
uses: actions/upload-artifact@v7
8484
with:
8585
name: full-repo-artifact-${{ github.ref_name }}
8686
path: ${{ github.workspace }}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
name: Build and Test
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build-and-test:
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 45
11+
env:
12+
WORKSPACE: "${{ github.workspace }}"
13+
GIT_BRANCH: "${{ github.ref }}"
14+
CURRENT_TAG: "${{ github.ref_name }}"
15+
# Worker endpoints: consumed from GitHub organisation variables first.
16+
# If the org variable is not set the value is an empty string; the probe
17+
# step below falls back to the same hardcoded defaults that are baked into
18+
# the mini-sdk (MiniAttesterConfig / Approov.m), keeping them in sync.
19+
# Worker management (create / redeploy) logic is centralized in the
20+
# core-service-layers-testing script, but invoked here when needed.
21+
TESTING_REPLY_URL: ${{ vars.TESTING_REPLY_URL }}
22+
TESTING_REPLY_URL_UNPROTECTED: ${{ vars.TESTING_REPLY_URL_UNPROTECTED }}
23+
# Required for the redeploy script invocation on failure
24+
CLOUDFLARE_API_TOKEN_WORKERS_DEV: ${{ secrets.CLOUDFLARE_API_TOKEN_WORKERS_DEV }}
25+
CLOUDFLARE_ACCOUNT_ID_WORKERS_DEV: ${{ secrets.CLOUDFLARE_ACCOUNT_ID_WORKERS_DEV }}
26+
27+
steps:
28+
# -----------------------------------------------------------------------
29+
# 1. Checkout this repository
30+
# -----------------------------------------------------------------------
31+
- name: Set up Git
32+
run: git config --global --add safe.directory '*'
33+
34+
- name: Checkout approov-service-retrofit
35+
uses: actions/checkout@v5
36+
with:
37+
path: approov-service-retrofit
38+
39+
# -----------------------------------------------------------------------
40+
# 2. Clone core-service-layers-testing as a sibling directory
41+
# settings.gradle expects: ../core-service-layers-testing/...
42+
# so both repos must sit under the same parent ($GITHUB_WORKSPACE).
43+
# -----------------------------------------------------------------------
44+
- name: Checkout core-service-layers-testing
45+
uses: actions/checkout@v5
46+
with:
47+
repository: approov/core-service-layers-testing
48+
token: ${{ secrets.CORE_SERVICE_LAYERS_TESTING_PAT }}
49+
path: core-service-layers-testing
50+
51+
# -----------------------------------------------------------------------
52+
# 3. Verify worker endpoints (with auto-redeploy)
53+
#
54+
# URLs are resolved with the following precedence (mirrors the mini-sdk
55+
# fallback logic in MiniAttesterConfig / Approov.m):
56+
# 1. GitHub org variable (TESTING_REPLY_URL / _UNPROTECTED)
57+
# 2. Mini-SDK hardcoded defaults (replay.ivol.workers.dev / ...)
58+
#
59+
# If the workers are unavailable, this step invokes the management
60+
# script in core-service-layers-testing to redeploy them.
61+
# -----------------------------------------------------------------------
62+
- name: Verify worker endpoints
63+
run: |
64+
# ── URL resolution ──────────────────────────────────────────────────
65+
PROTECTED_URL="${TESTING_REPLY_URL:-https://replay.ivol.workers.dev}"
66+
UNPROTECTED_URL="${TESTING_REPLY_URL_UNPROTECTED:-https://replay-unprotected.ivol.workers.dev}"
67+
68+
echo "TESTING_REPLY_URL=$PROTECTED_URL" >> "$GITHUB_ENV"
69+
echo "TESTING_REPLY_URL_UNPROTECTED=$UNPROTECTED_URL" >> "$GITHUB_ENV"
70+
71+
# ── Probe function ──────────────────────────────────────────────────
72+
probe_worker() {
73+
local url="$1"
74+
curl --silent --show-error --fail --max-time 10 \
75+
-X POST "$url" \
76+
-H "Content-Type: application/json" \
77+
-d '{"check":"probe"}' | grep -q '"body"'
78+
}
79+
80+
echo "==> Probing workers..."
81+
if probe_worker "$PROTECTED_URL" && probe_worker "$UNPROTECTED_URL"; then
82+
echo " All workers are healthy."
83+
exit 0
84+
fi
85+
86+
echo " WARNING: One or more workers are down. Attempting redeploy..."
87+
88+
# ── Invoke redeploy script ──────────────────────────────────────────
89+
# The script is in the sibling directory cloned in Step 2.
90+
# It uses the CLOUDFLARE_* env variables defined at the job level.
91+
./core-service-layers-testing/cloudflare-workers/redeploy-workers.sh
92+
93+
echo "==> Verifying after redeploy..."
94+
# Try up to 3 times with a 5s delay between attempts
95+
for i in {1..3}; do
96+
if probe_worker "$PROTECTED_URL" && probe_worker "$UNPROTECTED_URL"; then
97+
echo " Workers successfully restored."
98+
exit 0
99+
fi
100+
echo " Waiting for propagation (attempt $i/3)..."
101+
sleep 5
102+
done
103+
104+
echo "ERROR: Workers are still unreachable after redeployment effort."
105+
exit 1
106+
107+
# -----------------------------------------------------------------------
108+
# 4. Java / Android toolchain setup
109+
# -----------------------------------------------------------------------
110+
- name: Set Up Java
111+
uses: actions/setup-java@v5
112+
with:
113+
distribution: 'temurin'
114+
java-version: '21'
115+
116+
- name: Install Android SDK command-line tools
117+
run: |
118+
sudo apt-get update -q
119+
sudo apt-get install -y -q unzip curl
120+
mkdir -p "$ANDROID_HOME/cmdline-tools"
121+
curl -o android-sdk.zip \
122+
https://dl.google.com/android/repository/commandlinetools-linux-9123335_latest.zip
123+
unzip -q android-sdk.zip -d "$ANDROID_HOME/cmdline-tools"
124+
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools"
125+
rm android-sdk.zip
126+
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
127+
echo "$ANDROID_HOME/cmdline-tools/tools/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator" >> "$GITHUB_PATH"
128+
129+
- name: Accept Android SDK licenses
130+
run: yes | sdkmanager --licenses || true
131+
132+
- name: Install required Android SDK packages
133+
run: |
134+
sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0"
135+
136+
# -----------------------------------------------------------------------
137+
# 5. Build and run tests
138+
# Gradle is invoked from inside the checked-out service directory.
139+
# The mini-sdk is already available via the sibling path wired into
140+
# settings.gradle — no extra configuration needed here.
141+
# -----------------------------------------------------------------------
142+
- name: Build AAR
143+
working-directory: approov-service-retrofit
144+
run: ./gradlew assembleRelease
145+
146+
- name: Run unit tests
147+
working-directory: approov-service-retrofit
148+
env:
149+
TESTING_REPLY_URL: ${{ env.TESTING_REPLY_URL }}
150+
TESTING_REPLY_URL_UNPROTECTED: ${{ env.TESTING_REPLY_URL_UNPROTECTED }}
151+
run: ./gradlew test
152+
153+
# -----------------------------------------------------------------------
154+
# 6. Print test summary to the console and to the Actions Job Summary
155+
# Parses every JUnit XML produced by Gradle and emits:
156+
# • a formatted table in the step log
157+
# • a markdown table in the Job Summary (Actions UI → Summary tab)
158+
# -----------------------------------------------------------------------
159+
- name: Print test summary
160+
if: always()
161+
working-directory: approov-service-retrofit
162+
run: |
163+
python3 - <<'EOF'
164+
import os, sys, glob, xml.etree.ElementTree as ET
165+
from collections import defaultdict
166+
167+
PASS = "\u2705"
168+
FAIL = "\u274c"
169+
SKIP = "\u23ed\ufe0f"
170+
ERROR = "\u26a0\ufe0f"
171+
172+
results_root = "approov-service/build/test-results"
173+
xml_files = sorted(glob.glob(f"{results_root}/**/*.xml", recursive=True))
174+
175+
if not xml_files:
176+
print("No test-result XML files found.")
177+
sys.exit(0)
178+
179+
# Group suites by variant directory name (testDebugUnitTest, etc.)
180+
by_variant = defaultdict(list)
181+
for path in xml_files:
182+
variant = os.path.basename(os.path.dirname(path))
183+
try:
184+
root = ET.parse(path).getroot()
185+
except ET.ParseError:
186+
continue
187+
suite = {
188+
"name": root.get("name", os.path.basename(path)),
189+
"tests": int(root.get("tests", 0)),
190+
"failures": int(root.get("failures", 0)),
191+
"errors": int(root.get("errors", 0)),
192+
"skipped": int(root.get("skipped", 0)),
193+
"time": float(root.get("time", 0)),
194+
"cases": [],
195+
}
196+
for tc in root.findall("testcase"):
197+
status = PASS
198+
detail = ""
199+
if tc.find("failure") is not None:
200+
status = FAIL
201+
detail = (tc.find("failure").get("message") or "").split("\n")[0][:120]
202+
elif tc.find("error") is not None:
203+
status = ERROR
204+
detail = (tc.find("error").get("message") or "").split("\n")[0][:120]
205+
elif tc.find("skipped") is not None:
206+
status = SKIP
207+
suite["cases"].append({
208+
"name": tc.get("name", "?"),
209+
"time": float(tc.get("time", 0)),
210+
"status": status,
211+
"detail": detail,
212+
})
213+
by_variant[variant].append(suite)
214+
215+
# ── Console output ──────────────────────────────────────────────────
216+
overall_ok = True
217+
for variant, suites in sorted(by_variant.items()):
218+
total_t = sum(s["tests"] for s in suites)
219+
total_f = sum(s["failures"] + s["errors"] for s in suites)
220+
total_s = sum(s["skipped"] for s in suites)
221+
total_p = total_t - total_f - total_s
222+
icon = PASS if total_f == 0 else FAIL
223+
if total_f > 0:
224+
overall_ok = False
225+
print()
226+
print(f"{'='*70}")
227+
print(f" {icon} {variant} | {total_p}/{total_t} passed "
228+
f"| {total_f} failed | {total_s} skipped")
229+
print(f"{'='*70}")
230+
for suite in suites:
231+
short = suite["name"].split(".")[-1]
232+
p = suite["tests"] - suite["failures"] - suite["errors"] - suite["skipped"]
233+
f = suite["failures"] + suite["errors"]
234+
print(f" {PASS if f==0 else FAIL} {short:<55} {p}/{suite['tests']} ({suite['time']:.2f}s)")
235+
for case in suite["cases"]:
236+
if case["status"] != PASS:
237+
print(f" {case['status']} {case['name']}")
238+
if case["detail"]:
239+
print(f" {case['detail']}")
240+
print()
241+
242+
# ── GitHub Job Summary (markdown) ───────────────────────────────────
243+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY", "")
244+
if not summary_path:
245+
sys.exit(0 if overall_ok else 1)
246+
247+
with open(summary_path, "a") as md:
248+
md.write("## \U0001f9ea Unit Test Results\n\n")
249+
for variant, suites in sorted(by_variant.items()):
250+
total_t = sum(s["tests"] for s in suites)
251+
total_f = sum(s["failures"] + s["errors"] for s in suites)
252+
total_s = sum(s["skipped"] for s in suites)
253+
total_p = total_t - total_f - total_s
254+
badge = "\U0001f7e2 PASSED" if total_f == 0 else "\U0001f534 FAILED"
255+
md.write(f"### {variant} &nbsp; {badge}\n\n")
256+
md.write(f"> **{total_p} passed** &nbsp;|&nbsp; "
257+
f"**{total_f} failed** &nbsp;|&nbsp; "
258+
f"**{total_s} skipped** &nbsp;|&nbsp; "
259+
f"**{total_t} total**\n\n")
260+
md.write("| Status | Test suite | Tests | Failed | Skipped | Time |\n")
261+
md.write("|--------|------------|------:|-------:|--------:|-----:|\n")
262+
for suite in suites:
263+
short = suite["name"].split(".")[-1]
264+
f = suite["failures"] + suite["errors"]
265+
icon = "\U0001f7e2" if f == 0 else "\U0001f534"
266+
md.write(f"| {icon} | `{short}` | {suite['tests']} | {f} | {suite['skipped']} | {suite['time']:.2f}s |\n")
267+
# Expand failures inline
268+
failures = [c for s in suites for c in s["cases"] if c["status"] in (FAIL, ERROR)]
269+
if failures:
270+
md.write("\n<details><summary>Failed tests</summary>\n\n")
271+
for c in failures:
272+
md.write(f"- {c['status']} `{c['name']}`")
273+
if c["detail"]:
274+
md.write(f" \n > {c['detail']}")
275+
md.write("\n")
276+
md.write("\n</details>\n")
277+
md.write("\n")
278+
279+
sys.exit(0 if overall_ok else 1)
280+
EOF
281+
282+
# -----------------------------------------------------------------------
283+
# 7. Upload HTML reports as a downloadable artifact
284+
# -----------------------------------------------------------------------
285+
- name: Upload test results
286+
if: always()
287+
uses: actions/upload-artifact@v7
288+
with:
289+
name: test-results-${{ github.run_number }}
290+
path: approov-service-retrofit/approov-service/build/reports/tests/
291+
retention-days: 14

.github/workflows/build_only.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ jobs:
2323
run: git config --global --add safe.directory '*'
2424

2525
- name: Checkout Repository
26-
uses: actions/checkout@v4
26+
uses: actions/checkout@v5
2727

2828
- name: Set Up Java
29-
uses: actions/setup-java@v4
29+
uses: actions/setup-java@v5
3030
with:
3131
distribution: 'temurin' # Use Eclipse Temurin distribution
3232
java-version: '21' # Use Java 21 for Android builds

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ All notable changes to this package will be documented in this file.
55
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
66

77

8+
## [3.5.6] - 2026-04-21
9+
10+
### Added
11+
- Thread-safe failure mode caching for the interceptor path. When the platform SDK returns a failure status (`NO_NETWORK`, `POOR_NETWORK`, `MITM_DETECTED`, `NO_APPROOV_SERVICE`), the result is cached for 0.5 seconds. Subsequent requests within that window return the cached failure instantly, avoiding redundant ~1s SDK calls. Success is never cached.
12+
- Added `ApproovService.setFailureCacheTtlMs()` to customize the caching duration of failure statuses in milliseconds.
13+
- Added `ApproovService.isInitialized()` to expose the service-layer initialization state.
14+
- Integrated a localized testing framework for comprehensive service layer verification.
15+
- Added extensive test coverage for core service flows, including initialization, token management, and request mutation.
16+
17+
### Changed
18+
- Initializing with an empty config string now keeps the service layer initialized while forwarding requests without Approov processing.
19+
- Initializing first with an empty config string and later with a valid non-empty config string now enables Approov for newly obtained Retrofit instances instead of being rejected as a different-configuration initialization.
20+
- Tightened the initialization guard so only actual `reinit...` comments bypass same-config enforcement.
21+
22+
### Fixed
23+
- Added explicit cross-service-layer initialization handling so a benign same-config already-initialized native SDK outcome is tolerated, while real different-configuration failures still surface as initialization errors.
24+
- Updated the build manifest to support flexible dependency resolution for verification suites.
25+
826
## [3.5.5] - 2026-03-25
927

1028
### Added
@@ -23,4 +41,4 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver
2341
### Deprecated
2442
- ApproovInterceptorExtensions in favor of ApproovServiceMutator.
2543
- setProceedOnNetworkFail() and getProceedOnNetworkFail() in favor of setServiceMutator.
26-
- prefetch() is now automatically called when the service is initialized.
44+
- prefetch() is obsolete and is now a no-op. The underlying Approov SDK manages prefetching automatically.

0 commit comments

Comments
 (0)