From 95e3cc84e8f56dfc43dfd864c464635cd3e58320 Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Fri, 22 Mar 2024 08:28:12 -0400 Subject: [PATCH] db: dev --remote test fixture (#10527) * feat: scaffold basic proxy * feat: setupDbServer util * feat: ASTRO_INTERNAL_TEST_REMOTE bypass * feat: basic --remote test * chore: add port incrementer --- packages/db/src/core/integration/index.ts | 2 +- packages/db/src/core/tokens.ts | 3 + packages/db/test/basics.test.js | 65 ++++++++++ packages/db/test/test-utils.js | 139 ++++++++++++++++++++++ 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 packages/db/test/test-utils.js diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index 2dda3b7a95..e48b95c090 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -44,7 +44,7 @@ function astroDBIntegration(): AstroIntegration { let dbPlugin: VitePlugin | undefined = undefined; const args = parseArgs(process.argv.slice(3)); - connectToStudio = args['remote']; + connectToStudio = process.env.ASTRO_INTERNAL_TEST_REMOTE || args['remote']; if (connectToStudio) { appToken = await getManagedAppTokenOrExit(); diff --git a/packages/db/src/core/tokens.ts b/packages/db/src/core/tokens.ts index 12bf431d72..2c75e37230 100644 --- a/packages/db/src/core/tokens.ts +++ b/packages/db/src/core/tokens.ts @@ -173,6 +173,9 @@ export async function getManagedAppTokenOrExit(token?: string): Promise { let fixture; @@ -73,4 +74,68 @@ describe('astro:db', () => { expect($('.username').text()).to.equal('Mario'); }); }); + + describe('development --remote', () => { + let devServer; + let remoteDbServer; + + before(async () => { + remoteDbServer = await setupRemoteDbServer(fixture.config); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer?.stop(); + await remoteDbServer?.stop(); + }); + + it('Prints the list of authors', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); + + const ul = $('.authors-list'); + expect(ul.children()).to.have.a.lengthOf(5); + expect(ul.children().eq(0).text()).to.equal('Ben'); + }); + + it('Allows expression defaults for date columns', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); + + const themeAdded = $($('.themes-list .theme-added')[0]).text(); + expect(new Date(themeAdded).getTime()).to.not.be.NaN; + }); + + it('Defaults can be overridden for dates', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); + + const themeAdded = $($('.themes-list .theme-added')[1]).text(); + expect(new Date(themeAdded).getTime()).to.not.be.NaN; + }); + + it('Allows expression defaults for text columns', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); + + const themeOwner = $($('.themes-list .theme-owner')[0]).text(); + expect(themeOwner).to.equal(''); + }); + + it('Allows expression defaults for boolean columns', async () => { + const html = await fixture.fetch('/').then((res) => res.text()); + const $ = cheerioLoad(html); + + const themeDark = $($('.themes-list .theme-dark')[0]).text(); + expect(themeDark).to.equal('dark mode'); + }); + + it('text fields an be used as references', async () => { + const html = await fixture.fetch('/login').then((res) => res.text()); + const $ = cheerioLoad(html); + + expect($('.session-id').text()).to.equal('12345'); + expect($('.username').text()).to.equal('Mario'); + }); + }); }); diff --git a/packages/db/test/test-utils.js b/packages/db/test/test-utils.js new file mode 100644 index 0000000000..635ffd6e2c --- /dev/null +++ b/packages/db/test/test-utils.js @@ -0,0 +1,139 @@ +import { createClient } from '@libsql/client'; +import { createServer } from 'node:http'; +import { z } from 'zod'; +import { cli } from '../dist/core/cli/index.js'; +import { resolveDbConfig } from '../dist/core/load-file.js'; +import { getCreateTableQuery, getCreateIndexQueries } from '../dist/runtime/queries.js'; + +const singleQuerySchema = z.object({ + sql: z.string(), + args: z.array(z.any()).or(z.record(z.string(), z.any())), +}); + +const querySchema = singleQuerySchema.or(z.array(singleQuerySchema)); + +let portIncrementer = 8081; + +/** + * @param {import('astro').AstroConfig} astroConfig + * @param {number | undefined} port + */ +export async function setupRemoteDbServer(astroConfig) { + const port = portIncrementer++; + process.env.ASTRO_STUDIO_REMOTE_DB_URL = `http://localhost:${port}`; + process.env.ASTRO_INTERNAL_TEST_REMOTE = true; + const server = createRemoteDbServer().listen(port); + + const { dbConfig } = await resolveDbConfig(astroConfig); + const setupQueries = []; + for (const [name, table] of Object.entries(dbConfig?.tables ?? {})) { + const createQuery = getCreateTableQuery(name, table); + const indexQueries = getCreateIndexQueries(name, table); + setupQueries.push(createQuery, ...indexQueries); + } + await fetch(`http://localhost:${port}/db/query`, { + method: 'POST', + body: JSON.stringify(setupQueries.map((sql) => ({ sql, args: [] }))), + headers: { + 'Content-Type': 'application/json', + }, + }); + await cli({ + config: astroConfig, + flags: { + _: [undefined, 'astro', 'db', 'execute', 'db/seed.ts'], + remote: true, + }, + }); + + return { + server, + async stop() { + delete process.env.ASTRO_STUDIO_REMOTE_DB_URL; + delete process.env.ASTRO_INTERNAL_TEST_REMOTE; + return new Promise((resolve, reject) => { + server.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }, + }; +} + +function createRemoteDbServer() { + const dbClient = createClient({ + url: ':memory:', + }); + const server = createServer((req, res) => { + if ( + !req.url.startsWith('/db/query') || + req.method !== 'POST' || + req.headers['content-type'] !== 'application/json' + ) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + success: false, + }) + ); + return; + } + const rawBody = []; + req.on('data', (chunk) => { + rawBody.push(chunk); + }); + req.on('end', async () => { + let json; + try { + json = JSON.parse(Buffer.concat(rawBody).toString()); + } catch (e) { + applyParseError(res); + return; + } + const parsed = querySchema.safeParse(json); + if (parsed.success === false) { + applyParseError(res); + return; + } + const body = parsed.data; + try { + const result = Array.isArray(body) + ? await dbClient.batch(body) + : await dbClient.execute(body); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.statusMessage = e.message; + res.end( + JSON.stringify({ + success: false, + message: e.message, + }) + ); + } + }); + }); + + server.on('close', () => { + dbClient.close(); + }); + + return server; +} + +function applyParseError(res) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.statusMessage = 'Invalid request body'; + res.end( + JSON.stringify({ + // Use JSON response with `success: boolean` property + // to match remote error responses. + success: false, + }) + ); +}