ci: finish automating tests

This commit is contained in:
Ubuntu
2025-07-18 14:51:29 +00:00
parent 4f7f379684
commit 141ad67520
8 changed files with 349 additions and 84 deletions

View File

@@ -1,6 +1,9 @@
name: Run Autumn Tests
on: [push]
on:
push:
branches:
- main
env:
DATABASE_URL: ${{ secrets.SUPABASE_URL }}
@@ -11,6 +14,10 @@ env:
TESTS_ORG: ${{ secrets.TESTS_ORG }}
TESTS_ORG_ID: ${{ secrets.TESTS_ORG_ID }}
LOCALTUNNEL_RESERVED_KEY: ${{ secrets.LOCALTUNNEL_RESERVED_KEY }}
STRIPE_WEBHOOK_URL: ${{ secrets.STRIPE_WEBHOOK_URL }}
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}
BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}
jobs:
test:
@@ -31,13 +38,74 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
run: pnpm install --no-frozen-lockfile
- name: Setup CI
run: pnpm run setupci
- name: Debug
run: cat server/.env
- name: Install Chrome Dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
ca-certificates \
fonts-liberation \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
wget \
xdg-utils
- name: Install Chrome
run: |
cd server/
npx puppeteer browsers install chrome
- name: Create .env file
run: |
echo "Creating .env file..."
echo "DATABASE_URL=${{ env.DATABASE_URL }}" >> .env
echo "UNIT_TEST_AUTUMN_SECRET_KEY=${{ env.UNIT_TEST_AUTUMN_SECRET_KEY }}" >> .env
echo "REDIS_URL=${{ env.REDIS_URL }}" >> .env
echo "ENCRYPTION_IV=${{ env.ENCRYPTION_IV }}" >> .env
echo "ENCRYPTION_PASSWORD=${{ env.ENCRYPTION_PASSWORD }}" >> .env
echo "TESTS_ORG=${{ env.TESTS_ORG }}" >> .env
echo "TESTS_ORG_ID=${{ env.TESTS_ORG_ID }}" >> .env
echo "LOCALTUNNEL_RESERVED_KEY=${{ env.LOCALTUNNEL_RESERVED_KEY }}" >> .env
echo "BETTER_AUTH_SECRET=${{ env.BETTER_AUTH_SECRET }}" >> .env
echo "STRIPE_WEBHOOK_URL=${{ env.STRIPE_WEBHOOK_URL }}" >> .env
echo "BROWSERBASE_API_KEY=${{ env.BROWSERBASE_API_KEY }}" >> .env
echo "BROWSERBASE_PROJECT_ID=${{ env.BROWSERBASE_PROJECT_ID }}" >> .env
cat .env
- name: Build shared
run: |
@@ -45,14 +113,20 @@ jobs:
pnpm build
- name: Startup server
run: sudo docker compose -f docker-compose.unix.yml up --detach
run: sudo docker compose -f docker-compose.ci.yml up --detach
- name: Wait for server to be ready
run: |
echo "Waiting for server to start..."
timeout 60s bash -c 'until curl -s --fail http://localhost:8080; do echo "Waiting..."; sleep 2; done'
timeout 120s bash -c 'until curl -s --fail http://localhost:8080; do echo "Waiting..."; sleep 2; done'
echo "Server is up!"
- name: Ping Local Tunnel
run: |
echo "Pinging Local Tunnel..."
curl https://askjdnaslkjdalkjen.loca.lt
echo "Local Tunnel pinged!"
- name: Run G1 tests
run: |
cd server/

125
docker-compose.ci.yml Normal file
View File

