aboutsummaryrefslogtreecommitdiffstats
path: root/packages/pipeline/test/db_setup.ts
blob: bf31d15b604091ea2772babd53cbdb580c375096 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import * as Docker from 'dockerode';
import * as fs from 'fs';
import * as R from 'ramda';
import { Connection, ConnectionOptions, createConnection } from 'typeorm';

import * as ormConfig from '../src/ormconfig';

// The name of the image to pull and use for the container. This also affects
// which version of Postgres we use.
const DOCKER_IMAGE_NAME = 'postgres:11-alpine';
// The name to use for the Docker container which will run Postgres.
const DOCKER_CONTAINER_NAME = '0x_pipeline_postgres_test';
// The port which will be exposed on the Docker container.
const POSTGRES_HOST_PORT = '15432';
// Number of milliseconds to wait for postgres to finish initializing after
// starting the docker container.
const POSTGRES_SETUP_DELAY_MS = 5000;

/**
 * Sets up the database for testing purposes. If the
 * ZEROEX_DATA_PIPELINE_TEST_DB_URL env var is specified, it will create a
 * connection using that url. Otherwise it will spin up a new Docker container
 * with a Postgres database and then create a connection to that database.
 */
export async function setUpDbAsync(): Promise<void> {
    const connection = await createDbConnectionOnceAsync();
    await connection.runMigrations({ transaction: true });
}

/**
 * Tears down the database used for testing. This completely destroys any data.
 * If a docker container was created, it destroys that container too.
 */
export async function tearDownDbAsync(): Promise<void> {
    const connection = await createDbConnectionOnceAsync();
    for (const _ of connection.migrations) {
        await connection.undoLastMigration({ transaction: true });
    }
    if (needsDocker()) {
        const docker = initDockerOnce();
        const postgresContainer = docker.getContainer(DOCKER_CONTAINER_NAME);
        await postgresContainer.kill();
        await postgresContainer.remove();
    }
}

let savedConnection: Connection;

/**
 * The first time this is run, it creates and returns a new TypeORM connection.
 * Each subsequent time, it returns the existing connection. This is helpful
 * because only one TypeORM connection can be active at a time.
 */
export async function createDbConnectionOnceAsync(): Promise<Connection> {
    if (savedConnection !== undefined) {
        return savedConnection;
    }

    if (needsDocker()) {
        await initContainerAsync();
    }
    const testDbUrl =
        process.env.ZEROEX_DATA_PIPELINE_TEST_DB_URL ||
        `postgresql://postgres@localhost:${POSTGRES_HOST_PORT}/postgres`;
    const testOrmConfig = R.merge(ormConfig, { url: testDbUrl }) as ConnectionOptions;

    savedConnection = await createConnection(testOrmConfig);
    return savedConnection;
}

async function sleepAsync(ms: number): Promise<{}> {
    return new Promise<{}>(resolve => setTimeout(resolve, ms));
}

let savedDocker: Docker;

function initDockerOnce(): Docker {
    if (savedDocker !== undefined) {
        return savedDocker;
    }

    // Note(albrow): Code for determining the right socket path is partially
    // based on https://github.com/apocas/dockerode/blob/8f3aa85311fab64d58eca08fef49aa1da5b5f60b/test/spec_helper.js
    const isWin = require('os').type() === 'Windows_NT';
    const socketPath = process.env.DOCKER_SOCKET || (isWin ? '//./pipe/docker_engine' : '/var/run/docker.sock');
    const isSocket = fs.existsSync(socketPath) ? fs.statSync(socketPath).isSocket() : false;
    if (!isSocket) {
        throw new Error(`Failed to connect to Docker using socket path: "${socketPath}".

The database integration tests need to be able to connect to a Postgres database. Make sure that Docker is running and accessible at the expected socket path. If Docker isn't working you have two options:

    1) Set the DOCKER_SOCKET environment variable to a socket path that can be used to connect to Docker or
    2) Set the ZEROEX_DATA_PIPELINE_TEST_DB_URL environment variable to connect directly to an existing Postgres database instead of trying to start Postgres via Docker
`);
    }
    savedDocker = new Docker({
        socketPath,
    });
    return savedDocker;
}

// Creates the container, waits for it to initialize, and returns it.
async function initContainerAsync(): Promise<Docker.Container> {
    const docker = initDockerOnce();

    // Tear down any existing containers with the same name.
    await tearDownExistingContainerIfAnyAsync();

    // Pull the image we need.
    await pullImageAsync(docker, DOCKER_IMAGE_NAME);

    // Create the container.
    const postgresContainer = await docker.createContainer({
        name: DOCKER_CONTAINER_NAME,
        Image: DOCKER_IMAGE_NAME,
        ExposedPorts: {
            '5432': {},
        },
        HostConfig: {
            PortBindings: {
                '5432': [
                    {
                        HostPort: POSTGRES_HOST_PORT,
                    },
                ],
            },
        },
    });
    await postgresContainer.start();
    await sleepAsync(POSTGRES_SETUP_DELAY_MS);
    return postgresContainer;
}

async function tearDownExistingContainerIfAnyAsync(): Promise<void> {
    const docker = initDockerOnce();

    // Check if a container with the desired name already exists. If so, this
    // probably means we didn't clean up properly on the last test run.
    const existingContainer = docker.getContainer(DOCKER_CONTAINER_NAME);
    if (existingContainer != null) {
        try {
            await existingContainer.kill();
        } catch {
            // If this fails, it's fine. The container was probably already
            // killed.
        }
        try {
            await existingContainer.remove();
        } catch {
            // If this fails, it's fine. The container was probably already
            // removed.
        }
    }
}

function needsDocker(): boolean {
    return process.env.ZEROEX_DATA_PIPELINE_TEST_DB_URL === undefined;
}

// Note(albrow): This is partially based on
// https://stackoverflow.com/questions/38258263/how-do-i-wait-for-a-pull
async function pullImageAsync(docker: Docker, imageName: string): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        docker.pull(imageName, {}, (err, stream) => {
            if (err != null) {
                reject(err);
                return;
            }
            docker.modem.followProgress(stream, () => {
                resolve();
            });
        });
    });
}