Skip to content

Commit

Permalink
Merge pull request #1 from qtomlinson/qt/pull_fetch
Browse files Browse the repository at this point in the history
Qt/pull fetch
  • Loading branch information
yashkohli88 authored Jul 12, 2024
2 parents f8c500f + 2fc4a8e commit 5e13b1e
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 74 deletions.
51 changes: 39 additions & 12 deletions lib/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,46 @@

const axios = require('axios')

async function callFetch(request) {
function buildRequestOptions(request) {
let responseType = 'text'
if (request.json) {
responseType = 'json'
} else if (request.encode === null) {
responseType = 'stream'
}

const validateOptions = {}
if (request.simple === false) {
validateOptions.validateStatus = () => true
}

return {
method: request.method,
url: request.url,
responseType,
headers: request.headers,
data: request.body,
...validateOptions
}
}

async function callFetch(request, axiosInstance = axios) {
try {
const response = await axios({
method: request.method,
url: request.url,
responseType: request.responseType,
headers: request.headers,
data: request.body
})
if (request.resolveWithFullResponse) return response
return response.data
const options = buildRequestOptions(request)
const response = await axiosInstance(options)
if (!request.resolveWithFullResponse) return response.data
response.statusCode = response.status
response.statusMessage = response.statusText
return response
} catch (error) {
return error.response
error.statusCode = error.response?.status
throw error
}
}
module.exports = { callFetch }

function withDefaults(opts) {
const axiosInstance = axios.create(opts)
return request => callFetch(request, axiosInstance)
}

module.exports = { callFetch, withDefaults }
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"mocha": "^8.2.0",
"mockttp": "^3.13.0",
"nyc": "^15.0.0",
"prettier": "3.2.4",
"proxyquire": "^2.1.3",
Expand Down
6 changes: 3 additions & 3 deletions providers/fetch/debianFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const memCache = require('memory-cache')
const nodeRequest = require('request')
const path = require('path')
const { promisify } = require('util')
const { callFetch: requestPromise } = require('../../lib/fetch')
const tmp = require('tmp')
const unixArchive = require('ar-async')
const FetchResult = require('../../lib/fetchResult')
const { callFetch } = require('../../lib/fetch')

const exec = promisify(require('child_process').exec)
const exists = promisify(fs.exists)
Expand Down Expand Up @@ -79,7 +79,7 @@ class DebianFetch extends AbstractFetch {
// Example: https://sources.debian.org/api/src/amoeba/latest
async _getLatestVersion(spec) {
const url = `https://sources.debian.org/api/src/${spec.name}/latest`
const response = await callFetch({ url, responseType: 'json' })
const response = await requestPromise({ url, json: true })
return response.version
}

Expand Down Expand Up @@ -314,7 +314,7 @@ class DebianFetch extends AbstractFetch {
if (!copyrightUrl) return []
let response = ''
try {
response = await callFetch({ url: copyrightUrl, responseType: 'text' })
response = await requestPromise({ url: copyrightUrl, json: false })
} catch (error) {
if (error.statusCode === 404) return []
else throw error
Expand Down
11 changes: 5 additions & 6 deletions providers/fetch/mavenBasedFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT

const AbstractFetch = require('./abstractFetch')
const { withDefaults } = require('../../lib/fetch')
const nodeRequest = require('request')
const { clone, get } = require('lodash')
const { promisify } = require('util')
Expand All @@ -14,7 +15,6 @@ const parseString = promisify(require('xml2js').parseString)
const EntitySpec = require('../../lib/entitySpec')
const { extractDate } = require('../../lib/utils')
const FetchResult = require('../../lib/fetchResult')
const { callFetch } = require('../../lib/fetch')

const extensionMap = {
sourcesJar: '-sources.jar',
Expand All @@ -29,7 +29,7 @@ class MavenBasedFetch extends AbstractFetch {
constructor(providerMap, options) {
super(options)
this._providerMap = { ...providerMap }
this._handleCallFetch = options.callFetch || callFetch
this._handleRequestPromise = options.requestPromise || withDefaults(defaultHeaders)
this._handleRequestStream = options.requestStream || nodeRequest.defaults(defaultHeaders).get
}

Expand Down Expand Up @@ -71,7 +71,7 @@ class MavenBasedFetch extends AbstractFetch {
//Use Maven repository meta data model to get the latest version
//https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html#class_versioning
const url = `${this._buildBaseUrl(spec)}/maven-metadata.xml`
const response = await this._requestPromise({ url, responseType: 'text' })
const response = await this._requestPromise({ url, json: false })
if (!response) return null
const meta = await parseString(response)
return get(meta, 'metadata.versioning[0].release[0]')
Expand Down Expand Up @@ -115,7 +115,7 @@ class MavenBasedFetch extends AbstractFetch {

async _getPom(spec) {
const url = this._buildUrl(spec, extensionMap.pom)
const content = await this._requestPromise({ url, responseType: 'text' })
const content = await this._requestPromise({ url, json: false })
if (!content) return null
const pom = await parseString(content)
// clean up some stuff we don't actually look at.
Expand Down Expand Up @@ -182,8 +182,7 @@ class MavenBasedFetch extends AbstractFetch {

async _requestPromise(options) {
try {
options.headers = { ...defaultHeaders.headers, ...options.headers }
return await this._handleCallFetch(options)
return await this._handleRequestPromise(options)
} catch (error) {
if (error.statusCode === 404) return null
else throw error
Expand Down
201 changes: 148 additions & 53 deletions test/unit/lib/fetchTests.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,165 @@
const { callFetch } = require('../../../lib/fetch')
const { callFetch, withDefaults } = require('../../../lib/fetch')
const { expect } = require('chai')
const fs = require('fs')
const mockttp = require('mockttp')

describe('CallFetch', () => {
it('checks if the response is JSON while sending GET request', async () => {
const response = await callFetch({
url: 'https://registry.npmjs.com/redis/0.1.0',
method: 'GET',
responseType: 'json'
describe('with mock server', () => {
const mockServer = mockttp.getLocal()
beforeEach(async () => await mockServer.start())
afterEach(async () => await mockServer.stop())

it('checks if the response is JSON while sending GET request', async () => {
const path = '/registry.npmjs.com/redis/0.1.0'
const expected = fs.readFileSync('test/fixtures/fetch/redis-0.1.0.json')
await mockServer.forGet(path).thenReply(200, expected)

const response = await callFetch({
url: mockServer.urlFor(path),
method: 'GET',
json: true
})
expect(response).to.be.deep.equal(JSON.parse(expected))
})
expect(response).to.be.deep.equal(JSON.parse(fs.readFileSync('test/fixtures/fetch/redis-0.1.0.json')))
})

it('checks if the full response is fetched', async () => {
const response = await callFetch({
url: 'https://registry.npmjs.com/redis/0.1.0',
method: 'GET',
responseType: 'json',
resolveWithFullResponse: true
it('checks if the full response is fetched', async () => {
const path = '/registry.npmjs.com/redis/0.1.0'
const expected = fs.readFileSync('test/fixtures/fetch/redis-0.1.0.json')
await mockServer.forGet(path).thenReply(200, expected)

const response = await callFetch({
url: mockServer.urlFor(path),
method: 'GET',
resolveWithFullResponse: true
})
expect(response.statusCode).to.be.equal(200)
expect(response.statusMessage).to.be.equal('OK')
})
expect(response.status).to.be.equal(200)
expect(response.statusText).to.be.equal('OK')
})

it('checks if the full response is fetched with error code', async () => {
const response = await callFetch({
url: 'https://registry.npmjs.com/redis/0.',
method: 'GET',
responseType: 'json',
resolveWithFullResponse: true
it('should throw error with error code', async () => {
const path = '/registry.npmjs.com/redis/0.1.'
await mockServer.forGet(path).thenReply(404)

await callFetch({
url: mockServer.urlFor(path),
method: 'GET',
json: 'true',
resolveWithFullResponse: true
}).catch(err => {
expect(err.statusCode).to.be.equal(404)
})
})
expect(response.status).to.be.equal(404)
expect(response.statusText).to.be.equal('Not Found')
})

it('checks if the response is text while sending GET request', async () => {
const response = await callFetch({
url: 'https://proxy.golang.org/rsc.io/quote/@v/v1.3.0.mod',
method: 'GET',
responseType: 'text'
it('checks if the response is text while sending GET request', async () => {
const path = '/proxy.golang.org/rsc.io/quote/@v/v1.3.0.mod'
await mockServer.forGet(path).thenReply(200, 'module "rsc.io/quote"\n')

const response = await callFetch({
url: mockServer.urlFor(path),
method: 'GET'
})
expect(response).to.be.equal('module "rsc.io/quote"\n')
})
expect(response).to.be.equal('module "rsc.io/quote"\n')
})

//This test case downloads a crate package
//This URL would send a JSON response if the header is not provided as a part of request.
it('should follow redirect and download the package when responseType is stream', async () => {
const response = await callFetch({
url: 'https://crates.io/api/v1/crates/bitflags/1.0.4/download',
method: 'GET',
responseType: 'stream',
headers: {
Accept: 'text/html'
}
it('should download stream successfully with GET request', async () => {
const expected = JSON.parse(fs.readFileSync('test/fixtures/fetch/redis-0.1.0.json'))
const path = '/registry.npmjs.com/redis/0.1.0'
await mockServer.forGet(path).thenStream(200, fs.createReadStream('test/fixtures/fetch/redis-0.1.0.json'))

const response = await callFetch({
url: mockServer.urlFor(path),
encode: null
})
const destination = 'test/fixtures/fetch/temp.json'
await new Promise((resolve) => {
response.pipe(fs.createWriteStream(destination).on('finish', () => resolve(true)))
})
const downloaded = JSON.parse(fs.readFileSync(destination))
expect(downloaded).to.be.deep.equal(expected)
fs.unlinkSync(destination)
})

it('should apply default headers ', async () => {
const path = '/registry.npmjs.com/redis/0.1.0'
const url = mockServer.urlFor(path)
const endpointMock = await mockServer.forGet(path).thenReply(200)

const defaultOptions = { headers: { 'user-agent': 'clearlydefined.io crawler ([email protected])' } }
const requestWithDefaults = withDefaults(defaultOptions)
await requestWithDefaults({ url })
await requestWithDefaults({ url })

const requests = await endpointMock.getSeenRequests()
expect(requests.length).to.equal(2)
expect(requests[0].url).to.equal(url)
expect(requests[0].headers).to.include(defaultOptions.headers)
expect(requests[1].url).to.equal(url)
expect(requests[1].headers).to.include(defaultOptions.headers)
})

describe('test simple', () => {
it('should handle 300 when simple is true by default', async () => {
const path = '/registry.npmjs.com/redis/0.1.0'
await mockServer.forGet(path).thenReply(300, 'test')

await callFetch({
url: mockServer.urlFor(path)
}).catch(err => {
expect(err.statusCode).to.be.equal(300)
})
})

it('should handle 300 with simple === false', async () => {
const path = '/registry.npmjs.com/redis/0.1.0'
await mockServer.forGet(path).thenReply(300, 'test')

const response = await callFetch({
url: mockServer.urlFor(path),
simple: false
})
expect(response).to.be.equal('test')
})

it('should return status 300 with simple === false', async () => {
const path = '/registry.npmjs.com/redis/0.1.0'
await mockServer.forGet(path).thenReply(300, 'test')

const response = await callFetch({
url: mockServer.urlFor(path),
simple: false,
resolveWithFullResponse: true
})
expect(response.statusCode).to.be.equal(300)
expect(response.statusMessage).to.be.equal('Multiple Choices')
})
})
//Validating the length of the content inorder to verify the response is a crate package.
// JSON response would not return this header in response resulting in failing this test case.
expect(response.headers['content-length']).to.be.equal('15282')
})

it('should follow redirect and download the package when responseType is stream', async () => {
const response = await callFetch({
url: 'https://static.crates.io/crates/bitflags/bitflags-1.0.4.crate',
method: 'GET',
responseType: 'stream'
describe('test crate download', () => {
// This test case downloads a crate package
// This URL would send a JSON response if the header is not provided as a part of request.
it('should follow redirect and download the package when responseType is stream', async () => {
const response = await callFetch({
url: 'https://crates.io/api/v1/crates/bitflags/1.0.4/download',
method: 'GET',
encode: null,
headers: {
Accept: 'text/html'
}
})
// Validating the length of the content in order to verify the response is a crate package.
// JSON response would not return this header in response resulting in failing this test case.
expect(response.headers['content-length']).to.be.equal('15282')
})

it('should download the package when responseType is stream', async () => {
const response = await callFetch({
url: 'https://static.crates.io/crates/bitflags/bitflags-1.0.4.crate',
method: 'GET',
encode: null
})
// Validating the length of the content inorder to verify the response is a crate package.
expect(response.headers['content-length']).to.be.equal('15282')
})
//Validating the length of the content inorder to verify the response is a crate package.
expect(response.headers['content-length']).to.be.equal('15282')
})
})
Loading

0 comments on commit 5e13b1e

Please sign in to comment.