Skip to content

Commit

Permalink
Merge pull request #64 from github/feelepxyz/fake-api-server
Browse files Browse the repository at this point in the history
Add a fake dependabot-api server
  • Loading branch information
Philip Harrison authored and GitHub committed Jul 27, 2021
2 parents a60cf4d + 4e61b75 commit 91f3d34
Show file tree
Hide file tree
Showing 10 changed files with 3,230 additions and 120 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: 'ci'
on: pull_request

jobs:
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: GPR login
run: docker login docker.pkg.github.com -u x -p ${{ secrets.GPR_TOKEN }}
- name: GRP pull
run: docker pull docker.pkg.github.com/dependabot/dependabot-updater:latest
- run: |
npm ci
- name: Run integration test files
run: npm test "integration"
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: 'build-test'
name: 'ci'
on: # rebuild any PRs and main branch changes
pull_request:
push:
Expand All @@ -14,7 +14,7 @@ jobs:
- run: |
npm ci
- run: |
CI=true npm run all
SKIP_INTEGRATION_TESTS=true npm run all
test: # make sure the action works on a clean machine without building
runs-on: ubuntu-latest
steps:
Expand Down
40 changes: 40 additions & 0 deletions __tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Docker from 'dockerode'
import {UPDATER_IMAGE_NAME} from '../src/main'
import waitPort from 'wait-port'
import path from 'path'
import {spawn} from 'child_process'

export const removeDanglingUpdaterContainers = async (): Promise<void> => {
const docker = new Docker()
const containers = (await docker.listContainers()) || []

for (const container of containers) {
if (container.Image.includes(UPDATER_IMAGE_NAME)) {
try {
await docker.getContainer(container.Id).remove({v: true, force: true})
} catch (e) {
// ignore
}
}
}
}

