cicd: removed chromium install step and added hyperbrowser

This commit is contained in:
John Yeo
2025-07-19 12:35:07 +01:00
parent da671f5c1d
commit 4541db250e
14 changed files with 316 additions and 404 deletions

View File

@@ -22,7 +22,7 @@ env:
jobs:
test:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
@@ -44,52 +44,52 @@ jobs:
- name: Setup CI
run: pnpm run setupci
- 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 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: Install Chrome
# run: |
# cd server/
# npx puppeteer browsers install chrome
- name: Create .env file
run: |
@@ -104,8 +104,7 @@ jobs:
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
echo "HYPERBROWSER_API_KEY=${{ env.HYPERBROWSER_API_KEY }}" >> .env
cat .env
- name: Build shared

4
.gitignore vendored
View File

@@ -45,6 +45,10 @@ pull.sh
# testing
/coverage
# temporary files
**/.tmp/
.tmp/
# next.js
/.next/
/out/

View File

@@ -74,8 +74,7 @@ services:
- 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}
- HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
depends_on:
- shared
restart: unless-stopped

154
pnpm-lock.yaml generated
View File

@@ -51,6 +51,9 @@ importers:
'@axiomhq/pino':
specifier: ^1.3.1
version: 1.3.1
'@browserbasehq/sdk':
specifier: ^2.6.0
version: 2.6.0
'@clerk/express':
specifier: ^1.3.22
version: 1.7.5(express@4.21.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -63,6 +66,9 @@ importers:
'@date-fns/utc':
specifier: ^2.1.0
version: 2.1.0
'@hyperbrowser/sdk':
specifier: ^0.54.0
version: 0.54.0
'@hyperdx/node-opentelemetry':
specifier: ^0.8.2
version: 0.8.2
@@ -213,6 +219,9 @@ importers:
posthog-node:
specifier: ^4.17.2
version: 4.18.0
puppeteer-core:
specifier: ^24.14.0
version: 24.14.0
react:
specifier: ^18.2.0
version: 18.3.1
@@ -238,9 +247,6 @@ 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
@@ -283,12 +289,6 @@ importers:
mocha:
specifier: ^11.1.0
version: 11.7.1
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)
@@ -1231,6 +1231,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@hyperbrowser/sdk@0.54.0':
resolution: {integrity: sha512-QmbwMG6niqInlS0FfUWWzvv+DfyINHGnF8i/0oNuyyuiIdqSXnp+UG/74Ci+yz8QuyezmuKzBSp6dBXlVG0Glw==}
'@hyperdx/instrumentation-exception@0.1.0':
resolution: {integrity: sha512-Jgk7JY5J07Mq9fgXApGVhSkS4+WdzzRcWLReAZhxgo46KShxE6w614mFqUSnuo+z6ghlehsy4ForViUfxrFyew==}
peerDependencies:
@@ -2658,11 +2661,6 @@ packages:
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@puppeteer/browsers@2.10.5':
resolution: {integrity: sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==}
engines: {node: '>=18'}
hasBin: true
'@puppeteer/browsers@2.10.6':
resolution: {integrity: sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==}
engines: {node: '>=18'}
@@ -4414,11 +4412,6 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chromium-bidi@5.1.0:
resolution: {integrity: sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==}
peerDependencies:
devtools-protocol: '*'
chromium-bidi@7.1.1:
resolution: {integrity: sha512-L2BKQ0rSLADgbPMIdDh3wnYHs3EiUiMay2Sq0CTolheaADmWIf6Pe+T9LJRcnh5rcMz0U7MVk0cQVvKsGRMa1g==}
peerDependencies:
@@ -4565,15 +4558,6 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
cosmiconfig@9.0.0:
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
engines: {node: '>=14'}
peerDependencies:
typescript: '>=4.9.5'
peerDependenciesMeta:
typescript:
optional: true
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
@@ -4968,13 +4952,6 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -5524,9 +5501,6 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
@@ -5656,9 +5630,6 @@ packages:
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -5868,9 +5839,6 @@ packages:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -6307,10 +6275,6 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parseley@0.12.1:
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
@@ -6524,19 +6488,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
puppeteer-core@24.12.0:
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'}
hasBin: true
pvtsutils@1.3.6:
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
@@ -8207,6 +8162,15 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@hyperbrowser/sdk@0.54.0':
dependencies:
form-data: 4.0.3
node-fetch: 2.7.0
zod: 3.25.76
zod-to-json-schema: 3.24.6(zod@3.25.76)
transitivePeerDependencies:
- encoding
'@hyperdx/instrumentation-exception@0.1.0(@opentelemetry/api@1.9.0)':
dependencies:
'@hyperdx/instrumentation-sentry-node': 0.1.0(@opentelemetry/api@1.9.0)
@@ -10177,19 +10141,6 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
'@puppeteer/browsers@2.10.5':
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
'@puppeteer/browsers@2.10.6':
dependencies:
debug: 4.4.1
@@ -12031,12 +11982,6 @@ snapshots:
dependencies:
readdirp: 4.1.2
chromium-bidi@5.1.0(devtools-protocol@0.0.1464554):
dependencies:
devtools-protocol: 0.0.1464554
mitt: 3.0.1
zod: 3.25.76
chromium-bidi@7.1.1(devtools-protocol@0.0.1464554):
dependencies:
devtools-protocol: 0.0.1464554
@@ -12176,15 +12121,6 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
cosmiconfig@9.0.0(typescript@5.7.3):
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
js-yaml: 4.1.0
parse-json: 5.2.0
optionalDependencies:
typescript: 5.7.3
create-require@1.1.1: {}
cron-parser@4.9.0:
@@ -12471,12 +12407,6 @@ snapshots:
entities@4.5.0: {}
env-paths@2.2.1: {}
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -13155,8 +13085,6 @@ snapshots:
ipaddr.js@1.9.1: {}
is-arrayish@0.2.1: {}
is-arrayish@0.3.2:
optional: true
@@ -13249,8 +13177,6 @@ snapshots:
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@0.4.1: {}
json-schema@0.4.0: {}
@@ -13467,8 +13393,6 @@ snapshots:
lightningcss-win32-x64-msvc: 1.30.1
optional: true
lines-and-columns@1.2.4: {}
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -13920,13 +13844,6 @@ snapshots:
dependencies:
callsites: 3.1.0
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.27.1
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parseley@0.12.1:
dependencies:
leac: 0.6.0
@@ -14151,20 +14068,6 @@ snapshots:
punycode@2.3.1: {}
puppeteer-core@24.12.0:
dependencies:
'@puppeteer/browsers': 2.10.5
chromium-bidi: 5.1.0(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-core@24.14.0:
dependencies:
'@puppeteer/browsers': 2.10.6
@@ -14179,21 +14082,6 @@ snapshots:
- supports-color
- utf-8-validate
puppeteer@24.12.0(typescript@5.7.3):
dependencies:
'@puppeteer/browsers': 2.10.5
chromium-bidi: 5.1.0(devtools-protocol@0.0.1464554)
cosmiconfig: 9.0.0(typescript@5.7.3)
devtools-protocol: 0.0.1464554
puppeteer-core: 24.12.0
typed-query-selector: 2.12.0
transitivePeerDependencies:
- bare-buffer
- bufferutil
- supports-color
- typescript
- utf-8-validate
pvtsutils@1.3.6:
dependencies:
tslib: 2.8.1

View File

@@ -19,6 +19,7 @@
"cron": "tsx src/cron.ts",
"cron:start": "node --no-deprecation dist/src/cron.js",
"testa-parallel": "mocha 'tests/alex/00_setup.ts' && mocha --parallel --timeout 10000000 'tests/alex/**/*.ts' --ignore 'tests/alex/00_setup.ts'",
"testa-browser": "mocha 'tests/alex/00_setup.ts' && mocha --timeout 10000000 'tests/alex/**/*.ts' --ignore 'tests/alex/00_setup.ts'",
"testa": "mocha 'tests/alex/00_setup.ts' && mocha --timeout 10000000 'tests/alex/01_free.ts' --ignore 'tests/alex/00_setup.ts'",
"test": "mocha --timeout 10000000 'tests/**/*.ts' --ignore 'tests/alex/**/*.ts'",
"test-custom": "mocha --timeout 1000000 'tests/01_product.ts'",
@@ -40,10 +41,12 @@
"@anthropic-ai/sdk": "^0.32.1",
"@autumn/shared": "workspace:*",
"@axiomhq/pino": "^1.3.1",
"@browserbasehq/sdk": "^2.6.0",
"@clerk/express": "^1.3.22",
"@clickhouse/client": "^1.11.2",
"@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"@hyperbrowser/sdk": "^0.54.0",
"@hyperdx/node-opentelemetry": "^0.8.2",
"@logtail/node": "^0.5.2",
"@opentelemetry/api": "^1.9.0",
@@ -94,6 +97,7 @@
"pino-pretty": "^13.0.0",
"postgres": "^3.4.7",
"posthog-node": "^4.17.2",
"puppeteer-core": "^24.14.0",
"react": "^18.2.0",
"recaseai": "^0.0.37",
"resend": "^4.1.1",
@@ -118,7 +122,6 @@
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.1",
"mocha": "^11.1.0",
"puppeteer": "^24.2.0",
"react-email": "4.0.16",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",

View File

@@ -3,7 +3,12 @@
# Source shared configuration
source "$(dirname "$0")/config.sh"
MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \
# If contains setup then run $MOCHA_SETUP
if [[ "$1" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
$MOCHA_CMD \
'tests/attach/migrations/*.ts' \
'tests/attach/newVersion/*.ts' \
'tests/attach/upgradeOld/*.ts' \

View File

@@ -19,6 +19,11 @@ import { initDrizzle } from "@/db/initDrizzle.js";
const ORG_SLUG = process.env.TESTS_ORG!;
const DEFAULT_ENV = AppEnv.Sandbox;
import { Hyperbrowser } from "@hyperbrowser/sdk";
const hyperbrowser = new Hyperbrowser({
apiKey: process.env.HYPERBROWSER_API_KEY,
});
describe("Initialize org for tests", () => {
it("should initialize org", async function () {
this.timeout(1000000000);

View File

@@ -66,11 +66,11 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
const res: any = await AutumnCli.entitled(
customerId,
entitlement.feature_id!,
entitlement.feature_id!
);
const entBalance = res!.balances.find(
(b: any) => b.feature_id === entitlement.feature_id,
(b: any) => b.feature_id === entitlement.feature_id
);
try {
@@ -91,205 +91,4 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
}
}
});
return;
const oneTimeBillingUnits =
products.oneTimeAddOnMetered1.prices[0].config.billing_units;
const monthlyBillingUnits =
products.monthlyAddOnMetered1.prices[0].config.billing_units;
describe("One time add on (force checkout)", () => {
// PURCHASE ONE TIME ADD ON
it("POST /attach -- attaching one time add on (force checkout) [no quantity passed in]", async function () {
try {
for (let i = 0; i < oneTimePurchaseCount; i++) {
const res = await AutumnCli.attach({
customerId: customerId,
productId: products.oneTimeAddOnMetered1.id,
forceCheckout: true,
});
await completeCheckoutForm(res.checkout_url, oneTimeOverrideQuantity);
await timeout(10000); // for webhook to be processed
console.log(` ${chalk.greenBright("Attached one time add on")}`);
}
} catch (error) {
console.group();
console.group();
console.log("Failed to attach one time add on");
console.log("Error data:", error);
console.groupEnd();
console.groupEnd();
process.exit(1);
}
});
// TODO: Attach one time add on again (with quantity?)
it("GET /customers/:id -- checking product & entitlements (one time add on)", async function () {
const cusRes = await AutumnCli.getCustomer(customerId);
// 1. Metered1 balance should be pro + one time add on
// Fetch balance
const addOnBalance = cusRes.entitlements.find(
(e: any) =>
e.feature_id === features.metered1.id &&
e.interval ==
products.oneTimeAddOnMetered1.entitlements.metered1.interval,
);
const expectedAmt =
(oneTimeOverrideQuantity || oneTimeQuantity) *
oneTimeBillingUnits *
oneTimePurchaseCount;
try {
assert.equal(addOnBalance!.balance, expectedAmt);
assert.equal(cusRes.add_ons.length, 1);
assert.equal(cusRes.add_ons[0].id, products.oneTimeAddOnMetered1.id);
assert.equal(cusRes.invoices.length, 1 + oneTimePurchaseCount);
} catch (error) {
console.group();
console.group();
console.log("GET customer, balances failed");
console.log(
"Add on entitlement:",
products.oneTimeAddOnMetered1.entitlements.metered1,
);
console.log("Customer entitlements:", cusRes.entitlements);
console.groupEnd();
console.groupEnd();
throw error;
}
});
it("GET /entitled -- checking entitled for metered1", async function () {
const res: any = await AutumnCli.entitled(
customerId,
features.metered1.id,
);
expect(res!.allowed).to.be.true;
// pro metered1
const proMetered1Amt = products.pro.entitlements.metered1.allowance;
const addOnBalance = res!.balances.find(
(b: any) => b.feature_id === features.metered1.id,
);
expect(res!.allowed).to.be.true;
expect(addOnBalance!.balance).to.equal(
proMetered1Amt! +
(oneTimeOverrideQuantity || oneTimeQuantity) *
oneTimeBillingUnits *
oneTimePurchaseCount,
);
});
});
// PURCHASE MONTHLY ADD ON
describe("Monthly add on", () => {
it("POST /attach -- attaching monthly add on", async function () {
await AutumnCli.attach({
customerId: customerId,
productId: products.monthlyAddOnMetered1.id,
forceCheckout: false,
options: [
{
feature_id: features.metered1.id,
quantity: monthlyQuantity * monthlyBillingUnits,
},
],
});
await timeout(10000);
console.log(` ${chalk.greenBright("Attached monthly top up")}`);
});
it("GET /customers/:id -- checking product & entitlements (monthly add on)", async function () {
const cusRes = await AutumnCli.getCustomer(customerId);
// 1. Metered1 balance should be pro + one time add on
const proMetered1 = products.pro.entitlements.metered1.allowance;
// Fetch balance
const monthlyMetered1Balance = cusRes.entitlements.find(
(e: any) =>
e.feature_id === features.metered1.id &&
e.interval ==
products.monthlyAddOnMetered1.entitlements.metered1.interval,
);
try {
assert.equal(
monthlyMetered1Balance!.balance,
proMetered1! + monthlyQuantity * monthlyBillingUnits,
);
assert.equal(cusRes.add_ons.length, 2);
const monthlyAddOnId = cusRes.add_ons.find(
(a: any) => a.id === products.monthlyAddOnMetered1.id,
);
assert.exists(monthlyAddOnId);
expect(cusRes.invoices.length).to.equal(2 + oneTimePurchaseCount);
} catch (error) {
console.group();
console.group();
console.log("GET customer, balances failed");
console.log(
"Add on entitlement:",
products.monthlyAddOnMetered1.entitlements.metered1,
);
console.log("Customer entitlements:", cusRes.entitlements);
console.groupEnd();
console.groupEnd();
throw error;
}
});
it("GET /entitled -- checking entitlements (monthly add on)", async function () {
const res: any = await AutumnCli.entitled(
customerId,
features.metered1.id,
);
const metered1Balance = res!.balances.find(
(b: any) => b.feature_id === features.metered1.id,
);
const proMetered1Amt = products.pro.entitlements.metered1.allowance;
const monthlyAddOnMetered1Amt = monthlyQuantity * monthlyBillingUnits;
const oneTimeAddOnMetered1Amt =
(oneTimeOverrideQuantity || oneTimeQuantity) *
oneTimeBillingUnits *
oneTimePurchaseCount;
try {
expect(metered1Balance!.balance).to.equal(
proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt,
);
} catch (error) {
console.group();
console.group();
console.log("GET entitled, balances failed");
console.log("/entitled response:", res);
console.log("Pro metered1 amt:", proMetered1Amt);
console.log("Monthly add on metered1 amt:", monthlyAddOnMetered1Amt);
console.log("One time add on metered1 amt:", oneTimeAddOnMetered1Amt);
console.groupEnd();
console.groupEnd();
throw error;
}
});
});
});

View File

@@ -104,7 +104,7 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on
await completeCheckoutForm(
res.checkout_url,
oneTimeQuantity / oneTimeBillingUnits!,
oneTimeQuantity / oneTimeBillingUnits!
);
await timeout(15000);
}
@@ -117,26 +117,26 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on
(e: any) =>
e.feature_id === features.metered1.id &&
e.interval ==
products.oneTimeAddOnMetered1.entitlements.metered1.interval,
products.oneTimeAddOnMetered1.entitlements.metered1.interval
);
const expectedAmt = oneTimeQuantity * oneTimePurchaseCount;
expect(addOnBalance!.balance).to.equal(
expectedAmt,
"add on balance should be correct",
"add on balance should be correct"
);
expect(cusRes.add_ons).to.have.lengthOf(
1,
"should only have one add on product after two purchases (since they combine)",
"should only have one add on product after two purchases (since they combine)"
);
expect(cusRes.add_ons[0].id).to.equal(
oneTime.id,
"add on product should exist",
"add on product should exist"
);
expect(cusRes.invoices.length).to.equal(
1 + oneTimePurchaseCount,
"invoices should be correct",
"invoices should be correct"
);
});
@@ -147,12 +147,12 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on
const proMetered1Amt = products.pro.entitlements.metered1.allowance;
const addOnBalance = res!.balances.find(
(b: any) => b.feature_id === features.metered1.id,
(b: any) => b.feature_id === features.metered1.id
);
expect(res!.allowed).to.be.true;
expect(addOnBalance!.balance).to.equal(
proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount,
proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount
);
});
});

