Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,23 @@ jobs:
spec: websocket-disruption
browser: << parameters.browser >>

integ_react_api_optimistic_ui:
parameters:
browser:
type: string
executor: js-test-executor
<<: *test_env_vars
working_directory: ~/amplify-js-samples-staging/samples/react/api/optimistic-ui
steps:
- prepare_test_env
- integ_test_js:
test_name: 'API (GraphQL) - Optimistic UI'
framework: react
category: api
sample_name: optimistic-ui
spec: optimistic-ui
browser: << parameters.browser >>

deploy:
executor: macos-executor
working_directory: ~/amplify-js
Expand Down Expand Up @@ -2237,6 +2254,15 @@ workflows:
matrix:
parameters:
<<: *minimal_browser_list
- integ_react_api_optimistic_ui:
requires:
- integ_setup
- build
filters:
<<: *releasable_branches
matrix:
parameters:
<<: *extended_browser_list
- deploy:
filters:
<<: *releasable_branches
Expand Down Expand Up @@ -2308,6 +2334,7 @@ workflows:
- integ_vanilla_js_datastore_basic_crud
- integ_react_datastore_docs_examples
- integ_react_datastore_websocket_disruption
- integ_react_api_optimistic_ui
- post_release:
filters:
branches:
Expand Down
1 change: 1 addition & 0 deletions license_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"**/lib-esm",
"**/node_modules",
"**/pushnotification",
"**/polyfills/URL/*.js",
"**/vendor",
"**/__tests__",
"**/__mocks__"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"setup-dev": "yarn && yarn bootstrap && yarn link-all && yarn build",
"setup-dev:react-native": "node ./scripts/setup-dev-rn",
"bootstrap": "lerna bootstrap",
"test": "lerna run test --stream",
"test": "lerna run test --stream && yarn test:license",
"test:size": "lerna run test:size --no-bail",
"test:duplicates": "./scripts/duplicates-yarn.sh",
"test:license": "license-check-and-add check -f license_config.json",
Expand Down
79 changes: 29 additions & 50 deletions packages/api-rest/__tests__/RestAPI-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Signer, Credentials, DateUtils } from '@aws-amplify/core';

jest.mock('axios');

const mockAxios = jest.spyOn(axios as any, 'default');

