serverless-spy
Version:
CDK-based library for writing elegant integration tests on AWS serverless architecture and an additional web console to monitor events in real time.
274 lines (234 loc) • 8.42 kB
text/typescript
import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
import { promisify } from 'util';
import { device } from 'aws-iot-device-sdk';
import * as progam from 'caporal';
import * as open from 'open';
import { WebSocketServer } from 'ws';
// @ts-ignore
import { getConnection } from '../listener/iot-connection';
import { getTopic } from '../listener/topic';
const readFileAsync = promisify(fs.readFile);
//resolve issue with module import
let opener = open;
if ((open as any).default) {
opener = (open as any).default;
}
async function run() {
let stackList: string[] | undefined;
let cdkOutput: Record<string, Record<string, string>>;
let options: any;
progam
.description('ServerlessSpy web console')
.option('--ws <ws>', 'Websocket link')
.option(
'--cdkoutput <cdkoutput>',
'CDK output file that contains IoT Endpoint link in a property ServerlessSpyWsUrl'
)
.option(
'--cdkstack <cdkstack>',
'CDK stack in cdk output file. If not specified the first one is picked.'
)
.option('--open <open>', 'Open browser', progam.BOOL, true)
.option(
'--port <p>',
`A port on localhost where ServerlessSpy web console is accessible.`,
progam.INT,
'3456'
)
.option(
'--wsport <wsp>',
`A port on localhost where ServerlessSpy websocket is accessible.`,
progam.INT,
'3457'
)
.action((_args, opt, _logger) => {
options = opt;
});
progam.parse(process.argv);
if (!options.ws && !options.cdkoutput) {
throw new Error('--ws or --cdkoutput parameter not specified');
}
if (options.cdkoutput) {
const rawdata = fs.readFileSync(options.cdkoutput);
cdkOutput = JSON.parse(rawdata.toString());
stackList = Object.keys(cdkOutput);
}
const wss = new WebSocketServer({ port: options.wsport });
let connection: device | undefined = undefined;
wss.on('close', async () => {
if (connection) connection.end(true);
});
wss.on('connection', async function connect(ws) {
console.log('Connection');
ws.on('message', function message(data) {
console.log('received: %s', data);
});
let wsUrl: string | undefined;
if (options.ws) {
wsUrl = options.ws;
} else if (cdkOutput) {
if (cdkOutput[options.cdkstack]) {
wsUrl = cdkOutput[options.cdkstack].ServerlessSpyWsUrl;
} else if (cdkOutput[Object.keys(cdkOutput)[0]]) {
wsUrl = cdkOutput[Object.keys(cdkOutput)[0]].ServerlessSpyWsUrl;
}
}
if (!wsUrl) {
throw new Error('Missing IoT endpoint url');
}
const wsUrlWithoutScope = wsUrl.split('/')[0];
connection = await getConnection(true, wsUrlWithoutScope);
const topic = getTopic('#');
console.log(`Subscribing to ${topic}`);
connection.on('connect', () => {
console.log('Connection opened');
if (connection) {
connection.subscribe(topic);
}
});
connection.on('message', (topic: string, data: Buffer) => {
ws.send(
JSON.stringify({
...JSON.parse(JSON.parse(data.toString()).data),
topic,
})
);
});
});
http
.createServer((request, response) => {
void (async () => {
try {
//console.log('request ', request.url);
let filePath: string = `.${request.url}`;
//remove query parameters
filePath = filePath.split('?')[0];
let rootFolder = __dirname;
if (request.url?.startsWith('/webServerlessSpy.js')) {
//get transpiled TS to JS files
rootFolder = getCompiledJsPath();
} else if (request.url?.startsWith('/bootstrap/')) {
filePath = filePath.substring('/bootstrap/'.length);
const bootstrapFolder = await getNpmModuleInstalledPath(
'bootstrap'
);
rootFolder = bootstrapFolder;
} else if (request.url?.startsWith('/bootstrap-icons/')) {
filePath = filePath.substring('/bootstrap-icons/'.length);
const bootstrapFolder = await getNpmModuleInstalledPath(
'bootstrap-icons'
);
rootFolder = bootstrapFolder;
} else {
if (filePath === './') {
filePath = './index.html';
}
}
filePath = path.join(rootFolder, filePath);
//console.log(`${request.url} --> ${filePath}`);
const extname = String(path.extname(filePath)).toLowerCase();
const mimeTypes: any = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm',
};
const contentType = mimeTypes[extname] || 'application/octet-stream';
if (request.url === '/stackList') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(stackList), 'utf-8');
} else if (request.url === '/stackTopicMappings') {
response.writeHead(200, { 'Content-Type': 'application/json' });
const mappings: Record<string, string> = {};
if (cdkOutput) {
for (const [stackName, stack] of Object.entries(cdkOutput)) {
if (stack.ServerlessSpyWsUrl) {
const [_, scope] = stack.ServerlessSpyWsUrl.split('/');
if (scope) {
mappings[stackName] = scope;
}
}
}
}
response.end(JSON.stringify(mappings), 'utf-8');
} else if (request.url?.match('^/wsUrl')) {
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(`ws:localhost:${options.wsport}`, 'utf-8');
} else {
try {
const content = await readFileAsync(filePath);
response.writeHead(200, { 'Content-Type': contentType });
response.end(content, 'utf-8');
} catch (error: any) {
if (error.code === 'ENOENT') {
response.writeHead(404, { 'Content-Type': 'text/html' });
response.end(
`No such file or directory ${request.url}`,
'utf-8'
);
} else {
response.writeHead(500);
response.end(`Error: ${error.code} ..\n`);
}
}
}
} catch (err: any) {
response.writeHead(500, { 'Content-Type': 'text/html' });
response.end(err.message, 'utf-8');
}
})();
})
.listen(options.port);
console.log(
`ServerlessSpy console runing at http://localhost:${options.port}`
);
if (options.open) {
await opener(`http://localhost:${options.port}`);
}
}
run().catch(console.error);
function getNpmModuleInstalledPath(npm: string) {
let folder = path.join(__dirname, '../', 'node_modules', npm);
if (fs.existsSync(folder)) {
return folder;
}
let folderAsPackage = path.join(__dirname, '../../', 'node_modules', npm);
if (fs.existsSync(folderAsPackage)) {
return folderAsPackage;
}
// When boostrap ends up in importing projects root node_modules and not in serverless-spys node_modules
folderAsPackage = path.join(__dirname, '../../../../', 'node_modules', npm);
if (fs.existsSync(folderAsPackage)) {
return folderAsPackage;
}
throw new Error(
`Can not find package in folder ${folder} and ${folderAsPackage}`
);
}
function getCompiledJsPath() {
let folder = path.join(__dirname, '../', 'lib/cli');
if (fs.existsSync(folder)) {
return folder;
}
let folderAsPackage = path.join(__dirname, '../../', 'lib/cli');
if (fs.existsSync(folderAsPackage)) {
return folderAsPackage;
}
throw new Error(
`Can not find compiled files in folder ${folder} and ${folderAsPackage}`
);
}