Skip to content

Commit

Permalink
feat(geo): add updateGeofences api (#9312)
Browse files Browse the repository at this point in the history
* feat(geo): add listGeofences api

* feat(geo): add initial updateGeofences api

* refactor(geo): optimize _batchPutGeofence function
  • Loading branch information
TreTuna authored and Tré Ammatuna committed Dec 13, 2021
1 parent 09371c5 commit ef9bfba
Show file tree
Hide file tree
Showing 6 changed files with 429 additions and 85 deletions.
87 changes: 87 additions & 0 deletions packages/geo/__tests__/Geo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,93 @@ describe('Geo', () => {
});
});

describe('updateGeofences', () => {
test('updateGeofences with a single geofence', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});

LocationClient.prototype.send = jest
.fn()
.mockImplementationOnce(mockBatchPutGeofenceCommand);

const geo = new GeoClass();
geo.configure(awsConfig);

// Check that results are what's expected
const results = await geo.updateGeofences(validGeofence1);
expect(results).toEqual(singleGeofenceCamelcaseResults);

// Expect that the API was called with the proper input
const spyon = jest.spyOn(LocationClient.prototype, 'send');
const input = spyon.mock.calls[0][0].input;
const output = {
Entries: [
{
GeofenceId: validGeofence1.geofenceId,
Geometry: {
Polygon: validGeofence1.geometry.polygon,
},
},
],
CollectionName: 'geofenceCollectionExample',
};
expect(input).toEqual(output);
});

test('updateGeofences with multiple geofences', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});

LocationClient.prototype.send = jest
.fn()
.mockImplementation(mockBatchPutGeofenceCommand);

const geo = new GeoClass();
geo.configure(awsConfig);

// Check that results are what's expected
const results = await geo.updateGeofences(validGeofences);
expect(results).toEqual(batchGeofencesCamelcaseResults);

// Expect that the API was called the right amount of times
const expectedNumberOfCalls = Math.floor(validGeofences.length / 10) + 1;
expect(LocationClient.prototype.send).toHaveBeenCalledTimes(
expectedNumberOfCalls
);
});

test('should error if there is a bad geofence in the input', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});

const geo = new GeoClass();
geo.configure(awsConfig);

await expect(
geo.updateGeofences(geofencesWithInvalidId)
).rejects.toThrowError(
`Invalid geofenceId: t|-|!$ !$ N()T V@|_!D Ids can only contain alphanumeric characters, hyphens, underscores and periods.`
);
});

test('should fail if there is no provider', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});

const geo = new GeoClass();
geo.configure(awsConfig);
geo.removePluggable('AmazonLocationService');

await expect(geo.updateGeofences(validGeofence1)).rejects.toThrow(
'No plugin found in Geo for the provider'
);
});
});