@@ -0,0 +1,125 @@
services:
valkey:
image: docker.io/bitnami/valkey:8.0
environment:
- ALLOW_EMPTY_PASSWORD=yes
- VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL
volumes:
- valkey-data:/bitnami/valkey/data
healthcheck:
test: ['CMD', 'redis-cli', '-h', 'localhost', '-p', '6379', 'ping']
interval: 10s
timeout: 5s
retries: 5
ports:
- "6379:6379"
restart: unless-stopped
shared:
build:
dockerfile: docker/dev.dockerfile
context: .
target: shared
volumes:
- ./shared:/app/shared
- shared-dist:/app/shared/dist
- shared-node-modules:/app/shared/node_modules
- root-node-modules:/app/node_modules
environment:
- NODE_ENV=development
restart: unless-stopped
# Vite frontend
vite:
build:
dockerfile: docker/dev.dockerfile
context: .
target: vite
ports:
- "3000:3000"
volumes:
- ./vite:/app/vite
- shared-dist:/app/shared/dist
- vite-node-modules:/app/vite/node_modules
- root-node-modules:/app/node_modules
environment:
- NODE_ENV=development
depends_on:
- shared
restart: unless-stopped
# Main Express server
server:
build:
dockerfile: docker/dev.dockerfile
context: .
target: server
ports:
- "8080:8080"
volumes:
- ./server:/app/server
- shared-dist:/app/shared/dist
- server-node-modules:/app/server/node_modules
- root-node-modules:/app/node_modules
environment:
- NODE_ENV=development
- REDIS_URL=redis://valkey:6379
- DATABASE_URL=${DATABASE_URL}
- ENCRYPTION_IV=${ENCRYPTION_IV}
- ENCRYPTION_PASSWORD=${ENCRYPTION_PASSWORD}
- TESTS_ORG=${TESTS_ORG}
- TESTS_ORG_ID=${TESTS_ORG_ID}
- LOCALTUNNEL_RESERVED_KEY=${LOCALTUNNEL_RESERVED_KEY}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BETTER_AUTH_URL=http://localhost:8080
- STRIPE_WEBHOOK_URL=${STRIPE_WEBHOOK_URL}
- BROWSERBASE_API_KEY=${BROWSERBASE_API_KEY}
- BROWSERBASE_PROJECT_ID=${BROWSERBASE_PROJECT_ID}
depends_on:
- shared
restart: unless-stopped
# BullMQ Workers
workers:
build:
dockerfile: docker/dev.dockerfile
context: .
target: workers
volumes:
# Mount server source for hot reload (workers use server code)
- ./server:/app/server
- shared-dist:/app/shared/dist
- server-node-modules:/app/server/node_modules
- root-node-modules:/app/node_modules
environment:
- NODE_ENV=development
- REDIS_URL=redis://valkey:6379
depends_on:
- shared
restart: unless-stopped
# Run localtunnel
localtunnel:
build:
dockerfile: docker/dev.dockerfile
context: .
target: localtunnel
volumes:
- ./server:/app/server
depends_on:
- server
restart: unless-stopped
volumes:
# Shared package dist output
shared-dist:
valkey-data:
# Node modules volumes to avoid host/container conflicts
shared-node-modules:
server-node-modules:
vite-node-modules:
root-node-modules:

View File

@@ -1,5 +1,4 @@
services:
valkey:
image: docker.io/bitnami/valkey:8.0
environment:
@@ -8,7 +7,7 @@ services:
volumes:
- valkey-data:/bitnami/valkey/data
healthcheck:
test: ['CMD', 'redis-cli', '-h', 'localhost', '-p', '6379', 'ping']
test: ["CMD", "redis-cli", "-h", "localhost", "-p", "6379", "ping"]
interval: 10s
timeout: 5s
retries: 5
@@ -88,7 +87,6 @@ services:
- shared
restart: unless-stopped
# Run localtunnel
localtunnel:
build:
@@ -101,7 +99,6 @@ services:
- server
restart: unless-stopped
volumes:
# Shared package dist output
shared-dist:

17
localtunnel-start.sh Executable file → Normal file
View File

@@ -1,19 +1,22 @@
#!/bin/sh
# Read LOCALTUNNEL_RESERVED_KEY from .env file
if [ -f "/app/server/.env" ]; then
export $(cat /app/server/.env | grep LOCALTUNNEL_RESERVED_KEY | grep -v '^#')
# Check for LOCAL_TUNNEL_RESERVED_KEY environment variable first
if [ -z "$LOCAL_TUNNEL_RESERVED_KEY" ]; then
# Read LOCAL_TUNNEL_RESERVED_KEY from .env file if env var not set
if [ -f "/app/server/.env" ]; then
export $(cat /app/server/.env | grep LOCAL_TUNNEL_RESERVED_KEY | grep -v '^#')
fi
fi
# Set default subdomain if env var not found
if [ -z "$LOCALTUNNEL_RESERVED_KEY" ]; then
LOCALTUNNEL_RESERVED_KEY="autumn-dev"
if [ -z "$LOCAL_TUNNEL_RESERVED_KEY" ]; then
LOCAL_TUNNEL_RESERVED_KEY="autumn-dev"
fi
echo "Installing localtunnel..."
echo "Reserved key: ${LOCALTUNNEL_RESERVED_KEY}"
echo "Reserved key: ${LOCAL_TUNNEL_RESERVED_KEY}"
npm install -g localtunnel
echo "Server is ready! Starting localtunnel..."
lt --port 8080 --local-host server --subdomain ${LOCALTUNNEL_RESERVED_KEY} --print-requests
lt --port 8080 --local-host server --subdomain ${LOCAL_TUNNEL_RESERVED_KEY} --print-requests