export const runFakeDependabotApi = async (port: number): Promise<Function> => {
const server = spawn('node', [
`${path.join(__dirname, 'server/server.js')}`,
`${port}`
])

server.stdout.on('data', (data: any) => {
console.log(`json-server log: ${data}`) // eslint-disable-line no-console
})
server.stderr.on('data', (data: any) => {
console.error(`json-server error: ${data}`) // eslint-disable-line no-console
})

await waitPort({port})

return (): void => {
server.kill()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"data": {
"attributes": {
"update_jobs": [
{
"id": 1,
"allowed-updates": [
{
"dependency-type": "direct",
Expand All @@ -9,7 +10,8 @@
],
"credentials-metadata": [
{
"type": "git_source", "host": "github.com"
"type": "git_source",
"host": "github.com"
}
],
"dependencies": null,
Expand All @@ -33,14 +35,21 @@
"security-updates-only": false,
"vendor-dependencies": false,
"reject-external-code": false,
"experiments": { "build-pull-request-message": true },
"experiments": {
"build-pull-request-message": true
},
"commit-message-options": {
"include-scope": null,
"prefix": null,
"prefix-development": null
},
"data": {
"base-commit-sha": "a8d9c08ff29358f80f6194b5909de9879741780d"
}
},
"id": "1",
"type": "update-jobs"
}
}
],
"credentials": [{"id": 1}],
"dependencies": [],
"update_job_errors": [],
"pull_requests": []
}
96 changes: 96 additions & 0 deletions __tests__/server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const jsonServer = require('json-server')
const path = require('path')
const fs = require('fs')
const server = jsonServer.create()
const db = JSON.parse(fs.readFileSync(path.join(__dirname, 'db.json')))
const router = jsonServer.router(db)
const middlewares = jsonServer.defaults()
const SERVER_PORT = process.argv.slice(2)[0] || 9000

// Sets up a fake dependabot-api using json-server
//
// Test it locally by running this script directly:
//
// $ node __tests__/server/server.js Running on http://localhost:9000
//
// Verify it works: curl http://localhost:9000/update_jobs/1/details
//
// The 'id' attribute is significant for json-server and maps requests tp the
// 'id' key in the db.json for the resource, for example:
//
// - GET /update_jobs/1/details and GET /update_jobs/1 return hard-coded update
// job in db.json
// - GET /update_jobs/2 would 404
// - POST /update_jobs {data: {...attrs}} would persist a new update job with id
// 2

// NOTE: Serialise the response like dependabot-api
router.render = (_, res) => {
const id = res.locals.data.id
const data = {
attributes: res.locals.data
}
if (id) {
data.id = id
}
res.jsonp({
data
})
}

server.use(middlewares)

// Inject a legit GITHUB_TOKEN to increase rate limits fetching manifests from github
server.get('/update_jobs/:id/credentials', (_, res) => {
res.jsonp({
data: {
attributes: {
credentials: {
type: 'git_source',
host: 'github.com',
username: 'x-access-token',
password: process.env.GITHUB_TOKEN
}
}
}
})
})

server.post(
'/update_jobs/:id/create_pull_request',
jsonServer.bodyParser,
(req, res) => {
const data = {...req.body.data, id: req.params.id}
db.pull_requests.push(data)
router.db.write()

res.jsonp({})
}
)

// TEMP HACK: Always return 204 on post so the updater doesn't buil out
server.use(jsonServer.bodyParser, (req, res, next) => {
if (req.method === 'POST' && req.body.data) {
req.body = req.body.data
res.sendStatus(204)
return
}
next()
})

// NOTE: These map to resources in db.json
server.use(
jsonServer.rewriter({
'/update_jobs/:id/details': '/update_jobs/:id',
'/update_jobs/:id/credentials': '/credentials/:id',
'/update_jobs/:id/record_update_job_error': '/update_job_errors/:id',
'/update_jobs/:id/mark_as_processed': '/update_jobs/:id',
'/update_jobs/:id/update_dependency_list': '/dependencies/:id',
'/update_jobs/:id/record_package_manager_version': '/update_jobs/:id'
})
)

server.use(router)
server.listen(SERVER_PORT, () => {
console.log(`json-server is running on http://localhost:${SERVER_PORT}`)
})
114 changes: 54 additions & 60 deletions __tests__/updater-integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,81 @@
import Docker from 'dockerode'
import fs from 'fs'
import path from 'path'
import {Updater} from '../src/updater'
import {UPDATER_IMAGE_NAME} from '../src/main'
import axios from 'axios'

import {APIClient, JobParameters} from '../src/api-client'
import {ImageService} from '../src/image-service'
import {UPDATER_IMAGE_NAME} from '../src/main'
import {Updater} from '../src/updater'

import {removeDanglingUpdaterContainers, runFakeDependabotApi} from './helpers'

const FAKE_SERVER_PORT = 9000

describe('Updater', () => {
let server: any

// To run the js-code itself against API:
// const params = {
// jobID: 1,
// jobToken: 'xxx',
// credentialsToken: 'xxx',
// dependabotAPI: 'http://host.docker.internal:3001'
// }
// const client = axios.create({baseURL: params.dependabotAPI})
// const api = new DependabotAPI(client, params)
// const updater = new Updater(UPDATER_IMAGE_NAME, api)

// This stubs out API calls from JS, but will run the updater against an API
// running on the specified API endpoint.
const mockAPIClient: any = {
getJobDetails: jest.fn(),
getCredentials: jest.fn(),
params: {
jobID: 1,
jobToken: process.env.JOB_TOKEN,
credentialsToken: process.env.CREDENTIALS_TOKEN,
dependabotAPIURL: 'http://host.docker.internal:3001'
}
}
const updater = new Updater(UPDATER_IMAGE_NAME, mockAPIClient)
// This runs the tests against a fake dependabot-api server using json-server
const fakeDependabotApiUrl = `http://localhost:${FAKE_SERVER_PORT}`
// Used from this action to get job details and credentials
const externalDependabotApiUrl =
process.env.DEPENDABOT_API_URL || fakeDependabotApiUrl
// Used from within the updater container to update the job state and create prs
const internalDockerHost =
process.platform === 'darwin' ? 'host.docker.internal' : 'localhost'
const internalDependabotApiUrl =
process.env.DEPENDABOT_API_URL ||
`http://${internalDockerHost}:${FAKE_SERVER_PORT}`
const params = new JobParameters(
1,
process.env.JOB_TOKEN || 'job-token',
process.env.CREDENTIALS_TOKEN || 'cred-token',
internalDependabotApiUrl
)

const client = axios.create({baseURL: externalDependabotApiUrl})
const apiClient = new APIClient(client, params)
const updater = new Updater(UPDATER_IMAGE_NAME, apiClient)

beforeAll(async () => {
if (process.env.CI) {
// Skip this test on CI, as it takes too long to download the image
// Skip the test when we haven't preloaded the updater image
if (process.env.SKIP_INTEGRATION_TESTS) {
return
}

await ImageService.pull(UPDATER_IMAGE_NAME)

if (externalDependabotApiUrl === fakeDependabotApiUrl) {
server = await runFakeDependabotApi(FAKE_SERVER_PORT)
}
})

afterEach(async () => {
const docker = new Docker()
const containers = (await docker.listContainers()) || []

for (const container of containers) {
if (
container.Image.includes(
'docker.pkg.github.com/dependabot/dependabot-updater'
)
) {
try {
await docker.getContainer(container.Id).remove({v: true, force: true})
} catch (e) {
// ignore
}
}
}
server && server() // teardown server process
await removeDanglingUpdaterContainers()
})

jest.setTimeout(20000)
it('should fetch manifests', async () => {
if (process.env.CI) {
// Skip this test on CI, as it takes too long to download the image
jest.setTimeout(25000)
it('should run the updater and create a pull request', async () => {
// Skip the test when we haven't preloaded the updater image
if (process.env.SKIP_INTEGRATION_TESTS) {
return
}

mockAPIClient.getJobDetails.mockImplementation(() => {
return JSON.parse(
fs
.readFileSync(path.join(__dirname, 'fixtures/job-details/npm.json'))
.toString()
).data.attributes
})
mockAPIClient.getCredentials.mockImplementation(() => {
return [
{
type: 'git_source',
host: 'github.com',
username: 'x-access-token',
password: process.env.GITHUB_TOKEN
}
]
})
await updater.runUpdater()

// NOTE: This will not work when running against the actual dependabot-api
// Checks if the pr was persisted in the fake json-server
const res = await client.get('/pull_requests/1')

expect(res.status).toEqual(200)
expect(res.data.data.attributes['pr-title']).toEqual(
'Bump fetch-factory from 0.0.1 to 0.2.1'
)
})
})
Loading

0 comments on commit 91f3d34

Please sign in to comment.