Skip to content

Commit 7318d2a

Browse files
committed
Initial commit
0 parents  commit 7318d2a

18 files changed

+4184
-0
lines changed

.editorconfig

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
root = true
2+
3+
[*.*]
4+
indent_style = space
5+
indent_size = 2
6+
7+
insert_final_newline = true

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.idea

.prettierrc.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"trailingComma": "all",
3+
"tabWidth": 2,
4+
"semi": true,
5+
"singleQuote": true,
6+
"printWidth": 140
7+
}

jest.config.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property and type check, visit:
3+
* https://jestjs.io/docs/en/configuration.html
4+
*/
5+
6+
export default {
7+
preset: 'ts-jest',
8+
transform: {
9+
'^.+\\.ts$': 'ts-jest',
10+
},
11+
12+
// The directory where Jest should output its coverage files
13+
coverageDirectory: 'coverage',
14+
15+
// The test environment that will be used for testing
16+
testEnvironment: 'node',
17+
};

package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "http-forwarder",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"devDependencies": {
7+
"jest": "^26.6.1",
8+
"prettier": "^2.1.2",
9+
"ts-jest": "^26.4.3",
10+
"ts-node": "^9.0.0",
11+
"typescript": "^4.0.5"
12+
},
13+
"scripts": {
14+
"test": "jest"
15+
},
16+
"dependencies": {
17+
"axios": "^0.21.0",
18+
"axios-logger": "^2.4.0"
19+
}
20+
}

src/controllers/RootController.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { IDashboardServices, TrackingEvent } from '../services/IDashboardServices';
2+
import { IncomingMessage, ServerResponse } from 'http';
3+
4+
type EventsPayload = { events: TrackingEvent[] };
5+
6+
export class RootController {
7+
constructor(private request: IncomingMessage, private response: ServerResponse, private dashboardServices: IDashboardServices[]) {}
8+
9+
handleRequest(data: unknown) {
10+
if (this.request.url === '/' && this.request.method === 'POST') {
11+
this.processEvents(data as EventsPayload);
12+
} else {
13+
this.response.writeHead(404);
14+
this.response.end('Not found');
15+
}
16+
}
17+
18+
private async processEvents({ events }: EventsPayload) {
19+
const promises = [];
20+
21+
events.forEach(event => {
22+
promises.push(...this.dashboardServices.map(service => service.send(event)));
23+
});
24+
25+
await Promise.all(promises);
26+
this.response.writeHead(200);
27+
this.response.end('OK');
28+
}
29+
}

src/index.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as http from 'http';
2+
import { RootController } from './controllers/RootController';
3+
import { MonitorService } from './services/MonitorService';
4+
import { SkyAnalyticsService } from './services/SkyAnalyticsService';
5+
import { MapcodeService } from './services/MapcodeService';
6+
import { SpaceshipService } from './services/SpaceshipService';
7+
8+
http.createServer((request, response) => {
9+
let chunks = '';
10+
request.on('data', chunk => {
11+
chunks += chunk;
12+
if (chunks.length > 1e6) {
13+
// kill the request if a payload too big
14+
request.connection.destroy();
15+
}
16+
});
17+
request.on('end', () => {
18+
const data = JSON.parse(chunks);
19+
20+
new RootController(request, response, [
21+
new MonitorService(),
22+
new SkyAnalyticsService(new MapcodeService()),
23+
new SpaceshipService(),
24+
]).handleRequest(data);
25+
})
26+
}).listen(1337);

src/mocks/testEvents.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { TrackingEvent } from '../services/IDashboardServices';
2+
3+
export const testEvent1: TrackingEvent = {
4+
t: 'lift-off',
5+
engines: 4,
6+
fuel: 78,
7+
successful: true,
8+
temperature: {
9+
engine: 80,
10+
cabin: 31,
11+
},
12+
timestamp: 1595244264059,
13+
'lat-lon': [-16.270183, 168.110748],
14+
};
15+
16+
export const testEvent2: TrackingEvent = {
17+
t: 'landing',
18+
engines: 1,
19+
fuel: 26,
20+
successful: true,
21+
temperature: {
22+
engine: 80,
23+
cabin: 31,
24+
},
25+
timestamp: 1595524813145,
26+
'lat-lon': [51.769455, 182.81861],
27+
};

