Skip to content

Commit

Permalink
Merge pull request #25 from CamiloGarciaLaRotta/feat/headers
Browse files Browse the repository at this point in the history
Feat/headers
  • Loading branch information
CamiloGarciaLaRotta committed Apr 7, 2020
2 parents 98ad3f7 + 380c9da commit f014912
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 59 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/graphql_2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Test GitHub GraphQL API
on: push

jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Get the sources
uses: actions/checkout@v2
- name: Execute a dummy query
id: query
uses: ./
with:
headers: '{"Authorization": "bearer ${{ secrets.TOKEN }}" }'
graphql: |
query {
repository(name: "watermelon-http-client", owner:"camilogarcialarotta"){
issues(first:1) {
nodes{
title
state
}
}
}
}
- name: Print the response status
run: echo ${{ steps.query.outputs.status }}
shell: bash
- name: Print the response headers
run: echo ${{ steps.query.outputs.headers }}
shell: bash
- name: Print the response payload
run: echo ${{ steps.query.outputs.response }}
shell: bash
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A Typescript Action that performs HTTP requests within your workflow. It support
| url | Endpoint to query | https://api.github.com/graphql |
| method | HTTP method | `GET` |
| data | HTTP request payload | |
| headers | HTTP headers | Empty by default. If no headers are supplied, GraphQL queries will be sent with `{'Content-Type': 'application/json'}` |
| graphql | GraphQL query to execute. If defined, the `data` field is automatically populated with this payload and the `method` is set to `POST` |


Expand Down
12 changes: 6 additions & 6 deletions __tests__/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ describe('when calling get', () => {

when(fakeRequest)
.calledWith(expect.anything())
.mockReturnValue(Promise.resolve({status: 200, data: {some: 'JSON'}}))
.mockReturnValue(Promise.resolve({status: 200, data: {}, headers: {}}))
})

it('should call axios with the appropriate url and payload', async () => {
await request('url', 'get')
await request('url', 'get', '{"some": "JSON"}', {some: 'headers'})

expect(fakeRequest).toHaveBeenCalledWith({
url: 'url',
method: 'get',
data: '{}',
headers: expect.anything()
data: '{"some": "JSON"}',
headers: {some: 'headers'}
})
})
})
Expand All @@ -39,13 +39,13 @@ describe('when calling post', () => {
})

