Skip to content

Commit 20bdb88

Browse files
update API
1 parent fe49ff7 commit 20bdb88

File tree

4 files changed

+208
-57
lines changed

4 files changed

+208
-57
lines changed

ui/src/App.test.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

ui/src/api/CWaterPumpAPI.js

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios from 'axios';
2+
import { CWaterPumpAPIImpl } from './CWaterPumpAPIImpl.js';
23

34
// helper function to preprocess the API host
45
function preprocessApiHost(apiHost) {
@@ -10,61 +11,16 @@ function preprocessApiHost(apiHost) {
1011
return url;
1112
}
1213

13-
function preprocessResponse(response) {
14-
if(null == response) return null;
15-
if('error' in response) {
16-
// TODO: handle errors in slice/SystemStatus.js
17-
throw new Error(response.error);
18-
}
19-
// normal response
20-
// convert "water threshold" to "waterThreshold"
21-
response.waterThreshold = response["water threshold"];
22-
delete response["water threshold"];
23-
24-
// convert "time left" to "timeLeft"
25-
response.pump.timeLeft = response.pump["time left"];
26-
delete response.pump["time left"];
27-
28-
// add field "updated"
29-
response.updated = Date.now();
30-
// difference between current time on client and time on device
31-
response.timeDelta = response.updated - response.time;
32-
// TODO: add field response.pump.estimatedEndTime
33-
return response;
34-
}
35-
36-
// TODO: probably we need to know "ping" time to sync time more accurately
37-
// Example:
38-
// 00:00.000 - client sends request
39-
// 00:00.100 - server receives request and set 'time' to 00:00.100, timeLeft = 1000ms
40-
// 00:00.200 - server sends response
41-
// 00:00.300 - client receives response, but 'time' is 00:00.100 and timeLeft = 1000ms
42-
// total time: 300ms
43-
// on average, time to one-way trip is 150ms
44-
// so, we adjust time by 150ms i.e. time = 00:00.250, timeLeft = 850ms
45-
// in this case, error is 50ms (150ms - actual 00:00.100), instead of 200ms (300ms - actual 00:00.100)
46-
//////////////////////////////////////////////////////////////////////
4714
class CWaterPumpAPI {
48-
constructor({ client=null, URL }) {
49-
this._client = client || axios.create({ baseURL: preprocessApiHost(URL) });
50-
}
51-
52-
async start(runTimeMs) {
53-
const response = await this._client.get('/pour_tea', {
54-
milliseconds: runTimeMs,
15+
constructor({ URL }) {
16+
this._impl = new CWaterPumpAPIImpl({
17+
client: axios.create({ baseURL: preprocessApiHost(URL) }),
5518
});
56-
return preprocessResponse(response.data);
5719
}
5820

59-
async stop() {
60-
const response = await this._client.get('/stop');
61-
return preprocessResponse(response.data);
62-
}
63-
64-
async status() {
65-
const response = await this._client.get('/status');
66-
return preprocessResponse(response.data);
67-
}
21+
async start(runTimeMs) { return await this._impl.start(runTimeMs); }
22+
async stop() { return await this._impl.stop(); }
23+
async status() { return await this._impl.status(); }
6824
}
6925

7026
export default CWaterPumpAPI;

ui/src/api/CWaterPumpAPIImpl.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
class CWaterPumpAPIImpl {
2+
constructor({ client, currentTime=null }) {
3+
this._client = client;
4+
this._currentTime = currentTime || Date.now;
5+
}
6+
7+
async _execute(callback) {
8+
const start = this._currentTime();
9+
const response = await callback();
10+
const end = this._currentTime();
11+
return { response, requestTime: end - start };
12+
}
13+
14+
async start(runTimeMs) {
15+
const { response: { data }, requestTime } = await this._execute(
16+
async () => await this._client.get('/pour_tea', { milliseconds: runTimeMs })
17+
);
18+
return this.preprocessResponse({ response: data, requestTime });
19+
}
20+
21+
async stop() {
22+
const { response: { data }, requestTime } = await this._execute(
23+
async () => await this._client.get('/stop', {})
24+
);
25+
return this.preprocessResponse({ response: data, requestTime });
26+
}
27+
28+
async status() {
29+
const { response: { data }, requestTime } = await this._execute(
30+
async () => await this._client.get('/status', {})
31+
);
32+
return this.preprocessResponse({ response: data, requestTime });
33+
}
34+
///////////////////////
35+
// helper functions
36+
preprocessResponse({ response, requestTime }) {
37+
if(null == response) return null;
38+
if('error' in response) {
39+
// TODO: handle errors in slice/SystemStatus.js
40+
throw new Error(response.error);
41+
}
42+
// make a deep copy of response
43+
response = JSON.parse(JSON.stringify(response));
44+
// normal response
45+
// convert "water threshold" to "waterThreshold"
46+
response.waterThreshold = response["water threshold"];
47+
delete response["water threshold"];
48+
49+
// convert "time left" to "timeLeft" and adjust time
50+
response.pump.timeLeft = response.pump["time left"];
51+
delete response.pump["time left"];
52+
53+
// adjust time by network delay
54+
const oneWayTripTime = Math.round(requestTime / 2);
55+
response.time += oneWayTripTime;
56+
response.pump.timeLeft -= oneWayTripTime;
57+
58+
const now = this._currentTime();
59+
response.updated = now;
60+
response.pump.estimatedEndTime = response.pump.timeLeft + now;
61+
return response;
62+
}
63+
}
64+
65+
export default CWaterPumpAPIImpl;
66+
export { CWaterPumpAPIImpl };

ui/src/api/CWaterPumpAPIImpl.test.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { CWaterPumpAPIImpl } from './CWaterPumpAPIImpl.js';
2+
3+
describe('CWaterPumpAPIImpl', () => {
4+
const DUMMY_STATUS = {
5+
pump: {
6+
"running": true,
7+
"time left": 1000,
8+
"water threshold": 100,
9+
},
10+
time: 1000,
11+
};
12+
// common test cases
13+
async function shouldThrowErrorFromResponse(apiCall) {
14+
const mockClient = { get: jest.fn() };
15+
const errorMessage = 'Error ' + Math.random();
16+
mockClient.get.mockResolvedValue({ data: { error: errorMessage } });
17+
18+
const api = new CWaterPumpAPIImpl({ client: mockClient });
19+
await expect(apiCall(api)).rejects.toThrow(errorMessage);
20+
}
21+
22+
async function shouldBeCalledWith(apiCall, url, params=null) {
23+
const mockClient = { get: jest.fn() };
24+
mockClient.get.mockResolvedValue({ data: DUMMY_STATUS });
25+
26+
const api = new CWaterPumpAPIImpl({ client: mockClient });
27+
await apiCall(api);
28+
29+
expect(mockClient.get).toHaveBeenCalledWith(url, params);
30+
}
31+
32+
async function shouldRethrowError(apiCall) {
33+
const mockClient = { get: jest.fn() };
34+
mockClient.get.mockRejectedValue(new Error('Network Error'));
35+
36+
const api = new CWaterPumpAPIImpl({ client: mockClient });
37+
await expect(apiCall(api)).rejects.toThrow('Network Error');
38+
}
39+
40+
async function shouldPreprocessResponse(apiCall) {
41+
const mockClient = { get: jest.fn() };
42+
mockClient.get.mockResolvedValue({ data: DUMMY_STATUS });
43+
44+
const api = new CWaterPumpAPIImpl({ client: mockClient });
45+
const response = await apiCall(api);
46+
47+
expect(response.waterThreshold).toBe(DUMMY_STATUS["water threshold"]);
48+
expect(response.pump.timeLeft).toBe(DUMMY_STATUS.pump["time left"]);
49+
expect(response).toHaveProperty('updated');
50+
}
51+
// end of common test cases
52+
// tests per method
53+
describe('start', () => {
54+
it('common test cases', async () => {
55+
const T = Math.random() * 1000;
56+
const callback = async (api) => await api.start(T);
57+
await shouldThrowErrorFromResponse(callback);
58+
await shouldRethrowError(callback);
59+
await shouldPreprocessResponse(callback);
60+
await shouldBeCalledWith(callback, '/pour_tea', { milliseconds: T });
61+
});
62+
});
63+
64+
describe('stop', () => {
65+
it('common test cases', async () => {
66+
const callback = async (api) => await api.stop();
67+
await shouldThrowErrorFromResponse(callback);
68+
await shouldRethrowError(callback);
69+
await shouldPreprocessResponse(callback);
70+
await shouldBeCalledWith(callback, '/stop', {});
71+
});
72+
});
73+
74+
describe('status', () => {
75+
it('common test cases', async () => {
76+
const callback = async (api) => await api.status();
77+
await shouldThrowErrorFromResponse(callback);
78+
await shouldRethrowError(callback);
79+
await shouldPreprocessResponse(callback);
80+
await shouldBeCalledWith(callback, '/status', {});
81+
});
82+
});
83+
// tests for helper function preprocessResponse
84+
describe('preprocessResponse', () => {
85+
it('should return null if response is null', () => {
86+
const api = new CWaterPumpAPIImpl({ client: {} });
87+
expect(api.preprocessResponse({ response: null, requestTime: 0 })).toBeNull();
88+
});
89+
90+
it('should throw error if response has error', () => {
91+
const api = new CWaterPumpAPIImpl({ client: {} });
92+
const errorMessage = 'Error ' + Math.random();
93+
expect(() => api.preprocessResponse({
94+
response: { error: errorMessage },
95+
requestTime: 0,
96+
})).toThrow(errorMessage);
97+
});
98+
99+
it('should preprocess response', () => {
100+
const api = new CWaterPumpAPIImpl({ client: {} });
101+
const response = api.preprocessResponse({ response: DUMMY_STATUS, requestTime: 0 });
102+
expect(response.waterThreshold).toBe(DUMMY_STATUS["water threshold"]);
103+
expect(response.pump.timeLeft).toBe(DUMMY_STATUS.pump["time left"]);
104+
});
105+
106+
it('should add field "updated" with current time', () => {
107+
const T = Math.random() * 1000;
108+
const api = new CWaterPumpAPIImpl({ client: {}, currentTime: () => T });
109+
const response = api.preprocessResponse({ response: DUMMY_STATUS, requestTime: 0 });
110+
expect(response.updated).toBe(T);
111+
});
112+
113+
///////////
114+
// Scenario:
115+
// 00:00.000 - client sends request
116+
// 00:00.100 - server receives request and set 'time' to 00:00.100, timeLeft = 1234ms
117+
// 00:00.200 - server sends response
118+
// 00:00.300 - client receives response, but 'time' is 00:00.100 and timeLeft = 1234ms
119+
// total time: 300ms
120+
// on average, time to one-way trip is 150ms
121+
// so, we adjust time by 150ms i.e. time = 00:00.250, timeLeft = 1084ms
122+
// estimatedEndTime = 00:00.300 + 1084ms = 00:01.384
123+
it('should adjust time', () => {
124+
const responseObj = JSON.parse(JSON.stringify(DUMMY_STATUS));
125+
responseObj.time = 100;
126+
responseObj.pump["time left"] = 1234;
127+
128+
const api = new CWaterPumpAPIImpl({ client: {}, currentTime: () => 300 });
129+
const response = api.preprocessResponse({ response: responseObj, requestTime: 300 });
130+
expect(response.time).toBe(250);
131+
expect(response.pump.timeLeft).toBe(1084);
132+
expect(response.pump.estimatedEndTime).toBe(1384);
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)