src/services/IDashboardServices.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { AxiosInstance } from 'axios';
2+
3+
export interface TrackingEvent {
4+
t: string;
5+
engines: number;
6+
fuel: number;
7+
successful: boolean;
8+
temperature: Temperature;
9+
timestamp: number;
10+
'lat-lon': [number, number];
11+
}
12+
13+
export interface Temperature {
14+
engine: number;
15+
cabin: number;
16+
}
17+
18+
export interface IDashboardServices {
19+
URL: string;
20+
client: AxiosInstance;
21+
send(payload: TrackingEvent): Promise<any>;
22+
}

src/services/MapcodeService.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import axios, { AxiosInstance } from 'axios';
2+
import * as AxiosLogger from 'axios-logger';
3+
4+
type LatLon = [number, number];
5+
6+
export interface IMapcodeService {
7+
convertLocationToMapcode(location: LatLon): Promise<Mapcode>;
8+
}
9+
10+
export class MapcodeService implements IMapcodeService {
11+
client: AxiosInstance;
12+
13+
constructor() {
14+
this.client = axios.create({
15+
baseURL: 'https://api.mapcode.com/mapcode',
16+
});
17+
this.client.interceptors.response.use(AxiosLogger.responseLogger);
18+
this.client.interceptors.request.use(AxiosLogger.requestLogger);
19+
}
20+
21+
async convertLocationToMapcode(location: LatLon) {
22+
const response = await this.client.get<Mapcode>(`/codes/${location.join(',')}`);
23+
return response.data;
24+
}
25+
}
26+
27+
export interface Mapcode {
28+
international: International;
29+
local: Local;
30+
mapcodes: Local[];
31+
}
32+
33+
export interface International {
34+
mapcode: string;
35+
}
36+
37+
export interface Local {
38+
mapcode: string;
39+
territory?: string;
40+
}

src/services/MonitorService.test.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import axios, { AxiosInstance } from 'axios';
2+
3+
import { MonitorService } from './MonitorService';
4+
import { testEvent1, testEvent2 } from '../mocks/testEvents';
5+
6+
let monitorService: MonitorService;
7+
let axiosInstanceMock: AxiosInstance;
8+
const testEvent1Transformed = { ...testEvent1 };
9+
delete testEvent1Transformed.timestamp;
10+
11+
const testEvent2Transformed = { ...testEvent2 };
12+
delete testEvent2Transformed.timestamp;
13+
14+
beforeEach(() => {
15+
axiosInstanceMock = {
16+
put: jest.fn().mockResolvedValue('OK'),
17+
} as any;
18+
(axios as any).create = jest.fn(() => axiosInstanceMock);
19+
monitorService = new MonitorService();
20+
});
21+
22+
describe('MonitorService', () => {
23+
test('should exist', () => {
24+
expect(monitorService).toBeTruthy();
25+
});
26+
27+
test.each([
28+
['event1', testEvent1, testEvent1Transformed],
29+
['event2', testEvent2, testEvent2Transformed],
30+
])('should send tracking event "%s" properly', async (eventName, input, result) => {
31+
expect(await monitorService.send(input)).toBe('OK');
32+
33+
expect((axios as any).create).toBeCalledWith({
34+
baseURL: monitorService.URL,
35+
});
36+
37+
const timestamp = (input.timestamp / 1000).toFixed(0);
38+
39+
expect(axiosInstanceMock.put).toBeCalledWith(timestamp, result);
40+
});
41+
});

