Skip to content

Commit

Permalink
Add Postgres database support (#293)
Browse files Browse the repository at this point in the history
* Rename database contexts to their method (in memory, filesystem) rather than dev vs test

* Add Postgres Testcontainer, implemented via a describeDatabase() test suite factory that will run tests via both Sqlite and Postgres

* Wire describeDatabase up to each db gateway test, and create db helper module for dealing with date differences between sqlite (INTEGER) and Postgres (TIMESTAMP).

* Add ability to share Postgres container across all the database package tests.

* Get Postgres and its testability wired up to the DOJ demo app. The entrypoint still needs cloud.gov environment settings.

* Add Postgres RDS to cloud.gov environment

* temp: run the deploy on this branch for testing

* Postgres Terraform config + app wiring

* Fix aws-rds VCAP_SERVICES lookup path

* on cloud, use ssl with postgres

* Ignore self-signed cert errors with rejectUnauthorized. Revisit by seeing if we can install the RDS certs on the container

* Hack in Postgres Lucia adapter

* To satisfy Lucia, use TEXT for session and user ID types. We will want to move these queries into the database package so we can get test coverage over them.

* Add allowlists for demo apps

* Remove deploy on feature branch
  • Loading branch information
danielnaab authored Aug 22, 2024
1 parent 318d3ee commit 7dd7dd8
Show file tree
Hide file tree
Showing 106 changed files with 1,491 additions and 415 deletions.
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "CC0",
"main": "src/index.ts",
"scripts": {
"build": "node ./build.js",
"build": "tsup src/* --format cjs",
"cli": "node dist/index.js",
"dev": "tsup src/* --watch",
"test": "vitest run --coverage"
Expand Down
14 changes: 14 additions & 0 deletions apps/server-doj/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import esbuild from 'esbuild';

esbuild
.build({
bundle: true,
entryPoints: ['./src/index.ts'],
format: 'esm',
minify: true,
outdir: './dist',
platform: 'node',
sourcemap: true,
target: 'esnext',
})
.catch(() => process.exit(1));
21 changes: 17 additions & 4 deletions apps/server-doj/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { createCustomServer } from './server.js';

const port = process.env.PORT || 4321;
const app = await createCustomServer();

app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
const getCloudGovServerSecrets = () => {
if (process.env.VCAP_SERVICES === undefined) {
throw new Error('VCAP_SERVICES not found');
}
const services = JSON.parse(process.env.VCAP_SERVICES || '{}');
return {
//loginGovClientSecret: services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY,
dbUri: services['aws-rds'][0].credentials.uri as string,
};
};

const secrets = getCloudGovServerSecrets();
createCustomServer({ dbUri: secrets?.dbUri }).then((server: any) =>
server.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
})
);
47 changes: 23 additions & 24 deletions apps/server-doj/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import {
createDatabaseGateway,
createPostgresDatabaseContext,
} from '@atj/database';
import { createServer } from '@atj/server';

const getDirname = () => dirname(fileURLToPath(import.meta.url));