69
pnpm-lock.yaml generated
View File

@@ -235,6 +235,9 @@ importers:
specifier: ^3.25.23
version: 3.25.76
devDependencies:
'@browserbasehq/sdk':
specifier: ^2.6.0
version: 2.6.0
'@types/chai':
specifier: ^5.0.1
version: 5.2.2
@@ -280,6 +283,9 @@ importers:
puppeteer:
specifier: ^24.2.0
version: 24.12.0(typescript@5.7.3)
puppeteer-core:
specifier: ^24.14.0
version: 24.14.0
react-email:
specifier: 4.0.16
version: 4.0.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -754,6 +760,9 @@ packages:
'@better-fetch/fetch@1.1.18':
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
'@browserbasehq/sdk@2.6.0':
resolution: {integrity: sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA==}
'@clerk/backend@2.4.1':
resolution: {integrity: sha512-NryqYBslAWMNkw/8ff6qK9kMrbWPIAtNOfkr62Rmtyh/MODRQJI0GmHgLg0DcBQofPrRCIXhauhfJRWqweN3Mw==}
engines: {node: '>=18.17.0'}
@@ -2195,6 +2204,7 @@ packages:
'@opentelemetry/instrumentation-redis-4@0.49.0':
resolution: {integrity: sha512-i+Wsl7M2LXEDA2yXouNJ3fttSzzb5AhlehvSBVRIFuinY51XrrKSH66biO0eox+pYQMwAlPxJ778XcMQffN78A==}
engines: {node: ^18.19.0 || >=20.6.0}
deprecated: Use "@opentelemetry/instrumentation-redis", which (as of v0.50.0) includes support for instrumenting redis v4.
peerDependencies:
'@opentelemetry/api': ^1.3.0
@@ -2650,6 +2660,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@puppeteer/browsers@2.10.6':
resolution: {integrity: sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==}
engines: {node: '>=18'}
hasBin: true
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
@@ -4401,6 +4416,11 @@ packages:
peerDependencies:
devtools-protocol: '*'
chromium-bidi@7.1.1:
resolution: {integrity: sha512-L2BKQ0rSLADgbPMIdDh3wnYHs3EiUiMay2Sq0CTolheaADmWIf6Pe+T9LJRcnh5rcMz0U7MVk0cQVvKsGRMa1g==}
peerDependencies:
devtools-protocol: '*'
cjs-module-lexer@1.4.3:
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
@@ -6502,6 +6522,10 @@ packages:
resolution: {integrity: sha512-VrPXPho5Q90Ao86FwJVb+JeAF2Tf41wOTGg8k2SyQJePiJ6hJ5iujYpmP+bmhlb6o+J26bQYRDPOYXP7ALWcxQ==}
engines: {node: '>=18'}
puppeteer-core@24.14.0:
resolution: {integrity: sha512-NO9XpCl+i8oB0zJp81iPhzMo2QK8/JTj4ramSvTpGCo9CPCNo4AZ8qVOGpSgXzlcOfOT3VHOkzTfPo08GOE5jA==}
engines: {node: '>=18'}
puppeteer@24.12.0:
resolution: {integrity: sha512-MJtM71qex8h03bDBZTyPfSC7tfvDLILnWWl4rNdo3+HODiFZX+3yj/qLVwVu/gXoxQ7U8dNDKyFz4e8VBHdcmw==}
engines: {node: '>=18'}
@@ -7817,6 +7841,18 @@ snapshots:
'@better-fetch/fetch@1.1.18': {}
'@browserbasehq/sdk@2.6.0':
dependencies:
'@types/node': 18.19.117
'@types/node-fetch': 2.6.12
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
'@clerk/backend@2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@clerk/shared': 3.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -10148,6 +10184,19 @@ snapshots:
- bare-buffer
- supports-color
'@puppeteer/browsers@2.10.6':
dependencies:
debug: 4.4.1
extract-zip: 2.0.1
progress: 2.0.3
proxy-agent: 6.5.0
semver: 7.7.2
tar-fs: 3.1.0
yargs: 17.7.2
transitivePeerDependencies:
- bare-buffer
- supports-color
'@radix-ui/number@1.1.0': {}
'@radix-ui/primitive@1.1.1': {}
@@ -11982,6 +12031,12 @@ snapshots:
mitt: 3.0.1
zod: 3.25.76
chromium-bidi@7.1.1(devtools-protocol@0.0.1464554):
dependencies:
devtools-protocol: 0.0.1464554
mitt: 3.0.1
zod: 3.25.76
cjs-module-lexer@1.4.3: {}
class-variance-authority@0.7.1:
@@ -14092,6 +14147,20 @@ snapshots:
- supports-color
- utf-8-validate
puppeteer-core@24.14.0:
dependencies:
'@puppeteer/browsers': 2.10.6
chromium-bidi: 7.1.1(devtools-protocol@0.0.1464554)
debug: 4.4.1
devtools-protocol: 0.0.1464554
typed-query-selector: 2.12.0
ws: 8.18.3
transitivePeerDependencies:
- bare-buffer
- bufferutil
- supports-color
- utf-8-validate
puppeteer@24.12.0(typescript@5.7.3):
dependencies:
'@puppeteer/browsers': 2.10.5

