feat: ability to refresh tokens for oauth flows (#4302)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -159,6 +159,30 @@ describe("hopp test [options] <file_path_or_id>", () => {
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
describe("OAuth 2 Authorization type with Authorization Code Grant Type", () => {
|
||||
test("Successfully translates the authorization information to headers/query params and sends it along with the request", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"oauth2-auth-code-coll.json",
|
||||
"collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("multipart/form-data content type", () => {
|
||||
test("Successfully derives the relevant headers based and sends the form data in the request body", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"oauth2-auth-code-coll.json",
|
||||
"collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"v": 3,
|
||||
"name": "Multpart form data content type - Collection",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "7",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "multipart-form-data-sample-req",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "POST",
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true,
|
||||
"addTo": "HEADERS",
|
||||
"grantTypeInfo": {
|
||||
"authEndpoint": "test-authorization-endpoint",
|
||||
"tokenEndpoint": "test-token-endpont",
|
||||
"clientID": "test-client-id",
|
||||
"clientSecret": "test-client-secret",
|
||||
"isPKCE": true,
|
||||
"codeVerifierMethod": "S256",
|
||||
"grantType": "AUTHORIZATION_CODE",
|
||||
"token": "test-token"
|
||||
}
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully derives the relevant headers based on the content type\", () => {\n pw.expect(pw.response.body.headers['content-type']).toInclude(\"multipart/form-data\");\n});\n\npw.test(\"Successfully sends the form data in the request body\", () => {\n // Dynamic value\n pw.expect(pw.response.body.data).toBeType(\"string\");\n});",
|
||||
"body": {
|
||||
"contentType": "multipart/form-data",
|
||||
"body": [
|
||||
{
|
||||
"key": "key1",
|
||||
"value": "value1",
|
||||
"active": true,
|
||||
"isFile": false
|
||||
},
|
||||
{
|
||||
"key": "key2",
|
||||
"value": [{}],
|
||||
"active": true,
|
||||
"isFile": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"v": 3,
|
||||
"name": "OAuth2 Authorization Code Grant Type - Collection",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "7",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "oauth2-auth-code-sample-req-pass-by-headers",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "oauth-2",
|
||||
"authActive": true,
|
||||
"addTo": "HEADERS",
|
||||
"grantTypeInfo": {
|
||||
"authEndpoint": "test-authorization-endpoint",
|
||||
"tokenEndpoint": "test-token-endpont",
|
||||
"clientID": "test-client-id",
|
||||
"clientSecret": "test-client-secret",
|
||||
"isPKCE": true,
|
||||
"codeVerifierMethod": "S256",
|
||||
"grantType": "AUTHORIZATION_CODE",
|
||||
"token": "test-token"
|
||||
}
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully derives Authorization header from the supplied fields\", ()=> {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBeType(\"string\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": []
|
||||
},
|
||||
{
|
||||
"v": "7",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"name": "oauth2-auth-code-sample-req-pass-by-query-params",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"auth": {
|
||||
"authType": "oauth-2",
|
||||
"authActive": true,
|
||||
"addTo": "HEADERS",
|
||||
"grantTypeInfo": {
|
||||
"authEndpoint": "test-authorization-endpoint",
|
||||
"tokenEndpoint": "test-token-endpont",
|
||||
"clientID": "test-client-id",
|
||||
"clientSecret": "test-client-secret",
|
||||
"isPKCE": true,
|
||||
"codeVerifierMethod": "S256",
|
||||
"grantType": "AUTHORIZATION_CODE",
|
||||
"token": "test-token"
|
||||
}
|
||||
},
|
||||
"preRequestScript": "",
|
||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully derives Authorization header from the supplied fields\", ()=> {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBeType(\"string\");\n});",
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"authType": "none",
|
||||
"authActive": true
|
||||
},
|
||||
"headers": []
|
||||
}
|
||||
@@ -15,7 +15,6 @@ describe("requestRunner", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
SAMPLE_REQUEST_CONFIG.supported = false;
|
||||
SAMPLE_REQUEST_CONFIG.url = "https://example.com";
|
||||
SAMPLE_REQUEST_CONFIG.method = "GET";
|
||||
jest.clearAllMocks();
|
||||
@@ -70,7 +69,6 @@ describe("requestRunner", () => {
|
||||
|
||||
it("Should handle axios-error with request info.", () => {
|
||||
jest.spyOn(axios, "isAxiosError").mockReturnValue(true);
|
||||
SAMPLE_REQUEST_CONFIG.supported = true;
|
||||
(axios as unknown as jest.Mock).mockRejectedValueOnce(<AxiosError>{
|
||||
name: "name",
|
||||
message: "message",
|
||||
@@ -91,7 +89,6 @@ describe("requestRunner", () => {
|
||||
});
|
||||
|
||||
it("Should successfully execute.", () => {
|
||||
SAMPLE_REQUEST_CONFIG.supported = true;
|
||||
(axios as unknown as jest.Mock).mockResolvedValue(<AxiosResponse>{
|
||||
data: "data",
|
||||
status: 200,
|
||||
|
||||
@@ -20,8 +20,7 @@ export interface RequestStack {
|
||||
* @property {boolean} supported - Boolean check for supported or unsupported requests.
|
||||
*/
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
supported: boolean;
|
||||
displayUrl?: string
|
||||
displayUrl?: string;
|
||||
}
|
||||
|
||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
@@ -32,7 +31,17 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
*/
|
||||
effectiveFinalURL: string;
|
||||
effectiveFinalDisplayURL?: string;
|
||||
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalHeaders: {
|
||||
key: string;
|
||||
value: string;
|
||||
active: boolean;
|
||||
description: string;
|
||||
}[];
|
||||
effectiveFinalParams: {
|
||||
key: string;
|
||||
value: string;
|
||||
active: boolean;
|
||||
description: string;
|
||||
}[];
|
||||
effectiveFinalBody: FormData | string | null;
|
||||
}
|
||||
|
||||
@@ -72,11 +72,12 @@ export const getEffectiveFinalMetaData = (
|
||||
* Selecting only non-empty and active pairs.
|
||||
*/
|
||||
A.filter(({ key, active }) => !S.isEmpty(key) && active),
|
||||
A.map(({ key, value }) => {
|
||||
A.map(({ key, value, description }) => {
|
||||
return {
|
||||
active: true,
|
||||
key: parseTemplateStringE(key, resolvedVariables),
|
||||
value: parseTemplateStringE(value, resolvedVariables),
|
||||
description,
|
||||
};
|
||||
}),
|
||||
E.fromPredicate(
|
||||
@@ -91,9 +92,14 @@ export const getEffectiveFinalMetaData = (
|
||||
/**
|
||||
* Filtering and mapping only right-eithers for each key-value as [string, string].
|
||||
*/
|
||||
A.filterMap(({ key, value }) =>
|
||||
A.filterMap(({ key, value, description }) =>
|
||||
E.isRight(key) && E.isRight(value)
|
||||
? O.some({ active: true, key: key.right, value: value.right })
|
||||
? O.some({
|
||||
active: true,
|
||||
key: key.right,
|
||||
value: value.right,
|
||||
description,
|
||||
})
|
||||
: O.none
|
||||
)
|
||||
)
|
||||
|
||||
@@ -123,12 +123,14 @@ export function getEffectiveRESTRequest(
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
description: "",
|
||||
});
|
||||
} else if (request.auth.authType === "bearer") {
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${parseTemplateString(request.auth.token, resolvedVariables)}`,
|
||||
description: "",
|
||||
});
|
||||
} else if (request.auth.authType === "oauth-2") {
|
||||
const { addTo } = request.auth;
|
||||
@@ -138,6 +140,7 @@ export function getEffectiveRESTRequest(
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${parseTemplateString(request.auth.grantTypeInfo.token, resolvedVariables)}`,
|
||||
description: "",
|
||||
});
|
||||
} else if (addTo === "QUERY_PARAMS") {
|
||||
effectiveFinalParams.push({
|
||||
@@ -147,6 +150,7 @@ export function getEffectiveRESTRequest(
|
||||
request.auth.grantTypeInfo.token,
|
||||
resolvedVariables
|
||||
),
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
} else if (request.auth.authType === "api-key") {
|
||||
@@ -156,12 +160,14 @@ export function getEffectiveRESTRequest(
|
||||
active: true,
|
||||
key: parseTemplateString(key, resolvedVariables),
|
||||
value: parseTemplateString(value, resolvedVariables),
|
||||
description: "",
|
||||
});
|
||||
} else if (addTo === "QUERY_PARAMS") {
|
||||
effectiveFinalParams.push({
|
||||
active: true,
|
||||
key: parseTemplateString(key, resolvedVariables),
|
||||
value: parseTemplateString(value, resolvedVariables),
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -187,6 +193,7 @@ export function getEffectiveRESTRequest(
|
||||
active: true,
|
||||
key: "Content-Type",
|
||||
value: request.body.contentType,
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ import { getDurationInSeconds, getMetaDataPairs } from "./getters";
|
||||
import { preRequestScriptRunner } from "./pre-request";
|
||||
import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
||||
|
||||
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
|
||||
|
||||
/**
|
||||
* Processes given variable, which includes checking for secret variables
|
||||
* and getting value from system environment
|
||||
@@ -75,46 +73,20 @@ const processEnvs = (envs: Partial<HoppEnvs>) => {
|
||||
*/
|
||||
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
||||
const config: RequestConfig = {
|
||||
supported: true,
|
||||
displayUrl: req.effectiveFinalDisplayURL,
|
||||
};
|
||||
|
||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||
|
||||
const reqParams = finalParams(req);
|
||||
const reqHeaders = finalHeaders(req);
|
||||
|
||||
config.url = finalEndpoint(req);
|
||||
config.method = req.method as Method;
|
||||
config.params = getMetaDataPairs(reqParams);
|
||||
config.headers = getMetaDataPairs(reqHeaders);
|
||||
if (req.auth.authActive) {
|
||||
switch (req.auth.authType) {
|
||||
case "oauth-2": {
|
||||
// TODO: OAuth2 Request Parsing
|
||||
// !NOTE: Temporary `config.supported` check
|
||||
config.supported = false;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedContentType =
|
||||
config.headers["Content-Type"] ?? req.body.contentType;
|
||||
|
||||
if (resolvedContentType) {
|
||||
switch (resolvedContentType) {
|
||||
case "multipart/form-data": {
|
||||
// TODO: Parse Multipart Form Data
|
||||
// !NOTE: Temporary `config.supported` check
|
||||
config.supported = false;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
config.data = finalBody(req);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
config.data = finalBody(req);
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -149,13 +121,6 @@ export const requestRunner =
|
||||
duration: 0,
|
||||
};
|
||||
|
||||
// !NOTE: Temporary `config.supported` check
|
||||
if ((config as RequestConfig).supported === false) {
|
||||
status = 501;
|
||||
runnerResponse.status = status;
|
||||
runnerResponse.statusText = responseErrors[status];
|
||||
}
|
||||
|
||||
const end = hrtime(start);
|
||||
const duration = getDurationInSeconds(end);
|
||||
runnerResponse.duration = duration;
|
||||
@@ -182,10 +147,6 @@ export const requestRunner =
|
||||
runnerResponse.statusText = statusText;
|
||||
runnerResponse.status = status;
|
||||
runnerResponse.headers = headers;
|
||||
} else if ((e.config as RequestConfig).supported === false) {
|
||||
status = 501;
|
||||
runnerResponse.status = status;
|
||||
runnerResponse.statusText = responseErrors[status];
|
||||
} else if (e.request) {
|
||||
return E.left(error({ code: "REQUEST_ERROR", data: E.toError(e) }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user