axios.CancelToken = <CancelTokenStatic>{
source: () => ({ token: null, cancel: null }),
};
Expand All @@ -28,6 +30,7 @@ const config = {

afterEach(() => {
jest.restoreAllMocks();
mockAxios.mockClear();
});

describe('Rest API test', () => {
Expand Down Expand Up @@ -166,19 +169,11 @@ describe('Rest API test', () => {
api.configure(custom_config);
const spyon = jest
.spyOn(Credentials, 'get')
.mockImplementationOnce(() => {
return new Promise((res, rej) => {
res('cred');
});
});
.mockResolvedValueOnce('cred');

const spyonRequest = jest
.spyOn(RestClient.prototype as any, '_request')
.mockImplementationOnce(() => {
return new Promise((res, rej) => {
res({});
});
});
.mockResolvedValueOnce({});
await api.get('apiName', 'path', {});

expect(spyonRequest).toBeCalledWith(
Expand Down Expand Up @@ -221,25 +216,15 @@ describe('Rest API test', () => {
session_token: 'token',
};

const spyon = jest.spyOn(Credentials, 'get').mockImplementation(() => {
return new Promise((res, rej) => {
res(creds);
});
});
const spyon = jest.spyOn(Credentials, 'get').mockResolvedValue(creds);

const spyonSigner = jest
.spyOn(Signer, 'sign')
.mockImplementationOnce(() => {
return { headers: {} };
});

const spyAxios = jest
.spyOn(axios as any, 'default')
.mockImplementationOnce(() => {
return new Promise((res, rej) => {
res(resp);
});
});
mockAxios.mockResolvedValue(resp);

const init = {
timeout: 2500,
Expand Down Expand Up @@ -297,13 +282,7 @@ describe('Rest API test', () => {
return { headers: {} };
});

const spyAxios = jest
.spyOn(axios as any, 'default')
.mockImplementationOnce(() => {
return new Promise((res, rej) => {
res(resp);
});
});
mockAxios.mockResolvedValue(resp);

const init = {
queryStringParameters: {
Expand Down Expand Up @@ -363,13 +342,7 @@ describe('Rest API test', () => {
return { headers: {} };
});

const spyAxios = jest
.spyOn(axios as any, 'default')
.mockImplementationOnce(() => {
return new Promise((res, rej) => {
res(resp);
});
});
mockAxios.mockResolvedValue(resp);

const init = {
queryStringParameters: {
Expand Down Expand Up @@ -429,13 +402,7 @@ describe('Rest API test', () => {
return { headers: {} };
});

const spyAxios = jest
.spyOn(axios as any, 'default')
.mockImplementationOnce(() => {
return new Promise((res, rej) => {
res(resp);
});
});
mockAxios.mockResolvedValue(resp);

const init = {
queryStringParameters: {
Expand Down Expand Up @@ -557,19 +524,31 @@ describe('Rest API test', () => {
.mockImplementation(() => 'endpoint');

jest.spyOn(Credentials, 'get').mockResolvedValue('creds');

jest
.spyOn(RestClient.prototype as any, '_signed')
.mockRejectedValueOnce(normalError);
jest.spyOn(RestClient.prototype as any, '_sign').mockReturnValue({
...init,
headers: { ...init.headers, Authorization: 'signed' },
});
mockAxios.mockImplementationOnce(() => {
return new Promise((_, rej) => {
rej(normalError);
});
});

await expect(api.post('url', 'path', init)).rejects.toThrow(normalError);

// Clock should not be skewed from normal errors
expect(DateUtils.getClockOffset()).toBe(0);

jest
.spyOn(RestClient.prototype as any, '_signed')
.mockRejectedValueOnce(clockSkewError);
// mock clock skew error response and successful response after retry
mockAxios
.mockImplementationOnce(() => {
return new Promise((_, rej) => {
rej(clockSkewError);
});
})
.mockResolvedValue({
data: [{ name: 'Bob' }],
});

await expect(api.post('url', 'path', init)).resolves.toEqual([
{ name: 'Bob' },
Expand Down
2 changes: 1 addition & 1 deletion packages/api-rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"name": "API (rest client)",
"path": "./lib-esm/index.js",
"import": "{ Amplify, RestAPI }",
"limit": "30.91 kB"
"limit": "31 kB"
}
],
"jest": {
Expand Down
76 changes: 37 additions & 39 deletions packages/api-rest/src/RestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,39 +171,42 @@ export class RestClient {
return this._request(params, isAllResponse);
}

// Signing the request in case there credentials are available
return this.Credentials.get().then(
credentials => {
return this._signed({ ...params }, credentials, isAllResponse, {
region,
service,
}).catch(error => {
if (DateUtils.isClockSkewError(error)) {
const { headers } = error.response;
const dateHeader = headers && (headers.date || headers.Date);
const responseDate = new Date(dateHeader);
const requestDate = DateUtils.getDateFromHeaderString(
params.headers['x-amz-date']
);

// Compare local clock to the server clock
if (DateUtils.isClockSkewed(responseDate)) {
DateUtils.setClockOffset(
responseDate.getTime() - requestDate.getTime()
);

return this.ajax(urlOrApiInfo, method, init);
}
}

throw error;
});
},
err => {
logger.debug('No credentials available, the request will be unsigned');
return this._request(params, isAllResponse);
let credentials;
try {
credentials = await this.Credentials.get();
} catch (error) {
logger.debug('No credentials available, the request will be unsigned');
return this._request(params, isAllResponse);
}
let signedParams;
try {
signedParams = this._sign({ ...params }, credentials, {
region,
service,
});
const response = await axios(signedParams);
return isAllResponse ? response : response.data;
} catch (error) {
logger.debug(error);
if (DateUtils.isClockSkewError(error)) {
const { headers } = error.response;
const dateHeader = headers && (headers.date || headers.Date);
const responseDate = new Date(dateHeader);
const requestDate = DateUtils.getDateFromHeaderString(
signedParams.headers['x-amz-date']
);

// Compare local clock to the server clock
if (DateUtils.isClockSkewed(responseDate)) {
DateUtils.setClockOffset(
responseDate.getTime() - requestDate.getTime()
);

return this.ajax(urlOrApiInfo, method, init);
}
}
);
throw error;
}
}

/**
Expand Down Expand Up @@ -356,7 +359,7 @@ export class RestClient {

/** private methods **/

private _signed(params, credentials, isAllResponse, { service, region }) {
private _sign(params, credentials, { service, region }) {
const { signerServiceInfo: signerServiceInfoParams, ...otherParams } =
params;

Expand Down Expand Up @@ -391,12 +394,7 @@ export class RestClient {

delete signed_params.headers['host'];

return axios(signed_params)
.then(response => (isAllResponse ? response : response.data))
.catch(error => {
logger.debug(error);
throw error;
});
return signed_params;
}

private _request(params, isAllResponse = false) {
Expand Down
18 changes: 14 additions & 4 deletions packages/core/__tests__/Platform-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,18 @@ describe('Platform test', () => {
expect(
getAmplifyUserAgentObject({
category: Category.API,
action: ApiAction.None,
action: ApiAction.GraphQl,
additionalInfo: [
['amplify-ui', '1.x.x'],
['uicomponent', '1'],
],
})
).toStrictEqual([
['aws-amplify', version],
[Category.API, ApiAction.None],
[Category.API, ApiAction.GraphQl],
['framework', Framework.WebUnknown],
['amplify-ui', '1.x.x'],
['uicomponent', '1'],
]);
});
});
Expand All @@ -50,10 +56,14 @@ describe('Platform test', () => {
expect(
getAmplifyUserAgent({
category: Category.API,
action: ApiAction.None,
action: ApiAction.GraphQl,
additionalInfo: [
['amplify-ui', '1.x.x'],
['uicomponent', '1'],
],
})
).toBe(
`${Platform.userAgent} ${Category.API}/${ApiAction.None} framework/${Framework.WebUnknown}`
`${Platform.userAgent} ${Category.API}/${ApiAction.GraphQl} framework/${Framework.WebUnknown} amplify-ui/1.x.x uicomponent/1`
);
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
"name": "Core (Credentials)",
"path": "./lib-esm/index.js",
"import": "{ Credentials }",
"limit": "13.3 kB"
"limit": "13.33 kB"
},
{
"name": "Core (Signer)",
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/Platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { CustomUserAgentDetails, Framework } from './types';
import { version } from './version';
import { detectFramework, observeFrameworkChanges } from './detectFramework';
import { UserAgent as AWSUserAgent } from '@aws-sdk/types';
import type { UserAgent as AWSUserAgent } from '@aws-sdk/types';

const BASE_USER_AGENT = `aws-amplify`;

Expand Down Expand Up @@ -32,12 +32,17 @@ export const getAmplifyUserAgentObject = ({
category,
action,
framework,
additionalInfo,
}: CustomUserAgentDetails = {}): AWSUserAgent => {
const userAgent: AWSUserAgent = [[BASE_USER_AGENT, version]];
let userAgent: AWSUserAgent = [[BASE_USER_AGENT, version]];
if (category) {
userAgent.push([category, action]);
}
userAgent.push(['framework', detectFramework()]);
userAgent.push(['framework', framework || detectFramework()]);

if (additionalInfo) {
userAgent = userAgent.concat(additionalInfo);
}

return userAgent;
};
Expand Down
Loading