Skip to content

Commit 10aaebd

Browse files
committed
test: seperate out functional tests
1 parent 66cda36 commit 10aaebd

9 files changed

Lines changed: 549 additions & 262 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Functional Tests
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
environment:
7+
description: 'Environment to run tests against'
8+
required: true
9+
default: 'dev'
10+
type: choice
11+
options:
12+
- dev
13+
- staging
14+
test_pattern:
15+
description: 'Test pattern to run (optional)'
16+
required: false
17+
default: ''
18+
type: string
19+
20+
jobs:
21+
functional-tests:
22+
environment: ${{ inputs.environment }}
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 30
25+
26+
steps:
27+
- name: Checkout code
28+
uses: actions/checkout@v4
29+
30+
- name: Setup Node.js
31+
uses: volta-cli/action@v4
32+
33+
- name: Install dependencies
34+
run: npm ci --no-audit
35+
36+
- name: Build project
37+
run: npm run build --if-present
38+
39+
- name: Run functional tests
40+
run: |
41+
if [ -n "${{ inputs.test_pattern }}" ]; then
42+
npm run test:functional -- --reporter=verbose --reporter=github-actions --testNamePattern="${{ inputs.test_pattern }}"
43+
else
44+
npm run test:functional
45+
fi
46+
env:
47+
VITE_ATHENA_CLIENT_ID: ${{ secrets.VITE_ATHENA_CLIENT_ID }}
48+
VITE_ATHENA_CLIENT_SECRET: ${{ secrets.VITE_ATHENA_CLIENT_SECRET }}
49+
VITE_ATHENA_DEPLOYMENT_ID: ${{ secrets.VITE_ATHENA_DEPLOYMENT_ID }}
50+
VITE_ATHENA_AFFILIATE: ${{ secrets.VITE_ATHENA_AFFILIATE }}
51+
VITE_OAUTH_ISSUER: ${{ secrets.VITE_OAUTH_ISSUER }}
52+
53+
- name: Upload test results
54+
if: always()
55+
uses: actions/upload-artifact@v4
56+
with:
57+
name: functional-test-results-${{ inputs.environment }}
58+
path: |
59+
**/*test-results*
60+
**/*coverage*
61+
retention-days: 30
62+
63+
- name: Report test summary
64+
if: always()
65+
run: |
66+
echo "## Functional Test Results" >> $GITHUB_STEP_SUMMARY
67+
echo "- Environment: ${{ inputs.environment }}" >> $GITHUB_STEP_SUMMARY
68+
echo "- Test pattern: ${{ inputs.test_pattern || 'All tests' }}" >> $GITHUB_STEP_SUMMARY
69+
echo "- Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY

.github/workflows/nodejs.yml

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,35 @@ on:
66

77
jobs:
88
build:
9-
environment: dev
109
runs-on: ubuntu-latest
1110
steps:
12-
- uses: actions/checkout@v4
13-
- uses: volta-cli/action@v4
14-
- run: npm ci --no-audit
15-
- run: npm run lint --if-present
16-
- run: npm run prettier:check --if-present
17-
- run: npm test
18-
env:
19-
VITE_ATHENA_CLIENT_ID: ${{ secrets.VITE_ATHENA_CLIENT_ID }}
20-
VITE_ATHENA_CLIENT_SECRET: ${{ secrets.VITE_ATHENA_CLIENT_SECRET }}
21-
VITE_ATHENA_DEPLOYMENT_ID: ${{ secrets.VITE_ATHENA_DEPLOYMENT_ID }}
22-
VITE_ATHENA_AFFILIATE: ${{ secrets.VITE_ATHENA_AFFILIATE }}
23-
VITE_OAUTH_ISSUER: ${{ secrets.VITE_OAUTH_ISSUER }}
24-
- run: npm run build --if-present
11+
- name: Checkout code
12+
uses: actions/checkout@v4
13+
14+
- name: Setup Node.js
15+
uses: volta-cli/action@v4
16+
17+
- name: Install dependencies
18+
run: npm ci --no-audit
19+
20+
- name: Run linting
21+
run: npm run lint --if-present
22+
23+
- name: Check code formatting
24+
run: npm run prettier:check --if-present
25+
26+
- name: Run unit tests
27+
run: npm run test:unit
28+
29+
- name: Build project
30+
run: npm run build --if-present
31+
32+
- name: Upload test results
33+
if: always()
34+
uses: actions/upload-artifact@v4
35+
with:
36+
name: unit-test-results
37+
path: |
38+
**/*test-results*
39+
**/*coverage*
40+
retention-days: 7

__tests__/functional/448x448.jpg

4.53 KB
Loading

__tests__/functional/578x478.jpg