export const createCustomServer = async (): Promise<any> => {
const { createDevDatabaseContext, createDatabaseGateway } = await import(
'@atj/database'
);
const { createServer } = await import('@atj/server');

const dbCtx = await createDevDatabaseContext(
path.join(getDirname(), '../doj.db')
export const createCustomServer = async (ctx: {
dbUri: string;
}): Promise<any> => {
const db = createDatabaseGateway(
await createPostgresDatabaseContext(ctx.dbUri, true)
);
const db = createDatabaseGateway(dbCtx);

return createServer({
title: 'DOJ Form Service',
Expand All @@ -23,16 +20,18 @@ export const createCustomServer = async (): Promise<any> => {
'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:tts-10x-atj-dev-server-doj',
//clientSecret: '', // secrets.loginGovClientSecret,
},
isUserAuthorized: async (email: string) => {
return [
// 10x team members
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
// DOJ test users
'[email protected]',
'[email protected]',
].includes(email);
},
});
};

/*
const getServerSecrets = () => {
const services = JSON.parse(process.env.VCAP_SERVICES || '{}');
const loginClientSecret =
services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY;
return {
loginGovClientSecret: loginClientSecret,
};
};
*/
14 changes: 8 additions & 6 deletions apps/server-doj/tests/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import request from 'supertest';
import { beforeAll, describe, expect, test } from 'vitest';
import { describe, expect, test } from 'vitest';
import { describeDatabase } from '@atj/database/testing';

import { createCustomServer } from '../src/server';

describe('DOJ Form Service', () => {
let app: any;

beforeAll(async () => {
app = await createCustomServer();
test('avoid "No test suite found in file" error', async () => {
expect(true).toBe(true);
});
});

test('renders the home page', async () => {
describeDatabase('DOJ Form Service', () => {
test('renders the home page', async ({ db }) => {
const app = await createCustomServer({ dbUri: db.ctx.connectionUri });
const response = await request(app).get('/');
expect(response.ok).toBe(true);
expect(response.text).toMatch(/DOJ Form Service/);
Expand Down
9 changes: 4 additions & 5 deletions apps/server-doj/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"emitDeclarationOnly": true,
"module": "CommonJS",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"target": "es2022"
"emitDeclarationOnly": true
},
"include": ["./src/**/*"],
"exclude": ["./dist"],
"include": ["./src"],
"references": []
}
12 changes: 12 additions & 0 deletions apps/server-doj/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config';
import { getDatabaseTestContainerGlobalSetupPath } from '@atj/database';

import sharedTestConfig from '../../vitest.shared';

export default defineConfig({
...sharedTestConfig,
test: {
...sharedTestConfig.test,
globalSetup: [getDatabaseTestContainerGlobalSetupPath()],
},
});
23 changes: 18 additions & 5 deletions apps/server-kansas/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { createCustomServer } from './server';
import { createCustomServer } from './server.js';

const port = process.env.PORT || 4321;
const app = await createCustomServer();

app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
const getCloudGovServerSecrets = () => {
if (process.env.VCAP_SERVICES === undefined) {
throw new Error('VCAP_SERVICES not found');
}
const services = JSON.parse(process.env.VCAP_SERVICES || '{}');
return {
//loginGovClientSecret: services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY,
dbUri: services['aws-rds'][0].credentials.uri as string,
};
};

const secrets = getCloudGovServerSecrets();
createCustomServer({ dbUri: secrets?.dbUri }).then((server: any) =>
server.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
})
);
45 changes: 20 additions & 25 deletions apps/server-kansas/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import {
createDatabaseGateway,
createPostgresDatabaseContext,
} from '@atj/database';
import { createServer } from '@atj/server';

const getDirname = () => dirname(fileURLToPath(import.meta.url));

export const createCustomServer = async (): Promise<any> => {
const { createDevDatabaseContext, createDatabaseGateway } = await import(
'@atj/database'
);
const { createServer } = await import('@atj/server');

const dbCtx = await createDevDatabaseContext(
path.join(getDirname(), '../doj.db')
export const createCustomServer = async (ctx: {
dbUri: string;
}): Promise<any> => {
const db = createDatabaseGateway(
await createPostgresDatabaseContext(ctx.dbUri, true)
);
const db = createDatabaseGateway(dbCtx);

return createServer({
title: 'KS Courts Form Service',
title: 'DOJ Form Service',
db,
loginGovOptions: {
loginGovUrl: 'https://idp.int.identitysandbox.gov',
clientId:
'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:tts-10x-atj-dev-server-doj',
//clientSecret: '', // secrets.loginGovClientSecret,
},
isUserAuthorized: async (email: string) => {
return [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
].includes(email);
},
});
};

/*
const getServerSecrets = () => {
const services = JSON.parse(process.env.VCAP_SERVICES || '{}');
const loginClientSecret =
services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY;
return {
loginGovClientSecret: loginClientSecret,
};
};
*/
18 changes: 10 additions & 8 deletions apps/server-kansas/tests/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import request from 'supertest';
import { beforeAll, describe, expect, test } from 'vitest';
import { describe, expect, test } from 'vitest';
import { describeDatabase } from '@atj/database/testing';

import { createCustomServer } from '../src/server';

describe('Kansas State Courts Form Service', () => {
let app: any;

beforeAll(async () => {
app = await createCustomServer();
describe('DOJ Form Service', () => {
test('avoid "No test suite found in file" error', async () => {
expect(true).toBe(true);
});
});

test('renders the home page', async () => {
describeDatabase('Kansas State Courts Form Service', () => {
test('renders the home page', async ({ db }) => {
const app = await createCustomServer({ dbUri: db.ctx.connectionUri });
const response = await request(app).get('/');
expect(response.ok).toBe(true);
expect(response.text).toMatch(/KS Courts Form Service/);
expect(response.text).toMatch(/DOJ Form Service/);
});
});
31 changes: 31 additions & 0 deletions infra/cdktf/src/lib/cloud.gov/node-astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ export class AstroService extends Construct {
}
);

const rds =
new cloudfoundry.dataCloudfoundryService.DataCloudfoundryService(
scope,
`${id}-data-aws-rds`,
{
name: 'aws-rds',
}
);

const dbInstance = new cloudfoundry.serviceInstance.ServiceInstance(
this,
`${id}-db`,
{
name: `${id}-db`,
servicePlan: rds.servicePlans.lookup('micro-psql'),
space: spaceId,
jsonParams: '{"version": "15"}',
lifecycle: {
preventDestroy: true,
},
timeouts: {
create: '60m',
update: '60m',
delete: '2h',
},
}
);

new cloudfoundry.app.App(this, `${id}-app`, {
name: `${id}-app`,
space: spaceId,
Expand All @@ -55,6 +83,9 @@ export class AstroService extends Construct {
},
],
serviceBinding: [
{
serviceInstance: dbInstance.id,
},
{
serviceInstance: loginGovService.id,
},
Expand Down
1 change: 1 addition & 0 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@atj/common": "workspace:^",
"@atj/database": "workspace:*",
"@lucia-auth/adapter-postgresql": "^3.1.2",
"@lucia-auth/adapter-sqlite": "^3.0.2",
"arctic": "^1.9.2",
"better-sqlite3": "^11.1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,30 @@ import { Cookie, Lucia } from 'lucia';
import { type DatabaseGateway } from '@atj/database';

import { type AuthContext, type UserSession } from '..';
import { createTestLuciaAdapter } from '../lucia';
import { createPostgresLuciaAdapter, createSqliteLuciaAdapter } from '../lucia';
import { LoginGov } from '../provider';

export class DevAuthContext implements AuthContext {
export class BaseAuthContext implements AuthContext {
private lucia?: Lucia;

constructor(
public db: DatabaseGateway,
public provider: LoginGov,
public getCookie: (name: string) => string | undefined,
public setCookie: (cookie: Cookie) => void,
public setUserSession: (userSession: UserSession) => void
public setUserSession: (userSession: UserSession) => void,
public isUserAuthorized: (email: string) => Promise<boolean>
) {}

async getLucia() {
const sqlite3Adapter = createTestLuciaAdapter(
await (this.db.getContext() as any).getSqlite3()
);
const sqlite3Adapter =
this.db.getContext().engine === 'sqlite'
? createSqliteLuciaAdapter(
await (this.db.getContext() as any).getSqlite3()
)
: createPostgresLuciaAdapter(
await (this.db.getContext() as any).getPostgresPool()
);
if (!this.lucia) {
this.lucia = new Lucia(sqlite3Adapter, {
sessionCookie: {
Expand Down
Loading

0 comments on commit 7dd7dd8

Please sign in to comment.