Skip to content

Commit a8f486e

Browse files
authored
chore(kotlin): Setup e2e tests for Kotlin (#103)
1 parent 0f51f66 commit a8f486e

34 files changed

Lines changed: 1528 additions & 88 deletions

File tree

.github/workflows/native-sdk-e2e.yml

Lines changed: 210 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ jobs:
2424
name: Detect Changes
2525
runs-on: ubuntu-latest
2626
outputs:
27-
source-changed: ${{ steps.check.outputs.source-changed }}
27+
# Per-platform flags so a change to one language's SDK doesn't fan out to
28+
# the others. `any-changed` gates the shared setup/gate jobs.
29+
dart-changed: ${{ steps.check.outputs.dart-changed }}
30+
swift-changed: ${{ steps.check.outputs.swift-changed }}
31+
kotlin-changed: ${{ steps.check.outputs.kotlin-changed }}
32+
any-changed: ${{ steps.check.outputs.any-changed }}
2833
steps:
2934
- uses: actions/checkout@v5
3035
with:
@@ -33,18 +38,39 @@ jobs:
3338
- id: check
3439
run: |
3540
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
36-
echo "source-changed=true" >> "$GITHUB_OUTPUT"
37-
elif git diff --name-only origin/main...HEAD | grep -qE '^(native/(dart|kotlin|swift)|test-apps/native-bindings)/|^\.github/workflows/native-sdk-e2e\.yml$'; then
38-
echo "source-changed=true" >> "$GITHUB_OUTPUT"
41+
dart=true; swift=true; kotlin=true
3942
else
40-
echo "source-changed=false" >> "$GITHUB_OUTPUT"
43+
CHANGED="$(git diff --name-only origin/main...HEAD)"
44+
# Shared inputs affect EVERY platform: the test backend that
45+
# generates the OpenRPC spec all SDKs are code-genned from, and this
46+
# workflow file itself. A change here forces all platforms to run.
47+
shared=false
48+
if echo "$CHANGED" | grep -qE '^test-apps/native-bindings/|^\.github/workflows/native-sdk-e2e\.yml$'; then
49+
shared=true
50+
fi
51+
# Each platform runs when a shared input changed OR its own SDK dir
52+
# changed — so a Dart-only change no longer triggers Kotlin/Swift.
53+
dart=$shared; swift=$shared; kotlin=$shared
54+
if echo "$CHANGED" | grep -qE '^native/dart/'; then dart=true; fi
55+
if echo "$CHANGED" | grep -qE '^native/swift/'; then swift=true; fi
56+
if echo "$CHANGED" | grep -qE '^native/kotlin/'; then kotlin=true; fi
57+
fi
58+
any=false
59+
if [ "$dart" = true ] || [ "$swift" = true ] || [ "$kotlin" = true ]; then
60+
any=true
4161
fi
62+
{
63+
echo "dart-changed=$dart"
64+
echo "swift-changed=$swift"
65+
echo "kotlin-changed=$kotlin"
66+
echo "any-changed=$any"
67+
} >> "$GITHUB_OUTPUT"
4268
4369
# Shared setup: build monorepo, generate spec, upload artifact
4470
setup:
4571
name: Build & Generate Spec
4672
needs: [detect-changes]
47-
if: needs.detect-changes.outputs.source-changed == 'true'
73+
if: needs.detect-changes.outputs.any-changed == 'true'
4874
runs-on: ubuntu-latest
4975
timeout-minutes: 10
5076
outputs:
@@ -81,7 +107,7 @@ jobs:
81107
dart-e2e:
82108
name: Dart E2E
83109
needs: [detect-changes, setup]
84-
if: needs.detect-changes.outputs.source-changed == 'true'
110+
if: needs.detect-changes.outputs.dart-changed == 'true'
85111
runs-on: ubuntu-latest
86112
timeout-minutes: 15
87113
steps:
@@ -164,17 +190,74 @@ jobs:
164190
# BLOCKS_URL is provided via $GITHUB_ENV from the "Pick a free port" step.
165191
run: dart run bin/e2e_test.dart
166192

167-
# Kotlin E2E (TODO: add when ready)
168-
# kotlin-e2e:
169-
# name: Kotlin E2E
170-
# needs: [detect-changes, setup]
171-
# if: needs.detect-changes.outputs.source-changed == 'true'
172-
# ...
193+
kotlin-e2e:
194+
name: Kotlin E2E (${{ matrix.label }})
195+
needs: [detect-changes, setup]
196+
if: needs.detect-changes.outputs.kotlin-changed == 'true'
197+
runs-on: ${{ matrix.runs-on }}
198+
timeout-minutes: ${{ matrix.timeout }}
199+
strategy:
200+
fail-fast: false
201+
matrix:
202+
include:
203+
- label: JVM
204+
runs-on: ubuntu-latest
205+
gradle-task: jvmTest
206+
timeout: 15
207+
- label: iOS
208+
runs-on: macos-15
209+
gradle-task: iosSimulatorArm64Test
210+
timeout: 20
211+
steps:
212+
- uses: actions/checkout@v5
213+
with:
214+
ref: ${{ github.event.pull_request.head.sha }}
215+
persist-credentials: false
216+
217+
- uses: actions/setup-node@v5
218+
with:
219+
node-version-file: '.nvmrc'
220+
cache: npm
221+
222+
- uses: actions/setup-java@v4
223+
with:
224+
distribution: temurin
225+
java-version: 17
226+
227+
- run: npm ci
228+
- run: npm run build
229+
230+
- name: Download spec
231+
uses: actions/download-artifact@v4
232+
with:
233+
name: blocks-spec
234+
path: native/kotlin/e2e/
235+
236+
- name: Run Kotlin codegen
237+
working-directory: native/kotlin/e2e
238+
run: ./gradlew awsBlocksCodegen
239+
240+
- name: Start native-bindings server
241+
working-directory: test-apps/native-bindings
242+
run: |
243+
npx tsx aws-blocks/scripts/server.ts &
244+
for i in $(seq 1 30); do
245+
curl -s -X POST http://localhost:3001/aws-blocks/api \
246+
-H "Content-Type: application/json" \
247+
-d '{"jsonrpc":"2.0","method":"api.kvGet","params":{"key":"healthcheck"},"id":1}' && break
248+
sleep 1
249+
done
250+
251+
- name: Run E2E tests
252+
working-directory: native/kotlin/e2e
253+
env:
254+
BLOCKS_URL: http://localhost:3001/aws-blocks/api
255+
run: ./gradlew ${{ matrix.gradle-task }}
173256

174257
swift-e2e:
175258
name: Swift E2E
176259
needs: [detect-changes, setup]
177-
if: needs.detect-changes.outputs.source-changed == 'true'
260+
if: needs.detect-changes.outputs.swift-changed == 'true'
178261
runs-on: macos-15
179262
timeout-minutes: 20
180263
steps:
@@ -265,7 +348,7 @@ jobs:
265348
name: Dart E2E (sandbox)
266349
needs: [detect-changes, dart-e2e]
267350
if: >-
268-
needs.detect-changes.outputs.source-changed == 'true' &&
351+
needs.detect-changes.outputs.dart-changed == 'true' &&
269352
(github.event_name == 'workflow_dispatch' ||
270353
github.event.pull_request.head.repo.full_name == github.repository)
271354
runs-on: ubuntu-latest
@@ -403,7 +486,7 @@ jobs:
403486
name: Swift E2E (sandbox)
404487
needs: [detect-changes, swift-e2e]
405488
if: >-
406-
needs.detect-changes.outputs.source-changed == 'true' &&
489+
needs.detect-changes.outputs.swift-changed == 'true' &&
407490
(github.event_name == 'workflow_dispatch' ||
408491
github.event.pull_request.head.repo.full_name == github.repository)
409492
runs-on: macos-15
@@ -497,31 +580,131 @@ jobs:
497580
working-directory: test-apps/native-bindings
498581
run: npm run destroy || true
499582

583+
kotlin-e2e-sandbox:
584+
name: Kotlin E2E (${{ matrix.label }}, sandbox)
585+
needs: [detect-changes, kotlin-e2e]
586+
if: >-
587+
needs.detect-changes.outputs.kotlin-changed == 'true' &&
588+
(github.event_name == 'workflow_dispatch' ||
589+
github.event.pull_request.head.repo.full_name == github.repository)
590+
runs-on: ${{ matrix.runs-on }}
591+
timeout-minutes: 30
592+
environment: publish
593+
strategy:
594+
fail-fast: false
595+
matrix:
596+
include:
597+
- label: JVM
598+
suffix: jvm
599+
runs-on: ubuntu-latest
600+
gradle-task: jvmTest
601+
- label: iOS
602+
suffix: ios
603+
runs-on: macos-15
604+
gradle-task: iosSimulatorArm64Test
605+
env:
606+
BLOCKS_STACK_SUFFIX: native-kotlin-${{ matrix.suffix }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.run_attempt }}
607+
steps:
608+
- uses: actions/checkout@v5
609+
with:
610+
ref: ${{ github.event.pull_request.head.sha }}
611+
persist-credentials: false
612+
613+
- uses: actions/setup-node@v5
614+
with:
615+
node-version-file: '.nvmrc'
616+
cache: npm
617+
618+
- uses: actions/setup-java@v4
619+
with:
620+
distribution: temurin
621+
java-version: 17
622+
623+
- run: npm ci
624+
- run: npm run build
625+
626+
- name: Configure AWS credentials
627+
uses: aws-actions/configure-aws-credentials@v6
628+
with:
629+
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
630+
aws-region: us-east-1
631+
632+
- name: Deploy sandbox
633+
working-directory: test-apps/native-bindings
634+
run: npm run deploy
635+
636+
- name: Resolve BLOCKS_URL
637+
id: url
638+
working-directory: test-apps/native-bindings
639+
run: |
640+
URL=$(python3 -c "import json; print(json.load(open('.blocks-sandbox/config.json'))['apiUrl'])")
641+
echo "blocks_url=${URL}/aws-blocks/api" >> $GITHUB_OUTPUT
642+
643+
- name: Warm up sandbox API
644+
env:
645+
BLOCKS_URL: ${{ steps.url.outputs.blocks_url }}
646+
run: |
647+
attempts=24
648+
delay=2
649+
for i in $(seq 1 "$attempts"); do
650+
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
651+
-X POST -H 'content-type: application/json' -d '{}' \
652+
"$BLOCKS_URL" 2>/dev/null || echo 000)
653+
if [ "$code" = "200" ]; then
654+
echo "✅ Sandbox API warm after $i attempt(s) (http=200)."
655+
exit 0
656+
fi
657+
echo "⏳ attempt $i/$attempts: API not ready (http=$code); retrying in ${delay}s..."
658+
sleep "$delay"
659+
if [ "$delay" -lt 10 ]; then delay=$((delay + 2)); fi
660+
done
661+
echo "❌ Sandbox API did not return HTTP 200 after $attempts attempts."
662+
exit 1
663+
664+
- name: Generate OpenRPC spec
665+
working-directory: test-apps/native-bindings
666+
run: npm run spec
667+
668+
- name: Download spec
669+
uses: actions/download-artifact@v4
670+
with:
671+
name: blocks-spec
672+
path: native/kotlin/e2e/
673+
674+
- name: Run Kotlin codegen
675+
working-directory: native/kotlin/e2e
676+
run: ./gradlew awsBlocksCodegen
677+
678+
- name: Run Kotlin E2E against sandbox
679+
working-directory: native/kotlin/e2e
680+
env:
681+
BLOCKS_URL: ${{ steps.url.outputs.blocks_url }}
682+
run: ./gradlew ${{ matrix.gradle-task }}
683+
684+
- name: Destroy sandbox
685+
if: always()
686+
working-directory: test-apps/native-bindings
687+
run: npm run destroy || true
688+
500689
# Aggregate gate so this can be a single required status check in branch
501690
# protection. Always runs (even when the e2e jobs are skipped for unrelated
502691
# or fork PRs) and only fails if a job that actually ran failed.
503692
native-sdk-e2e-required:
504693
name: Native SDK E2E (Required)
505694
if: always()
506-
needs: [detect-changes, setup, dart-e2e, swift-e2e]
695+
needs: [detect-changes, setup, dart-e2e, kotlin-e2e, swift-e2e]
507696
runs-on: ubuntu-latest
508697
steps:
509698
- name: Check results
510699
run: |
511-
echo "detect-changes=${{ needs.detect-changes.result }} setup=${{ needs.setup.result }} dart-e2e=${{ needs.dart-e2e.result }} swift-e2e=${{ needs.swift-e2e.result }}"
512-
# Required gate = SDK correctness: detect-changes + setup + the local
513-
# dart-e2e suite (no AWS). The deploying `dart-e2e-sandbox` job still
514-
# runs as a NON-BLOCKING signal — it depends on deployed-backend config
515-
# that isn't part of the SDK (real-Cognito email code delivery, which a
516-
# dev-only `cognitoGetLastCode` hook can't satisfy; and fresh-deploy
517-
# OIDC state). The OIDC relay suite was verified 5/5 against a live
518-
# sandbox, so this is environment, not SDK. Re-add `dart-e2e-sandbox`
519-
# to this gate once the deployed Cognito sign-in path is testable.
520-
# Skipped jobs (no source changes) are treated as passing.
700+
echo "detect-changes=${{ needs.detect-changes.result }} setup=${{ needs.setup.result }} dart-e2e=${{ needs.dart-e2e.result }} kotlin-e2e=${{ needs.kotlin-e2e.result }} swift-e2e=${{ needs.swift-e2e.result }}"
701+
# Required gate = SDK correctness. Skipped jobs (no source changes)
702+
# are treated as passing.
521703
for result in \
522704
"${{ needs.detect-changes.result }}" \
523705
"${{ needs.setup.result }}" \
524706
"${{ needs.dart-e2e.result }}" \
707+
"${{ needs.kotlin-e2e.result }}" \
525708
"${{ needs.swift-e2e.result }}"; do
526709
if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
527710
echo "A required native SDK E2E job failed (or was cancelled)."

