Skip to content

Commit

Permalink
update API
Browse files Browse the repository at this point in the history
  • Loading branch information
GreenWizard2015 committed Jan 10, 2024
1 parent fe49ff7 commit 20bdb88
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 57 deletions.
6 changes: 0 additions & 6 deletions ui/src/App.test.js

This file was deleted.

58 changes: 7 additions & 51 deletions ui/src/api/CWaterPumpAPI.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios';
import { CWaterPumpAPIImpl } from './CWaterPumpAPIImpl.js';

// helper function to preprocess the API host
function preprocessApiHost(apiHost) {
Expand All @@ -10,61 +11,16 @@ function preprocessApiHost(apiHost) {
return url;
}

function preprocessResponse(response) {
if(null == response) return null;
if('error' in response) {
// TODO: handle errors in slice/SystemStatus.js
throw new Error(response.error);
}
// normal response
// convert "water threshold" to "waterThreshold"
response.waterThreshold = response["water threshold"];
delete response["water threshold"];

// convert "time left" to "timeLeft"
response.pump.timeLeft = response.pump["time left"];
delete response.pump["time left"];

// add field "updated"
response.updated = Date.now();
// difference between current time on client and time on device
response.timeDelta = response.updated - response.time;
// TODO: add field response.pump.estimatedEndTime
return response;
}

// TODO: probably we need to know "ping" time to sync time more accurately
// Example:
// 00:00.000 - client sends request
// 00:00.100 - server receives request and set 'time' to 00:00.100, timeLeft = 1000ms
// 00:00.200 - server sends response
// 00:00.300 - client receives response, but 'time' is 00:00.100 and timeLeft = 1000ms
// total time: 300ms
// on average, time to one-way trip is 150ms
// so, we adjust time by 150ms i.e. time = 00:00.250, timeLeft = 850ms
// in this case, error is 50ms (150ms - actual 00:00.100), instead of 200ms (300ms - actual 00:00.100)
//////////////////////////////////////////////////////////////////////
class CWaterPumpAPI {
constructor({ client=null, URL }) {
this._client = client || axios.create({ baseURL: preprocessApiHost(URL) });
}

async start(runTimeMs) {
const response = await this._client.get('/pour_tea', {
milliseconds: runTimeMs,
constructor({ URL }) {
this._impl = new CWaterPumpAPIImpl({
client: axios.create({ baseURL: preprocessApiHost(URL) }),
});
return preprocessResponse(response.data);
}

async stop() {
const response = await this._client.get('/stop');
return preprocessResponse(response.data);
}

async status() {
const response = await this._client.get('/status');
return preprocessResponse(response.data);
}
async start(runTimeMs) { return await this._impl.start(runTimeMs); }
async stop() { return await this._impl.stop(); }
async status() { return await this._impl.status(); }
}

export default CWaterPumpAPI;
Expand Down
66 changes: 66 additions & 0 deletions ui/src/api/CWaterPumpAPIImpl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
class CWaterPumpAPIImpl {
constructor({ client, currentTime=null }) {
this._client = client;
this._currentTime = currentTime || Date.now;
}

async _execute(callback) {
const start = this._currentTime();
const response = await callback();
const end = this._currentTime();
return { response, requestTime: end - start };
}

async start(runTimeMs) {
const { response: { data }, requestTime } = await this._execute(
async () => await this._client.get('/pour_tea', { milliseconds: runTimeMs })
);
return this.preprocessResponse({ response: data, requestTime });
}

async stop() {
const { response: { data }, requestTime } = await this._execute(
async () => await this._client.get('/stop', {})
);
return this.preprocessResponse({ response: data, requestTime });
}

async status() {
const { response: { data }, requestTime } = await this._execute(
async () => await this._client.get('/status', {})
);
return this.preprocessResponse({ response: data, requestTime });
}
///////////////////////
// helper functions
preprocessResponse({ response, requestTime }) {
if(null == response) return null;
if('error' in response) {
// TODO: handle errors in slice/SystemStatus.js
throw new Error(response.error);
}
// make a deep copy of response
response = JSON.parse(JSON.stringify(response));
// normal response
// convert "water threshold" to "waterThreshold"
response.waterThreshold = response["water threshold"];
delete response["water threshold"];

// convert "time left" to "timeLeft" and adjust time
response.pump.timeLeft = response.pump["time left"];
delete response.pump["time left"];

// adjust time by network delay
const oneWayTripTime = Math.round(requestTime / 2);
response.time += oneWayTripTime;
response.pump.timeLeft -= oneWayTripTime;

const now = this._currentTime();
response.updated = now;
response.pump.estimatedEndTime = response.pump.timeLeft + now;
return response;
}
}

export default CWaterPumpAPIImpl;
export { CWaterPumpAPIImpl };
135 changes: 135 additions & 0 deletions ui/src/api/CWaterPumpAPIImpl.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { CWaterPumpAPIImpl } from './CWaterPumpAPIImpl.js';

describe('CWaterPumpAPIImpl', () => {
const DUMMY_STATUS = {
pump: {
"running": true,
"time left": 1000,
"water threshold": 100,
},
time: 1000,
};
// common test cases
async function shouldThrowErrorFromResponse(apiCall) {
const mockClient = { get: jest.fn() };
const errorMessage = 'Error ' + Math.random();
mockClient.get.mockResolvedValue({ data: { error: errorMessage } });

const api = new CWaterPumpAPIImpl({ client: mockClient });
await expect(apiCall(api)).rejects.toThrow(errorMessage);
}

async function shouldBeCalledWith(apiCall, url, params=null) {
const mockClient = { get: jest.fn() };
mockClient.get.mockResolvedValue({ data: DUMMY_STATUS });

const api = new CWaterPumpAPIImpl({ client: mockClient });
await apiCall(api);

expect(mockClient.get).toHaveBeenCalledWith(url, params);
}

async function shouldRethrowError(apiCall) {
const mockClient = { get: jest.fn() };
mockClient.get.mockRejectedValue(new Error('Network Error'));

const api = new CWaterPumpAPIImpl({ client: mockClient });
await expect(apiCall(api)).rejects.toThrow('Network Error');
}

async function shouldPreprocessResponse(apiCall) {
const mockClient = { get: jest.fn() };
mockClient.get.mockResolvedValue({ data: DUMMY_STATUS });

const api = new CWaterPumpAPIImpl({ client: mockClient });
const response = await apiCall(api);

expect(response.waterThreshold).toBe(DUMMY_STATUS["water threshold"]);
expect(response.pump.timeLeft).toBe(DUMMY_STATUS.pump["time left"]);
expect(response).toHaveProperty('updated');
}
// end of common test cases
// tests per method
describe('start', () => {
it('common test cases', async () => {
const T = Math.random() * 1000;
const callback = async (api) => await api.start(T);
await shouldThrowErrorFromResponse(callback);
await shouldRethrowError(callback);
await shouldPreprocessResponse(callback);
await shouldBeCalledWith(callback, '/pour_tea', { milliseconds: T });
});
});

describe('stop', () => {
it('common test cases', async () => {
const callback = async (api) => await api.stop();
await shouldThrowErrorFromResponse(callback);
await shouldRethrowError(callback);
await shouldPreprocessResponse(callback);
await shouldBeCalledWith(callback, '/stop', {});
});
});

describe('status', () => {
it('common test cases', async () => {
const callback = async (api) => await api.status();
await shouldThrowErrorFromResponse(callback);
await shouldRethrowError(callback);
await shouldPreprocessResponse(callback);
await shouldBeCalledWith(callback, '/status', {});
});
});
// tests for helper function preprocessResponse
describe('preprocessResponse', () => {
it('should return null if response is null', () => {
const api = new CWaterPumpAPIImpl({ client: {} });
expect(api.preprocessResponse({ response: null, requestTime: 0 })).toBeNull();
});

it('should throw error if response has error', () => {
const api = new CWaterPumpAPIImpl({ client: {} });
const errorMessage = 'Error ' + Math.random();
expect(() => api.preprocessResponse({
response: { error: errorMessage },
requestTime: 0,
})).toThrow(errorMessage);
});

it('should preprocess response', () => {
const api = new CWaterPumpAPIImpl({ client: {} });
const response = api.preprocessResponse({ response: DUMMY_STATUS, requestTime: 0 });
expect(response.waterThreshold).toBe(DUMMY_STATUS["water threshold"]);
expect(response.pump.timeLeft).toBe(DUMMY_STATUS.pump["time left"]);
});

it('should add field "updated" with current time', () => {
const T = Math.random() * 1000;
const api = new CWaterPumpAPIImpl({ client: {}, currentTime: () => T });
const response = api.preprocessResponse({ response: DUMMY_STATUS, requestTime: 0 });
expect(response.updated).toBe(T);
});

///////////
// Scenario:
// 00:00.000 - client sends request
// 00:00.100 - server receives request and set 'time' to 00:00.100, timeLeft = 1234ms
// 00:00.200 - server sends response
// 00:00.300 - client receives response, but 'time' is 00:00.100 and timeLeft = 1234ms
// total time: 300ms
// on average, time to one-way trip is 150ms
// so, we adjust time by 150ms i.e. time = 00:00.250, timeLeft = 1084ms
// estimatedEndTime = 00:00.300 + 1084ms = 00:01.384
it('should adjust time', () => {
const responseObj = JSON.parse(JSON.stringify(DUMMY_STATUS));
responseObj.time = 100;
responseObj.pump["time left"] = 1234;

const api = new CWaterPumpAPIImpl({ client: {}, currentTime: () => 300 });
const response = api.preprocessResponse({ response: responseObj, requestTime: 300 });
expect(response.time).toBe(250);
expect(response.pump.timeLeft).toBe(1084);
expect(response.pump.estimatedEndTime).toBe(1384);
});
});
});

0 comments on commit 20bdb88

Please sign in to comment.