Skip to content

Commit 37dd583

Browse files
committed
add behavioural unit tests and cross-repo integration CI job
1 parent 2ce5554 commit 37dd583

4 files changed

Lines changed: 397 additions & 3 deletions

File tree

.github/workflows/main.yml

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,174 @@ jobs:
135135
testLabels: websocketclient
136136
testAdditional: ${{ github.workspace }}/tests
137137

138+
test-integration:
139+
# Runs the integration test suite that lives in the server repo's tests/integration/
140+
# folder against this branch's freshly-built client .lex. Sparse-checkouts only the
141+
# integration subtree so we don't pull the whole server repo.
142+
runs-on: ubuntu-latest
143+
needs: [build]
144+
strategy:
145+
fail-fast: false
146+
matrix:
147+
lucee: [ "6.2/stable/jar", "7.0/stable/jar" ]
148+
steps:
149+
- uses: actions/checkout@v6
150+
151+
- name: Determine Tomcat and Java version
152+
id: config
153+
run: |
154+
MAJOR_MINOR=$(echo "${{ matrix.lucee }}" | grep -oE '^[0-9]+\.[0-9]+')
155+
case "$MAJOR_MINOR" in
156+
6.2)
157+
echo "tomcat=tomcat-9" >> $GITHUB_OUTPUT
158+
echo "java=11" >> $GITHUB_OUTPUT
159+
;;
160+
7.0|7.1)
161+
echo "tomcat=tomcat-11" >> $GITHUB_OUTPUT
162+
echo "java=21" >> $GITHUB_OUTPUT
163+
;;
164+
*)
165+
echo "ERROR: Unknown Lucee major version: $MAJOR_MINOR"; exit 1 ;;
166+
esac
167+
168+
- name: Set up JDK ${{ steps.config.outputs.java }}
169+
uses: actions/setup-java@v5
170+
with:
171+
java-version: '${{ steps.config.outputs.java }}'
172+
distribution: 'temurin'
173+
174+
- name: Download this branch's client .lex
175+
uses: actions/download-artifact@v8
176+
with:
177+
name: websocketclient-lex
178+
path: client-lex
179+
180+
- name: Sparse-checkout server tests
181+
uses: actions/checkout@v6
182+
with:
183+
repository: lucee/extension-websocket
184+
path: server-repo
185+
sparse-checkout: |
186+
tests
187+
sparse-checkout-cone-mode: false
188+
189+
- name: Download latest server extension
190+
run: |
191+
# Pull the latest snapshot — follows the same naming convention the server repo uses
192+
curl -L -o server.lex "https://ext.lucee.org/websocket-extension-3.0.0.20-SNAPSHOT.lex"
193+
ls -la server.lex
194+
195+
- name: Download Lucee Express template
196+
run: |
197+
EXPRESS_URL=$(curl -s https://update.lucee.org/rest/update/provider/expressTemplates | jq -r '.["${{ steps.config.outputs.tomcat }}"]')
198+
curl -L -o express-template.zip "$EXPRESS_URL"
199+
unzip -q express-template.zip -d lucee-express
200+
201+
- name: Download Lucee JAR
202+
id: lucee-jar
203+
run: |
204+
LUCEE_URL=$(curl -s "https://update.lucee.org/rest/update/provider/latest/${{ matrix.lucee }}/url" | tr -d '"')
205+
LUCEE_VERSION=$(basename "$LUCEE_URL" | sed 's/lucee-//;s/\.jar//')
206+
echo "LUCEE_VERSION=$LUCEE_VERSION" >> $GITHUB_OUTPUT
207+
curl -L -f -o lucee.jar "$LUCEE_URL"
208+
rm -f lucee-express/lib/lucee-*.jar
209+
cp lucee.jar lucee-express/lib/
210+
211+
- name: Install both extensions into Express
212+
run: |
213+
mkdir -p lucee-express/lucee-server/deploy
214+
cp client-lex/*.lex lucee-express/lucee-server/deploy/
215+
cp server.lex lucee-express/lucee-server/deploy/
216+
ls -la lucee-express/lucee-server/deploy/
217+
218+
- name: Copy listeners and tests from server repo
219+
run: |
220+
mkdir -p lucee-express/lucee-server/context/websockets
221+
find server-repo/tests -path '*/websockets/*.cfc' -exec cp {} lucee-express/lucee-server/context/websockets/ \;
222+
cp -r server-repo/tests lucee-express/webapps/ROOT/
223+
ls -la lucee-express/lucee-server/context/websockets/
224+
225+
- name: Configure Tomcat port
226+
run: sed -i 's/port="8080"/port="8888"/g' lucee-express/conf/server.xml
227+
228+
- name: Start Lucee Express
229+
run: |
230+
cd lucee-express && ./bin/catalina.sh start
231+
232+
- name: Wait for server
233+
run: |
234+
for i in {1..60}; do
235+
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/ | grep -q "200\|302\|404"; then
236+
echo "HTTP ready after $i seconds"; break
237+
fi
238+
sleep 1
239+
done
240+
241+
- name: Run integration tests
242+
id: integration-tests
243+
continue-on-error: true
244+
run: |
245+
FAILED=false
246+
for test in test-websocket-client test-lifecycle-callbacks test-return-value-send test-wsclient-broadcast test-wsclients-plural test-binary-send; do
247+
echo ""
248+
echo "=== Running ${test} ==="
249+
RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/${test}.cfm)
250+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
251+
BODY=$(echo "$RESPONSE" | sed '$d')
252+
echo "$BODY"
253+
echo "$BODY" > /tmp/${test}.txt
254+
if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
255+
echo "${test}: ❌ FAILED"
256+
FAILED=true
257+
else
258+
echo "${test}: ✅ PASSED"
259+
fi
260+
done
261+
if [ "$FAILED" = "true" ]; then
262+
echo "INTEGRATION_FAILED=true" >> $GITHUB_OUTPUT
263+
fi
264+
265+
- name: Dump WebSocket event logs
266+
if: always()
267+
run: |
268+
echo "=== WebSocket Test Event Logs ==="
269+
found=false
270+
for f in /tmp/ws-*-events.log; do
271+
[ -f "$f" ] || continue
272+
found=true
273+
echo ""
274+
echo "--- $f ---"
275+
cat "$f"
276+
done
277+
if [ "$found" = "false" ]; then
278+
echo "(no event logs found)"
279+
fi
280+
281+
- name: Stop Lucee Express
282+
if: always()
283+
run: cd lucee-express && ./bin/shutdown.sh || true
284+
285+
- name: Show catalina.out
286+
if: always()
287+
run: cat lucee-express/logs/catalina.out || echo "No catalina.out found"
288+
289+
- name: Upload logs
290+
if: always()
291+
uses: actions/upload-artifact@v7
292+
with:
293+
name: lucee-logs-${{ steps.lucee-jar.outputs.LUCEE_VERSION }}
294+
path: |
295+
lucee-express/logs/
296+
lucee-express/lucee-server/context/logs/
297+
298+
- name: Fail if integration tests failed
299+
if: steps.integration-tests.outputs.INTEGRATION_FAILED == 'true'
300+
run: exit 1
301+
138302
deploy:
139303
runs-on: ubuntu-latest
140-
needs: [setup, test]
141-
if: always() && needs.test.result == 'success' && github.ref == 'refs/heads/master' && !inputs.dry-run
304+
needs: [setup, test, test-integration]
305+
if: always() && needs.test.result == 'success' && needs.test-integration.result == 'success' && github.event_name == 'workflow_dispatch' && !inputs.dry-run
142306
steps:
143307
- name: Checkout repository
144308
uses: actions/checkout@v6

tests/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# WebSocket Client Extension — Tests
2+
3+
## Two test tiers
4+
5+
### Unit tests (this folder)
6+
7+
`WebSocketClientTest.cfc` extends `org.lucee.cfml.test.LuceeTestCase` and exercises the BIF's own contract — no WebSocket server required:
8+
9+
- `CreateWebSocketClient()` argument validation (arg counts, non-component listener)
10+
- Scheme handling (`ws://`, `wss://`, rejection of `http://`)
11+
- Connection error timing (refused-connection fails fast, unroutable IP hits the library's 5s timeout)
12+
- `onError( type="connect", ... )` listener invocation on connect failure
13+
- OSGi loader health
14+
15+
Runs in the `test` job via `lucee/script-runner@main` with `testLabels: websocketclient`. Fast, no network dependencies beyond loopback.
16+
17+
Fixture: `RecordingListener.cfc` captures every callback with its arguments for later assertion. Use it whenever a test needs to verify a callback fired.
18+
19+
### Integration tests (server repo)
20+
21+
The real client⇄server coverage — `sendText`, `sendBinary`, `onMessage`, `onClose`, `disconnect`, `isOpen`, lifecycle event ordering — lives in the server extension repo:
22+
23+
[lucee/extension-websocket — `tests/integration/`](https://github.com/lucee/extension-websocket/tree/master/tests/integration)
24+
25+
Those tests use `CreateWebSocketClient` as the driver against a Lucee WebSocket server listener, so they cover both extensions end to end.
26+
27+
## Why not duplicate the integration tests here?
28+
29+
Every integration test needs both extensions running. Duplicating the suite across two repos guarantees drift. Instead:
30+
31+
- One copy lives in the server repo.
32+
- The `test-integration` job in this repo's workflow **sparse-checkouts** `tests/integration/` from the server repo, builds this branch's client `.lex`, pulls the latest server `.lex` from download.lucee.org, and runs the suite against the combined pair.
33+
- Each side's CI catches the bug on its own side — server changes fail in the server repo, client changes fail here.
34+
35+
## What fails where
36+
37+
| Bug source | Caught by |
38+
|---|---|
39+
| Client BIF contract (arg handling, error shape) | Unit tests in this repo |
40+
| Client callback invocation (onError on connect fail) | Unit tests in this repo |
41+
| Client send/receive, lifecycle, reconnect | Integration tests (cross-repo) |
42+
| Server callback firing, broadcast, state | Integration tests (server repo CI) |
43+
44+
## Adding a test
45+
46+
Needs a server to drive it? **Add it to the server repo's [`tests/integration/`](https://github.com/lucee/extension-websocket/tree/master/tests/integration).** Both CIs will pick it up.
47+
48+
No server needed (argument validation, error shape, scheme parsing)? **Add a `testXxx()` method to [`WebSocketClientTest.cfc`](WebSocketClientTest.cfc) here.**
49+
50+
## Running locally
51+
52+
Unit tests — whatever CFML test runner you use that picks up `labels="websocketclient"`. Or invoke `WebSocketClientTest.cfc` methods directly from a test script.
53+
54+
Integration tests — spin up Lucee with both extensions, drop the server listeners into `{lucee-config}/websockets/`, and `curl` the `.cfm` scripts from the server repo's `tests/integration/` folder. See [that repo's CI workflow](https://github.com/lucee/extension-websocket/blob/master/.github/workflows/main.yml) for the exact setup — it's the same one this repo's `test-integration` job uses.

tests/RecordingListener.cfc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
component hint="Test fixture — records every callback invocation for assertion" {
2+
3+
variables.events = [];
4+
5+
function onMessage( message ) {
6+
arrayAppend( variables.events, "onMessage:" & arguments.message );
7+
}
8+
9+
function onBinaryMessage( binary ) {
10+
arrayAppend( variables.events, "onBinaryMessage:" & arrayLen( arguments.binary ) );
11+
}
12+
13+
function onClose() {
14+
arrayAppend( variables.events, "onClose" );
15+
}
16+
17+
function onError( type, cause, data ) {
18+
var entry = "onError:" & arguments.type;
19+
if ( structKeyExists( arguments, "data" ) && !isNull( arguments.data ) )
20+
entry &= ":withData";
21+
arrayAppend( variables.events, entry );
22+
}
23+
24+
function onPing() {
25+
arrayAppend( variables.events, "onPing" );
26+
}
27+
28+
function onPong() {
29+
arrayAppend( variables.events, "onPong" );
30+
}
31+
32+
array function getEvents() {
33+
return variables.events;
34+
}
35+
36+
function reset() {
37+
variables.events = [];
38+
}
39+
40+
}

0 commit comments

Comments
 (0)