describe('getGeofence', () => {
test('getGeofence returns the right geofence', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
Expand Down
136 changes: 132 additions & 4 deletions packages/geo/__tests__/Providers/AmazonLocationServiceProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ describe('AmazonLocationServiceProvider', () => {

describe('createGeofences', () => {
test('createGeofences with multiple geofences', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
jest.spyOn(Credentials, 'get').mockImplementation(() => {
return Promise.resolve(credentials);
});

Expand All @@ -389,14 +389,15 @@ describe('AmazonLocationServiceProvider', () => {
});

test('createGeofences calls batchPutGeofences in batches of 10 from input', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
jest.spyOn(Credentials, 'get').mockImplementation(() => {
return Promise.resolve(credentials);
});

const locationProvider = new AmazonLocationServiceProvider();
locationProvider.configure(awsConfig.geo.amazon_location_service);

const input = createGeofenceInputArray(44);
const numberOfGeofences = 44;
const input = createGeofenceInputArray(numberOfGeofences);

const spyonProvider = jest.spyOn(locationProvider, 'createGeofences');
const spyonClient = jest.spyOn(LocationClient.prototype, 'send');
Expand Down Expand Up @@ -425,7 +426,7 @@ describe('AmazonLocationServiceProvider', () => {
});

test('createGeofences properly handles errors with bad network calls', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
jest.spyOn(Credentials, 'get').mockImplementation(() => {
return Promise.resolve(credentials);
});

Expand Down Expand Up @@ -496,6 +497,133 @@ describe('AmazonLocationServiceProvider', () => {
});
});

describe('updateGeofences', () => {
test('updateGeofences with multiple geofences', async () => {
jest.spyOn(Credentials, 'get').mockImplementation(() => {
return Promise.resolve(credentials);
});

LocationClient.prototype.send = jest
.fn()
.mockImplementation(mockBatchPutGeofenceCommand);

const locationProvider = new AmazonLocationServiceProvider();
locationProvider.configure(awsConfig.geo.amazon_location_service);

const results = await locationProvider.updateGeofences(validGeofences);

expect(results).toEqual(batchGeofencesCamelcaseResults);
});

test('updateGeofences calls batchPutGeofences in batches of 10 from input', async () => {
jest.spyOn(Credentials, 'get').mockImplementation(() => {
return Promise.resolve(credentials);
});

const locationProvider = new AmazonLocationServiceProvider();
locationProvider.configure(awsConfig.geo.amazon_location_service);

const numberOfGeofences = 44;
const input = createGeofenceInputArray(numberOfGeofences);

const spyonProvider = jest.spyOn(locationProvider, 'updateGeofences');
const spyonClient = jest.spyOn(LocationClient.prototype, 'send');

const results = await locationProvider.updateGeofences(input);

const expected = {
successes: input.map(({ geofenceId }) => {
return {
geofenceId,
createTime: '2020-04-01T21:00:00.000Z',
updateTime: '2020-04-01T21:00:00.000Z',
};
}),
errors: [],
};
expect(results).toEqual(expected);

const spyProviderInput = spyonProvider.mock.calls[0][0];

const spyClientInput = spyonClient.mock.calls;

expect(spyClientInput.length).toEqual(
Math.ceil(spyProviderInput.length / 10)
);
});

test('updateGeofences properly handles errors with bad network calls', async () => {
jest.spyOn(Credentials, 'get').mockImplementation(() => {
return Promise.resolve(credentials);
});

const locationProvider = new AmazonLocationServiceProvider();
locationProvider.configure(awsConfig.geo.amazon_location_service);

const input = createGeofenceInputArray(44);
input[22].geofenceId = 'badId';
const validEntries = [...input.slice(0, 20), ...input.slice(30, 44)];

const spyonClient = jest.spyOn(LocationClient.prototype, 'send');
spyonClient.mockImplementation(geofenceInput => {
const entries = geofenceInput.input as any;

if (entries.Entries.some(entry => entry.GeofenceId === 'badId')) {
return Promise.reject(new Error('Bad network call'));
}

const resolution = {
Successes: entries.Entries.map(({ GeofenceId }) => {
return {
GeofenceId,
CreateTime: '2020-04-01T21:00:00.000Z',
UpdateTime: '2020-04-01T21:00:00.000Z',
};
}),
Errors: [],
};
return Promise.resolve(resolution);
});

const results = await locationProvider.updateGeofences(input);
const badResults = input.slice(20, 30).map(input => {
return {
error: {
code: 'APIConnectionError',
message: 'Bad network call',
},
geofenceId: input.geofenceId,
};
});
const expected = {
successes: validEntries.map(({ geofenceId }) => {
return {
geofenceId,
createTime: '2020-04-01T21:00:00.000Z',
updateTime: '2020-04-01T21:00:00.000Z',
};
}),
errors: badResults,
};
expect(results).toEqual(expected);
});

test('should error if there are no geofenceCollections in config', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});

const locationProvider = new AmazonLocationServiceProvider();
locationProvider.configure({});

await expect(
locationProvider.updateGeofences(validGeofences)
).rejects.toThrow(
'No Geofence Collections found, please run `amplify add geo` to create one and run `amplify push` after.'
);
});
});

describe('getGeofence', () => {
test('getGeofence returns the right geofence', async () => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
Expand Down
39 changes: 28 additions & 11 deletions packages/geo/__tests__/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,21 +263,38 @@ export function mockBatchPutGeofenceCommand(command) {
Errors: [],
};
}
if (command instanceof GetGeofenceCommand) {
return mockGetGeofenceCommand(command);
}
}

export function mockGetGeofenceCommand(command) {
const geofence = {
GeofenceId: command.input.GeofenceId,
Geometry: {
Polygon: validPolygon,
},
CreateTime: '2020-04-01T21:00:00.000Z',
UpdateTime: '2020-04-01T21:00:00.000Z',
Status: 'ACTIVE',
};

if (command instanceof GetGeofenceCommand) {
return geofence;
return {
GeofenceId: command.input.GeofenceId,
Geometry: {
Polygon: validPolygon,
},
CreateTime: '2020-04-01T21:00:00.000Z',
UpdateTime: '2020-04-01T21:00:00.000Z',
Status: 'ACTIVE',
};
}
}

export function mockListGeofencesCommand(command) {
if (command instanceof ListGeofencesCommand) {
const geofences = createGeofenceOutputArray(200);
if (command.input.NextToken === 'THIS IS YOUR TOKEN') {
return {
Entries: geofences.slice(100, 200),
NextToken: 'THIS IS YOUR SECOND TOKEN',
};
}
return {
Entries: geofences.slice(0, 100),
NextToken: 'THIS IS YOUR TOKEN',
};
}
}

Expand Down
39 changes: 39 additions & 0 deletions packages/geo/src/Geo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,45 @@ export class GeoClass {
}
}

/**
* Update geofences inside of a geofence collection
* @param geofences - Single or array of geofence objects to create
* @param options? - Optional parameters for creating geofences
* @returns {Promise<CreateUpdateGeofenceResults>} - Promise that resolves to an object with:
* successes: list of geofences successfully created
* errors: list of geofences that failed to create
*/
public async updateGeofences(
geofences: GeofenceInput | GeofenceInput[],
options?: GeofenceOptions
): Promise<CreateUpdateGeofenceResults> {
const { providerName = DEFAULT_PROVIDER } = options || {};
const prov = this.getPluggable(providerName);

// If single geofence input, make it an array for batch call
let geofenceInputArray;
if (!Array.isArray(geofences)) {
geofenceInputArray = [geofences];
} else {
geofenceInputArray = geofences;
}

// Validate all geofences are unique and valid
try {
validateGeofences(geofenceInputArray);
} catch (error) {
logger.debug(error);
throw error;
}

try {
return await prov.updateGeofences(geofenceInputArray, options);
} catch (error) {
logger.debug(error);
throw error;
}
}

public async getGeofence(
geofenceId: string,
options?: GeofenceOptions
Expand Down
Loading

0 comments on commit ef9bfba

Please sign in to comment.