native/dart/example/bin/e2e/realtime_test.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ void main() async {
2323
group('Realtime: subscribe and receive');
2424
final ch = await blocks.api.realtimeGetChannel(channel: 'dart-test');
2525
final stream = ch.subscribe();
26-
final completer = Completer<dynamic>();
26+
final completer = Completer<Cursor>();
2727

2828
final sub = stream.listen((msg) {
2929
if (!completer.isCompleted) completer.complete(msg);
@@ -38,13 +38,10 @@ void main() async {
3838

3939
try {
4040
final msg = await completer.future.timeout(Duration(seconds: 5));
41-
check(msg != null, 'received message via WebSocket');
42-
if (msg is Map<String, dynamic>) {
43-
check(msg['userId'] == 'dart-sub-test', 'message userId matches');
44-
check(msg['x'] == 42, 'message x matches');
45-
} else {
46-
check(true, 'received message (type: ${msg.runtimeType})');
47-
}
41+
check(msg.userId == 'dart-sub-test', 'message userId matches');
42+
check(msg.x == 42, 'message x matches');
43+
check(msg.y == 99, 'message y matches');
44+
check(msg.color == '#00ff00', 'message color matches');
4845
} on TimeoutException {
4946
check(false, 'WebSocket message received within 5s (timed out)');
5047
} finally {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"aws-blocks-kotlin": patch
3+
---
4+
5+
Fix handling of keychain in the iOS runtime
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"aws-blocks-kotlin": minor
3+
---
4+
5+
Bump Kotlin version to 2.2.10
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"aws-blocks-kotlin": minor
3+
---
4+
5+
Add ability to clear cookies

native/kotlin/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# AWS Blocks Kotlin
22

33
[![Maven Central](https://img.shields.io/maven-central/v/com.aws.blocks.kotlin/runtime)](https://central.sonatype.com/search?namespace=com.aws.blocks.kotlin)
4-
[![Kotlin](https://img.shields.io/badge/kotlin-2.1.21-blue.svg?logo=kotlin)](https://kotlinlang.org)
4+
[![Kotlin](https://img.shields.io/badge/kotlin-2.2.10-blue.svg?logo=kotlin)](https://kotlinlang.org)
55
![Android](http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat)
66
![iOS](http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat)
77
![Desktop](http://img.shields.io/badge/platform-desktop-DB413D.svg?style=flat)
@@ -106,7 +106,7 @@ See the [`example/android`](example/android) directory for a complete Android ap
106106

107107
## Requirements
108108

109-
- Kotlin 2.x
109+
- Kotlin 2.1+
110110
- JDK 17+
111111
- Gradle 7.4+
112112
- Android Gradle Plugin 7.1+ (for Android targets)

native/kotlin/e2e/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
build/
2+
.gradle/
3+
blocks.spec.json

0 commit comments

Comments
 (0)