Skip to content

Commit 46c31f4

Browse files
committed
Break up server.ts into app.ts and handlers/*.ts so that unit test can be written more easily.
1 parent a2d47cc commit 46c31f4

21 files changed

+1435
-523
lines changed

frontend/.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ backstop_data/bitmaps_test/
2424
npm-debug.log*
2525
yarn-debug.log*
2626
yarn-error.log*
27+
28+
# coverage reports
29+
coverage
30+
31+
# vscode
32+
.vscode

frontend/.vscode/launch.json

-23
This file was deleted.

frontend/server/app.test.ts

+333
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
import * as os from 'os';
15+
import * as fs from 'fs';
16+
import * as path from 'path';
17+
import { PassThrough } from 'stream';
18+
19+
import fetch from 'node-fetch';
20+
import * as requests from 'supertest';
21+
import { Client as MinioClient } from 'minio';
22+
import { Storage as GCSStorage } from '@google-cloud/storage';
23+
24+
import { UIServer } from './app';
25+
import { loadConfigs } from './configs';
26+
import * as minioHelper from './minio-helper';
27+
import { getTensorboardInstance } from './k8s-helper';
28+
29+
jest.mock('minio');
30+
jest.mock('node-fetch');
31+
jest.mock('@google-cloud/storage');
32+
jest.mock('./minio-helper');
33+
jest.mock('./k8s-helper');
34+
35+
describe('UIServer', () => {
36+
let app: UIServer;
37+
const indexHtmlPath = path.resolve(os.tmpdir(), 'index.html');
38+
const argv = ['node', 'dist/server.js', os.tmpdir(), '3000'];
39+
const buildDate = new Date().toISOString();
40+
const commitHash = 'abcdefg';
41+
const indexHtmlContent = `
42+
<html>
43+
<head>
44+
<script>
45+
window.KFP_FLAGS.DEPLOYMENT=null
46+
</script>
47+
<script id="kubeflow-client-placeholder"></script>
48+
</head>
49+
</html>`;
50+
const expectedIndexHtml = `
51+
<html>
52+
<head>
53+
<script>
54+
window.KFP_FLAGS.DEPLOYMENT="KUBEFLOW"
55+
</script>
56+
<script id="kubeflow-client-placeholder" src="/dashboard_lib.bundle.js"></script>
57+
</head>
58+
</html>`;
59+
60+
beforeAll(() => {
61+
fs.writeFileSync(path.resolve(__dirname, 'BUILD_DATE'), buildDate);
62+
fs.writeFileSync(path.resolve(__dirname, 'COMMIT_HASH'), commitHash);
63+
fs.writeFileSync(indexHtmlPath, indexHtmlContent);
64+
});
65+
66+
afterAll(() => {
67+
fs.unlinkSync(path.resolve(__dirname, 'BUILD_DATE'));
68+
fs.unlinkSync(path.resolve(__dirname, 'COMMIT_HASH'));
69+
fs.unlinkSync(indexHtmlPath);
70+
});
71+
72+
beforeEach(() => {
73+
(MinioClient as any).mockClear();
74+
(fetch as any).mockClear();
75+
(GCSStorage as any).mockClear();
76+
(getTensorboardInstance as any).mockClear();
77+
});
78+
79+
afterEach(() => {
80+
app && app.close();
81+
});
82+
83+
it('api server is not ready', done => {
84+
(fetch as any).mockImplementationOnce((_url: string, _opt: any) => ({
85+
json: () => Promise.reject('Unknown error'),
86+
}));
87+
88+
const configs = loadConfigs(argv, {});
89+
app = new UIServer(configs);
90+
requests(app.start())
91+
.get('/apis/v1beta1/healthz')
92+
.expect(
93+
200,
94+
{
95+
buildDate,
96+
frontendCommitHash: commitHash,
97+
apiServerReady: false,
98+
},
99+
done,
100+
);
101+
});
102+
103+
it('api server is ready', done => {
104+
(fetch as any).mockImplementationOnce((_url: string, _opt: any) => ({
105+
json: () =>
106+
Promise.resolve({
107+
commit_sha: 'commit_sha',
108+
}),
109+
}));
110+
111+
const configs = loadConfigs(argv, {});
112+
app = new UIServer(configs);
113+
requests(app.start())
114+
.get('/apis/v1beta1/healthz')
115+
.expect(
116+
200,
117+
{
118+
buildDate,
119+
frontendCommitHash: commitHash,
120+
apiServerReady: true,
121+
apiServerCommitHash: 'commit_sha',
122+
},
123+
done,
124+
);
125+
});
126+
127+
it('is not a kubeflow deployment', done => {
128+
const configs = loadConfigs(argv, {});
129+
app = new UIServer(configs);
130+
131+
const request = requests(app.start());
132+
request
133+
.get('/')
134+
.expect('Content-Type', 'text/html; charset=utf-8')
135+
.expect(200, indexHtmlContent, done);
136+
});
137+
138+
it('is a kubeflow deployment', done => {
139+
const configs = loadConfigs(argv, { DEPLOYMENT: 'kubeflow' });
140+
app = new UIServer(configs);
141+
142+
const request = requests(app.start());
143+
request
144+
.get('/')
145+
.expect('Content-Type', 'text/html; charset=utf-8')
146+
.expect(200, expectedIndexHtml, done);
147+
});
148+
149+
it('is a minio artifact', done => {
150+
const artifactContent = 'hello world';
151+
const mockedMinioClient: jest.Mock = MinioClient as any;
152+
const mockedGetTarObjectAsString: jest.Mock = minioHelper.getTarObjectAsString as any;
153+
mockedGetTarObjectAsString.mockImplementationOnce(opt =>
154+
opt.bucket === 'ml-pipeline' && opt.key === 'hello/world.txt'
155+
? Promise.resolve(artifactContent)
156+
: Promise.reject('Unable to retrieve minio artifact.'),
157+
);
158+
const configs = loadConfigs(argv, {
159+
MINIO_ACCESS_KEY: 'minio',
160+
MINIO_SECRET_KEY: 'minio123',
161+
MINIO_PORT: '9000',
162+
MINIO_HOST: 'minio-service',
163+
MINIO_NAMESPACE: 'kubeflow',
164+
MINIO_SSL: 'false',
165+
});
166+
app = new UIServer(configs);
167+
168+
const request = requests(app.start());
169+
request
170+
.get('/artifacts/get?source=minio&bucket=ml-pipeline&encodedKey=hello%2Fworld.txt')
171+
.expect(200, artifactContent, err => {
172+
expect(mockedMinioClient).toBeCalledWith({
173+
accessKey: 'minio',
174+
secretKey: 'minio123',
175+
endPoint: 'minio-service.kubeflow',
176+
port: 9000,
177+
useSSL: false,
178+
});
179+
done(err);
180+
});
181+
});
182+
183+
it('is a s3 artifact', done => {
184+
const artifactContent = 'hello world';
185+
const mockedMinioClient: jest.Mock = minioHelper.createMinioClient as any;
186+
const mockedGetObjectStream: jest.Mock = minioHelper.getObjectStream as any;
187+
const stream = new PassThrough();
188+
stream.write(artifactContent);
189+
stream.end();
190+
191+
mockedGetObjectStream.mockImplementationOnce(opt =>
192+
opt.bucket === 'ml-pipeline' && opt.key === 'hello/world.txt'
193+
? Promise.resolve(stream)
194+
: Promise.reject('Unable to retrieve s3 artifact.'),
195+
);
196+
const configs = loadConfigs(argv, {
197+
AWS_ACCESS_KEY_ID: 'aws123',
198+
AWS_SECRET_ACCESS_KEY: 'awsSecret123',
199+
});
200+
app = new UIServer(configs);
201+
202+
const request = requests(app.start());
203+
request
204+
.get('/artifacts/get?source=s3&bucket=ml-pipeline&encodedKey=hello%2Fworld.txt')
205+
.expect(200, artifactContent, err => {
206+
expect(mockedMinioClient).toBeCalledWith({
207+
accessKey: 'aws123',
208+
secretKey: 'awsSecret123',
209+
endPoint: 's3.amazonaws.com',
210+
});
211+
done(err);
212+
});
213+
});
214+
215+
it('is a http artifact', done => {
216+
const artifactContent = 'hello world';
217+
const mockedFetch: jest.Mock = fetch as any;
218+
mockedFetch.mockImplementationOnce((url: string, opts: any) =>
219+
url === 'http://foo.bar/ml-pipeline/hello/world.txt'
220+
? Promise.resolve({ buffer: () => Promise.resolve(artifactContent) })
221+
: Promise.reject('Unable to retrieve http artifact.'),
222+
);
223+
const configs = loadConfigs(argv, {
224+
HTTP_BASE_URL: 'foo.bar/',
225+
});
226+
app = new UIServer(configs);
227+
228+
const request = requests(app.start());
229+
request
230+
.get('/artifacts/get?source=http&bucket=ml-pipeline&encodedKey=hello%2Fworld.txt')
231+
.expect(200, artifactContent, err => {
232+
expect(mockedFetch).toBeCalledWith('http://foo.bar/ml-pipeline/hello/world.txt', {
233+
headers: {},
234+
});
235+
done(err);
236+
});
237+
});
238+
239+
it('is a https artifact', done => {
240+
const artifactContent = 'hello world';
241+
const mockedFetch: jest.Mock = fetch as any;
242+
mockedFetch.mockImplementationOnce((url: string, opts: any) =>
243+
url === 'https://foo.bar/ml-pipeline/hello/world.txt' &&
244+
opts.headers.Authorization === 'someToken'
245+
? Promise.resolve({ buffer: () => Promise.resolve(artifactContent) })
246+
: Promise.reject('Unable to retrieve http artifact.'),
247+
);
248+
const configs = loadConfigs(argv, {
249+
HTTP_BASE_URL: 'foo.bar/',
250+
HTTP_AUTHORIZATION_KEY: 'Authorization',
251+
HTTP_AUTHORIZATION_DEFAULT_VALUE: 'someToken',
252+
});
253+
app = new UIServer(configs);
254+
255+
const request = requests(app.start());
256+
request
257+
.get('/artifacts/get?source=https&bucket=ml-pipeline&encodedKey=hello%2Fworld.txt')
258+
.expect(200, artifactContent, err => {
259+
expect(mockedFetch).toBeCalledWith('https://foo.bar/ml-pipeline/hello/world.txt', {
260+
headers: {
261+
Authorization: 'someToken',
262+
},
263+
});
264+
done(err);
265+
});
266+
});
267+
268+
it('is a https artifact and inherits the headers', done => {
269+
const artifactContent = 'hello world';
270+
const mockedFetch: jest.Mock = fetch as any;
271+
mockedFetch.mockImplementationOnce((url: string, opts: any) =>
272+
url === 'https://foo.bar/ml-pipeline/hello/world.txt'
273+
? Promise.resolve({ buffer: () => Promise.resolve(artifactContent) })
274+
: Promise.reject('Unable to retrieve http artifact.'),
275+
);
276+
const configs = loadConfigs(argv, {
277+
HTTP_BASE_URL: 'foo.bar/',
278+
HTTP_AUTHORIZATION_KEY: 'Authorization',
279+
});
280+
app = new UIServer(configs);
281+
282+
const request = requests(app.start());
283+
request
284+
.get('/artifacts/get?source=https&bucket=ml-pipeline&encodedKey=hello%2Fworld.txt')
285+
.set('Authorization', 'inheritedToken')
286+
.expect(200, artifactContent, err => {
287+
expect(mockedFetch).toBeCalledWith('https://foo.bar/ml-pipeline/hello/world.txt', {
288+
headers: {
289+
Authorization: 'inheritedToken',
290+
},
291+
});
292+
done(err);
293+
});
294+
});
295+
296+
it('is a gcs artifact', done => {
297+
const artifactContent = 'hello world';
298+
const mockedGcsStorage: jest.Mock = GCSStorage as any;
299+
const stream = new PassThrough();
300+
stream.write(artifactContent);
301+
stream.end();
302+
mockedGcsStorage.mockImplementationOnce(() => ({
303+
bucket: () => ({
304+
getFiles: () =>
305+
Promise.resolve([[{ name: 'hello/world.txt', createReadStream: () => stream }]]),
306+
}),
307+
}));
308+
const configs = loadConfigs(argv, {});
309+
app = new UIServer(configs);
310+
311+
const request = requests(app.start());
312+
request
313+
.get('/artifacts/get?source=gcs&bucket=ml-pipeline&encodedKey=hello%2Fworld.txt')
314+
.expect(200, artifactContent + '\n', done);
315+
});
316+
317+
// TODO: refractor k8s helper module so that k8s APIs can be mocked
318+
// it('get a tensorboard url', done => {
319+
// const tensorboardUrl = 'http://tensorboard.view/abc';
320+
// const mockedGetTensorboardInstance: jest.Mock = getTensorboardInstance as any;
321+
// mockedGetTensorboardInstance.mockImplementationOnce(logdir =>
322+
// logdir === 'hello/world'
323+
// ? Promise.resolve(tensorboardUrl)
324+
// : Promise.reject('Invalid logdir.'),
325+
// );
326+
// const configs = loadConfigs(argv, {});
327+
// app = new UIServer(configs);
328+
329+
// const request = requests(app.start());
330+
// request.get('/apps/tensorboard?logdir=hello%2Fworld')
331+
// .expect(200, tensorboardUrl, done);
332+
// });
333+
});

0 commit comments

Comments
 (0)