View File

@@ -2,6 +2,8 @@ import { logger } from "@/external/logtail/logtailUtils.js";
import "dotenv/config";
export const checkEnvVars = () => {
console.log("Database URL", process.env.DATABASE_URL);
if (!process.env.DATABASE_URL) {
console.error(`DATABASE_URL is not set`);
process.exit(1);

View File

@@ -32,56 +32,60 @@ export const completeCheckoutForm = async (
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
await page.goto(url);
await page.goto(url);
// await page.waitForSelector("#payment-method-accordion-item-title-card");
// await page.click("#payment-method-accordion-item-title-card");
// await page.waitForSelector("#payment-method-accordion-item-title-card");
// await page.click("#payment-method-accordion-item-title-card");
await page.waitForSelector("#cardNumber");
await page.type("#cardNumber", "4242424242424242");
await page.waitForSelector("#cardNumber");
await page.type("#cardNumber", "4242424242424242");
await page.waitForSelector("#cardExpiry");
await page.type("#cardExpiry", "1234");
await page.waitForSelector("#cardExpiry");
await page.type("#cardExpiry", "1234");
await page.waitForSelector("#cardCvc");
await page.type("#cardCvc", "123");
await page.waitForSelector("#cardCvc");
await page.type("#cardCvc", "123");
await page.waitForSelector("#billingName");
await page.type("#billingName", "Test Customer");
await page.waitForSelector("#billingPostalCode");
await page.type("#billingPostalCode", "123456");
await page.waitForSelector("#billingName");
await page.type("#billingName", "Test Customer");
await page.waitForSelector("#billingPostalCode");
await page.type("#billingPostalCode", "123456");
if (overrideQuantity) {
// console.log(" - Overriding quantity");
const quantityBtn = await page.$(".AdjustableQuantitySelector");
await quantityBtn?.evaluate((b: any) => (b as HTMLElement).click());
if (overrideQuantity) {
// console.log(" - Overriding quantity");
const quantityBtn = await page.$(".AdjustableQuantitySelector");
await quantityBtn?.evaluate((b: any) => (b as HTMLElement).click());
await page.waitForSelector("#adjustQuantity");
await page.click("#adjustQuantity", { clickCount: 3 }); // Select all text
await page.keyboard.press("Backspace"); // Delete selected text
await page.type("#adjustQuantity", overrideQuantity.toString());
await page.waitForSelector("#adjustQuantity");
await page.click("#adjustQuantity", { clickCount: 3 }); // Select all text
await page.keyboard.press("Backspace"); // Delete selected text
await page.type("#adjustQuantity", overrideQuantity.toString());
const updateBtn = await page.$(".AdjustQuantityFooter-btn");
await updateBtn?.evaluate((b: any) => (b as HTMLElement).click());
await timeout(1000);
const updateBtn = await page.$(".AdjustQuantityFooter-btn");
await updateBtn?.evaluate((b: any) => (b as HTMLElement).click());
await timeout(1000);
}
if (promoCode) {
await page.waitForSelector("#promotionCode");
await page.click("#promotionCode");
await page.type("#promotionCode", promoCode);
await page.keyboard.press("Enter");
await timeout(5000);
}
// const submitButton = await page.$(".SubmitButton-TextContainer");
const submitButton = await page.$(".SubmitButton-TextContainer");
await submitButton?.evaluate((b: any) => (b as HTMLElement).click());
await timeout(7000);
} finally {
// always close browser
await browser.close();
}
if (promoCode) {
await page.waitForSelector("#promotionCode");
await page.click("#promotionCode");
await page.type("#promotionCode", promoCode);
await page.keyboard.press("Enter");
await timeout(5000);
}
// const submitButton = await page.$(".SubmitButton-TextContainer");
const submitButton = await page.$(".SubmitButton-TextContainer");
await submitButton?.evaluate((b: any) => (b as HTMLElement).click());
await timeout(7000);
await browser.close();
};
export const deleteAllStripeProducts = async ({

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env node
import { randomBytes } from 'crypto';
import { writeFileSync, copyFileSync } from 'fs';
import inquirer from 'inquirer';
import { spawnSync } from 'child_process';
import chalk from 'chalk';
const genUrlSafeBase64 = (bytes) => {
@@ -30,39 +32,21 @@ const genAlphanumericPassword = (length = 24) => {
return result;
}
const handleLocalRunSetup = async () => {
// Step 10: Stripe webhook URL setup
console.log(chalk.magentaBright('\n================ Stripe Webhook Setup ================\n'));
const subdomain = genRandomSubdomain(32);
const webhookUrl = `https://${subdomain}.loca.lt`;
stripeWebhookVars.push(`STRIPE_WEBHOOK_URL=${webhookUrl}`);
stripeWebhookVars.push(`LOCALHOST_RUN_SUBDOMAIN=${subdomain}`);
console.log(chalk.greenBright(`\nTo start your webhook tunnel, run this in another terminal:\n`));
console.log(chalk.yellowBright(` npx localtunnel --port 8080 --subdomain ${subdomain}\n`));
console.log(chalk.greenBright(`\nTest your webhook URL with:\n`));
console.log(chalk.yellowBright(` curl ${webhookUrl}\n`));
console.log(chalk.cyan('If you need to restart the tunnel in the future, use the same command.'));
console.log(chalk.yellow('--------------------------------'));
return stripeWebhookVars;
}
async function main() {
// Step 1: Generate secrets
console.log(chalk.magentaBright('\n================ Autumn Setup ================\n'));
const localtunnelReservedKey = "askjdnaslkjdalkjen";
const secrets = {
BETTER_AUTH_SECRET: genUrlSafeBase64(64),
ENCRYPTION_IV: process.env.ENCRYPTION_IV,
ENCRYPTION_PASSWORD: process.env.ENCRYPTION_PASSWORD,
BETTER_AUTH_URL: 'http://localhost:8080',
CLIENT_URL: 'http://localhost:3000',
STRIPE_WEBHOOK_URL: `https://${localtunnelReservedKey}.loca.lt`,
LOCALTUNNEL_RESERVED_KEY: process.env.LOCALTUNNEL_RESERVED_KEY,
STRIPE_WEBHOOK_URL: process.env.STRIPE_WEBHOOK_URL,
};
let databaseUrl = "";
let databaseUrl = process.env.DATABASE_URL;
let stripeWebhookVars = [];
// databaseUrl = await handleDatabaseSetup();
// stripeWebhookVars = await handleLocalRunSetup();
// Step 11: Write to server/.env
@@ -81,14 +65,21 @@ async function main() {
// Stripe required section
envSections.push(
'# Stripe',
`LOCALTUNNEL_RESERVED_KEY=${secrets.LOCALTUNNEL_RESERVED_KEY}`,
`ENCRYPTION_IV=${secrets.ENCRYPTION_IV}`,
`ENCRYPTION_PASSWORD=${secrets.ENCRYPTION_PASSWORD}`,
`STRIPE_WEBHOOK_URL=${secrets.STRIPE_WEBHOOK_URL}`,
''
);
envSections.push(
'# Database',
''
);
// Database section
if (databaseUrl) {
envSections.push(
'# Database',
`DATABASE_URL=${databaseUrl}`,
''
);
}
// Stripe Webhooks section
if (stripeWebhookVars.length > 0) {