it('should call axios with the appropriate url and payload', async () => {
await request('url', 'post', '{"a": "JSON"}')
await request('url', 'post', '{"a": "JSON"}', {})

expect(fakeRequest).toHaveBeenCalledWith({
url: 'url',
method: 'post',
data: '{"a": "JSON"}',
headers: expect.anything()
headers: {}
})
})
})
26 changes: 26 additions & 0 deletions __tests__/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ describe('when called with a GraphQL query', () => {
emoji
}
}`
process.env['INPUT_HEADERS'] = '{"content-type":"application/json"}'
})

afterEach(() => {
delete process.env['INPUT_URL']
delete process.env['INPUT_GRAPHQL']
delete process.env['INPUT_HEADERS']
})

it('should output something if a query was supplied', async () => {
Expand All @@ -77,6 +79,30 @@ describe('when called with a GraphQL query', () => {
})
})

describe('when called with a custom header containing wrong auth', () => {
beforeEach(() => {
process.env['INPUT_URL'] = 'https://api.github.com'
process.env['INPUT_HEADERS'] =
'{"content-type":"application/json", "Authorization": "Basic DummyToken123"}'
})

afterEach(() => {
delete process.env['INPUT_URL']
delete process.env['INPUT_HEADERS']
})

it('should reply with a 401 Unauthorized', async () => {
const fakeSetOutput = jest.spyOn(core, 'setOutput')
const fakeLogError = jest.spyOn(core, 'error')

await run()

expect(fakeLogError).toHaveBeenCalledTimes(3)
expect(fakeSetOutput).not.toHaveBeenCalled()
expect(fakeLogError).toBeCalledWith('response status: 401')
})
})

describe('when action fails', () => {
it('should handle missing input gracefully', async () => {
const fakeSetOutput = jest.spyOn(core, 'setOutput')
Expand Down
15 changes: 12 additions & 3 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ describe('when running the action with valid inputs', () => {
jest.resetModules()

when(fakeRequest)
.calledWith('url', expect.anything())
.mockReturnValue(Promise.resolve([200, 'headers', {some: 'JSON'}]))
.calledWith('url', expect.anything(), expect.anything(), {
some: 'input-headers'
})
.mockReturnValue(
Promise.resolve([200, {some: 'response-headers'}, {some: 'JSON'}])
)

process.env['INPUT_URL'] = 'url'
})
Expand All @@ -25,6 +29,7 @@ describe('when running the action with valid inputs', () => {
it('should print as info level message the inputs and outputs', async () => {
process.env['INPUT_METHOD'] = 'POST'
process.env['INPUT_DATA'] = '{ a: "payload" }'
process.env['INPUT_HEADERS'] = '{"some": "input-headers"}'

const infoMock = jest.spyOn(core, 'info')

Expand All @@ -33,18 +38,21 @@ describe('when running the action with valid inputs', () => {
expect(infoMock.mock.calls).toEqual([
['url: url'],
['method: POST'],
['headers: {"some":"input-headers"}'],
['data: { a: "payload" }'],
['response status: 200'],
['response headers: "headers"'],
['response headers: {"some":"response-headers"}'],
['response body:\n{"some":"JSON"}']
])

delete process.env['INPUT_METHOD']
delete process.env['INPUT_DATA']
delete process.env['INPUT_HEADERS']
})

it('should not blow with GET', async () => {
process.env['INPUT_METHOD'] = 'GET'
process.env['INPUT_HEADERS'] = '{"some": "input-headers" }'

const fakeSetOutput = jest.spyOn(core, 'setOutput')

Expand All @@ -55,5 +63,6 @@ describe('when running the action with valid inputs', () => {
expect(fakeSetOutput).toBeCalledWith('response', '{"some":"JSON"}')

delete process.env['INPUT_METHOD']
delete process.env['INPUT_HEADERS']
})
})
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ inputs:
description: HTTP request payload
graphql:
description: GraphQL query or mutation to execute. If defined, the 'data' field is automatically populated with the this payload and the 'method' is set to POST
headers:
description: HTTP custom headers
outputs:
status:
description: The status of the HTTP request
Expand Down
41 changes: 28 additions & 13 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -860,30 +860,42 @@ function run() {
let method = core.getInput('method');
let data = core.getInput('data');
const graphql = core.getInput('graphql');
const rawInputHeaders = core.getInput('headers');
let inputHeaders;
if (rawInputHeaders.length > 0) {
inputHeaders = JSON.parse(rawInputHeaders);
}
else {
inputHeaders = {};
}
if (graphql.length !== 0) {
method = 'POST';
data = graphql_1.graphqlPayloadFor(graphql);
if (isEmpty(inputHeaders)) {
inputHeaders = { 'Content-Type': 'application/json' };
}
core.info(`graphql:\n${graphql}`);
}
core.info(`url: ${url}`);
core.info(`method: ${method}`);
core.info(`headers: ${JSON.stringify(inputHeaders)}`);
if (data.length !== 0) {
core.info(`data: ${data}`);
}
const [status, rawHeaders, rawResponse] = yield http_1.request(url, method, data);
const headers = JSON.stringify(rawHeaders);
const [status, rawResponseHeaders, rawResponse] = yield http_1.request(url, method, data, inputHeaders);
const responseHeaders = JSON.stringify(rawResponseHeaders);
const response = JSON.stringify(rawResponse);
if (status < 200 || status >= 300) {
core.error(`response status: ${status}`);
core.error(`response headers: ${headers}`);
core.error(`response headers: ${responseHeaders}`);
core.error(`response body:\n${response}`);
throw new Error(`request failed: ${response}`);
}
core.info(`response status: ${status}`);
core.info(`response headers: ${headers}`);
core.info(`response headers: ${responseHeaders}`);
core.info(`response body:\n${response}`);
core.setOutput('status', `${status}`);
core.setOutput('headers', `${headers}`);
core.setOutput('headers', `${responseHeaders}`);
core.setOutput('response', `${response}`);
}
catch (error) {
Expand All @@ -892,6 +904,7 @@ function run() {
});
}
exports.run = run;
const isEmpty = (o) => Object.keys(o).length === 0;
run();


Expand Down Expand Up @@ -1757,7 +1770,7 @@ module.exports = require("assert");
/***/ 361:
/***/ (function(module) {

module.exports = {"_from":"axios@^0.19.2","_id":"axios@0.19.2","_inBundle":false,"_integrity":"sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==","_location":"/axios","_phantomChildren":{},"_requested":{"type":"range","registry":true,"raw":"axios@^0.19.2","name":"axios","escapedName":"axios","rawSpec":"^0.19.2","saveSpec":null,"fetchSpec":"^0.19.2"},"_requiredBy":["#USER","/"],"_resolved":"https://registry.npmjs.org/axios/-/axios-0.19.2.tgz","_shasum":"3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27","_spec":"axios@^0.19.2","_where":"/Users/camilogarcialarotta/playground/graphql-client-action","author":{"name":"Matt Zabriskie"},"browser":{"./lib/adapters/http.js":"./lib/adapters/xhr.js"},"bugs":{"url":"https://github.com/axios/axios/issues"},"bundleDependencies":false,"bundlesize":[{"path":"./dist/axios.min.js","threshold":"5kB"}],"dependencies":{"follow-redirects":"1.5.10"},"deprecated":false,"description":"Promise based HTTP client for the browser and node.js","devDependencies":{"bundlesize":"^0.17.0","coveralls":"^3.0.0","es6-promise":"^4.2.4","grunt":"^1.0.2","grunt-banner":"^0.6.0","grunt-cli":"^1.2.0","grunt-contrib-clean":"^1.1.0","grunt-contrib-watch":"^1.0.0","grunt-eslint":"^20.1.0","grunt-karma":"^2.0.0","grunt-mocha-test":"^0.13.3","grunt-ts":"^6.0.0-beta.19","grunt-webpack":"^1.0.18","istanbul-instrumenter-loader":"^1.0.0","jasmine-core":"^2.4.1","karma":"^1.3.0","karma-chrome-launcher":"^2.2.0","karma-coverage":"^1.1.1","karma-firefox-launcher":"^1.1.0","karma-jasmine":"^1.1.1","karma-jasmine-ajax":"^0.1.13","karma-opera-launcher":"^1.0.0","karma-safari-launcher":"^1.0.0","karma-sauce-launcher":"^1.2.0","karma-sinon":"^1.0.5","karma-sourcemap-loader":"^0.3.7","karma-webpack":"^1.7.0","load-grunt-tasks":"^3.5.2","minimist":"^1.2.0","mocha":"^5.2.0","sinon":"^4.5.0","typescript":"^2.8.1","url-search-params":"^0.10.0","webpack":"^1.13.1","webpack-dev-server":"^1.14.1"},"homepage":"https://github.com/axios/axios","keywords":["xhr","http","ajax","promise","node"],"license":"MIT","main":"index.js","name":"axios","repository":{"type":"git","url":"git+https://github.com/axios/axios.git"},"scripts":{"build":"NODE_ENV=production grunt build","coveralls":"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js","examples":"node ./examples/server.js","fix":"eslint --fix lib/**/*.js","postversion":"git push && git push --tags","preversion":"npm test","start":"node ./sandbox/server.js","test":"grunt test && bundlesize","version":"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json"},"typings":"./index.d.ts","version":"0.19.2"};
module.exports = {"_args":[["axios@0.19.2","/home/titoine54/watermelon-http-client"]],"_from":"axios@0.19.2","_id":"axios@0.19.2","_inBundle":false,"_integrity":"sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==","_location":"/axios","_phantomChildren":{},"_requested":{"type":"version","registry":true,"raw":"axios@0.19.2","name":"axios","escapedName":"axios","rawSpec":"0.19.2","saveSpec":null,"fetchSpec":"0.19.2"},"_requiredBy":["/"],"_resolved":"https://registry.npmjs.org/axios/-/axios-0.19.2.tgz","_spec":"0.19.2","_where":"/home/titoine54/watermelon-http-client","author":{"name":"Matt Zabriskie"},"browser":{"./lib/adapters/http.js":"./lib/adapters/xhr.js"},"bugs":{"url":"https://github.com/axios/axios/issues"},"bundlesize":[{"path":"./dist/axios.min.js","threshold":"5kB"}],"dependencies":{"follow-redirects":"1.5.10"},"description":"Promise based HTTP client for the browser and node.js","devDependencies":{"bundlesize":"^0.17.0","coveralls":"^3.0.0","es6-promise":"^4.2.4","grunt":"^1.0.2","grunt-banner":"^0.6.0","grunt-cli":"^1.2.0","grunt-contrib-clean":"^1.1.0","grunt-contrib-watch":"^1.0.0","grunt-eslint":"^20.1.0","grunt-karma":"^2.0.0","grunt-mocha-test":"^0.13.3","grunt-ts":"^6.0.0-beta.19","grunt-webpack":"^1.0.18","istanbul-instrumenter-loader":"^1.0.0","jasmine-core":"^2.4.1","karma":"^1.3.0","karma-chrome-launcher":"^2.2.0","karma-coverage":"^1.1.1","karma-firefox-launcher":"^1.1.0","karma-jasmine":"^1.1.1","karma-jasmine-ajax":"^0.1.13","karma-opera-launcher":"^1.0.0","karma-safari-launcher":"^1.0.0","karma-sauce-launcher":"^1.2.0","karma-sinon":"^1.0.5","karma-sourcemap-loader":"^0.3.7","karma-webpack":"^1.7.0","load-grunt-tasks":"^3.5.2","minimist":"^1.2.0","mocha":"^5.2.0","sinon":"^4.5.0","typescript":"^2.8.1","url-search-params":"^0.10.0","webpack":"^1.13.1","webpack-dev-server":"^1.14.1"},"homepage":"https://github.com/axios/axios","keywords":["xhr","http","ajax","promise","node"],"license":"MIT","main":"index.js","name":"axios","repository":{"type":"git","url":"git+https://github.com/axios/axios.git"},"scripts":{"build":"NODE_ENV=production grunt build","coveralls":"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js","examples":"node ./examples/server.js","fix":"eslint --fix lib/**/*.js","postversion":"git push && git push --tags","preversion":"npm test","start":"node ./sandbox/server.js","test":"grunt test && bundlesize","version":"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json"},"typings":"./index.d.ts","version":"0.19.2"};

/***/ }),

Expand Down Expand Up @@ -2727,31 +2740,33 @@ const axios_1 = __importDefault(__webpack_require__(53));
* Send an HTTP request with JSON as content and response type.
*
* @param url - the endpoint to send the request to
* @param method - the HTTP method for the request. e.g `get, GET, post POST`
* @param method - the HTTP method for the request. e.g `get, GET, post, POST`
* @param data - the JSON encoded payload if any
* @param headers - the JSON encoded custom headers
* @returns `[status code, response body]`
*/
function request(url, method, data = '{}') {
function request(url, method, //using Method from axios
data = '{}', headers = {}) {
return __awaiter(this, void 0, void 0, function* () {
try {
const response = yield axios_1.default.request({
url,
method,
data,
headers: { 'Content-Type': 'application/json' }
headers
});
const status = response.status;
const headers = response.headers;
const responseHeaders = response.headers;
const payload = response.data;
return [status, headers, payload];
return [status, responseHeaders, payload];
}
catch (error) {
if (error.response) {
// The request was made and the server responded with a status code
const status = error.response.status;
const headers = error.response.headers;
const responseHeaders = error.response.headers;
const payload = error.response.data;
return [status, headers, payload];
return [status, responseHeaders, payload];
}
// Something happened in setting up the request that triggered an error
return [
Expand Down
42 changes: 12 additions & 30 deletions src/http.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
import axios from 'axios'
import axios, {Method} from 'axios'

/**
* Send an HTTP request with JSON as content and response type.
*
* @param url - the endpoint to send the request to
* @param method - the HTTP method for the request. e.g `get, GET, post POST`
* @param method - the HTTP method for the request. e.g `get, GET, post, POST`
* @param data - the JSON encoded payload if any
* @param headers - the JSON encoded custom headers
* @returns `[status code, response body]`
*/
export async function request(
url: string,
method: Method,
data = '{}'
method: Method, //using Method from axios
data = '{}',
headers: Object = {}
): Promise<[number, Object, Object]> {
try {
const response = await axios.request({
url,
method,
data,
headers: {'Content-Type': 'application/json'}
headers
})

const status = response.status
const headers = response.headers
const payload = response.data as Object
const responseHeaders = response.headers
const payload = response.data

return [status, headers, payload]
return [status, responseHeaders, payload]
} catch (error) {
if (error.response) {
// The request was made and the server responded with a status code
const status = error.response.status
const headers = error.response.headers
const responseHeaders = error.response.headers
const payload = error.response.data as Object

return [status, headers, payload]
return [status, responseHeaders, payload]
}

// Something happened in setting up the request that triggered an error
Expand All @@ -44,23 +46,3 @@ export async function request(
]
}
}

export type Method =
| 'get'
| 'GET'
| 'delete'
| 'DELETE'
| 'head'
| 'HEAD'
| 'options'
| 'OPTIONS'
| 'post'
| 'POST'
| 'put'
| 'PUT'
| 'patch'
| 'PATCH'
| 'link'
| 'LINK'
| 'unlink'
| 'UNLINK'

0 comments on commit f014912

Please sign in to comment.