24.7 KB
Loading
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { describe, it, } from 'vitest';
2+
import { ClassificationOutput, ClassifierSdk, type ClassifyImageInput, ImageFormat } from '../../src';
3+
import fs from 'fs';
4+
import { randomUUID } from 'crypto';
5+
6+
describe('ClassifierSdk Functional Tests', () => {
7+
describe('listDeployments', () => {
8+
it('should listDeployments and return responses (smoke test)', async ({ expect }) => {
9+
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
10+
// You may want to mock the gRPC client for true unit testing.
11+
const sdk = new ClassifierSdk({
12+
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
13+
affiliate: process.env.VITE_ATHENA_AFFILIATE,
14+
authentication: {
15+
issuerUrl: process.env.VITE_OAUTH_ISSUER,
16+
clientId: process.env.VITE_ATHENA_CLIENT_ID,
17+
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
18+
scope: 'manage:classify'
19+
}
20+
});
21+
22+
let error: any = null;
23+
try {
24+
const responses = await sdk.listDeployments();
25+
expect(Array.isArray(responses)).toBe(true);
26+
} catch (err) {
27+
error = err;
28+
}
29+
// Assert error is unset
30+
expect(error).toBeNull();
31+
}, 10000)
32+
});
33+
34+
describe('classifyImage', () => {
35+
it('should classify 10 images in a single request and return responses (integration smoke test)', async ({ expect, annotate }) => {
36+
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
37+
// You may want to mock the gRPC client for true unit testing.
38+
const imagePath = __dirname + '/448x448.jpg';
39+
const sdk = new ClassifierSdk({
40+
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
41+
affiliate: process.env.VITE_ATHENA_AFFILIATE,
42+
authentication: {
43+
issuerUrl: process.env.VITE_OAUTH_ISSUER,
44+
clientId: process.env.VITE_ATHENA_CLIENT_ID,
45+
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
46+
scope: 'manage:classify'
47+
}
48+
});
49+
50+
// Generate 10 unique correlationIds
51+
const correlationIds = Array.from({ length: 10 }, () => randomUUID().toString());
52+
53+
correlationIds.sort((a, b) => a.localeCompare(b));
54+
55+
annotate(`Correlation IDs: ${correlationIds.join(', ')}`);
56+
57+
// Create 10 input objects, each with a new stream and unique correlationId
58+
const inputs: ClassifyImageInput[] = correlationIds.map((correlationId) => ({
59+
data: fs.createReadStream(imagePath),
60+
format: ImageFormat.PNG,
61+
correlationId
62+
}));
63+
64+
// Create a promise to wrap the event emitter event 'data'
65+
const promise = new Promise<ClassificationOutput[]>((resolve, reject) => {
66+
const results: ClassificationOutput[] = [];
67+
68+
sdk.on('data', (data) => {
69+
if (data.globalError) {
70+
reject(data.globalError);
71+
}
72+
73+
// Check that all correlationIds are present in the outputs
74+
for (const result of data.outputs) {
75+
if (correlationIds.includes(result.correlationId)) {
76+
results.push(result);
77+
}
78+
}
79+
if (results.length == correlationIds.length) {
80+
resolve(results);
81+
}
82+
});
83+
sdk.once('error', (err) => {
84+
reject(err);
85+
});
86+
});
87+
88+
let error: any = undefined;
89+
90+
await sdk.open();
91+
92+
try {
93+
await sdk.sendClassifyRequest(inputs);
94+
} catch (err) {
95+
error = err;
96+
}
97+
98+
// Wait for classifier to process some data....
99+
const outputs = await promise;
100+
sdk.close();
101+
102+
expect(error).toBeUndefined();
103+
104+
outputs.sort((a, b) => a.correlationId.localeCompare(b.correlationId));
105+
106+
expect(outputs).toBeDefined();
107+
// Check that all correlationIds are present in the outputs
108+
expect(outputs.length).toBe(correlationIds.length);
109+
110+
const expectedOutputs = correlationIds.map(id => (
111+
{
112+
correlationId: id,
113+
classifications: expect.toBeOneOf([expect.arrayContaining([
114+
{
115+
label: expect.any(String),
116+
weight: expect.any(Number)
117+
}
118+
]), []])
119+
}
120+
));
121+
122+
expect(outputs).toMatchObject(expectedOutputs);
123+
}, 120000);
124+
125+
it('should classify with raw uint8 resize return responses (integration smoke test)', async ({ expect, annotate }) => {
126+
127+
const imagePath = __dirname + '/448x448.jpg';
128+
const sdk = new ClassifierSdk({
129+
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
130+
affiliate: process.env.VITE_ATHENA_AFFILIATE,
131+
authentication: {
132+
issuerUrl: process.env.VITE_OAUTH_ISSUER,
133+
clientId: process.env.VITE_ATHENA_CLIENT_ID,
134+
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
135+
scope: 'manage:classify'
136+
}
137+
});
138+
139+
const correlationId = randomUUID();
140+
141+
annotate(`Correlation IDs: ${correlationId}`);
142+
143+
// Create a promise to wrap the event emitter event 'data'
144+
const promise = new Promise<ClassificationOutput[]>((resolve, reject) => {
145+
// Add a timeout to reject the promise if no data is received in 30 seconds
146+
const timeout = setTimeout(() => {
147+
reject(new Error('Timeout waiting for classification response'));
148+
}, 30000);
149+
150+
sdk.on('data', (data) => {
151+
const byCorrelationId = data.outputs.filter(o => o.correlationId === correlationId);
152+
if (byCorrelationId.length > 0) {
153+
clearTimeout(timeout);
154+
resolve(byCorrelationId);
155+
}
156+
});
157+
sdk.once('error', (err) => {
158+
clearTimeout(timeout);
159+
reject(err);
160+
});
161+
});
162+
163+
// This will fail if no server is running, but will exercise the code path.
164+
let error: any = undefined;
165+
166+
await sdk.open();
167+
168+
const data = fs.createReadStream(imagePath);
169+
const options: ClassifyImageInput = {
170+
data,
171+
correlationId,
172+
resize: true,
173+
};
174+
try {
175+
await sdk.sendClassifyRequest(options);
176+
} catch (err) {
177+
error = err;
178+
}
179+
180+
// Wait for classifier to process some data....
181+
const first = await promise;
182+
sdk.close();
183+
184+
expect(first).toBeDefined();
185+
expect(first).toMatchObject([
186+
{
187+
correlationId,
188+
classifications: expect.toBeOneOf([expect.arrayContaining([
189+
{
190+
label: expect.any(String),
191+
weight: expect.any(Number)
192+
}
193+
]), []])
194+
} as ClassificationOutput
195+
]);
196+
197+
// Accept either a successful call or a connection error (for CI/dev convenience)
198+
expect(error).toBeUndefined();
199+
}, 120000);
200+
201+
it('should classify return responses (integration smoke test)', async ({ expect, annotate }) => {
202+
// This is a smoke test. You must have a running gRPC server at localhost:50051 for this to pass.
203+
// You may want to mock the gRPC client for true unit testing.
204+
const imagePath = __dirname + '/448x448.jpg';
205+
const sdk = new ClassifierSdk({
206+
deploymentId: process.env.VITE_ATHENA_DEPLOYMENT_ID,
207+
affiliate: process.env.VITE_ATHENA_AFFILIATE,
208+
authentication: {
209+
issuerUrl: process.env.VITE_OAUTH_ISSUER,
210+
clientId: process.env.VITE_ATHENA_CLIENT_ID,
211+
clientSecret: process.env.VITE_ATHENA_CLIENT_SECRET,
212+
scope: 'manage:classify'
213+
}
214+
});
215+
216+
const correlationId = randomUUID();
217+
218+
annotate(`Correlation IDs: ${correlationId}`);
219+
220+
// Create a promise to wrap the event emitter event 'data'
221+
const promise = new Promise<ClassificationOutput[]>((resolve, reject) => {
222+
// Add a timeout to reject the promise if no data is received in 30 seconds
223+
const timeout = setTimeout(() => {
224+
reject(new Error('Timeout waiting for classification response'));
225+
}, 30000);
226+
227+
sdk.on('data', (data) => {
228+
const byCorrelationId = data.outputs.filter(o => o.correlationId === correlationId);
229+
if (byCorrelationId.length > 0) {
230+
clearTimeout(timeout);
231+
resolve(byCorrelationId);
232+
}
233+
});
234+
sdk.once('error', (err) => {
235+
clearTimeout(timeout);
236+
reject(err);
237+
});
238+
});
239+
240+
// This will fail if no server is running, but will exercise the code path.
241+
let error: any = undefined;
242+
243+
await sdk.open();
244+
245+
const data = fs.createReadStream(imagePath);
246+
const options: ClassifyImageInput = {
247+
data,
248+
correlationId,
249+
format: ImageFormat.JPEG,
250+
};
251+
try {
252+
await sdk.sendClassifyRequest(options);
253+
} catch (err) {
254+
error = err;
255+
}
256+
257+
// Wait for classifier to process some data....
258+
const first = await promise;
259+
sdk.close();
260+
261+
expect(first).toBeDefined();
262+
expect(first).toMatchObject([
263+
{
264+
correlationId,
265+
classifications: expect.toBeOneOf([expect.arrayContaining([
266+
{
267+
label: expect.any(String),
268+
weight: expect.any(Number)
269+
}
270+
]), []])
271+
} as ClassificationOutput
272+
]);
273+
274+
// Accept either a successful call or a connection error (for CI/dev convenience)
275+
expect(error).toBeUndefined();
276+
}, 120000);
277+
});
278+
});

0 commit comments

Comments
 (0)