Skip to content

Commit

Permalink
Merge pull request #526 from kubeshop/f1ames/fix/synchronizer-explici…
Browse files Browse the repository at this point in the history
…t-auth-errors

fix(synchronizer): add more explicit GraphQL API error handling
  • Loading branch information
f1ames authored Sep 21, 2023
2 parents 9ce05d5 + 7838f08 commit 5669efb
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-knives-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@monokle/synchronizer": patch
---

Added more explicit GraphQL API error handling
90 changes: 90 additions & 0 deletions packages/synchronizer/src/__tests__/synchronizer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,96 @@ describe('Synchronizer Tests', () => {
assert.deepEqual(newPolicy, getPolicyResult);
});

it('throws unauthorized error when invalid auth credentials passed', async () => {
const storagePath = await createTmpConfigDir();
const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath));

const sendRequestStub = sinon
.stub((synchronizer as any)._apiHandler, 'sendRequest')
.callsFake(async (...args) => {
return {
ok: true,
json: async () => {
return {
errors: [
{
message: 'Unauthorized',
locations: [
{
line: 2,
column: 3,
},
],
path: ['getProject'],
extensions: {
code: 'UNAUTHENTICATED',
originalError: {
statusCode: 401,
message: 'Unauthorized',
},
},
},
],
data: null,
};
},
};
});
stubs.push(sendRequestStub);

const policyData = {
slug: 'user6-proj-abc',
};

try {
await synchronizer.getPolicy(policyData, true, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'ApiKey',
});
assert.fail('Should have thrown error.');
} catch (err: any) {
assert.match(err.message, /Unauthorized error\. Make sure that valid auth/);
}
});

it('throws when there is no policy for project (project slug)', async () => {
const storagePath = await createTmpConfigDir();
const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath));

const sendRequestStub = sinon
.stub((synchronizer as any)._apiHandler, 'sendRequest')
.callsFake(async (...args) => {
return {
ok: true,
json: async () => {
return {
data: {
getProject: {
id: 6000,
name: 'User6 Project',
},
},
};
},
};
});
stubs.push(sendRequestStub);

const policyData = {
slug: 'user6-proj-abc',
};

try {
await synchronizer.getPolicy(policyData, true, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'ApiKey',
});
assert.fail('Should have thrown error.');
} catch (err: any) {
assert.match(err.message, /project does not have policy defined\. Configure it/);
}
});

it('emits synchronize event after policy is fetched', async () => {
const storagePath = await createTmpConfigDir();
const synchronizer = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath));
Expand Down
37 changes: 28 additions & 9 deletions packages/synchronizer/src/handlers/apiHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,35 @@ export class ApiHandler {

private async queryApi<OUT>(query: string, tokenInfo: TokenInfo, variables = {}): Promise<OUT | undefined> {
const apiEndpointUrl = normalizeUrl(`${this.apiUrl}/graphql`);
const response = await this.sendRequest(apiEndpointUrl, tokenInfo, query, variables);

const response = await fetch(apiEndpointUrl, {
if (!response.ok) {
throw new Error(
`Connection error. Cannot fetch data from ${apiEndpointUrl}. Error '${response.statusText}' (${response.status}).`
);
}

const responseJson = await response.json();

if (responseJson?.errors?.length > 0) {
const error = responseJson.errors[0];
const msg = error.message;
const code = error.extensions?.code;

if (msg === 'Unauthorized' || code === 'UNAUTHENTICATED') {
throw new Error(
`Unauthorized error. Make sure that valid auth credentials were used. Cannot fetch data from ${apiEndpointUrl}.`
);
}

throw new Error(`${msg} error (code: ${code}). Cannot fetch data from ${apiEndpointUrl}.`);
}

return responseJson as Promise<OUT>;
}

private async sendRequest(apiEndpointUrl: string, tokenInfo: TokenInfo, query: string, variables = {}) {
return fetch(apiEndpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -204,14 +231,6 @@ export class ApiHandler {
variables,
}),
});

if (!response.ok) {
throw new Error(
`Connection error. Cannot fetch data from ${apiEndpointUrl}. Error '${response.statusText}' (${response.status}).`
);
}

return response.json() as Promise<OUT>;
}

private formatAuthorizationHeader(tokenInfo: TokenInfo) {
Expand Down

0 comments on commit 5669efb

Please sign in to comment.