|
| 1 | +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * Integration test for EnvironmentProvisioningService + TursoEnvironmentDatabaseAdapter. |
| 5 | + * |
| 6 | + * Requires TURSO_ORG_NAME and TURSO_API_TOKEN — loaded from .env.local if present. |
| 7 | + * |
| 8 | + * Run: pnpm --filter @objectstack/server test:provisioning |
| 9 | + */ |
| 10 | + |
| 11 | +import { readFileSync } from 'node:fs'; |
| 12 | +import { resolve, dirname } from 'node:path'; |
| 13 | +import { fileURLToPath } from 'node:url'; |
| 14 | + |
| 15 | +// --------------------------------------------------------------------------- |
| 16 | +// Load .env.local |
| 17 | +// --------------------------------------------------------------------------- |
| 18 | + |
| 19 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 20 | +const envLocalPath = resolve(__dirname, '../.env.local'); |
| 21 | +try { |
| 22 | + const lines = readFileSync(envLocalPath, 'utf-8').split('\n'); |
| 23 | + for (const line of lines) { |
| 24 | + const trimmed = line.trim(); |
| 25 | + if (!trimmed || trimmed.startsWith('#')) continue; |
| 26 | + const eq = trimmed.indexOf('='); |
| 27 | + if (eq === -1) continue; |
| 28 | + const key = trimmed.slice(0, eq).trim(); |
| 29 | + const raw = trimmed.slice(eq + 1).trim(); |
| 30 | + const value = raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw; |
| 31 | + if (!(key in process.env)) process.env[key] = value; |
| 32 | + } |
| 33 | +} catch { |
| 34 | + // .env.local absent — rely on environment variables already being set |
| 35 | +} |
| 36 | + |
| 37 | +// --------------------------------------------------------------------------- |
| 38 | +// Assertions |
| 39 | +// --------------------------------------------------------------------------- |
| 40 | + |
| 41 | +function assert(condition: boolean, message: string): asserts condition { |
| 42 | + if (!condition) throw new Error(`FAIL: ${message}`); |
| 43 | +} |
| 44 | + |
| 45 | +function ok(label: string) { |
| 46 | + console.log(` ✓ ${label}`); |
| 47 | +} |
| 48 | + |
| 49 | +// --------------------------------------------------------------------------- |
| 50 | +// Test: TursoEnvironmentDatabaseAdapter |
| 51 | +// --------------------------------------------------------------------------- |
| 52 | + |
| 53 | +import { |
| 54 | + TursoEnvironmentDatabaseAdapter, |
| 55 | + createDefaultEnvironmentAdapters, |
| 56 | + EnvironmentProvisioningService, |
| 57 | +} from '@objectstack/service-tenant'; |
| 58 | +import { TursoPlatformClient } from '@objectstack/service-tenant'; |
| 59 | + |
| 60 | +const orgName = process.env.TURSO_ORG_NAME; |
| 61 | +const apiToken = process.env.TURSO_API_TOKEN; |
| 62 | + |
| 63 | +if (!orgName || !apiToken) { |
| 64 | + console.error('SKIP: TURSO_ORG_NAME and TURSO_API_TOKEN must be set.'); |
| 65 | + process.exit(0); |
| 66 | +} |
| 67 | + |
| 68 | +console.log('\n── Environment Provisioning Integration Test ──\n'); |
| 69 | +console.log(` org: ${orgName}`); |
| 70 | + |
| 71 | +const createdDbs: string[] = []; |
| 72 | + |
| 73 | +async function cleanup(client: TursoPlatformClient) { |
| 74 | + for (const name of createdDbs) { |
| 75 | + try { |
| 76 | + await client.deleteDatabase(name); |
| 77 | + console.log(` 🗑 cleaned up: ${name}`); |
| 78 | + } catch (e) { |
| 79 | + console.warn(` ⚠ failed to delete ${name}:`, (e as Error).message); |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +async function run() { |
| 85 | + const client = new TursoPlatformClient({ apiToken: apiToken!, organization: orgName! }); |
| 86 | + const adapter = new TursoEnvironmentDatabaseAdapter({ apiToken: apiToken!, organization: orgName! }); |
| 87 | + |
| 88 | + // ── Test 1: createDefaultEnvironmentAdapters picks Turso when TURSO_ORG_NAME is set |
| 89 | + { |
| 90 | + const adapters = createDefaultEnvironmentAdapters(); |
| 91 | + assert(adapters.length === 1, 'should return exactly one adapter'); |
| 92 | + assert(adapters[0].driver === 'turso', `expected driver=turso, got ${adapters[0].driver}`); |
| 93 | + ok('createDefaultEnvironmentAdapters → TursoEnvironmentDatabaseAdapter'); |
| 94 | + } |
| 95 | + |
| 96 | + // ── Test 2: TursoEnvironmentDatabaseAdapter.createDatabase |
| 97 | + let databaseUrl = ''; |
| 98 | + let plaintextSecret = ''; |
| 99 | + let databaseName = ''; |
| 100 | + { |
| 101 | + const envId = `test-${Date.now()}`; |
| 102 | + databaseName = `env-${envId}`; |
| 103 | + createdDbs.push(databaseName); |
| 104 | + |
| 105 | + const result = await adapter.createDatabase({ |
| 106 | + environmentId: envId, |
| 107 | + databaseName, |
| 108 | + region: 'us-east-1', |
| 109 | + storageLimitMb: 256, |
| 110 | + }); |
| 111 | + |
| 112 | + assert(result.databaseUrl.startsWith('libsql://'), `expected libsql:// URL, got: ${result.databaseUrl}`); |
| 113 | + assert(result.plaintextSecret.length > 0, 'expected non-empty JWT token'); |
| 114 | + ok(`createDatabase → ${result.databaseUrl}`); |
| 115 | + |
| 116 | + databaseUrl = result.databaseUrl; |
| 117 | + plaintextSecret = result.plaintextSecret; |
| 118 | + } |
| 119 | + |
| 120 | + // ── Test 3: EnvironmentProvisioningService.provisionEnvironment end-to-end |
| 121 | + { |
| 122 | + const svc = new EnvironmentProvisioningService({ |
| 123 | + adapters: [adapter], |
| 124 | + defaultDriver: 'turso', |
| 125 | + }); |
| 126 | + |
| 127 | + const dbName2 = `env-svc-${Date.now()}`; |
| 128 | + createdDbs.push(dbName2); |
| 129 | + |
| 130 | + const result = await svc.provisionEnvironment({ |
| 131 | + organizationId: 'org-integration-test', |
| 132 | + slug: 'test-env', |
| 133 | + envType: 'test', |
| 134 | + createdBy: 'integration-test', |
| 135 | + driver: 'turso', |
| 136 | + }); |
| 137 | + |
| 138 | + assert(result.environment.databaseUrl.startsWith('libsql://'), 'environment URL must be libsql://'); |
| 139 | + assert(result.environment.databaseDriver === 'turso', 'driver must be turso'); |
| 140 | + assert(result.credential.secretCiphertext.length > 0, 'credential must have ciphertext'); |
| 141 | + ok(`provisionEnvironment → ${result.environment.databaseUrl}`); |
| 142 | + |
| 143 | + // Track the actual db name for cleanup |
| 144 | + const hostname = result.environment.databaseUrl.replace('libsql://', ''); |
| 145 | + const actualDbName = hostname.split('.')[0]; |
| 146 | + if (!createdDbs.includes(actualDbName)) createdDbs.push(actualDbName); |
| 147 | + } |
| 148 | + |
| 149 | + await cleanup(client); |
| 150 | + console.log('\n✅ All integration tests passed.\n'); |
| 151 | +} |
| 152 | + |
| 153 | +run().catch(async (err) => { |
| 154 | + const client = new TursoPlatformClient({ apiToken: apiToken!, organization: orgName! }); |
| 155 | + await cleanup(client); |
| 156 | + console.error('\n❌', err.message); |
| 157 | + process.exit(1); |
| 158 | +}); |
0 commit comments