|
| 1 | +// Copyright © 2017 Jan Keromnes. All rights reserved. |
| 2 | +// The following code is covered by the AGPL-3.0 license. |
| 3 | + |
| 4 | +'use strict'; |
| 5 | + |
| 6 | +const { Camp } = require('camp'); |
| 7 | +const selfapi = require('selfapi'); |
| 8 | +const stream = require('stream'); |
| 9 | +const { promisify } = require('util'); |
| 10 | + |
| 11 | +describe('Janitor API self-tests', () => { |
| 12 | + jest.mock('../lib/boot'); |
| 13 | + jest.mock('../lib/db'); |
| 14 | + jest.mock('../lib/docker'); |
| 15 | + |
| 16 | + const db = require('../lib/db'); |
| 17 | + db.__setData({ |
| 18 | + // Tell our fake Janitor app that it runs on the fake host "example.com" |
| 19 | + hostname: 'example.com', |
| 20 | + // Disable sending any emails (for invites or signing in) |
| 21 | + mailer: { |
| 22 | + block: true |
| 23 | + }, |
| 24 | + security: { |
| 25 | + // Disable Let's Encrypt HTTPS certificate generation and verification |
| 26 | + forceHttp: true, |
| 27 | + // Disable any security policies that could get in the way of testing |
| 28 | + forceInsecure: true |
| 29 | + }, |
| 30 | + tokens: 'test/tokens' |
| 31 | + }); |
| 32 | + |
| 33 | + const boot = require('../lib/boot'); |
| 34 | + boot.ensureDockerTlsCertificates.mockResolvedValue(); |
| 35 | + |
| 36 | + const docker = require('../lib/docker'); |
| 37 | + const hosts = require('../lib/hosts'); |
| 38 | + const machines = require('../lib/machines'); |
| 39 | + const users = require('../lib/users'); |
| 40 | + |
| 41 | + // Fake several Docker methods for testing. |
| 42 | + // TODO Maybe use a real Docker Remote API server (or a full mock) for more |
| 43 | + // realistic tests? |
| 44 | + docker.pullImage.mockImplementation((parameters) => { |
| 45 | + const readable = new stream.Readable(); |
| 46 | + readable.push('ok'); |
| 47 | + readable.push(null); // End the stream. |
| 48 | + return Promise.resolve(readable); |
| 49 | + }); |
| 50 | + docker.inspectImage.mockResolvedValue({ Created: 1500000000000 }); |
| 51 | + docker.tagImage.mockResolvedValue(); |
| 52 | + docker.runContainer.mockResolvedValue({ container: { id: 'abcdef0123456789' }, logs: '' }); |
| 53 | + docker.copyIntoContainer.mockResolvedValue(); |
| 54 | + docker.execInContainer.mockResolvedValue(); |
| 55 | + docker.listChangedFilesInContainer.mockResolvedValue([ |
| 56 | + { Path: '/tmp', Kind: 0 }, |
| 57 | + { Path: '/tmp/test', Kind: 1 } |
| 58 | + ]); |
| 59 | + docker.version.mockResolvedValue({ Version: '17.06.0-ce' }); |
| 60 | + |
| 61 | + const api = require('../api/'); |
| 62 | + |
| 63 | + const app = new Camp({ |
| 64 | + documentRoot: 'static' |
| 65 | + }); |
| 66 | + |
| 67 | + beforeAll(async () => { |
| 68 | + function registerTestUser () { |
| 69 | + // Grant administrative privileges to the fake email "[email protected]". |
| 70 | + db.get('admins')['[email protected]'] = true; |
| 71 | + // Create the user "[email protected]" by "sending" them an invite email. |
| 72 | + return promisify(users.sendInviteEmail)('[email protected]'); |
| 73 | + } |
| 74 | + |
| 75 | + function createTestHost () { |
| 76 | + return promisify(hosts.create)('example.com', {}); |
| 77 | + } |
| 78 | + |
| 79 | + function createTestProject () { |
| 80 | + machines.setProject({ |
| 81 | + 'id': 'test-project', |
| 82 | + '/name': 'Test Project', |
| 83 | + '/docker/host': 'example.com', |
| 84 | + '/docker/image': 'image:latest', |
| 85 | + }); |
| 86 | + } |
| 87 | + |
| 88 | + function createTestContainer () { |
| 89 | + const user = db.get('users')['[email protected]']; |
| 90 | + // Create a new user machine for the project "test-project". |
| 91 | + return machines.spawn(user, 'test-project'); |
| 92 | + } |
| 93 | + |
| 94 | + await Promise.all([ |
| 95 | + registerTestUser(), |
| 96 | + createTestHost(), |
| 97 | + createTestProject(), |
| 98 | + ]); |
| 99 | + await createTestContainer(); |
| 100 | + |
| 101 | + // Authenticate test requests with a server middleware. |
| 102 | + const sessions = require('../lib/sessions'); |
| 103 | + app.handle((request, response, next) => { |
| 104 | + sessions.get(request, (error, session, token) => { |
| 105 | + if (error || !session || !session.id) { |
| 106 | + console.error('[fail] session:', session, error); |
| 107 | + response.statusCode = 500; // Internal Server Error |
| 108 | + response.end(); |
| 109 | + return; |
| 110 | + } |
| 111 | + request.session = session; |
| 112 | + if (!('client_secret' in request.query)) { |
| 113 | + request.user = db.get('users')['[email protected]']; |
| 114 | + } |
| 115 | + next(); |
| 116 | + }); |
| 117 | + }); |
| 118 | + |
| 119 | + // Mount the Janitor API. |
| 120 | + selfapi(app, '/api', api); |
| 121 | + |
| 122 | + await promisify(app.listen).call(app, 0, '127.0.0.1'); |
| 123 | + }); |
| 124 | + |
| 125 | + afterAll(() => { |
| 126 | + return promisify(app.close).call(app); |
| 127 | + }); |
| 128 | + |
| 129 | + it('follows API examples', async () => { |
| 130 | + const { passed, failed } = await promisify(api.test).call(api, `http://127.0.0.1:${app.address().port}`); |
| 131 | + console.info(`${passed.length} passed, ${failed.length} failed`); |
| 132 | + expect(failed).toHaveLength(0); |
| 133 | + }); |
| 134 | +}); |
0 commit comments