View File

@@ -13,6 +13,11 @@ import { after } from "mocha";
const ORG_SLUG = process.env.TESTS_ORG!;
const DEFAULT_ENV = AppEnv.Sandbox;
import { Hyperbrowser } from "@hyperbrowser/sdk";
const hyperbrowser = new Hyperbrowser({
apiKey: process.env.HYPERBROWSER_API_KEY,
});
export const setupBefore = async (instance: any) => {
const { db, client } = initDrizzle();
@@ -24,6 +29,11 @@ export const setupBefore = async (instance: any) => {
const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!;
const autumn = new AutumnInt({ apiKey: autumnSecretKey });
// console.log("instance.browserSession", instance.browserSession);
// if (!instance.browserSession) {
// instance.browserSession = await hyperbrowser.sessions.create();
// }
const autumnJs = new AutumnJS({
secretKey: autumnSecretKey,
url: "http://localhost:8080/v1",

View File

@@ -0,0 +1,186 @@
import { Hyperbrowser } from "@hyperbrowser/sdk";
import * as fs from "fs";
import * as path from "path";
const client = new Hyperbrowser({
apiKey: process.env.HYPERBROWSER_API_KEY,
});
let sessionPromise: Promise<any> | null = null;
let currentSession: any | null = null;
// File paths for coordination across processes
const TEMP_DIR = path.join(process.cwd(), ".tmp");
const SESSION_FILE = path.join(TEMP_DIR, "browser-session.json");
const LOCK_FILE = path.join(TEMP_DIR, "browser-session.lock");
// Ensure temp directory exists
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
export const getBrowserSession = async () => {
// If we already have a session in memory, return it
if (currentSession) {
return currentSession;
}
// Check if another process has already created a session
const existingSession = loadSessionFromFile();
if (existingSession) {
currentSession = existingSession;
console.log("Using existing browser session:", existingSession.id);
return existingSession;
}
// If session creation is already in progress, wait for it
if (sessionPromise) {
return await sessionPromise;
}
// Try to acquire lock and create session
sessionPromise = createSessionWithLock();
try {
currentSession = await sessionPromise;
return currentSession;
} catch (error) {
// Reset promise on failure so we can retry
sessionPromise = null;
throw error;
}
};
const createSessionWithLock = async (): Promise<any> => {
// Try to acquire lock
const lockAcquired = await acquireLock();
if (!lockAcquired) {
// Another process is creating the session, wait for it
console.log("Waiting for another process to create browser session...");
return await waitForSession();
}
try {
// Double-check if session was created while we were acquiring lock
const existingSession = loadSessionFromFile();
if (existingSession) {
console.log(
"Session was created by another process:",
existingSession.id
);
return existingSession;
}
// Create new session
console.log("Creating new browser session...");
const session = await client.sessions.create();
console.log("Browser session created successfully:", session.id);
// Save session to file for other processes
saveSessionToFile(session);
return session;
} finally {
// Always release lock
releaseLock();
}
};
const acquireLock = async (): Promise<boolean> => {
try {
// Try to create lock file exclusively
fs.writeFileSync(LOCK_FILE, process.pid.toString(), { flag: "wx" });
return true;
} catch (error) {
// Lock file exists, another process has the lock
return false;
}
};
const releaseLock = () => {
try {
if (fs.existsSync(LOCK_FILE)) {
fs.unlinkSync(LOCK_FILE);
}
} catch (error) {
// Ignore errors when releasing lock
console.warn("Warning: Could not release lock file:", error);
}
};
const saveSessionToFile = (session: any) => {
try {
fs.writeFileSync(SESSION_FILE, JSON.stringify(session));
} catch (error) {
console.error("Failed to save session to file:", error);
}
};
const loadSessionFromFile = (): any | null => {
try {
if (fs.existsSync(SESSION_FILE)) {
const sessionData = fs.readFileSync(SESSION_FILE, "utf-8");
return JSON.parse(sessionData);
}
} catch (error) {
// If file is corrupted or doesn't exist, ignore
console.warn("Could not load session from file:", error);
}
return null;
};
const waitForSession = async (): Promise<any> => {
// Poll for session file to appear
for (let i = 0; i < 30; i++) {
// Wait up to 30 seconds
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
const session = loadSessionFromFile();
if (session) {
console.log("Found session created by another process:", session.id);
return session;
}
}
throw new Error(
"Timeout waiting for browser session to be created by another process"
);
};
// Reset session state (call this between test suites if needed)
export const resetBrowserSession = () => {
console.log("Resetting browser session state...");
currentSession = null;
sessionPromise = null;
// Clean up files
try {
if (fs.existsSync(SESSION_FILE)) {
fs.unlinkSync(SESSION_FILE);
}
if (fs.existsSync(LOCK_FILE)) {
fs.unlinkSync(LOCK_FILE);
}
} catch (error) {
console.warn("Warning: Could not clean up session files:", error);
}
};
// Cleanup on process exit
process.on("exit", () => {
if (currentSession) {
console.log("Process exiting, cleaning up browser session files...");
releaseLock();
}
});
process.on("SIGINT", () => {
resetBrowserSession();
process.exit(0);
});
process.on("SIGTERM", () => {
resetBrowserSession();
process.exit(0);
});

View File

@@ -236,7 +236,7 @@ export const setupOrg = async ({
logger: console,
});
const allFeatures = await FeatureService.list({ db, orgId, env });
// const allFeatures = await FeatureService.list({ db, orgId, env });
let org: Organization | null = null;
let newFeatures: Feature[] = [];

View File

@@ -1,4 +1,5 @@
import puppeteer from "puppeteer";
import "dotenv/config";
import { timeout } from "./genUtils.js";
import { Stripe } from "stripe";
import { BillingInterval, Customer, FullProduct } from "@autumn/shared";
@@ -10,23 +11,36 @@ import {
addWeeks,
format,
} from "date-fns";
import puppeteer from "puppeteer-core";
import Browserbase from "@browserbasehq/sdk";
const STRIPE_TEST_CLOCK_TIMING = 20000; // 30s
// const STRIPE_TEST_CLOCK_TIMING = 40000; // 30s
import { Hyperbrowser } from "@hyperbrowser/sdk";
const client = new Hyperbrowser({
apiKey: process.env.HYPERBROWSER_API_KEY,
});
export const completeCheckoutForm = async (
url: string,
overrideQuantity?: number,
promoCode?: string
) => {
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
const session = await client.sessions.create();
const browser = await puppeteer.connect({
browserWSEndpoint: session!.wsEndpoint,
defaultViewport: null,
});
// const browser = await puppeteer.launch({
// headless: true,
// args: ["--no-sandbox", "--disable-setuid-sandbox"],
// });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
await page.goto(url);
// await page.waitForSelector("#payment-method-accordion-item-title-card");

View File

@@ -1,4 +1,4 @@
import { usePostSWR } from "@/services/useAxiosSwr";
import { usePostSWR } from "@/services/useAxiosSwr.js";
import { ErrCode } from "@autumn/shared";
export const useTopEventNames = () => {