From 3ca115f843ba0c720c201dc601427eff7d219453 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 7 Jan 2025 15:54:21 -0500 Subject: [PATCH 1/6] shard realm server tests --- .github/workflows/ci.yaml | 22 ++++++++++++++++++++-- packages/realm-server/package.json | 3 ++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5899aaf6a2..249eab7a15 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -234,8 +234,24 @@ jobs: name: Realm Server Tests runs-on: ubuntu-latest concurrency: - group: realm-server-test-${{ github.head_ref || github.run_id }} + group: realm-server-test-${{ matrix.testModule || github.head_ref || github.run_id }} cancel-in-progress: true + strategy: + fail-fast: false + matrix: + testModule: + [ + "auth-client-test.ts", + "billing-test.ts", + "index-query-engine-test.ts", + "index-writer-test.ts", + "indexing-test.ts", + "loader-test.ts", + "module-syntax-test.ts", + "queue-test.ts", + "realm-server-test.ts", + "virtual-network-test.ts", + ] steps: - uses: actions/checkout@v4 - uses: ./.github/actions/init @@ -265,11 +281,13 @@ jobs: - name: realm server test suite run: pnpm test:wait-for-servers working-directory: packages/realm-server + env: + TEST_MODULE: ${{matrix.testModule}} - name: Upload realm server log uses: actions/upload-artifact@v4 if: always() with: - name: realm-server-log + name: realm-server-log-${{matrix.testModule}} path: /tmp/server.log retention-days: 30 diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index c5664c441a..fd3c2e0acc 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -71,11 +71,12 @@ }, "scripts": { "test": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"pg-adapter=warn,realm:requests=warn,current-run=error${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key qunit --require ts-node/register/transpile-only tests/index.ts", + "test-module": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"pg-adapter=warn,realm:requests=warn,current-run=error${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key qunit --require ts-node/register/transpile-only --module ${TEST_MODULE} tests/index.ts", "start:matrix": "cd ../matrix && pnpm assert-synapse-running", "start:smtp": "cd ../matrix && pnpm assert-smtp-running", "start:pg": "./scripts/start-pg.sh", "stop:pg": "./scripts/stop-pg.sh", - "test:wait-for-servers": "NODE_NO_WARNINGS=1 start-server-and-test 'pnpm run wait' 'http-get://localhost:4201/base/fields/boolean-field?acceptHeader=application%2Fvnd.card%2Bjson' 'pnpm run wait' 'http-get://localhost:4202/node-test/person-1?acceptHeader=application%2Fvnd.card%2Bjson|http://localhost:8008|http://localhost:5001' 'test'", + "test:wait-for-servers": "NODE_NO_WARNINGS=1 start-server-and-test 'pnpm run wait' 'http-get://localhost:4201/base/fields/boolean-field?acceptHeader=application%2Fvnd.card%2Bjson' 'pnpm run wait' 'http-get://localhost:4202/node-test/person-1?acceptHeader=application%2Fvnd.card%2Bjson|http://localhost:8008|http://localhost:5001' 'test-module'", "setup:base-in-deployment": "mkdir -p /persistent/base && rsync --dry-run --itemize-changes --size-only --recursive ../base/. /persistent/base/ && rsync --size-only --recursive ../base/. /persistent/base/", "setup:experiments-in-deployment": "mkdir -p /persistent/experiments && rsync --dry-run --itemize-changes --size-only --recursive ../experiments-realm/. /persistent/experiments/ && rsync --size-only --recursive ../experiments-realm/. /persistent/experiments/", "setup:seed-in-deployment": "mkdir -p /persistent/seed && rsync --dry-run --itemize-changes --size-only --recursive ../seed-realm/. /persistent/seed/ && rsync --size-only --recursive ../seed-realm/. /persistent/seed/", From 2e7c69bc43907b9b691193745127384ed8bf9b5b Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 7 Jan 2025 16:35:04 -0500 Subject: [PATCH 2/6] add lint for test files and test module wrapper --- packages/realm-server/package.json | 3 + .../realm-server/scripts/lint-test-shards.ts | 83 + .../realm-server/tests/auth-client-test.ts | 157 +- packages/realm-server/tests/billing-test.ts | 1720 ++-- .../tests/index-query-engine-test.ts | 882 +- .../realm-server/tests/index-writer-test.ts | 253 +- packages/realm-server/tests/indexing-test.ts | 1375 +-- packages/realm-server/tests/loader-test.ts | 290 +- .../realm-server/tests/module-syntax-test.ts | 1340 +-- packages/realm-server/tests/queue-test.ts | 288 +- .../realm-server/tests/realm-server-test.ts | 8698 +++++++++-------- .../tests/virtual-network-test.ts | 97 +- pnpm-lock.yaml | 10 + 13 files changed, 7730 insertions(+), 7466 deletions(-) create mode 100644 packages/realm-server/scripts/lint-test-shards.ts diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index 5da0c56913..5994be340c 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -14,6 +14,7 @@ "@types/eventsource": "^1.1.11", "@types/flat": "^5.0.5", "@types/fs-extra": "^9.0.13", + "@types/js-yaml": "^4.0.9", "@types/jsdom": "^21.1.1", "@types/jsonwebtoken": "^9.0.5", "@types/koa": "^2.13.5", @@ -41,6 +42,7 @@ "flat": "^5.0.2", "fs-extra": "^10.1.0", "http-server": "^14.1.1", + "js-yaml": "^4.1.0", "jsdom": "^21.1.1", "jsonwebtoken": "^9.0.2", "koa": "^2.14.1", @@ -96,6 +98,7 @@ "lint:js": "eslint . --cache", "lint:js:fix": "eslint . --fix", "lint:glint": "glint", + "lint:test-shards": "ts-node --transpileOnly scripts/lint-test-shards.ts", "full-reset": "./scripts/full-reset.sh", "sync-stripe-products": "NODE_NO_WARNINGS=1 PGDATABASE=boxel PGPORT=5435 ts-node --transpileOnly scripts/sync-stripe-products.ts", "stripe": "docker run --rm --add-host=host.docker.internal:host-gateway -it stripe/stripe-cli:latest" diff --git a/packages/realm-server/scripts/lint-test-shards.ts b/packages/realm-server/scripts/lint-test-shards.ts new file mode 100644 index 0000000000..aec21f1f1d --- /dev/null +++ b/packages/realm-server/scripts/lint-test-shards.ts @@ -0,0 +1,83 @@ +import { readFileSync, readdirSync } from 'fs-extra'; +import yaml from 'js-yaml'; +import { join, basename } from 'path'; + +const YAML_FILE = join( + __dirname, + '..', + '..', + '..', + '.github', + 'workflows', + 'ci.yaml', +); +const TEST_DIR = join(__dirname, '..', 'tests'); + +function getCiTestModules(yamlFilePath: string) { + try { + const yamlContent = readFileSync(yamlFilePath, 'utf8'); + const yamlData = yaml.load(yamlContent) as Record; + + const shardIndexes: string[] = + yamlData?.jobs?.['realm-server-test']?.strategy?.matrix?.testModule; + + if (!Array.isArray(shardIndexes)) { + throw new Error( + `Invalid 'jobs.realm-server-test.strategy.matrix.testModule' format in the YAML file.`, + ); + } + + return shardIndexes; + } catch (error: any) { + console.error(`Error reading shardIndex from YAML file: ${error.message}`); + process.exit(1); + } +} + +function getFilesystemTestModules(testDir: string) { + try { + const files = readdirSync(testDir); + return files + .filter((file) => file.endsWith('-test.ts')) + .map((file) => basename(file)); + } catch (error: any) { + console.error( + `Error reading test files from dir ${testDir}: ${error.message}`, + ); + process.exit(1); + } +} + +function validateTestFiles(yamlFilePath: string, testDir: string) { + const ciTestModules = getCiTestModules(yamlFilePath); + const filesystemTestModules = getFilesystemTestModules(testDir); + + let errorFound = false; + + for (let filename of filesystemTestModules) { + if (!ciTestModules.includes(filename)) { + console.error( + `Error: Test file '${filename}' exists in the filesystem but not in the ${yamlFilePath} file.`, + ); + errorFound = true; + } + } + for (let filename of ciTestModules) { + if (!filesystemTestModules.includes(filename)) { + console.error( + `Error: Test file '${filename}' exists in the YAML file but not in the ${yamlFilePath} filesystem.`, + ); + errorFound = true; + } + } + + if (errorFound) { + process.exit(1); + } else { + console.log( + `All test files are accounted for in the ${yamlFilePath} file for the realm-server matrix strategy.`, + ); + } +} + +validateTestFiles(YAML_FILE, TEST_DIR); diff --git a/packages/realm-server/tests/auth-client-test.ts b/packages/realm-server/tests/auth-client-test.ts index 7c8249bec9..730fe2a6a2 100644 --- a/packages/realm-server/tests/auth-client-test.ts +++ b/packages/realm-server/tests/auth-client-test.ts @@ -7,93 +7,104 @@ import { } from '@cardstack/runtime-common/realm-auth-client'; import { VirtualNetwork } from '@cardstack/runtime-common'; import jwt from 'jsonwebtoken'; +import { basename } from 'path'; function createJWT(expiresIn: string | number) { return jwt.sign({}, 'secret', { expiresIn }); } -module('realm-auth-client', function (assert) { - let client: RealmAuthClient; +module(basename(__filename), function () { + module('realm-auth-client', function (assert) { + let client: RealmAuthClient; - assert.beforeEach(function () { - let mockMatrixClient = { - isLoggedIn() { - return true; - }, - getUserId() { - return 'userId'; - }, - async getJoinedRooms() { - return Promise.resolve({ joined_rooms: [] }); - }, - async joinRoom() { - return Promise.resolve(); - }, - async sendEvent() { - return Promise.resolve(); - }, - async hashMessageWithSecret(_message: string): Promise { - throw new Error('Method not implemented.'); - }, - } as RealmAuthMatrixClientInterface; + assert.beforeEach(function () { + let mockMatrixClient = { + isLoggedIn() { + return true; + }, + getUserId() { + return 'userId'; + }, + async getJoinedRooms() { + return Promise.resolve({ joined_rooms: [] }); + }, + async joinRoom() { + return Promise.resolve(); + }, + async sendEvent() { + return Promise.resolve(); + }, + async hashMessageWithSecret(_message: string): Promise { + throw new Error('Method not implemented.'); + }, + } as RealmAuthMatrixClientInterface; - let virtualNetwork = new VirtualNetwork(); + let virtualNetwork = new VirtualNetwork(); - client = new RealmAuthClient( - new URL('http://testrealm.com/'), - mockMatrixClient, - virtualNetwork.fetch, - ) as any; + client = new RealmAuthClient( + new URL('http://testrealm.com/'), + mockMatrixClient, + virtualNetwork.fetch, + ) as any; - // [] notation is a hack to make TS happy so we can set private properties with mocks - client['initiateSessionRequest'] = async function (): Promise { - return { - status: 401, - json() { - return Promise.resolve({ - room: 'room', - challenge: 'challenge', - }); - }, - } as Response; - }; - client['challengeRequest'] = async function (): Promise { - return { - ok: true, - headers: { - get() { - return createJWT('1h'); + // [] notation is a hack to make TS happy so we can set private properties with mocks + client['initiateSessionRequest'] = async function (): Promise { + return { + status: 401, + json() { + return Promise.resolve({ + room: 'room', + challenge: 'challenge', + }); }, - }, - } as unknown as Response; - }; - }); + } as Response; + }; + client['challengeRequest'] = async function (): Promise { + return { + ok: true, + headers: { + get() { + return createJWT('1h'); + }, + }, + } as unknown as Response; + }; + }); - test('it authenticates and caches the jwt until it expires', async function (assert) { - let jwtFromClient = await client.getJWT(); + test('it authenticates and caches the jwt until it expires', async function (assert) { + let jwtFromClient = await client.getJWT(); - assert.strictEqual( - jwtFromClient.split('.').length, - 3, - 'jwtFromClient looks like a jwt', - ); + assert.strictEqual( + jwtFromClient.split('.').length, + 3, + 'jwtFromClient looks like a jwt', + ); - assert.strictEqual( - jwtFromClient, - await client.getJWT(), - 'jwt is the same which means it is cached until it is about to expire', - ); - }); + assert.strictEqual( + jwtFromClient, + await client.getJWT(), + 'jwt is the same which means it is cached until it is about to expire', + ); + }); - test('it refreshes the jwt if it is about to expire in the client', async function (assert) { - let jwtFromClient = createJWT('10s'); // Expires very soon, so the client will first refresh it - client['_jwt'] = jwtFromClient; - assert.notEqual(jwtFromClient, await client.getJWT(), 'jwt got refreshed'); - }); + test('it refreshes the jwt if it is about to expire in the client', async function (assert) { + let jwtFromClient = createJWT('10s'); // Expires very soon, so the client will first refresh it + client['_jwt'] = jwtFromClient; + assert.notEqual( + jwtFromClient, + await client.getJWT(), + 'jwt got refreshed', + ); + }); - test('it refreshes the jwt if it expired in the client', async function (assert) { - let jwtFromClient = createJWT(-1); // Expired 1 second ago - client['_jwt'] = jwtFromClient; - assert.notEqual(jwtFromClient, await client.getJWT(), 'jwt got refreshed'); + test('it refreshes the jwt if it expired in the client', async function (assert) { + let jwtFromClient = createJWT(-1); // Expired 1 second ago + client['_jwt'] = jwtFromClient; + assert.notEqual( + jwtFromClient, + await client.getJWT(), + 'jwt got refreshed', + ); + }); }); }); diff --git a/packages/realm-server/tests/billing-test.ts b/packages/realm-server/tests/billing-test.ts index d5cb1da83a..3cb6c809b7 100644 --- a/packages/realm-server/tests/billing-test.ts +++ b/packages/realm-server/tests/billing-test.ts @@ -34,6 +34,8 @@ import { StripeCheckoutSessionCompletedWebhookEvent, } from '@cardstack/billing/stripe-webhook-handlers'; +import { basename } from 'path'; + async function fetchStripeEvents(dbAdapter: PgAdapter) { return await query(dbAdapter, [`SELECT * FROM stripe_events`]); } @@ -86,507 +88,670 @@ async function fetchCreditsLedgerByUser( ); } -module('billing utils', function () { - test('encoding client_reference_id to be web safe in payment links', function (assert) { - assert.strictEqual( - decodeWebSafeBase64(encodeWebSafeBase64('@mike_1:cardstack.com')), - '@mike_1:cardstack.com', - ); - assert.strictEqual( - decodeWebSafeBase64(encodeWebSafeBase64('@hans.müller:matrix.de')), - '@hans.müller:matrix.de', - ); +module(basename(__filename), function () { + module('billing utils', function () { + test('encoding client_reference_id to be web safe in payment links', function (assert) { + assert.strictEqual( + decodeWebSafeBase64(encodeWebSafeBase64('@mike_1:cardstack.com')), + '@mike_1:cardstack.com', + ); + assert.strictEqual( + decodeWebSafeBase64(encodeWebSafeBase64('@hans.müller:matrix.de')), + '@hans.müller:matrix.de', + ); + }); }); -}); -module('billing', function (hooks) { - let dbAdapter: PgAdapter; + module('billing', function (hooks) { + let dbAdapter: PgAdapter; - hooks.beforeEach(async function () { - prepareTestDB(); - dbAdapter = new PgAdapter({ autoMigrate: true }); - }); + hooks.beforeEach(async function () { + prepareTestDB(); + dbAdapter = new PgAdapter({ autoMigrate: true }); + }); - hooks.afterEach(async function () { - await dbAdapter.close(); - }); + hooks.afterEach(async function () { + await dbAdapter.close(); + }); - module('invoice payment succeeded', function () { - module('new subscription without any previous subscription', function () { - test('creates a new subscription and adds plan allowance in credits', async function (assert) { - let user = await insertUser( - dbAdapter, - 'user@test', - 'cus_123', - 'user@test.com', - ); - let plan = await insertPlan( - dbAdapter, - 'Free plan', - 0, - 100, - 'prod_free', - ); + module('invoice payment succeeded', function () { + module('new subscription without any previous subscription', function () { + test('creates a new subscription and adds plan allowance in credits', async function (assert) { + let user = await insertUser( + dbAdapter, + 'user@test', + 'cus_123', + 'user@test.com', + ); + let plan = await insertPlan( + dbAdapter, + 'Free plan', + 0, + 100, + 'prod_free', + ); - // Omitted version of a real stripe invoice.payment_succeeded event - let stripeInvoicePaymentSucceededEvent = { - id: 'evt_1234567890', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: 0, // free plan - billing_reason: 'subscription_create', - period_end: 1638465600, - period_start: 1635873600, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: 0, - price: { product: 'prod_free' }, - }, - ], + // Omitted version of a real stripe invoice.payment_succeeded event + let stripeInvoicePaymentSucceededEvent = { + id: 'evt_1234567890', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: 0, // free plan + billing_reason: 'subscription_create', + period_end: 1638465600, + period_start: 1635873600, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: 0, + price: { product: 'prod_free' }, + }, + ], + }, }, }, - }, - } as StripeInvoicePaymentSucceededWebhookEvent; + } as StripeInvoicePaymentSucceededWebhookEvent; - await handlePaymentSucceeded( - dbAdapter, - stripeInvoicePaymentSucceededEvent, - ); + await handlePaymentSucceeded( + dbAdapter, + stripeInvoicePaymentSucceededEvent, + ); - // Assert that the stripe event was inserted and processed - let stripeEvents = await fetchStripeEvents(dbAdapter); - assert.strictEqual(stripeEvents.length, 1); - assert.strictEqual( - stripeEvents[0].stripe_event_id, - stripeInvoicePaymentSucceededEvent.id, - ); - assert.true(stripeEvents[0].is_processed); + // Assert that the stripe event was inserted and processed + let stripeEvents = await fetchStripeEvents(dbAdapter); + assert.strictEqual(stripeEvents.length, 1); + assert.strictEqual( + stripeEvents[0].stripe_event_id, + stripeInvoicePaymentSucceededEvent.id, + ); + assert.true(stripeEvents[0].is_processed); - // Assert that the subscription was created - let subscriptions = await fetchSubscriptionsByUserId( - dbAdapter, - user.id, - ); - assert.strictEqual(subscriptions.length, 1); - let subscription = subscriptions[0]; + // Assert that the subscription was created + let subscriptions = await fetchSubscriptionsByUserId( + dbAdapter, + user.id, + ); + assert.strictEqual(subscriptions.length, 1); + let subscription = subscriptions[0]; - assert.strictEqual(subscription.userId, user.id); - assert.strictEqual(subscription.planId, plan.id); - assert.strictEqual(subscription.status, 'active'); - assert.strictEqual(subscription.stripeSubscriptionId, 'sub_1234567890'); + assert.strictEqual(subscription.userId, user.id); + assert.strictEqual(subscription.planId, plan.id); + assert.strictEqual(subscription.status, 'active'); + assert.strictEqual( + subscription.stripeSubscriptionId, + 'sub_1234567890', + ); - // Assert that the subscription cycle was created - let subscriptionCycles = await fetchSubscriptionCyclesBySubscriptionId( - dbAdapter, - subscription.id, - ); - assert.strictEqual(subscriptionCycles.length, 1); - let subscriptionCycle = subscriptionCycles[0]; + // Assert that the subscription cycle was created + let subscriptionCycles = + await fetchSubscriptionCyclesBySubscriptionId( + dbAdapter, + subscription.id, + ); + assert.strictEqual(subscriptionCycles.length, 1); + let subscriptionCycle = subscriptionCycles[0]; - assert.strictEqual(subscriptionCycle.subscriptionId, subscription.id); - assert.strictEqual( - subscriptionCycle.periodStart, - stripeInvoicePaymentSucceededEvent.data.object.period_start, - ); - assert.strictEqual( - subscriptionCycle.periodEnd, - stripeInvoicePaymentSucceededEvent.data.object.period_end, - ); + assert.strictEqual(subscriptionCycle.subscriptionId, subscription.id); + assert.strictEqual( + subscriptionCycle.periodStart, + stripeInvoicePaymentSucceededEvent.data.object.period_start, + ); + assert.strictEqual( + subscriptionCycle.periodEnd, + stripeInvoicePaymentSucceededEvent.data.object.period_end, + ); - // Assert that the credits were added to the user's balance - let creditsLedger = await fetchCreditsLedgerByUser(dbAdapter, user.id); - assert.strictEqual(creditsLedger.length, 1); - let creditLedgerEntry = creditsLedger[0]; - assert.strictEqual(creditLedgerEntry.userId, user.id); - assert.strictEqual( - creditLedgerEntry.creditAmount, - plan.creditsIncluded, - ); - assert.strictEqual(creditLedgerEntry.creditType, 'plan_allowance'); - assert.strictEqual( - creditLedgerEntry.subscriptionCycleId, - subscriptionCycle.id, - ); + // Assert that the credits were added to the user's balance + let creditsLedger = await fetchCreditsLedgerByUser( + dbAdapter, + user.id, + ); + assert.strictEqual(creditsLedger.length, 1); + let creditLedgerEntry = creditsLedger[0]; + assert.strictEqual(creditLedgerEntry.userId, user.id); + assert.strictEqual( + creditLedgerEntry.creditAmount, + plan.creditsIncluded, + ); + assert.strictEqual(creditLedgerEntry.creditType, 'plan_allowance'); + assert.strictEqual( + creditLedgerEntry.subscriptionCycleId, + subscriptionCycle.id, + ); - // Error if stripe event is attempted to be processed again when it's already been processed - await assert.rejects( - handlePaymentSucceeded(dbAdapter, stripeInvoicePaymentSucceededEvent), - 'error: duplicate key value violates unique constraint "stripe_events_pkey"', - ); + // Error if stripe event is attempted to be processed again when it's already been processed + await assert.rejects( + handlePaymentSucceeded( + dbAdapter, + stripeInvoicePaymentSucceededEvent, + ), + 'error: duplicate key value violates unique constraint "stripe_events_pkey"', + ); + }); }); - }); - module('subscription update', function () { - test('updates the subscription and prorates credits', async function (assert) { - let user = await insertUser( - dbAdapter, - 'user@test', - 'cus_123', - 'user@test.com', - ); - let freePlan = await insertPlan( - dbAdapter, - 'Free plan', - 0, - 1000, - 'prod_free', - ); - let creatorPlan = await insertPlan( - dbAdapter, - 'Creator', - 12, - 5000, - 'prod_creator', - ); - let powerUserPlan = await insertPlan( - dbAdapter, - 'Power User', - 49, - 25000, - 'prod_power_user', - ); + module('subscription update', function () { + test('updates the subscription and prorates credits', async function (assert) { + let user = await insertUser( + dbAdapter, + 'user@test', + 'cus_123', + 'user@test.com', + ); + let freePlan = await insertPlan( + dbAdapter, + 'Free plan', + 0, + 1000, + 'prod_free', + ); + let creatorPlan = await insertPlan( + dbAdapter, + 'Creator', + 12, + 5000, + 'prod_creator', + ); + let powerUserPlan = await insertPlan( + dbAdapter, + 'Power User', + 49, + 25000, + 'prod_power_user', + ); - let subscription = await insertSubscription(dbAdapter, { - user_id: user.id, - plan_id: freePlan.id, - started_at: 1, - status: 'active', - stripe_subscription_id: 'sub_1234567890', - }); + let subscription = await insertSubscription(dbAdapter, { + user_id: user.id, + plan_id: freePlan.id, + started_at: 1, + status: 'active', + stripe_subscription_id: 'sub_1234567890', + }); - let subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { - subscriptionId: subscription.id, - periodStart: 1, - periodEnd: 2, - }); + let subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { + subscriptionId: subscription.id, + periodStart: 1, + periodEnd: 2, + }); - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: 1000, - creditType: 'plan_allowance', - subscriptionCycleId: subscriptionCycle.id, - }); + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: 1000, + creditType: 'plan_allowance', + subscriptionCycleId: subscriptionCycle.id, + }); - // User spent 500 credits from his plan allowance, now he has 500 left - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: -500, - creditType: 'plan_allowance_used', - subscriptionCycleId: subscriptionCycle.id, - }); + // User spent 500 credits from his plan allowance, now he has 500 left + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: -500, + creditType: 'plan_allowance_used', + subscriptionCycleId: subscriptionCycle.id, + }); - let stripeInvoicePaymentSucceededEvent = { - id: 'evt_1234567890', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: creatorPlan.monthlyPrice * 100, - billing_reason: 'subscription_update', - period_start: 1, - period_end: 2, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: creatorPlan.monthlyPrice * 100, - price: { product: 'prod_creator' }, - period: { start: 1, end: 2 }, - }, - ], + let stripeInvoicePaymentSucceededEvent = { + id: 'evt_1234567890', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: creatorPlan.monthlyPrice * 100, + billing_reason: 'subscription_update', + period_start: 1, + period_end: 2, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: creatorPlan.monthlyPrice * 100, + price: { product: 'prod_creator' }, + period: { start: 1, end: 2 }, + }, + ], + }, }, }, - }, - } as StripeInvoicePaymentSucceededWebhookEvent; - - // User upgraded to the creator plan for $12 - await handlePaymentSucceeded( - dbAdapter, - stripeInvoicePaymentSucceededEvent, - ); - - // Assert that new subscription was created - let subscriptions = await fetchSubscriptionsByUserId( - dbAdapter, - user.id, - ); - assert.strictEqual(subscriptions.length, 2); + } as StripeInvoicePaymentSucceededWebhookEvent; - // Assert that old subscription was ended due to plan change - assert.strictEqual(subscriptions[0].status, 'ended_due_to_plan_change'); - assert.ok(subscriptions[0].endedAt); + // User upgraded to the creator plan for $12 + await handlePaymentSucceeded( + dbAdapter, + stripeInvoicePaymentSucceededEvent, + ); - // Assert that new subscription is active - assert.strictEqual(subscriptions[1].status, 'active'); + // Assert that new subscription was created + let subscriptions = await fetchSubscriptionsByUserId( + dbAdapter, + user.id, + ); + assert.strictEqual(subscriptions.length, 2); - // Assert that there is a new subscription cycle - let subscriptionCycles = await fetchSubscriptionCyclesBySubscriptionId( - dbAdapter, - subscriptions[1].id, - ); - assert.strictEqual(subscriptionCycles.length, 1); - assert.strictEqual( - subscriptionCycles[0].periodStart, - stripeInvoicePaymentSucceededEvent.data.object.period_start, - ); - assert.strictEqual( - subscriptionCycles[0].periodEnd, - stripeInvoicePaymentSucceededEvent.data.object.period_end, - ); + // Assert that old subscription was ended due to plan change + assert.strictEqual( + subscriptions[0].status, + 'ended_due_to_plan_change', + ); + assert.ok(subscriptions[0].endedAt); + + // Assert that new subscription is active + assert.strictEqual(subscriptions[1].status, 'active'); + + // Assert that there is a new subscription cycle + let subscriptionCycles = + await fetchSubscriptionCyclesBySubscriptionId( + dbAdapter, + subscriptions[1].id, + ); + assert.strictEqual(subscriptionCycles.length, 1); + assert.strictEqual( + subscriptionCycles[0].periodStart, + stripeInvoicePaymentSucceededEvent.data.object.period_start, + ); + assert.strictEqual( + subscriptionCycles[0].periodEnd, + stripeInvoicePaymentSucceededEvent.data.object.period_end, + ); - let creditsBalance = await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }); + let creditsBalance = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }); - subscriptionCycle = subscriptionCycles[0]; + subscriptionCycle = subscriptionCycles[0]; - // User received 5000 credits from the creator plan, but the 500 credits from the plan allowance they had left from the free plan were expired - assert.strictEqual(creditsBalance, 5000); + // User received 5000 credits from the creator plan, but the 500 credits from the plan allowance they had left from the free plan were expired + assert.strictEqual(creditsBalance, 5000); - // User spent 2000 credits from the plan allowance - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: -2000, - creditType: 'plan_allowance_used', - subscriptionCycleId: subscriptionCycle.id, - }); + // User spent 2000 credits from the plan allowance + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: -2000, + creditType: 'plan_allowance_used', + subscriptionCycleId: subscriptionCycle.id, + }); - // Assert that the user now has 3000 credits left - creditsBalance = await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }); - assert.strictEqual(creditsBalance, 3000); + // Assert that the user now has 3000 credits left + creditsBalance = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }); + assert.strictEqual(creditsBalance, 3000); - // Now, user upgrades to power user plan ($49 monthly) in the middle of the month: + // Now, user upgrades to power user plan ($49 monthly) in the middle of the month: - let amountCreditedForUnusedTimeOnPreviousPlan = 200; - let amountCreditedForRemainingTimeOnNewPlan = 3800; + let amountCreditedForUnusedTimeOnPreviousPlan = 200; + let amountCreditedForRemainingTimeOnNewPlan = 3800; - stripeInvoicePaymentSucceededEvent = { - id: 'evt_1234567891', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: 3400, // prorated amount for going from creator to power user plan - billing_reason: 'subscription_update', - period_start: 3, - period_end: 4, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: -amountCreditedForUnusedTimeOnPreviousPlan, - description: 'Unused time on Creator plan', - price: { product: 'prod_creator' }, - period: { start: 3, end: 4 }, - }, - { - amount: amountCreditedForRemainingTimeOnNewPlan, - description: 'Remaining time on Power User plan', - price: { product: 'prod_power_user' }, - period: { start: 4, end: 5 }, - }, - ], + stripeInvoicePaymentSucceededEvent = { + id: 'evt_1234567891', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: 3400, // prorated amount for going from creator to power user plan + billing_reason: 'subscription_update', + period_start: 3, + period_end: 4, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: -amountCreditedForUnusedTimeOnPreviousPlan, + description: 'Unused time on Creator plan', + price: { product: 'prod_creator' }, + period: { start: 3, end: 4 }, + }, + { + amount: amountCreditedForRemainingTimeOnNewPlan, + description: 'Remaining time on Power User plan', + price: { product: 'prod_power_user' }, + period: { start: 4, end: 5 }, + }, + ], + }, }, }, - }, - } as StripeInvoicePaymentSucceededWebhookEvent; + } as StripeInvoicePaymentSucceededWebhookEvent; - await handlePaymentSucceeded( - dbAdapter, - stripeInvoicePaymentSucceededEvent, - ); + await handlePaymentSucceeded( + dbAdapter, + stripeInvoicePaymentSucceededEvent, + ); - // Assert there are now three subscriptions and last one is active - subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 3); - assert.strictEqual(subscriptions[0].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[1].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[2].status, 'active'); + // Assert there are now three subscriptions and last one is active + subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); + assert.strictEqual(subscriptions.length, 3); + assert.strictEqual( + subscriptions[0].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual( + subscriptions[1].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual(subscriptions[2].status, 'active'); - // Assert that subscriptions have correct plan ids - assert.strictEqual(subscriptions[0].planId, freePlan.id); - assert.strictEqual(subscriptions[1].planId, creatorPlan.id); - assert.strictEqual(subscriptions[2].planId, powerUserPlan.id); + // Assert that subscriptions have correct plan ids + assert.strictEqual(subscriptions[0].planId, freePlan.id); + assert.strictEqual(subscriptions[1].planId, creatorPlan.id); + assert.strictEqual(subscriptions[2].planId, powerUserPlan.id); - // Assert that the new subscription has the correct period start and end - assert.strictEqual(subscriptions[2].startedAt, 4); - assert.strictEqual(subscriptions[2].endedAt, null); + // Assert that the new subscription has the correct period start and end + assert.strictEqual(subscriptions[2].startedAt, 4); + assert.strictEqual(subscriptions[2].endedAt, null); - subscriptionCycles = await fetchSubscriptionCyclesBySubscriptionId( - dbAdapter, - subscriptions[2].id, - ); + subscriptionCycles = await fetchSubscriptionCyclesBySubscriptionId( + dbAdapter, + subscriptions[2].id, + ); - // Assert that latest subscription cycle has the correct period start and end - assert.strictEqual(subscriptionCycles.length, 1); - assert.strictEqual(subscriptionCycles[0].periodStart, 4); - assert.strictEqual(subscriptionCycles[0].periodEnd, 5); + // Assert that latest subscription cycle has the correct period start and end + assert.strictEqual(subscriptionCycles.length, 1); + assert.strictEqual(subscriptionCycles[0].periodStart, 4); + assert.strictEqual(subscriptionCycles[0].periodEnd, 5); - let previousCreditsBalance = creditsBalance; + let previousCreditsBalance = creditsBalance; - creditsBalance = await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }); + creditsBalance = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }); - // Assert that the credits balance is the prorated amount for going from creator to power user plan - let creditsToExpireforUnusedTimeOnPreviousPlan = Math.round( - (amountCreditedForUnusedTimeOnPreviousPlan / - (creatorPlan.monthlyPrice * 100)) * - creatorPlan.creditsIncluded, - ); - let creditsToAddForRemainingTime = Math.round( - (amountCreditedForRemainingTimeOnNewPlan / - (powerUserPlan.monthlyPrice * 100)) * - powerUserPlan.creditsIncluded, - ); - assert.strictEqual( - creditsBalance, - previousCreditsBalance - - creditsToExpireforUnusedTimeOnPreviousPlan + - creditsToAddForRemainingTime, - ); + // Assert that the credits balance is the prorated amount for going from creator to power user plan + let creditsToExpireforUnusedTimeOnPreviousPlan = Math.round( + (amountCreditedForUnusedTimeOnPreviousPlan / + (creatorPlan.monthlyPrice * 100)) * + creatorPlan.creditsIncluded, + ); + let creditsToAddForRemainingTime = Math.round( + (amountCreditedForRemainingTimeOnNewPlan / + (powerUserPlan.monthlyPrice * 100)) * + powerUserPlan.creditsIncluded, + ); + assert.strictEqual( + creditsBalance, + previousCreditsBalance - + creditsToExpireforUnusedTimeOnPreviousPlan + + creditsToAddForRemainingTime, + ); - // Downgrade to creator plan - stripeInvoicePaymentSucceededEvent = { - id: 'evt_12345678901', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: creatorPlan.monthlyPrice * 100, - billing_reason: 'subscription_update', - period_start: 5, - period_end: 6, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: creatorPlan.monthlyPrice * 100, - price: { product: 'prod_creator' }, - period: { start: 5, end: 6 }, - }, - ], + // Downgrade to creator plan + stripeInvoicePaymentSucceededEvent = { + id: 'evt_12345678901', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: creatorPlan.monthlyPrice * 100, + billing_reason: 'subscription_update', + period_start: 5, + period_end: 6, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: creatorPlan.monthlyPrice * 100, + price: { product: 'prod_creator' }, + period: { start: 5, end: 6 }, + }, + ], + }, }, }, - }, - } as StripeInvoicePaymentSucceededWebhookEvent; + } as StripeInvoicePaymentSucceededWebhookEvent; - await handlePaymentSucceeded( - dbAdapter, - stripeInvoicePaymentSucceededEvent, - ); + await handlePaymentSucceeded( + dbAdapter, + stripeInvoicePaymentSucceededEvent, + ); - // Assert there are now four subscriptions and last one is active - subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 4); - assert.strictEqual(subscriptions[0].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[1].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[2].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[3].status, 'active'); - - // Assert that subscriptions have correct plan ids - assert.strictEqual(subscriptions[0].planId, freePlan.id); - assert.strictEqual(subscriptions[1].planId, creatorPlan.id); - assert.strictEqual(subscriptions[2].planId, powerUserPlan.id); - assert.strictEqual(subscriptions[3].planId, creatorPlan.id); - - // Assert that the new subscription has the correct period start and end - assert.strictEqual(subscriptions[3].startedAt, 5); - assert.strictEqual(subscriptions[3].endedAt, null); - - subscriptionCycles = await fetchSubscriptionCyclesBySubscriptionId( - dbAdapter, - subscriptions[3].id, - ); + // Assert there are now four subscriptions and last one is active + subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); + assert.strictEqual(subscriptions.length, 4); + assert.strictEqual( + subscriptions[0].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual( + subscriptions[1].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual( + subscriptions[2].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual(subscriptions[3].status, 'active'); - // Assert that latest subscription cycle has the correct period start and end - assert.strictEqual(subscriptionCycles.length, 1); - assert.strictEqual(subscriptionCycles[0].periodStart, 5); - assert.strictEqual(subscriptionCycles[0].periodEnd, 6); + // Assert that subscriptions have correct plan ids + assert.strictEqual(subscriptions[0].planId, freePlan.id); + assert.strictEqual(subscriptions[1].planId, creatorPlan.id); + assert.strictEqual(subscriptions[2].planId, powerUserPlan.id); + assert.strictEqual(subscriptions[3].planId, creatorPlan.id); - // Assert that user now has the plan's allowance (No proration will happen because Stripe assures us that downgrading to a cheaper plan will happen at the end of the billing period) - // (This is a setting in Stripe's customer portal) - creditsBalance = await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }); + // Assert that the new subscription has the correct period start and end + assert.strictEqual(subscriptions[3].startedAt, 5); + assert.strictEqual(subscriptions[3].endedAt, null); - assert.strictEqual(creditsBalance, creatorPlan.creditsIncluded); + subscriptionCycles = await fetchSubscriptionCyclesBySubscriptionId( + dbAdapter, + subscriptions[3].id, + ); - // Now user switches back to free plan - stripeInvoicePaymentSucceededEvent = { - id: 'evt_123456789011', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: 0, - billing_reason: 'subscription_update', - period_start: 1635873600, - period_end: 1638465600, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: 0, - price: { product: 'prod_free' }, - }, - ], + // Assert that latest subscription cycle has the correct period start and end + assert.strictEqual(subscriptionCycles.length, 1); + assert.strictEqual(subscriptionCycles[0].periodStart, 5); + assert.strictEqual(subscriptionCycles[0].periodEnd, 6); + + // Assert that user now has the plan's allowance (No proration will happen because Stripe assures us that downgrading to a cheaper plan will happen at the end of the billing period) + // (This is a setting in Stripe's customer portal) + creditsBalance = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }); + + assert.strictEqual(creditsBalance, creatorPlan.creditsIncluded); + + // Now user switches back to free plan + stripeInvoicePaymentSucceededEvent = { + id: 'evt_123456789011', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: 0, + billing_reason: 'subscription_update', + period_start: 1635873600, + period_end: 1638465600, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: 0, + price: { product: 'prod_free' }, + }, + ], + }, }, }, - }, - } as StripeInvoicePaymentSucceededWebhookEvent; + } as StripeInvoicePaymentSucceededWebhookEvent; - await handlePaymentSucceeded( - dbAdapter, - stripeInvoicePaymentSucceededEvent, - ); + await handlePaymentSucceeded( + dbAdapter, + stripeInvoicePaymentSucceededEvent, + ); - // Assert there are now 5 subscriptions and last one is active - subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 5); - assert.strictEqual(subscriptions[0].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[1].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[2].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[3].status, 'ended_due_to_plan_change'); - assert.strictEqual(subscriptions[4].status, 'active'); - - // Assert that subscriptions have correct plan ids - assert.strictEqual(subscriptions[0].planId, freePlan.id); - assert.strictEqual(subscriptions[1].planId, creatorPlan.id); - assert.strictEqual(subscriptions[2].planId, powerUserPlan.id); - assert.strictEqual(subscriptions[3].planId, creatorPlan.id); - assert.strictEqual(subscriptions[4].planId, freePlan.id); - - creditsBalance = await sumUpCreditsLedger(dbAdapter, { - userId: user.id, + // Assert there are now 5 subscriptions and last one is active + subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); + assert.strictEqual(subscriptions.length, 5); + assert.strictEqual( + subscriptions[0].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual( + subscriptions[1].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual( + subscriptions[2].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual( + subscriptions[3].status, + 'ended_due_to_plan_change', + ); + assert.strictEqual(subscriptions[4].status, 'active'); + + // Assert that subscriptions have correct plan ids + assert.strictEqual(subscriptions[0].planId, freePlan.id); + assert.strictEqual(subscriptions[1].planId, creatorPlan.id); + assert.strictEqual(subscriptions[2].planId, powerUserPlan.id); + assert.strictEqual(subscriptions[3].planId, creatorPlan.id); + assert.strictEqual(subscriptions[4].planId, freePlan.id); + + creditsBalance = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }); + assert.strictEqual(creditsBalance, freePlan.creditsIncluded); + }); + }); + + module('subscription cycle', function () { + test('renews the subscription', async function (assert) { + let user = await insertUser( + dbAdapter, + 'user@test', + 'cus_123', + 'user@test.com', + ); + let plan = await insertPlan( + dbAdapter, + 'Creator', + 12, + 2500, + 'prod_creator', + ); + let subscription = await insertSubscription(dbAdapter, { + user_id: user.id, + plan_id: plan.id, + started_at: 1, + status: 'active', + stripe_subscription_id: 'sub_1234567890', + }); + let subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { + subscriptionId: subscription.id, + periodStart: 1, + periodEnd: 2, + }); + + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: plan.creditsIncluded, + creditType: 'plan_allowance', + subscriptionCycleId: subscriptionCycle.id, + }); + + // User spent 2000 credits in this cycle (from his plan allowance, which is 2500 credits) + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: -1000, + creditType: 'plan_allowance_used', + subscriptionCycleId: subscriptionCycle.id, + }); + + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: -1000, + creditType: 'plan_allowance_used', + subscriptionCycleId: subscriptionCycle.id, + }); + + // User added 100 additional credits in this cycle (even though user has some plan allowance left but for the sake of a more thorough test we want to simulate a purchase of extra credits) + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: 100, + creditType: 'extra_credit', + subscriptionCycleId: subscriptionCycle.id, + }); + + // Next cycle + let stripeInvoicePaymentSucceededEvent = { + id: 'evt_1234567890', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: 12, + billing_reason: 'subscription_cycle', + period_start: 2, + period_end: 3, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: 1200, + price: { product: 'prod_creator' }, + }, + ], + }, + }, + }, + } as StripeInvoicePaymentSucceededWebhookEvent; + + let availableCredits = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }); + assert.strictEqual( + availableCredits, + plan.creditsIncluded - 2000 + 100, + ); + + await handlePaymentSucceeded( + dbAdapter, + stripeInvoicePaymentSucceededEvent, + ); + + // Assert that there are now two subscription cycles + let subscriptionCycles = + await fetchSubscriptionCyclesBySubscriptionId( + dbAdapter, + subscription.id, + ); + assert.strictEqual(subscriptionCycles.length, 2); + + // Assert both subscription cycles have the correct period start and end + assert.strictEqual(subscriptionCycles[0].periodStart, 1); + assert.strictEqual(subscriptionCycles[0].periodEnd, 2); + assert.strictEqual(subscriptionCycles[1].periodStart, 2); + assert.strictEqual(subscriptionCycles[1].periodEnd, 3); + + // Assert that the ledger has the correct sum of credits going in and out + availableCredits = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }); + assert.strictEqual(availableCredits, plan.creditsIncluded + 100); // Remaining credits from the previous cycle expired, new credits added, plus 100 from the extra credit }); - assert.strictEqual(creditsBalance, freePlan.creditsIncluded); }); }); - module('subscription cycle', function () { - test('renews the subscription', async function (assert) { + module('subscription deleted', function () { + test('handles subscription cancellation', async function (assert) { let user = await insertUser( dbAdapter, 'user@test', @@ -600,6 +765,7 @@ module('billing', function (hooks) { 2500, 'prod_creator', ); + let subscription = await insertSubscription(dbAdapter, { user_id: user.id, plan_id: plan.id, @@ -607,461 +773,347 @@ module('billing', function (hooks) { status: 'active', stripe_subscription_id: 'sub_1234567890', }); - let subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { + + await insertSubscriptionCycle(dbAdapter, { subscriptionId: subscription.id, periodStart: 1, periodEnd: 2, }); - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: plan.creditsIncluded, - creditType: 'plan_allowance', - subscriptionCycleId: subscriptionCycle.id, - }); - - // User spent 2000 credits in this cycle (from his plan allowance, which is 2500 credits) - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: -1000, - creditType: 'plan_allowance_used', - subscriptionCycleId: subscriptionCycle.id, - }); - - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: -1000, - creditType: 'plan_allowance_used', - subscriptionCycleId: subscriptionCycle.id, - }); - - // User added 100 additional credits in this cycle (even though user has some plan allowance left but for the sake of a more thorough test we want to simulate a purchase of extra credits) - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: 100, - creditType: 'extra_credit', - subscriptionCycleId: subscriptionCycle.id, - }); - - // Next cycle - let stripeInvoicePaymentSucceededEvent = { - id: 'evt_1234567890', + let stripeSubscriptionDeletedEvent = { + id: 'evt_sub_deleted_1', object: 'event', - type: 'invoice.payment_succeeded', + type: 'customer.subscription.deleted', data: { object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: 12, - billing_reason: 'subscription_cycle', - period_start: 2, - period_end: 3, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: 1200, - price: { product: 'prod_creator' }, - }, - ], + id: 'sub_1234567890', + canceled_at: 2, + cancellation_details: { + reason: 'cancellation_requested', }, }, }, - } as StripeInvoicePaymentSucceededWebhookEvent; - - let availableCredits = await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }); - assert.strictEqual(availableCredits, plan.creditsIncluded - 2000 + 100); + } as StripeSubscriptionDeletedWebhookEvent; - await handlePaymentSucceeded( + await handleSubscriptionDeleted( dbAdapter, - stripeInvoicePaymentSucceededEvent, + stripeSubscriptionDeletedEvent, ); - // Assert that there are now two subscription cycles - let subscriptionCycles = await fetchSubscriptionCyclesBySubscriptionId( + let subscriptions = await fetchSubscriptionsByUserId( dbAdapter, - subscription.id, + user.id, ); - assert.strictEqual(subscriptionCycles.length, 2); - - // Assert both subscription cycles have the correct period start and end - assert.strictEqual(subscriptionCycles[0].periodStart, 1); - assert.strictEqual(subscriptionCycles[0].periodEnd, 2); - assert.strictEqual(subscriptionCycles[1].periodStart, 2); - assert.strictEqual(subscriptionCycles[1].periodEnd, 3); + assert.strictEqual(subscriptions.length, 1); + assert.strictEqual(subscriptions[0].status, 'canceled'); + assert.strictEqual(subscriptions[0].endedAt, 2); - // Assert that the ledger has the correct sum of credits going in and out - availableCredits = await sumUpCreditsLedger(dbAdapter, { + let availableCredits = await sumUpCreditsLedger(dbAdapter, { userId: user.id, }); - assert.strictEqual(availableCredits, plan.creditsIncluded + 100); // Remaining credits from the previous cycle expired, new credits added, plus 100 from the extra credit + assert.strictEqual(availableCredits, 0); }); }); - }); - module('subscription deleted', function () { - test('handles subscription cancellation', async function (assert) { - let user = await insertUser( - dbAdapter, - 'user@test', - 'cus_123', - 'user@test.com', - ); - let plan = await insertPlan( - dbAdapter, - 'Creator', - 12, - 2500, - 'prod_creator', + module('checkout session completed', function () { + let user: User; + let matrixUserId = '@pepe:cardstack.com'; + + module( + 'without entry in users table before webhook arrival (legacy users registered prior to users table introduction)', + function () { + test('updates user stripe customer id on checkout session completed', async function (assert) { + let stripeCheckoutSessionCompletedEvent = { + id: 'evt_1234567890', + object: 'event', + data: { + object: { + id: 'cs_test_1234567890', + object: 'checkout.session', + client_reference_id: encodeWebSafeBase64(matrixUserId), + customer: 'cus_123', + metadata: {}, + }, + }, + type: 'checkout.session.completed', + } as StripeCheckoutSessionCompletedWebhookEvent; + + await handleCheckoutSessionCompleted( + dbAdapter, + stripeCheckoutSessionCompletedEvent, + ); + + let stripeEvents = await fetchStripeEvents(dbAdapter); + assert.strictEqual(stripeEvents.length, 1); + assert.strictEqual( + stripeEvents[0].stripe_event_id, + stripeCheckoutSessionCompletedEvent.id, + ); + + const updatedUser = await fetchUserByStripeCustomerId( + dbAdapter, + 'cus_123', + ); + assert.strictEqual(updatedUser.length, 1); + assert.strictEqual(updatedUser[0].stripe_customer_id, 'cus_123'); + assert.strictEqual(updatedUser[0].matrix_user_id, matrixUserId); + }); + }, ); - let subscription = await insertSubscription(dbAdapter, { - user_id: user.id, - plan_id: plan.id, - started_at: 1, - status: 'active', - stripe_subscription_id: 'sub_1234567890', - }); + module( + 'with entry in users table before webhook arrival', + function (hooks) { + hooks.beforeEach(async function () { + user = await insertUser( + dbAdapter, + matrixUserId, + 'cus_123', + 'user@test.com', + ); + }); - await insertSubscriptionCycle(dbAdapter, { - subscriptionId: subscription.id, - periodStart: 1, - periodEnd: 2, - }); + test('updates user stripe customer id on checkout session completed', async function (assert) { + let stripeCheckoutSessionCompletedEvent = { + id: 'evt_1234567890', + object: 'event', + data: { + object: { + id: 'cs_test_1234567890', + object: 'checkout.session', + client_reference_id: encodeWebSafeBase64(matrixUserId), + customer: 'cus_123', + metadata: {}, + }, + }, + type: 'checkout.session.completed', + } as StripeCheckoutSessionCompletedWebhookEvent; + + await handleCheckoutSessionCompleted( + dbAdapter, + stripeCheckoutSessionCompletedEvent, + ); + + let stripeEvents = await fetchStripeEvents(dbAdapter); + assert.strictEqual(stripeEvents.length, 1); + assert.strictEqual( + stripeEvents[0].stripe_event_id, + stripeCheckoutSessionCompletedEvent.id, + ); + + const updatedUser = await fetchUserByStripeCustomerId( + dbAdapter, + 'cus_123', + ); + assert.strictEqual(updatedUser.length, 1); + assert.strictEqual(updatedUser[0].stripe_customer_id, 'cus_123'); + assert.strictEqual(updatedUser[0].matrix_user_id, matrixUserId); + }); - let stripeSubscriptionDeletedEvent = { - id: 'evt_sub_deleted_1', - object: 'event', - type: 'customer.subscription.deleted', - data: { - object: { - id: 'sub_1234567890', - canceled_at: 2, - cancellation_details: { - reason: 'cancellation_requested', - }, - }, + test('add extra credits to user ledger when checkout session completed', async function (assert) { + let creatorPlan = await insertPlan( + dbAdapter, + 'Creator', + 12, + 2500, + 'prod_creator', + ); + let subscription = await insertSubscription(dbAdapter, { + user_id: user.id, + plan_id: creatorPlan.id, + started_at: 1, + status: 'active', + stripe_subscription_id: 'sub_1234567890', + }); + await insertSubscriptionCycle(dbAdapter, { + subscriptionId: subscription.id, + periodStart: 1, + periodEnd: 2, + }); + let stripeCheckoutSessionCompletedEvent = { + id: 'evt_1234567890', + object: 'event', + data: { + object: { + id: 'cs_test_1234567890', + object: 'checkout.session', + customer: null, + client_reference_id: encodeWebSafeBase64(matrixUserId), + metadata: { + credit_reload_amount: '25000', + }, + }, + }, + type: 'checkout.session.completed', + } as StripeCheckoutSessionCompletedWebhookEvent; + + await handleCheckoutSessionCompleted( + dbAdapter, + stripeCheckoutSessionCompletedEvent, + ); + + let availableExtraCredits = await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + creditType: 'extra_credit', + }); + assert.strictEqual(availableExtraCredits, 25000); + }); }, - } as StripeSubscriptionDeletedWebhookEvent; - - await handleSubscriptionDeleted( - dbAdapter, - stripeSubscriptionDeletedEvent, ); - - let subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 1); - assert.strictEqual(subscriptions[0].status, 'canceled'); - assert.strictEqual(subscriptions[0].endedAt, 2); - - let availableCredits = await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }); - assert.strictEqual(availableCredits, 0); }); - }); - - module('checkout session completed', function () { - let user: User; - let matrixUserId = '@pepe:cardstack.com'; - - module( - 'without entry in users table before webhook arrival (legacy users registered prior to users table introduction)', - function () { - test('updates user stripe customer id on checkout session completed', async function (assert) { - let stripeCheckoutSessionCompletedEvent = { - id: 'evt_1234567890', - object: 'event', - data: { - object: { - id: 'cs_test_1234567890', - object: 'checkout.session', - client_reference_id: encodeWebSafeBase64(matrixUserId), - customer: 'cus_123', - metadata: {}, - }, - }, - type: 'checkout.session.completed', - } as StripeCheckoutSessionCompletedWebhookEvent; - await handleCheckoutSessionCompleted( - dbAdapter, - stripeCheckoutSessionCompletedEvent, - ); + module('AI usage tracking', function (hooks) { + let user: User; + let creatorPlan: Plan; + let subscription: Subscription; + let subscriptionCycle: SubscriptionCycle; - let stripeEvents = await fetchStripeEvents(dbAdapter); - assert.strictEqual(stripeEvents.length, 1); - assert.strictEqual( - stripeEvents[0].stripe_event_id, - stripeCheckoutSessionCompletedEvent.id, - ); - - const updatedUser = await fetchUserByStripeCustomerId( - dbAdapter, - 'cus_123', - ); - assert.strictEqual(updatedUser.length, 1); - assert.strictEqual(updatedUser[0].stripe_customer_id, 'cus_123'); - assert.strictEqual(updatedUser[0].matrix_user_id, matrixUserId); + hooks.beforeEach(async function () { + user = await insertUser( + dbAdapter, + 'testuser', + 'cus_123', + 'user@test.com', + ); + creatorPlan = await insertPlan( + dbAdapter, + 'Creator', + 12, + 2500, + 'prod_creator', + ); + subscription = await insertSubscription(dbAdapter, { + user_id: user.id, + plan_id: creatorPlan.id, + started_at: 1, + status: 'active', + stripe_subscription_id: 'sub_1234567890', }); - }, - ); - - module( - 'with entry in users table before webhook arrival', - function (hooks) { - hooks.beforeEach(async function () { - user = await insertUser( - dbAdapter, - matrixUserId, - 'cus_123', - 'user@test.com', - ); + subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { + subscriptionId: subscription.id, + periodStart: 1, + periodEnd: 2, }); + }); - test('updates user stripe customer id on checkout session completed', async function (assert) { - let stripeCheckoutSessionCompletedEvent = { - id: 'evt_1234567890', - object: 'event', - data: { - object: { - id: 'cs_test_1234567890', - object: 'checkout.session', - client_reference_id: encodeWebSafeBase64(matrixUserId), - customer: 'cus_123', - metadata: {}, - }, - }, - type: 'checkout.session.completed', - } as StripeCheckoutSessionCompletedWebhookEvent; - - await handleCheckoutSessionCompleted( - dbAdapter, - stripeCheckoutSessionCompletedEvent, - ); - - let stripeEvents = await fetchStripeEvents(dbAdapter); - assert.strictEqual(stripeEvents.length, 1); - assert.strictEqual( - stripeEvents[0].stripe_event_id, - stripeCheckoutSessionCompletedEvent.id, - ); + test('spends ai credits correctly when no extra credits are available', async function (assert) { + // User receives 2500 credits for the creator plan and spends 2490 credits + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: creatorPlan.creditsIncluded, + creditType: 'plan_allowance', + subscriptionCycleId: subscriptionCycle.id, + }); - const updatedUser = await fetchUserByStripeCustomerId( - dbAdapter, - 'cus_123', - ); - assert.strictEqual(updatedUser.length, 1); - assert.strictEqual(updatedUser[0].stripe_customer_id, 'cus_123'); - assert.strictEqual(updatedUser[0].matrix_user_id, matrixUserId); + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: -2490, + creditType: 'plan_allowance_used', + subscriptionCycleId: subscriptionCycle.id, }); - test('add extra credits to user ledger when checkout session completed', async function (assert) { - let creatorPlan = await insertPlan( - dbAdapter, - 'Creator', - 12, - 2500, - 'prod_creator', - ); - let subscription = await insertSubscription(dbAdapter, { - user_id: user.id, - plan_id: creatorPlan.id, - started_at: 1, - status: 'active', - stripe_subscription_id: 'sub_1234567890', - }); - await insertSubscriptionCycle(dbAdapter, { - subscriptionId: subscription.id, - periodStart: 1, - periodEnd: 2, - }); - let stripeCheckoutSessionCompletedEvent = { - id: 'evt_1234567890', - object: 'event', - data: { - object: { - id: 'cs_test_1234567890', - object: 'checkout.session', - customer: null, - client_reference_id: encodeWebSafeBase64(matrixUserId), - metadata: { - credit_reload_amount: '25000', - }, - }, - }, - type: 'checkout.session.completed', - } as StripeCheckoutSessionCompletedWebhookEvent; + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }), + 10, + ); - await handleCheckoutSessionCompleted( - dbAdapter, - stripeCheckoutSessionCompletedEvent, - ); + await spendCredits(dbAdapter, user.id, 2); - let availableExtraCredits = await sumUpCreditsLedger(dbAdapter, { + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { userId: user.id, - creditType: 'extra_credit', - }); - assert.strictEqual(availableExtraCredits, 25000); - }); - }, - ); - }); + }), + 8, + ); - module('AI usage tracking', function (hooks) { - let user: User; - let creatorPlan: Plan; - let subscription: Subscription; - let subscriptionCycle: SubscriptionCycle; + await spendCredits(dbAdapter, user.id, 5); - hooks.beforeEach(async function () { - user = await insertUser( - dbAdapter, - 'testuser', - 'cus_123', - 'user@test.com', - ); - creatorPlan = await insertPlan( - dbAdapter, - 'Creator', - 12, - 2500, - 'prod_creator', - ); - subscription = await insertSubscription(dbAdapter, { - user_id: user.id, - plan_id: creatorPlan.id, - started_at: 1, - status: 'active', - stripe_subscription_id: 'sub_1234567890', - }); - subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { - subscriptionId: subscription.id, - periodStart: 1, - periodEnd: 2, - }); - }); - - test('spends ai credits correctly when no extra credits are available', async function (assert) { - // User receives 2500 credits for the creator plan and spends 2490 credits - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: creatorPlan.creditsIncluded, - creditType: 'plan_allowance', - subscriptionCycleId: subscriptionCycle.id, - }); + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }), + 3, + ); - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: -2490, - creditType: 'plan_allowance_used', - subscriptionCycleId: subscriptionCycle.id, + // Make sure that we can't spend more credits than the user has - in this case user has 3 credits left and we try to spend 5 + await spendCredits(dbAdapter, user.id, 5); + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }), + 0, + ); }); - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { + test('spends ai credits correctly when extra credits are available', async function (assert) { + // User receives 2500 credits for the creator plan and spends 2490 credits + await addToCreditsLedger(dbAdapter, { userId: user.id, - }), - 10, - ); - - await spendCredits(dbAdapter, user.id, 2); + creditAmount: creatorPlan.creditsIncluded, + creditType: 'plan_allowance', + subscriptionCycleId: subscriptionCycle.id, + }); - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { + await addToCreditsLedger(dbAdapter, { userId: user.id, - }), - 8, - ); + creditAmount: -2490, + creditType: 'plan_allowance_used', + subscriptionCycleId: subscriptionCycle.id, + }); - await spendCredits(dbAdapter, user.id, 5); + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }), + 10, + ); - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { + // Add 5 extra credits + await addToCreditsLedger(dbAdapter, { userId: user.id, - }), - 3, - ); + creditAmount: 5, + creditType: 'extra_credit', + subscriptionCycleId: null, + }); - // Make sure that we can't spend more credits than the user has - in this case user has 3 credits left and we try to spend 5 - await spendCredits(dbAdapter, user.id, 5); - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }), - 0, - ); - }); + // User has 15 credits in total: 10 credits from the plan allowance and 5 extra credits + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }), + 15, + ); - test('spends ai credits correctly when extra credits are available', async function (assert) { - // User receives 2500 credits for the creator plan and spends 2490 credits - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: creatorPlan.creditsIncluded, - creditType: 'plan_allowance', - subscriptionCycleId: subscriptionCycle.id, - }); + // This should spend 10 credits from the plan allowance and 2 from the extra credits + await spendCredits(dbAdapter, user.id, 12); - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: -2490, - creditType: 'plan_allowance_used', - subscriptionCycleId: subscriptionCycle.id, - }); + // Plan allowance is now 0, 3 credits left from the extra credits + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + }), + 3, + ); - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }), - 10, - ); + // Make sure the available credits come from the extra credits and not the plan allowance + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + creditType: ['plan_allowance', 'plan_allowance_used'], + }), + 0, + ); - // Add 5 extra credits - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: 5, - creditType: 'extra_credit', - subscriptionCycleId: null, + assert.strictEqual( + await sumUpCreditsLedger(dbAdapter, { + userId: user.id, + creditType: ['extra_credit', 'extra_credit_used'], + }), + 3, + ); }); - - // User has 15 credits in total: 10 credits from the plan allowance and 5 extra credits - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }), - 15, - ); - - // This should spend 10 credits from the plan allowance and 2 from the extra credits - await spendCredits(dbAdapter, user.id, 12); - - // Plan allowance is now 0, 3 credits left from the extra credits - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - }), - 3, - ); - - // Make sure the available credits come from the extra credits and not the plan allowance - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - creditType: ['plan_allowance', 'plan_allowance_used'], - }), - 0, - ); - - assert.strictEqual( - await sumUpCreditsLedger(dbAdapter, { - userId: user.id, - creditType: ['extra_credit', 'extra_credit_used'], - }), - 3, - ); }); }); }); diff --git a/packages/realm-server/tests/index-query-engine-test.ts b/packages/realm-server/tests/index-query-engine-test.ts index 4f15632200..80a4ce2e2d 100644 --- a/packages/realm-server/tests/index-query-engine-test.ts +++ b/packages/realm-server/tests/index-query-engine-test.ts @@ -15,6 +15,8 @@ import { shimExternals } from '../lib/externals'; import { type CardDef } from 'https://cardstack.com/base/card-api'; import indexQueryEngineTests from '@cardstack/runtime-common/tests/index-query-engine-test'; +import { basename } from 'path'; + let cardApi: typeof import('https://cardstack.com/base/card-api'); async function makeTestCards(loader: Loader) { @@ -99,445 +101,447 @@ async function makeTestCards(loader: Loader) { return testCards; } -module('query', function (hooks) { - let dbAdapter: PgAdapter; - let indexQueryEngine: IndexQueryEngine; - let loader: Loader; - - hooks.beforeEach(async function () { - prepareTestDB(); - let virtualNetwork = new VirtualNetwork(); - virtualNetwork.addURLMapping( - new URL(baseRealm.url), - new URL('http://localhost:4201/base/'), - ); - shimExternals(virtualNetwork); - let fetch = fetcher(virtualNetwork.fetch, [ - async (req, next) => { - return (await maybeHandleScopedCSSRequest(req)) || next(req); - }, - ]); - loader = new Loader(fetch, virtualNetwork.resolveImport); - - dbAdapter = new PgAdapter({ autoMigrate: true }); - indexQueryEngine = new IndexQueryEngine(dbAdapter); - }); - - hooks.afterEach(async function () { - await dbAdapter.close(); - }); - - test('can get all cards with empty filter', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('deleted cards are not included in results', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('error docs are not included in results', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can filter by type', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'eq'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'eq' thru nested fields`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use 'eq' to match multiple fields`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use 'eq' to find 'null' values`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use 'eq' to match against number type`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use 'eq' to match against boolean type`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can filter eq from a code ref query value', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can filter eq from a date query value', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can search with a 'not' filter`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can handle a filter with double negatives', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use a 'contains' filter`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`contains filter is case insensitive`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use 'contains' to match multiple fields`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use a 'contains' filter to match 'null'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use 'every' to combine multiple filters`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can use 'any' to combine multiple filters`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`gives a good error when query refers to missing card`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`gives a good error when query refers to missing field`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`it can filter on a plural primitive field using 'eq'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`it can filter on a nested field within a plural composite field using 'eq'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('it can match a null in a plural field', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('it can match a leaf plural field nested in a plural composite field', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('it can match thru a plural nested composite field that is field of a singular composite field', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can return a single result for a card when there are multiple matches within a result's search doc`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can perform query against WIP version of the index', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can perform query against "production" version of the index', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can sort search results', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can sort descending', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('nulls are sorted to the end of search results', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can get paginated results that are stable during index mutations', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'gt'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'gte'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'gt' thru nested fields`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'gt' thru a plural primitive field`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'gt' thru a plural composite field`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'lt'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can filter using 'lte'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`can combine 'range' filter`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test(`cannot filter 'null' value using 'range'`, async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can get prerendered cards from the indexer', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can get prerendered cards in an error state from the indexer', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), - }); - }); - - test('can sort using a general field that is not an attribute of a card', async function (assert) { - await runSharedTest(indexQueryEngineTests, assert, { - indexQueryEngine, - dbAdapter, - loader, - testCards: await makeTestCards(loader), +module(basename(__filename), function () { + module('query', function (hooks) { + let dbAdapter: PgAdapter; + let indexQueryEngine: IndexQueryEngine; + let loader: Loader; + + hooks.beforeEach(async function () { + prepareTestDB(); + let virtualNetwork = new VirtualNetwork(); + virtualNetwork.addURLMapping( + new URL(baseRealm.url), + new URL('http://localhost:4201/base/'), + ); + shimExternals(virtualNetwork); + let fetch = fetcher(virtualNetwork.fetch, [ + async (req, next) => { + return (await maybeHandleScopedCSSRequest(req)) || next(req); + }, + ]); + loader = new Loader(fetch, virtualNetwork.resolveImport); + + dbAdapter = new PgAdapter({ autoMigrate: true }); + indexQueryEngine = new IndexQueryEngine(dbAdapter); + }); + + hooks.afterEach(async function () { + await dbAdapter.close(); + }); + + test('can get all cards with empty filter', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('deleted cards are not included in results', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('error docs are not included in results', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can filter by type', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'eq'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'eq' thru nested fields`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use 'eq' to match multiple fields`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use 'eq' to find 'null' values`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use 'eq' to match against number type`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use 'eq' to match against boolean type`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can filter eq from a code ref query value', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can filter eq from a date query value', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can search with a 'not' filter`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can handle a filter with double negatives', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use a 'contains' filter`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`contains filter is case insensitive`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use 'contains' to match multiple fields`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use a 'contains' filter to match 'null'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use 'every' to combine multiple filters`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can use 'any' to combine multiple filters`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`gives a good error when query refers to missing card`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`gives a good error when query refers to missing field`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`it can filter on a plural primitive field using 'eq'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`it can filter on a nested field within a plural composite field using 'eq'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('it can match a null in a plural field', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('it can match a leaf plural field nested in a plural composite field', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('it can match thru a plural nested composite field that is field of a singular composite field', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can return a single result for a card when there are multiple matches within a result's search doc`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can perform query against WIP version of the index', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can perform query against "production" version of the index', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can sort search results', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can sort descending', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('nulls are sorted to the end of search results', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can get paginated results that are stable during index mutations', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'gt'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'gte'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'gt' thru nested fields`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'gt' thru a plural primitive field`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'gt' thru a plural composite field`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'lt'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can filter using 'lte'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`can combine 'range' filter`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test(`cannot filter 'null' value using 'range'`, async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can get prerendered cards from the indexer', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can get prerendered cards in an error state from the indexer', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); + }); + + test('can sort using a general field that is not an attribute of a card', async function (assert) { + await runSharedTest(indexQueryEngineTests, assert, { + indexQueryEngine, + dbAdapter, + loader, + testCards: await makeTestCards(loader), + }); }); }); }); diff --git a/packages/realm-server/tests/index-writer-test.ts b/packages/realm-server/tests/index-writer-test.ts index fe20bb5fa2..18930258d7 100644 --- a/packages/realm-server/tests/index-writer-test.ts +++ b/packages/realm-server/tests/index-writer-test.ts @@ -4,164 +4,167 @@ import { IndexWriter, IndexQueryEngine } from '@cardstack/runtime-common'; import { runSharedTest } from '@cardstack/runtime-common/helpers'; import { PgAdapter } from '@cardstack/postgres'; import indexWriterTests from '@cardstack/runtime-common/tests/index-writer-test'; +import { basename } from 'path'; -module('index-writer', function (hooks) { - let adapter: PgAdapter; - let indexWriter: IndexWriter; - let indexQueryEngine: IndexQueryEngine; +module(basename(__filename), function () { + module('index-writer', function (hooks) { + let adapter: PgAdapter; + let indexWriter: IndexWriter; + let indexQueryEngine: IndexQueryEngine; - hooks.beforeEach(async function () { - prepareTestDB(); - adapter = new PgAdapter({ autoMigrate: true }); - indexWriter = new IndexWriter(adapter); - indexQueryEngine = new IndexQueryEngine(adapter); - }); - - hooks.afterEach(async function () { - await adapter.close(); - }); - - test('can perform invalidations for a instance entry', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + hooks.beforeEach(async function () { + prepareTestDB(); + adapter = new PgAdapter({ autoMigrate: true }); + indexWriter = new IndexWriter(adapter); + indexQueryEngine = new IndexQueryEngine(adapter); }); - }); - test('can perform invalidations for a module entry', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + hooks.afterEach(async function () { + await adapter.close(); }); - }); - test("invalidations don't cross realm boundaries", async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('can perform invalidations for a instance entry', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('only invalidates latest version of content', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('can perform invalidations for a module entry', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can update an index entry', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test("invalidations don't cross realm boundaries", async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('error entry includes last known good state when available', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('only invalidates latest version of content', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('error entry does not include last known good state when not available', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('can update an index entry', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can get an error doc', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('error entry includes last known good state when available', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can get "production" index entry', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('error entry does not include last known good state when not available', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can get work in progress card', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('can get an error doc', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('returns undefined when getting a deleted card', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('can get "production" index entry', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can perform invalidations for an instance with deps more than a thousand', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('can get work in progress card', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can get compiled module and source when requested with file extension', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('returns undefined when getting a deleted card', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can get compiled module and source when requested without file extension', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('can perform invalidations for an instance with deps more than a thousand', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); - }); - test('can get compiled module and source from WIP index', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, - }); - }); + test('can get compiled module and source when requested with file extension', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); + }); - test('can get error doc for module', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, - }); - }); + test('can get compiled module and source when requested without file extension', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); + }); - test('returns undefined when getting a deleted module', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, - }); - }); + test('can get compiled module and source from WIP index', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); + }); + + test('can get error doc for module', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); + }); + + test('returns undefined when getting a deleted module', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); + }); - test('update realm meta when indexing is done', async function (assert) { - await runSharedTest(indexWriterTests, assert, { - indexWriter, - indexQueryEngine, - adapter, + test('update realm meta when indexing is done', async function (assert) { + await runSharedTest(indexWriterTests, assert, { + indexWriter, + indexQueryEngine, + adapter, + }); }); }); }); diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index 97f3d364f8..2f1acb37ea 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -22,6 +22,7 @@ import { } from './helpers'; import stripScopedCSSAttributes from '@cardstack/runtime-common/helpers/strip-scoped-css-attributes'; import { Server } from 'http'; +import { basename } from 'path'; function trimCardContainer(text: string) { return cleanWhiteSpace(text).replace( @@ -39,34 +40,35 @@ setGracefulCleanup(); // underlying filesystem in a manner that doesn't leak into other tests (as well // as to test through loader caching) -module('indexing', function (hooks) { - let { virtualNetwork: baseRealmServerVirtualNetwork, loader } = - createVirtualNetworkAndLoader(); +module(basename(__filename), function () { + module('indexing', function (hooks) { + let { virtualNetwork: baseRealmServerVirtualNetwork, loader } = + createVirtualNetworkAndLoader(); - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); + setupCardLogs( + hooks, + async () => await loader.import(`${baseRealm.url}card-api`), + ); + + let dir: string; + let realm: Realm; + + setupBaseRealmServer(hooks, baseRealmServerVirtualNetwork, matrixURL); - let dir: string; - let realm: Realm; - - setupBaseRealmServer(hooks, baseRealmServerVirtualNetwork, matrixURL); - - setupDB(hooks, { - beforeEach: async (dbAdapter, publisher, runner) => { - testDbAdapter = dbAdapter; - let virtualNetwork = createVirtualNetwork(); - dir = dirSync().name; - realm = await createRealm({ - withWorker: true, - dir, - virtualNetwork, - dbAdapter, - publisher, - runner, - fileSystem: { - 'person.gts': ` + setupDB(hooks, { + beforeEach: async (dbAdapter, publisher, runner) => { + testDbAdapter = dbAdapter; + let virtualNetwork = createVirtualNetwork(); + dir = dirSync().name; + realm = await createRealm({ + withWorker: true, + dir, + virtualNetwork, + dbAdapter, + publisher, + runner, + fileSystem: { + 'person.gts': ` import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -97,7 +99,7 @@ module('indexing', function (hooks) { } } `, - 'pet.gts': ` + 'pet.gts': ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -105,7 +107,7 @@ module('indexing', function (hooks) { @field firstName = contains(StringCard); } `, - 'fancy-person.gts': ` + 'fancy-person.gts': ` import { contains, field, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; import { Person } from "./person"; @@ -124,7 +126,7 @@ module('indexing', function (hooks) { } } `, - 'post.gts': ` + 'post.gts': ` import { contains, field, linksTo, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; import { Person } from "./person"; @@ -140,7 +142,7 @@ module('indexing', function (hooks) { } } `, - 'boom.gts': ` + 'boom.gts': ` import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -156,7 +158,7 @@ module('indexing', function (hooks) { } } `, - 'boom2.gts': ` + 'boom2.gts': ` import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -171,299 +173,299 @@ module('indexing', function (hooks) { } } `, - 'mango.json': { - data: { - attributes: { - firstName: 'Mango', - }, - meta: { - adoptsFrom: { - module: './person', - name: 'Person', + 'mango.json': { + data: { + attributes: { + firstName: 'Mango', + }, + meta: { + adoptsFrom: { + module: './person', + name: 'Person', + }, }, }, }, - }, - 'vangogh.json': { - data: { - attributes: { - firstName: 'Van Gogh', - }, - meta: { - adoptsFrom: { - module: './person', - name: 'Person', + 'vangogh.json': { + data: { + attributes: { + firstName: 'Van Gogh', + }, + meta: { + adoptsFrom: { + module: './person', + name: 'Person', + }, }, }, }, - }, - 'ringo.json': { - data: { - attributes: { - firstName: 'Ringo', - }, - meta: { - adoptsFrom: { - module: './pet', - name: 'Pet', + 'ringo.json': { + data: { + attributes: { + firstName: 'Ringo', + }, + meta: { + adoptsFrom: { + module: './pet', + name: 'Pet', + }, }, }, }, - }, - 'post-1.json': { - data: { - attributes: { - message: 'Who wants to fetch?!', - }, - relationships: { - author: { - links: { - self: './vangogh', + 'post-1.json': { + data: { + attributes: { + message: 'Who wants to fetch?!', + }, + relationships: { + author: { + links: { + self: './vangogh', + }, }, }, - }, - meta: { - adoptsFrom: { - module: './post', - name: 'Post', + meta: { + adoptsFrom: { + module: './post', + name: 'Post', + }, }, }, }, - }, - 'bad-link.json': { - data: { - attributes: { - message: 'I have a bad link', - }, - relationships: { - author: { - links: { - self: 'http://localhost:9000/this-is-a-link-to-nowhere', + 'bad-link.json': { + data: { + attributes: { + message: 'I have a bad link', + }, + relationships: { + author: { + links: { + self: 'http://localhost:9000/this-is-a-link-to-nowhere', + }, }, }, - }, - meta: { - adoptsFrom: { - module: './post', - name: 'Post', + meta: { + adoptsFrom: { + module: './post', + name: 'Post', + }, }, }, }, - }, - 'boom.json': { - data: { - attributes: { - firstName: 'Boom!', - }, - meta: { - adoptsFrom: { - module: './boom', - name: 'Boom', + 'boom.json': { + data: { + attributes: { + firstName: 'Boom!', + }, + meta: { + adoptsFrom: { + module: './boom', + name: 'Boom', + }, }, }, }, - }, - 'boom2.json': { - data: { - attributes: { - firstName: 'Boom!', - }, - meta: { - adoptsFrom: { - module: './boom2', - name: 'Boom', + 'boom2.json': { + data: { + attributes: { + firstName: 'Boom!', + }, + meta: { + adoptsFrom: { + module: './boom2', + name: 'Boom', + }, }, }, }, - }, - 'empty.json': { - data: { - attributes: {}, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/card-api', - name: 'CardDef', + 'empty.json': { + data: { + attributes: {}, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/card-api', + name: 'CardDef', + }, }, }, }, + 'random-file.txt': 'hello', + 'random-image.png': 'i am an image', + '.DS_Store': + 'In macOS, .DS_Store is a file that stores custom attributes of its containing folder', }, - 'random-file.txt': 'hello', - 'random-image.png': 'i am an image', - '.DS_Store': - 'In macOS, .DS_Store is a file that stores custom attributes of its containing folder', - }, - }); - await realm.start(); - }, - }); - - test('can store card pre-rendered html in the index', async function (assert) { - let entry = await realm.realmIndexQueryEngine.instance( - new URL(`${testRealm}mango`), - ); - if (entry?.type === 'instance') { - assert.strictEqual( - trimCardContainer(stripScopedCSSAttributes(entry!.isolatedHtml!)), - cleanWhiteSpace(`

Mango

`), - 'pre-rendered isolated format html is correct', - ); - assert.strictEqual( - trimCardContainer( - stripScopedCSSAttributes( - entry!.embeddedHtml![`${testRealm}person/Person`], - ), - ), - cleanWhiteSpace(`

Embedded Card Person: Mango

`), - 'pre-rendered embedded format html is correct', - ); - assert.strictEqual( - trimCardContainer( - stripScopedCSSAttributes( - entry!.fittedHtml![`${testRealm}person/Person`], - ), - ), - cleanWhiteSpace(`

Fitted Card Person: Mango

`), - 'pre-rendered fitted format html is correct', - ); - } else { - assert.ok(false, 'expected index entry not to be an error'); - } - }); + }); + await realm.start(); + }, + }); - test('can recover from rendering a card that has a template error', async function (assert) { - { - let entry = await realm.realmIndexQueryEngine.cardDocument( - new URL(`${testRealm}boom`), + test('can store card pre-rendered html in the index', async function (assert) { + let entry = await realm.realmIndexQueryEngine.instance( + new URL(`${testRealm}mango`), ); - if (entry?.type === 'error') { + if (entry?.type === 'instance') { assert.strictEqual( - entry.error.errorDetail.message, - 'Encountered error rendering HTML for card: intentional error', + trimCardContainer(stripScopedCSSAttributes(entry!.isolatedHtml!)), + cleanWhiteSpace(`

Mango

`), + 'pre-rendered isolated format html is correct', ); - assert.deepEqual(entry.error.errorDetail.deps, [`${testRealm}boom`]); - } else { - assert.ok('false', 'expected search entry to be an error document'); - } - } - { - let entry = await realm.realmIndexQueryEngine.cardDocument( - new URL(`${testRealm}boom2`), - ); - if (entry?.type === 'error') { assert.strictEqual( - entry.error.errorDetail.message, - 'Encountered error rendering HTML for card: Attempted to resolve a modifier in a strict mode template, but it was not in scope: did-insert', + trimCardContainer( + stripScopedCSSAttributes( + entry!.embeddedHtml![`${testRealm}person/Person`], + ), + ), + cleanWhiteSpace(`

Embedded Card Person: Mango

`), + 'pre-rendered embedded format html is correct', + ); + assert.strictEqual( + trimCardContainer( + stripScopedCSSAttributes( + entry!.fittedHtml![`${testRealm}person/Person`], + ), + ), + cleanWhiteSpace(`

Fitted Card Person: Mango

`), + 'pre-rendered fitted format html is correct', ); - assert.deepEqual(entry.error.errorDetail.deps, [`${testRealm}boom2`]); } else { - assert.ok('false', 'expected search entry to be an error document'); + assert.ok(false, 'expected index entry not to be an error'); } - } - { - let entry = await realm.realmIndexQueryEngine.cardDocument( - new URL(`${testRealm}vangogh`), - ); - if (entry?.type === 'doc') { - assert.deepEqual(entry.doc.data.attributes?.firstName, 'Van Gogh'); - let item = await realm.realmIndexQueryEngine.instance( - new URL(`${testRealm}vangogh`), + }); + + test('can recover from rendering a card that has a template error', async function (assert) { + { + let entry = await realm.realmIndexQueryEngine.cardDocument( + new URL(`${testRealm}boom`), ); - if (item?.type === 'instance') { + if (entry?.type === 'error') { assert.strictEqual( - trimCardContainer(stripScopedCSSAttributes(item.isolatedHtml!)), - cleanWhiteSpace(`

Van Gogh

`), - ); - assert.strictEqual( - trimCardContainer( - stripScopedCSSAttributes( - item.embeddedHtml![`${testRealm}person/Person`]!, - ), - ), - cleanWhiteSpace(`

Embedded Card Person: Van Gogh

`), + entry.error.errorDetail.message, + 'Encountered error rendering HTML for card: intentional error', ); + assert.deepEqual(entry.error.errorDetail.deps, [`${testRealm}boom`]); + } else { + assert.ok('false', 'expected search entry to be an error document'); + } + } + { + let entry = await realm.realmIndexQueryEngine.cardDocument( + new URL(`${testRealm}boom2`), + ); + if (entry?.type === 'error') { assert.strictEqual( - trimCardContainer( - stripScopedCSSAttributes( - item.fittedHtml![`${testRealm}person/Person`]!, - ), - ), - cleanWhiteSpace(`

Fitted Card Person: Van Gogh

`), + entry.error.errorDetail.message, + 'Encountered error rendering HTML for card: Attempted to resolve a modifier in a strict mode template, but it was not in scope: did-insert', ); + assert.deepEqual(entry.error.errorDetail.deps, [`${testRealm}boom2`]); } else { - assert.ok(false, 'expected index entry not to be an error'); + assert.ok('false', 'expected search entry to be an error document'); } - } else { - assert.ok( - false, - `expected search entry to be a document but was: ${entry?.error.errorDetail.message}`, + } + { + let entry = await realm.realmIndexQueryEngine.cardDocument( + new URL(`${testRealm}vangogh`), ); + if (entry?.type === 'doc') { + assert.deepEqual(entry.doc.data.attributes?.firstName, 'Van Gogh'); + let item = await realm.realmIndexQueryEngine.instance( + new URL(`${testRealm}vangogh`), + ); + if (item?.type === 'instance') { + assert.strictEqual( + trimCardContainer(stripScopedCSSAttributes(item.isolatedHtml!)), + cleanWhiteSpace(`

Van Gogh

`), + ); + assert.strictEqual( + trimCardContainer( + stripScopedCSSAttributes( + item.embeddedHtml![`${testRealm}person/Person`]!, + ), + ), + cleanWhiteSpace(`

Embedded Card Person: Van Gogh

`), + ); + assert.strictEqual( + trimCardContainer( + stripScopedCSSAttributes( + item.fittedHtml![`${testRealm}person/Person`]!, + ), + ), + cleanWhiteSpace(`

Fitted Card Person: Van Gogh

`), + ); + } else { + assert.ok(false, 'expected index entry not to be an error'); + } + } else { + assert.ok( + false, + `expected search entry to be a document but was: ${entry?.error.errorDetail.message}`, + ); + } } - } - }); + }); - test('can make an error doc for a card that has a link to a URL that is not a card', async function (assert) { - let entry = await realm.realmIndexQueryEngine.cardDocument( - new URL(`${testRealm}bad-link`), - ); - if (entry?.type === 'error') { - assert.strictEqual( - entry.error.errorDetail.message, - 'unable to fetch http://localhost:9000/this-is-a-link-to-nowhere: fetch failed for http://localhost:9000/this-is-a-link-to-nowhere', + test('can make an error doc for a card that has a link to a URL that is not a card', async function (assert) { + let entry = await realm.realmIndexQueryEngine.cardDocument( + new URL(`${testRealm}bad-link`), ); - assert.deepEqual(entry.error.errorDetail.deps, [ - `${testRealm}post`, - `http://localhost:9000/this-is-a-link-to-nowhere`, - ]); - } else { - assert.ok('false', 'expected search entry to be an error document'); - } - }); + if (entry?.type === 'error') { + assert.strictEqual( + entry.error.errorDetail.message, + 'unable to fetch http://localhost:9000/this-is-a-link-to-nowhere: fetch failed for http://localhost:9000/this-is-a-link-to-nowhere', + ); + assert.deepEqual(entry.error.errorDetail.deps, [ + `${testRealm}post`, + `http://localhost:9000/this-is-a-link-to-nowhere`, + ]); + } else { + assert.ok('false', 'expected search entry to be an error document'); + } + }); - test('can incrementally index updated instance', async function (assert) { - await realm.write( - 'mango.json', - JSON.stringify({ - data: { - attributes: { - firstName: 'Mang-Mang', - }, - meta: { - adoptsFrom: { - module: './person.gts', - name: 'Person', + test('can incrementally index updated instance', async function (assert) { + await realm.write( + 'mango.json', + JSON.stringify({ + data: { + attributes: { + firstName: 'Mang-Mang', + }, + meta: { + adoptsFrom: { + module: './person.gts', + name: 'Person', + }, }, }, - }, - } as LooseSingleCardDocument), - ); + } as LooseSingleCardDocument), + ); - let { data: result } = await realm.realmIndexQueryEngine.search({ - filter: { - on: { module: `${testRealm}person`, name: 'Person' }, - eq: { firstName: 'Mang-Mang' }, - }, + let { data: result } = await realm.realmIndexQueryEngine.search({ + filter: { + on: { module: `${testRealm}person`, name: 'Person' }, + eq: { firstName: 'Mang-Mang' }, + }, + }); + assert.strictEqual(result.length, 1, 'found updated document'); + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 1, + instanceErrors: 0, + moduleErrors: 0, + modulesIndexed: 0, + totalIndexEntries: 11, + }, + 'indexed correct number of files', + ); }); - assert.strictEqual(result.length, 1, 'found updated document'); - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 1, - instanceErrors: 0, - moduleErrors: 0, - modulesIndexed: 0, - totalIndexEntries: 11, - }, - 'indexed correct number of files', - ); - }); - test('can recover from a card error after error is removed from card source', async function (assert) { - // introduce errors into 2 cards and observe that invalidation doesn't - // blindly invalidate all cards are in an error state - await realm.write( - 'pet.gts', - ` + test('can recover from a card error after error is removed from card source', async function (assert) { + // introduce errors into 2 cards and observe that invalidation doesn't + // blindly invalidate all cards are in an error state + await realm.write( + 'pet.gts', + ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; export class Pet extends CardDef { @@ -471,51 +473,51 @@ module('indexing', function (hooks) { } throw new Error('boom!'); `, - ); - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 0, - instanceErrors: 1, - moduleErrors: 1, - modulesIndexed: 0, - totalIndexEntries: 9, - }, - 'indexed correct number of files', - ); - await realm.write( - 'person.gts', - ` + ); + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 0, + instanceErrors: 1, + moduleErrors: 1, + modulesIndexed: 0, + totalIndexEntries: 9, + }, + 'indexed correct number of files', + ); + await realm.write( + 'person.gts', + ` // syntax error export class IntentionallyThrownError { `, - ); - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 0, - instanceErrors: 4, // 1 post, 2 persons, 1 bad-link post - moduleErrors: 3, // post, fancy person, person - modulesIndexed: 0, - totalIndexEntries: 3, - }, - 'indexed correct number of files', - ); - let { data: result } = await realm.realmIndexQueryEngine.search({ - filter: { - type: { module: `${testRealm}person`, name: 'Person' }, - }, - }); - assert.deepEqual( - result, - [], - 'the broken type results in no instance results', - ); - await realm.write( - 'person.gts', - ` + ); + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 0, + instanceErrors: 4, // 1 post, 2 persons, 1 bad-link post + moduleErrors: 3, // post, fancy person, person + modulesIndexed: 0, + totalIndexEntries: 3, + }, + 'indexed correct number of files', + ); + let { data: result } = await realm.realmIndexQueryEngine.search({ + filter: { + type: { module: `${testRealm}person`, name: 'Person' }, + }, + }); + assert.deepEqual( + result, + [], + 'the broken type results in no instance results', + ); + await realm.write( + 'person.gts', + ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -523,61 +525,61 @@ module('indexing', function (hooks) { @field firstName = contains(StringCard); } `, - ); - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 3, // 1 post and 2 persons - instanceErrors: 1, - moduleErrors: 0, - modulesIndexed: 3, - totalIndexEntries: 9, - }, - 'indexed correct number of files', - ); - result = ( - await realm.realmIndexQueryEngine.search({ - filter: { - type: { module: `${testRealm}person`, name: 'Person' }, + ); + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 3, // 1 post and 2 persons + instanceErrors: 1, + moduleErrors: 0, + modulesIndexed: 3, + totalIndexEntries: 9, }, - }) - ).data; - assert.strictEqual( - result.length, - 2, - 'correct number of instances returned', - ); - }); + 'indexed correct number of files', + ); + result = ( + await realm.realmIndexQueryEngine.search({ + filter: { + type: { module: `${testRealm}person`, name: 'Person' }, + }, + }) + ).data; + assert.strictEqual( + result.length, + 2, + 'correct number of instances returned', + ); + }); - test('can incrementally index deleted instance', async function (assert) { - await realm.delete('mango.json'); + test('can incrementally index deleted instance', async function (assert) { + await realm.delete('mango.json'); - let { data: result } = await realm.realmIndexQueryEngine.search({ - filter: { - on: { module: `${testRealm}person`, name: 'Person' }, - eq: { firstName: 'Mango' }, - }, + let { data: result } = await realm.realmIndexQueryEngine.search({ + filter: { + on: { module: `${testRealm}person`, name: 'Person' }, + eq: { firstName: 'Mango' }, + }, + }); + assert.strictEqual(result.length, 0, 'found no documents'); + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 0, + instanceErrors: 0, + moduleErrors: 0, + modulesIndexed: 0, + totalIndexEntries: 10, + }, + 'index did not touch any files', + ); }); - assert.strictEqual(result.length, 0, 'found no documents'); - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 0, - instanceErrors: 0, - moduleErrors: 0, - modulesIndexed: 0, - totalIndexEntries: 10, - }, - 'index did not touch any files', - ); - }); - test('can incrementally index instance that depends on updated card source', async function (assert) { - await realm.write( - 'post.gts', - ` + test('can incrementally index instance that depends on updated card source', async function (assert) { + await realm.write( + 'post.gts', + ` import { contains, linksTo, field, CardDef } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; import { Person } from "./person"; @@ -592,33 +594,33 @@ module('indexing', function (hooks) { }) } `, - ); + ); - let { data: result } = await realm.realmIndexQueryEngine.search({ - filter: { - on: { module: `${testRealm}post`, name: 'Post' }, - eq: { nickName: 'Van Gogh-poo' }, - }, + let { data: result } = await realm.realmIndexQueryEngine.search({ + filter: { + on: { module: `${testRealm}post`, name: 'Post' }, + eq: { nickName: 'Van Gogh-poo' }, + }, + }); + assert.strictEqual(result.length, 1, 'found updated document'); + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 1, + instanceErrors: 1, + moduleErrors: 0, + modulesIndexed: 1, + totalIndexEntries: 11, + }, + 'indexed correct number of files', + ); }); - assert.strictEqual(result.length, 1, 'found updated document'); - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 1, - instanceErrors: 1, - moduleErrors: 0, - modulesIndexed: 1, - totalIndexEntries: 11, - }, - 'indexed correct number of files', - ); - }); - test('can incrementally index instance that depends on updated card source consumed by other card sources', async function (assert) { - await realm.write( - 'person.gts', - ` + test('can incrementally index instance that depends on updated card source consumed by other card sources', async function (assert) { + await realm.write( + 'person.gts', + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -634,82 +636,82 @@ module('indexing', function (hooks) { } } `, - ); - - let { data: result } = await realm.realmIndexQueryEngine.search({ - filter: { - on: { module: `${testRealm}post`, name: 'Post' }, - eq: { 'author.nickName': 'Van Gogh-poo' }, - }, - }); - assert.strictEqual(result.length, 1, 'found updated document'); - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 3, - instanceErrors: 1, - moduleErrors: 0, - modulesIndexed: 3, - totalIndexEntries: 11, - }, - 'indexed correct number of files', - ); - }); + ); - test('can incrementally index instance that depends on deleted card source', async function (assert) { - await realm.delete('post.gts'); - { let { data: result } = await realm.realmIndexQueryEngine.search({ filter: { - type: { module: `${testRealm}post`, name: 'Post' }, + on: { module: `${testRealm}post`, name: 'Post' }, + eq: { 'author.nickName': 'Van Gogh-poo' }, }, }); + assert.strictEqual(result.length, 1, 'found updated document'); assert.deepEqual( - result, - [], - 'the deleted type results in no card instance results', + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 3, + instanceErrors: 1, + moduleErrors: 0, + modulesIndexed: 3, + totalIndexEntries: 11, + }, + 'indexed correct number of files', ); - } - let actual = await realm.realmIndexQueryEngine.cardDocument( - new URL(`${testRealm}post-1`), - ); - if (actual?.type === 'error') { - assert.ok(actual.error.errorDetail.stack, 'stack trace is included'); - delete actual.error.errorDetail.stack; + }); + + test('can incrementally index instance that depends on deleted card source', async function (assert) { + await realm.delete('post.gts'); + { + let { data: result } = await realm.realmIndexQueryEngine.search({ + filter: { + type: { module: `${testRealm}post`, name: 'Post' }, + }, + }); + assert.deepEqual( + result, + [], + 'the deleted type results in no card instance results', + ); + } + let actual = await realm.realmIndexQueryEngine.cardDocument( + new URL(`${testRealm}post-1`), + ); + if (actual?.type === 'error') { + assert.ok(actual.error.errorDetail.stack, 'stack trace is included'); + delete actual.error.errorDetail.stack; + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...actual.error.errorDetail }, + { + isCardError: true, + additionalErrors: null, + message: 'http://test-realm/post not found', + status: 404, + title: 'Not Found', + deps: ['http://test-realm/post'], + }, + 'card instance is an error document', + ); + } else { + assert.ok(false, 'search index entry is not an error document'); + } assert.deepEqual( // we splat because despite having the same shape, the constructors are different - { ...actual.error.errorDetail }, + { ...realm.realmIndexUpdater.stats }, { - isCardError: true, - additionalErrors: null, - message: 'http://test-realm/post not found', - status: 404, - title: 'Not Found', - deps: ['http://test-realm/post'], + instancesIndexed: 0, + instanceErrors: 2, + moduleErrors: 0, + modulesIndexed: 0, + totalIndexEntries: 9, }, - 'card instance is an error document', + 'indexed correct number of files', ); - } else { - assert.ok(false, 'search index entry is not an error document'); - } - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, - { - instancesIndexed: 0, - instanceErrors: 2, - moduleErrors: 0, - modulesIndexed: 0, - totalIndexEntries: 9, - }, - 'indexed correct number of files', - ); - // when the definitions is created again, the instance should mend its broken link - await realm.write( - 'post.gts', - ` + // when the definitions is created again, the instance should mend its broken link + await realm.write( + 'post.gts', + ` import { contains, linksTo, field, CardDef } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; import { Person } from "./person"; @@ -724,280 +726,283 @@ module('indexing', function (hooks) { }) } `, - ); - { - let { data: result } = await realm.realmIndexQueryEngine.search({ - filter: { - on: { module: `${testRealm}post`, name: 'Post' }, - eq: { nickName: 'Van Gogh-poo' }, - }, - }); - assert.strictEqual(result.length, 1, 'found the post instance'); - } - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...realm.realmIndexUpdater.stats }, + ); { - instancesIndexed: 1, - instanceErrors: 1, - moduleErrors: 0, - modulesIndexed: 1, - totalIndexEntries: 11, - }, - 'indexed correct number of files', - ); - }); + let { data: result } = await realm.realmIndexQueryEngine.search({ + filter: { + on: { module: `${testRealm}post`, name: 'Post' }, + eq: { nickName: 'Van Gogh-poo' }, + }, + }); + assert.strictEqual(result.length, 1, 'found the post instance'); + } + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...realm.realmIndexUpdater.stats }, + { + instancesIndexed: 1, + instanceErrors: 1, + moduleErrors: 0, + modulesIndexed: 1, + totalIndexEntries: 11, + }, + 'indexed correct number of files', + ); + }); - test('sets resource_created_at for modules and instances', async function (assert) { - let entry = (await realm.realmIndexQueryEngine.module( - new URL(`${testRealm}fancy-person.gts`), - )) as { resourceCreatedAt: number }; + test('sets resource_created_at for modules and instances', async function (assert) { + let entry = (await realm.realmIndexQueryEngine.module( + new URL(`${testRealm}fancy-person.gts`), + )) as { resourceCreatedAt: number }; - assert.ok(entry!.resourceCreatedAt, 'resourceCreatedAt is set'); + assert.ok(entry!.resourceCreatedAt, 'resourceCreatedAt is set'); - entry = (await realm.realmIndexQueryEngine.instance( - new URL(`${testRealm}mango`), - )) as { resourceCreatedAt: number }; + entry = (await realm.realmIndexQueryEngine.instance( + new URL(`${testRealm}mango`), + )) as { resourceCreatedAt: number }; - assert.ok(entry!.resourceCreatedAt, 'resourceCreatedAt is set'); - }); + assert.ok(entry!.resourceCreatedAt, 'resourceCreatedAt is set'); + }); - test('sets urls containing encoded CSS for deps for a module', async function (assert) { - let entry = (await realm.realmIndexQueryEngine.module( - new URL('http://test-realm/fancy-person.gts'), - )) as { deps: string[] }; - - let assertCssDependency = ( - deps: string[], - pattern: RegExp, - fileName: string, - ) => { - assert.true( - !!deps.find((dep) => pattern.test(dep)), - `css for ${fileName} is in the deps`, - ); - }; + test('sets urls containing encoded CSS for deps for a module', async function (assert) { + let entry = (await realm.realmIndexQueryEngine.module( + new URL('http://test-realm/fancy-person.gts'), + )) as { deps: string[] }; + + let assertCssDependency = ( + deps: string[], + pattern: RegExp, + fileName: string, + ) => { + assert.true( + !!deps.find((dep) => pattern.test(dep)), + `css for ${fileName} is in the deps`, + ); + }; - let dependencies = [ - { - pattern: /fancy-person\.gts.*\.glimmer-scoped\.css$/, - fileName: 'fancy-person.gts', - }, - { - pattern: /test-realm\/person\.gts.*\.glimmer-scoped\.css$/, - fileName: 'person.gts', - }, - { - pattern: - /cardstack.com\/base\/default-templates\/embedded\.gts.*\.glimmer-scoped\.css$/, - fileName: 'default-templates/embedded.gts', - }, - { - pattern: - /cardstack.com\/base\/default-templates\/isolated-and-edit\.gts.*\.glimmer-scoped\.css$/, - fileName: 'default-templates/isolated-and-edit.gts', - }, - { - pattern: - /cardstack.com\/base\/default-templates\/missing-embedded\.gts.*\.glimmer-scoped\.css$/, - fileName: 'default-templates/missing-embedded.gts', - }, - { - pattern: - /cardstack.com\/base\/default-templates\/field-edit\.gts.*\.glimmer-scoped\.css$/, - fileName: 'default-templates/field-edit.gts', - }, - { - pattern: - /cardstack.com\/base\/links-to-many-component.gts.*\.glimmer-scoped\.css$/, - fileName: 'links-to-many-component.gts', - }, - { - pattern: - /cardstack.com\/base\/links-to-editor.gts.*\.glimmer-scoped\.css$/, - fileName: 'links-to-editor.gts', - }, - { - pattern: - /cardstack.com\/base\/contains-many-component.gts.*\.glimmer-scoped\.css$/, - fileName: 'contains-many-component.gts', - }, - { - pattern: - /cardstack.com\/base\/field-component.gts.*\.glimmer-scoped\.css$/, - fileName: 'field-component.gts', - }, - ]; + let dependencies = [ + { + pattern: /fancy-person\.gts.*\.glimmer-scoped\.css$/, + fileName: 'fancy-person.gts', + }, + { + pattern: /test-realm\/person\.gts.*\.glimmer-scoped\.css$/, + fileName: 'person.gts', + }, + { + pattern: + /cardstack.com\/base\/default-templates\/embedded\.gts.*\.glimmer-scoped\.css$/, + fileName: 'default-templates/embedded.gts', + }, + { + pattern: + /cardstack.com\/base\/default-templates\/isolated-and-edit\.gts.*\.glimmer-scoped\.css$/, + fileName: 'default-templates/isolated-and-edit.gts', + }, + { + pattern: + /cardstack.com\/base\/default-templates\/missing-embedded\.gts.*\.glimmer-scoped\.css$/, + fileName: 'default-templates/missing-embedded.gts', + }, + { + pattern: + /cardstack.com\/base\/default-templates\/field-edit\.gts.*\.glimmer-scoped\.css$/, + fileName: 'default-templates/field-edit.gts', + }, + { + pattern: + /cardstack.com\/base\/links-to-many-component.gts.*\.glimmer-scoped\.css$/, + fileName: 'links-to-many-component.gts', + }, + { + pattern: + /cardstack.com\/base\/links-to-editor.gts.*\.glimmer-scoped\.css$/, + fileName: 'links-to-editor.gts', + }, + { + pattern: + /cardstack.com\/base\/contains-many-component.gts.*\.glimmer-scoped\.css$/, + fileName: 'contains-many-component.gts', + }, + { + pattern: + /cardstack.com\/base\/field-component.gts.*\.glimmer-scoped\.css$/, + fileName: 'field-component.gts', + }, + ]; - dependencies.forEach(({ pattern, fileName }) => { - assertCssDependency(entry.deps, pattern, fileName); + dependencies.forEach(({ pattern, fileName }) => { + assertCssDependency(entry.deps, pattern, fileName); + }); }); - }); - test('will not invalidate non-json/non-executable files', async function (assert) { - let deletedEntries = (await testDbAdapter.execute( - `SELECT url FROM boxel_index WHERE is_deleted = TRUE`, - )) as { url: string }[]; + test('will not invalidate non-json/non-executable files', async function (assert) { + let deletedEntries = (await testDbAdapter.execute( + `SELECT url FROM boxel_index WHERE is_deleted = TRUE`, + )) as { url: string }[]; - let deletedEntryUrls = deletedEntries.map((row) => row.url); + let deletedEntryUrls = deletedEntries.map((row) => row.url); - ['random-file.txt', 'random-image.png', '.DS_Store'].forEach((file) => { - assert.notOk(deletedEntryUrls.includes(file)); + ['random-file.txt', 'random-image.png', '.DS_Store'].forEach((file) => { + assert.notOk(deletedEntryUrls.includes(file)); + }); }); }); -}); -module('permissioned realm', function (hooks) { - let { virtualNetwork: baseRealmServerVirtualNetwork, loader } = - createVirtualNetworkAndLoader(); + module('permissioned realm', function (hooks) { + let { virtualNetwork: baseRealmServerVirtualNetwork, loader } = + createVirtualNetworkAndLoader(); - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); + setupCardLogs( + hooks, + async () => await loader.import(`${baseRealm.url}card-api`), + ); - setupBaseRealmServer(hooks, baseRealmServerVirtualNetwork, matrixURL); - - // We want 2 different realm users to test authorization between them - these - // names are selected because they are already available in the test - // environment (via register-realm-users.ts) - let matrixUser1 = 'test_realm'; - let matrixUser2 = 'node-test_realm'; - let testRealm1URL = new URL('http://127.0.0.1:4447/'); - let testRealm2URL = new URL('http://127.0.0.1:4448/'); - - let testRealm2: Realm; - let testRealmServer1: Server; - let testRealmServer2: Server; - - function setupRealms( - hooks: NestedHooks, - permissions: { - consumer: RealmPermissions; - provider: RealmPermissions; - }, - ) { - setupDB(hooks, { - beforeEach: async (dbAdapter, publisher, runner) => { - ({ testRealmHttpServer: testRealmServer1 } = await runTestRealmServer({ - virtualNetwork: await createVirtualNetwork(), - testRealmDir: dirSync().name, - realmsRootPath: dirSync().name, - realmURL: testRealm1URL, - fileSystem: { - 'article.gts': ` + setupBaseRealmServer(hooks, baseRealmServerVirtualNetwork, matrixURL); + + // We want 2 different realm users to test authorization between them - these + // names are selected because they are already available in the test + // environment (via register-realm-users.ts) + let matrixUser1 = 'test_realm'; + let matrixUser2 = 'node-test_realm'; + let testRealm1URL = new URL('http://127.0.0.1:4447/'); + let testRealm2URL = new URL('http://127.0.0.1:4448/'); + + let testRealm2: Realm; + let testRealmServer1: Server; + let testRealmServer2: Server; + + function setupRealms( + hooks: NestedHooks, + permissions: { + consumer: RealmPermissions; + provider: RealmPermissions; + }, + ) { + setupDB(hooks, { + beforeEach: async (dbAdapter, publisher, runner) => { + ({ testRealmHttpServer: testRealmServer1 } = await runTestRealmServer( + { + virtualNetwork: await createVirtualNetwork(), + testRealmDir: dirSync().name, + realmsRootPath: dirSync().name, + realmURL: testRealm1URL, + fileSystem: { + 'article.gts': ` import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; export class Article extends CardDef { @field title = contains(StringCard); } `, - }, - permissions: permissions.provider, - matrixURL, - matrixConfig: { - url: matrixURL, - username: matrixUser1, - }, - dbAdapter, - publisher, - runner, - })); - - ({ testRealmHttpServer: testRealmServer2, testRealm: testRealm2 } = - await runTestRealmServer({ - virtualNetwork: await createVirtualNetwork(), - testRealmDir: dirSync().name, - realmsRootPath: dirSync().name, - realmURL: testRealm2URL, - fileSystem: { - 'website.gts': ` + }, + permissions: permissions.provider, + matrixURL, + matrixConfig: { + url: matrixURL, + username: matrixUser1, + }, + dbAdapter, + publisher, + runner, + }, + )); + + ({ testRealmHttpServer: testRealmServer2, testRealm: testRealm2 } = + await runTestRealmServer({ + virtualNetwork: await createVirtualNetwork(), + testRealmDir: dirSync().name, + realmsRootPath: dirSync().name, + realmURL: testRealm2URL, + fileSystem: { + 'website.gts': ` import { contains, field, CardDef, linksTo } from "https://cardstack.com/base/card-api"; import { Article } from "${testRealm1URL.href}article" // importing from another realm; export class Website extends CardDef { @field linkedArticle = linksTo(Article); }`, - 'website-1.json': { - data: { - attributes: {}, - meta: { - adoptsFrom: { - module: './website', - name: 'Website', + 'website-1.json': { + data: { + attributes: {}, + meta: { + adoptsFrom: { + module: './website', + name: 'Website', + }, }, }, }, }, - }, - permissions: permissions.consumer, - matrixURL, - matrixConfig: { - url: matrixURL, - username: matrixUser2, - }, - dbAdapter, - publisher, - runner, - })); - }, - afterEach: async () => { - await closeServer(testRealmServer1); - await closeServer(testRealmServer2); - }, - }); - } - - module('readable realm', function (hooks) { - setupRealms(hooks, { - provider: { - '@node-test_realm:localhost': ['read'], - }, - consumer: { - '*': ['read', 'write'], - }, - }); + permissions: permissions.consumer, + matrixURL, + matrixConfig: { + url: matrixURL, + username: matrixUser2, + }, + dbAdapter, + publisher, + runner, + })); + }, + afterEach: async () => { + await closeServer(testRealmServer1); + await closeServer(testRealmServer2); + }, + }); + } - test('has no module errors when trying to index a card from another realm when it has permission to read', async function (assert) { - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...testRealm2.realmIndexUpdater.stats }, - { - instancesIndexed: 1, - instanceErrors: 0, - moduleErrors: 0, - modulesIndexed: 1, - totalIndexEntries: 2, + module('readable realm', function (hooks) { + setupRealms(hooks, { + provider: { + '@node-test_realm:localhost': ['read'], }, - 'has no module errors', - ); - }); - }); + consumer: { + '*': ['read', 'write'], + }, + }); - module('un-readable realm', function (hooks) { - setupRealms(hooks, { - provider: { - nobody: ['read', 'write'], // Consumer's matrix user not authorized to read from provider - }, - consumer: { - '*': ['read', 'write'], - }, + test('has no module errors when trying to index a card from another realm when it has permission to read', async function (assert) { + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...testRealm2.realmIndexUpdater.stats }, + { + instancesIndexed: 1, + instanceErrors: 0, + moduleErrors: 0, + modulesIndexed: 1, + totalIndexEntries: 2, + }, + 'has no module errors', + ); + }); }); - test('has a module error when trying to index a module from another realm when it has no permission to read', async function (assert) { - // Error during indexing will be: "Authorization error: Insufficient - // permissions to perform this action" - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...testRealm2.realmIndexUpdater.stats }, - { - instanceErrors: 1, - instancesIndexed: 0, - moduleErrors: 1, - modulesIndexed: 0, - totalIndexEntries: 0, + module('un-readable realm', function (hooks) { + setupRealms(hooks, { + provider: { + nobody: ['read', 'write'], // Consumer's matrix user not authorized to read from provider }, - 'has a module error', - ); + consumer: { + '*': ['read', 'write'], + }, + }); + + test('has a module error when trying to index a module from another realm when it has no permission to read', async function (assert) { + // Error during indexing will be: "Authorization error: Insufficient + // permissions to perform this action" + assert.deepEqual( + // we splat because despite having the same shape, the constructors are different + { ...testRealm2.realmIndexUpdater.stats }, + { + instanceErrors: 1, + instancesIndexed: 0, + moduleErrors: 1, + modulesIndexed: 0, + totalIndexEntries: 0, + }, + 'has a module error', + ); + }); }); }); }); diff --git a/packages/realm-server/tests/loader-test.ts b/packages/realm-server/tests/loader-test.ts index 2c5db92301..992b16a8b5 100644 --- a/packages/realm-server/tests/loader-test.ts +++ b/packages/realm-server/tests/loader-test.ts @@ -18,172 +18,180 @@ import { import { copySync } from 'fs-extra'; import { shimExternals } from '../lib/externals'; import { Server } from 'http'; -import { join } from 'path'; +import { join, basename } from 'path'; setGracefulCleanup(); const testRealmURL = new URL('http://127.0.0.1:4444/'); const testRealmHref = testRealmURL.href; -module('loader', function (hooks) { - let dir: DirResult; - let testRealmHttpServer: Server; +module(basename(__filename), function () { + module('loader', function (hooks) { + let dir: DirResult; + let testRealmHttpServer: Server; - let virtualNetwork = new VirtualNetwork(); - shimExternals(virtualNetwork); + let virtualNetwork = new VirtualNetwork(); + shimExternals(virtualNetwork); - function createLoader() { - let fetch = fetcher(virtualNetwork.fetch, [ - async (req, next) => { - return (await maybeHandleScopedCSSRequest(req)) || next(req); - }, - ]); - return new Loader(fetch, virtualNetwork.resolveImport); - } - - setupBaseRealmServer(hooks, virtualNetwork, matrixURL); - - hooks.before(async function () { - dir = dirSync(); - copySync(join(__dirname, 'cards'), dir.name); - }); - - setupDB(hooks, { - before: async (dbAdapter, publisher, runner) => { - ({ testRealmHttpServer } = await runTestRealmServer({ - virtualNetwork, - testRealmDir: dir.name, - realmsRootPath: dir.name, - realmURL: testRealmURL, - dbAdapter, - publisher, - runner, - matrixURL, - })); - }, - after: async () => { - await closeServer(testRealmHttpServer); - }, - }); + function createLoader() { + let fetch = fetcher(virtualNetwork.fetch, [ + async (req, next) => { + return (await maybeHandleScopedCSSRequest(req)) || next(req); + }, + ]); + return new Loader(fetch, virtualNetwork.resolveImport); + } - test('can dynamically load modules with cycles', async function (assert) { - let loader = createLoader(); - let module = await loader.import<{ three(): number }>( - `${testRealmHref}cycle-two`, - ); - assert.strictEqual(module.three(), 3); - }); + setupBaseRealmServer(hooks, virtualNetwork, matrixURL); - test('can resolve multiple import load races against a common dep', async function (assert) { - let loader = createLoader(); - let a = loader.import<{ a(): string }>(`${testRealmHref}a`); - let b = loader.import<{ b(): string }>(`${testRealmHref}b`); - let [aModule, bModule] = await Promise.all([a, b]); - assert.strictEqual(aModule.a(), 'abc', 'module executed successfully'); - assert.strictEqual(bModule.b(), 'bc', 'module executed successfully'); - }); + hooks.before(async function () { + dir = dirSync(); + copySync(join(__dirname, 'cards'), dir.name); + }); - test('can resolve a import deadlock', async function (assert) { - let loader = createLoader(); - let a = loader.import<{ a(): string }>(`${testRealmHref}deadlock/a`); - let b = loader.import<{ b(): string }>(`${testRealmHref}deadlock/b`); - let c = loader.import<{ c(): string }>(`${testRealmHref}deadlock/c`); - let [aModule, bModule, cModule] = await Promise.all([a, b, c]); - assert.strictEqual(aModule.a(), 'abcd', 'module executed successfully'); - assert.strictEqual(bModule.b(), 'bcd', 'module executed successfully'); - assert.strictEqual(cModule.c(), 'cd', 'module executed successfully'); - }); + setupDB(hooks, { + before: async (dbAdapter, publisher, runner) => { + ({ testRealmHttpServer } = await runTestRealmServer({ + virtualNetwork, + testRealmDir: dir.name, + realmsRootPath: dir.name, + realmURL: testRealmURL, + dbAdapter, + publisher, + runner, + matrixURL, + })); + }, + after: async () => { + await closeServer(testRealmHttpServer); + }, + }); - test('can determine consumed modules', async function (assert) { - let loader = createLoader(); - await loader.import<{ a(): string }>(`${testRealmHref}a`); - assert.deepEqual(await loader.getConsumedModules(`${testRealmHref}a`), [ - `${testRealmHref}b`, - `${testRealmHref}c`, - ]); - }); + test('can dynamically load modules with cycles', async function (assert) { + let loader = createLoader(); + let module = await loader.import<{ three(): number }>( + `${testRealmHref}cycle-two`, + ); + assert.strictEqual(module.three(), 3); + }); - test('can get consumed modules within a cycle', async function (assert) { - let loader = createLoader(); - await loader.import<{ three(): number }>(`${testRealmHref}cycle-two`); - let modules = await loader.getConsumedModules(`${testRealmHref}cycle-two`); - assert.deepEqual(modules, [`${testRealmHref}cycle-one`]); - }); + test('can resolve multiple import load races against a common dep', async function (assert) { + let loader = createLoader(); + let a = loader.import<{ a(): string }>(`${testRealmHref}a`); + let b = loader.import<{ b(): string }>(`${testRealmHref}b`); + let [aModule, bModule] = await Promise.all([a, b]); + assert.strictEqual(aModule.a(), 'abc', 'module executed successfully'); + assert.strictEqual(bModule.b(), 'bc', 'module executed successfully'); + }); - test('supports identify API', async function (assert) { - let loader = createLoader(); - let { Person } = await loader.import<{ Person: unknown }>( - `${testRealmHref}person`, - ); - assert.deepEqual(loader.identify(Person), { - module: `${testRealmHref}person`, - name: 'Person', + test('can resolve a import deadlock', async function (assert) { + let loader = createLoader(); + let a = loader.import<{ a(): string }>(`${testRealmHref}deadlock/a`); + let b = loader.import<{ b(): string }>(`${testRealmHref}deadlock/b`); + let c = loader.import<{ c(): string }>(`${testRealmHref}deadlock/c`); + let [aModule, bModule, cModule] = await Promise.all([a, b, c]); + assert.strictEqual(aModule.a(), 'abcd', 'module executed successfully'); + assert.strictEqual(bModule.b(), 'bcd', 'module executed successfully'); + assert.strictEqual(cModule.c(), 'cd', 'module executed successfully'); }); - // The loader knows which loader instance was used to import the card - assert.deepEqual(Loader.identify(Person), { - module: `${testRealmHref}person`, - name: 'Person', + + test('can determine consumed modules', async function (assert) { + let loader = createLoader(); + await loader.import<{ a(): string }>(`${testRealmHref}a`); + assert.deepEqual(await loader.getConsumedModules(`${testRealmHref}a`), [ + `${testRealmHref}b`, + `${testRealmHref}c`, + ]); }); - }); - test('exports cannot be mutated', async function (assert) { - let loader = createLoader(); - let module = await loader.import<{ Person: unknown }>( - `${testRealmHref}person`, - ); - assert.throws(() => { - module.Person = 1; - }, /modules are read only/); - }); + test('can get consumed modules within a cycle', async function (assert) { + let loader = createLoader(); + await loader.import<{ three(): number }>(`${testRealmHref}cycle-two`); + let modules = await loader.getConsumedModules( + `${testRealmHref}cycle-two`, + ); + assert.deepEqual(modules, [`${testRealmHref}cycle-one`]); + }); - test('can get a loader used to import a specific card', async function (assert) { - let loader = createLoader(); - let module = await loader.import(`${testRealmHref}person`); - let card = module.Person; - let testingLoader = Loader.getLoaderFor(card); - assert.strictEqual(testingLoader, loader, 'the loaders are the same'); - }); + test('supports identify API', async function (assert) { + let loader = createLoader(); + let { Person } = await loader.import<{ Person: unknown }>( + `${testRealmHref}person`, + ); + assert.deepEqual(loader.identify(Person), { + module: `${testRealmHref}person`, + name: 'Person', + }); + // The loader knows which loader instance was used to import the card + assert.deepEqual(Loader.identify(Person), { + module: `${testRealmHref}person`, + name: 'Person', + }); + }); - module('with a different realm', function (hooks) { - let loader2: Loader; - let realm: Realm; + test('exports cannot be mutated', async function (assert) { + let loader = createLoader(); + let module = await loader.import<{ Person: unknown }>( + `${testRealmHref}person`, + ); + assert.throws(() => { + module.Person = 1; + }, /modules are read only/); + }); - hooks.before(async function () { - dir = dirSync(); - copySync(join(__dirname, 'cards'), dir.name); - shimExternals(virtualNetwork); + test('can get a loader used to import a specific card', async function (assert) { + let loader = createLoader(); + let module = await loader.import(`${testRealmHref}person`); + let card = module.Person; + let testingLoader = Loader.getLoaderFor(card); + assert.strictEqual(testingLoader, loader, 'the loaders are the same'); }); - setupDB(hooks, { - before: async (dbAdapter, publisher, runner) => { - loader2 = createLoader(); - realm = await createRealm({ - withWorker: true, - dir: dir.name, - fileSystem: { - 'foo.js': ` + module('with a different realm', function (hooks) { + let loader2: Loader; + let realm: Realm; + + hooks.before(async function () { + dir = dirSync(); + copySync(join(__dirname, 'cards'), dir.name); + shimExternals(virtualNetwork); + }); + + setupDB(hooks, { + before: async (dbAdapter, publisher, runner) => { + loader2 = createLoader(); + realm = await createRealm({ + withWorker: true, + dir: dir.name, + fileSystem: { + 'foo.js': ` export function checkImportMeta() { return import.meta.url; } export function myLoader() { return import.meta.loader; } `, - }, - realmURL: 'http://example.com/', - virtualNetwork, - dbAdapter, - runner, - publisher, - }); - virtualNetwork.mount(realm.handle.bind(realm)); - await realm.start(); - }, - }); - - test('supports import.meta', async function (assert) { - let { checkImportMeta, myLoader } = await loader2.import<{ - checkImportMeta: () => string; - myLoader: () => Loader; - }>('http://example.com/foo'); - assert.strictEqual(checkImportMeta(), 'http://example.com/foo'); - assert.strictEqual(myLoader(), loader2, 'the loader instance is correct'); + }, + realmURL: 'http://example.com/', + virtualNetwork, + dbAdapter, + runner, + publisher, + }); + virtualNetwork.mount(realm.handle.bind(realm)); + await realm.start(); + }, + }); + + test('supports import.meta', async function (assert) { + let { checkImportMeta, myLoader } = await loader2.import<{ + checkImportMeta: () => string; + myLoader: () => Loader; + }>('http://example.com/foo'); + assert.strictEqual(checkImportMeta(), 'http://example.com/foo'); + assert.strictEqual( + myLoader(), + loader2, + 'the loader instance is correct', + ); + }); }); }); }); diff --git a/packages/realm-server/tests/module-syntax-test.ts b/packages/realm-server/tests/module-syntax-test.ts index 9af7327424..3a7eb5a71d 100644 --- a/packages/realm-server/tests/module-syntax-test.ts +++ b/packages/realm-server/tests/module-syntax-test.ts @@ -3,33 +3,35 @@ import { ModuleSyntax } from '@cardstack/runtime-common/module-syntax'; import { baseCardRef, baseFieldRef } from '@cardstack/runtime-common'; import { testRealm } from './helpers'; +import { basename } from 'path'; import '@cardstack/runtime-common/helpers/code-equality-assertion'; -module('module-syntax', function () { - function addField(src: string, addFieldAtIndex?: number) { - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person.gts`)); - mod.addField({ - cardBeingModified: { - module: `${testRealm}dir/person.gts`, - name: 'Person', - }, - fieldName: 'age', - fieldRef: { - module: 'https://cardstack.com/base/number', - name: 'default', - }, - fieldType: 'contains', - fieldDefinitionType: 'field', - addFieldAtIndex, - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); - return mod; - } +module(basename(__filename), function () { + module('module-syntax', function () { + function addField(src: string, addFieldAtIndex?: number) { + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person.gts`)); + mod.addField({ + cardBeingModified: { + module: `${testRealm}dir/person.gts`, + name: 'Person', + }, + fieldName: 'age', + fieldRef: { + module: 'https://cardstack.com/base/number', + name: 'default', + }, + fieldType: 'contains', + fieldDefinitionType: 'field', + addFieldAtIndex, + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); + return mod; + } - test('can get the code for a card', async function (assert) { - let src = ` + test('can get the code for a card', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -41,13 +43,13 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(testRealm)); - assert.codeEqual(mod.code(), src); - }); + let mod = new ModuleSyntax(src, new URL(testRealm)); + assert.codeEqual(mod.code(), src); + }); - test('can add a field to a card', async function (assert) { - let mod = addField( - ` + test('can add a field to a card', async function (assert) { + let mod = addField( + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { @@ -57,11 +59,11 @@ module('module-syntax', function () { } } `, - ); + ); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -73,10 +75,10 @@ module('module-syntax', function () { } } `, - ); - assert.strictEqual( - mod.code(), - ` + ); + assert.strictEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -88,61 +90,63 @@ module('module-syntax', function () { } } `, - 'original code formatting is preserved', - ); - - let card = mod.possibleCardsOrFields.find((c) => c.exportName === 'Person'); - let field = card!.possibleFields.get('age'); - assert.ok(field, 'new field was added to syntax'); - assert.deepEqual( - field?.card, - { - type: 'external', - module: 'https://cardstack.com/base/number', - name: 'default', - }, - 'the field card is correct', - ); - assert.deepEqual( - field?.type, - { - type: 'external', - module: 'https://cardstack.com/base/card-api', - name: 'contains', - }, - 'the field type is correct', - ); - assert.deepEqual( - field?.decorator, - { - type: 'external', - module: 'https://cardstack.com/base/card-api', - name: 'field', - }, - 'the field decorator is correct', - ); - - // add another field which will assert that the field path is correct since - // the new field must go after this field - mod.addField({ - cardBeingModified: { - module: `${testRealm}dir/person.gts`, - name: 'Person', - }, - fieldName: 'lastName', - fieldRef: { - module: 'https://cardstack.com/base/string', - name: 'default', - }, - fieldType: 'contains', - fieldDefinitionType: 'field', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); - assert.codeEqual( - mod.code(), - ` + 'original code formatting is preserved', + ); + + let card = mod.possibleCardsOrFields.find( + (c) => c.exportName === 'Person', + ); + let field = card!.possibleFields.get('age'); + assert.ok(field, 'new field was added to syntax'); + assert.deepEqual( + field?.card, + { + type: 'external', + module: 'https://cardstack.com/base/number', + name: 'default', + }, + 'the field card is correct', + ); + assert.deepEqual( + field?.type, + { + type: 'external', + module: 'https://cardstack.com/base/card-api', + name: 'contains', + }, + 'the field type is correct', + ); + assert.deepEqual( + field?.decorator, + { + type: 'external', + module: 'https://cardstack.com/base/card-api', + name: 'field', + }, + 'the field decorator is correct', + ); + + // add another field which will assert that the field path is correct since + // the new field must go after this field + mod.addField({ + cardBeingModified: { + module: `${testRealm}dir/person.gts`, + name: 'Person', + }, + fieldName: 'lastName', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + fieldType: 'contains', + fieldDefinitionType: 'field', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); + assert.codeEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -156,13 +160,13 @@ module('module-syntax', function () { } } `, - ); - }); + ); + }); - test('added field respects indentation of previous field', async function (assert) { - // 4 space indent - let mod = addField( - ` + test('added field respects indentation of previous field', async function (assert) { + // 4 space indent + let mod = addField( + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { @@ -172,10 +176,10 @@ module('module-syntax', function () { } } `, - ); - assert.strictEqual( - mod.code(), - ` + ); + assert.strictEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -187,14 +191,14 @@ module('module-syntax', function () { } } `, - 'original code formatting is preserved', - ); - }); + 'original code formatting is preserved', + ); + }); - test('added field respects indentation of previous class member', async function (assert) { - // 2 space indent - let mod = addField( - ` + test('added field respects indentation of previous class member', async function (assert) { + // 2 space indent + let mod = addField( + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { @@ -203,10 +207,10 @@ module('module-syntax', function () { } } `, - ); - assert.strictEqual( - mod.code(), - ` + ); + assert.strictEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -217,23 +221,23 @@ module('module-syntax', function () { } } `, - 'original code formatting is preserved', - ); - }); + 'original code formatting is preserved', + ); + }); - test(`added field defaults to a 2 space indent if it's the only class member`, async function (assert) { - let mod = addField( - ` + test(`added field defaults to a 2 space indent if it's the only class member`, async function (assert) { + let mod = addField( + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { } `, - ); + ); - assert.strictEqual( - mod.code(), - ` + assert.strictEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -241,19 +245,19 @@ module('module-syntax', function () { @field age = contains(NumberField); } `, - 'original code formatting is preserved', - ); - mod = addField( - ` + 'original code formatting is preserved', + ); + mod = addField( + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { } `, - ); + ); - assert.strictEqual( - mod.code(), - ` + assert.strictEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -261,20 +265,20 @@ module('module-syntax', function () { @field age = contains(NumberField); } `, - 'original code formatting is preserved', - ); + 'original code formatting is preserved', + ); - mod = addField( - ` + mod = addField( + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef {} `, - ); + ); - assert.strictEqual( - mod.code(), - ` + assert.strictEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -282,14 +286,14 @@ module('module-syntax', function () { @field age = contains(NumberField); } `, - 'original code formatting is preserved', - ); - }); + 'original code formatting is preserved', + ); + }); - test(`added field respects the indentation of the next field when adding field at specific position`, async function (assert) { - // 4 space indent - let mod = addField( - ` + test(`added field respects the indentation of the next field when adding field at specific position`, async function (assert) { + // 4 space indent + let mod = addField( + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { @@ -299,11 +303,11 @@ module('module-syntax', function () { } } `, - 0, - ); - assert.strictEqual( - mod.code(), - ` + 0, + ); + assert.strictEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -315,12 +319,12 @@ module('module-syntax', function () { } } `, - 'original code formatting is preserved', - ); - }); + 'original code formatting is preserved', + ); + }); - test('can add a base-card field to a card', async function (assert) { - let src = ` + test('can add a base-card field to a card', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Pet extends CardDef { @@ -328,22 +332,22 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); - - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // Card we want to add to - fieldName: 'card', - fieldRef: baseCardRef, - fieldType: 'linksTo', - fieldDefinitionType: 'card', - incomingRelativeTo: undefined, - outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); - assert.codeEqual( - mod.code(), - ` + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // Card we want to add to + fieldName: 'card', + fieldRef: baseCardRef, + fieldType: 'linksTo', + fieldDefinitionType: 'card', + incomingRelativeTo: undefined, + outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card + outgoingRealmURL: undefined, + }); + + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef, linksTo } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Pet extends CardDef { @@ -351,11 +355,11 @@ module('module-syntax', function () { @field card = linksTo(CardDef); } `, - ); - }); + ); + }); - test('can add a base-field field to a card', async function (assert) { - let src = ` + test('can add a base-field field to a card', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Pet extends CardDef { @@ -363,22 +367,22 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); - - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // Card we want to add to - fieldName: 'field', - fieldRef: baseFieldRef, - fieldType: 'contains', - fieldDefinitionType: 'field', - incomingRelativeTo: undefined, - outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); - assert.codeEqual( - mod.code(), - ` + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // Card we want to add to + fieldName: 'field', + fieldRef: baseFieldRef, + fieldType: 'contains', + fieldDefinitionType: 'field', + incomingRelativeTo: undefined, + outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card + outgoingRealmURL: undefined, + }); + + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef, FieldDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Pet extends CardDef { @@ -386,11 +390,11 @@ module('module-syntax', function () { @field field = contains(FieldDef); } `, - ); - }); + ); + }); - test('can add a field to a card when the module url is relative', async function (assert) { - let src = ` + test('can add a field to a card when the module url is relative', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Pet extends CardDef { @@ -398,27 +402,27 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); - - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // Card we want to add to - fieldName: 'bestFriend', - fieldRef: { - module: '../person', - name: 'Person', - }, - fieldType: 'linksTo', - fieldDefinitionType: 'card', - incomingRelativeTo: new URL( - `http://localhost:4202/node-test/catalog-entry/1`, - ), // hypothethical catalog entry that lives at this id - outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card - outgoingRealmURL: new URL('http://localhost:4202/node-test/'), // the realm that the catalog entry lives in - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); + + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // Card we want to add to + fieldName: 'bestFriend', + fieldRef: { + module: '../person', + name: 'Person', + }, + fieldType: 'linksTo', + fieldDefinitionType: 'card', + incomingRelativeTo: new URL( + `http://localhost:4202/node-test/catalog-entry/1`, + ), // hypothethical catalog entry that lives at this id + outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card + outgoingRealmURL: new URL('http://localhost:4202/node-test/'), // the realm that the catalog entry lives in + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { Person as PersonCard } from "./person"; import { contains, field, CardDef, linksTo } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -427,11 +431,11 @@ module('module-syntax', function () { @field bestFriend = linksTo(PersonCard); } `, - ); - }); + ); + }); - test('can add a field to a card when the module url is from another realm', async function (assert) { - let src = ` + test('can add a field to a card when the module url is from another realm', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Pet extends CardDef { @@ -439,25 +443,27 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); - - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // card we want to add to - fieldName: 'bestFriend', - fieldRef: { - module: '../person', // the other realm (will be from the /test realm not the /node-test) - name: 'Person', - }, - fieldType: 'linksTo', - fieldDefinitionType: 'card', - incomingRelativeTo: new URL(`http://localhost:4202/test/catalog-entry/1`), // hypothethical catalog entry that lives at this id - outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card - outgoingRealmURL: new URL('http://localhost:4202/node-test/'), // the realm that the catalog entry lives in - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/pet.gts`)); + + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/pet`, name: 'Pet' }, // card we want to add to + fieldName: 'bestFriend', + fieldRef: { + module: '../person', // the other realm (will be from the /test realm not the /node-test) + name: 'Person', + }, + fieldType: 'linksTo', + fieldDefinitionType: 'card', + incomingRelativeTo: new URL( + `http://localhost:4202/test/catalog-entry/1`, + ), // hypothethical catalog entry that lives at this id + outgoingRelativeTo: new URL('http://localhost:4202/node-test/pet'), // outgoing card + outgoingRealmURL: new URL('http://localhost:4202/node-test/'), // the realm that the catalog entry lives in + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { Person as PersonCard } from "http://localhost:4202/test/person"; import { contains, field, CardDef, linksTo } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -466,34 +472,34 @@ module('module-syntax', function () { @field bestFriend = linksTo(PersonCard); } `, - ); - }); + ); + }); - test("can add a field to a card that doesn't have any fields", async function (assert) { - let src = ` + test("can add a field to a card that doesn't have any fields", async function (assert) { + let src = ` import { CardDef } from "https://cardstack.com/base/card-api"; export class Person extends CardDef { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'firstName', - fieldRef: { - module: 'https://cardstack.com/base/string', - name: 'default', - }, - fieldType: 'contains', - fieldDefinitionType: 'field', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'firstName', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + fieldType: 'contains', + fieldDefinitionType: 'field', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import StringField from "https://cardstack.com/base/string"; import { CardDef, field, contains } from "https://cardstack.com/base/card-api"; @@ -501,11 +507,11 @@ module('module-syntax', function () { @field firstName = contains(StringField); } `, - ); - }); + ); + }); - test('can add a field to an interior card that is the field of card that is exported', async function (assert) { - let src = ` + test('can add a field to an interior card that is the field of card that is exported', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -522,28 +528,28 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { - type: 'fieldOf', - field: 'details', - card: { module: `${testRealm}dir/person`, name: 'Person' }, - }, - fieldName: 'age', - fieldDefinitionType: 'field', - fieldRef: { - module: 'https://cardstack.com/base/number', - name: 'default', - }, - fieldType: 'contains', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { + type: 'fieldOf', + field: 'details', + card: { module: `${testRealm}dir/person`, name: 'Person' }, + }, + fieldName: 'age', + fieldDefinitionType: 'field', + fieldRef: { + module: 'https://cardstack.com/base/number', + name: 'default', + }, + fieldType: 'contains', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -561,11 +567,11 @@ module('module-syntax', function () { } } `, - ); - }); + ); + }); - test('can add a field to an interior card that is the ancestor of card that is exported', async function (assert) { - let src = ` + test('can add a field to an interior card that is the ancestor of card that is exported', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -581,27 +587,27 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { - type: 'ancestorOf', - card: { module: `${testRealm}dir/person`, name: 'FancyPerson' }, - }, - fieldName: 'age', - fieldDefinitionType: 'field', - fieldRef: { - module: 'https://cardstack.com/base/number', - name: 'default', - }, - fieldType: 'contains', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { + type: 'ancestorOf', + card: { module: `${testRealm}dir/person`, name: 'FancyPerson' }, + }, + fieldName: 'age', + fieldDefinitionType: 'field', + fieldRef: { + module: 'https://cardstack.com/base/number', + name: 'default', + }, + fieldType: 'contains', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -618,11 +624,11 @@ module('module-syntax', function () { @field favoriteColor = contains(StringField); } `, - ); - }); + ); + }); - test('can add a field to an interior card within a module that also has non card declarations', async function (assert) { - let src = ` + test('can add a field to an interior card within a module that also has non card declarations', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -641,28 +647,28 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { - type: 'fieldOf', - field: 'details', - card: { module: `${testRealm}dir/person`, name: 'Person' }, - }, - fieldName: 'age', - fieldDefinitionType: 'field', - fieldRef: { - module: 'https://cardstack.com/base/number', - name: 'default', - }, - fieldType: 'contains', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { + type: 'fieldOf', + field: 'details', + card: { module: `${testRealm}dir/person`, name: 'Person' }, + }, + fieldName: 'age', + fieldDefinitionType: 'field', + fieldRef: { + module: 'https://cardstack.com/base/number', + name: 'default', + }, + fieldType: 'contains', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import NumberField from "https://cardstack.com/base/number"; import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -682,11 +688,11 @@ module('module-syntax', function () { } } `, - ); - }); + ); + }); - test('can add a containsMany field', async function (assert) { - let src = ` + test('can add a containsMany field', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -698,24 +704,24 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'aliases', - fieldDefinitionType: 'field', - fieldRef: { - module: 'https://cardstack.com/base/string', - name: 'default', - }, - fieldType: 'containsMany', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'aliases', + fieldDefinitionType: 'field', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + fieldType: 'containsMany', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { contains, field, Component, CardDef, containsMany } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -727,47 +733,49 @@ module('module-syntax', function () { } } `, - ); - let card = mod.possibleCardsOrFields.find((c) => c.exportName === 'Person'); - let field = card!.possibleFields.get('aliases'); - assert.ok(field, 'new field was added to syntax'); - assert.deepEqual( - field?.type, - { - type: 'external', - module: 'https://cardstack.com/base/card-api', - name: 'containsMany', - }, - 'the field type is correct', - ); - }); + ); + let card = mod.possibleCardsOrFields.find( + (c) => c.exportName === 'Person', + ); + let field = card!.possibleFields.get('aliases'); + assert.ok(field, 'new field was added to syntax'); + assert.deepEqual( + field?.type, + { + type: 'external', + module: 'https://cardstack.com/base/card-api', + name: 'containsMany', + }, + 'the field type is correct', + ); + }); - test('can add a linksTo field', async function (assert) { - let src = ` + test('can add a linksTo field', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { @field firstName = contains(StringField); } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'pet', - fieldRef: { - module: `${testRealm}dir/pet`, - name: 'Pet', - }, - fieldDefinitionType: 'card', - fieldType: 'linksTo', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'pet', + fieldRef: { + module: `${testRealm}dir/pet`, + name: 'Pet', + }, + fieldDefinitionType: 'card', + fieldType: 'linksTo', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { Pet as PetCard } from "${testRealm}dir/pet"; import { contains, field, CardDef, linksTo } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -776,23 +784,25 @@ module('module-syntax', function () { @field pet = linksTo(PetCard); } `, - ); - let card = mod.possibleCardsOrFields.find((c) => c.exportName === 'Person'); - let field = card!.possibleFields.get('pet'); - assert.ok(field, 'new field was added to syntax'); - assert.deepEqual( - field?.type, - { - type: 'external', - module: 'https://cardstack.com/base/card-api', - name: 'linksTo', - }, - 'the field type is correct', - ); - }); + ); + let card = mod.possibleCardsOrFields.find( + (c) => c.exportName === 'Person', + ); + let field = card!.possibleFields.get('pet'); + assert.ok(field, 'new field was added to syntax'); + assert.deepEqual( + field?.type, + { + type: 'external', + module: 'https://cardstack.com/base/card-api', + name: 'linksTo', + }, + 'the field type is correct', + ); + }); - test('can add a linksTo field with the same type as its enclosing card', async function (assert) { - let src = ` + test('can add a linksTo field with the same type as its enclosing card', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -800,24 +810,24 @@ module('module-syntax', function () { @field firstName = contains(StringField); } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'friend', - fieldRef: { - module: `${testRealm}dir/person`, - name: 'Person', - }, - fieldType: 'linksTo', - fieldDefinitionType: 'card', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'friend', + fieldRef: { + module: `${testRealm}dir/person`, + name: 'Person', + }, + fieldType: 'linksTo', + fieldDefinitionType: 'card', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef, linksTo } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -826,23 +836,25 @@ module('module-syntax', function () { @field friend = linksTo(() => Person); } `, - ); - let card = mod.possibleCardsOrFields.find((c) => c.exportName === 'Person'); - let field = card!.possibleFields.get('friend'); - assert.ok(field, 'new field was added to syntax'); - assert.deepEqual( - field?.type, - { - type: 'external', - module: 'https://cardstack.com/base/card-api', - name: 'linksTo', - }, - 'the field type is correct', - ); - }); + ); + let card = mod.possibleCardsOrFields.find( + (c) => c.exportName === 'Person', + ); + let field = card!.possibleFields.get('friend'); + assert.ok(field, 'new field was added to syntax'); + assert.deepEqual( + field?.type, + { + type: 'external', + module: 'https://cardstack.com/base/card-api', + name: 'linksTo', + }, + 'the field type is correct', + ); + }); - test('can handle field card declaration collisions when adding field', async function (assert) { - let src = ` + test('can handle field card declaration collisions when adding field', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -853,24 +865,24 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'age', - fieldRef: { - module: 'https://cardstack.com/base/number', - name: 'default', - }, - fieldType: 'contains', - fieldDefinitionType: 'field', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'age', + fieldRef: { + module: 'https://cardstack.com/base/number', + name: 'default', + }, + fieldType: 'contains', + fieldDefinitionType: 'field', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import NumberField0 from "https://cardstack.com/base/number"; import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -882,14 +894,14 @@ module('module-syntax', function () { @field age = contains(NumberField0); } `, - ); - }); + ); + }); - // At this level, we can only see this specific module. we'll need the - // upstream caller to perform a field existence check on the card - // definition to ensure this field does not already exist in the adoption chain - test('throws when adding a field with a name the card already has', async function (assert) { - let src = ` + // At this level, we can only see this specific module. we'll need the + // upstream caller to perform a field existence check on the card + // definition to ensure this field does not already exist in the adoption chain + test('throws when adding a field with a name the card already has', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -897,32 +909,35 @@ module('module-syntax', function () { @field firstName = contains(StringField); } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - try { - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'firstName', - fieldRef: { - module: 'https://cardstack.com/base/string', - name: 'default', - }, - fieldType: 'contains', - fieldDefinitionType: 'field', - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); - throw new Error('expected error was not thrown'); - } catch (err: any) { - assert.ok( - err.message.match(/field "firstName" already exists/), - 'expected error was thrown', - ); - } - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + try { + mod.addField({ + cardBeingModified: { + module: `${testRealm}dir/person`, + name: 'Person', + }, + fieldName: 'firstName', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + fieldType: 'contains', + fieldDefinitionType: 'field', + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); + throw new Error('expected error was not thrown'); + } catch (err: any) { + assert.ok( + err.message.match(/field "firstName" already exists/), + 'expected error was thrown', + ); + } + }); - test('can remove a field from a card', async function (assert) { - let src = ` + test('can remove a field from a card', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -931,15 +946,15 @@ module('module-syntax', function () { @field lastName = contains(StringField); } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.removeField( - { module: `${testRealm}dir/person`, name: 'Person' }, - 'firstName', - ); - - assert.codeEqual( - mod.code(), - ` + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.removeField( + { module: `${testRealm}dir/person`, name: 'Person' }, + 'firstName', + ); + + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -947,10 +962,10 @@ module('module-syntax', function () { @field lastName = contains(StringField); } `, - ); - assert.strictEqual( - mod.code().trim(), - ` + ); + assert.strictEqual( + mod.code().trim(), + ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -958,16 +973,18 @@ module('module-syntax', function () { @field lastName = contains(StringField); } `.trim(), - 'original code formatting is preserved', - ); + 'original code formatting is preserved', + ); - let card = mod.possibleCardsOrFields.find((c) => c.exportName === 'Person'); - let field = card!.possibleFields.get('firstName'); - assert.strictEqual(field, undefined, 'field does not exist in syntax'); - }); + let card = mod.possibleCardsOrFields.find( + (c) => c.exportName === 'Person', + ); + let field = card!.possibleFields.get('firstName'); + assert.strictEqual(field, undefined, 'field does not exist in syntax'); + }); - test('can use remove & add a field to achieve edit in place', async function (assert) { - let src = ` + test('can use remove & add a field to achieve edit in place', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -979,30 +996,30 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - let addFieldAtIndex = mod.removeField( - { module: `${testRealm}dir/person`, name: 'Person' }, - 'artistName', - ); - - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'artistNames', - fieldRef: { - module: 'https://cardstack.com/base/string', - name: 'default', - }, - fieldType: 'containsMany', - fieldDefinitionType: 'field', - addFieldAtIndex, - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + let addFieldAtIndex = mod.removeField( + { module: `${testRealm}dir/person`, name: 'Person' }, + 'artistName', + ); + + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'artistNames', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + fieldType: 'containsMany', + fieldDefinitionType: 'field', + addFieldAtIndex, + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef, containsMany } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1013,11 +1030,11 @@ module('module-syntax', function () { @field streetName = contains(StringField); } `, - ); - }); + ); + }); - test('can use remove & add a field to achieve edit in place - when field is at the beginning', async function (assert) { - let src = ` + test('can use remove & add a field to achieve edit in place - when field is at the beginning', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1029,30 +1046,30 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - let addFieldAtIndex = mod.removeField( - { module: `${testRealm}dir/person`, name: 'Person' }, - 'firstName', - ); - - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'firstNameAdjusted', - fieldRef: { - module: 'https://cardstack.com/base/string', - name: 'default', - }, - fieldType: 'contains', - fieldDefinitionType: 'field', - addFieldAtIndex, - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + let addFieldAtIndex = mod.removeField( + { module: `${testRealm}dir/person`, name: 'Person' }, + 'firstName', + ); - assert.codeEqual( - mod.code(), - ` + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'firstNameAdjusted', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + fieldType: 'contains', + fieldDefinitionType: 'field', + addFieldAtIndex, + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); + + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1063,11 +1080,11 @@ module('module-syntax', function () { @field streetName = contains(StringField); } `, - ); - }); + ); + }); - test('can use remove & add a field to achieve edit in place - when field is at the end', async function (assert) { - let src = ` + test('can use remove & add a field to achieve edit in place - when field is at the end', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1079,30 +1096,30 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - let addFieldAtIndex = mod.removeField( - { module: `${testRealm}dir/person`, name: 'Person' }, - 'streetName', - ); - - mod.addField({ - cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, - fieldName: 'streetNameAdjusted', - fieldRef: { - module: 'https://cardstack.com/base/string', - name: 'default', - }, - fieldType: 'contains', - fieldDefinitionType: 'field', - addFieldAtIndex, - incomingRelativeTo: undefined, - outgoingRelativeTo: undefined, - outgoingRealmURL: undefined, - }); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + let addFieldAtIndex = mod.removeField( + { module: `${testRealm}dir/person`, name: 'Person' }, + 'streetName', + ); + + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'streetNameAdjusted', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + fieldType: 'contains', + fieldDefinitionType: 'field', + addFieldAtIndex, + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + }); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1113,11 +1130,11 @@ module('module-syntax', function () { @field streetNameAdjusted = contains(StringField); } `, - ); - }); + ); + }); - test('can remove the last field from a card', async function (assert) { - let src = ` + test('can remove the last field from a card', async function (assert) { + let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1126,23 +1143,23 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.removeField( - { module: `${testRealm}dir/person`, name: 'Person' }, - 'firstName', - ); + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.removeField( + { module: `${testRealm}dir/person`, name: 'Person' }, + 'firstName', + ); - assert.codeEqual( - mod.code(), - ` + assert.codeEqual( + mod.code(), + ` import { CardDef } from "https://cardstack.com/base/card-api"; export class Person extends CardDef { } `, - ); - }); + ); + }); - test('can remove a linksTo field with the same type as its enclosing card', async function (assert) { - let src = ` + test('can remove a linksTo field with the same type as its enclosing card', async function (assert) { + let src = ` import { contains, field, CardDef, linksTo } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1151,15 +1168,15 @@ module('module-syntax', function () { @field friend = linksTo(() => Friend); } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.removeField( - { module: `${testRealm}dir/person`, name: 'Friend' }, - 'friend', - ); - - assert.codeEqual( - mod.code(), - ` + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.removeField( + { module: `${testRealm}dir/person`, name: 'Friend' }, + 'friend', + ); + + assert.codeEqual( + mod.code(), + ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1167,15 +1184,17 @@ module('module-syntax', function () { @field firstName = contains(StringField); } `, - ); + ); - let card = mod.possibleCardsOrFields.find((c) => c.exportName === 'Friend'); - let field = card!.possibleFields.get('friend'); - assert.strictEqual(field, undefined, 'field does not exist in syntax'); - }); + let card = mod.possibleCardsOrFields.find( + (c) => c.exportName === 'Friend', + ); + let field = card!.possibleFields.get('friend'); + assert.strictEqual(field, undefined, 'field does not exist in syntax'); + }); - test('can remove the field of an interior card that is the ancestor of a card that is exported', async function (assert) { - let src = ` + test('can remove the field of an interior card that is the ancestor of a card that is exported', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1188,18 +1207,18 @@ module('module-syntax', function () { @field favoriteColor = contains(StringField); } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.removeField( - { - type: 'ancestorOf', - card: { module: `${testRealm}dir/person`, name: 'FancyPerson' }, - }, - 'firstName', - ); - - assert.codeEqual( - mod.code(), - ` + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.removeField( + { + type: 'ancestorOf', + card: { module: `${testRealm}dir/person`, name: 'FancyPerson' }, + }, + 'firstName', + ); + + assert.codeEqual( + mod.code(), + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1211,11 +1230,11 @@ module('module-syntax', function () { @field favoriteColor = contains(StringField); } `, - ); - }); + ); + }); - test('can remove the field of an interior card that is the field of a card that is exported', async function (assert) { - let src = ` + test('can remove the field of an interior card that is the field of a card that is exported', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1230,19 +1249,19 @@ module('module-syntax', function () { @field details = contains(Details); } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - mod.removeField( - { - type: 'fieldOf', - field: 'details', - card: { module: `${testRealm}dir/person`, name: 'Person' }, - }, - 'nickName', - ); - - assert.codeEqual( - mod.code(), - ` + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.removeField( + { + type: 'fieldOf', + field: 'details', + card: { module: `${testRealm}dir/person`, name: 'Person' }, + }, + 'nickName', + ); + + assert.codeEqual( + mod.code(), + ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1256,11 +1275,11 @@ module('module-syntax', function () { @field details = contains(Details); } `, - ); - }); + ); + }); - test('throws when field to remove does not actually exist', async function (assert) { - let src = ` + test('throws when field to remove does not actually exist', async function (assert) { + let src = ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; import StringField from "https://cardstack.com/base/string"; @@ -1269,18 +1288,19 @@ module('module-syntax', function () { } `; - let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); - try { - mod.removeField( - { module: `${testRealm}dir/person`, name: 'Person' }, - 'foo', - ); - throw new Error('expected error was not thrown'); - } catch (err: any) { - assert.ok( - err.message.match(/field "foo" does not exist/), - 'expected error was thrown', - ); - } + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + try { + mod.removeField( + { module: `${testRealm}dir/person`, name: 'Person' }, + 'foo', + ); + throw new Error('expected error was not thrown'); + } catch (err: any) { + assert.ok( + err.message.match(/field "foo" does not exist/), + 'expected error was thrown', + ); + } + }); }); }); diff --git a/packages/realm-server/tests/queue-test.ts b/packages/realm-server/tests/queue-test.ts index 1ecd81978d..d55a27e436 100644 --- a/packages/realm-server/tests/queue-test.ts +++ b/packages/realm-server/tests/queue-test.ts @@ -13,171 +13,179 @@ import { } from '@cardstack/runtime-common'; import { runSharedTest } from '@cardstack/runtime-common/helpers'; import queueTests from '@cardstack/runtime-common/tests/queue-test'; - -module('queue', function (hooks) { - let publisher: QueuePublisher; - let runner: QueueRunner; - let adapter: PgAdapter; - - hooks.beforeEach(async function () { - prepareTestDB(); - adapter = new PgAdapter({ autoMigrate: true }); - publisher = new PgQueuePublisher(adapter); - runner = new PgQueueRunner(adapter, 'q1', 2); - await runner.start(); - }); - - hooks.afterEach(async function () { - await runner.destroy(); - await publisher.destroy(); - }); - - test('it can run a job', async function (assert) { - await runSharedTest(queueTests, assert, { runner, publisher }); - }); - - test(`a job can throw an exception`, async function (assert) { - await runSharedTest(queueTests, assert, { runner, publisher }); - }); - - test('jobs are processed serially within a particular queue', async function (assert) { - await runSharedTest(queueTests, assert, { runner, publisher }); - }); - - test('worker stops waiting for job after its been running longer than max time-out', async function (assert) { - let events: string[] = []; - let runs = 0; - let logJob = async () => { - let me = runs; - events.push(`job${me} start`); - if (runs++ === 0) { - await new Promise((r) => setTimeout(r, 3000)); - } - events.push(`job${me} finish`); - return me; - }; - - runner.register('logJob', logJob); - - let job = await publisher.publish('logJob', 'log-group', 1, null); - - try { - await job.done; - throw new Error(`expected timeout to be thrown`); - } catch (error: any) { - assert.strictEqual( - error.message, - 'Timed-out after 2s waiting for job 1 to complete', - ); - } - }); - - // Concurrency control using different queues is only supported in pg-queue, - // so these are not tests that are shared with the browser queue implementation. - module('multiple queue clients', function (nestedHooks) { - let runner2: QueueRunner; - let adapter2: PgAdapter; - nestedHooks.beforeEach(async function () { - adapter2 = new PgAdapter({ autoMigrate: true }); - runner2 = new PgQueueRunner(adapter2, 'q2'); - await runner2.start(); - - // Because we need tight timing control for this test, we don't want any - // concurrent migrations and their retries altering the timing. This - // ensures both adapters have gotten fully past that and are quiescent. - await adapter.execute('select 1'); - await adapter2.execute('select 1'); +import { basename } from 'path'; + +module(basename(__filename), function () { + module('queue', function (hooks) { + let publisher: QueuePublisher; + let runner: QueueRunner; + let adapter: PgAdapter; + + hooks.beforeEach(async function () { + prepareTestDB(); + adapter = new PgAdapter({ autoMigrate: true }); + publisher = new PgQueuePublisher(adapter); + runner = new PgQueueRunner(adapter, 'q1', 2); + await runner.start(); }); - nestedHooks.afterEach(async function () { - await runner2.destroy(); + hooks.afterEach(async function () { + await runner.destroy(); + await publisher.destroy(); }); - test('jobs in different concurrency groups can run in parallel', async function (assert) { - let events: string[] = []; - - let logJob = async (jobNum: number) => { - events.push(`job${jobNum} start`); - await new Promise((r) => setTimeout(r, 500)); - events.push(`job${jobNum} finish`); - }; - - runner.register('logJob', logJob); - runner2.register('logJob', logJob); - - let promiseForJob1 = publisher.publish('logJob', 'log-group', 5000, 1); - // start the 2nd job before the first job finishes - await new Promise((r) => setTimeout(r, 100)); - let promiseForJob2 = publisher.publish('logJob', 'other-group', 5000, 2); - let [job1, job2] = await Promise.all([promiseForJob1, promiseForJob2]); - - await Promise.all([job1.done, job2.done]); - assert.deepEqual(events, [ - 'job1 start', - 'job2 start', - 'job1 finish', - 'job2 finish', - ]); + test('it can run a job', async function (assert) { + await runSharedTest(queueTests, assert, { runner, publisher }); }); - test('jobs are processed serially within a particular queue across different queue clients', async function (assert) { - let events: string[] = []; - - let logJob = async (jobNum: number) => { - events.push(`job${jobNum} start`); - await new Promise((r) => setTimeout(r, 500)); - events.push(`job${jobNum} finish`); - }; + test(`a job can throw an exception`, async function (assert) { + await runSharedTest(queueTests, assert, { runner, publisher }); + }); - runner.register('logJob', logJob); - runner2.register('logJob', logJob); - - let promiseForJob1 = publisher.publish('logJob', 'log-group', 5000, 1); - // start the 2nd job before the first job finishes - await new Promise((r) => setTimeout(r, 100)); - let promiseForJob2 = publisher.publish('logJob', 'log-group', 5000, 2); - let [job1, job2] = await Promise.all([promiseForJob1, promiseForJob2]); - - await Promise.all([job1.done, job2.done]); - assert.deepEqual(events, [ - 'job1 start', - 'job1 finish', - 'job2 start', - 'job2 finish', - ]); + test('jobs are processed serially within a particular queue', async function (assert) { + await runSharedTest(queueTests, assert, { runner, publisher }); }); - test('job can timeout; timed out job is picked up by another worker', async function (assert) { + test('worker stops waiting for job after its been running longer than max time-out', async function (assert) { let events: string[] = []; let runs = 0; let logJob = async () => { let me = runs; events.push(`job${me} start`); if (runs++ === 0) { - await new Promise((r) => setTimeout(r, 2000)); + await new Promise((r) => setTimeout(r, 3000)); } events.push(`job${me} finish`); return me; }; runner.register('logJob', logJob); - runner2.register('logJob', logJob); let job = await publisher.publish('logJob', 'log-group', 1, null); - // just after our job has timed out, kick the queue so that another worker - // will notice it. Otherwise we'd be stuck until the polling comes around. - await new Promise((r) => setTimeout(r, 1100)); - await adapter.execute('NOTIFY jobs'); - - let result = await job.done; - - assert.strictEqual(result, 1); + try { + await job.done; + throw new Error(`expected timeout to be thrown`); + } catch (error: any) { + assert.strictEqual( + error.message, + 'Timed-out after 2s waiting for job 1 to complete', + ); + } + }); - // at this point the long-running first job is still stuck. it will - // eventually also log "job0 finish", but that is absorbed by our test - // afterEach - assert.deepEqual(events, ['job0 start', 'job1 start', 'job1 finish']); + // Concurrency control using different queues is only supported in pg-queue, + // so these are not tests that are shared with the browser queue implementation. + module('multiple queue clients', function (nestedHooks) { + let runner2: QueueRunner; + let adapter2: PgAdapter; + nestedHooks.beforeEach(async function () { + adapter2 = new PgAdapter({ autoMigrate: true }); + runner2 = new PgQueueRunner(adapter2, 'q2'); + await runner2.start(); + + // Because we need tight timing control for this test, we don't want any + // concurrent migrations and their retries altering the timing. This + // ensures both adapters have gotten fully past that and are quiescent. + await adapter.execute('select 1'); + await adapter2.execute('select 1'); + }); + + nestedHooks.afterEach(async function () { + await runner2.destroy(); + }); + + test('jobs in different concurrency groups can run in parallel', async function (assert) { + let events: string[] = []; + + let logJob = async (jobNum: number) => { + events.push(`job${jobNum} start`); + await new Promise((r) => setTimeout(r, 500)); + events.push(`job${jobNum} finish`); + }; + + runner.register('logJob', logJob); + runner2.register('logJob', logJob); + + let promiseForJob1 = publisher.publish('logJob', 'log-group', 5000, 1); + // start the 2nd job before the first job finishes + await new Promise((r) => setTimeout(r, 100)); + let promiseForJob2 = publisher.publish( + 'logJob', + 'other-group', + 5000, + 2, + ); + let [job1, job2] = await Promise.all([promiseForJob1, promiseForJob2]); + + await Promise.all([job1.done, job2.done]); + assert.deepEqual(events, [ + 'job1 start', + 'job2 start', + 'job1 finish', + 'job2 finish', + ]); + }); + + test('jobs are processed serially within a particular queue across different queue clients', async function (assert) { + let events: string[] = []; + + let logJob = async (jobNum: number) => { + events.push(`job${jobNum} start`); + await new Promise((r) => setTimeout(r, 500)); + events.push(`job${jobNum} finish`); + }; + + runner.register('logJob', logJob); + runner2.register('logJob', logJob); + + let promiseForJob1 = publisher.publish('logJob', 'log-group', 5000, 1); + // start the 2nd job before the first job finishes + await new Promise((r) => setTimeout(r, 100)); + let promiseForJob2 = publisher.publish('logJob', 'log-group', 5000, 2); + let [job1, job2] = await Promise.all([promiseForJob1, promiseForJob2]); + + await Promise.all([job1.done, job2.done]); + assert.deepEqual(events, [ + 'job1 start', + 'job1 finish', + 'job2 start', + 'job2 finish', + ]); + }); + + test('job can timeout; timed out job is picked up by another worker', async function (assert) { + let events: string[] = []; + let runs = 0; + let logJob = async () => { + let me = runs; + events.push(`job${me} start`); + if (runs++ === 0) { + await new Promise((r) => setTimeout(r, 2000)); + } + events.push(`job${me} finish`); + return me; + }; + + runner.register('logJob', logJob); + runner2.register('logJob', logJob); + + let job = await publisher.publish('logJob', 'log-group', 1, null); + + // just after our job has timed out, kick the queue so that another worker + // will notice it. Otherwise we'd be stuck until the polling comes around. + await new Promise((r) => setTimeout(r, 1100)); + await adapter.execute('NOTIFY jobs'); + + let result = await job.done; + + assert.strictEqual(result, 1); + + // at this point the long-running first job is still stuck. it will + // eventually also log "job0 finish", but that is absorbed by our test + // afterEach + assert.deepEqual(events, ['job0 start', 'job1 start', 'job1 finish']); + }); }); }); }); diff --git a/packages/realm-server/tests/realm-server-test.ts b/packages/realm-server/tests/realm-server-test.ts index f892a1f5ca..3bf4ec50ed 100644 --- a/packages/realm-server/tests/realm-server-test.ts +++ b/packages/realm-server/tests/realm-server-test.ts @@ -1,6 +1,6 @@ import { module, test } from 'qunit'; import supertest, { Test, SuperTest } from 'supertest'; -import { join, resolve } from 'path'; +import { join, resolve, basename } from 'path'; import { Server } from 'http'; import { dirSync, setGracefulCleanup, type DirResult } from 'tmp'; import { validate as uuidValidate } from 'uuid'; @@ -100,799 +100,853 @@ let createJWT = ( return realm.createJWT({ user, realm: realm.url, permissions }, '7d'); }; -module('Realm Server', function (hooks) { - async function expectEvent({ - assert, - expected, - expectedNumberOfEvents, - onEvents, - callback, - }: { - assert: Assert; - expected?: Record[]; - expectedNumberOfEvents?: number; - onEvents?: (events: Record[]) => void; - callback: () => Promise; - }): Promise { - let defer = new Deferred[]>(); - let events: Record[] = []; - let maybeNumEvents = expected?.length ?? expectedNumberOfEvents; - if (maybeNumEvents == null) { - throw new Error( - `expectEvent() must specify either 'expected' or 'expectedNumberOfEvents'`, - ); - } - let numEvents = maybeNumEvents; - let es = new eventSource(`${testRealmHref}_message`); - es.addEventListener('index', (ev: MessageEvent) => { - events.push(JSON.parse(ev.data)); - if (events.length >= numEvents) { - defer.fulfill(events); +module(basename(__filename), function () { + module('Realm Server', function (hooks) { + async function expectEvent({ + assert, + expected, + expectedNumberOfEvents, + onEvents, + callback, + }: { + assert: Assert; + expected?: Record[]; + expectedNumberOfEvents?: number; + onEvents?: (events: Record[]) => void; + callback: () => Promise; + }): Promise { + let defer = new Deferred[]>(); + let events: Record[] = []; + let maybeNumEvents = expected?.length ?? expectedNumberOfEvents; + if (maybeNumEvents == null) { + throw new Error( + `expectEvent() must specify either 'expected' or 'expectedNumberOfEvents'`, + ); } - }); - es.onerror = (err: Event) => defer.reject(err); - let timeout = setTimeout(() => { - defer.reject( - new Error( - `expectEvent timed out, saw events ${JSON.stringify(events)}`, - ), - ); - }, 5000); - await new Promise((resolve) => es.addEventListener('open', resolve)); - let result = await callback(); - let actualEvents = await defer.promise; - if (expected) { - assert.deepEqual(actualEvents, expected); - } - if (onEvents) { - onEvents(actualEvents); - } - clearTimeout(timeout); - es.close(); - return result; - } - - let testRealm: Realm; - let testRealmPath: string; - let testRealmHttpServer: Server; - let request: SuperTest; - let dir: DirResult; - let dbAdapter: PgAdapter; - - function setupPermissionedRealm( - hooks: NestedHooks, - permissions: RealmPermissions, - fileSystem?: Record, - ) { - setupDB(hooks, { - beforeEach: async (_dbAdapter, publisher, runner) => { - dbAdapter = _dbAdapter; - dir = dirSync(); - let testRealmDir = join(dir.name, 'realm_server_1', 'test'); - ensureDirSync(testRealmDir); - // If a fileSystem is provided, use it to populate the test realm, otherwise copy the default cards - if (!fileSystem) { - copySync(join(__dirname, 'cards'), testRealmDir); + let numEvents = maybeNumEvents; + let es = new eventSource(`${testRealmHref}_message`); + es.addEventListener('index', (ev: MessageEvent) => { + events.push(JSON.parse(ev.data)); + if (events.length >= numEvents) { + defer.fulfill(events); } - let virtualNetwork = createVirtualNetwork(); - ({ - testRealm, - testRealmHttpServer, - testRealmDir: testRealmPath, - } = await runTestRealmServer({ - virtualNetwork, - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_1'), - realmURL: testRealmURL, - permissions, - dbAdapter: _dbAdapter, - runner, - publisher, - matrixURL, - fileSystem, - })); + }); + es.onerror = (err: Event) => defer.reject(err); + let timeout = setTimeout(() => { + defer.reject( + new Error( + `expectEvent timed out, saw events ${JSON.stringify(events)}`, + ), + ); + }, 5000); + await new Promise((resolve) => es.addEventListener('open', resolve)); + let result = await callback(); + let actualEvents = await defer.promise; + if (expected) { + assert.deepEqual(actualEvents, expected); + } + if (onEvents) { + onEvents(actualEvents); + } + clearTimeout(timeout); + es.close(); + return result; + } - request = supertest(testRealmHttpServer); - }, - }); - } + let testRealm: Realm; + let testRealmPath: string; + let testRealmHttpServer: Server; + let request: SuperTest; + let dir: DirResult; + let dbAdapter: PgAdapter; - let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); + function setupPermissionedRealm( + hooks: NestedHooks, + permissions: RealmPermissions, + fileSystem?: Record, + ) { + setupDB(hooks, { + beforeEach: async (_dbAdapter, publisher, runner) => { + dbAdapter = _dbAdapter; + dir = dirSync(); + let testRealmDir = join(dir.name, 'realm_server_1', 'test'); + ensureDirSync(testRealmDir); + // If a fileSystem is provided, use it to populate the test realm, otherwise copy the default cards + if (!fileSystem) { + copySync(join(__dirname, 'cards'), testRealmDir); + } + let virtualNetwork = createVirtualNetwork(); + ({ + testRealm, + testRealmHttpServer, + testRealmDir: testRealmPath, + } = await runTestRealmServer({ + virtualNetwork, + testRealmDir, + realmsRootPath: join(dir.name, 'realm_server_1'), + realmURL: testRealmURL, + permissions, + dbAdapter: _dbAdapter, + runner, + publisher, + matrixURL, + fileSystem, + })); + + request = supertest(testRealmHttpServer); + }, + }); + } - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); + let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); - setupBaseRealmServer(hooks, virtualNetwork, matrixURL); + setupCardLogs( + hooks, + async () => await loader.import(`${baseRealm.url}card-api`), + ); - hooks.beforeEach(async function () { - dir = dirSync(); - copySync(join(__dirname, 'cards'), dir.name); - }); + setupBaseRealmServer(hooks, virtualNetwork, matrixURL); - hooks.afterEach(async function () { - await closeServer(testRealmHttpServer); - resetCatalogRealms(); - }); + hooks.beforeEach(async function () { + dir = dirSync(); + copySync(join(__dirname, 'cards'), dir.name); + }); - module('mtimes requests', function (hooks) { - setupPermissionedRealm(hooks, { - mary: ['read'], + hooks.afterEach(async function () { + await closeServer(testRealmHttpServer); + resetCatalogRealms(); }); - test('non read permission GET /_mtimes', async function (assert) { - let response = await request - .get('/_mtimes') - .set('Accept', 'application/vnd.api+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-mary')}`); + module('mtimes requests', function (hooks) { + setupPermissionedRealm(hooks, { + mary: ['read'], + }); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + test('non read permission GET /_mtimes', async function (assert) { + let response = await request + .get('/_mtimes') + .set('Accept', 'application/vnd.api+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-mary')}`); - test('read permission GET /_mtimes', async function (assert) { - let expectedMtimes = mtimes(testRealmPath, testRealmURL); - delete expectedMtimes[`${testRealmURL}.realm.json`]; + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); - let response = await request - .get('/_mtimes') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', ['read'])}`, - ); + test('read permission GET /_mtimes', async function (assert) { + let expectedMtimes = mtimes(testRealmPath, testRealmURL); + delete expectedMtimes[`${testRealmURL}.realm.json`]; - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'mtimes', - id: testRealmHref, - attributes: { - mtimes: expectedMtimes, + let response = await request + .get('/_mtimes') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', ['read'])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + type: 'mtimes', + id: testRealmHref, + attributes: { + mtimes: expectedMtimes, + }, }, }, - }, - 'mtimes response is correct', - ); + 'mtimes response is correct', + ); + }); }); - }); - module('permissions requests', function (hooks) { - setupPermissionedRealm(hooks, { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - }); + module('permissions requests', function (hooks) { + setupPermissionedRealm(hooks, { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], + }); - test('non-owner GET /_permissions', async function (assert) { - let response = await request - .get('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'bob', ['read', 'write'])}`, - ); + test('non-owner GET /_permissions', async function (assert) { + let response = await request + .get('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'bob', ['read', 'write'])}`, + ); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); - test('realm-owner GET /_permissions', async function (assert) { - let response = await request - .get('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ); + test('realm-owner GET /_permissions', async function (assert) { + let response = await request + .get('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'permissions', - id: testRealmHref, - attributes: { - permissions: { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + type: 'permissions', + id: testRealmHref, + attributes: { + permissions: { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], + }, }, }, }, - }, - 'permissions response is correct', - ); - }); + 'permissions response is correct', + ); + }); - test('non-owner PATCH /_permissions', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'bob', ['read', 'write'])}`, - ) - .send({ - data: { - id: testRealmHref, - type: 'permissions', - attributes: { - permissions: { - mango: ['read'], + test('non-owner PATCH /_permissions', async function (assert) { + let response = await request + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'bob', ['read', 'write'])}`, + ) + .send({ + data: { + id: testRealmHref, + type: 'permissions', + attributes: { + permissions: { + mango: ['read'], + }, }, }, - }, - }); + }); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - }, - 'permissions did not change', - ); - }); + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], + }, + 'permissions did not change', + ); + }); - test('realm-owner PATCH /_permissions', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ) - .send({ - data: { - id: testRealmHref, - type: 'permissions', - attributes: { - permissions: { - mango: ['read'], + test('realm-owner PATCH /_permissions', async function (assert) { + let response = await request + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ) + .send({ + data: { + id: testRealmHref, + type: 'permissions', + attributes: { + permissions: { + mango: ['read'], + }, }, }, - }, - }); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'permissions', - id: testRealmHref, - attributes: { - permissions: { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - mango: ['read'], + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + type: 'permissions', + id: testRealmHref, + attributes: { + permissions: { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], + mango: ['read'], + }, }, }, }, - }, - 'permissions response is correct', - ); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - mango: ['read'], - }, - 'permissions are correct', - ); - }); + 'permissions response is correct', + ); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], + mango: ['read'], + }, + 'permissions are correct', + ); + }); - test('remove permissions from PATCH /_permissions using empty array', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ) - .send({ - data: { - id: testRealmHref, - type: 'permissions', - attributes: { - permissions: { - bob: [], + test('remove permissions from PATCH /_permissions using empty array', async function (assert) { + let response = await request + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ) + .send({ + data: { + id: testRealmHref, + type: 'permissions', + attributes: { + permissions: { + bob: [], + }, }, }, - }, - }); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'permissions', - id: testRealmHref, - attributes: { - permissions: { - mary: ['read', 'write', 'realm-owner'], + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + type: 'permissions', + id: testRealmHref, + attributes: { + permissions: { + mary: ['read', 'write', 'realm-owner'], + }, }, }, }, - }, - 'permissions response is correct', - ); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - }, - 'permissions are correct', - ); - }); - - test('remove permissions from PATCH /_permissions using null', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ) - .send({ - data: { - id: testRealmHref, - type: 'permissions', - attributes: { - permissions: { - bob: null, - }, - }, + 'permissions response is correct', + ); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], }, - }); + 'permissions are correct', + ); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'permissions', - id: testRealmHref, - attributes: { - permissions: { - mary: ['read', 'write', 'realm-owner'], + test('remove permissions from PATCH /_permissions using null', async function (assert) { + let response = await request + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ) + .send({ + data: { + id: testRealmHref, + type: 'permissions', + attributes: { + permissions: { + bob: null, + }, }, }, - }, - }, - 'permissions response is correct', - ); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - }, - 'permissions are correct', - ); - }); + }); - test('cannot remove realm-owner permissions from PATCH /_permissions', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ) - .send({ - data: { - id: testRealmHref, - type: 'permissions', - attributes: { - permissions: { - mary: [], + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + type: 'permissions', + id: testRealmHref, + attributes: { + permissions: { + mary: ['read', 'write', 'realm-owner'], + }, }, }, }, - }); - - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - }, - 'permissions are correct', - ); - }); - - test('cannot add realm-owner permissions from PATCH /_permissions', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ) - .send({ - data: { - id: testRealmHref, - type: 'permissions', - attributes: { - permissions: { - mango: ['realm-owner', 'write', 'read'], - }, - }, + 'permissions response is correct', + ); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], }, - }); - - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - }, - 'permissions are correct', - ); - }); - - test('receive 400 error on invalid JSON API', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ) - .send({ - data: { nothing: null }, - }); - - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - }, - 'permissions are correct', - ); - }); + 'permissions are correct', + ); + }); - test('receive 400 error on invalid permissions shape', async function (assert) { - let response = await request - .patch('/_permissions') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'mary', [ - 'read', - 'write', - 'realm-owner', - ])}`, - ) - .send({ - data: { - id: testRealmHref, - type: 'permissions', - attributes: { - permissions: { - larry: { read: true }, + test('cannot remove realm-owner permissions from PATCH /_permissions', async function (assert) { + let response = await request + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ) + .send({ + data: { + id: testRealmHref, + type: 'permissions', + attributes: { + permissions: { + mary: [], + }, }, }, - }, - }); - - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); - assert.deepEqual( - permissions, - { - mary: ['read', 'write', 'realm-owner'], - bob: ['read', 'write'], - }, - 'permissions are correct', - ); - }); - }); + }); - module('card GET request', function (_hooks) { - module('public readable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read'], + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], + }, + 'permissions are correct', + ); }); - test('serves the request', async function (assert) { + test('cannot add realm-owner permissions from PATCH /_permissions', async function (assert) { let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json'); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.ok(json.data.meta.lastModified, 'lastModified exists'); - delete json.data.meta.lastModified; - delete json.data.meta.resourceCreatedAt; - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - assert.deepEqual(json, { - data: { - id: `${testRealmHref}person-1`, - type: 'card', - attributes: { - title: 'Mango', - firstName: 'Mango', - description: null, - thumbnailURL: null, - }, - meta: { - adoptsFrom: { - module: `./person`, - name: 'Person', + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ) + .send({ + data: { + id: testRealmHref, + type: 'permissions', + attributes: { + permissions: { + mango: ['realm-owner', 'write', 'read'], + }, }, - realmInfo: testRealmInfo, - realmURL: testRealmURL.href, - }, - links: { - self: `${testRealmHref}person-1`, }, + }); + + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], }, - }); + 'permissions are correct', + ); }); - test('serves a card error request without last known good state', async function (assert) { + test('receive 400 error on invalid JSON API', async function (assert) { let response = await request - .get('/missing-link') - .set('Accept', 'application/vnd.card+json'); + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ) + .send({ + data: { nothing: null }, + }); - assert.strictEqual(response.status, 500, 'HTTP 500 status'); - let json = response.body; - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], + }, + 'permissions are correct', ); + }); + + test('receive 400 error on invalid permissions shape', async function (assert) { + let response = await request + .patch('/_permissions') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'mary', [ + 'read', + 'write', + 'realm-owner', + ])}`, + ) + .send({ + data: { + id: testRealmHref, + type: 'permissions', + attributes: { + permissions: { + larry: { read: true }, + }, + }, + }, + }); - let errorBody = json.errors[0]; - assert.ok(errorBody.meta.stack.includes('at CurrentRun.visitFile')); - delete errorBody.meta.stack; - assert.deepEqual(errorBody, { - id: `${testRealmHref}missing-link`, - status: 404, - title: 'Not Found', - message: `missing file ${testRealmHref}does-not-exist.json`, - realm: testRealmHref, - meta: { - lastKnownGoodHtml: null, - scopedCssUrls: [], + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let permissions = await fetchUserPermissions(dbAdapter, testRealmURL); + assert.deepEqual( + permissions, + { + mary: ['read', 'write', 'realm-owner'], + bob: ['read', 'write'], }, - }); + 'permissions are correct', + ); }); }); - // using public writable realm to make it easy for test setup for the error tests - module('public writable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], - }); + module('card GET request', function (_hooks) { + module('public readable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read'], + }); - test('serves a card error request with last known good state', async function (assert) { - await request - .patch('/hassan') - .send({ + test('serves the request', async function (assert) { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.ok(json.data.meta.lastModified, 'lastModified exists'); + delete json.data.meta.lastModified; + delete json.data.meta.resourceCreatedAt; + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + assert.deepEqual(json, { data: { + id: `${testRealmHref}person-1`, type: 'card', - relationships: { - friend: { - links: { - self: './does-not-exist', - }, - }, + attributes: { + title: 'Mango', + firstName: 'Mango', + description: null, + thumbnailURL: null, }, meta: { adoptsFrom: { - module: './friend.gts', - name: 'Friend', + module: `./person`, + name: 'Person', }, + realmInfo: testRealmInfo, + realmURL: testRealmURL.href, + }, + links: { + self: `${testRealmHref}person-1`, }, }, - }) - .set('Accept', 'application/vnd.card+json'); - - let response = await request - .get('/hassan') - .set('Accept', 'application/vnd.card+json'); + }); + }); - assert.strictEqual(response.status, 500, 'HTTP 500 status'); - let json = response.body; - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); + test('serves a card error request without last known good state', async function (assert) { + let response = await request + .get('/missing-link') + .set('Accept', 'application/vnd.card+json'); - let errorBody = json.errors[0]; - let lastKnownGoodHtml = cleanWhiteSpace( - errorBody.meta.lastKnownGoodHtml, - ); + assert.strictEqual(response.status, 500, 'HTTP 500 status'); + let json = response.body; + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); - assert.ok(errorBody.meta.stack.includes('at CurrentRun.visitFile')); - assert.strictEqual(errorBody.status, 404); - assert.strictEqual(errorBody.title, 'Not Found'); - assert.strictEqual( - errorBody.message, - `missing file ${testRealmHref}does-not-exist.json`, - ); - assert.ok(lastKnownGoodHtml.includes('Hassan has a friend')); - assert.ok(lastKnownGoodHtml.includes('Jade')); - let scopedCssUrls = errorBody.meta.scopedCssUrls; - assertScopedCssUrlsContain( - assert, - scopedCssUrls, - cardDefModuleDependencies, - ); + let errorBody = json.errors[0]; + assert.ok(errorBody.meta.stack.includes('at CurrentRun.visitFile')); + delete errorBody.meta.stack; + assert.deepEqual(errorBody, { + id: `${testRealmHref}missing-link`, + status: 404, + title: 'Not Found', + message: `missing file ${testRealmHref}does-not-exist.json`, + realm: testRealmHref, + meta: { + lastKnownGoodHtml: null, + scopedCssUrls: [], + }, + }); + }); }); - }); - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read'], - }); + // using public writable realm to make it easy for test setup for the error tests + module('public writable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], + }); - test('401 with invalid JWT', async function (assert) { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer invalid-token`); + test('serves a card error request with last known good state', async function (assert) { + await request + .patch('/hassan') + .send({ + data: { + type: 'card', + relationships: { + friend: { + links: { + self: './does-not-exist', + }, + }, + }, + meta: { + adoptsFrom: { + module: './friend.gts', + name: 'Friend', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - undefined, - 'realm is not public readable', - ); - }); + let response = await request + .get('/hassan') + .set('Accept', 'application/vnd.card+json'); - test('401 without a JWT', async function (assert) { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json'); // no Authorization header + assert.strictEqual(response.status, 500, 'HTTP 500 status'); + let json = response.body; + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - undefined, - 'realm is not public readable', - ); + let errorBody = json.errors[0]; + let lastKnownGoodHtml = cleanWhiteSpace( + errorBody.meta.lastKnownGoodHtml, + ); + + assert.ok(errorBody.meta.stack.includes('at CurrentRun.visitFile')); + assert.strictEqual(errorBody.status, 404); + assert.strictEqual(errorBody.title, 'Not Found'); + assert.strictEqual( + errorBody.message, + `missing file ${testRealmHref}does-not-exist.json`, + ); + assert.ok(lastKnownGoodHtml.includes('Hassan has a friend')); + assert.ok(lastKnownGoodHtml.includes('Jade')); + let scopedCssUrls = errorBody.meta.scopedCssUrls; + assertScopedCssUrlsContain( + assert, + scopedCssUrls, + cardDefModuleDependencies, + ); + }); }); - test('403 without permission', async function (assert) { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read'], + }); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - undefined, - 'realm is not public readable', - ); - }); + test('401 with invalid JWT', async function (assert) { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer invalid-token`); - test('200 with permission', async function (assert) { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read'])}`, + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + undefined, + 'realm is not public readable', ); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - undefined, - 'realm is not public readable', - ); + test('401 without a JWT', async function (assert) { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json'); // no Authorization header + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + undefined, + 'realm is not public readable', + ); + }); + + test('403 without permission', async function (assert) { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + undefined, + 'realm is not public readable', + ); + }); + + test('200 with permission', async function (assert) { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read'])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + undefined, + 'realm is not public readable', + ); + }); }); }); - }); - module('card POST request', function (_hooks) { - module('public writable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], - }); + module('card POST request', function (_hooks) { + module('public writable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], + }); - test('serves the request', async function (assert) { - assert.expect(9); - let id: string | undefined; - let response = await expectEvent({ - assert, - expectedNumberOfEvents: 2, - onEvents: ([_, event]) => { - if (event.type === 'incremental') { - id = event.invalidations[0].split('/').pop()!; - assert.true(uuidValidate(id!), 'card identifier is a UUID'); - assert.strictEqual( - event.invalidations[0], - `${testRealmURL}CardDef/${id}`, - ); - } else { - assert.ok( - false, - `expect to receive 'incremental' event, but saw ${JSON.stringify( - event, - )} `, - ); - } - }, - callback: async () => { - return await request - .post('/') - .send({ + test('serves the request', async function (assert) { + assert.expect(9); + let id: string | undefined; + let response = await expectEvent({ + assert, + expectedNumberOfEvents: 2, + onEvents: ([_, event]) => { + if (event.type === 'incremental') { + id = event.invalidations[0].split('/').pop()!; + assert.true(uuidValidate(id!), 'card identifier is a UUID'); + assert.strictEqual( + event.invalidations[0], + `${testRealmURL}CardDef/${id}`, + ); + } else { + assert.ok( + false, + `expect to receive 'incremental' event, but saw ${JSON.stringify( + event, + )} `, + ); + } + }, + callback: async () => { + return await request + .post('/') + .send({ + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/card-api', + name: 'CardDef', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); + }, + }); + if (!id) { + assert.ok(false, 'new card identifier was undefined'); + } + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let json = response.body; + + if (isSingleCardDocument(json)) { + assert.strictEqual( + json.data.id, + `${testRealmHref}CardDef/${id}`, + 'the id is correct', + ); + assert.ok(json.data.meta.lastModified, 'lastModified is populated'); + let cardFile = join( + dir.name, + 'realm_server_1', + 'test', + 'CardDef', + `${id}.json`, + ); + assert.ok(existsSync(cardFile), 'card json exists'); + let card = readJSONSync(cardFile); + assert.deepEqual( + card, + { data: { + attributes: { + title: null, + description: null, + thumbnailURL: null, + }, type: 'card', - attributes: {}, meta: { adoptsFrom: { module: 'https://cardstack.com/base/card-api', @@ -900,52 +954,69 @@ module('Realm Server', function (hooks) { }, }, }, - }) - .set('Accept', 'application/vnd.card+json'); - }, + }, + 'file contents are correct', + ); + } else { + assert.ok(false, 'response body is not a card document'); + } }); - if (!id) { - assert.ok(false, 'new card identifier was undefined'); - } - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let json = response.body; + }); - if (isSingleCardDocument(json)) { - assert.strictEqual( - json.data.id, - `${testRealmHref}CardDef/${id}`, - 'the id is correct', - ); - assert.ok(json.data.meta.lastModified, 'lastModified is populated'); - let cardFile = join( - dir.name, - 'realm_server_1', - 'test', - 'CardDef', - `${id}.json`, - ); - assert.ok(existsSync(cardFile), 'card json exists'); - let card = readJSONSync(cardFile); - assert.deepEqual( - card, - { + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read', 'write'], + }); + + test('401 with invalid JWT', async function (assert) { + let response = await request + .post('/') + .send({}) + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer invalid-token`); + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); + + test('401 without a JWT', async function (assert) { + let response = await request + .post('/') + .send({}) + .set('Accept', 'application/vnd.card+json'); // no Authorization header + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); + + test('401 permissions have been updated', async function (assert) { + let response = await request + .post('/') + .send({}) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read'])}`, + ); + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); + + test('403 without permission', async function (assert) { + let response = await request + .post('/') + .send({}) + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); + + test('201 with permission', async function (assert) { + let response = await request + .post('/') + .send({ data: { - attributes: { - title: null, - description: null, - thumbnailURL: null, - }, type: 'card', + attributes: {}, meta: { adoptsFrom: { module: 'https://cardstack.com/base/card-api', @@ -953,889 +1024,625 @@ module('Realm Server', function (hooks) { }, }, }, - }, - 'file contents are correct', - ); - } else { - assert.ok(false, 'response body is not a card document'); - } - }); - }); - - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read', 'write'], - }); - - test('401 with invalid JWT', async function (assert) { - let response = await request - .post('/') - .send({}) - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer invalid-token`); + }) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + ); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + }); }); + }); - test('401 without a JWT', async function (assert) { - let response = await request - .post('/') - .send({}) - .set('Accept', 'application/vnd.card+json'); // no Authorization header - - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + module('card PATCH request', function (_hooks) { + module('public writable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], + }); - test('401 permissions have been updated', async function (assert) { - let response = await request - .post('/') - .send({}) - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read'])}`, - ); - - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); - - test('403 without permission', async function (assert) { - let response = await request - .post('/') - .send({}) - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); - - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); - - test('201 with permission', async function (assert) { - let response = await request - .post('/') - .send({ - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/card-api', - name: 'CardDef', - }, - }, + test('serves the request', async function (assert) { + let entry = 'person-1.json'; + let expected = [ + { + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}person-1.json`, }, - }) - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, - ); - - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - }); - }); - }); + { + type: 'incremental', + invalidations: [`${testRealmURL}person-1`], + realmURL: testRealmURL.href, + clientRequestId: null, + }, + ]; + let response = await expectEvent({ + assert, + expected, + callback: async () => { + return await request + .patch('/person-1') + .send({ + data: { + type: 'card', + attributes: { + firstName: 'Van Gogh', + }, + meta: { + adoptsFrom: { + module: './person.gts', + name: 'Person', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); + }, + }); - module('card PATCH request', function (_hooks) { - module('public writable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], - }); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); - test('serves the request', async function (assert) { - let entry = 'person-1.json'; - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}person-1.json`, - }, - { - type: 'incremental', - invalidations: [`${testRealmURL}person-1`], - realmURL: testRealmURL.href, - clientRequestId: null, - }, - ]; - let response = await expectEvent({ - assert, - expected, - callback: async () => { - return await request - .patch('/person-1') - .send({ + let json = response.body; + assert.ok(json.data.meta.lastModified, 'lastModified exists'); + if (isSingleCardDocument(json)) { + assert.strictEqual( + json.data.attributes?.firstName, + 'Van Gogh', + 'the field data is correct', + ); + assert.ok(json.data.meta.lastModified, 'lastModified is populated'); + delete json.data.meta.lastModified; + delete json.data.meta.resourceCreatedAt; + let cardFile = join(dir.name, 'realm_server_1', 'test', entry); + assert.ok(existsSync(cardFile), 'card json exists'); + let card = readJSONSync(cardFile); + assert.deepEqual( + card, + { data: { type: 'card', attributes: { firstName: 'Van Gogh', + description: null, + thumbnailURL: null, }, meta: { adoptsFrom: { - module: './person.gts', + module: `./person`, name: 'Person', }, }, }, - }) - .set('Accept', 'application/vnd.card+json'); - }, + }, + 'file contents are correct', + ); + } else { + assert.ok(false, 'response body is not a card document'); + } + + let query: Query = { + filter: { + on: { + module: `${testRealmHref}person`, + name: 'Person', + }, + eq: { + firstName: 'Van Gogh', + }, + }, + }; + + response = await request + .get(`/_search?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual(response.body.data.length, 1, 'found one card'); }); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read', 'write'], + }); - let json = response.body; - assert.ok(json.data.meta.lastModified, 'lastModified exists'); - if (isSingleCardDocument(json)) { - assert.strictEqual( - json.data.attributes?.firstName, - 'Van Gogh', - 'the field data is correct', - ); - assert.ok(json.data.meta.lastModified, 'lastModified is populated'); - delete json.data.meta.lastModified; - delete json.data.meta.resourceCreatedAt; - let cardFile = join(dir.name, 'realm_server_1', 'test', entry); - assert.ok(existsSync(cardFile), 'card json exists'); - let card = readJSONSync(cardFile); - assert.deepEqual( - card, - { + test('401 with invalid JWT', async function (assert) { + let response = await request + .patch('/person-1') + .send({}) + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer invalid-token`); + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); + + test('403 without permission', async function (assert) { + let response = await request + .patch('/person-1') + .send({ + data: { + type: 'card', + attributes: { + firstName: 'Van Gogh', + }, + meta: { + adoptsFrom: { + module: './person.gts', + name: 'Person', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); + + test('200 with permission', async function (assert) { + let response = await request + .patch('/person-1') + .send({ data: { type: 'card', attributes: { firstName: 'Van Gogh', - description: null, - thumbnailURL: null, }, meta: { adoptsFrom: { - module: `./person`, + module: './person.gts', name: 'Person', }, }, }, + }) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + }); + }); + }); + + module('card DELETE request', function (_hooks) { + module('public writable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], + }); + + test('serves the request', async function (assert) { + let entry = 'person-1.json'; + let expected = [ + { + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}person-1.json`, + }, + { + type: 'incremental', + realmURL: testRealmURL.href, + invalidations: [`${testRealmURL}person-1`], + }, + ]; + let response = await expectEvent({ + assert, + expected, + callback: async () => { + return await request + .delete('/person-1') + .set('Accept', 'application/vnd.card+json'); }, - 'file contents are correct', + }); + + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', ); - } else { - assert.ok(false, 'response body is not a card document'); - } + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let cardFile = join(dir.name, entry); + assert.false(existsSync(cardFile), 'card json does not exist'); + }); - let query: Query = { - filter: { - on: { - module: `${testRealmHref}person`, - name: 'Person', + test('serves a card DELETE request with .json extension in the url', async function (assert) { + let entry = 'person-1.json'; + let expected = [ + { + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}person-1.json`, }, - eq: { - firstName: 'Van Gogh', + { + type: 'incremental', + realmURL: testRealmURL.href, + invalidations: [`${testRealmURL}person-1`], }, - }, - }; + ]; - response = await request - .get(`/_search?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + let response = await expectEvent({ + assert, + expected, + callback: async () => { + return await request + .delete('/person-1.json') + .set('Accept', 'application/vnd.card+json'); + }, + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual(response.body.data.length, 1, 'found one card'); + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let cardFile = join(dir.name, entry); + assert.false(existsSync(cardFile), 'card json does not exist'); + }); }); - }); - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read', 'write'], - }); + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read', 'write'], + }); - test('401 with invalid JWT', async function (assert) { - let response = await request - .patch('/person-1') - .send({}) - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer invalid-token`); + test('401 with invalid JWT', async function (assert) { + let response = await request + .delete('/person-1') - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer invalid-token`); - test('403 without permission', async function (assert) { - let response = await request - .patch('/person-1') - .send({ - data: { - type: 'card', - attributes: { - firstName: 'Van Gogh', - }, - meta: { - adoptsFrom: { - module: './person.gts', - name: 'Person', - }, - }, - }, - }) - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + test('403 without permission', async function (assert) { + let response = await request + .delete('/person-1') + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); - test('200 with permission', async function (assert) { - let response = await request - .patch('/person-1') - .send({ - data: { - type: 'card', - attributes: { - firstName: 'Van Gogh', - }, - meta: { - adoptsFrom: { - module: './person.gts', - name: 'Person', - }, - }, - }, - }) - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, - ); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - }); - }); - }); - - module('card DELETE request', function (_hooks) { - module('public writable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], - }); - - test('serves the request', async function (assert) { - let entry = 'person-1.json'; - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}person-1.json`, - }, - { - type: 'incremental', - realmURL: testRealmURL.href, - invalidations: [`${testRealmURL}person-1`], - }, - ]; - let response = await expectEvent({ - assert, - expected, - callback: async () => { - return await request - .delete('/person-1') - .set('Accept', 'application/vnd.card+json'); - }, + assert.strictEqual(response.status, 403, 'HTTP 403 status'); }); - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let cardFile = join(dir.name, entry); - assert.false(existsSync(cardFile), 'card json does not exist'); - }); - - test('serves a card DELETE request with .json extension in the url', async function (assert) { - let entry = 'person-1.json'; - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}person-1.json`, - }, - { - type: 'incremental', - realmURL: testRealmURL.href, - invalidations: [`${testRealmURL}person-1`], - }, - ]; + test('204 with permission', async function (assert) { + let response = await request + .delete('/person-1') + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + ); - let response = await expectEvent({ - assert, - expected, - callback: async () => { - return await request - .delete('/person-1.json') - .set('Accept', 'application/vnd.card+json'); - }, + assert.strictEqual(response.status, 204, 'HTTP 204 status'); }); - - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let cardFile = join(dir.name, entry); - assert.false(existsSync(cardFile), 'card json does not exist'); }); }); - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read', 'write'], - }); - - test('401 with invalid JWT', async function (assert) { - let response = await request - .delete('/person-1') - - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer invalid-token`); - - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); - - test('403 without permission', async function (assert) { - let response = await request - .delete('/person-1') - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + module('card source GET request', function (_hooks) { + module('public readable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read'], + }); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + test('serves the request', async function (assert) { + let response = await request + .get('/person.gts') + .set('Accept', 'application/vnd.card+source'); - test('204 with permission', async function (assert) { - let response = await request - .delete('/person-1') - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let result = response.text.trim(); + assert.strictEqual(result, cardSrc, 'the card source is correct'); + assert.ok( + response.headers['last-modified'], + 'last-modified header exists', + ); + }); - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - }); - }); - }); - - module('card source GET request', function (_hooks) { - module('public readable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read'], - }); - - test('serves the request', async function (assert) { - let response = await request - .get('/person.gts') - .set('Accept', 'application/vnd.card+source'); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let result = response.text.trim(); - assert.strictEqual(result, cardSrc, 'the card source is correct'); - assert.ok( - response.headers['last-modified'], - 'last-modified header exists', - ); - }); - - test('serves a card-source GET request that results in redirect', async function (assert) { - let response = await request - .get('/person') - .set('Accept', 'application/vnd.card+source'); + test('serves a card-source GET request that results in redirect', async function (assert) { + let response = await request + .get('/person') + .set('Accept', 'application/vnd.card+source'); - assert.strictEqual(response.status, 302, 'HTTP 302 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - assert.strictEqual(response.headers['location'], '/person.gts'); - }); + assert.strictEqual(response.status, 302, 'HTTP 302 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + assert.strictEqual(response.headers['location'], '/person.gts'); + }); - test('serves a card instance GET request with card-source accept header that results in redirect', async function (assert) { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+source'); + test('serves a card instance GET request with card-source accept header that results in redirect', async function (assert) { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+source'); - assert.strictEqual(response.status, 302, 'HTTP 302 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - assert.strictEqual(response.headers['location'], '/person-1.json'); - }); + assert.strictEqual(response.status, 302, 'HTTP 302 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + assert.strictEqual(response.headers['location'], '/person-1.json'); + }); - test('serves a card instance GET request with a .json extension and json accept header that results in redirect', async function (assert) { - let response = await request - .get('/person.json') - .set('Accept', 'application/vnd.card+json'); + test('serves a card instance GET request with a .json extension and json accept header that results in redirect', async function (assert) { + let response = await request + .get('/person.json') + .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 302, 'HTTP 302 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - assert.strictEqual(response.headers['location'], '/person'); - }); + assert.strictEqual(response.status, 302, 'HTTP 302 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + assert.strictEqual(response.headers['location'], '/person'); + }); - test('serves a module GET request', async function (assert) { - let response = await request.get('/person'); + test('serves a module GET request', async function (assert) { + let response = await request.get('/person'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm URL header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let body = response.text.trim(); - let moduleAbsolutePath = resolve(join(__dirname, '..', 'person.gts')); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm URL header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let body = response.text.trim(); + let moduleAbsolutePath = resolve(join(__dirname, '..', 'person.gts')); - // Remove platform-dependent id, from https://github.com/emberjs/babel-plugin-ember-template-compilation/blob/d67cca121cfb3bbf5327682b17ed3f2d5a5af528/__tests__/tests.ts#LL1430C1-L1431C1 - body = stripScopedCSSGlimmerAttributes( - body.replace(/"id":\s"[^"]+"/, '"id": ""'), - ); + // Remove platform-dependent id, from https://github.com/emberjs/babel-plugin-ember-template-compilation/blob/d67cca121cfb3bbf5327682b17ed3f2d5a5af528/__tests__/tests.ts#LL1430C1-L1431C1 + body = stripScopedCSSGlimmerAttributes( + body.replace(/"id":\s"[^"]+"/, '"id": ""'), + ); - assert.codeEqual( - body, - compiledCard('""', moduleAbsolutePath), - 'module JS is correct', - ); + assert.codeEqual( + body, + compiledCard('""', moduleAbsolutePath), + 'module JS is correct', + ); + }); }); - }); - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read'], - }); + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read'], + }); - test('401 with invalid JWT', async function (assert) { - let response = await request - .get('/person.gts') - .set('Accept', 'application/vnd.card+source') - .set('Authorization', `Bearer invalid-token`); + test('401 with invalid JWT', async function (assert) { + let response = await request + .get('/person.gts') + .set('Accept', 'application/vnd.card+source') + .set('Authorization', `Bearer invalid-token`); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - test('401 without a JWT', async function (assert) { - let response = await request - .get('/person.gts') - .set('Accept', 'application/vnd.card+source'); // no Authorization header + test('401 without a JWT', async function (assert) { + let response = await request + .get('/person.gts') + .set('Accept', 'application/vnd.card+source'); // no Authorization header - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - test('403 without permission', async function (assert) { - let response = await request - .get('/person.gts') - .set('Accept', 'application/vnd.card+source') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + test('403 without permission', async function (assert) { + let response = await request + .get('/person.gts') + .set('Accept', 'application/vnd.card+source') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); - test('200 with permission', async function (assert) { - let response = await request - .get('/person.gts') - .set('Accept', 'application/vnd.card+source') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read'])}`, - ); + test('200 with permission', async function (assert) { + let response = await request + .get('/person.gts') + .set('Accept', 'application/vnd.card+source') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read'])}`, + ); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + }); }); }); - }); - module('card-source DELETE request', function (_hooks) { - module('public writable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], - }); + module('card-source DELETE request', function (_hooks) { + module('public writable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], + }); - test('serves the request', async function (assert) { - let entry = 'unused-card.gts'; - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}unused-card.gts`, - }, - { - type: 'incremental', - realmURL: testRealmURL.href, - invalidations: [`${testRealmURL}unused-card.gts`], - }, - ]; - let response = await expectEvent({ - assert, - expected, - callback: async () => { - return await request - .delete('/unused-card.gts') - .set('Accept', 'application/vnd.card+source'); - }, - }); - - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let cardFile = join(dir.name, entry); - assert.false(existsSync(cardFile), 'card module does not exist'); - }); - - test('serves a card-source DELETE request for a card instance', async function (assert) { - let entry = 'person-1'; - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}person-1.json`, - }, - { - type: 'incremental', - realmURL: testRealmURL.href, - invalidations: [`${testRealmURL}person-1`], - }, - ]; - let response = await expectEvent({ - assert, - expected, - callback: async () => { - return await request - .delete('/person-1') - .set('Accept', 'application/vnd.card+source'); - }, - }); - - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let cardFile = join(dir.name, entry); - assert.false(existsSync(cardFile), 'card instance does not exist'); - }); - }); - - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read', 'write'], - }); - - test('401 with invalid JWT', async function (assert) { - let response = await request - .delete('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .set('Authorization', `Bearer invalid-token`); - - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); - - test('403 without permission', async function (assert) { - let response = await request - .delete('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); - - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); - - test('204 with permission', async function (assert) { - let response = await request - .delete('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, - ); - - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - }); - }); - }); - - module('card-source POST request', function (_hooks) { - module('public writable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], - }); - - test('serves a card-source POST request', async function (assert) { - let entry = 'unused-card.gts'; - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}unused-card.gts`, - }, - { - type: 'incremental', - invalidations: [`${testRealmURL}unused-card.gts`], - realmURL: testRealmURL.href, - clientRequestId: null, - }, - ]; - let response = await expectEvent({ - assert, - expected, - callback: async () => { - return await request - .post('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .send(`//TEST UPDATE\n${cardSrc}`); - }, - }); - - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - - let srcFile = join(dir.name, 'realm_server_1', 'test', entry); - assert.ok(existsSync(srcFile), 'card src exists'); - let src = readFileSync(srcFile, { encoding: 'utf8' }); - assert.codeEqual( - src, - `//TEST UPDATE - ${cardSrc}`, - ); - }); - - test('serves a card-source POST request for a .txt file', async function (assert) { - let response = await request - .post('/hello-world.txt') - .set('Accept', 'application/vnd.card+source') - .send(`Hello World`); - - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - - let txtFile = join( - dir.name, - 'realm_server_1', - 'test', - 'hello-world.txt', - ); - assert.ok(existsSync(txtFile), 'file exists'); - let src = readFileSync(txtFile, { encoding: 'utf8' }); - assert.strictEqual(src, 'Hello World'); - }); - - test('can serialize a card instance correctly after card definition is changed', async function (assert) { - // create a card def - { + test('serves the request', async function (assert) { + let entry = 'unused-card.gts'; let expected = [ { type: 'incremental-index-initiation', realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}test-card.gts`, + updatedFile: `${testRealmURL}unused-card.gts`, }, { type: 'incremental', - invalidations: [`${testRealmURL}test-card.gts`], realmURL: testRealmURL.href, - clientRequestId: null, + invalidations: [`${testRealmURL}unused-card.gts`], }, ]; - let response = await expectEvent({ assert, expected, callback: async () => { return await request - .post('/test-card.gts') - .set('Accept', 'application/vnd.card+source').send(` - import { contains, field, CardDef } from 'https://cardstack.com/base/card-api'; - import StringCard from 'https://cardstack.com/base/string'; - - export class TestCard extends CardDef { - @field field1 = contains(StringCard); - @field field2 = contains(StringCard); - } - `); + .delete('/unused-card.gts') + .set('Accept', 'application/vnd.card+source'); }, }); - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - } - // make an instance of the card def - let maybeId: string | undefined; - { - let response = await expectEvent({ - assert, - expectedNumberOfEvents: 2, - callback: async () => { - return await request - .post('/') - .send({ - data: { - type: 'card', - attributes: { - field1: 'a', - field2: 'b', - }, - meta: { - adoptsFrom: { - module: `${testRealmURL}test-card`, - name: 'TestCard', - }, - }, - }, - }) - .set('Accept', 'application/vnd.card+json'); - }, - }); - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - maybeId = response.body.data.id; - } - if (!maybeId) { - assert.ok(false, 'new card identifier was undefined'); - // eslint-disable-next-line qunit/no-early-return - return; - } - let id = maybeId; + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let cardFile = join(dir.name, entry); + assert.false(existsSync(cardFile), 'card module does not exist'); + }); - // modify field - { + test('serves a card-source DELETE request for a card instance', async function (assert) { + let entry = 'person-1'; let expected = [ { type: 'incremental-index-initiation', realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}test-card.gts`, + updatedFile: `${testRealmURL}person-1.json`, }, { type: 'incremental', - invalidations: [`${testRealmURL}test-card.gts`, id], realmURL: testRealmURL.href, - clientRequestId: null, + invalidations: [`${testRealmURL}person-1`], }, ]; - let response = await expectEvent({ assert, expected, callback: async () => { return await request - .post('/test-card.gts') - .set('Accept', 'application/vnd.card+source').send(` - import { contains, field, CardDef } from 'https://cardstack.com/base/card-api'; - import StringCard from 'https://cardstack.com/base/string'; - - export class TestCard extends CardDef { - @field field1 = contains(StringCard); - @field field2a = contains(StringCard); // rename field2 -> field2a - } - `); + .delete('/person-1') + .set('Accept', 'application/vnd.card+source'); }, }); + assert.strictEqual(response.status, 204, 'HTTP 204 status'); - } + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let cardFile = join(dir.name, entry); + assert.false(existsSync(cardFile), 'card instance does not exist'); + }); + }); - // verify serialization matches new card def - { + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read', 'write'], + }); + + test('401 with invalid JWT', async function (assert) { let response = await request - .get(new URL(id).pathname) - .set('Accept', 'application/vnd.card+json'); + .delete('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .set('Authorization', `Bearer invalid-token`); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual(json.data.attributes, { - field1: 'a', - field2a: null, - title: null, - description: null, - thumbnailURL: null, - }); - } + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - // set value on renamed field - { + test('403 without permission', async function (assert) { + let response = await request + .delete('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); + + test('204 with permission', async function (assert) { + let response = await request + .delete('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + ); + + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + }); + }); + }); + + module('card-source POST request', function (_hooks) { + module('public writable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], + }); + + test('serves a card-source POST request', async function (assert) { + let entry = 'unused-card.gts'; let expected = [ { type: 'incremental-index-initiation', realmURL: testRealmURL.href, - updatedFile: `${id}.json`, + updatedFile: `${testRealmURL}unused-card.gts`, }, { type: 'incremental', - invalidations: [id], + invalidations: [`${testRealmURL}unused-card.gts`], realmURL: testRealmURL.href, clientRequestId: null, }, @@ -1845,26 +1652,13 @@ module('Realm Server', function (hooks) { expected, callback: async () => { return await request - .patch(new URL(id).pathname) - .send({ - data: { - type: 'card', - attributes: { - field2a: 'c', - }, - meta: { - adoptsFrom: { - module: `${testRealmURL}test-card`, - name: 'TestCard', - }, - }, - }, - }) - .set('Accept', 'application/vnd.card+json'); + .post('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .send(`//TEST UPDATE\n${cardSrc}`); }, }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual(response.status, 204, 'HTTP 204 status'); assert.strictEqual( response.get('X-boxel-realm-url'), testRealmURL.href, @@ -1876,333 +1670,540 @@ module('Realm Server', function (hooks) { 'realm is public readable', ); - let json = response.body; - assert.deepEqual(json.data.attributes, { - field1: 'a', - field2a: 'c', - title: null, - description: null, - thumbnailURL: null, - }); - } + let srcFile = join(dir.name, 'realm_server_1', 'test', entry); + assert.ok(existsSync(srcFile), 'card src exists'); + let src = readFileSync(srcFile, { encoding: 'utf8' }); + assert.codeEqual( + src, + `//TEST UPDATE + ${cardSrc}`, + ); + }); - // verify file serialization is correct - { - let localPath = new RealmPaths(testRealmURL).local(new URL(id)); - let jsonFile = `${join( + test('serves a card-source POST request for a .txt file', async function (assert) { + let response = await request + .post('/hello-world.txt') + .set('Accept', 'application/vnd.card+source') + .send(`Hello World`); + + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + + let txtFile = join( dir.name, 'realm_server_1', 'test', - localPath, - )}.json`; - let doc = JSON.parse( - readFileSync(jsonFile, { encoding: 'utf8' }), - ) as LooseSingleCardDocument; - assert.deepEqual( - doc, - { - data: { - type: 'card', - attributes: { - field1: 'a', - field2a: 'c', - title: null, - description: null, - thumbnailURL: null, - }, - meta: { - adoptsFrom: { - module: '/test-card', - name: 'TestCard', - }, - }, - }, - }, - 'instance serialized to filesystem correctly', + 'hello-world.txt', ); - } + assert.ok(existsSync(txtFile), 'file exists'); + let src = readFileSync(txtFile, { encoding: 'utf8' }); + assert.strictEqual(src, 'Hello World'); + }); - // verify instance GET is correct - { - let response = await request - .get(new URL(id).pathname) - .set('Accept', 'application/vnd.card+json'); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual(json.data.attributes, { - field1: 'a', - field2a: 'c', - title: null, - description: null, - thumbnailURL: null, - }); - } - }); - }); - - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read', 'write'], - }); - - test('401 with invalid JWT', async function (assert) { - let response = await request - .post('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .send(`//TEST UPDATE\n${cardSrc}`) - .set('Authorization', `Bearer invalid-token`); + test('can serialize a card instance correctly after card definition is changed', async function (assert) { + // create a card def + { + let expected = [ + { + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}test-card.gts`, + }, + { + type: 'incremental', + invalidations: [`${testRealmURL}test-card.gts`], + realmURL: testRealmURL.href, + clientRequestId: null, + }, + ]; + + let response = await expectEvent({ + assert, + expected, + callback: async () => { + return await request + .post('/test-card.gts') + .set('Accept', 'application/vnd.card+source').send(` + import { contains, field, CardDef } from 'https://cardstack.com/base/card-api'; + import StringCard from 'https://cardstack.com/base/string'; - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + export class TestCard extends CardDef { + @field field1 = contains(StringCard); + @field field2 = contains(StringCard); + } + `); + }, + }); + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + } - test('401 without a JWT', async function (assert) { - let response = await request - .post('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .send(`//TEST UPDATE\n${cardSrc}`); // no Authorization header + // make an instance of the card def + let maybeId: string | undefined; + { + let response = await expectEvent({ + assert, + expectedNumberOfEvents: 2, + callback: async () => { + return await request + .post('/') + .send({ + data: { + type: 'card', + attributes: { + field1: 'a', + field2: 'b', + }, + meta: { + adoptsFrom: { + module: `${testRealmURL}test-card`, + name: 'TestCard', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); + }, + }); + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + maybeId = response.body.data.id; + } + if (!maybeId) { + assert.ok(false, 'new card identifier was undefined'); + // eslint-disable-next-line qunit/no-early-return + return; + } + let id = maybeId; - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + // modify field + { + let expected = [ + { + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}test-card.gts`, + }, + { + type: 'incremental', + invalidations: [`${testRealmURL}test-card.gts`, id], + realmURL: testRealmURL.href, + clientRequestId: null, + }, + ]; + + let response = await expectEvent({ + assert, + expected, + callback: async () => { + return await request + .post('/test-card.gts') + .set('Accept', 'application/vnd.card+source').send(` + import { contains, field, CardDef } from 'https://cardstack.com/base/card-api'; + import StringCard from 'https://cardstack.com/base/string'; - test('403 without permission', async function (assert) { - let response = await request - .post('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .send(`//TEST UPDATE\n${cardSrc}`) - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + export class TestCard extends CardDef { + @field field1 = contains(StringCard); + @field field2a = contains(StringCard); // rename field2 -> field2a + } + `); + }, + }); + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + } - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + // verify serialization matches new card def + { + let response = await request + .get(new URL(id).pathname) + .set('Accept', 'application/vnd.card+json'); - test('204 with permission', async function (assert) { - let response = await request - .post('/unused-card.gts') - .set('Accept', 'application/vnd.card+source') - .send(`//TEST UPDATE\n${cardSrc}`) - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, - ); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual(json.data.attributes, { + field1: 'a', + field2a: null, + title: null, + description: null, + thumbnailURL: null, + }); + } - assert.strictEqual(response.status, 204, 'HTTP 204 status'); - }); - }); - }); + // set value on renamed field + { + let expected = [ + { + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${id}.json`, + }, + { + type: 'incremental', + invalidations: [id], + realmURL: testRealmURL.href, + clientRequestId: null, + }, + ]; + let response = await expectEvent({ + assert, + expected, + callback: async () => { + return await request + .patch(new URL(id).pathname) + .send({ + data: { + type: 'card', + attributes: { + field2a: 'c', + }, + meta: { + adoptsFrom: { + module: `${testRealmURL}test-card`, + name: 'TestCard', + }, + }, + }, + }) + .set('Accept', 'application/vnd.card+json'); + }, + }); - module('directory GET request', function (_hooks) { - module('public readable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read'], - }); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); - test('serves the request', async function (assert) { - let response = await request - .get('/dir/') - .set('Accept', 'application/vnd.api+json'); + let json = response.body; + assert.deepEqual(json.data.attributes, { + field1: 'a', + field2a: 'c', + title: null, + description: null, + thumbnailURL: null, + }); + } - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let json = response.body; - for (let relationship of Object.values(json.data.relationships)) { - delete (relationship as any).meta.lastModified; - } - assert.deepEqual( - json, + // verify file serialization is correct { - data: { - id: `${testRealmHref}dir/`, - type: 'directory', - relationships: { - 'bar.txt': { - links: { - related: `${testRealmHref}dir/bar.txt`, - }, - meta: { - kind: 'file', - }, - }, - 'foo.txt': { - links: { - related: `${testRealmHref}dir/foo.txt`, - }, - meta: { - kind: 'file', - }, - }, - 'subdir/': { - links: { - related: `${testRealmHref}dir/subdir/`, + let localPath = new RealmPaths(testRealmURL).local(new URL(id)); + let jsonFile = `${join( + dir.name, + 'realm_server_1', + 'test', + localPath, + )}.json`; + let doc = JSON.parse( + readFileSync(jsonFile, { encoding: 'utf8' }), + ) as LooseSingleCardDocument; + assert.deepEqual( + doc, + { + data: { + type: 'card', + attributes: { + field1: 'a', + field2a: 'c', + title: null, + description: null, + thumbnailURL: null, }, meta: { - kind: 'directory', + adoptsFrom: { + module: '/test-card', + name: 'TestCard', + }, }, }, }, - }, - }, - 'the directory response is correct', - ); - }); - }); + 'instance serialized to filesystem correctly', + ); + } - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read'], + // verify instance GET is correct + { + let response = await request + .get(new URL(id).pathname) + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual(json.data.attributes, { + field1: 'a', + field2a: 'c', + title: null, + description: null, + thumbnailURL: null, + }); + } + }); }); - test('401 with invalid JWT', async function (assert) { - let response = await request - .get('/dir/') - .set('Accept', 'application/vnd.api+json') - .set('Authorization', `Bearer invalid-token`); + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read', 'write'], + }); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + test('401 with invalid JWT', async function (assert) { + let response = await request + .post('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .send(`//TEST UPDATE\n${cardSrc}`) + .set('Authorization', `Bearer invalid-token`); - test('401 without a JWT', async function (assert) { - let response = await request - .get('/dir/') - .set('Accept', 'application/vnd.api+json'); // no Authorization header + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + test('401 without a JWT', async function (assert) { + let response = await request + .post('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .send(`//TEST UPDATE\n${cardSrc}`); // no Authorization header - test('403 without permission', async function (assert) { - let response = await request - .get('/dir/') - .set('Accept', 'application/vnd.api+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + test('403 without permission', async function (assert) { + let response = await request + .post('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .send(`//TEST UPDATE\n${cardSrc}`) + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); - test('200 with permission', async function (assert) { - let response = await request - .get('/dir/') - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read'])}`, - ); + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); + test('204 with permission', async function (assert) { + let response = await request + .post('/unused-card.gts') + .set('Accept', 'application/vnd.card+source') + .send(`//TEST UPDATE\n${cardSrc}`) + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + ); + + assert.strictEqual(response.status, 204, 'HTTP 204 status'); + }); }); }); - }); - module('_search GET request', function (_hooks) { - let query: Query = { - filter: { - on: { - module: `${testRealmHref}person`, - name: 'Person', - }, - eq: { - firstName: 'Mango', - }, - }, - }; - - module('public readable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read'], - }); + module('directory GET request', function (_hooks) { + module('public readable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read'], + }); - test('serves a /_search GET request', async function (assert) { - let response = await request - .get(`/_search?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + test('serves the request', async function (assert) { + let response = await request + .get('/dir/') + .set('Accept', 'application/vnd.api+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let json = response.body; - assert.strictEqual( - json.data.length, - 1, - 'the card is returned in the search results', - ); - assert.strictEqual( - json.data[0].id, - `${testRealmHref}person-1`, - 'card ID is correct', - ); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let json = response.body; + for (let relationship of Object.values(json.data.relationships)) { + delete (relationship as any).meta.lastModified; + } + assert.deepEqual( + json, + { + data: { + id: `${testRealmHref}dir/`, + type: 'directory', + relationships: { + 'bar.txt': { + links: { + related: `${testRealmHref}dir/bar.txt`, + }, + meta: { + kind: 'file', + }, + }, + 'foo.txt': { + links: { + related: `${testRealmHref}dir/foo.txt`, + }, + meta: { + kind: 'file', + }, + }, + 'subdir/': { + links: { + related: `${testRealmHref}dir/subdir/`, + }, + meta: { + kind: 'directory', + }, + }, + }, + }, + }, + 'the directory response is correct', + ); + }); }); - }); - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read'], - }); + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read'], + }); - test('401 with invalid JWT', async function (assert) { - let response = await request - .get(`/_search?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + test('401 with invalid JWT', async function (assert) { + let response = await request + .get('/dir/') + .set('Accept', 'application/vnd.api+json') + .set('Authorization', `Bearer invalid-token`); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - test('401 without a JWT', async function (assert) { - let response = await request - .get(`/_search?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); // no Authorization header + test('401 without a JWT', async function (assert) { + let response = await request + .get('/dir/') + .set('Accept', 'application/vnd.api+json'); // no Authorization header - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - test('403 without permission', async function (assert) { - let response = await request - .get(`/_search?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + test('403 without permission', async function (assert) { + let response = await request + .get('/dir/') + .set('Accept', 'application/vnd.api+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); - test('200 with permission', async function (assert) { - let response = await request - .get(`/_search?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read'])}`, - ); + test('200 with permission', async function (assert) { + let response = await request + .get('/dir/') + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read'])}`, + ); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + }); }); }); - }); - module('/_search-prerendered GET request', function (_hooks) { - module( - 'instances with no embedded template css of its own', - function (hooks) { - setupPermissionedRealm( - hooks, - { - '*': ['read'], + module('_search GET request', function (_hooks) { + let query: Query = { + filter: { + on: { + module: `${testRealmHref}person`, + name: 'Person', }, - { - 'person.gts': ` + eq: { + firstName: 'Mango', + }, + }, + }; + + module('public readable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read'], + }); + + test('serves a /_search GET request', async function (assert) { + let response = await request + .get(`/_search?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let json = response.body; + assert.strictEqual( + json.data.length, + 1, + 'the card is returned in the search results', + ); + assert.strictEqual( + json.data[0].id, + `${testRealmHref}person-1`, + 'card ID is correct', + ); + }); + }); + + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read'], + }); + + test('401 with invalid JWT', async function (assert) { + let response = await request + .get(`/_search?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); + + test('401 without a JWT', async function (assert) { + let response = await request + .get(`/_search?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); // no Authorization header + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); + + test('403 without permission', async function (assert) { + let response = await request + .get(`/_search?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + }); + + test('200 with permission', async function (assert) { + let response = await request + .get(`/_search?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read'])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + }); + }); + }); + + module('/_search-prerendered GET request', function (_hooks) { + module( + 'instances with no embedded template css of its own', + function (hooks) { + setupPermissionedRealm( + hooks, + { + '*': ['read'], + }, + { + 'person.gts': ` import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -2225,100 +2226,104 @@ module('Realm Server', function (hooks) { } } `, - 'john.json': { - data: { - attributes: { - firstName: 'John', - }, - meta: { - adoptsFrom: { - module: './person', - name: 'Person', + 'john.json': { + data: { + attributes: { + firstName: 'John', + }, + meta: { + adoptsFrom: { + module: './person', + name: 'Person', + }, }, }, }, }, - }, - ); + ); - test('endpoint will respond with a bad request if html format is not provided', async function (assert) { - let response = await request - .get(`/_search-prerendered`) - .set('Accept', 'application/vnd.card+json'); + test('endpoint will respond with a bad request if html format is not provided', async function (assert) { + let response = await request + .get(`/_search-prerendered`) + .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 400, 'HTTP 200 status'); + assert.strictEqual(response.status, 400, 'HTTP 200 status'); - assert.ok( - response.body.errors[0].message.includes( - "Must include a 'prerenderedHtmlFormat' parameter with a value of 'embedded' or 'atom' to use this endpoint", - ), - ); - }); + assert.ok( + response.body.errors[0].message.includes( + "Must include a 'prerenderedHtmlFormat' parameter with a value of 'embedded' or 'atom' to use this endpoint", + ), + ); + }); - test('returns prerendered instances', async function (assert) { - let query: Query & { prerenderedHtmlFormat: string } = { - filter: { - on: { - module: `${testRealmHref}person`, - name: 'Person', - }, - eq: { - firstName: 'John', + test('returns prerendered instances', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + filter: { + on: { + module: `${testRealmHref}person`, + name: 'Person', + }, + eq: { + firstName: 'John', + }, }, - }, - prerenderedHtmlFormat: 'embedded', - }; - let response = await request - .get(`/_search-prerendered?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let json = response.body; + prerenderedHtmlFormat: 'embedded', + }; + let response = await request + .get(`/_search-prerendered?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); - assert.strictEqual( - json.data.length, - 1, - 'one card instance is returned in the search results', - ); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let json = response.body; - assert.strictEqual(json.data[0].type, 'prerendered-card'); + assert.strictEqual( + json.data.length, + 1, + 'one card instance is returned in the search results', + ); - assert.true( - json.data[0].attributes.html - .replace(/\s+/g, ' ') - .includes('Embedded Card Person: John'), - 'embedded html looks correct', - ); + assert.strictEqual(json.data[0].type, 'prerendered-card'); - assertScopedCssUrlsContain( - assert, - json.meta.scopedCssUrls, - cardDefModuleDependencies, - ); + assert.true( + json.data[0].attributes.html + .replace(/\s+/g, ' ') + .includes('Embedded Card Person: John'), + 'embedded html looks correct', + ); - assert.strictEqual(json.meta.page.total, 1, 'total count is correct'); - }); - }, - ); + assertScopedCssUrlsContain( + assert, + json.meta.scopedCssUrls, + cardDefModuleDependencies, + ); - module('instances whose embedded template has css', function (hooks) { - setupPermissionedRealm( - hooks, - { - '*': ['read'], + assert.strictEqual( + json.meta.page.total, + 1, + 'total count is correct', + ); + }); }, - { - 'person.gts': ` + ); + + module('instances whose embedded template has css', function (hooks) { + setupPermissionedRealm( + hooks, + { + '*': ['read'], + }, + { + 'person.gts': ` import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -2342,7 +2347,7 @@ module('Realm Server', function (hooks) { } } `, - 'fancy-person.gts': ` + 'fancy-person.gts': ` import { Person } from './person'; import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; import StringCard from "https://cardstack.com/base/string"; @@ -2363,382 +2368,376 @@ module('Realm Server', function (hooks) { } } `, - 'aaron.json': { - data: { - attributes: { - firstName: 'Aaron', - title: 'Person Aaron', - }, - meta: { - adoptsFrom: { - module: './person', - name: 'Person', + 'aaron.json': { + data: { + attributes: { + firstName: 'Aaron', + title: 'Person Aaron', + }, + meta: { + adoptsFrom: { + module: './person', + name: 'Person', + }, }, }, }, - }, - 'craig.json': { - data: { - attributes: { - firstName: 'Craig', - title: 'Person Craig', - }, - meta: { - adoptsFrom: { - module: './person', - name: 'Person', + 'craig.json': { + data: { + attributes: { + firstName: 'Craig', + title: 'Person Craig', + }, + meta: { + adoptsFrom: { + module: './person', + name: 'Person', + }, }, }, }, - }, - 'jane.json': { - data: { - attributes: { - firstName: 'Jane', - favoriteColor: 'blue', - title: 'FancyPerson Jane', - }, - meta: { - adoptsFrom: { - module: './fancy-person', - name: 'FancyPerson', + 'jane.json': { + data: { + attributes: { + firstName: 'Jane', + favoriteColor: 'blue', + title: 'FancyPerson Jane', + }, + meta: { + adoptsFrom: { + module: './fancy-person', + name: 'FancyPerson', + }, }, }, }, - }, - 'jimmy.json': { - data: { - attributes: { - firstName: 'Jimmy', - favoriteColor: 'black', - title: 'FancyPerson Jimmy', - }, - meta: { - adoptsFrom: { - module: './fancy-person', - name: 'FancyPerson', + 'jimmy.json': { + data: { + attributes: { + firstName: 'Jimmy', + favoriteColor: 'black', + title: 'FancyPerson Jimmy', + }, + meta: { + adoptsFrom: { + module: './fancy-person', + name: 'FancyPerson', + }, }, }, }, }, - }, - ); - - test('returns instances with CardDef prerendered embedded html + css when there is no "on" filter', async function (assert) { - let response = await request - .get(`/_search-prerendered?prerenderedHtmlFormat=embedded`) - .set('Accept', 'application/vnd.card+json'); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let json = response.body; - - assert.strictEqual( - json.data.length, - 4, - 'returned results count is correct', ); - // 1st card: Person Aaron - assert.strictEqual(json.data[0].type, 'prerendered-card'); - assert.true( - json.data[0].attributes.html - .replace(/\s+/g, ' ') - .includes('Person Aaron'), - 'embedded html looks correct (CardDef template)', - ); + test('returns instances with CardDef prerendered embedded html + css when there is no "on" filter', async function (assert) { + let response = await request + .get(`/_search-prerendered?prerenderedHtmlFormat=embedded`) + .set('Accept', 'application/vnd.card+json'); - // 2nd card: Person Craig - assert.strictEqual(json.data[1].type, 'prerendered-card'); - assert.true( - json.data[1].attributes.html - .replace(/\s+/g, ' ') - .includes('Person Craig'), - 'embedded html for Craig looks correct (CardDef template)', - ); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let json = response.body; - // 3rd card: FancyPerson Jane - assert.strictEqual(json.data[2].type, 'prerendered-card'); - assert.true( - json.data[2].attributes.html - .replace(/\s+/g, ' ') - .includes('FancyPerson Jane'), - 'embedded html for Jane looks correct (CardDef template)', - ); + assert.strictEqual( + json.data.length, + 4, + 'returned results count is correct', + ); - // 4th card: FancyPerson Jimmy - assert.strictEqual(json.data[3].type, 'prerendered-card'); - assert.true( - json.data[3].attributes.html - .replace(/\s+/g, ' ') - .includes('FancyPerson Jimmy'), - 'embedded html for Jimmy looks correct (CardDef template)', - ); + // 1st card: Person Aaron + assert.strictEqual(json.data[0].type, 'prerendered-card'); + assert.true( + json.data[0].attributes.html + .replace(/\s+/g, ' ') + .includes('Person Aaron'), + 'embedded html looks correct (CardDef template)', + ); - assertScopedCssUrlsContain( - assert, - json.meta.scopedCssUrls, - cardDefModuleDependencies, - ); + // 2nd card: Person Craig + assert.strictEqual(json.data[1].type, 'prerendered-card'); + assert.true( + json.data[1].attributes.html + .replace(/\s+/g, ' ') + .includes('Person Craig'), + 'embedded html for Craig looks correct (CardDef template)', + ); - assert.strictEqual(json.meta.page.total, 4, 'total count is correct'); - }); + // 3rd card: FancyPerson Jane + assert.strictEqual(json.data[2].type, 'prerendered-card'); + assert.true( + json.data[2].attributes.html + .replace(/\s+/g, ' ') + .includes('FancyPerson Jane'), + 'embedded html for Jane looks correct (CardDef template)', + ); - test('returns correct css in relationships, even the one indexed in another realm (CardDef)', async function (assert) { - let query: Query & { prerenderedHtmlFormat: string } = { - filter: { - on: { - module: `${testRealmHref}fancy-person`, - name: 'FancyPerson', - }, - not: { - eq: { - firstName: 'Peter', + // 4th card: FancyPerson Jimmy + assert.strictEqual(json.data[3].type, 'prerendered-card'); + assert.true( + json.data[3].attributes.html + .replace(/\s+/g, ' ') + .includes('FancyPerson Jimmy'), + 'embedded html for Jimmy looks correct (CardDef template)', + ); + + assertScopedCssUrlsContain( + assert, + json.meta.scopedCssUrls, + cardDefModuleDependencies, + ); + + assert.strictEqual(json.meta.page.total, 4, 'total count is correct'); + }); + + test('returns correct css in relationships, even the one indexed in another realm (CardDef)', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + filter: { + on: { + module: `${testRealmHref}fancy-person`, + name: 'FancyPerson', + }, + not: { + eq: { + firstName: 'Peter', + }, }, }, - }, - prerenderedHtmlFormat: 'embedded', - }; + prerenderedHtmlFormat: 'embedded', + }; - let response = await request - .get(`/_search-prerendered?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + let response = await request + .get(`/_search-prerendered?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); - let json = response.body; + let json = response.body; - assert.strictEqual( - json.data.length, - 2, - 'returned results count is correct', - ); + assert.strictEqual( + json.data.length, + 2, + 'returned results count is correct', + ); - // 1st card: FancyPerson Jane - assert.true( - json.data[0].attributes.html - .replace(/\s+/g, ' ') - .includes('Embedded Card FancyPerson: Jane'), - 'embedded html for Jane looks correct (FancyPerson template)', - ); + // 1st card: FancyPerson Jane + assert.true( + json.data[0].attributes.html + .replace(/\s+/g, ' ') + .includes('Embedded Card FancyPerson: Jane'), + 'embedded html for Jane looks correct (FancyPerson template)', + ); - // 2nd card: FancyPerson Jimmy - assert.true( - json.data[1].attributes.html - .replace(/\s+/g, ' ') - .includes('Embedded Card FancyPerson: Jimmy'), - 'embedded html for Jimmy looks correct (FancyPerson template)', - ); + // 2nd card: FancyPerson Jimmy + assert.true( + json.data[1].attributes.html + .replace(/\s+/g, ' ') + .includes('Embedded Card FancyPerson: Jimmy'), + 'embedded html for Jimmy looks correct (FancyPerson template)', + ); - assertScopedCssUrlsContain(assert, json.meta.scopedCssUrls, [ - ...cardDefModuleDependencies, - ...[`${testRealmHref}fancy-person.gts`, `${testRealmHref}person.gts`], - ]); - }); + assertScopedCssUrlsContain(assert, json.meta.scopedCssUrls, [ + ...cardDefModuleDependencies, + ...[ + `${testRealmHref}fancy-person.gts`, + `${testRealmHref}person.gts`, + ], + ]); + }); - test('can filter prerendered instances', async function (assert) { - let query: Query & { prerenderedHtmlFormat: string } = { - filter: { - on: { - module: `${testRealmHref}person`, - name: 'Person', - }, - eq: { - firstName: 'Jimmy', + test('can filter prerendered instances', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + filter: { + on: { + module: `${testRealmHref}person`, + name: 'Person', + }, + eq: { + firstName: 'Jimmy', + }, }, - }, - prerenderedHtmlFormat: 'embedded', - }; - let response = await request - .get(`/_search-prerendered?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + prerenderedHtmlFormat: 'embedded', + }; + let response = await request + .get(`/_search-prerendered?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); - let json = response.body; + let json = response.body; - assert.strictEqual( - json.data.length, - 1, - 'one prerendered card instance is returned in the filtered search results', - ); - assert.strictEqual(json.data[0].id, 'http://127.0.0.1:4444/jimmy.json'); - }); + assert.strictEqual( + json.data.length, + 1, + 'one prerendered card instance is returned in the filtered search results', + ); + assert.strictEqual( + json.data[0].id, + 'http://127.0.0.1:4444/jimmy.json', + ); + }); - test('can use cardUrls to filter prerendered instances', async function (assert) { - let query: Query & { - prerenderedHtmlFormat: string; - cardUrls: string[]; - } = { - prerenderedHtmlFormat: 'embedded', - cardUrls: [`${testRealmHref}jimmy.json`], - }; - let response = await request - .get(`/_search-prerendered?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + test('can use cardUrls to filter prerendered instances', async function (assert) { + let query: Query & { + prerenderedHtmlFormat: string; + cardUrls: string[]; + } = { + prerenderedHtmlFormat: 'embedded', + cardUrls: [`${testRealmHref}jimmy.json`], + }; + let response = await request + .get(`/_search-prerendered?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); - let json = response.body; + let json = response.body; - assert.strictEqual( - json.data.length, - 1, - 'one prerendered card instance is returned in the filtered search results', - ); - assert.strictEqual(json.data[0].id, 'http://127.0.0.1:4444/jimmy.json'); + assert.strictEqual( + json.data.length, + 1, + 'one prerendered card instance is returned in the filtered search results', + ); + assert.strictEqual( + json.data[0].id, + 'http://127.0.0.1:4444/jimmy.json', + ); - query = { - prerenderedHtmlFormat: 'embedded', - cardUrls: [`${testRealmHref}jimmy.json`, `${testRealmHref}jane.json`], - }; - response = await request - .get(`/_search-prerendered?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + query = { + prerenderedHtmlFormat: 'embedded', + cardUrls: [ + `${testRealmHref}jimmy.json`, + `${testRealmHref}jane.json`, + ], + }; + response = await request + .get(`/_search-prerendered?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); - json = response.body; + json = response.body; - assert.strictEqual( - json.data.length, - 2, - '2 prerendered card instances are returned in the filtered search results', - ); - assert.strictEqual(json.data[0].id, 'http://127.0.0.1:4444/jane.json'); - assert.strictEqual(json.data[1].id, 'http://127.0.0.1:4444/jimmy.json'); - }); + assert.strictEqual( + json.data.length, + 2, + '2 prerendered card instances are returned in the filtered search results', + ); + assert.strictEqual( + json.data[0].id, + 'http://127.0.0.1:4444/jane.json', + ); + assert.strictEqual( + json.data[1].id, + 'http://127.0.0.1:4444/jimmy.json', + ); + }); - test('can sort prerendered instances', async function (assert) { - let query: Query & { prerenderedHtmlFormat: string } = { - sort: [ - { - by: 'firstName', - on: { module: `${testRealmHref}person`, name: 'Person' }, - direction: 'desc', - }, - ], - prerenderedHtmlFormat: 'embedded', - }; - let response = await request - .get(`/_search-prerendered?${stringify(query)}`) - .set('Accept', 'application/vnd.card+json'); + test('can sort prerendered instances', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + sort: [ + { + by: 'firstName', + on: { module: `${testRealmHref}person`, name: 'Person' }, + direction: 'desc', + }, + ], + prerenderedHtmlFormat: 'embedded', + }; + let response = await request + .get(`/_search-prerendered?${stringify(query)}`) + .set('Accept', 'application/vnd.card+json'); - let json = response.body; + let json = response.body; - assert.strictEqual(json.data.length, 4, 'results count is correct'); + assert.strictEqual(json.data.length, 4, 'results count is correct'); - // firstName descending - assert.strictEqual(json.data[0].id, 'http://127.0.0.1:4444/jimmy.json'); - assert.strictEqual(json.data[1].id, 'http://127.0.0.1:4444/jane.json'); - assert.strictEqual(json.data[2].id, 'http://127.0.0.1:4444/craig.json'); - assert.strictEqual(json.data[3].id, 'http://127.0.0.1:4444/aaron.json'); + // firstName descending + assert.strictEqual( + json.data[0].id, + 'http://127.0.0.1:4444/jimmy.json', + ); + assert.strictEqual( + json.data[1].id, + 'http://127.0.0.1:4444/jane.json', + ); + assert.strictEqual( + json.data[2].id, + 'http://127.0.0.1:4444/craig.json', + ); + assert.strictEqual( + json.data[3].id, + 'http://127.0.0.1:4444/aaron.json', + ); + }); }); }); - }); - module('_info GET request', function (_hooks) { - module('public readable realm', function (hooks) { - setupPermissionedRealm(hooks, { - '*': ['read'], - }); + module('_info GET request', function (_hooks) { + module('public readable realm', function (hooks) { + setupPermissionedRealm(hooks, { + '*': ['read'], + }); - test('serves the request', async function (assert) { - let response = await request - .get(`/_info`) - .set('Accept', 'application/vnd.api+json'); + test('serves the request', async function (assert) { + let response = await request + .get(`/_info`) + .set('Accept', 'application/vnd.api+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - let json = response.body; - assert.deepEqual( - json, - { - data: { - id: testRealmHref, - type: 'realm-info', - attributes: testRealmInfo, + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + let json = response.body; + assert.deepEqual( + json, + { + data: { + id: testRealmHref, + type: 'realm-info', + attributes: testRealmInfo, + }, }, - }, - '/_info response is correct', - ); - }); - }); - - module('permissioned realm', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read', 'write'], - }); - - test('401 with invalid JWT', async function (assert) { - let response = await request - .get(`/_info`) - .set('Accept', 'application/vnd.api+json'); - - assert.strictEqual(response.status, 401, 'HTTP 401 status'); + '/_info response is correct', + ); + }); }); - test('401 without a JWT', async function (assert) { - let response = await request - .get(`/_info`) - .set('Accept', 'application/vnd.api+json'); // no Authorization header + module('permissioned realm', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read', 'write'], + }); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); + test('401 with invalid JWT', async function (assert) { + let response = await request + .get(`/_info`) + .set('Accept', 'application/vnd.api+json'); - test('403 without permission', async function (assert) { - let response = await request - .get(`/_info`) - .set('Accept', 'application/vnd.api+json') - .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - }); + test('401 without a JWT', async function (assert) { + let response = await request + .get(`/_info`) + .set('Accept', 'application/vnd.api+json'); // no Authorization header - test('200 with permission', async function (assert) { - let response = await request - .get(`/_info`) - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, - ); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - id: testRealmHref, - type: 'realm-info', - attributes: { - ...testRealmInfo, - visibility: 'private', - realmUserId: '@node-test_realm:localhost', - }, - }, - }, - '/_info response is correct', - ); - }); - }); + test('403 without permission', async function (assert) { + let response = await request + .get(`/_info`) + .set('Accept', 'application/vnd.api+json') + .set('Authorization', `Bearer ${createJWT(testRealm, 'not-john')}`); - module( - 'shared realm because there is `users` permission', - function (hooks) { - setupPermissionedRealm(hooks, { - users: ['read'], + assert.strictEqual(response.status, 403, 'HTTP 403 status'); }); test('200 with permission', async function (assert) { @@ -2747,7 +2746,7 @@ module('Realm Server', function (hooks) { .set('Accept', 'application/vnd.api+json') .set( 'Authorization', - `Bearer ${createJWT(testRealm, 'users', ['read'])}`, + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, ); assert.strictEqual(response.status, 200, 'HTTP 200 status'); @@ -2760,7 +2759,7 @@ module('Realm Server', function (hooks) { type: 'realm-info', attributes: { ...testRealmInfo, - visibility: 'shared', + visibility: 'private', realmUserId: '@node-test_realm:localhost', }, }, @@ -2768,322 +2767,376 @@ module('Realm Server', function (hooks) { '/_info response is correct', ); }); - }, - ); - - module('shared realm because there are multiple users', function (hooks) { - setupPermissionedRealm(hooks, { - bob: ['read'], - jane: ['read'], - john: ['read', 'write'], }); - test('200 with permission', async function (assert) { - let response = await request - .get(`/_info`) - .set('Accept', 'application/vnd.api+json') + module( + 'shared realm because there is `users` permission', + function (hooks) { + setupPermissionedRealm(hooks, { + users: ['read'], + }); + + test('200 with permission', async function (assert) { + let response = await request + .get(`/_info`) + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'users', ['read'])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + id: testRealmHref, + type: 'realm-info', + attributes: { + ...testRealmInfo, + visibility: 'shared', + realmUserId: '@node-test_realm:localhost', + }, + }, + }, + '/_info response is correct', + ); + }); + }, + ); + + module('shared realm because there are multiple users', function (hooks) { + setupPermissionedRealm(hooks, { + bob: ['read'], + jane: ['read'], + john: ['read', 'write'], + }); + + test('200 with permission', async function (assert) { + let response = await request + .get(`/_info`) + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + id: testRealmHref, + type: 'realm-info', + attributes: { + ...testRealmInfo, + visibility: 'shared', + realmUserId: '@node-test_realm:localhost', + }, + }, + }, + '/_info response is correct', + ); + }); + }); + }); + + module('_user GET request', function (hooks) { + setupPermissionedRealm(hooks, { + john: ['read', 'write'], + }); + + test('responds with 404 if user is not found', async function (assert) { + let response = await request + .get(`/_user`) + .set('Accept', 'application/vnd.api+json') .set( 'Authorization', - `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + `Bearer ${createJWT(testRealm, 'user', ['read', 'write'])}`, ); + assert.strictEqual(response.status, 404, 'HTTP 404 status'); + }); + test('responds with 200 and null subscription values if user is not subscribed', async function (assert) { + let user = await insertUser( + dbAdapter, + 'user@test', + 'cus_123', + 'user@test.com', + ); + let response = await request + .get(`/_user`) + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'user@test', ['read', 'write'])}`, + ); assert.strictEqual(response.status, 200, 'HTTP 200 status'); let json = response.body; assert.deepEqual( json, { data: { - id: testRealmHref, - type: 'realm-info', + type: 'user', + id: user.id, attributes: { - ...testRealmInfo, - visibility: 'shared', - realmUserId: '@node-test_realm:localhost', + matrixUserId: user.matrixUserId, + stripeCustomerId: user.stripeCustomerId, + stripeCustomerEmail: user.stripeCustomerEmail, + creditsAvailableInPlanAllowance: null, + creditsIncludedInPlanAllowance: null, + extraCreditsAvailableInBalance: null, + }, + relationships: { + subscription: null, }, }, + included: null, }, - '/_info response is correct', + '/_user response is correct', ); }); - }); - }); - - module('_user GET request', function (hooks) { - setupPermissionedRealm(hooks, { - john: ['read', 'write'], - }); - test('responds with 404 if user is not found', async function (assert) { - let response = await request - .get(`/_user`) - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'user', ['read', 'write'])}`, + test('response has correct values for subscribed user who has some extra credits', async function (assert) { + let user = await insertUser( + dbAdapter, + 'user@test', + 'cus_123', + 'user@test.com', ); - assert.strictEqual(response.status, 404, 'HTTP 404 status'); - }); + let someOtherUser = await insertUser( + dbAdapter, + 'some-other-user@test', + 'cus_1234', + 'other@test.com', + ); // For the purposes of testing that we don't return the wrong user's subscription's data - test('responds with 200 and null subscription values if user is not subscribed', async function (assert) { - let user = await insertUser( - dbAdapter, - 'user@test', - 'cus_123', - 'user@test.com', - ); - let response = await request - .get(`/_user`) - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'user@test', ['read', 'write'])}`, + let plan = await insertPlan( + dbAdapter, + 'Creator', + 12, + 2500, + 'prod_creator', ); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'user', - id: user.id, - attributes: { - matrixUserId: user.matrixUserId, - stripeCustomerId: user.stripeCustomerId, - stripeCustomerEmail: user.stripeCustomerEmail, - creditsAvailableInPlanAllowance: null, - creditsIncludedInPlanAllowance: null, - extraCreditsAvailableInBalance: null, - }, - relationships: { - subscription: null, - }, - }, - included: null, - }, - '/_user response is correct', - ); - }); - - test('response has correct values for subscribed user who has some extra credits', async function (assert) { - let user = await insertUser( - dbAdapter, - 'user@test', - 'cus_123', - 'user@test.com', - ); - let someOtherUser = await insertUser( - dbAdapter, - 'some-other-user@test', - 'cus_1234', - 'other@test.com', - ); // For the purposes of testing that we don't return the wrong user's subscription's data - - let plan = await insertPlan( - dbAdapter, - 'Creator', - 12, - 2500, - 'prod_creator', - ); - let subscription = await insertSubscription(dbAdapter, { - user_id: user.id, - plan_id: plan.id, - started_at: 1, - status: 'active', - stripe_subscription_id: 'sub_1234567890', - }); + let subscription = await insertSubscription(dbAdapter, { + user_id: user.id, + plan_id: plan.id, + started_at: 1, + status: 'active', + stripe_subscription_id: 'sub_1234567890', + }); - let subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { - subscriptionId: subscription.id, - periodStart: 1, - periodEnd: 2, - }); + let subscriptionCycle = await insertSubscriptionCycle(dbAdapter, { + subscriptionId: subscription.id, + periodStart: 1, + periodEnd: 2, + }); - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: 100, - creditType: 'extra_credit', - subscriptionCycleId: subscriptionCycle.id, - }); + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: 100, + creditType: 'extra_credit', + subscriptionCycleId: subscriptionCycle.id, + }); - await addToCreditsLedger(dbAdapter, { - userId: user.id, - creditAmount: 2500, - creditType: 'plan_allowance', - subscriptionCycleId: subscriptionCycle.id, - }); + await addToCreditsLedger(dbAdapter, { + userId: user.id, + creditAmount: 2500, + creditType: 'plan_allowance', + subscriptionCycleId: subscriptionCycle.id, + }); - // Set up other user's subscription - let otherUserSubscription = await insertSubscription(dbAdapter, { - user_id: someOtherUser.id, - plan_id: plan.id, - started_at: 1, - status: 'active', - stripe_subscription_id: 'sub_1234567891', - }); + // Set up other user's subscription + let otherUserSubscription = await insertSubscription(dbAdapter, { + user_id: someOtherUser.id, + plan_id: plan.id, + started_at: 1, + status: 'active', + stripe_subscription_id: 'sub_1234567891', + }); - let otherUserSubscriptionCycle = await insertSubscriptionCycle( - dbAdapter, - { - subscriptionId: otherUserSubscription.id, - periodStart: 1, - periodEnd: 2, - }, - ); + let otherUserSubscriptionCycle = await insertSubscriptionCycle( + dbAdapter, + { + subscriptionId: otherUserSubscription.id, + periodStart: 1, + periodEnd: 2, + }, + ); - await addToCreditsLedger(dbAdapter, { - userId: someOtherUser.id, - creditAmount: 100, - creditType: 'extra_credit', - subscriptionCycleId: otherUserSubscriptionCycle.id, - }); // this is to test that this extra credit amount does not influence the original user's credit calculation + await addToCreditsLedger(dbAdapter, { + userId: someOtherUser.id, + creditAmount: 100, + creditType: 'extra_credit', + subscriptionCycleId: otherUserSubscriptionCycle.id, + }); // this is to test that this extra credit amount does not influence the original user's credit calculation - let response = await request - .get(`/_user`) - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, 'user@test', ['read', 'write'])}`, - ); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'user', - id: user.id, - attributes: { - matrixUserId: user.matrixUserId, - stripeCustomerId: user.stripeCustomerId, - stripeCustomerEmail: user.stripeCustomerEmail, - creditsAvailableInPlanAllowance: 2500, - creditsIncludedInPlanAllowance: 2500, - extraCreditsAvailableInBalance: 100, - }, - relationships: { - subscription: { - data: { - type: 'subscription', - id: subscription.id, - }, - }, - }, - }, - included: [ - { - type: 'subscription', - id: subscription.id, + let response = await request + .get(`/_user`) + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'user@test', ['read', 'write'])}`, + ); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + type: 'user', + id: user.id, attributes: { - startedAt: 1, - endedAt: null, - status: 'active', + matrixUserId: user.matrixUserId, + stripeCustomerId: user.stripeCustomerId, + stripeCustomerEmail: user.stripeCustomerEmail, + creditsAvailableInPlanAllowance: 2500, + creditsIncludedInPlanAllowance: 2500, + extraCreditsAvailableInBalance: 100, }, relationships: { - plan: { + subscription: { data: { - type: 'plan', - id: plan.id, + type: 'subscription', + id: subscription.id, }, }, }, }, - { - type: 'plan', - id: plan.id, - attributes: { - name: plan.name, - monthlyPrice: plan.monthlyPrice, - creditsIncluded: plan.creditsIncluded, + included: [ + { + type: 'subscription', + id: subscription.id, + attributes: { + startedAt: 1, + endedAt: null, + status: 'active', + }, + relationships: { + plan: { + data: { + type: 'plan', + id: plan.id, + }, + }, + }, }, - }, - ], - }, - '/_user response is correct', - ); + { + type: 'plan', + id: plan.id, + attributes: { + name: plan.name, + monthlyPrice: plan.monthlyPrice, + creditsIncluded: plan.creditsIncluded, + }, + }, + ], + }, + '/_user response is correct', + ); + }); }); - }); - - module('various other realm tests', function (hooks) { - let testRealmHttpServer2: Server; - let testRealmServer2: RealmServer; - let testRealm2: Realm; - let dbAdapter: PgAdapter; - let publisher: QueuePublisher; - let runner: QueueRunner; - let request2: SuperTest; - let testRealmDir: string; - hooks.beforeEach(async function () { - shimExternals(virtualNetwork); - }); + module('various other realm tests', function (hooks) { + let testRealmHttpServer2: Server; + let testRealmServer2: RealmServer; + let testRealm2: Realm; + let dbAdapter: PgAdapter; + let publisher: QueuePublisher; + let runner: QueueRunner; + let request2: SuperTest; + let testRealmDir: string; + + hooks.beforeEach(async function () { + shimExternals(virtualNetwork); + }); - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], - }); + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], + }); - async function startRealmServer( - dbAdapter: PgAdapter, - publisher: QueuePublisher, - runner: QueueRunner, - ) { - if (testRealm2) { - virtualNetwork.unmount(testRealm2.handle); + async function startRealmServer( + dbAdapter: PgAdapter, + publisher: QueuePublisher, + runner: QueueRunner, + ) { + if (testRealm2) { + virtualNetwork.unmount(testRealm2.handle); + } + ({ + testRealm: testRealm2, + testRealmServer: testRealmServer2, + testRealmHttpServer: testRealmHttpServer2, + } = await runTestRealmServer({ + virtualNetwork, + testRealmDir, + realmsRootPath: join(dir.name, 'realm_server_2'), + realmURL: testRealm2URL, + dbAdapter, + publisher, + runner, + matrixURL, + })); + request2 = supertest(testRealmHttpServer2); } - ({ - testRealm: testRealm2, - testRealmServer: testRealmServer2, - testRealmHttpServer: testRealmHttpServer2, - } = await runTestRealmServer({ - virtualNetwork, - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_2'), - realmURL: testRealm2URL, - dbAdapter, - publisher, - runner, - matrixURL, - })); - request2 = supertest(testRealmHttpServer2); - } - setupDB(hooks, { - beforeEach: async (_dbAdapter, _publisher, _runner) => { - dbAdapter = _dbAdapter; - publisher = _publisher; - runner = _runner; - testRealmDir = join(dir.name, 'realm_server_2', 'test'); - ensureDirSync(testRealmDir); - copySync(join(__dirname, 'cards'), testRealmDir); - await startRealmServer(dbAdapter, publisher, runner); - }, - afterEach: async () => { - await closeServer(testRealmHttpServer2); - }, - }); + setupDB(hooks, { + beforeEach: async (_dbAdapter, _publisher, _runner) => { + dbAdapter = _dbAdapter; + publisher = _publisher; + runner = _runner; + testRealmDir = join(dir.name, 'realm_server_2', 'test'); + ensureDirSync(testRealmDir); + copySync(join(__dirname, 'cards'), testRealmDir); + await startRealmServer(dbAdapter, publisher, runner); + }, + afterEach: async () => { + await closeServer(testRealmHttpServer2); + }, + }); + + test('POST /_create-realm', async function (assert) { + // we randomize the realm and owner names so that we can isolate matrix + // test state--there is no "delete user" matrix API + let endpoint = `test-realm-${uuidv4()}`; + let owner = 'mango'; + let ownerUserId = '@mango:boxel.ai'; + let response = await request2 + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: ownerUserId, sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + ...testRealmInfo, + endpoint, + backgroundURL: 'http://example.com/background.jpg', + iconURL: 'http://example.com/icon.jpg', + }, + }, + }), + ); - test('POST /_create-realm', async function (assert) { - // we randomize the realm and owner names so that we can isolate matrix - // test state--there is no "delete user" matrix API - let endpoint = `test-realm-${uuidv4()}`; - let owner = 'mango'; - let ownerUserId = '@mango:boxel.ai'; - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: ownerUserId, sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send( - JSON.stringify({ + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + let json = response.body; + assert.deepEqual( + json, + { data: { type: 'realm', + id: `${testRealm2URL.origin}/${owner}/${endpoint}/`, attributes: { ...testRealmInfo, endpoint, @@ -3091,262 +3144,329 @@ module('Realm Server', function (hooks) { iconURL: 'http://example.com/icon.jpg', }, }, - }), + }, + 'realm creation JSON is correct', ); - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - let json = response.body; - assert.deepEqual( - json, - { - data: { - type: 'realm', - id: `${testRealm2URL.origin}/${owner}/${endpoint}/`, - attributes: { - ...testRealmInfo, - endpoint, - backgroundURL: 'http://example.com/background.jpg', - iconURL: 'http://example.com/icon.jpg', - }, + let realmPath = join(dir.name, 'realm_server_2', owner, endpoint); + let realmJSON = readJSONSync(join(realmPath, '.realm.json')); + assert.deepEqual( + realmJSON, + { + name: 'Test Realm', + backgroundURL: 'http://example.com/background.jpg', + iconURL: 'http://example.com/icon.jpg', }, - }, - 'realm creation JSON is correct', - ); - - let realmPath = join(dir.name, 'realm_server_2', owner, endpoint); - let realmJSON = readJSONSync(join(realmPath, '.realm.json')); - assert.deepEqual( - realmJSON, - { - name: 'Test Realm', - backgroundURL: 'http://example.com/background.jpg', - iconURL: 'http://example.com/icon.jpg', - }, - '.realm.json is correct', - ); - assert.ok( - existsSync(join(realmPath, 'index.json')), - 'seed file index.json exists', - ); - assert.ok( - existsSync( - join( - realmPath, - 'HelloWorld/47c0fc54-5099-4e9c-ad0d-8a58572d05c0.json', + '.realm.json is correct', + ); + assert.ok( + existsSync(join(realmPath, 'index.json')), + 'seed file index.json exists', + ); + assert.ok( + existsSync( + join( + realmPath, + 'HelloWorld/47c0fc54-5099-4e9c-ad0d-8a58572d05c0.json', + ), ), - ), - 'seed file HelloWorld/47c0fc54-5099-4e9c-ad0d-8a58572d05c0.json exists', - ); - assert.notOk( - existsSync(join(realmPath, 'package.json')), - 'ignored seed file package.json does not exist', - ); - assert.notOk( - existsSync(join(realmPath, 'node_modules')), - 'ignored seed file node_modules/ does not exist', - ); - assert.notOk( - existsSync(join(realmPath, '.gitignore')), - 'ignored seed file .gitignore does not exist', - ); - assert.notOk( - existsSync(join(realmPath, 'tsconfig.json')), - 'ignored seed file tsconfig.json does not exist', - ); + 'seed file HelloWorld/47c0fc54-5099-4e9c-ad0d-8a58572d05c0.json exists', + ); + assert.notOk( + existsSync(join(realmPath, 'package.json')), + 'ignored seed file package.json does not exist', + ); + assert.notOk( + existsSync(join(realmPath, 'node_modules')), + 'ignored seed file node_modules/ does not exist', + ); + assert.notOk( + existsSync(join(realmPath, '.gitignore')), + 'ignored seed file .gitignore does not exist', + ); + assert.notOk( + existsSync(join(realmPath, 'tsconfig.json')), + 'ignored seed file tsconfig.json does not exist', + ); - let permissions = await fetchUserPermissions( - dbAdapter, - new URL(json.data.id), - ); - assert.deepEqual(permissions, { - [`@realm/mango_${endpoint}:localhost`]: [ - 'read', - 'write', - 'realm-owner', - ], - [ownerUserId]: ['read', 'write', 'realm-owner'], - }); + let permissions = await fetchUserPermissions( + dbAdapter, + new URL(json.data.id), + ); + assert.deepEqual(permissions, { + [`@realm/mango_${endpoint}:localhost`]: [ + 'read', + 'write', + 'realm-owner', + ], + [ownerUserId]: ['read', 'write', 'realm-owner'], + }); - let id: string; - let realm = testRealmServer2.testingOnlyRealms.find( - (r) => r.url === json.data.id, - )!; - { - // owner can create an instance - let response = await request2 - .post(`/${owner}/${endpoint}/`) - .send({ - data: { - type: 'card', - attributes: { - title: 'Test Card', - }, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/card-api', - name: 'CardDef', + let id: string; + let realm = testRealmServer2.testingOnlyRealms.find( + (r) => r.url === json.data.id, + )!; + { + // owner can create an instance + let response = await request2 + .post(`/${owner}/${endpoint}/`) + .send({ + data: { + type: 'card', + attributes: { + title: 'Test Card', + }, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/card-api', + name: 'CardDef', + }, }, }, - }, - }) - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(realm, ownerUserId, [ - 'read', - 'write', - 'realm-owner', - ])}`, - ); + }) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(realm, ownerUserId, [ + 'read', + 'write', + 'realm-owner', + ])}`, + ); - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - let doc = response.body as SingleCardDocument; - id = doc.data.id; - } + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + let doc = response.body as SingleCardDocument; + id = doc.data.id; + } - { - // owner can get an instance - let response = await request2 - .get(new URL(id).pathname) - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(realm, ownerUserId, [ - 'read', - 'write', - 'realm-owner', - ])}`, + { + // owner can get an instance + let response = await request2 + .get(new URL(id).pathname) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(realm, ownerUserId, [ + 'read', + 'write', + 'realm-owner', + ])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let doc = response.body as SingleCardDocument; + assert.strictEqual( + doc.data.attributes?.title, + 'Test Card', + 'instance data is correct', ); + } - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let doc = response.body as SingleCardDocument; - assert.strictEqual( - doc.data.attributes?.title, - 'Test Card', - 'instance data is correct', - ); - } + { + // owner can search in the realm + let response = await request2 + .get( + `${new URL(realm.url).pathname}_search?${stringify({ + filter: { + on: baseCardRef, + eq: { + title: 'Test Card', + }, + }, + } as Query)}`, + ) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(realm, ownerUserId, [ + 'read', + 'write', + 'realm-owner', + ])}`, + ); - { - // owner can search in the realm + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let results = response.body as CardCollectionDocument; + assert.strictEqual(results.data.length, 1), + 'correct number of search results'; + } + }); + + test('dynamically created realms are not publicly readable or writable', async function (assert) { + let endpoint = `test-realm-${uuidv4()}`; + let owner = 'mango'; + let ownerUserId = '@mango:boxel.ai'; let response = await request2 - .get( - `${new URL(realm.url).pathname}_search?${stringify({ - filter: { - on: baseCardRef, - eq: { - title: 'Test Card', - }, - }, - } as Query)}`, - ) - .set('Accept', 'application/vnd.card+json') + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') .set( 'Authorization', - `Bearer ${createJWT(realm, ownerUserId, [ - 'read', - 'write', - 'realm-owner', - ])}`, + `Bearer ${createRealmServerJWT( + { user: ownerUserId, sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + name: 'Test Realm', + endpoint, + }, + }, + }), ); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let results = response.body as CardCollectionDocument; - assert.strictEqual(results.data.length, 1), - 'correct number of search results'; - } - }); + let realmURL = response.body.data.id; + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + let realm = testRealmServer2.testingOnlyRealms.find( + (r) => r.url === realmURL, + )!; - test('dynamically created realms are not publicly readable or writable', async function (assert) { - let endpoint = `test-realm-${uuidv4()}`; - let owner = 'mango'; - let ownerUserId = '@mango:boxel.ai'; - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: ownerUserId, sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - name: 'Test Realm', - endpoint, - }, - }, - }), - ); + { + let response = await request2 + .get( + `${new URL(realmURL).pathname}_search?${stringify({ + filter: { + on: baseCardRef, + eq: { + title: 'Test Card', + }, + }, + } as Query)}`, + ) + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer ${createJWT(realm, 'rando')}`); - let realmURL = response.body.data.id; - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - let realm = testRealmServer2.testingOnlyRealms.find( - (r) => r.url === realmURL, - )!; + assert.strictEqual(response.status, 403, 'HTTP 403 status'); - { - let response = await request2 - .get( - `${new URL(realmURL).pathname}_search?${stringify({ - filter: { - on: baseCardRef, - eq: { + response = await request2 + .post(`/${owner}/${endpoint}/`) + .send({ + data: { + type: 'card', + attributes: { title: 'Test Card', }, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/card-api', + name: 'CardDef', + }, + }, }, - } as Query)}`, - ) - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer ${createJWT(realm, 'rando')}`); + }) + .set('Accept', 'application/vnd.card+json') + .set('Authorization', `Bearer ${createJWT(realm, 'rando')}`); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); + assert.strictEqual(response.status, 403, 'HTTP 403 status'); + } + }); - response = await request2 - .post(`/${owner}/${endpoint}/`) - .send({ - data: { - type: 'card', - attributes: { - title: 'Test Card', - }, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/card-api', - name: 'CardDef', + test('can restart a realm that was created dynamically', async function (assert) { + let endpoint = `test-realm-${uuidv4()}`; + let owner = 'mango'; + let ownerUserId = '@mango:boxel.ai'; + let realmURL: string; + { + let response = await request2 + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + name: 'Test Realm', + endpoint, + }, + }, + }), + ); + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + realmURL = response.body.data.id; + } + + let id: string; + let realm = testRealmServer2.testingOnlyRealms.find( + (r) => r.url === realmURL, + )!; + { + let response = await request2 + .post(`/${owner}/${endpoint}/`) + .send({ + data: { + type: 'card', + attributes: { + title: 'Test Card', + }, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/card-api', + name: 'CardDef', + }, }, }, - }, - }) - .set('Accept', 'application/vnd.card+json') - .set('Authorization', `Bearer ${createJWT(realm, 'rando')}`); + }) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(realm, ownerUserId, [ + 'read', + 'write', + 'realm-owner', + ])}`, + ); - assert.strictEqual(response.status, 403, 'HTTP 403 status'); - } - }); + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + id = response.body.data.id; + } - test('can restart a realm that was created dynamically', async function (assert) { - let endpoint = `test-realm-${uuidv4()}`; - let owner = 'mango'; - let ownerUserId = '@mango:boxel.ai'; - let realmURL: string; - { + // Stop and restart the server + testRealmServer2.testingOnlyUnmountRealms(); + await closeServer(testRealmHttpServer2); + await startRealmServer(dbAdapter, publisher, runner); + await testRealmServer2.start(); + + { + let response = await request2 + .get(new URL(id).pathname) + .set('Accept', 'application/vnd.card+json') + .set( + 'Authorization', + `Bearer ${createJWT(realm, ownerUserId, [ + 'read', + 'write', + 'realm-owner', + ])}`, + ); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let doc = response.body as SingleCardDocument; + assert.strictEqual( + doc.data.attributes?.title, + 'Test Card', + 'instance data is correct', + ); + } + }); + + test('POST /_create-realm without JWT', async function (assert) { + let endpoint = `test-realm-${uuidv4()}`; let response = await request2 .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) .send( JSON.stringify({ data: { @@ -3358,282 +3478,85 @@ module('Realm Server', function (hooks) { }, }), ); - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - realmURL = response.body.data.id; - } + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + let error = response.body.errors[0]; + assert.strictEqual( + error, + 'Missing Authorization header', + 'error message is correct', + ); + }); - let id: string; - let realm = testRealmServer2.testingOnlyRealms.find( - (r) => r.url === realmURL, - )!; - { + test('POST /_create-realm with invalid JWT', async function (assert) { + let endpoint = `test-realm-${uuidv4()}`; let response = await request2 - .post(`/${owner}/${endpoint}/`) - .send({ - data: { - type: 'card', - attributes: { - title: 'Test Card', - }, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/card-api', - name: 'CardDef', + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer invalid-jwt') + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + name: 'Test Realm', + endpoint, }, }, - }, - }) - .set('Accept', 'application/vnd.card+json') - .set( - 'Authorization', - `Bearer ${createJWT(realm, ownerUserId, [ - 'read', - 'write', - 'realm-owner', - ])}`, + }), ); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + let error = response.body.errors[0]; + assert.strictEqual(error, 'Token invalid', 'error message is correct'); + }); - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - id = response.body.data.id; - } - - // Stop and restart the server - testRealmServer2.testingOnlyUnmountRealms(); - await closeServer(testRealmHttpServer2); - await startRealmServer(dbAdapter, publisher, runner); - await testRealmServer2.start(); - - { + test('POST /_create-realm with invalid JSON', async function (assert) { let response = await request2 - .get(new URL(id).pathname) - .set('Accept', 'application/vnd.card+json') + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') .set( 'Authorization', - `Bearer ${createJWT(realm, ownerUserId, [ - 'read', - 'write', - 'realm-owner', - ])}`, - ); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let doc = response.body as SingleCardDocument; - assert.strictEqual( - doc.data.attributes?.title, - 'Test Card', - 'instance data is correct', - ); - } - }); - - test('POST /_create-realm without JWT', async function (assert) { - let endpoint = `test-realm-${uuidv4()}`; - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - name: 'Test Realm', - endpoint, - }, - }, - }), - ); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - let error = response.body.errors[0]; - assert.strictEqual( - error, - 'Missing Authorization header', - 'error message is correct', - ); - }); - - test('POST /_create-realm with invalid JWT', async function (assert) { - let endpoint = `test-realm-${uuidv4()}`; - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set('Authorization', 'Bearer invalid-jwt') - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - name: 'Test Realm', - endpoint, - }, - }, - }), - ); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - let error = response.body.errors[0]; - assert.strictEqual(error, 'Token invalid', 'error message is correct'); - }); - - test('POST /_create-realm with invalid JSON', async function (assert) { - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send('make a new realm please!'); - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let error = response.body.errors[0]; - assert.ok(error.match(/not valid JSON-API/), 'error message is correct'); - }); - - test('POST /_create-realm with bad JSON-API', async function (assert) { - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send( - JSON.stringify({ - name: 'mango-realm', - }), - ); - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let error = response.body.errors[0]; - assert.ok(error.match(/not valid JSON-API/), 'error message is correct'); - }); - - test('POST /_create-realm without a realm endpoint', async function (assert) { - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - name: 'Test Realm', - }, - }, - }), - ); - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let error = response.body.errors[0]; - assert.ok( - error.match(/endpoint is required and must be a string/), - 'error message is correct', - ); - }); - - test('POST /_create-realm without a realm name', async function (assert) { - let endpoint = `test-realm-${uuidv4()}`; - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - endpoint, - }, - }, - }), + `Bearer ${createRealmServerJWT( + { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send('make a new realm please!'); + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let error = response.body.errors[0]; + assert.ok( + error.match(/not valid JSON-API/), + 'error message is correct', ); - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let error = response.body.errors[0]; - assert.ok( - error.match(/name is required and must be a string/), - 'error message is correct', - ); - }); + }); - test('cannot create a realm on a realm server that has a realm mounted at the origin', async function (assert) { - let response = await request - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - endpoint: 'mango-realm', - name: 'Test Realm', - }, - }, - }), + test('POST /_create-realm with bad JSON-API', async function (assert) { + let response = await request2 + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ + name: 'mango-realm', + }), + ); + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let error = response.body.errors[0]; + assert.ok( + error.match(/not valid JSON-API/), + 'error message is correct', ); - assert.strictEqual(response.status, 400, 'HTTP 400 status'); - let error = response.body.errors[0]; - assert.ok( - error.match(/a realm is already mounted at the origin of this server/), - 'error message is correct', - ); - }); + }); - test('cannot create a new realm that collides with an existing realm', async function (assert) { - let endpoint = `test-realm-${uuidv4()}`; - let ownerUserId = '@mango:boxel.ai'; - let response = await request2 - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: ownerUserId, sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - endpoint, - name: 'Test Realm', - }, - }, - }), - ); - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - { + test('POST /_create-realm without a realm endpoint', async function (assert) { let response = await request2 .post('/_create-realm') .set('Accept', 'application/vnd.api+json') @@ -3641,7 +3564,7 @@ module('Realm Server', function (hooks) { .set( 'Authorization', `Bearer ${createRealmServerJWT( - { user: ownerUserId, sessionRoom: 'session-room-test' }, + { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, secretSeed, )}`, ) @@ -3650,8 +3573,7 @@ module('Realm Server', function (hooks) { data: { type: 'realm', attributes: { - endpoint, - name: 'Another Test Realm', + name: 'Test Realm', }, }, }), @@ -3659,15 +3581,13 @@ module('Realm Server', function (hooks) { assert.strictEqual(response.status, 400, 'HTTP 400 status'); let error = response.body.errors[0]; assert.ok( - error.match(/already exists on this server/), + error.match(/endpoint is required and must be a string/), 'error message is correct', ); - } - }); + }); - test('cannot create a realm with invalid characters in endpoint', async function (assert) { - let ownerUserId = '@mango:boxel.ai'; - { + test('POST /_create-realm without a realm name', async function (assert) { + let endpoint = `test-realm-${uuidv4()}`; let response = await request2 .post('/_create-realm') .set('Accept', 'application/vnd.api+json') @@ -3675,7 +3595,7 @@ module('Realm Server', function (hooks) { .set( 'Authorization', `Bearer ${createRealmServerJWT( - { user: ownerUserId, sessionRoom: 'session-room-test' }, + { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, secretSeed, )}`, ) @@ -3684,8 +3604,7 @@ module('Realm Server', function (hooks) { data: { type: 'realm', attributes: { - endpoint: 'invalid_realm_endpoint', - name: 'Test Realm', + endpoint, }, }, }), @@ -3693,19 +3612,20 @@ module('Realm Server', function (hooks) { assert.strictEqual(response.status, 400, 'HTTP 400 status'); let error = response.body.errors[0]; assert.ok( - error.match(/contains invalid characters/), + error.match(/name is required and must be a string/), 'error message is correct', ); - } - { - let response = await request2 + }); + + test('cannot create a realm on a realm server that has a realm mounted at the origin', async function (assert) { + let response = await request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') .set( 'Authorization', `Bearer ${createRealmServerJWT( - { user: ownerUserId, sessionRoom: 'session-room-test' }, + { user: '@mango:boxel.ai', sessionRoom: 'session-room-test' }, secretSeed, )}`, ) @@ -3714,7 +3634,7 @@ module('Realm Server', function (hooks) { data: { type: 'realm', attributes: { - endpoint: 'invalid realm endpoint', + endpoint: 'mango-realm', name: 'Test Realm', }, }, @@ -3723,566 +3643,1033 @@ module('Realm Server', function (hooks) { assert.strictEqual(response.status, 400, 'HTTP 400 status'); let error = response.body.errors[0]; assert.ok( - error.match(/contains invalid characters/), + error.match( + /a realm is already mounted at the origin of this server/, + ), 'error message is correct', ); - } - }); - - test('returns 404 for request that has malformed URI', async function (assert) { - let response = await request2.get('/%c0').set('Accept', '*/*'); - assert.strictEqual(response.status, 404, 'HTTP 404 status'); - }); - - test('can create a user', async function (assert) { - let ownerUserId = '@mango:boxel.ai'; - let response = await request2 - .post('/_user') - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: ownerUserId, sessionRoom: 'session-room-test' }, - secretSeed, - )}`, - ) - .send({ - data: { - type: 'user', - attributes: { - registrationToken: 'reg_token_123', - }, - }, - }); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual(response.text, 'ok', 'response body is correct'); - - let user = await getUserByMatrixUserId(dbAdapter, ownerUserId); - if (!user) { - throw new Error('user does not exist in db'); - } - assert.strictEqual( - user.matrixUserId, - ownerUserId, - 'matrix user ID is correct', - ); - assert.strictEqual( - user.matrixRegistrationToken, - 'reg_token_123', - 'registration token is correct', - ); - }); - - test('can not create a user without a jwt', async function (assert) { - let response = await request2.post('/_user').send({}); - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - }); - - test('can dynamically load a card definition from own realm', async function (assert) { - let ref = { - module: `${testRealmHref}person`, - name: 'Person', - }; - await loadCard(ref, { loader }); - let doc = { - data: { - attributes: { firstName: 'Mango' }, - meta: { adoptsFrom: ref }, - }, - }; - let api = await loader.import( - 'https://cardstack.com/base/card-api', - ); - let person = await api.createFromSerialized( - doc.data, - doc, - undefined, - ); - assert.strictEqual(person.firstName, 'Mango', 'card data is correct'); - }); - - test('can dynamically load a card definition from a different realm', async function (assert) { - let ref = { - module: `${testRealm2Href}person`, - name: 'Person', - }; - await loadCard(ref, { loader }); - let doc = { - data: { - attributes: { firstName: 'Mango' }, - meta: { adoptsFrom: ref }, - }, - }; - let api = await loader.import( - 'https://cardstack.com/base/card-api', - ); - let person = await api.createFromSerialized( - doc.data, - doc, - undefined, - ); - assert.strictEqual(person.firstName, 'Mango', 'card data is correct'); - }); - - test('can instantiate a card that uses a code-ref field', async function (assert) { - let adoptsFrom = { - module: `${testRealm2Href}code-ref-test`, - name: 'TestCard', - }; - await loadCard(adoptsFrom, { loader }); - let ref = { module: `${testRealm2Href}person`, name: 'Person' }; - let doc = { - data: { - attributes: { ref }, - meta: { adoptsFrom }, - }, - }; - let api = await loader.import( - 'https://cardstack.com/base/card-api', - ); - let testCard = await api.createFromSerialized( - doc.data, - doc, - undefined, - ); - assert.deepEqual(testCard.ref, ref, 'card data is correct'); - }); + }); - test('can index a newly added file to the filesystem', async function (assert) { - { - let response = await request - .get('/new-card') - .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 404, 'HTTP 404 status'); - } - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}new-card.json`, - }, - { - type: 'incremental', - realmURL: testRealmURL.href, - invalidations: [`${testRealmURL}new-card`], - }, - ]; - await expectEvent({ - assert, - expected, - callback: async () => { - writeJSONSync( - join(dir.name, 'realm_server_1', 'test', 'new-card.json'), - { + test('cannot create a new realm that collides with an existing realm', async function (assert) { + let endpoint = `test-realm-${uuidv4()}`; + let ownerUserId = '@mango:boxel.ai'; + let response = await request2 + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: ownerUserId, sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ data: { + type: 'realm', attributes: { - firstName: 'Mango', - }, - meta: { - adoptsFrom: { - module: './person', - name: 'Person', - }, + endpoint, + name: 'Test Realm', }, }, - } as LooseSingleCardDocument, + }), ); - }, - }); - - { - let response = await request - .get('/new-card') - .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - assert.ok(json.data.meta.lastModified, 'lastModified exists'); - delete json.data.meta.lastModified; - delete json.data.meta.resourceCreatedAt; - assert.strictEqual( - response.get('X-boxel-realm-url'), - testRealmURL.href, - 'realm url header is correct', - ); - assert.strictEqual( - response.get('X-boxel-realm-public-readable'), - 'true', - 'realm is public readable', - ); - assert.deepEqual(json, { - data: { - id: `${testRealmHref}new-card`, - type: 'card', - attributes: { - title: 'Mango', - firstName: 'Mango', - description: null, - thumbnailURL: null, - }, - meta: { - adoptsFrom: { - module: `./person`, - name: 'Person', - }, - realmInfo: testRealmInfo, - realmURL: testRealmURL.href, - }, - links: { - self: `${testRealmHref}new-card`, - }, - }, - }); - } - }); - - test('can index a changed file in the filesystem', async function (assert) { - { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json'); - let json = response.body as LooseSingleCardDocument; - assert.strictEqual( - json.data.attributes?.firstName, - 'Mango', - 'initial firstName value is correct', - ); - } - - let expected = [ - { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}person-1.json`, - }, + assert.strictEqual(response.status, 201, 'HTTP 201 status'); { - type: 'incremental', - realmURL: testRealmURL.href, - invalidations: [`${testRealmURL}person-1`], - }, - ]; - await expectEvent({ - assert, - expected, - callback: async () => { - writeJSONSync( - join(dir.name, 'realm_server_1', 'test', 'person-1.json'), - { - data: { - type: 'card', - attributes: { - firstName: 'Van Gogh', - }, - meta: { - adoptsFrom: { - module: './person.gts', - name: 'Person', + let response = await request2 + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: ownerUserId, sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + endpoint, + name: 'Another Test Realm', }, }, - }, - } as LooseSingleCardDocument, + }), + ); + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let error = response.body.errors[0]; + assert.ok( + error.match(/already exists on this server/), + 'error message is correct', ); - }, + } }); - { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json'); - let json = response.body as LooseSingleCardDocument; - assert.strictEqual( - json.data.attributes?.firstName, - 'Van Gogh', - 'updated firstName value is correct', - ); - } - }); - - test('can index a file deleted from the filesystem', async function (assert) { - { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - } - - let expected = [ + test('cannot create a realm with invalid characters in endpoint', async function (assert) { + let ownerUserId = '@mango:boxel.ai'; { - type: 'incremental-index-initiation', - realmURL: testRealmURL.href, - updatedFile: `${testRealmURL}person-1.json`, - }, + let response = await request2 + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: ownerUserId, sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + endpoint: 'invalid_realm_endpoint', + name: 'Test Realm', + }, + }, + }), + ); + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let error = response.body.errors[0]; + assert.ok( + error.match(/contains invalid characters/), + 'error message is correct', + ); + } { - type: 'incremental', - realmURL: testRealmURL.href, - invalidations: [`${testRealmURL}person-1`], - }, - ]; - await expectEvent({ - assert, - expected, - callback: async () => { - removeSync(join(dir.name, 'realm_server_1', 'test', 'person-1.json')); - }, + let response = await request2 + .post('/_create-realm') + .set('Accept', 'application/vnd.api+json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: ownerUserId, sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + endpoint: 'invalid realm endpoint', + name: 'Test Realm', + }, + }, + }), + ); + assert.strictEqual(response.status, 400, 'HTTP 400 status'); + let error = response.body.errors[0]; + assert.ok( + error.match(/contains invalid characters/), + 'error message is correct', + ); + } }); - { - let response = await request - .get('/person-1') - .set('Accept', 'application/vnd.card+json'); + test('returns 404 for request that has malformed URI', async function (assert) { + let response = await request2.get('/%c0').set('Accept', '*/*'); assert.strictEqual(response.status, 404, 'HTTP 404 status'); - } - }); + }); - test('can make HEAD request to get realmURL and isPublicReadable status', async function (assert) { - let response = await request - .head('/person-1') - .set('Accept', 'application/vnd.card+json'); + test('can create a user', async function (assert) { + let ownerUserId = '@mango:boxel.ai'; + let response = await request2 + .post('/_user') + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .set( + 'Authorization', + `Bearer ${createRealmServerJWT( + { user: ownerUserId, sessionRoom: 'session-room-test' }, + secretSeed, + )}`, + ) + .send({ + data: { + type: 'user', + attributes: { + registrationToken: 'reg_token_123', + }, + }, + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.strictEqual( - response.headers['x-boxel-realm-url'], - testRealmURL.href, - ); - assert.strictEqual( - response.headers['x-boxel-realm-public-readable'], - 'true', - ); - }); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual(response.text, 'ok', 'response body is correct'); - test('can fetch card type summary', async function (assert) { - let response = await request - .get('/_types') - .set('Accept', 'application/json'); + let user = await getUserByMatrixUserId(dbAdapter, ownerUserId); + if (!user) { + throw new Error('user does not exist in db'); + } + assert.strictEqual( + user.matrixUserId, + ownerUserId, + 'matrix user ID is correct', + ); + assert.strictEqual( + user.matrixRegistrationToken, + 'reg_token_123', + 'registration token is correct', + ); + }); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.deepEqual(response.body, { - data: [ + test('can not create a user without a jwt', async function (assert) { + let response = await request2.post('/_user').send({}); + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + }); + + test('can dynamically load a card definition from own realm', async function (assert) { + let ref = { + module: `${testRealmHref}person`, + name: 'Person', + }; + await loadCard(ref, { loader }); + let doc = { + data: { + attributes: { firstName: 'Mango' }, + meta: { adoptsFrom: ref }, + }, + }; + let api = await loader.import( + 'https://cardstack.com/base/card-api', + ); + let person = await api.createFromSerialized( + doc.data, + doc, + undefined, + ); + assert.strictEqual(person.firstName, 'Mango', 'card data is correct'); + }); + + test('can dynamically load a card definition from a different realm', async function (assert) { + let ref = { + module: `${testRealm2Href}person`, + name: 'Person', + }; + await loadCard(ref, { loader }); + let doc = { + data: { + attributes: { firstName: 'Mango' }, + meta: { adoptsFrom: ref }, + }, + }; + let api = await loader.import( + 'https://cardstack.com/base/card-api', + ); + let person = await api.createFromSerialized( + doc.data, + doc, + undefined, + ); + assert.strictEqual(person.firstName, 'Mango', 'card data is correct'); + }); + + test('can instantiate a card that uses a code-ref field', async function (assert) { + let adoptsFrom = { + module: `${testRealm2Href}code-ref-test`, + name: 'TestCard', + }; + await loadCard(adoptsFrom, { loader }); + let ref = { module: `${testRealm2Href}person`, name: 'Person' }; + let doc = { + data: { + attributes: { ref }, + meta: { adoptsFrom }, + }, + }; + let api = await loader.import( + 'https://cardstack.com/base/card-api', + ); + let testCard = await api.createFromSerialized( + doc.data, + doc, + undefined, + ); + assert.deepEqual(testCard.ref, ref, 'card data is correct'); + }); + + test('can index a newly added file to the filesystem', async function (assert) { + { + let response = await request + .get('/new-card') + .set('Accept', 'application/vnd.card+json'); + assert.strictEqual(response.status, 404, 'HTTP 404 status'); + } + let expected = [ { - type: 'card-type-summary', - id: `${testRealm.url}friend/Friend`, - attributes: { - displayName: 'Friend', - total: 2, - }, + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}new-card.json`, }, { - type: 'card-type-summary', - id: `${testRealm.url}home/Home`, - attributes: { - displayName: 'Home', - total: 1, + type: 'incremental', + realmURL: testRealmURL.href, + invalidations: [`${testRealmURL}new-card`], + }, + ]; + await expectEvent({ + assert, + expected, + callback: async () => { + writeJSONSync( + join(dir.name, 'realm_server_1', 'test', 'new-card.json'), + { + data: { + attributes: { + firstName: 'Mango', + }, + meta: { + adoptsFrom: { + module: './person', + name: 'Person', + }, + }, + }, + } as LooseSingleCardDocument, + ); + }, + }); + + { + let response = await request + .get('/new-card') + .set('Accept', 'application/vnd.card+json'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.ok(json.data.meta.lastModified, 'lastModified exists'); + delete json.data.meta.lastModified; + delete json.data.meta.resourceCreatedAt; + assert.strictEqual( + response.get('X-boxel-realm-url'), + testRealmURL.href, + 'realm url header is correct', + ); + assert.strictEqual( + response.get('X-boxel-realm-public-readable'), + 'true', + 'realm is public readable', + ); + assert.deepEqual(json, { + data: { + id: `${testRealmHref}new-card`, + type: 'card', + attributes: { + title: 'Mango', + firstName: 'Mango', + description: null, + thumbnailURL: null, + }, + meta: { + adoptsFrom: { + module: `./person`, + name: 'Person', + }, + realmInfo: testRealmInfo, + realmURL: testRealmURL.href, + }, + links: { + self: `${testRealmHref}new-card`, + }, }, + }); + } + }); + + test('can index a changed file in the filesystem', async function (assert) { + { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json'); + let json = response.body as LooseSingleCardDocument; + assert.strictEqual( + json.data.attributes?.firstName, + 'Mango', + 'initial firstName value is correct', + ); + } + + let expected = [ + { + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}person-1.json`, }, { - type: 'card-type-summary', - id: `${testRealm.url}person/Person`, - attributes: { - displayName: 'Person', - total: 3, - }, + type: 'incremental', + realmURL: testRealmURL.href, + invalidations: [`${testRealmURL}person-1`], }, - ], + ]; + await expectEvent({ + assert, + expected, + callback: async () => { + writeJSONSync( + join(dir.name, 'realm_server_1', 'test', 'person-1.json'), + { + data: { + type: 'card', + attributes: { + firstName: 'Van Gogh', + }, + meta: { + adoptsFrom: { + module: './person.gts', + name: 'Person', + }, + }, + }, + } as LooseSingleCardDocument, + ); + }, + }); + + { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json'); + let json = response.body as LooseSingleCardDocument; + assert.strictEqual( + json.data.attributes?.firstName, + 'Van Gogh', + 'updated firstName value is correct', + ); + } }); - }); - test('can fetch catalog realms', async function (assert) { - let response = await request2 - .get('/_catalog-realms') - .set('Accept', 'application/json'); + test('can index a file deleted from the filesystem', async function (assert) { + { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + } - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.deepEqual(response.body, { - data: [ + let expected = [ { - type: 'catalog-realm', - id: `${testRealm2URL}`, - attributes: testRealmInfo, + type: 'incremental-index-initiation', + realmURL: testRealmURL.href, + updatedFile: `${testRealmURL}person-1.json`, + }, + { + type: 'incremental', + realmURL: testRealmURL.href, + invalidations: [`${testRealmURL}person-1`], }, - ], + ]; + await expectEvent({ + assert, + expected, + callback: async () => { + removeSync( + join(dir.name, 'realm_server_1', 'test', 'person-1.json'), + ); + }, + }); + + { + let response = await request + .get('/person-1') + .set('Accept', 'application/vnd.card+json'); + assert.strictEqual(response.status, 404, 'HTTP 404 status'); + } }); - }); - test(`returns 200 with empty data if failed to fetch catalog realm's info`, async function (assert) { - virtualNetwork.mount( - async (req: Request) => { - if (req.url.includes('_info')) { - return new Response('Failed to fetch realm info', { - status: 500, - statusText: 'Internal Server Error', - }); - } - return null; - }, - { prepend: true }, - ); - let response = await request2 - .get('/_catalog-realms') - .set('Accept', 'application/json'); + test('can make HEAD request to get realmURL and isPublicReadable status', async function (assert) { + let response = await request + .head('/person-1') + .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - assert.deepEqual(response.body, { - data: [], + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.strictEqual( + response.headers['x-boxel-realm-url'], + testRealmURL.href, + ); + assert.strictEqual( + response.headers['x-boxel-realm-public-readable'], + 'true', + ); }); - }); - }); - module('stripe webhook handler', function (hooks) { - let createSubscriptionStub: sinon.SinonStub; - let fetchPriceListStub: sinon.SinonStub; - let matrixClient: MatrixClient; - let roomId: string; - let userId = '@test_realm:localhost'; - let waitForBillingNotification = async function ( - assert: Assert, - done: () => void, - ) { - let messages = await matrixClient.roomMessages(roomId); - if ( - messages[0].content.msgtype === APP_BOXEL_REALM_SERVER_EVENT_MSGTYPE - ) { - assert.strictEqual( - messages[0].content.body, - JSON.stringify({ eventType: 'billing-notification' }), + test('can fetch card type summary', async function (assert) { + let response = await request + .get('/_types') + .set('Accept', 'application/json'); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.deepEqual(response.body, { + data: [ + { + type: 'card-type-summary', + id: `${testRealm.url}friend/Friend`, + attributes: { + displayName: 'Friend', + total: 2, + }, + }, + { + type: 'card-type-summary', + id: `${testRealm.url}home/Home`, + attributes: { + displayName: 'Home', + total: 1, + }, + }, + { + type: 'card-type-summary', + id: `${testRealm.url}person/Person`, + attributes: { + displayName: 'Person', + total: 3, + }, + }, + ], + }); + }); + + test('can fetch catalog realms', async function (assert) { + let response = await request2 + .get('/_catalog-realms') + .set('Accept', 'application/json'); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.deepEqual(response.body, { + data: [ + { + type: 'catalog-realm', + id: `${testRealm2URL}`, + attributes: testRealmInfo, + }, + ], + }); + }); + + test(`returns 200 with empty data if failed to fetch catalog realm's info`, async function (assert) { + virtualNetwork.mount( + async (req: Request) => { + if (req.url.includes('_info')) { + return new Response('Failed to fetch realm info', { + status: 500, + statusText: 'Internal Server Error', + }); + } + return null; + }, + { prepend: true }, ); - done(); - } else { - setTimeout(() => waitForBillingNotification(assert, done), 1); - } - }; + let response = await request2 + .get('/_catalog-realms') + .set('Accept', 'application/json'); - setupPermissionedRealm(hooks, { - '*': ['read', 'write'], + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + assert.deepEqual(response.body, { + data: [], + }); + }); }); - hooks.beforeEach(async function () { - shimExternals(virtualNetwork); - let stripe = getStripe(); - createSubscriptionStub = sinon.stub(stripe.subscriptions, 'create'); - fetchPriceListStub = sinon.stub(stripe.prices, 'list'); + module('stripe webhook handler', function (hooks) { + let createSubscriptionStub: sinon.SinonStub; + let fetchPriceListStub: sinon.SinonStub; + let matrixClient: MatrixClient; + let roomId: string; + let userId = '@test_realm:localhost'; + let waitForBillingNotification = async function ( + assert: Assert, + done: () => void, + ) { + let messages = await matrixClient.roomMessages(roomId); + if ( + messages[0].content.msgtype === APP_BOXEL_REALM_SERVER_EVENT_MSGTYPE + ) { + assert.strictEqual( + messages[0].content.body, + JSON.stringify({ eventType: 'billing-notification' }), + ); + done(); + } else { + setTimeout(() => waitForBillingNotification(assert, done), 1); + } + }; - matrixClient = new MatrixClient({ - matrixURL: realmServerTestMatrix.url, - username: 'test_realm', - seed: secretSeed, + setupPermissionedRealm(hooks, { + '*': ['read', 'write'], }); - await matrixClient.login(); - let userId = matrixClient.getUserId(); - let response = await request - .post('/_server-session') - .send(JSON.stringify({ user: userId })) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json'); - let json = response.body; + hooks.beforeEach(async function () { + shimExternals(virtualNetwork); + let stripe = getStripe(); + createSubscriptionStub = sinon.stub(stripe.subscriptions, 'create'); + fetchPriceListStub = sinon.stub(stripe.prices, 'list'); - let { joined_rooms: rooms } = await matrixClient.getJoinedRooms(); + matrixClient = new MatrixClient({ + matrixURL: realmServerTestMatrix.url, + username: 'test_realm', + seed: secretSeed, + }); + await matrixClient.login(); + let userId = matrixClient.getUserId(); - if (!rooms.includes(json.room)) { - await matrixClient.joinRoom(json.room); - } + let response = await request + .post('/_server-session') + .send(JSON.stringify({ user: userId })) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json'); + let json = response.body; - await matrixClient.sendEvent(json.room, 'm.room.message', { - body: `auth-response: ${json.challenge}`, - msgtype: 'm.text', + let { joined_rooms: rooms } = await matrixClient.getJoinedRooms(); + + if (!rooms.includes(json.room)) { + await matrixClient.joinRoom(json.room); + } + + await matrixClient.sendEvent(json.room, 'm.room.message', { + body: `auth-response: ${json.challenge}`, + msgtype: 'm.text', + }); + + response = await request + .post('/_server-session') + .send(JSON.stringify({ user: userId, challenge: json.challenge })) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json'); + roomId = json.room; }); - response = await request - .post('/_server-session') - .send(JSON.stringify({ user: userId, challenge: json.challenge })) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json'); - roomId = json.room; - }); + hooks.afterEach(async function () { + createSubscriptionStub.restore(); + fetchPriceListStub.restore(); + }); - hooks.afterEach(async function () { - createSubscriptionStub.restore(); - fetchPriceListStub.restore(); - }); + test('subscribes user back to free plan when the current subscription is expired', async function (assert) { + const secret = process.env.STRIPE_WEBHOOK_SECRET; + let user = await insertUser( + dbAdapter, + userId, + 'cus_123', + 'user@test.com', + ); + let freePlan = await insertPlan( + dbAdapter, + 'Free plan', + 0, + 100, + 'prod_free', + ); + let creatorPlan = await insertPlan( + dbAdapter, + 'Creator', + 12, + 5000, + 'prod_creator', + ); - test('subscribes user back to free plan when the current subscription is expired', async function (assert) { - const secret = process.env.STRIPE_WEBHOOK_SECRET; - let user = await insertUser( - dbAdapter, - userId, - 'cus_123', - 'user@test.com', - ); - let freePlan = await insertPlan( - dbAdapter, - 'Free plan', - 0, - 100, - 'prod_free', - ); - let creatorPlan = await insertPlan( - dbAdapter, - 'Creator', - 12, - 5000, - 'prod_creator', - ); + if (!secret) { + throw new Error('STRIPE_WEBHOOK_SECRET is not set'); + } + let stripeInvoicePaymentSucceededEvent = { + id: 'evt_1234567890', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: 12, + billing_reason: 'subscription_create', + period_end: 1638465600, + period_start: 1635873600, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: 12, + price: { product: 'prod_creator' }, + }, + ], + }, + }, + }, + }; - if (!secret) { - throw new Error('STRIPE_WEBHOOK_SECRET is not set'); - } - let stripeInvoicePaymentSucceededEvent = { - id: 'evt_1234567890', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: 12, - billing_reason: 'subscription_create', - period_end: 1638465600, - period_start: 1635873600, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: 12, - price: { product: 'prod_creator' }, + let timestamp = Math.floor(Date.now() / 1000); + let stripeInvoicePaymentSucceededPayload = JSON.stringify( + stripeInvoicePaymentSucceededEvent, + ); + let stripeInvoicePaymentSucceededSignature = + Stripe.webhooks.generateTestHeaderString({ + payload: stripeInvoicePaymentSucceededPayload, + secret, + timestamp, + }); + await request + .post('/_stripe-webhook') + .send(stripeInvoicePaymentSucceededPayload) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .set('stripe-signature', stripeInvoicePaymentSucceededSignature); + + let subscriptions = await fetchSubscriptionsByUserId( + dbAdapter, + user.id, + ); + assert.strictEqual(subscriptions.length, 1); + assert.strictEqual(subscriptions[0].status, 'active'); + assert.strictEqual(subscriptions[0].planId, creatorPlan.id); + + let waitForSubscriptionExpiryProcessed = new Deferred(); + let waitForFreePlanSubscriptionProcessed = new Deferred(); + + // A function to simulate webhook call from stripe after we call 'stripe.subscription.create' endpoint + let subscribeToFreePlan = async function () { + await waitForSubscriptionExpiryProcessed.promise; + let stripeInvoicePaymentSucceededEvent = { + id: 'evt_1234567892', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: 0, // free plan + billing_reason: 'subscription_create', + period_end: 1638465600, + period_start: 1635873600, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: 0, + price: { product: 'prod_free' }, + }, + ], }, - ], + }, }, + }; + let stripeInvoicePaymentSucceededPayload = JSON.stringify( + stripeInvoicePaymentSucceededEvent, + ); + let stripeInvoicePaymentSucceededSignature = + Stripe.webhooks.generateTestHeaderString({ + payload: stripeInvoicePaymentSucceededPayload, + secret, + timestamp, + }); + await request + .post('/_stripe-webhook') + .send(stripeInvoicePaymentSucceededPayload) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .set('stripe-signature', stripeInvoicePaymentSucceededSignature); + waitForFreePlanSubscriptionProcessed.fulfill(); + }; + const createSubscriptionResponse = { + id: 'sub_1MowQVLkdIwHu7ixeRlqHVzs', + object: 'subscription', + automatic_tax: { + enabled: false, }, - }, - }; + billing_cycle_anchor: 1679609767, + cancel_at_period_end: false, + collection_method: 'charge_automatically', + created: 1679609767, + currency: 'usd', + current_period_end: 1682288167, + current_period_start: 1679609767, + customer: 'cus_123', + invoice_settings: { + issuer: { + type: 'self', + }, + }, + }; + createSubscriptionStub.callsFake(() => { + subscribeToFreePlan(); + return createSubscriptionResponse; + }); - let timestamp = Math.floor(Date.now() / 1000); - let stripeInvoicePaymentSucceededPayload = JSON.stringify( - stripeInvoicePaymentSucceededEvent, - ); - let stripeInvoicePaymentSucceededSignature = - Stripe.webhooks.generateTestHeaderString({ - payload: stripeInvoicePaymentSucceededPayload, - secret, - timestamp, + let fetchPriceListResponse = { + object: 'list', + data: [ + { + id: 'price_1QMRCxH9rBd1yAHRD4BXhAHW', + object: 'price', + active: true, + billing_scheme: 'per_unit', + created: 1731921923, + currency: 'usd', + custom_unit_amount: null, + livemode: false, + lookup_key: null, + metadata: {}, + nickname: null, + product: 'prod_REv3E69DbAPv4K', + recurring: { + aggregate_usage: null, + interval: 'month', + interval_count: 1, + meter: null, + trial_period_days: null, + usage_type: 'licensed', + }, + tax_behavior: 'unspecified', + tiers_mode: null, + transform_quantity: null, + type: 'recurring', + unit_amount: 0, + unit_amount_decimal: '0', + }, + ], + has_more: false, + url: '/v1/prices', + }; + fetchPriceListStub.resolves(fetchPriceListResponse); + + let stripeSubscriptionDeletedEvent = { + id: 'evt_sub_deleted_1', + object: 'event', + type: 'customer.subscription.deleted', + data: { + object: { + id: 'sub_1234567890', + canceled_at: 2, + cancellation_details: { + reason: 'payment_failure', + }, + customer: 'cus_123', + }, + }, + }; + let stripeSubscriptionDeletedPayload = JSON.stringify( + stripeSubscriptionDeletedEvent, + ); + let stripeSubscriptionDeletedSignature = + Stripe.webhooks.generateTestHeaderString({ + payload: stripeSubscriptionDeletedPayload, + secret, + timestamp, + }); + await request + .post('/_stripe-webhook') + .send(stripeSubscriptionDeletedPayload) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .set('stripe-signature', stripeSubscriptionDeletedSignature); + waitForSubscriptionExpiryProcessed.fulfill(); + + await waitForFreePlanSubscriptionProcessed.promise; + subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); + assert.strictEqual(subscriptions.length, 2); + assert.strictEqual(subscriptions[0].status, 'expired'); + assert.strictEqual(subscriptions[0].planId, creatorPlan.id); + + assert.strictEqual(subscriptions[1].status, 'active'); + assert.strictEqual(subscriptions[1].planId, freePlan.id); + waitForBillingNotification(assert, assert.async()); + }); + + test('ensures the current subscription expires when free plan subscription fails', async function (assert) { + const secret = process.env.STRIPE_WEBHOOK_SECRET; + let user = await insertUser( + dbAdapter, + userId, + 'cus_123', + 'user@test.com', + ); + await insertPlan(dbAdapter, 'Free plan', 0, 100, 'prod_free'); + let creatorPlan = await insertPlan( + dbAdapter, + 'Creator', + 12, + 5000, + 'prod_creator', + ); + + if (!secret) { + throw new Error('STRIPE_WEBHOOK_SECRET is not set'); + } + let stripeInvoicePaymentSucceededEvent = { + id: 'evt_1234567890', + object: 'event', + type: 'invoice.payment_succeeded', + data: { + object: { + id: 'in_1234567890', + object: 'invoice', + amount_paid: 12, + billing_reason: 'subscription_create', + period_end: 1638465600, + period_start: 1635873600, + subscription: 'sub_1234567890', + customer: 'cus_123', + lines: { + data: [ + { + amount: 12, + price: { product: 'prod_creator' }, + }, + ], + }, + }, + }, + }; + + let timestamp = Math.floor(Date.now() / 1000); + let stripeInvoicePaymentSucceededPayload = JSON.stringify( + stripeInvoicePaymentSucceededEvent, + ); + let stripeInvoicePaymentSucceededSignature = + Stripe.webhooks.generateTestHeaderString({ + payload: stripeInvoicePaymentSucceededPayload, + secret, + timestamp, + }); + await request + .post('/_stripe-webhook') + .send(stripeInvoicePaymentSucceededPayload) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .set('stripe-signature', stripeInvoicePaymentSucceededSignature); + + let subscriptions = await fetchSubscriptionsByUserId( + dbAdapter, + user.id, + ); + assert.strictEqual(subscriptions.length, 1); + assert.strictEqual(subscriptions[0].status, 'active'); + assert.strictEqual(subscriptions[0].planId, creatorPlan.id); + + createSubscriptionStub.throws({ + message: 'Failed subscribing to free plan', }); - await request - .post('/_stripe-webhook') - .send(stripeInvoicePaymentSucceededPayload) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .set('stripe-signature', stripeInvoicePaymentSucceededSignature); + let fetchPriceListResponse = { + object: 'list', + data: [ + { + id: 'price_1QMRCxH9rBd1yAHRD4BXhAHW', + object: 'price', + active: true, + billing_scheme: 'per_unit', + created: 1731921923, + currency: 'usd', + custom_unit_amount: null, + livemode: false, + lookup_key: null, + metadata: {}, + nickname: null, + product: 'prod_REv3E69DbAPv4K', + recurring: { + aggregate_usage: null, + interval: 'month', + interval_count: 1, + meter: null, + trial_period_days: null, + usage_type: 'licensed', + }, + tax_behavior: 'unspecified', + tiers_mode: null, + transform_quantity: null, + type: 'recurring', + unit_amount: 0, + unit_amount_decimal: '0', + }, + ], + has_more: false, + url: '/v1/prices', + }; + fetchPriceListStub.resolves(fetchPriceListResponse); + + let stripeSubscriptionDeletedEvent = { + id: 'evt_sub_deleted_1', + object: 'event', + type: 'customer.subscription.deleted', + data: { + object: { + id: 'sub_1234567890', + canceled_at: 2, + cancellation_details: { + reason: 'payment_failure', + }, + customer: 'cus_123', + }, + }, + }; + let stripeSubscriptionDeletedPayload = JSON.stringify( + stripeSubscriptionDeletedEvent, + ); + let stripeSubscriptionDeletedSignature = + Stripe.webhooks.generateTestHeaderString({ + payload: stripeSubscriptionDeletedPayload, + secret, + timestamp, + }); + await request + .post('/_stripe-webhook') + .send(stripeSubscriptionDeletedPayload) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .set('stripe-signature', stripeSubscriptionDeletedSignature); - let subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 1); - assert.strictEqual(subscriptions[0].status, 'active'); - assert.strictEqual(subscriptions[0].planId, creatorPlan.id); + subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); + assert.strictEqual(subscriptions.length, 1); + assert.strictEqual(subscriptions[0].status, 'expired'); + assert.strictEqual(subscriptions[0].planId, creatorPlan.id); - let waitForSubscriptionExpiryProcessed = new Deferred(); - let waitForFreePlanSubscriptionProcessed = new Deferred(); + // ensures the subscription info is null, + // so the host can use that to redirect user to checkout free plan page + let response = await request + .get(`/_user`) + .set('Accept', 'application/vnd.api+json') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, '@test_realm:localhost', [ + 'read', + 'write', + ])}`, + ); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + assert.deepEqual( + json, + { + data: { + type: 'user', + id: user.id, + attributes: { + matrixUserId: user.matrixUserId, + stripeCustomerId: user.stripeCustomerId, + stripeCustomerEmail: user.stripeCustomerEmail, + creditsAvailableInPlanAllowance: null, + creditsIncludedInPlanAllowance: null, + extraCreditsAvailableInBalance: null, + }, + relationships: { + subscription: null, + }, + }, + included: null, + }, + '/_user response is correct', + ); + }); - // A function to simulate webhook call from stripe after we call 'stripe.subscription.create' endpoint - let subscribeToFreePlan = async function () { - await waitForSubscriptionExpiryProcessed.promise; - let stripeInvoicePaymentSucceededEvent = { - id: 'evt_1234567892', + test('sends billing notification on invoice payment succeeded event', async function (assert) { + const secret = process.env.STRIPE_WEBHOOK_SECRET; + await insertUser(dbAdapter, userId!, 'cus_123', 'user@test.com'); + await insertPlan(dbAdapter, 'Free plan', 0, 100, 'prod_free'); + if (!secret) { + throw new Error('STRIPE_WEBHOOK_SECRET is not set'); + } + let event = { + id: 'evt_1234567890', object: 'event', type: 'invoice.payment_succeeded', data: { @@ -4306,978 +4693,643 @@ module('Realm Server', function (hooks) { }, }, }; - let stripeInvoicePaymentSucceededPayload = JSON.stringify( - stripeInvoicePaymentSucceededEvent, - ); - let stripeInvoicePaymentSucceededSignature = - Stripe.webhooks.generateTestHeaderString({ - payload: stripeInvoicePaymentSucceededPayload, - secret, - timestamp, - }); + + let payload = JSON.stringify(event); + let timestamp = Math.floor(Date.now() / 1000); + let signature = Stripe.webhooks.generateTestHeaderString({ + payload, + secret, + timestamp, + }); + await request .post('/_stripe-webhook') - .send(stripeInvoicePaymentSucceededPayload) + .send(payload) .set('Accept', 'application/json') .set('Content-Type', 'application/json') - .set('stripe-signature', stripeInvoicePaymentSucceededSignature); - waitForFreePlanSubscriptionProcessed.fulfill(); - }; - const createSubscriptionResponse = { - id: 'sub_1MowQVLkdIwHu7ixeRlqHVzs', - object: 'subscription', - automatic_tax: { - enabled: false, - }, - billing_cycle_anchor: 1679609767, - cancel_at_period_end: false, - collection_method: 'charge_automatically', - created: 1679609767, - currency: 'usd', - current_period_end: 1682288167, - current_period_start: 1679609767, - customer: 'cus_123', - invoice_settings: { - issuer: { - type: 'self', - }, - }, - }; - createSubscriptionStub.callsFake(() => { - subscribeToFreePlan(); - return createSubscriptionResponse; + .set('stripe-signature', signature); + waitForBillingNotification(assert, assert.async()); }); - let fetchPriceListResponse = { - object: 'list', - data: [ - { - id: 'price_1QMRCxH9rBd1yAHRD4BXhAHW', - object: 'price', - active: true, - billing_scheme: 'per_unit', - created: 1731921923, - currency: 'usd', - custom_unit_amount: null, - livemode: false, - lookup_key: null, - metadata: {}, - nickname: null, - product: 'prod_REv3E69DbAPv4K', - recurring: { - aggregate_usage: null, - interval: 'month', - interval_count: 1, - meter: null, - trial_period_days: null, - usage_type: 'licensed', - }, - tax_behavior: 'unspecified', - tiers_mode: null, - transform_quantity: null, - type: 'recurring', - unit_amount: 0, - unit_amount_decimal: '0', - }, - ], - has_more: false, - url: '/v1/prices', - }; - fetchPriceListStub.resolves(fetchPriceListResponse); - - let stripeSubscriptionDeletedEvent = { - id: 'evt_sub_deleted_1', - object: 'event', - type: 'customer.subscription.deleted', - data: { - object: { - id: 'sub_1234567890', - canceled_at: 2, - cancellation_details: { - reason: 'payment_failure', + test('sends billing notification on checkout session completed event', async function (assert) { + const secret = process.env.STRIPE_WEBHOOK_SECRET; + await insertUser(dbAdapter, userId!, 'cus_123', 'user@test.com'); + await insertPlan(dbAdapter, 'Free plan', 0, 100, 'prod_free'); + if (!secret) { + throw new Error('STRIPE_WEBHOOK_SECRET is not set'); + } + let event = { + id: 'evt_1234567890', + object: 'event', + data: { + object: { + id: 'cs_test_1234567890', + object: 'checkout.session', + client_reference_id: encodeWebSafeBase64(userId), + customer: undefined, + metadata: {}, }, - customer: 'cus_123', }, - }, - }; - let stripeSubscriptionDeletedPayload = JSON.stringify( - stripeSubscriptionDeletedEvent, - ); - let stripeSubscriptionDeletedSignature = - Stripe.webhooks.generateTestHeaderString({ - payload: stripeSubscriptionDeletedPayload, + type: 'checkout.session.completed', + }; + + let payload = JSON.stringify(event); + let timestamp = Math.floor(Date.now() / 1000); + let signature = Stripe.webhooks.generateTestHeaderString({ + payload, secret, timestamp, }); - await request - .post('/_stripe-webhook') - .send(stripeSubscriptionDeletedPayload) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .set('stripe-signature', stripeSubscriptionDeletedSignature); - waitForSubscriptionExpiryProcessed.fulfill(); - - await waitForFreePlanSubscriptionProcessed.promise; - subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 2); - assert.strictEqual(subscriptions[0].status, 'expired'); - assert.strictEqual(subscriptions[0].planId, creatorPlan.id); - - assert.strictEqual(subscriptions[1].status, 'active'); - assert.strictEqual(subscriptions[1].planId, freePlan.id); - waitForBillingNotification(assert, assert.async()); + + await request + .post('/_stripe-webhook') + .send(payload) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json') + .set('stripe-signature', signature); + waitForBillingNotification(assert, assert.async()); + }); }); + }); - test('ensures the current subscription expires when free plan subscription fails', async function (assert) { - const secret = process.env.STRIPE_WEBHOOK_SECRET; - let user = await insertUser( - dbAdapter, - userId, - 'cus_123', - 'user@test.com', - ); - await insertPlan(dbAdapter, 'Free plan', 0, 100, 'prod_free'); - let creatorPlan = await insertPlan( - dbAdapter, - 'Creator', - 12, - 5000, - 'prod_creator', - ); + module('Realm server with realm mounted at the origin', function (hooks) { + let testRealmServer: Server; - if (!secret) { - throw new Error('STRIPE_WEBHOOK_SECRET is not set'); - } - let stripeInvoicePaymentSucceededEvent = { - id: 'evt_1234567890', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: 12, - billing_reason: 'subscription_create', - period_end: 1638465600, - period_start: 1635873600, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: 12, - price: { product: 'prod_creator' }, - }, - ], - }, - }, - }, - }; + let request: SuperTest; - let timestamp = Math.floor(Date.now() / 1000); - let stripeInvoicePaymentSucceededPayload = JSON.stringify( - stripeInvoicePaymentSucceededEvent, - ); - let stripeInvoicePaymentSucceededSignature = - Stripe.webhooks.generateTestHeaderString({ - payload: stripeInvoicePaymentSucceededPayload, - secret, - timestamp, - }); - await request - .post('/_stripe-webhook') - .send(stripeInvoicePaymentSucceededPayload) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .set('stripe-signature', stripeInvoicePaymentSucceededSignature); + let dir: DirResult; - let subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 1); - assert.strictEqual(subscriptions[0].status, 'active'); - assert.strictEqual(subscriptions[0].planId, creatorPlan.id); + let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); - createSubscriptionStub.throws({ - message: 'Failed subscribing to free plan', - }); - let fetchPriceListResponse = { - object: 'list', - data: [ - { - id: 'price_1QMRCxH9rBd1yAHRD4BXhAHW', - object: 'price', - active: true, - billing_scheme: 'per_unit', - created: 1731921923, - currency: 'usd', - custom_unit_amount: null, - livemode: false, - lookup_key: null, - metadata: {}, - nickname: null, - product: 'prod_REv3E69DbAPv4K', - recurring: { - aggregate_usage: null, - interval: 'month', - interval_count: 1, - meter: null, - trial_period_days: null, - usage_type: 'licensed', - }, - tax_behavior: 'unspecified', - tiers_mode: null, - transform_quantity: null, - type: 'recurring', - unit_amount: 0, - unit_amount_decimal: '0', - }, - ], - has_more: false, - url: '/v1/prices', - }; - fetchPriceListStub.resolves(fetchPriceListResponse); - - let stripeSubscriptionDeletedEvent = { - id: 'evt_sub_deleted_1', - object: 'event', - type: 'customer.subscription.deleted', - data: { - object: { - id: 'sub_1234567890', - canceled_at: 2, - cancellation_details: { - reason: 'payment_failure', - }, - customer: 'cus_123', - }, - }, - }; - let stripeSubscriptionDeletedPayload = JSON.stringify( - stripeSubscriptionDeletedEvent, - ); - let stripeSubscriptionDeletedSignature = - Stripe.webhooks.generateTestHeaderString({ - payload: stripeSubscriptionDeletedPayload, - secret, - timestamp, - }); - await request - .post('/_stripe-webhook') - .send(stripeSubscriptionDeletedPayload) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .set('stripe-signature', stripeSubscriptionDeletedSignature); + setupCardLogs( + hooks, + async () => await loader.import(`${baseRealm.url}card-api`), + ); + + setupBaseRealmServer(hooks, virtualNetwork, matrixURL); + + hooks.beforeEach(async function () { + dir = dirSync(); + }); - subscriptions = await fetchSubscriptionsByUserId(dbAdapter, user.id); - assert.strictEqual(subscriptions.length, 1); - assert.strictEqual(subscriptions[0].status, 'expired'); - assert.strictEqual(subscriptions[0].planId, creatorPlan.id); + setupDB(hooks, { + beforeEach: async (dbAdapter, publisher, runner) => { + let testRealmDir = join(dir.name, 'realm_server_3', 'test'); + ensureDirSync(testRealmDir); + copySync(join(__dirname, 'cards'), testRealmDir); + testRealmServer = ( + await runTestRealmServer({ + virtualNetwork: createVirtualNetwork(), + testRealmDir, + realmsRootPath: join(dir.name, 'realm_server_3'), + realmURL: testRealmURL, + dbAdapter, + publisher, + runner, + matrixURL, + }) + ).testRealmHttpServer; + request = supertest(testRealmServer); + }, + afterEach: async () => { + await closeServer(testRealmServer); + }, + }); - // ensures the subscription info is null, - // so the host can use that to redirect user to checkout free plan page + test('serves an origin realm directory GET request', async function (assert) { let response = await request - .get(`/_user`) - .set('Accept', 'application/vnd.api+json') - .set( - 'Authorization', - `Bearer ${createJWT(testRealm, '@test_realm:localhost', [ - 'read', - 'write', - ])}`, - ); + .get('/') + .set('Accept', 'application/vnd.api+json'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); let json = response.body; + for (let relationship of Object.values(json.data.relationships)) { + delete (relationship as any).meta.lastModified; + } assert.deepEqual( json, { data: { - type: 'user', - id: user.id, - attributes: { - matrixUserId: user.matrixUserId, - stripeCustomerId: user.stripeCustomerId, - stripeCustomerEmail: user.stripeCustomerEmail, - creditsAvailableInPlanAllowance: null, - creditsIncludedInPlanAllowance: null, - extraCreditsAvailableInBalance: null, - }, + id: testRealmHref, + type: 'directory', relationships: { - subscription: null, - }, - }, - included: null, - }, - '/_user response is correct', - ); - }); - - test('sends billing notification on invoice payment succeeded event', async function (assert) { - const secret = process.env.STRIPE_WEBHOOK_SECRET; - await insertUser(dbAdapter, userId!, 'cus_123', 'user@test.com'); - await insertPlan(dbAdapter, 'Free plan', 0, 100, 'prod_free'); - if (!secret) { - throw new Error('STRIPE_WEBHOOK_SECRET is not set'); - } - let event = { - id: 'evt_1234567890', - object: 'event', - type: 'invoice.payment_succeeded', - data: { - object: { - id: 'in_1234567890', - object: 'invoice', - amount_paid: 0, // free plan - billing_reason: 'subscription_create', - period_end: 1638465600, - period_start: 1635873600, - subscription: 'sub_1234567890', - customer: 'cus_123', - lines: { - data: [ - { - amount: 0, - price: { product: 'prod_free' }, + '%F0%9F%98%80.gts': { + links: { + related: 'http://127.0.0.1:4444/%F0%9F%98%80.gts', + }, + meta: { + kind: 'file', }, - ], - }, - }, - }, - }; - - let payload = JSON.stringify(event); - let timestamp = Math.floor(Date.now() / 1000); - let signature = Stripe.webhooks.generateTestHeaderString({ - payload, - secret, - timestamp, - }); - - await request - .post('/_stripe-webhook') - .send(payload) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .set('stripe-signature', signature); - waitForBillingNotification(assert, assert.async()); - }); - - test('sends billing notification on checkout session completed event', async function (assert) { - const secret = process.env.STRIPE_WEBHOOK_SECRET; - await insertUser(dbAdapter, userId!, 'cus_123', 'user@test.com'); - await insertPlan(dbAdapter, 'Free plan', 0, 100, 'prod_free'); - if (!secret) { - throw new Error('STRIPE_WEBHOOK_SECRET is not set'); - } - let event = { - id: 'evt_1234567890', - object: 'event', - data: { - object: { - id: 'cs_test_1234567890', - object: 'checkout.session', - client_reference_id: encodeWebSafeBase64(userId), - customer: undefined, - metadata: {}, - }, - }, - type: 'checkout.session.completed', - }; - - let payload = JSON.stringify(event); - let timestamp = Math.floor(Date.now() / 1000); - let signature = Stripe.webhooks.generateTestHeaderString({ - payload, - secret, - timestamp, - }); - - await request - .post('/_stripe-webhook') - .send(payload) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .set('stripe-signature', signature); - waitForBillingNotification(assert, assert.async()); - }); - }); -}); - -module('Realm server with realm mounted at the origin', function (hooks) { - let testRealmServer: Server; - - let request: SuperTest; - - let dir: DirResult; - - let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); - - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); - - setupBaseRealmServer(hooks, virtualNetwork, matrixURL); - - hooks.beforeEach(async function () { - dir = dirSync(); - }); - - setupDB(hooks, { - beforeEach: async (dbAdapter, publisher, runner) => { - let testRealmDir = join(dir.name, 'realm_server_3', 'test'); - ensureDirSync(testRealmDir); - copySync(join(__dirname, 'cards'), testRealmDir); - testRealmServer = ( - await runTestRealmServer({ - virtualNetwork: createVirtualNetwork(), - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_3'), - realmURL: testRealmURL, - dbAdapter, - publisher, - runner, - matrixURL, - }) - ).testRealmHttpServer; - request = supertest(testRealmServer); - }, - afterEach: async () => { - await closeServer(testRealmServer); - }, - }); - - test('serves an origin realm directory GET request', async function (assert) { - let response = await request - .get('/') - .set('Accept', 'application/vnd.api+json'); - - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - let json = response.body; - for (let relationship of Object.values(json.data.relationships)) { - delete (relationship as any).meta.lastModified; - } - assert.deepEqual( - json, - { - data: { - id: testRealmHref, - type: 'directory', - relationships: { - '%F0%9F%98%80.gts': { - links: { - related: 'http://127.0.0.1:4444/%F0%9F%98%80.gts', - }, - meta: { - kind: 'file', - }, - }, - 'a.js': { - links: { - related: `${testRealmHref}a.js`, - }, - meta: { - kind: 'file', - }, - }, - 'b.js': { - links: { - related: `${testRealmHref}b.js`, - }, - meta: { - kind: 'file', - }, - }, - 'c.js': { - links: { - related: `${testRealmHref}c.js`, - }, - meta: { - kind: 'file', - }, - }, - 'code-ref-test.gts': { - links: { - related: `${testRealmHref}code-ref-test.gts`, - }, - meta: { - kind: 'file', - }, - }, - 'cycle-one.js': { - links: { - related: `${testRealmHref}cycle-one.js`, - }, - meta: { - kind: 'file', - }, - }, - 'cycle-two.js': { - links: { - related: `${testRealmHref}cycle-two.js`, - }, - meta: { - kind: 'file', - }, - }, - 'd.js': { - links: { - related: `${testRealmHref}d.js`, - }, - meta: { - kind: 'file', - }, - }, - 'deadlock/': { - links: { - related: `${testRealmHref}deadlock/`, - }, - meta: { - kind: 'directory', - }, - }, - 'dir/': { - links: { - related: `${testRealmHref}dir/`, - }, - meta: { - kind: 'directory', - }, - }, - 'e.js': { - links: { - related: `${testRealmHref}e.js`, - }, - meta: { - kind: 'file', - }, - }, - 'family_photo_card.gts': { - links: { - related: `${testRealmHref}family_photo_card.gts`, - }, - meta: { - kind: 'file', - }, - }, - 'FamilyPhotoCard/': { - links: { - related: `${testRealmHref}FamilyPhotoCard/`, - }, - meta: { - kind: 'directory', - }, - }, - 'friend.gts': { - links: { - related: `${testRealmHref}friend.gts`, - }, - meta: { - kind: 'file', }, - }, - 'hassan.json': { - links: { - related: `${testRealmHref}hassan.json`, + 'a.js': { + links: { + related: `${testRealmHref}a.js`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'b.js': { + links: { + related: `${testRealmHref}b.js`, + }, + meta: { + kind: 'file', + }, }, - }, - 'home.gts': { - links: { - related: `${testRealmHref}home.gts`, + 'c.js': { + links: { + related: `${testRealmHref}c.js`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'code-ref-test.gts': { + links: { + related: `${testRealmHref}code-ref-test.gts`, + }, + meta: { + kind: 'file', + }, }, - }, - 'index.json': { - links: { - related: `${testRealmHref}index.json`, + 'cycle-one.js': { + links: { + related: `${testRealmHref}cycle-one.js`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'cycle-two.js': { + links: { + related: `${testRealmHref}cycle-two.js`, + }, + meta: { + kind: 'file', + }, }, - }, - 'jade.json': { - links: { - related: `${testRealmHref}jade.json`, + 'd.js': { + links: { + related: `${testRealmHref}d.js`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'deadlock/': { + links: { + related: `${testRealmHref}deadlock/`, + }, + meta: { + kind: 'directory', + }, }, - }, - 'missing-link.json': { - links: { - related: `${testRealmHref}missing-link.json`, + 'dir/': { + links: { + related: `${testRealmHref}dir/`, + }, + meta: { + kind: 'directory', + }, }, - meta: { - kind: 'file', + 'e.js': { + links: { + related: `${testRealmHref}e.js`, + }, + meta: { + kind: 'file', + }, }, - }, - 'person-1.json': { - links: { - related: `${testRealmHref}person-1.json`, + 'family_photo_card.gts': { + links: { + related: `${testRealmHref}family_photo_card.gts`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'FamilyPhotoCard/': { + links: { + related: `${testRealmHref}FamilyPhotoCard/`, + }, + meta: { + kind: 'directory', + }, }, - }, - 'person-2.json': { - links: { - related: `${testRealmHref}person-2.json`, + 'friend.gts': { + links: { + related: `${testRealmHref}friend.gts`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'hassan.json': { + links: { + related: `${testRealmHref}hassan.json`, + }, + meta: { + kind: 'file', + }, }, - }, - 'person-with-error.gts': { - links: { - related: `${testRealmHref}person-with-error.gts`, + 'home.gts': { + links: { + related: `${testRealmHref}home.gts`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'index.json': { + links: { + related: `${testRealmHref}index.json`, + }, + meta: { + kind: 'file', + }, }, - }, - 'person.gts': { - links: { - related: `${testRealmHref}person.gts`, + 'jade.json': { + links: { + related: `${testRealmHref}jade.json`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'missing-link.json': { + links: { + related: `${testRealmHref}missing-link.json`, + }, + meta: { + kind: 'file', + }, }, - }, - 'person.json': { - links: { - related: `${testRealmHref}person.json`, + 'person-1.json': { + links: { + related: `${testRealmHref}person-1.json`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'person-2.json': { + links: { + related: `${testRealmHref}person-2.json`, + }, + meta: { + kind: 'file', + }, }, - }, - 'PersonCard/': { - links: { - related: `${testRealmHref}PersonCard/`, + 'person-with-error.gts': { + links: { + related: `${testRealmHref}person-with-error.gts`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'directory', + 'person.gts': { + links: { + related: `${testRealmHref}person.gts`, + }, + meta: { + kind: 'file', + }, }, - }, - 'query-test-cards.gts': { - links: { - related: `${testRealmHref}query-test-cards.gts`, + 'person.json': { + links: { + related: `${testRealmHref}person.json`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'PersonCard/': { + links: { + related: `${testRealmHref}PersonCard/`, + }, + meta: { + kind: 'directory', + }, }, - }, - 'unused-card.gts': { - links: { - related: `${testRealmHref}unused-card.gts`, + 'query-test-cards.gts': { + links: { + related: `${testRealmHref}query-test-cards.gts`, + }, + meta: { + kind: 'file', + }, }, - meta: { - kind: 'file', + 'unused-card.gts': { + links: { + related: `${testRealmHref}unused-card.gts`, + }, + meta: { + kind: 'file', + }, }, }, }, }, - }, - 'the directory response is correct', - ); + 'the directory response is correct', + ); + }); }); -}); -module('Realm server serving multiple realms', function (hooks) { - let testRealmServer: Server; - let request: SuperTest; - let dir: DirResult; - let base: Realm; - let testRealm: Realm; + module('Realm server serving multiple realms', function (hooks) { + let testRealmServer: Server; + let request: SuperTest; + let dir: DirResult; + let base: Realm; + let testRealm: Realm; - let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); - const basePath = resolve(join(__dirname, '..', '..', 'base')); + let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); + const basePath = resolve(join(__dirname, '..', '..', 'base')); - hooks.beforeEach(async function () { - dir = dirSync(); - ensureDirSync(join(dir.name, 'demo')); - copySync(join(__dirname, 'cards'), join(dir.name, 'demo')); - }); + hooks.beforeEach(async function () { + dir = dirSync(); + ensureDirSync(join(dir.name, 'demo')); + copySync(join(__dirname, 'cards'), join(dir.name, 'demo')); + }); - setupDB(hooks, { - beforeEach: async (dbAdapter, publisher, runner) => { - let localBaseRealmURL = new URL('http://127.0.0.1:4446/base/'); - virtualNetwork.addURLMapping(new URL(baseRealm.url), localBaseRealmURL); - - base = await createRealm({ - withWorker: true, - dir: basePath, - realmURL: baseRealm.url, - virtualNetwork, - publisher, - runner, - dbAdapter, - deferStartUp: true, - }); - virtualNetwork.mount(base.handle); - - testRealm = await createRealm({ - withWorker: true, - dir: join(dir.name, 'demo'), - virtualNetwork, - realmURL: 'http://127.0.0.1:4446/demo/', - publisher, - runner, - dbAdapter, - deferStartUp: true, - }); - virtualNetwork.mount(testRealm.handle); + setupDB(hooks, { + beforeEach: async (dbAdapter, publisher, runner) => { + let localBaseRealmURL = new URL('http://127.0.0.1:4446/base/'); + virtualNetwork.addURLMapping(new URL(baseRealm.url), localBaseRealmURL); + + base = await createRealm({ + withWorker: true, + dir: basePath, + realmURL: baseRealm.url, + virtualNetwork, + publisher, + runner, + dbAdapter, + deferStartUp: true, + }); + virtualNetwork.mount(base.handle); - let matrixClient = new MatrixClient({ - matrixURL: realmServerTestMatrix.url, - username: realmServerTestMatrix.username, - seed: secretSeed, - }); - let getIndexHTML = (await getFastbootState()).getIndexHTML; - testRealmServer = new RealmServer({ - realms: [base, testRealm], - virtualNetwork, - matrixClient, - secretSeed, - matrixRegistrationSecret, - realmsRootPath: dir.name, - dbAdapter, - queue: publisher, - getIndexHTML, - seedPath, - serverURL: new URL('http://127.0.0.1:4446'), - assetsURL: new URL(`http://example.com/notional-assets-host/`), - }).listen(parseInt(localBaseRealmURL.port)); - await base.start(); - await testRealm.start(); - - request = supertest(testRealmServer); - }, - afterEach: async () => { - await closeServer(testRealmServer); - }, - }); + testRealm = await createRealm({ + withWorker: true, + dir: join(dir.name, 'demo'), + virtualNetwork, + realmURL: 'http://127.0.0.1:4446/demo/', + publisher, + runner, + dbAdapter, + deferStartUp: true, + }); + virtualNetwork.mount(testRealm.handle); + + let matrixClient = new MatrixClient({ + matrixURL: realmServerTestMatrix.url, + username: realmServerTestMatrix.username, + seed: secretSeed, + }); + let getIndexHTML = (await getFastbootState()).getIndexHTML; + testRealmServer = new RealmServer({ + realms: [base, testRealm], + virtualNetwork, + matrixClient, + secretSeed, + matrixRegistrationSecret, + realmsRootPath: dir.name, + dbAdapter, + queue: publisher, + getIndexHTML, + seedPath, + serverURL: new URL('http://127.0.0.1:4446'), + assetsURL: new URL(`http://example.com/notional-assets-host/`), + }).listen(parseInt(localBaseRealmURL.port)); + await base.start(); + await testRealm.start(); + + request = supertest(testRealmServer); + }, + afterEach: async () => { + await closeServer(testRealmServer); + }, + }); - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); + setupCardLogs( + hooks, + async () => await loader.import(`${baseRealm.url}card-api`), + ); - test(`Can perform full indexing multiple times on a server that runs multiple realms`, async function (assert) { - { - let response = await request - .get('/demo/person-1') - .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - } + test(`Can perform full indexing multiple times on a server that runs multiple realms`, async function (assert) { + { + let response = await request + .get('/demo/person-1') + .set('Accept', 'application/vnd.card+json'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + } - await base.reindex(); - await testRealm.reindex(); + await base.reindex(); + await testRealm.reindex(); - { - let response = await request - .get('/demo/person-1') - .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - } + { + let response = await request + .get('/demo/person-1') + .set('Accept', 'application/vnd.card+json'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + } - await base.reindex(); - await testRealm.reindex(); + await base.reindex(); + await testRealm.reindex(); - { - let response = await request - .get('/demo/person-1') - .set('Accept', 'application/vnd.card+json'); - assert.strictEqual(response.status, 200, 'HTTP 200 status'); - } + { + let response = await request + .get('/demo/person-1') + .set('Accept', 'application/vnd.card+json'); + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + } + }); }); -}); - -module('Realm Server serving from a subdirectory', function (hooks) { - let testRealmServer: Server; - let request: SuperTest; + module('Realm Server serving from a subdirectory', function (hooks) { + let testRealmServer: Server; - let dir: DirResult; + let request: SuperTest; - let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); + let dir: DirResult; - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); + let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); - setupBaseRealmServer(hooks, virtualNetwork, matrixURL); + setupCardLogs( + hooks, + async () => await loader.import(`${baseRealm.url}card-api`), + ); - hooks.beforeEach(async function () { - dir = dirSync(); - }); + setupBaseRealmServer(hooks, virtualNetwork, matrixURL); - setupDB(hooks, { - beforeEach: async (dbAdapter, publisher, runner) => { + hooks.beforeEach(async function () { dir = dirSync(); - let testRealmDir = join(dir.name, 'realm_server_4', 'test'); - ensureDirSync(testRealmDir); - copySync(join(__dirname, 'cards'), testRealmDir); - testRealmServer = ( - await runTestRealmServer({ - virtualNetwork: createVirtualNetwork(), - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_4'), - realmURL: new URL('http://127.0.0.1:4446/demo/'), - dbAdapter, - publisher, - runner, - matrixURL, - }) - ).testRealmHttpServer; - request = supertest(testRealmServer); - }, - afterEach: async () => { - await closeServer(testRealmServer); - }, - }); + }); - test('serves a subdirectory GET request that results in redirect', async function (assert) { - let response = await request.get('/demo'); + setupDB(hooks, { + beforeEach: async (dbAdapter, publisher, runner) => { + dir = dirSync(); + let testRealmDir = join(dir.name, 'realm_server_4', 'test'); + ensureDirSync(testRealmDir); + copySync(join(__dirname, 'cards'), testRealmDir); + testRealmServer = ( + await runTestRealmServer({ + virtualNetwork: createVirtualNetwork(), + testRealmDir, + realmsRootPath: join(dir.name, 'realm_server_4'), + realmURL: new URL('http://127.0.0.1:4446/demo/'), + dbAdapter, + publisher, + runner, + matrixURL, + }) + ).testRealmHttpServer; + request = supertest(testRealmServer); + }, + afterEach: async () => { + await closeServer(testRealmServer); + }, + }); - assert.strictEqual(response.status, 302, 'HTTP 302 status'); - assert.strictEqual( - response.headers['location'], - 'http://127.0.0.1:4446/demo/', - ); - }); + test('serves a subdirectory GET request that results in redirect', async function (assert) { + let response = await request.get('/demo'); - test('redirection keeps query params intact', async function (assert) { - let response = await request.get( - '/demo?operatorModeEnabled=true&operatorModeState=%7B%22stacks%22%3A%5B%7B%22items%22%3A%5B%7B%22card%22%3A%7B%22id%22%3A%22http%3A%2F%2Flocalhost%3A4204%2Findex%22%7D%2C%22format%22%3A%22isolated%22%7D%5D%7D%5D%7D', - ); + assert.strictEqual(response.status, 302, 'HTTP 302 status'); + assert.strictEqual( + response.headers['location'], + 'http://127.0.0.1:4446/demo/', + ); + }); - assert.strictEqual(response.status, 302, 'HTTP 302 status'); - assert.strictEqual( - response.headers['location'], - 'http://127.0.0.1:4446/demo/?operatorModeEnabled=true&operatorModeState=%7B%22stacks%22%3A%5B%7B%22items%22%3A%5B%7B%22card%22%3A%7B%22id%22%3A%22http%3A%2F%2Flocalhost%3A4204%2Findex%22%7D%2C%22format%22%3A%22isolated%22%7D%5D%7D%5D%7D', - ); - }); -}); + test('redirection keeps query params intact', async function (assert) { + let response = await request.get( + '/demo?operatorModeEnabled=true&operatorModeState=%7B%22stacks%22%3A%5B%7B%22items%22%3A%5B%7B%22card%22%3A%7B%22id%22%3A%22http%3A%2F%2Flocalhost%3A4204%2Findex%22%7D%2C%22format%22%3A%22isolated%22%7D%5D%7D%5D%7D', + ); -module('Realm server authentication', function (hooks) { - let testRealmServer: Server; + assert.strictEqual(response.status, 302, 'HTTP 302 status'); + assert.strictEqual( + response.headers['location'], + 'http://127.0.0.1:4446/demo/?operatorModeEnabled=true&operatorModeState=%7B%22stacks%22%3A%5B%7B%22items%22%3A%5B%7B%22card%22%3A%7B%22id%22%3A%22http%3A%2F%2Flocalhost%3A4204%2Findex%22%7D%2C%22format%22%3A%22isolated%22%7D%5D%7D%5D%7D', + ); + }); + }); - let request: SuperTest; + module('Realm server authentication', function (hooks) { + let testRealmServer: Server; - let dir: DirResult; + let request: SuperTest; - let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); + let dir: DirResult; - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); + let { virtualNetwork, loader } = createVirtualNetworkAndLoader(); - setupBaseRealmServer(hooks, virtualNetwork, matrixURL); + setupCardLogs( + hooks, + async () => await loader.import(`${baseRealm.url}card-api`), + ); - hooks.beforeEach(async function () { - dir = dirSync(); - }); + setupBaseRealmServer(hooks, virtualNetwork, matrixURL); - setupDB(hooks, { - beforeEach: async (dbAdapter, publisher, runner) => { - let testRealmDir = join(dir.name, 'realm_server_5', 'test'); - ensureDirSync(testRealmDir); - copySync(join(__dirname, 'cards'), testRealmDir); - testRealmServer = ( - await runTestRealmServer({ - virtualNetwork: createVirtualNetwork(), - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_5'), - realmURL: testRealmURL, - dbAdapter, - publisher, - runner, - matrixURL, - }) - ).testRealmHttpServer; - request = supertest(testRealmServer); - }, - afterEach: async () => { - await closeServer(testRealmServer); - }, - }); + hooks.beforeEach(async function () { + dir = dirSync(); + }); - test('authenticates user', async function (assert) { - let matrixClient = new MatrixClient({ - matrixURL: realmServerTestMatrix.url, - // it's a little awkward that we are hijacking a realm user to pretend to - // act like a normal user, but that's what's happening here - username: 'test_realm', - seed: secretSeed, + setupDB(hooks, { + beforeEach: async (dbAdapter, publisher, runner) => { + let testRealmDir = join(dir.name, 'realm_server_5', 'test'); + ensureDirSync(testRealmDir); + copySync(join(__dirname, 'cards'), testRealmDir); + testRealmServer = ( + await runTestRealmServer({ + virtualNetwork: createVirtualNetwork(), + testRealmDir, + realmsRootPath: join(dir.name, 'realm_server_5'), + realmURL: testRealmURL, + dbAdapter, + publisher, + runner, + matrixURL, + }) + ).testRealmHttpServer; + request = supertest(testRealmServer); + }, + afterEach: async () => { + await closeServer(testRealmServer); + }, }); - await matrixClient.login(); - let userId = matrixClient.getUserId(); - let response = await request - .post('/_server-session') - .send(JSON.stringify({ user: userId })) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json'); + test('authenticates user', async function (assert) { + let matrixClient = new MatrixClient({ + matrixURL: realmServerTestMatrix.url, + // it's a little awkward that we are hijacking a realm user to pretend to + // act like a normal user, but that's what's happening here + username: 'test_realm', + seed: secretSeed, + }); + await matrixClient.login(); + let userId = matrixClient.getUserId(); + + let response = await request + .post('/_server-session') + .send(JSON.stringify({ user: userId })) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json'); + + assert.strictEqual(response.status, 401, 'HTTP 401 status'); + let json = response.body; - assert.strictEqual(response.status, 401, 'HTTP 401 status'); - let json = response.body; + let { joined_rooms: rooms } = await matrixClient.getJoinedRooms(); - let { joined_rooms: rooms } = await matrixClient.getJoinedRooms(); + if (!rooms.includes(json.room)) { + await matrixClient.joinRoom(json.room); + } - if (!rooms.includes(json.room)) { - await matrixClient.joinRoom(json.room); - } + await matrixClient.sendEvent(json.room, 'm.room.message', { + body: `auth-response: ${json.challenge}`, + msgtype: 'm.text', + }); + + response = await request + .post('/_server-session') + .send(JSON.stringify({ user: userId, challenge: json.challenge })) + .set('Accept', 'application/json') + .set('Content-Type', 'application/json'); + assert.strictEqual(response.status, 201, 'HTTP 201 status'); + let token = response.headers['authorization']; + let decoded = jwt.verify(token, secretSeed) as RealmServerTokenClaim; + assert.strictEqual(decoded.user, userId); + assert.notStrictEqual( + decoded.sessionRoom, + undefined, + 'sessionRoom should be defined', + ); + }); + }); + + function assertScopedCssUrlsContain( + assert: Assert, + scopedCssUrls: string[], + moduleUrls: string[], + ) { + moduleUrls.forEach((url) => { + let pattern = new RegExp(`^${url}\\.[^.]+\\.glimmer-scoped\\.css$`); - await matrixClient.sendEvent(json.room, 'm.room.message', { - body: `auth-response: ${json.challenge}`, - msgtype: 'm.text', + assert.true( + scopedCssUrls.some((scopedCssUrl) => pattern.test(scopedCssUrl)), + `css url for ${url} is in the deps`, + ); }); + } - response = await request - .post('/_server-session') - .send(JSON.stringify({ user: userId, challenge: json.challenge })) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json'); - assert.strictEqual(response.status, 201, 'HTTP 201 status'); - let token = response.headers['authorization']; - let decoded = jwt.verify(token, secretSeed) as RealmServerTokenClaim; - assert.strictEqual(decoded.user, userId); - assert.notStrictEqual( - decoded.sessionRoom, - undefined, - 'sessionRoom should be defined', - ); - }); + // These modules have CSS that CardDef consumes, so we expect to see them in all relationships of a prerendered card + let cardDefModuleDependencies = [ + 'https://cardstack.com/base/default-templates/embedded.gts', + 'https://cardstack.com/base/default-templates/isolated-and-edit.gts', + 'https://cardstack.com/base/default-templates/field-edit.gts', + 'https://cardstack.com/base/field-component.gts', + 'https://cardstack.com/base/contains-many-component.gts', + 'https://cardstack.com/base/links-to-editor.gts', + 'https://cardstack.com/base/links-to-many-component.gts', + ]; }); - -function assertScopedCssUrlsContain( - assert: Assert, - scopedCssUrls: string[], - moduleUrls: string[], -) { - moduleUrls.forEach((url) => { - let pattern = new RegExp(`^${url}\\.[^.]+\\.glimmer-scoped\\.css$`); - - assert.true( - scopedCssUrls.some((scopedCssUrl) => pattern.test(scopedCssUrl)), - `css url for ${url} is in the deps`, - ); - }); -} - -// These modules have CSS that CardDef consumes, so we expect to see them in all relationships of a prerendered card -let cardDefModuleDependencies = [ - 'https://cardstack.com/base/default-templates/embedded.gts', - 'https://cardstack.com/base/default-templates/isolated-and-edit.gts', - 'https://cardstack.com/base/default-templates/field-edit.gts', - 'https://cardstack.com/base/field-component.gts', - 'https://cardstack.com/base/contains-many-component.gts', - 'https://cardstack.com/base/links-to-editor.gts', - 'https://cardstack.com/base/links-to-many-component.gts', -]; diff --git a/packages/realm-server/tests/virtual-network-test.ts b/packages/realm-server/tests/virtual-network-test.ts index d3cd8efecd..21e0d7eb4c 100644 --- a/packages/realm-server/tests/virtual-network-test.ts +++ b/packages/realm-server/tests/virtual-network-test.ts @@ -3,61 +3,66 @@ import { VirtualNetwork, } from '@cardstack/runtime-common'; import { module, test } from 'qunit'; +import { basename } from 'path'; -module('virtual-network', function () { - test('will respond with real (not virtual) url when handler makes a redirect', async function (assert) { - let virtualNetwork = new VirtualNetwork(); - virtualNetwork.addURLMapping( - new URL('https://cardstack.com/base/'), - new URL('http://localhost:4201/base/'), - ); - virtualNetwork.mount(async (_request: Request) => { - // Normally there would be some redirection logic here, but for this test we just want to make sure that the redirect is handled correctly - return new Response(null, { - status: 302, - headers: { - Location: 'https://cardstack.com/base/__boxel/assets/', // This virtual url should be converted to a real url so that the client can follow the redirect - }, - }) as ResponseWithNodeStream; - }); - - let response = await virtualNetwork.handle( - new Request('http://localhost:4201/__boxel/assets/'), - ); - - assert.strictEqual(response.status, 302); - assert.strictEqual( - response.headers.get('Location'), - 'http://localhost:4201/base/__boxel/assets/', - ); - }); - - test('is able to follow redirects', async function (assert) { - let virtualNetwork = new VirtualNetwork(); - - virtualNetwork.mount(async (request: Request) => { - // Normally there would be some redirection logic here, but for this test we just want to make sure that the redirect is handled correctly - if (request.url == 'http://test-realm/test/person') { +module(basename(__filename), function () { + module('virtual-network', function () { + test('will respond with real (not virtual) url when handler makes a redirect', async function (assert) { + let virtualNetwork = new VirtualNetwork(); + virtualNetwork.addURLMapping( + new URL('https://cardstack.com/base/'), + new URL('http://localhost:4201/base/'), + ); + virtualNetwork.mount(async (_request: Request) => { + // Normally there would be some redirection logic here, but for this test we just want to make sure that the redirect is handled correctly return new Response(null, { status: 302, headers: { - Location: 'http://test-realm/test/person.gts', + Location: 'https://cardstack.com/base/__boxel/assets/', // This virtual url should be converted to a real url so that the client can follow the redirect }, }) as ResponseWithNodeStream; - } + }); - return null; - }); + let response = await virtualNetwork.handle( + new Request('http://localhost:4201/__boxel/assets/'), + ); - virtualNetwork.mount(async (request: Request) => { - if (request.url == 'http://test-realm/test/person.gts') { - return new Response(null, { status: 200 }); - } - return null; + assert.strictEqual(response.status, 302); + assert.strictEqual( + response.headers.get('Location'), + 'http://localhost:4201/base/__boxel/assets/', + ); }); - let response = await virtualNetwork.fetch(`http://test-realm/test/person`); - assert.strictEqual(response.url, 'http://test-realm/test/person.gts'); - assert.true(response.redirected); + test('is able to follow redirects', async function (assert) { + let virtualNetwork = new VirtualNetwork(); + + virtualNetwork.mount(async (request: Request) => { + // Normally there would be some redirection logic here, but for this test we just want to make sure that the redirect is handled correctly + if (request.url == 'http://test-realm/test/person') { + return new Response(null, { + status: 302, + headers: { + Location: 'http://test-realm/test/person.gts', + }, + }) as ResponseWithNodeStream; + } + + return null; + }); + + virtualNetwork.mount(async (request: Request) => { + if (request.url == 'http://test-realm/test/person.gts') { + return new Response(null, { status: 200 }); + } + return null; + }); + + let response = await virtualNetwork.fetch( + `http://test-realm/test/person`, + ); + assert.strictEqual(response.url, 'http://test-realm/test/person.gts'); + assert.true(response.redirected); + }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6983a56d76..f379694f4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1783,6 +1783,9 @@ importers: '@types/fs-extra': specifier: ^9.0.13 version: 9.0.13 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/jsdom': specifier: ^21.1.1 version: 21.1.1 @@ -1864,6 +1867,9 @@ importers: http-server: specifier: ^14.1.1 version: 14.1.1 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 jsdom: specifier: ^21.1.1 version: 21.1.1 @@ -7190,6 +7196,10 @@ packages: resolution: {integrity: sha512-s3Tz/P+u4X78n0TdgNR0l9Yu1jyH2dRwofi/DqGLpbbjiuIs0n6W8W4XfUI6+9K0lPK1fF4KHA5HcqtOsy1V0Q==} dev: true + /@types/js-yaml@4.0.9: + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + dev: true + /@types/jsdom@21.1.1: resolution: {integrity: sha512-cZFuoVLtzKP3gmq9eNosUL1R50U+USkbLtUQ1bYVgl/lKp0FZM7Cq4aIHAL8oIvQ17uSHi7jXPtfDOdjPwBE7A==} dependencies: From 072ab4a90f211aeb65e9a5a76cbfe4bfbe5110d4 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 7 Jan 2025 17:43:33 -0500 Subject: [PATCH 3/6] Force server tests to exist promptly --- packages/realm-server/tests/index.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/realm-server/tests/index.ts b/packages/realm-server/tests/index.ts index 389cd7f547..9db10fcbed 100644 --- a/packages/realm-server/tests/index.ts +++ b/packages/realm-server/tests/index.ts @@ -12,3 +12,29 @@ import './queue-test'; import './realm-server-test'; import './virtual-network-test'; import './billing-test'; + +// There is some timer that is preventing the node process from ending promptly. +// This forces the test to end with the correct response code. Note than a +// message "Error: Process exited before tests finished running" will be +// displayed because of this approach. +import QUnit from 'qunit'; +(QUnit as any).on( + 'runEnd', + ({ + testCounts, + }: { + testCounts: { + passed: number; + failed: number; + total: number; + skipped: number; + todo: number; + }; + }) => { + if (testCounts.failed > 0) { + process.exit(1); + } else { + process.exit(0); + } + }, +); From 87eaf8025b5c88b048a908a0b3845f0eb2b06549 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 7 Jan 2025 17:48:38 -0500 Subject: [PATCH 4/6] typo --- packages/realm-server/tests/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm-server/tests/index.ts b/packages/realm-server/tests/index.ts index 9db10fcbed..a717c1f120 100644 --- a/packages/realm-server/tests/index.ts +++ b/packages/realm-server/tests/index.ts @@ -14,7 +14,7 @@ import './virtual-network-test'; import './billing-test'; // There is some timer that is preventing the node process from ending promptly. -// This forces the test to end with the correct response code. Note than a +// This forces the test to end with the correct response code. Note that a // message "Error: Process exited before tests finished running" will be // displayed because of this approach. import QUnit from 'qunit'; From 3ffabe65c70f0246b82e5738d0d33462db2903fe Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Jan 2025 06:58:25 -0500 Subject: [PATCH 5/6] Update packages/realm-server/scripts/lint-test-shards.ts Co-authored-by: Buck Doyle --- packages/realm-server/scripts/lint-test-shards.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm-server/scripts/lint-test-shards.ts b/packages/realm-server/scripts/lint-test-shards.ts index aec21f1f1d..c269894bd8 100644 --- a/packages/realm-server/scripts/lint-test-shards.ts +++ b/packages/realm-server/scripts/lint-test-shards.ts @@ -65,7 +65,7 @@ function validateTestFiles(yamlFilePath: string, testDir: string) { for (let filename of ciTestModules) { if (!filesystemTestModules.includes(filename)) { console.error( - `Error: Test file '${filename}' exists in the YAML file but not in the ${yamlFilePath} filesystem.`, + `Error: Test file '${filename}' exists in the YAML file but not in the filesystem.`, ); errorFound = true; } From 94e1037f5ae36d4eb9e064db86ec3af6373f1fb9 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Jan 2025 09:24:57 -0500 Subject: [PATCH 6/6] Update .github/workflows/ci.yaml Co-authored-by: Buck Doyle --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 249eab7a15..13dd2cdfcd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -234,7 +234,7 @@ jobs: name: Realm Server Tests runs-on: ubuntu-latest concurrency: - group: realm-server-test-${{ matrix.testModule || github.head_ref || github.run_id }} + group: realm-server-test-${{ matrix.testModule }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true strategy: fail-fast: false