src/services/MonitorService.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { IDashboardServices, TrackingEvent } from './IDashboardServices';
2+
import axios, { AxiosInstance } from 'axios';
3+
import * as AxiosLogger from 'axios-logger';
4+
5+
export class MonitorService implements IDashboardServices {
6+
URL = 'https://sweeps.proxy.beeceptor.com/m0nit0r.com/track_ship/';
7+
8+
client: AxiosInstance;
9+
10+
constructor() {
11+
this.client = axios.create({
12+
baseURL: this.URL,
13+
});
14+
this.client.interceptors.response.use(AxiosLogger.responseLogger);
15+
this.client.interceptors.request.use(AxiosLogger.requestLogger);
16+
}
17+
18+
transformData(payload: TrackingEvent) {
19+
const { timestamp, ..._payload } = payload;
20+
return { timestamp: (timestamp / 1000).toFixed(0), payload: _payload };
21+
}
22+
23+
async send(payload: TrackingEvent) {
24+
const { timestamp, payload: event } = this.transformData(payload);
25+
return this.client.put(timestamp, event);
26+
}
27+
}
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import axios, { AxiosInstance } from 'axios';
2+
3+
import { testEvent1, testEvent2 } from '../mocks/testEvents';
4+
import { SkyAnalyticsService } from './SkyAnalyticsService';
5+
import { IMapcodeService } from './MapcodeService';
6+
7+
let skyAnalyticsService: SkyAnalyticsService;
8+
let mapcodeService: IMapcodeService;
9+
let axiosInstanceMock: AxiosInstance;
10+
11+
const testEvent1Transformed = {
12+
t: 'lift-off',
13+
Engines: 4,
14+
Fuel: 78,
15+
Successful: true,
16+
Temperature: {
17+
engine: 80,
18+
cabin: 31,
19+
},
20+
Timestamp: 1595244264059,
21+
'Lat-lon': 'CYZ7V.DYDG',
22+
};
23+
24+
const testEvent2Transformed = {
25+
t: 'landing',
26+
Engines: 1,
27+
Fuel: 26,
28+
Successful: true,
29+
Temperature: {
30+
engine: 80,
31+
cabin: 31,
32+
},
33+
Timestamp: 1595524813145,
34+
'Lat-lon': 'V07HV.VS2D',
35+
};
36+
37+
beforeEach(() => {
38+
axiosInstanceMock = {
39+
request: jest.fn().mockResolvedValue('OK'),
40+
} as any;
41+
(axios as any).create = jest.fn(() => axiosInstanceMock);
42+
mapcodeService = {
43+
convertLocationToMapcode: jest.fn((input) => {
44+
switch (input.join(',')) {
45+
case '-16.270183,168.110748':
46+
return { international: { mapcode: 'CYZ7V.DYDG' } };
47+
case '51.769455,182.81861':
48+
return { international: { mapcode: 'V07HV.VS2D' } };
49+
}
50+
}),
51+
} as any;
52+
53+
skyAnalyticsService = new SkyAnalyticsService(mapcodeService);
54+
});
55+
56+
describe('SkyAnalyticsService', () => {
57+
test('should exist', () => {
58+
expect(skyAnalyticsService).toBeTruthy();
59+
});
60+
61+
test.each([
62+
['event1', testEvent1, testEvent1Transformed],
63+
['event2', testEvent2, testEvent2Transformed],
64+
])('should send tracking event "%s" properly', async (eventName, input, result) => {
65+
expect(await skyAnalyticsService.send(input)).toBe('OK');
66+
67+
expect((axios as any).create).toBeCalledWith({
68+
baseURL: skyAnalyticsService.URL,
69+
});
70+
71+
expect(axiosInstanceMock.request).toBeCalledWith({
72+
method: 'POST',
73+
data: result,
74+
});
75+
});
76+
});

src/services/SkyAnalyticsService.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { IDashboardServices, TrackingEvent } from './IDashboardServices';
2+
import axios, { AxiosInstance } from 'axios';
3+
import { IMapcodeService } from './MapcodeService';
4+
import { capitalize } from '../utils';
5+
import * as AxiosLogger from 'axios-logger';
6+
7+
export class SkyAnalyticsService implements IDashboardServices {
8+
URL = 'https://sweeps.proxy.beeceptor.com/skyanalytics/get';
9+
10+
client: AxiosInstance;
11+
12+
constructor(private mapcodeService: IMapcodeService) {
13+
this.client = axios.create({
14+
baseURL: this.URL,
15+
});
16+
this.client.interceptors.response.use(AxiosLogger.responseLogger);
17+
this.client.interceptors.request.use(AxiosLogger.requestLogger);
18+
}
19+
20+
async transformData(payload: TrackingEvent) {
21+
const { international } = await this.mapcodeService.convertLocationToMapcode(payload['lat-lon']);
22+
23+
return Object.keys(payload).reduce((res, key) => {
24+
if (key === 't') {
25+
return { ...res, t: payload[key] };
26+
}
27+
28+
if (key === 'lat-lon') {
29+
res[capitalize(key)] = international.mapcode;
30+
} else {
31+
res[capitalize(key)] = payload[key];
32+
}
33+
34+
return res;
35+
}, {});
36+
}
37+
38+
async send(payload: TrackingEvent) {
39+
return this.client.request({
40+
method: 'POST',
41+
data: await this.transformData(payload),
42+
});
43+
}
44+
}

0 commit comments

Comments
 (0)