diff --git a/packages/realm-server/setup-localhost-resolver.ts b/packages/realm-server/setup-localhost-resolver.ts index 8b061e9021..6665de2e99 100644 --- a/packages/realm-server/setup-localhost-resolver.ts +++ b/packages/realm-server/setup-localhost-resolver.ts @@ -9,8 +9,21 @@ if (process.env.BOXEL_ENVIRONMENT) { try { + type UndiciModule = { + Agent: new (opts: { + connect: { + lookup: ( + hostname: string, + options: any, + cb: (...args: any[]) => void, + ) => void; + }; + }) => unknown; + setGlobalDispatcher: (dispatcher: unknown) => void; + }; + // eslint-disable-next-line @typescript-eslint/no-var-requires - const undici = require('undici') as typeof import('undici'); + const undici = require('undici') as UndiciModule; // eslint-disable-next-line @typescript-eslint/no-var-requires const dns = require('dns'); diff --git a/packages/realm-server/tests/card-endpoints-test.ts b/packages/realm-server/tests/card-endpoints-test.ts index fd4032f893..ffa4bc7160 100644 --- a/packages/realm-server/tests/card-endpoints-test.ts +++ b/packages/realm-server/tests/card-endpoints-test.ts @@ -1163,6 +1163,7 @@ module(basename(__filename), function () { assert, getMessagesSince, realm: testRealmHref, + timeout: 5000, }, ); let id = incrementalEventContent.invalidations[0].split('/').pop()!; diff --git a/packages/realm-server/tests/helpers/index.ts b/packages/realm-server/tests/helpers/index.ts index 61224687d5..1cdab7b54f 100644 --- a/packages/realm-server/tests/helpers/index.ts +++ b/packages/realm-server/tests/helpers/index.ts @@ -7,7 +7,7 @@ import { copySync, } from 'fs-extra'; import { NodeAdapter } from '../../node-realm'; -import { join } from 'path'; +import { dirname, join } from 'path'; import { createHash } from 'crypto'; import type { LooseSingleCardDocument, @@ -717,10 +717,12 @@ export async function createRealm({ } for (let [filename, contents] of Object.entries(fileSystem)) { + let path = join(dir, filename); + ensureDirSync(dirname(path)); if (typeof contents === 'string') { - writeFileSync(join(dir, filename), contents); + writeFileSync(path, contents); } else { - writeJSONSync(join(dir, filename), contents); + writeJSONSync(path, contents); } } @@ -891,6 +893,7 @@ export async function runTestRealmServer({ testRealmHttpServer, testRealmAdapter, matrixClient, + virtualNetwork, }; } @@ -1596,6 +1599,7 @@ export function setupPermissionedRealm( testRealmAdapter: RealmAdapter; request: SuperTest; dir: DirResult; + virtualNetwork: VirtualNetwork; }) => void; subscribeToRealmEvents?: boolean; mode?: 'beforeEach' | 'before'; @@ -1643,6 +1647,7 @@ export function setupPermissionedRealm( testRealmPath: testRealmServer.testRealmDir, testRealmHttpServer: testRealmServer.testRealmHttpServer, testRealmAdapter: testRealmServer.testRealmAdapter, + virtualNetwork: testRealmServer.virtualNetwork, request, dir, }); diff --git a/packages/realm-server/tests/helpers/indexing.ts b/packages/realm-server/tests/helpers/indexing.ts index 6bf68e2281..cf504eecf9 100644 --- a/packages/realm-server/tests/helpers/indexing.ts +++ b/packages/realm-server/tests/helpers/indexing.ts @@ -15,22 +15,27 @@ interface IncrementalIndexEventTestContext { getMessagesSince: (since: number) => Promise; realm: string; type?: string; + timeout?: number; } export async function waitForIncrementalIndexEvent( getMessagesSince: (since: number) => Promise, since: number, + timeout = 5000, ) { - await waitUntil(async () => { - let matrixMessages = await getMessagesSince(since); - - return matrixMessages.some( - (m) => - m.type === APP_BOXEL_REALM_EVENT_TYPE && - m.content.eventName === 'index' && - m.content.indexType === 'incremental', - ); - }); + await waitUntil( + async () => { + let matrixMessages = await getMessagesSince(since); + + return matrixMessages.some( + (m) => + m.type === APP_BOXEL_REALM_EVENT_TYPE && + m.content.eventName === 'index' && + m.content.indexType === 'incremental', + ); + }, + { timeout }, + ); } export async function expectIncrementalIndexEvent( @@ -38,7 +43,7 @@ export async function expectIncrementalIndexEvent( since: number, opts: IncrementalIndexEventTestContext, ) { - let { assert, getMessagesSince, realm, type } = opts; + let { assert, getMessagesSince, realm, type, timeout } = opts; type = type ?? 'CardDef'; @@ -48,7 +53,7 @@ export async function expectIncrementalIndexEvent( if (!hasExtension && !endsWithSlash) { throw new Error('Invalid file path'); } - await waitForIncrementalIndexEvent(getMessagesSince, since); + await waitForIncrementalIndexEvent(getMessagesSince, since, timeout); let messages = await getMessagesSince(since); let incrementalIndexInitiationEventContent = findRealmEvent( diff --git a/packages/realm-server/tests/server-endpoints/authentication-test.ts b/packages/realm-server/tests/server-endpoints/authentication-test.ts index f1b8e404ed..7e81172d59 100644 --- a/packages/realm-server/tests/server-endpoints/authentication-test.ts +++ b/packages/realm-server/tests/server-endpoints/authentication-test.ts @@ -1,21 +1,14 @@ import { module, test } from 'qunit'; -import { basename, join } from 'path'; +import { basename } from 'path'; import type { Test, SuperTest } from 'supertest'; -import supertest from 'supertest'; -import { dirSync, type DirResult } from 'tmp'; -import { copySync, ensureDirSync } from 'fs-extra'; import type { Server } from 'http'; import jwt from 'jsonwebtoken'; import { MatrixClient } from '@cardstack/runtime-common/matrix-client'; import type { RealmServerTokenClaim } from '../../utils/jwt'; import { - closeServer, - createVirtualNetwork, - matrixURL, realmSecretSeed, realmServerTestMatrix, - runTestRealmServer, - setupDB, + setupPermissionedRealmCached, testRealmURL, } from '../helpers'; import { createRealmServerSession } from './helpers'; @@ -25,41 +18,25 @@ import '@cardstack/runtime-common/helpers/code-equality-assertion'; module(`server-endpoints/${basename(__filename)}`, function () { module('Realm server authentication', function (hooks) { - let testRealmServer: Server; let request: SuperTest; - let dir: DirResult; let dbAdapter: PgAdapter; - hooks.beforeEach(async function () { - dir = dirSync(); - }); + function onRealmSetup(args: { + testRealmHttpServer: Server; + request: SuperTest; + dbAdapter: PgAdapter; + }) { + dbAdapter = args.dbAdapter; + request = args.request; + } - setupDB(hooks, { - beforeEach: async (_dbAdapter, publisher, runner) => { - dbAdapter = _dbAdapter; - 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, - permissions: { - '@test_realm:localhost': ['read', 'realm-owner'], - }, - dbAdapter, - publisher, - runner, - matrixURL, - }) - ).testRealmHttpServer; - request = supertest(testRealmServer); - }, - afterEach: async () => { - await closeServer(testRealmServer); + setupPermissionedRealmCached(hooks, { + fileSystem: {}, + permissions: { + '@test_realm:localhost': ['read', 'realm-owner'], }, + realmURL: testRealmURL, + onRealmSetup: onRealmSetup, }); test('authenticates user and creates session room', async function (assert) { diff --git a/packages/realm-server/tests/server-endpoints/bot-commands-test.ts b/packages/realm-server/tests/server-endpoints/bot-commands-test.ts index 0aed614f7e..09e0c2795c 100644 --- a/packages/realm-server/tests/server-endpoints/bot-commands-test.ts +++ b/packages/realm-server/tests/server-endpoints/bot-commands-test.ts @@ -10,12 +10,12 @@ module(`server-endpoints/${basename(__filename)}`, function () { let context = setupServerEndpointsTest(hooks); test('requires auth to add bot command', async function (assert) { - let response = await context.request2.post('/_bot-commands').send({}); + let response = await context.request.post('/_bot-commands').send({}); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('requires auth to list bot commands', async function (assert) { - let response = await context.request2.get('/_bot-commands'); + let response = await context.request.get('/_bot-commands'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); @@ -39,7 +39,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -139,7 +139,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { ]); let commandSpecifier = '@cardstack/boxel-host/commands/show-card/default'; - let response = await context.request2 + let response = await context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -245,7 +245,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .get('/_bot-commands') .set( 'Authorization', @@ -291,7 +291,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -329,7 +329,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -367,7 +367,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -416,7 +416,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -460,7 +460,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let baseRequest = context.request2 + let baseRequest = context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -527,7 +527,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -594,7 +594,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let deleteResponse = await context.request2 + let deleteResponse = await context.request .delete('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -663,7 +663,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .delete('/_bot-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') diff --git a/packages/realm-server/tests/server-endpoints/bot-registration-test.ts b/packages/realm-server/tests/server-endpoints/bot-registration-test.ts index 3f768a1c70..6007c408a2 100644 --- a/packages/realm-server/tests/server-endpoints/bot-registration-test.ts +++ b/packages/realm-server/tests/server-endpoints/bot-registration-test.ts @@ -12,26 +12,24 @@ module(`server-endpoints/${basename(__filename)}`, function () { let context = setupServerEndpointsTest(hooks); test('requires auth to register bot', async function (assert) { - let response = await context.request2 + let response = await context.request .post('/_bot-registration') .send({}); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('requires auth to list bot registrations', async function (assert) { - let response = await context.request2.get('/_bot-registrations'); + let response = await context.request.get('/_bot-registrations'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('requires auth to unregister bot', async function (assert) { - let response = await context.request2 - .delete('/_bot-registration') - .send({ - data: { - type: 'bot-registration', - id: 'bot-reg-1', - }, - }); + let response = await context.request.delete('/_bot-registration').send({ + data: { + type: 'bot-registration', + id: 'bot-reg-1', + }, + }); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); @@ -44,7 +42,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -98,7 +96,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let firstResponse = await context.request2 + let firstResponse = await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -118,7 +116,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }, }); - let secondResponse = await context.request2 + let secondResponse = await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -157,7 +155,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -189,7 +187,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let registerResponse = await context.request2 + let registerResponse = await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -210,7 +208,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); let botRegistrationId = registerResponse.body.data.id; - let deleteResponse = await context.request2 + let deleteResponse = await context.request .delete('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -245,7 +243,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .delete('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -282,7 +280,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'other@example.com', ); - let registerResponse = await context.request2 + let registerResponse = await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -303,7 +301,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); let botRegistrationId = registerResponse.body.data.id; - let response = await context.request2 + let response = await context.request .delete('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -365,7 +363,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .get('/_bot-registrations') .set( 'Authorization', @@ -403,7 +401,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'other@example.com', ); - await context.request2 + await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -423,7 +421,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }, }); - await context.request2 + await context.request .post('/_bot-registration') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -443,7 +441,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }, }); - let response = await context.request2 + let response = await context.request .get('/_bot-registrations') .set( 'Authorization', diff --git a/packages/realm-server/tests/server-endpoints/download-realm-test.ts b/packages/realm-server/tests/server-endpoints/download-realm-test.ts index 3dfb9226df..f8b198c690 100644 --- a/packages/realm-server/tests/server-endpoints/download-realm-test.ts +++ b/packages/realm-server/tests/server-endpoints/download-realm-test.ts @@ -1,6 +1,6 @@ import { module, test } from 'qunit'; import { basename } from 'path'; -import { setupServerEndpointsTest, testRealm2URL } from './helpers'; +import { setupServerEndpointsTest, testRealmURL } from './helpers'; import { realmSecretSeed } from '../helpers'; import { createJWT } from '../../utils/jwt'; import { createURLSignatureSync } from '@cardstack/runtime-common/url-signature'; @@ -27,9 +27,9 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { let context = setupServerEndpointsTest(hooks); test('downloads realm as a zip archive', async function (assert) { - let response = await context.request2 + let response = await context.request .get('/_download-realm') - .query({ realm: testRealm2URL.href }) + .query({ realm: testRealmURL.href }) .buffer(true) .parse(binaryParser); @@ -58,18 +58,18 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { test('requires auth when realm is not public', async function (assert) { await context.dbAdapter.execute( - `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealmURL.href}' AND username = '*'`, ); - let response = await context.request2 + let response = await context.request .get('/_download-realm') - .query({ realm: testRealm2URL.href }); + .query({ realm: testRealmURL.href }); assert.strictEqual(response.status, 401, 'returns unauthorized'); }); test('returns 400 when realm is missing from query params', async function (assert) { - let response = await context.request2.get('/_download-realm'); + let response = await context.request.get('/_download-realm'); assert.strictEqual(response.status, 400, 'returns bad request'); assert.ok( @@ -79,7 +79,7 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { }); test('returns 404 when realm is not registered on the server', async function (assert) { - let response = await context.request2 + let response = await context.request .get('/_download-realm') .query({ realm: 'http://127.0.0.1:4445/missing/' }); @@ -93,14 +93,14 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { test('accepts auth token via query param with valid signature', async function (assert) { // Remove public permissions to require authentication await context.dbAdapter.execute( - `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealmURL.href}' AND username = '*'`, ); // Add read permission for the test user let testUser = '@test:localhost'; await context.dbAdapter.execute( `INSERT INTO realm_user_permissions (realm_url, username, read, write, realm_owner) - VALUES ('${testRealm2URL.href}', '${testUser}', true, false, false)`, + VALUES ('${testRealmURL.href}', '${testUser}', true, false, false)`, ); // Create a valid JWT token @@ -110,15 +110,15 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { ); // Build the URL and compute signature - let downloadURL = new URL('http://127.0.0.1:4445/_download-realm'); - downloadURL.searchParams.set('realm', testRealm2URL.href); + let downloadURL = new URL('/_download-realm', testRealmURL.origin); + downloadURL.searchParams.set('realm', testRealmURL.href); downloadURL.searchParams.set('token', token); let sig = createURLSignatureSync(token, downloadURL); // Request with token and signature in query params - let response = await context.request2 + let response = await context.request .get('/_download-realm') - .query({ realm: testRealm2URL.href, token, sig }) + .query({ realm: testRealmURL.href, token, sig }) .buffer(true) .parse(binaryParser); @@ -134,7 +134,7 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { test('rejects token via query param without signature', async function (assert) { // Remove public permissions to require authentication await context.dbAdapter.execute( - `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealmURL.href}' AND username = '*'`, ); let token = createJWT( @@ -142,9 +142,9 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { realmSecretSeed, ); - let response = await context.request2 + let response = await context.request .get('/_download-realm') - .query({ realm: testRealm2URL.href, token }); + .query({ realm: testRealmURL.href, token }); assert.strictEqual( response.status, @@ -156,7 +156,7 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { test('rejects token via query param with invalid signature', async function (assert) { // Remove public permissions to require authentication await context.dbAdapter.execute( - `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealmURL.href}' AND username = '*'`, ); let token = createJWT( @@ -164,9 +164,9 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { realmSecretSeed, ); - let response = await context.request2 + let response = await context.request .get('/_download-realm') - .query({ realm: testRealm2URL.href, token, sig: 'invalid-signature' }); + .query({ realm: testRealmURL.href, token, sig: 'invalid-signature' }); assert.strictEqual( response.status, @@ -178,13 +178,13 @@ module(`server-endpoints/${basename(__filename)}`, function (hooks) { test('rejects invalid token via query param', async function (assert) { // Remove public permissions to require authentication await context.dbAdapter.execute( - `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealmURL.href}' AND username = '*'`, ); // Invalid token with a signature (signature doesn't matter since token is invalid) - let response = await context.request2 + let response = await context.request .get('/_download-realm') - .query({ realm: testRealm2URL.href, token: 'invalid-token', sig: 'any' }); + .query({ realm: testRealmURL.href, token: 'invalid-token', sig: 'any' }); assert.strictEqual( response.status, diff --git a/packages/realm-server/tests/server-endpoints/helpers.ts b/packages/realm-server/tests/server-endpoints/helpers.ts index c4bae28b65..420d74d7d5 100644 --- a/packages/realm-server/tests/server-endpoints/helpers.ts +++ b/packages/realm-server/tests/server-endpoints/helpers.ts @@ -1,76 +1,66 @@ -import { join } from 'path'; -import supertest from 'supertest'; import type { Test, SuperTest } from 'supertest'; -import { dirSync, type DirResult } from 'tmp'; -import { copySync, ensureDirSync } from 'fs-extra'; +import type { DirResult } from 'tmp'; import type { QueuePublisher, QueueRunner, Realm, VirtualNetwork, } from '@cardstack/runtime-common'; -import { DEFAULT_PERMISSIONS } from '@cardstack/runtime-common'; import type { Server } from 'http'; import type { PgAdapter } from '@cardstack/postgres'; import type { RealmServer } from '../../server'; import { - closeServer, - createVirtualNetwork, - matrixURL, - runTestRealmServer, - setupDB, setupPermissionedRealmCached, + testRealmURL as baseTestRealmURL, } from '../helpers'; import type { MatrixClient } from '@cardstack/runtime-common/matrix-client'; -export const testRealm2URL = new URL('http://127.0.0.1:4445/test/'); +export const testRealmURL = new URL('/test/', baseTestRealmURL); export type ServerEndpointsTestContext = { testRealm: Realm; request: SuperTest; dir: DirResult; dbAdapter: PgAdapter; - testRealmServer2: RealmServer; - testRealmHttpServer2: Server; + testRealmServer: RealmServer; + testRealmHttpServer: Server; publisher: QueuePublisher; runner: QueueRunner; - request2: SuperTest; testRealmDir: string; virtualNetwork: VirtualNetwork; - startRealmServer: () => Promise; }; -export type ServerEndpointsTestOptions = { - beforeStartRealmServer?: ( - context: ServerEndpointsTestContext, - ) => void | Promise; -}; - -export function setupServerEndpointsTest( - hooks: NestedHooks, - options: ServerEndpointsTestOptions = {}, -) { +export function setupServerEndpointsTest(hooks: NestedHooks) { let context = {} as ServerEndpointsTestContext; - let ownerUserId = '@mango:localhost'; function onRealmSetup(args: { + testRealmServer: { + testRealmServer: RealmServer; + testRealmHttpServer: Server; + }; testRealm: Realm; + testRealmPath: string; request: SuperTest; dir: DirResult; dbAdapter: PgAdapter; + runner: QueueRunner; + publisher: QueuePublisher; + virtualNetwork: VirtualNetwork; }) { + context.testRealmServer = args.testRealmServer.testRealmServer; + context.testRealmHttpServer = args.testRealmServer.testRealmHttpServer; context.testRealm = args.testRealm; context.request = args.request; context.dir = args.dir; context.dbAdapter = args.dbAdapter; + context.runner = args.runner; + context.publisher = args.publisher; + context.testRealmDir = args.testRealmPath; + context.virtualNetwork = args.virtualNetwork; } - hooks.beforeEach(async function () { - context.dir = dirSync(); - copySync(join(__dirname, '..', 'cards'), context.dir.name); - }); - setupPermissionedRealmCached(hooks, { + realmURL: testRealmURL, permissions: { '*': ['read', 'write'], '@node-test_realm:localhost': ['read', 'realm-owner'], @@ -78,58 +68,6 @@ export function setupServerEndpointsTest( onRealmSetup, }); - async function startRealmServer( - dbAdapter: PgAdapter, - publisher: QueuePublisher, - runner: QueueRunner, - ) { - context.virtualNetwork = createVirtualNetwork(); - ({ - testRealmServer: context.testRealmServer2, - testRealmHttpServer: context.testRealmHttpServer2, - } = await runTestRealmServer({ - virtualNetwork: context.virtualNetwork, - testRealmDir: context.testRealmDir, - realmsRootPath: join(context.dir.name, 'realm_server_2'), - realmURL: testRealm2URL, - dbAdapter, - publisher, - runner, - matrixURL, - permissions: { - '*': ['read', 'write'], - [ownerUserId]: DEFAULT_PERMISSIONS, - }, - })); - context.request2 = supertest(context.testRealmHttpServer2); - } - - context.startRealmServer = async () => { - await startRealmServer( - context.dbAdapter, - context.publisher, - context.runner, - ); - }; - - setupDB(hooks, { - beforeEach: async (_dbAdapter, _publisher, _runner) => { - context.dbAdapter = _dbAdapter; - context.publisher = _publisher; - context.runner = _runner; - context.testRealmDir = join(context.dir.name, 'realm_server_2', 'test'); - ensureDirSync(context.testRealmDir); - copySync(join(__dirname, '..', 'cards'), context.testRealmDir); - if (options.beforeStartRealmServer) { - await options.beforeStartRealmServer(context); - } - await startRealmServer(_dbAdapter, _publisher, _runner); - }, - afterEach: async () => { - await closeServer(context.testRealmHttpServer2); - }, - }); - return context; } diff --git a/packages/realm-server/tests/server-endpoints/incoming-webhook-test.ts b/packages/realm-server/tests/server-endpoints/incoming-webhook-test.ts index 3d160c38a6..5adac24e43 100644 --- a/packages/realm-server/tests/server-endpoints/incoming-webhook-test.ts +++ b/packages/realm-server/tests/server-endpoints/incoming-webhook-test.ts @@ -10,19 +10,17 @@ module(`server-endpoints/${basename(__filename)}`, function () { let context = setupServerEndpointsTest(hooks); test('requires auth to create incoming webhook', async function (assert) { - let response = await context.request2 - .post('/_incoming-webhooks') - .send({}); + let response = await context.request.post('/_incoming-webhooks').send({}); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('requires auth to list incoming webhooks', async function (assert) { - let response = await context.request2.get('/_incoming-webhooks'); + let response = await context.request.get('/_incoming-webhooks'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('requires auth to delete incoming webhook', async function (assert) { - let response = await context.request2.delete('/_incoming-webhooks').send({ + let response = await context.request.delete('/_incoming-webhooks').send({ data: { type: 'incoming-webhook', id: uuidv4(), @@ -40,7 +38,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -130,7 +128,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -166,7 +164,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -201,7 +199,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -283,7 +281,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .get('/_incoming-webhooks') .set( 'Authorization', @@ -311,7 +309,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let createResponse = await context.request2 + let createResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -337,7 +335,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let webhookId = createResponse.body.data.id; - let deleteResponse = await context.request2 + let deleteResponse = await context.request .delete('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -379,7 +377,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'other@example.com', ); - let createResponse = await context.request2 + let createResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -405,7 +403,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let webhookId = createResponse.body.data.id; - let deleteResponse = await context.request2 + let deleteResponse = await context.request .delete('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -440,7 +438,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let createResponse = await context.request2 + let createResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -482,7 +480,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let deleteResponse = await context.request2 + let deleteResponse = await context.request .delete('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') diff --git a/packages/realm-server/tests/server-endpoints/index-responses-test.ts b/packages/realm-server/tests/server-endpoints/index-responses-test.ts index ff8fae8df1..336917d2a4 100644 --- a/packages/realm-server/tests/server-endpoints/index-responses-test.ts +++ b/packages/realm-server/tests/server-endpoints/index-responses-test.ts @@ -7,12 +7,11 @@ import { dirSync, type DirResult } from 'tmp'; import { DEFAULT_PERMISSIONS, systemInitiatedPriority, - type QueuePublisher, - type QueueRunner, + type DBAdapter, type Realm, } from '@cardstack/runtime-common'; import type { PgAdapter } from '@cardstack/postgres'; -import { testRealm2URL } from './helpers'; +import { testRealmURL } from './helpers'; import { closeServer, createVirtualNetwork, @@ -24,1148 +23,1125 @@ import { waitUntil, } from '../helpers'; import { createJWT as createRealmServerJWT } from '../../utils/jwt'; -import { - copySync, - ensureDirSync, - writeFileSync, - writeJSONSync, -} from 'fs-extra'; +import { ensureDirSync } from 'fs-extra'; import '@cardstack/runtime-common/helpers/code-equality-assertion'; -function writeTestCards(testRealmDir: string) { - let subdirectoryPath = join(testRealmDir, 'subdirectory'); - ensureDirSync(subdirectoryPath); - writeJSONSync(join(subdirectoryPath, 'index.json'), { - data: { - type: 'card', - attributes: { - firstName: 'Subdirectory Index', - }, - meta: { - adoptsFrom: { - module: '../person.gts', - name: 'Person', - }, - }, - }, - }); - - writeFileSync( - join(testRealmDir, 'isolated-card.gts'), - ` - import { Component, CardDef } from 'https://cardstack.com/base/card-api'; - - export class IsolatedCard extends CardDef { - static isolated = class Isolated extends Component { - - }; +module(`server-endpoints/${basename(__filename)}`, function () { + module( + 'Realm Server Endpoints (not specific to one realm)', + function (hooks) { + let request: SuperTest; + let dbAdapter: DBAdapter; + + function onRealmSetup(args: { + request: SuperTest; + testRealm: Realm; + dbAdapter: DBAdapter; + }) { + request = args.request; + dbAdapter = args.dbAdapter; } - `, - ); - - writeJSONSync(join(testRealmDir, 'isolated-test.json'), { - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: './isolated-card.gts', - name: 'IsolatedCard', - }, - }, - }, - }); - writeFileSync( - join(testRealmDir, 'dollar-sign-card.gts'), - ` - import { Component, CardDef } from 'https://cardstack.com/base/card-api'; - - export class DollarSignCard extends CardDef { - static isolated = class Isolated extends Component { - - }; - } - `, - ); - - writeJSONSync(join(testRealmDir, 'dollar-sign-test.json'), { - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: './dollar-sign-card.gts', - name: 'DollarSignCard', - }, - }, - }, - }); - - writeFileSync( - join(testRealmDir, 'head-card.gts'), - ` - import { Component, CardDef } from 'https://cardstack.com/base/card-api'; - - export class HeadCard extends CardDef { - static isolated = class Isolated extends Component { - - }; - - static head = class Head extends Component { - - }; - } - `, - ); - - writeJSONSync(join(testRealmDir, 'private-index-test.json'), { - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: './head-card.gts', - name: 'HeadCard', - }, - }, - }, - }); + setupPermissionedRealmCached(hooks, { + realmURL: testRealmURL, + fileSystem: { + 'index.json': { + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: './home.gts', + name: 'Home', + }, + }, + }, + }, + 'home.gts': `import { Component, CardDef } from 'https://cardstack.com/base/card-api'; + export class Home extends CardDef { + static isolated = class Isolated extends Component { + + }; + }`, + 'person.gts': `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 { + static displayName = 'Person'; + @field firstName = contains(StringField); + @field cardTitle = contains(StringField, { + computeVia: function (this: Person) { + return this.firstName; + }, + }); + static isolated = class Isolated extends Component { + + }; + }`, + 'subdirectory/index.json': { + data: { + type: 'card', + attributes: { + firstName: 'Subdirectory Index', + }, + meta: { + adoptsFrom: { + module: '../person.gts', + name: 'Person', + }, + }, + }, + }, + 'isolated-card.gts': ` + import { Component, CardDef } from 'https://cardstack.com/base/card-api'; + + export class IsolatedCard extends CardDef { + static isolated = class Isolated extends Component { + + }; + } + `, + 'isolated-test.json': { + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: './isolated-card.gts', + name: 'IsolatedCard', + }, + }, + }, + }, - writeFileSync( - join(testRealmDir, 'unsafe-head-card.gts'), - ` - import { Component, CardDef } from 'https://cardstack.com/base/card-api'; - - export class UnsafeHeadCard extends CardDef { - static isolated = class Isolated extends Component { - - }; - - static head = class Head extends Component { - - }; - } - `, - ); + 'dollar-sign-card.gts': ` + import { Component, CardDef } from 'https://cardstack.com/base/card-api'; - writeJSONSync(join(testRealmDir, 'unsafe-head-test.json'), { - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: './unsafe-head-card.gts', - name: 'UnsafeHeadCard', - }, - }, - }, - }); + export class DollarSignCard extends CardDef { + static isolated = class Isolated extends Component { + + }; + } + `, + + 'dollar-sign-test.json': { + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: './dollar-sign-card.gts', + name: 'DollarSignCard', + }, + }, + }, + }, - writeFileSync( - join(testRealmDir, 'scoped-css-card.gts'), - ` - import { Component, CardDef } from 'https://cardstack.com/base/card-api'; - - export class ScopedCssCard extends CardDef { - static isolated = class Isolated extends Component { - - }; - } - `, - ); + `, + + 'private-index-test.json': { + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: './head-card.gts', + name: 'HeadCard', + }, + }, + }, + }, - writeJSONSync(join(testRealmDir, 'scoped-css-test.json'), { - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: './scoped-css-card.gts', - name: 'ScopedCssCard', - }, - }, - }, - }); + 'unsafe-head-card.gts': ` + import { Component, CardDef } from 'https://cardstack.com/base/card-api'; + + export class UnsafeHeadCard extends CardDef { + static isolated = class Isolated extends Component { + + }; + + static head = class Head extends Component { + + }; + } + `, + + 'unsafe-head-test.json': { + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: './unsafe-head-card.gts', + name: 'UnsafeHeadCard', + }, + }, + }, + }, - // Cards for testing scoped CSS from linked card instances. - // The parent declares linksTo with a base type, but the actual linked - // instance is a subclass with its own scoped CSS. This means the child's - // CSS is NOT reachable through the parent's static module imports — it - // can only be found by iterating over serialized.included resources. - writeFileSync( - join(testRealmDir, 'linked-css-base.gts'), - ` - import { Component, CardDef } from 'https://cardstack.com/base/card-api'; - - export class LinkedCssBase extends CardDef { - static embedded = class Embedded extends Component { - - }; - } - `, - ); + 'scoped-css-card.gts': ` + import { Component, CardDef } from 'https://cardstack.com/base/card-api'; + + export class ScopedCssCard extends CardDef { + static isolated = class Isolated extends Component { + + }; + } + `, + 'scoped-css-test.json': { + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: './scoped-css-card.gts', + name: 'ScopedCssCard', + }, + }, + }, + }, - writeFileSync( - join(testRealmDir, 'linked-css-child.gts'), - ` - import { Component } from 'https://cardstack.com/base/card-api'; - import { LinkedCssBase } from './linked-css-base.gts'; - - export class LinkedCssChild extends LinkedCssBase { - static isolated = class Isolated extends Component { - - }; - static embedded = class Embedded extends Component { - + }; + static embedded = class Embedded extends Component { + + }; } - - - }; - } - `, - ); - - writeFileSync( - join(testRealmDir, 'linked-css-parent.gts'), - ` - import { Component, CardDef, field, linksTo } from 'https://cardstack.com/base/card-api'; - import { LinkedCssBase } from './linked-css-base.gts'; - - export class LinkedCssParent extends CardDef { - @field child = linksTo(() => LinkedCssBase); - static isolated = class Isolated extends Component { - - }; - } - `, - ); - - writeJSONSync(join(testRealmDir, 'linked-css-child-1.json'), { - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: './linked-css-child.gts', - name: 'LinkedCssChild', - }, - }, - }, - }); + `, + + 'linked-css-parent.gts': ` + import { Component, CardDef, field, linksTo } from 'https://cardstack.com/base/card-api'; + import { LinkedCssBase } from './linked-css-base.gts'; + + export class LinkedCssParent extends CardDef { + @field child = linksTo(() => LinkedCssBase); + static isolated = class Isolated extends Component { + + }; + } + `, + + 'linked-css-child-1.json': { + data: { + type: 'card', + attributes: {}, + meta: { + adoptsFrom: { + module: './linked-css-child.gts', + name: 'LinkedCssChild', + }, + }, + }, + }, - writeJSONSync(join(testRealmDir, 'linked-css-parent-1.json'), { - data: { - type: 'card', - attributes: {}, - relationships: { - child: { - links: { - self: './linked-css-child-1', + 'linked-css-parent-1.json': { + data: { + type: 'card', + attributes: {}, + relationships: { + child: { + links: { + self: './linked-css-child-1', + }, + }, + }, + meta: { + adoptsFrom: { + module: './linked-css-parent.gts', + name: 'LinkedCssParent', + }, + }, + }, }, - }, - }, - meta: { - adoptsFrom: { - module: './linked-css-parent.gts', - name: 'LinkedCssParent', - }, - }, - }, - }); - // Cards for testing default head template with cardInfo.theme - writeJSONSync(join(testRealmDir, 'a-test-theme.json'), { - data: { - type: 'card', - attributes: { - cardInfo: { - cardThumbnailURL: 'https://example.com/brand-icon.png', - }, - }, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/card-api', - name: 'Theme', - }, - }, - }, - }); + // Cards for testing default head template with cardInfo.theme + 'a-test-theme.json': { + data: { + type: 'card', + attributes: { + cardInfo: { + cardThumbnailURL: 'https://example.com/brand-icon.png', + }, + }, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/card-api', + name: 'Theme', + }, + }, + }, + }, - writeJSONSync(join(testRealmDir, 'a-brand-guide-theme.json'), { - data: { - type: 'card', - attributes: { - markUsage: { - socialMediaProfileIcon: 'https://example.com/social-icon.png', - }, - }, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/brand-guide', - name: 'default', + 'a-brand-guide-theme.json': { + data: { + type: 'card', + attributes: { + markUsage: { + socialMediaProfileIcon: 'https://example.com/social-icon.png', + }, + }, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/brand-guide', + name: 'default', + }, + }, + }, + }, }, - }, - }, - }); -} - -function setupSingleRealmTest( - hooks: NestedHooks, - mode: 'before' | 'beforeEach', -) { - let context = {} as { - request: SuperTest; - dbAdapter: PgAdapter; - }; - let testRealmHttpServer: Server; - let ownerUserId = '@mango:localhost'; - - setupDB(hooks, { - [mode]: async ( - dbAdapter: PgAdapter, - publisher: QueuePublisher, - runner: QueueRunner, - ) => { - let dir = dirSync(); - let testRealmDir = join(dir.name, 'realm_server_1', 'test'); - ensureDirSync(testRealmDir); - copySync(join(__dirname, '..', 'cards'), testRealmDir); - writeTestCards(testRealmDir); - - let virtualNetwork = createVirtualNetwork(); - let result = await runTestRealmServer({ - virtualNetwork, - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_1'), - realmURL: testRealm2URL, - dbAdapter, - publisher, - runner, - matrixURL, permissions: { '*': ['read', 'write'], - [ownerUserId]: DEFAULT_PERMISSIONS, }, + onRealmSetup, }); - testRealmHttpServer = result.testRealmHttpServer; - context.request = supertest(result.testRealmHttpServer); - context.dbAdapter = dbAdapter; - }, - }); + test('startup indexing uses system initiated queue priority', async function (assert) { + let [job] = (await dbAdapter.execute( + `SELECT priority FROM jobs WHERE job_type = 'from-scratch-index' AND args->>'realmURL' = '${testRealmURL.href}' ORDER BY created_at DESC LIMIT 1`, + )) as { priority: number }[]; - hooks[mode === 'beforeEach' ? 'afterEach' : 'after'](async function () { - await closeServer(testRealmHttpServer); - }); + assert.ok(job, 'found startup from-scratch index job for realm'); + assert.strictEqual( + job.priority, + systemInitiatedPriority, + 'realm startup uses system initiated priority', + ); + }); - return context; -} + test('serves isolated HTML for realm index request', async function (assert) { + let response = await request.get('/test').set('Accept', 'text/html'); -module(`server-endpoints/${basename(__filename)}`, function () { - module('Index responses (read-only)', function (hooks) { - let context = setupSingleRealmTest(hooks, 'before'); - - test('startup indexing uses system initiated queue priority', async function (assert) { - let [job] = (await context.dbAdapter.execute( - `SELECT priority FROM jobs WHERE job_type = 'from-scratch-index' AND args->>'realmURL' = '${testRealm2URL.href}' ORDER BY created_at DESC LIMIT 1`, - )) as { priority: number }[]; - - assert.ok(job, 'found startup from-scratch index job for realm'); - assert.strictEqual( - job.priority, - systemInitiatedPriority, - 'realm startup uses system initiated priority', - ); - }); + assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.ok( + response.text.includes('data-test-home-card'), + 'isolated HTML for index card is injected into the HTML response', + ); + }); - test('serves isolated HTML for realm index request', async function (assert) { - let response = await context.request - .get('/test') - .set('Accept', 'text/html'); + test('serves isolated HTML in index responses for card URLs', async function (assert) { + let response = await request + .get('/test/isolated-test') + .set('Accept', 'text/html'); - assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.ok( - response.text.includes('data-test-home-card'), - 'isolated HTML for index card is injected into the HTML response', - ); - }); + assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.ok( + response.text.includes('data-test-isolated-html'), + 'isolated HTML is injected into the HTML response', + ); + }); - test('serves isolated HTML in index responses for card URLs', async function (assert) { - let response = await context.request - .get('/test/isolated-test') - .set('Accept', 'text/html'); + test('HTML response does not include boxel-ready class on body', async function (assert) { + let response = await request + .get('/test/isolated-test') + .set('Accept', 'text/html'); - assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.ok( - response.text.includes('data-test-isolated-html'), - 'isolated HTML is injected into the HTML response', - ); - }); + assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.notOk( + response.text.includes('boxel-ready'), + 'boxel-ready class is not present in server-rendered HTML', + ); + }); - test('HTML response does not include boxel-ready class on body', async function (assert) { - let response = await context.request - .get('/test/isolated-test') - .set('Accept', 'text/html'); + test('serves isolated HTML for /subdirectory/index.json at /subdirectory/', async function (assert) { + let response = await request + .get('/test/subdirectory/') + .set('Accept', 'text/html'); - assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.notOk( - response.text.includes('boxel-ready'), - 'boxel-ready class is not present in server-rendered HTML', - ); - }); + assert.strictEqual(response.status, 200, 'serves HTML response'); - test('serves isolated HTML for /subdirectory/index.json at /subdirectory/', async function (assert) { - let response = await context.request - .get('/test/subdirectory/') - .set('Accept', 'text/html'); + assert.ok( + response.text.includes('Subdirectory Index'), + 'isolated HTML is injected into the HTML response', + ); + }); - assert.strictEqual(response.status, 200, 'serves HTML response'); + test('does not inject head or isolated HTML when realm is not public', async function (assert) { + await dbAdapter.execute( + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealmURL.href}' AND username = '*'`, + ); - assert.ok( - response.text.includes('Subdirectory Index'), - 'isolated HTML is injected into the HTML response', - ); - }); + let response = await request + .get('/test/private-index-test') + .set('Accept', 'text/html'); - test('serves scoped CSS in index responses for card URLs', async function (assert) { - let response = await context.request - .get('/test/scoped-css-test') - .set('Accept', 'text/html'); + assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.notOk( + response.text.includes('data-test-head-html'), + 'head HTML is not injected into the HTML response', + ); + assert.notOk( + response.text.includes('data-test-isolated-html'), + 'isolated HTML is not injected into the HTML response', + ); + }); - assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.ok( - response.text.includes('data-boxel-scoped-css'), - 'scoped CSS style tag is injected into the HTML response', - ); - assert.ok( - response.text.includes('--scoped-css-marker: 1'), - 'scoped CSS is included in the HTML response', - ); - }); + test('serves scoped CSS in index responses for card URLs', async function (assert) { + let response = await request + .get('/test/scoped-css-test') + .set('Accept', 'text/html'); - test('serves scoped CSS from linked cards in index responses', async function (assert) { - let response = await context.request - .get('/test/linked-css-parent-1') - .set('Accept', 'text/html'); + assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.ok( + response.text.includes('data-boxel-scoped-css'), + 'scoped CSS style tag is injected into the HTML response', + ); + assert.ok( + response.text.includes('--scoped-css-marker: 1'), + 'scoped CSS is included in the HTML response', + ); + }); - assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.ok( - response.text.includes('data-test-linked-parent'), - 'parent isolated HTML is in the response', - ); - assert.ok( - response.text.includes('--linked-child-css: 1'), - 'scoped CSS from linked card is included in the HTML response', - ); - }); + test('serves scoped CSS from linked cards in index responses', async function (assert) { + let response = await request + .get('/test/linked-css-parent-1') + .set('Accept', 'text/html'); - test('sanitizes disallowed tags from head HTML in index responses', async function (assert) { - let response = await context.request - .get('/test/unsafe-head-test') - .set('Accept', 'text/html'); + assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.ok( + response.text.includes('data-test-linked-parent'), + 'parent isolated HTML is in the response', + ); + assert.ok( + response.text.includes('--linked-child-css: 1'), + 'scoped CSS from linked card is included in the HTML response', + ); + }); - assert.strictEqual(response.status, 200, 'serves HTML response'); + test('sanitizes disallowed tags from head HTML in index responses', async function (assert) { + let response = await request + .get('/test/unsafe-head-test') + .set('Accept', 'text/html'); - // Extract content between head markers - let headMatch = response.text.match( - /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, - ); - let headContent = headMatch?.[1] ?? ''; + assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.ok( - headContent.includes(''), - 'title tag is preserved in head HTML', - ); - assert.ok( - headContent.includes('<meta'), - 'meta tag is preserved in head HTML', - ); - assert.notOk( - headContent.includes('<script'), - 'script tag is stripped from head HTML', - ); - assert.notOk( - headContent.includes('void 0'), - 'script content is stripped from head HTML', - ); - assert.notOk( - headContent.includes('.injected-style'), - 'user-injected style content is stripped from head HTML', - ); - }); + // Extract content between head markers + let headMatch = response.text.match( + /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, + ); + let headContent = headMatch?.[1] ?? ''; - test('serves isolated HTML containing dollar signs without corruption', async function (assert) { - let response = await context.request - .get('/test/dollar-sign-test') - .set('Accept', 'text/html'); + assert.ok( + headContent.includes('<title>'), + 'title tag is preserved in head HTML', + ); + assert.ok( + headContent.includes('<meta'), + 'meta tag is preserved in head HTML', + ); + assert.notOk( + headContent.includes('<script'), + 'script tag is stripped from head HTML', + ); + assert.notOk( + headContent.includes('void 0'), + 'script content is stripped from head HTML', + ); + assert.notOk( + headContent.includes('.injected-style'), + 'user-injected style content is stripped from head HTML', + ); + }); - assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.ok( - response.text.includes('data-test-dollar-sign'), - 'isolated HTML with dollar signs is injected into the HTML response', - ); - assert.ok( - response.text.includes('$0.50'), - 'dollar sign content is preserved without regex replacement pattern corruption', - ); - assert.ok( - response.text.includes('boxel-isolated-end'), - 'isolated end boundary marker is present (not corrupted by $0 backreference)', - ); - }); + test('serves isolated HTML containing dollar signs without corruption', async function (assert) { + let response = await request + .get('/test/dollar-sign-test') + .set('Accept', 'text/html'); - test('HTML response includes exactly one favicon and one apple-touch-icon', async function (assert) { - let response = await context.request - .get('/test/isolated-test') - .set('Accept', 'text/html'); + assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.ok( + response.text.includes('data-test-dollar-sign'), + 'isolated HTML with dollar signs is injected into the HTML response', + ); + assert.ok( + response.text.includes('$0.50'), + 'dollar sign content is preserved without regex replacement pattern corruption', + ); + assert.ok( + response.text.includes('boxel-isolated-end'), + 'isolated end boundary marker is present (not corrupted by $0 backreference)', + ); + }); - assert.strictEqual(response.status, 200, 'serves HTML response'); + test('ignores deleted index entries for head, isolated, and scoped CSS injection', async function (assert) { + let deleteSlugs = ['private-index-test', 'scoped-css-test']; - let faviconCount = (response.text.match(/rel="icon"/g) || []).length; - let appleTouchIconCount = ( - response.text.match(/rel="apple-touch-icon"/g) || [] - ).length; + for (let slug of deleteSlugs) { + let deleteResponse = await request + .delete(`/test/${slug}`) + .set('Accept', 'application/vnd.card+json'); - assert.strictEqual( - faviconCount, - 1, - 'exactly one favicon link is present in the HTML response', - ); - assert.strictEqual( - appleTouchIconCount, - 1, - 'exactly one apple-touch-icon link is present in the HTML response', - ); - assert.ok( - /<title[\s>]/.test(response.text), - 'title element is present in the HTML response', - ); - }); + assert.strictEqual( + deleteResponse.status, + 204, + `deleted ${slug} via card API`, + ); + } - test('default icon links are injected when card has no theme', async function (assert) { - let response = await context.request - .get('/test/isolated-test') - .set('Accept', 'text/html'); + await waitUntil( + async () => { + let realmURLNoProtocol = testRealmURL.href.replace( + /^https?:\/\//, + '', + ); - assert.strictEqual(response.status, 200, 'serves HTML response'); + for (let slug of deleteSlugs) { + for (let table of ['boxel_index', 'boxel_index_working']) { + let rows = (await dbAdapter.execute( + `SELECT COUNT(*) AS count + FROM ${table} + WHERE type = 'instance' + AND is_deleted IS NOT TRUE + AND (regexp_replace(url, '^https?://', '') LIKE '${realmURLNoProtocol}%${slug}%' + OR regexp_replace(file_alias, '^https?://', '') LIKE '${realmURLNoProtocol}%${slug}%')`, + )) as { count: string | number }[]; + + if (Number(rows[0]?.count ?? 0) > 0) { + return false; + } + } + } - let headMatch = response.text.match( - /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, - ); - let headContent = headMatch?.[1] ?? ''; + return true; + }, + { + timeout: 5000, + interval: 200, + timeoutMessage: + 'Timed out waiting for deleted index entries to be tombstoned', + }, + ); - assert.ok( - /<title[\s>]/.test(headContent), - 'title element is preserved in head when no theme is present', - ); - assert.ok( - headContent.includes('rel="icon"'), - 'default favicon link is injected into head when no theme is present', - ); - assert.ok( - headContent.includes('rel="apple-touch-icon"'), - 'default apple-touch-icon link is injected into head when no theme is present', - ); - assert.ok( - headContent.includes('boxel-favicon.png'), - 'default favicon points to boxel-favicon.png', - ); - assert.ok( - headContent.includes('boxel-webclip.png'), - 'default apple-touch-icon points to boxel-webclip.png', - ); - }); + let headResponse = await request + .get('/test/private-index-test') + .set('Accept', 'text/html'); - test('returns 404 for request that has malformed URI', async function (assert) { - let response = await context.request.get('/%c0').set('Accept', '*/*'); - assert.strictEqual(response.status, 404, 'HTTP 404 status'); - }); - }); + assert.strictEqual(headResponse.status, 200, 'serves HTML response'); + assert.notOk( + headResponse.text.includes('data-test-head-html'), + 'deleted head HTML is not injected into the HTML response', + ); + assert.notOk( + headResponse.text.includes('data-test-isolated-html'), + 'deleted isolated HTML is not injected into the HTML response', + ); - module('Index responses (mutating)', function (hooks) { - let context = setupSingleRealmTest(hooks, 'beforeEach'); + let scopedCSSResponse = await request + .get('/test/scoped-css-test') + .set('Accept', 'text/html'); - test('does not inject head or isolated HTML when realm is not public', async function (assert) { - await context.dbAdapter.execute( - `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, - ); + assert.strictEqual( + scopedCSSResponse.status, + 200, + 'serves HTML response', + ); + assert.notOk( + scopedCSSResponse.text.includes('data-boxel-scoped-css'), + 'deleted scoped CSS is not injected into the HTML response', + ); + assert.notOk( + scopedCSSResponse.text.includes('--scoped-css-marker: 1'), + 'deleted scoped CSS contents are not included in the HTML response', + ); + assert.notOk( + scopedCSSResponse.text.includes('data-test-scoped-css'), + 'deleted isolated HTML is not injected for scoped CSS card', + ); + }); - let response = await context.request - .get('/test/private-index-test') - .set('Accept', 'text/html'); + test('HTML response includes exactly one favicon and one apple-touch-icon', async function (assert) { + let response = await request + .get('/test/isolated-test') + .set('Accept', 'text/html'); - assert.strictEqual(response.status, 200, 'serves HTML response'); - assert.notOk( - response.text.includes('data-test-head-html'), - 'head HTML is not injected into the HTML response', - ); - assert.notOk( - response.text.includes('data-test-isolated-html'), - 'isolated HTML is not injected into the HTML response', - ); - }); + assert.strictEqual(response.status, 200, 'serves HTML response'); + + let faviconCount = (response.text.match(/rel="icon"/g) || []).length; + let appleTouchIconCount = ( + response.text.match(/rel="apple-touch-icon"/g) || [] + ).length; + + assert.strictEqual( + faviconCount, + 1, + 'exactly one favicon link is present in the HTML response', + ); + assert.strictEqual( + appleTouchIconCount, + 1, + 'exactly one apple-touch-icon link is present in the HTML response', + ); + assert.ok( + /<title[\s>]/.test(response.text), + 'title element is present in the HTML response', + ); + }); + + test('default icon links are injected when card has no theme', async function (assert) { + let response = await request + .get('/test/isolated-test') + .set('Accept', 'text/html'); + + assert.strictEqual(response.status, 200, 'serves HTML response'); + + let headMatch = response.text.match( + /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, + ); + let headContent = headMatch?.[1] ?? ''; + + assert.ok( + /<title[\s>]/.test(headContent), + 'title element is preserved in head when no theme is present', + ); + assert.ok( + headContent.includes('rel="icon"'), + 'default favicon link is injected into head when no theme is present', + ); + assert.ok( + headContent.includes('rel="apple-touch-icon"'), + 'default apple-touch-icon link is injected into head when no theme is present', + ); + assert.ok( + headContent.includes('boxel-favicon.png'), + 'default favicon points to boxel-favicon.png', + ); + assert.ok( + headContent.includes('boxel-webclip.png'), + 'default apple-touch-icon points to boxel-webclip.png', + ); + }); - test('non-public realm includes exactly one favicon and one apple-touch-icon', async function (assert) { - await context.dbAdapter.execute( - `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealm2URL.href}' AND username = '*'`, - ); + test('non-public realm includes exactly one favicon and one apple-touch-icon', async function (assert) { + await dbAdapter.execute( + `DELETE FROM realm_user_permissions WHERE realm_url = '${testRealmURL.href}' AND username = '*'`, + ); - let response = await context.request - .get('/test/private-index-test') - .set('Accept', 'text/html'); + let response = await request + .get('/test/private-index-test') + .set('Accept', 'text/html'); - assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.strictEqual(response.status, 200, 'serves HTML response'); - let faviconCount = (response.text.match(/rel="icon"/g) || []).length; - let appleTouchIconCount = ( - response.text.match(/rel="apple-touch-icon"/g) || [] - ).length; + let faviconCount = (response.text.match(/rel="icon"/g) || []).length; + let appleTouchIconCount = ( + response.text.match(/rel="apple-touch-icon"/g) || [] + ).length; - assert.strictEqual( - faviconCount, - 1, - 'exactly one favicon link is present even without head injection', - ); - assert.strictEqual( - appleTouchIconCount, - 1, - 'exactly one apple-touch-icon link is present even without head injection', - ); - assert.ok( - response.text.includes('<title>Boxel'), - 'title element is present even for non-public realm', - ); - }); + assert.strictEqual( + faviconCount, + 1, + 'exactly one favicon link is present even without head injection', + ); + assert.strictEqual( + appleTouchIconCount, + 1, + 'exactly one apple-touch-icon link is present even without head injection', + ); + assert.ok( + response.text.includes('Boxel'), + 'title element is present even for non-public realm', + ); + }); - test('missing apple-touch-icon is filled with default when only favicon is present in head HTML', async function (assert) { - // Directly set head_html to contain only a favicon link (no apple-touch-icon) - let cardURL = `${testRealm2URL.href}isolated-test.json`; - await context.dbAdapter.execute( - `UPDATE boxel_index - SET head_html = 'Test' - WHERE url = '${cardURL}' - AND type = 'instance' - AND is_deleted IS NOT TRUE`, - ); + test('missing apple-touch-icon is filled with default when only favicon is present in head HTML', async function (assert) { + // Directly set head_html to contain only a favicon link (no apple-touch-icon) + let cardURL = `${testRealmURL.href}isolated-test.json`; + await dbAdapter.execute( + `UPDATE boxel_index + SET head_html = 'Test' + WHERE url = '${cardURL}' + AND type = 'instance' + AND is_deleted IS NOT TRUE`, + ); - let response = await context.request - .get('/test/isolated-test') - .set('Accept', 'text/html'); + let response = await request + .get('/test/isolated-test') + .set('Accept', 'text/html'); - assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.strictEqual(response.status, 200, 'serves HTML response'); - let headMatch = response.text.match( - /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, - ); - let headContent = headMatch?.[1] ?? ''; + let headMatch = response.text.match( + /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, + ); + let headContent = headMatch?.[1] ?? ''; - assert.ok( - headContent.includes( - ']*>([\s\S]*?)data-boxel-head-end/, - ); - let headContent = headMatch?.[1] ?? ''; + let headMatch = response.text.match( + /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, + ); + let headContent = headMatch?.[1] ?? ''; - assert.ok( - headContent.includes( - ' { - let rows = (await context.dbAdapter.execute( - `SELECT url, head_html FROM boxel_index - WHERE url LIKE '%card-with-theme%' - AND type = 'instance' - AND is_deleted IS NOT TRUE - LIMIT 1`, - )) as { url: string; head_html: string | null }[]; - - return rows.length > 0 && rows[0].head_html != null; - }, - { - timeout: 30000, - interval: 500, - timeoutMessage: 'Timed out waiting for card-with-theme to be indexed', - }, - ); + // Wait for the card to be indexed (head_html populated, even if empty string). + await waitUntil( + async () => { + let rows = (await dbAdapter.execute( + `SELECT url, head_html FROM boxel_index + WHERE url LIKE '%card-with-theme%' + AND type = 'instance' + AND is_deleted IS NOT TRUE + LIMIT 1`, + )) as { url: string; head_html: string | null }[]; + + return rows.length > 0 && rows[0].head_html != null; + }, + { + timeout: 30000, + interval: 500, + timeoutMessage: + 'Timed out waiting for card-with-theme to be indexed', + }, + ); - let response = await context.request - .get('/test/card-with-theme') - .set('Accept', 'text/html'); + let response = await request + .get('/test/card-with-theme') + .set('Accept', 'text/html'); - assert.strictEqual(response.status, 200, 'serves HTML response'); + assert.strictEqual(response.status, 200, 'serves HTML response'); - let headMatch = response.text.match( - /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, - ); - let headContent = headMatch?.[1] ?? ''; + let headMatch = response.text.match( + /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, + ); + let headContent = headMatch?.[1] ?? ''; - assert.ok( - headContent.includes( - ' { - let rows = (await context.dbAdapter.execute( - `SELECT url, head_html FROM boxel_index - WHERE url LIKE '%card-with-brand-guide-theme%' - AND type = 'instance' - AND is_deleted IS NOT TRUE - LIMIT 1`, - )) as { url: string; head_html: string | null }[]; - - return rows.length > 0 && rows[0].head_html != null; - }, - { - timeout: 30000, - interval: 500, - timeoutMessage: - 'Timed out waiting for card-with-brand-guide-theme to be indexed', - }, - ); - - let response = await context.request - .get('/test/card-with-brand-guide-theme') - .set('Accept', 'text/html'); - - assert.strictEqual(response.status, 200, 'serves HTML response'); - - let headMatch = response.text.match( - /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, - ); - let headContent = headMatch?.[1] ?? ''; - - assert.ok( - headContent.includes( - ' { - let realmURLNoProtocol = testRealm2URL.href.replace( - /^https?:\/\//, - '', - ); + await waitUntil( + async () => { + let rows = (await dbAdapter.execute( + `SELECT url, head_html FROM boxel_index + WHERE url LIKE '%card-with-brand-guide-theme%' + AND type = 'instance' + AND is_deleted IS NOT TRUE + LIMIT 1`, + )) as { url: string; head_html: string | null }[]; + + return rows.length > 0 && rows[0].head_html != null; + }, + { + timeout: 30000, + interval: 500, + timeoutMessage: + 'Timed out waiting for card-with-brand-guide-theme to be indexed', + }, + ); - for (let slug of deleteSlugs) { - for (let table of ['boxel_index', 'boxel_index_working']) { - let rows = (await context.dbAdapter.execute( - `SELECT COUNT(*) AS count - FROM ${table} - WHERE type = 'instance' - AND is_deleted IS NOT TRUE - AND (regexp_replace(url, '^https?://', '') LIKE '${realmURLNoProtocol}%${slug}%' - OR regexp_replace(file_alias, '^https?://', '') LIKE '${realmURLNoProtocol}%${slug}%')`, - )) as { count: string | number }[]; - - if (Number(rows[0]?.count ?? 0) > 0) { - return false; - } - } - } + let response = await request + .get('/test/card-with-brand-guide-theme') + .set('Accept', 'text/html'); - return true; - }, - { - timeout: 5000, - interval: 200, - timeoutMessage: - 'Timed out waiting for deleted index entries to be tombstoned', - }, - ); + assert.strictEqual(response.status, 200, 'serves HTML response'); - let headResponse = await context.request - .get('/test/private-index-test') - .set('Accept', 'text/html'); + let headMatch = response.text.match( + /data-boxel-head-start[^>]*>([\s\S]*?)data-boxel-head-end/, + ); + let headContent = headMatch?.[1] ?? ''; - assert.strictEqual(headResponse.status, 200, 'serves HTML response'); - assert.notOk( - headResponse.text.includes('data-test-head-html'), - 'deleted head HTML is not injected into the HTML response', - ); - assert.notOk( - headResponse.text.includes('data-test-isolated-html'), - 'deleted isolated HTML is not injected into the HTML response', - ); + assert.ok( + headContent.includes( + ' { - let rows = (await context.dbAdapter.execute( - `SELECT has_error FROM boxel_index - WHERE url = '${testRealm2URL.href}scoped-css-test.json' - AND type = 'instance'`, - )) as { has_error: boolean }[]; + // Wait for the index to reflect the error state + await waitUntil( + async () => { + let rows = (await dbAdapter.execute( + `SELECT has_error FROM boxel_index + WHERE url = '${testRealmURL.href}scoped-css-test.json' + AND type = 'instance'`, + )) as { has_error: boolean }[]; - return rows.length > 0 && rows[0].has_error === true; - }, - { - timeout: 10000, - interval: 200, - timeoutMessage: 'Timed out waiting for instance to enter error state', - }, - ); + return rows.length > 0 && rows[0].has_error === true; + }, + { + timeout: 10000, + interval: 200, + timeoutMessage: + 'Timed out waiting for instance to enter error state', + }, + ); - // Verify the database row has an error - let errorRows = (await context.dbAdapter.execute( - `SELECT has_error, last_known_good_deps FROM boxel_index - WHERE url = '${testRealm2URL.href}scoped-css-test.json' - AND type = 'instance'`, - )) as { has_error: boolean; last_known_good_deps: string[] | null }[]; - - assert.strictEqual(errorRows.length, 1, 'found the index entry'); - assert.true( - errorRows[0].has_error, - 'instance is in error state in the database', - ); - assert.ok( - errorRows[0].last_known_good_deps, - 'last_known_good_deps is preserved', - ); - assert.ok( - errorRows[0].last_known_good_deps!.some((dep: string) => - dep.includes('.glimmer-scoped.css'), - ), - 'last_known_good_deps contains scoped CSS URL', - ); + // Verify the database row has an error + let errorRows = (await dbAdapter.execute( + `SELECT has_error, last_known_good_deps FROM boxel_index + WHERE url = '${testRealmURL.href}scoped-css-test.json' + AND type = 'instance'`, + )) as { has_error: boolean; last_known_good_deps: string[] | null }[]; + + assert.strictEqual(errorRows.length, 1, 'found the index entry'); + assert.true( + errorRows[0].has_error, + 'instance is in error state in the database', + ); + assert.ok( + errorRows[0].last_known_good_deps, + 'last_known_good_deps is preserved', + ); + assert.ok( + errorRows[0].last_known_good_deps!.some((dep: string) => + dep.includes('.glimmer-scoped.css'), + ), + 'last_known_good_deps contains scoped CSS URL', + ); - // Now request the HTML again - it should still include scoped CSS from last_known_good_deps - let errorStateResponse = await context.request - .get('/test/scoped-css-test') - .set('Accept', 'text/html'); + // Now request the HTML again - it should still include scoped CSS from last_known_good_deps + let errorStateResponse = await request + .get('/test/scoped-css-test') + .set('Accept', 'text/html'); - assert.strictEqual( - errorStateResponse.status, - 200, - 'HTML response is still successful even with errored card', - ); - assert.ok( - errorStateResponse.text.includes('data-boxel-scoped-css'), - 'scoped CSS style tag is still present after error (from last_known_good_deps)', - ); - assert.ok( - errorStateResponse.text.includes('--scoped-css-marker: 1'), - 'scoped CSS content is preserved from last_known_good_deps after card enters error state', - ); - }); - }); + assert.strictEqual( + errorStateResponse.status, + 200, + 'HTML response is still successful even with errored card', + ); + assert.ok( + errorStateResponse.text.includes('data-boxel-scoped-css'), + 'scoped CSS style tag is still present after error (from last_known_good_deps)', + ); + assert.ok( + errorStateResponse.text.includes('--scoped-css-marker: 1'), + 'scoped CSS content is preserved from last_known_good_deps after card enters error state', + ); + }); + }, + ); module('Published realm index responses', function (hooks) { // Use a URL with a path segment. Server-level routes are now namespaced @@ -1190,7 +1166,6 @@ module(`server-endpoints/${basename(__filename)}`, function () { }, published: true, onRealmSetup, - mode: 'before', }); hooks.beforeEach(async function () { @@ -1250,18 +1225,16 @@ module(`server-endpoints/${basename(__filename)}`, function () { hooks.beforeEach(function () { dir = dirSync(); }); - setupDB(hooks, { beforeEach: async (_dbAdapter, _publisher, _runner) => { dbAdapter = _dbAdapter; let virtualNetwork = createVirtualNetwork(); let testRealmDir = join(dir.name, 'realm_server_theme', 'test'); ensureDirSync(testRealmDir); - copySync(join(__dirname, '..', 'cards'), testRealmDir); - ({ testRealmHttpServer } = await runTestRealmServer({ virtualNetwork, testRealmDir, + fileSystem: {}, realmsRootPath: join(dir.name, 'realm_server_theme'), realmURL: new URL('http://127.0.0.1:4444/test/'), dbAdapter: _dbAdapter, diff --git a/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts b/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts index 6013518136..e895186188 100644 --- a/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts +++ b/packages/realm-server/tests/server-endpoints/maintenance-endpoints-test.ts @@ -7,7 +7,7 @@ import { sumUpCreditsLedger } from '@cardstack/billing/billing-queries'; import * as boxelUIChangeChecker from '../../lib/boxel-ui-change-checker'; import { grafanaSecret, insertUser, realmSecretSeed } from '../helpers'; import { createJWT as createRealmServerJWT } from '../../utils/jwt'; -import { setupServerEndpointsTest, testRealm2URL } from './helpers'; +import { setupServerEndpointsTest, testRealmURL } from './helpers'; import '@cardstack/runtime-common/helpers/code-equality-assertion'; module(`server-endpoints/${basename(__filename)}`, function () { @@ -21,13 +21,13 @@ module(`server-endpoints/${basename(__filename)}`, function () { (args, job_type, concurrency_group, timeout, priority) VALUES ( - '{"realmURL": "${testRealm2URL.href}", "realmUsername":"node-test_realm"}', + '{"realmURL": "${testRealmURL.href}", "realmUsername":"node-test_realm"}', 'from-scratch-index', - 'indexing:${testRealm2URL.href}', + 'indexing:${testRealmURL.href}', 180, 0 ) RETURNING id`)) as { id: string }[]; - let response = await context.request2 + let response = await context.request .get( `/_grafana-complete-job?authHeader=${grafanaSecret}&job_id=${id}`, ) @@ -54,9 +54,9 @@ module(`server-endpoints/${basename(__filename)}`, function () { (args, job_type, concurrency_group, timeout, priority) VALUES ( - '{"realmURL": "${testRealm2URL.href}", "realmUsername":"node-test_realm"}', + '{"realmURL": "${testRealmURL.href}", "realmUsername":"node-test_realm"}', 'from-scratch-index', - 'indexing:${testRealm2URL.href}', + 'indexing:${testRealmURL.href}', 180, 0 ) RETURNING id`)) as { id: string }[]; @@ -65,16 +65,16 @@ module(`server-endpoints/${basename(__filename)}`, function () { (args, job_type, concurrency_group, timeout, priority) VALUES ( - '{"realmURL": "${testRealm2URL.href}", "realmUsername":"node-test_realm"}', + '{"realmURL": "${testRealmURL.href}", "realmUsername":"node-test_realm"}', 'incremental-index', - 'indexing:${testRealm2URL.href}', + 'indexing:${testRealmURL.href}', 180, 0 ) RETURNING id`)) as { id: string }[]; await context.dbAdapter.execute(`INSERT INTO job_reservations (job_id, locked_until ) VALUES (${runningJobId}, NOW() + INTERVAL '3 minutes')`); - let pendingResponse = await context.request2 + let pendingResponse = await context.request .get( `/_grafana-complete-job?authHeader=${grafanaSecret}&job_id=${pendingJobId}`, ) @@ -85,7 +85,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'pending job cancel returns 204', ); - let runningResponse = await context.request2 + let runningResponse = await context.request .get( `/_grafana-complete-job?authHeader=${grafanaSecret}&job_id=${runningJobId}`, ) @@ -136,9 +136,9 @@ module(`server-endpoints/${basename(__filename)}`, function () { (args, job_type, concurrency_group, timeout, priority) VALUES ( - '{"realmURL": "${testRealm2URL.href}", "realmUsername":"node-test_realm"}', + '{"realmURL": "${testRealmURL.href}", "realmUsername":"node-test_realm"}', 'from-scratch-index', - 'indexing:${testRealm2URL.href}', + 'indexing:${testRealmURL.href}', 180, 0 ) RETURNING id`)) as { id: string }[]; @@ -149,7 +149,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }[]; await context.dbAdapter.execute(`INSERT INTO job_reservations (job_id, locked_until ) VALUES (${jobId}, NOW() + INTERVAL '2 minutes')`); - let response = await context.request2 + let response = await context.request .get( `/_grafana-complete-job?authHeader=${grafanaSecret}&reservation_id=${reservationId}`, ) @@ -183,9 +183,9 @@ module(`server-endpoints/${basename(__filename)}`, function () { (args, job_type, concurrency_group, timeout, priority) VALUES ( - '{"realmURL": "${testRealm2URL.href}", "realmUsername":"node-test_realm"}', + '{"realmURL": "${testRealmURL.href}", "realmUsername":"node-test_realm"}', 'from-scratch-index', - 'indexing:${testRealm2URL.href}', + 'indexing:${testRealmURL.href}', 180, 0 ) RETURNING id`)) as { id: string }[]; @@ -193,7 +193,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { (job_id, locked_until ) VALUES (${jobId}, NOW() + INTERVAL '3 minutes')`); await context.dbAdapter.execute(`INSERT INTO job_reservations (job_id, locked_until ) VALUES (${jobId}, NOW() + INTERVAL '2 minutes')`); - let response = await context.request2 + let response = await context.request .get( `/_grafana-complete-job?authHeader=${grafanaSecret}&job_id=${jobId}`, ) @@ -271,7 +271,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let reservationId = reservation?.id; assert.ok(reservationId, 'reservation exists for running job'); - let response = await context.request2 + let response = await context.request .get( `/_grafana-complete-job?authHeader=${grafanaSecret}&reservation_id=${reservationId}`, ) @@ -345,13 +345,13 @@ module(`server-endpoints/${basename(__filename)}`, function () { (args, job_type, concurrency_group, timeout, priority) VALUES ( - '{"realmURL": "${testRealm2URL.href}", "realmUsername":"node-test_realm"}', + '{"realmURL": "${testRealmURL.href}", "realmUsername":"node-test_realm"}', 'from-scratch-index', - 'indexing:${testRealm2URL.href}', + 'indexing:${testRealmURL.href}', 180, 0 ) RETURNING id`)) as { id: string }[]; - let response = await context.request2 + let response = await context.request .get(`/_grafana-complete-job?job_id=${id}`) .set('Content-Type', 'application/json'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); @@ -379,7 +379,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); assert.strictEqual(sum, 0, `user has 0 extra credit`); - let response = await context.request2 + let response = await context.request .get( `/_grafana-add-credit?authHeader=${grafanaSecret}&user=${user.matrixUserId}&credit=1000`, ) @@ -400,7 +400,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test('returns 400 when calling grafana add credit endpoint without a user', async function (assert) { - let response = await context.request2 + let response = await context.request .get(`/_grafana-add-credit?authHeader=${grafanaSecret}&credit=1000`) .set('Content-Type', 'application/json'); assert.strictEqual(response.status, 400, 'HTTP 400 status'); @@ -413,7 +413,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'cus_123', 'user@test.com', ); - let response = await context.request2 + let response = await context.request .get( `/_grafana-add-credit?authHeader=${grafanaSecret}&user=${user.matrixUserId}&credit=a+million+dollars`, ) @@ -427,7 +427,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test("returns 400 when calling grafana add credit endpoint when user doesn't exist", async function (assert) { - let response = await context.request2 + let response = await context.request .get( `/_grafana-add-credit?authHeader=${grafanaSecret}&user=nobody&credit=1000`, ) @@ -442,7 +442,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'cus_123', 'user@test.com', ); - let response = await context.request2 + let response = await context.request .get(`/_grafana-add-credit?user=${user.matrixUserId}&credit=1000`) .set('Content-Type', 'application/json'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); @@ -459,7 +459,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let ownerUserId = `@${owner}:localhost`; let realmURL: string; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -491,14 +491,14 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'number of jobs initially is correct', ); let staleModuleForTargetRealmURL = `${realmURL}stale-module-${uuidv4()}.gts`; - let staleModuleForOtherRealmURL = `${testRealm2URL.href}stale-module-${uuidv4()}.gts`; + let staleModuleForOtherRealmURL = `${testRealmURL.href}stale-module-${uuidv4()}.gts`; await context.dbAdapter.execute( `INSERT INTO modules (url, file_alias, definitions, deps, created_at, resolved_realm_url, cache_scope, auth_user_id) VALUES ('${staleModuleForTargetRealmURL}', '${staleModuleForTargetRealmURL}', '{}', '[]', ${Date.now()}, '${realmURL}', 'public', '')`, ); await context.dbAdapter.execute( `INSERT INTO modules (url, file_alias, definitions, deps, created_at, resolved_realm_url, cache_scope, auth_user_id) - VALUES ('${staleModuleForOtherRealmURL}', '${staleModuleForOtherRealmURL}', '{}', '[]', ${Date.now()}, '${testRealm2URL.href}', 'public', '')`, + VALUES ('${staleModuleForOtherRealmURL}', '${staleModuleForOtherRealmURL}', '{}', '[]', ${Date.now()}, '${testRealmURL.href}', 'public', '')`, ); let seededTargetRowsBefore = await context.dbAdapter.execute( `SELECT * FROM modules WHERE url = '${staleModuleForTargetRealmURL}'`, @@ -518,9 +518,9 @@ module(`server-endpoints/${basename(__filename)}`, function () { ); { let realmPath = realmURL.substring( - new URL(testRealm2URL.origin).href.length, + new URL(testRealmURL.origin).href.length, ); - let response = await context.request2 + let response = await context.request .get( `/_grafana-reindex?authHeader=${grafanaSecret}&realm=${realmPath}`, ) @@ -584,7 +584,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let ownerUserId = `@${owner}:localhost`; let realmURL: string; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -611,7 +611,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { } let initialJobs = await context.dbAdapter.execute('select * from jobs'); { - let response = await context.request2 + let response = await context.request .get(`/_grafana-reindex?realm=${encodeURIComponent(realmURL)}`) .set('Content-Type', 'application/json'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); @@ -625,7 +625,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test('post-deployment endpoint requires authorization header', async function (assert: Assert) { - let response = await context.request2 + let response = await context.request .post('/_post-deployment') .set('Content-Type', 'application/json'); @@ -637,7 +637,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test('post-deployment endpoint rejects incorrect authorization', async function (assert: Assert) { - let response = await context.request2 + let response = await context.request .post('/_post-deployment') .set('Content-Type', 'application/json') .set('Authorization', 'wrong-secret'); @@ -679,7 +679,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { await context.dbAdapter.execute('select * from jobs'); let initialJobCount = initialJobs.length; - let response = await context.request2 + let response = await context.request .post('/_post-deployment') .set('Content-Type', 'application/json') .set('Authorization', "mum's the word"); @@ -771,7 +771,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { await context.dbAdapter.execute('select * from jobs'); let initialJobCount = initialJobs.length; - let response = await context.request2 + let response = await context.request .post('/_post-deployment') .set('Content-Type', 'application/json') .set('Authorization', "mum's the word"); @@ -818,7 +818,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let ownerUserId = `@${owner}:localhost`; let realmURL: string; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -849,11 +849,11 @@ module(`server-endpoints/${basename(__filename)}`, function () { 2, 'number of jobs initially is correct', ); - let staleModuleForRealmOneURL = `${testRealm2URL.href}stale-module-${uuidv4()}.gts`; + let staleModuleForRealmOneURL = `${testRealmURL.href}stale-module-${uuidv4()}.gts`; let staleModuleForRealmTwoURL = `${realmURL}stale-module-${uuidv4()}.gts`; await context.dbAdapter.execute( `INSERT INTO modules (url, file_alias, definitions, deps, created_at, resolved_realm_url, cache_scope, auth_user_id) - VALUES ('${staleModuleForRealmOneURL}', '${staleModuleForRealmOneURL}', '{}', '[]', ${Date.now()}, '${testRealm2URL.href}', 'public', '')`, + VALUES ('${staleModuleForRealmOneURL}', '${staleModuleForRealmOneURL}', '{}', '[]', ${Date.now()}, '${testRealmURL.href}', 'public', '')`, ); await context.dbAdapter.execute( `INSERT INTO modules (url, file_alias, definitions, deps, created_at, resolved_realm_url, cache_scope, auth_user_id) @@ -868,12 +868,12 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'stale module rows were seeded before full reindex', ); { - let response = await context.request2 + let response = await context.request .get(`/_grafana-full-reindex?authHeader=${grafanaSecret}`) .set('Content-Type', 'application/json'); assert.deepEqual( response.body.realms, - [testRealm2URL.href, realmURL], + [testRealmURL.href, realmURL], 'indexed realms are correct', ); } @@ -910,7 +910,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let ownerUserId = `@${owner}:localhost`; let botRealmURL: string; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -936,18 +936,18 @@ module(`server-endpoints/${basename(__filename)}`, function () { botRealmURL = response.body.data.id; } - let staleModuleForNonBotRealmURL = `${testRealm2URL.href}stale-module-${uuidv4()}.gts`; + let staleModuleForNonBotRealmURL = `${testRealmURL.href}stale-module-${uuidv4()}.gts`; let staleModuleForBotRealmURL = `${botRealmURL}stale-module-${uuidv4()}.gts`; await context.dbAdapter.execute( `INSERT INTO modules (url, file_alias, definitions, deps, created_at, resolved_realm_url, cache_scope, auth_user_id) - VALUES ('${staleModuleForNonBotRealmURL}', '${staleModuleForNonBotRealmURL}', '{}', '[]', ${Date.now()}, '${testRealm2URL.href}', 'public', '')`, + VALUES ('${staleModuleForNonBotRealmURL}', '${staleModuleForNonBotRealmURL}', '{}', '[]', ${Date.now()}, '${testRealmURL.href}', 'public', '')`, ); await context.dbAdapter.execute( `INSERT INTO modules (url, file_alias, definitions, deps, created_at, resolved_realm_url, cache_scope, auth_user_id) VALUES ('${staleModuleForBotRealmURL}', '${staleModuleForBotRealmURL}', '{}', '[]', ${Date.now()}, '${botRealmURL}', 'public', '')`, ); - let response = await context.request2 + let response = await context.request .get(`/_grafana-full-reindex?authHeader=${grafanaSecret}`) .set('Content-Type', 'application/json'); assert.strictEqual(response.status, 200, 'HTTP 200 status'); @@ -973,7 +973,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { test('returns 401 when calling grafana full reindex endpoint without a grafana secret', async function (assert) { let initialJobs = await context.dbAdapter.execute('select * from jobs'); { - let response = await context.request2 + let response = await context.request .get(`/_grafana-full-reindex`) .set('Content-Type', 'application/json'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); diff --git a/packages/realm-server/tests/server-endpoints/queue-status-test.ts b/packages/realm-server/tests/server-endpoints/queue-status-test.ts index 65474b6b81..3da55bcac8 100644 --- a/packages/realm-server/tests/server-endpoints/queue-status-test.ts +++ b/packages/realm-server/tests/server-endpoints/queue-status-test.ts @@ -21,6 +21,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { } setupPermissionedRealmCached(hooks, { + fileSystem: {}, permissions: { '*': ['read', 'write'], }, diff --git a/packages/realm-server/tests/server-endpoints/realm-lifecycle-test.ts b/packages/realm-server/tests/server-endpoints/realm-lifecycle-test.ts index 9f83466119..ec091e6d8d 100644 --- a/packages/realm-server/tests/server-endpoints/realm-lifecycle-test.ts +++ b/packages/realm-server/tests/server-endpoints/realm-lifecycle-test.ts @@ -1,6 +1,8 @@ import { module, test } from 'qunit'; import { basename, join } from 'path'; import { existsSync, readJSONSync } from 'fs-extra'; +import supertest from 'supertest'; +import type { Test, SuperTest } from 'supertest'; import { v4 as uuidv4 } from 'uuid'; import type { Query } from '@cardstack/runtime-common/query'; import { @@ -14,11 +16,15 @@ import { cardSrc } from '@cardstack/runtime-common/etc/test-fixtures'; import { closeServer, createJWT, + matrixURL, realmSecretSeed, + runTestRealmServer, + setupPermissionedRealmCached, testRealmInfo, + testRealmURL as rootTestRealmURL, } from '../helpers'; import { createJWT as createRealmServerJWT } from '../../utils/jwt'; -import { setupServerEndpointsTest, testRealm2URL } from './helpers'; +import { setupServerEndpointsTest, testRealmURL } from './helpers'; import '@cardstack/runtime-common/helpers/code-equality-assertion'; module(`server-endpoints/${basename(__filename)}`, function () { @@ -33,7 +39,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let endpoint = `test-realm-${uuidv4()}`; let owner = 'mango'; let ownerUserId = '@mango:localhost'; - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -65,7 +71,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { { data: { type: 'realm', - id: `${testRealm2URL.origin}/${owner}/${endpoint}/`, + id: `${testRealmURL.origin}/${owner}/${endpoint}/`, attributes: { ...testRealmInfo, endpoint, @@ -80,7 +86,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let realmPath = join( context.dir.name, - 'realm_server_2', + 'realm_server_1', owner, endpoint, ); @@ -119,12 +125,12 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); let id: string; - let realm = context.testRealmServer2.testingOnlyRealms.find( + let realm = context.testRealmServer.testingOnlyRealms.find( (r) => r.url === json.data.id, )!; { // owner can create an instance - let response = await context.request2 + let response = await context.request .post(`/${owner}/${endpoint}/`) .send({ data: { @@ -155,7 +161,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { { // owner can get an instance - let response = await context.request2 + let response = await context.request .get(new URL(id).pathname) .set('Accept', 'application/vnd.card+json') .set( @@ -178,7 +184,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { { // owner can search in the realm - let response = await context.request2 + let response = await context.request .post(`${new URL(realm.url).pathname}_search`) .set('Accept', 'application/vnd.card+json') .set('X-HTTP-Method-Override', 'QUERY') @@ -210,7 +216,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let endpoint = `test-realm-${uuidv4()}`; let owner = 'mango'; let ownerUserId = '@mango:localhost'; - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -235,12 +241,12 @@ module(`server-endpoints/${basename(__filename)}`, function () { let realmURL = response.body.data.id; assert.strictEqual(response.status, 201, 'HTTP 201 status'); - let realm = context.testRealmServer2.testingOnlyRealms.find( + let realm = context.testRealmServer.testingOnlyRealms.find( (r) => r.url === realmURL, )!; { - let response = await context.request2 + let response = await context.request .post(`${new URL(realmURL).pathname}_search`) .set('Accept', 'application/vnd.card+json') .set('X-HTTP-Method-Override', 'QUERY') @@ -256,7 +262,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { assert.strictEqual(response.status, 403, 'HTTP 403 status'); - response = await context.request2 + response = await context.request .post(`/${owner}/${endpoint}/`) .send({ data: { @@ -285,7 +291,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let ownerUserId = '@mango:localhost'; let realmURL: string; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -315,11 +321,11 @@ module(`server-endpoints/${basename(__filename)}`, function () { } let id: string; - let realm = context.testRealmServer2.testingOnlyRealms.find( + let realm = context.testRealmServer.testingOnlyRealms.find( (r) => r.url === realmURL, )!; { - let response = await context.request2 + let response = await context.request .post(`/${owner}/${endpoint}/`) .send({ data: { @@ -350,27 +356,45 @@ module(`server-endpoints/${basename(__filename)}`, function () { let jobsBeforeRestart = await context.dbAdapter.execute('select * from jobs'); - // Stop and restart the server - context.testRealmServer2.testingOnlyUnmountRealms(); - await closeServer(context.testRealmHttpServer2); - await context.startRealmServer(); - await context.testRealmServer2.start(); + context.testRealmServer.testingOnlyUnmountRealms(); + await closeServer(context.testRealmHttpServer); + + let restartedServer = await runTestRealmServer({ + virtualNetwork: context.virtualNetwork, + testRealmDir: context.testRealmDir, + realmsRootPath: join(context.dir.name, 'realm_server_1'), + realmURL: testRealmURL, + permissions: { + '*': ['read', 'write'], + '@node-test_realm:localhost': ['read', 'realm-owner'], + }, + dbAdapter: context.dbAdapter, + publisher: context.publisher, + runner: context.runner, + matrixURL, + }); - let jobsAfterRestart = - await context.dbAdapter.execute('select * from jobs'); - assert.strictEqual( - jobsBeforeRestart.length, - jobsAfterRestart.length, - 'no new indexing jobs were created on boot for the created realm', - ); + try { + let jobsAfterRestart = + await context.dbAdapter.execute('select * from jobs'); + assert.strictEqual( + jobsBeforeRestart.length, + jobsAfterRestart.length, + 'no new indexing jobs were created on boot for the created realm', + ); - { - let response = await context.request2 + let restartedRealm = + restartedServer.testRealmServer.testingOnlyRealms.find( + (r) => r.url === realmURL, + ); + assert.ok(restartedRealm, 'realm is mounted after restart'); + let restartedRequest = supertest(restartedServer.testRealmHttpServer); + let response = await restartedRequest .get(new URL(id).pathname) .set('Accept', 'application/vnd.card+json') .set( 'Authorization', - `Bearer ${createJWT(realm, ownerUserId, [ + `Bearer ${createJWT(restartedRealm!, ownerUserId, [ 'read', 'write', 'realm-owner', @@ -384,12 +408,15 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'Test Card', 'instance data is correct', ); + } finally { + restartedServer.testRealmServer.testingOnlyUnmountRealms(); + await closeServer(restartedServer.testRealmHttpServer); } }); test('POST /_create-realm without JWT', async function (assert) { let endpoint = `test-realm-${uuidv4()}`; - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -415,7 +442,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { test('POST /_create-realm with invalid JWT', async function (assert) { let endpoint = `test-realm-${uuidv4()}`; - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -437,7 +464,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test('POST /_create-realm with invalid JSON', async function (assert) { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -458,7 +485,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test('POST /_create-realm with bad JSON-API', async function (assert) { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -483,7 +510,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test('POST /_create-realm without a realm endpoint', async function (assert) { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -514,7 +541,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { test('POST /_create-realm without a realm name', async function (assert) { let endpoint = `test-realm-${uuidv4()}`; - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -543,43 +570,10 @@ module(`server-endpoints/${basename(__filename)}`, function () { ); }); - test('cannot create a realm on a realm server that has a realm mounted at the origin', async function (assert) { - let response = await context.request - .post('/_create-realm') - .set('Accept', 'application/vnd.api+json') - .set('Content-Type', 'application/json') - .set( - 'Authorization', - `Bearer ${createRealmServerJWT( - { user: '@mango:localhost', sessionRoom: 'session-room-test' }, - realmSecretSeed, - )}`, - ) - .send( - JSON.stringify({ - data: { - type: 'realm', - attributes: { - endpoint: 'mango-realm', - name: 'Test Realm', - }, - }, - }), - ); - 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:localhost'; - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -603,7 +597,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { ); assert.strictEqual(response.status, 201, 'HTTP 201 status'); { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -637,7 +631,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { test('cannot create a realm with invalid characters in endpoint', async function (assert) { let ownerUserId = '@mango:localhost'; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -667,7 +661,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { ); } { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -704,7 +698,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let providerEndpoint = `test-realm-provider-${uuidv4()}`; let providerRealmURL: string; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -732,12 +726,12 @@ module(`server-endpoints/${basename(__filename)}`, function () { assert.strictEqual(response.status, 201, 'HTTP 201 status'); providerRealmURL = response.body.data.id; } - let providerRealm = context.testRealmServer2.testingOnlyRealms.find( + let providerRealm = context.testRealmServer.testingOnlyRealms.find( (r) => r.url === providerRealmURL, )!; { // create a card def - let response = await context.request2 + let response = await context.request .post(`/${owner}/${providerEndpoint}/test-card.gts`) .set('Accept', 'application/vnd.card+source') .set( @@ -755,7 +749,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let consumerEndpoint = `test-realm-consumer-${uuidv4()}`; let consumerRealmURL: string; { - let response = await context.request2 + let response = await context.request .post('/_create-realm') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/json') @@ -784,13 +778,13 @@ module(`server-endpoints/${basename(__filename)}`, function () { consumerRealmURL = response.body.data.id; } - let consumerRealm = context.testRealmServer2.testingOnlyRealms.find( + let consumerRealm = context.testRealmServer.testingOnlyRealms.find( (r) => r.url === consumerRealmURL, )!; let id: string; { // create an instance using card def in different private realm - let response = await context.request2 + let response = await context.request .post(`/${owner}/${consumerEndpoint}/`) .send({ data: { @@ -823,7 +817,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { { // get the instance - let response = await context.request2 + let response = await context.request .get(new URL(id).pathname) .set('Accept', 'application/vnd.card+json') .set( @@ -846,4 +840,56 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); }, ); + + module( + 'Realm creation when a realm is mounted at server origin', + function (hooks) { + let request!: SuperTest; + + setupPermissionedRealmCached(hooks, { + realmURL: rootTestRealmURL, + permissions: { + '*': ['read', 'write'], + '@node-test_realm:localhost': ['read', 'realm-owner'], + }, + onRealmSetup(args) { + request = args.request; + }, + }); + + 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:localhost', sessionRoom: 'session-room-test' }, + realmSecretSeed, + )}`, + ) + .send( + JSON.stringify({ + data: { + type: 'realm', + attributes: { + endpoint: 'mango-realm', + name: 'Test Realm', + }, + }, + }), + ); + + 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', + ); + }); + }, + ); }); diff --git a/packages/realm-server/tests/server-endpoints/stripe-session-test.ts b/packages/realm-server/tests/server-endpoints/stripe-session-test.ts index 967e659e2c..0ba32f74a2 100644 --- a/packages/realm-server/tests/server-endpoints/stripe-session-test.ts +++ b/packages/realm-server/tests/server-endpoints/stripe-session-test.ts @@ -38,6 +38,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { } setupPermissionedRealmCached(hooks, { + fileSystem: {}, permissions: { '*': ['read', 'write'], }, diff --git a/packages/realm-server/tests/server-endpoints/user-and-catalog-test.ts b/packages/realm-server/tests/server-endpoints/user-and-catalog-test.ts index 605944e36e..ab7820cbd0 100644 --- a/packages/realm-server/tests/server-endpoints/user-and-catalog-test.ts +++ b/packages/realm-server/tests/server-endpoints/user-and-catalog-test.ts @@ -3,7 +3,7 @@ import { basename } from 'path'; import { getUserByMatrixUserId } from '@cardstack/billing/billing-queries'; import { realmSecretSeed, testRealmInfo } from '../helpers'; import { createJWT as createRealmServerJWT } from '../../utils/jwt'; -import { setupServerEndpointsTest, testRealm2URL } from './helpers'; +import { setupServerEndpointsTest, testRealmURL } from './helpers'; import '@cardstack/runtime-common/helpers/code-equality-assertion'; module(`server-endpoints/${basename(__filename)}`, function () { @@ -28,7 +28,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { test('can create a user', async function (assert) { let ownerUserId = '@mango-new:localhost'; - let response = await context.request2 + let response = await context.request .post('/_user') .set('Accept', 'application/json') .set('Content-Type', 'application/json') @@ -68,12 +68,12 @@ module(`server-endpoints/${basename(__filename)}`, function () { }); test('can not create a user without a jwt', async function (assert) { - let response = await context.request2.post('/_user').send({}); + let response = await context.request.post('/_user').send({}); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('can fetch catalog realms', async function (assert) { - let response = await context.request2 + let response = await context.request .get('/_catalog-realms') .set('Accept', 'application/json'); @@ -82,7 +82,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { data: [ { type: 'catalog-realm', - id: `${testRealm2URL}`, + id: `${testRealmURL}`, attributes: { ...testRealmInfo, }, @@ -102,7 +102,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { return null; }; context.virtualNetwork.mount(failedRealmInfoMock, { prepend: true }); - let response = await context.request2 + let response = await context.request .get('/_catalog-realms') .set('Accept', 'application/json'); diff --git a/packages/realm-server/tests/server-endpoints/webhook-commands-test.ts b/packages/realm-server/tests/server-endpoints/webhook-commands-test.ts index 1017efa091..82191b2eb4 100644 --- a/packages/realm-server/tests/server-endpoints/webhook-commands-test.ts +++ b/packages/realm-server/tests/server-endpoints/webhook-commands-test.ts @@ -10,17 +10,17 @@ module(`server-endpoints/${basename(__filename)}`, function () { let context = setupServerEndpointsTest(hooks); test('requires auth to add webhook command', async function (assert) { - let response = await context.request2.post('/_webhook-commands').send({}); + let response = await context.request.post('/_webhook-commands').send({}); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('requires auth to list webhook commands', async function (assert) { - let response = await context.request2.get('/_webhook-commands'); + let response = await context.request.get('/_webhook-commands'); assert.strictEqual(response.status, 401, 'HTTP 401 status'); }); test('requires auth to delete webhook command', async function (assert) { - let response = await context.request2.delete('/_webhook-commands').send({ + let response = await context.request.delete('/_webhook-commands').send({ data: { type: 'webhook-command', id: uuidv4(), @@ -59,7 +59,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -155,7 +155,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -221,7 +221,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -255,7 +255,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -289,7 +289,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let response = await context.request2 + let response = await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -344,7 +344,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -457,7 +457,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .get('/_webhook-commands') .set( 'Authorization', @@ -557,7 +557,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .get(`/_webhook-commands?incomingWebhookId=${webhookId1}`) .set( 'Authorization', @@ -623,7 +623,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .delete('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -703,7 +703,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { `)`, ]); - let response = await context.request2 + let response = await context.request .delete('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') diff --git a/packages/realm-server/tests/server-endpoints/webhook-receiver-test.ts b/packages/realm-server/tests/server-endpoints/webhook-receiver-test.ts index d4a1975f71..017d4988e3 100644 --- a/packages/realm-server/tests/server-endpoints/webhook-receiver-test.ts +++ b/packages/realm-server/tests/server-endpoints/webhook-receiver-test.ts @@ -11,7 +11,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let context = setupServerEndpointsTest(hooks); test('returns 404 for unknown webhook path', async function (assert) { - let response = await context.request2 + let response = await context.request .post('/_webhooks/whk_nonexistent') .set('Content-Type', 'application/json') .send(JSON.stringify({ event: 'test' })); @@ -28,7 +28,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let createResponse = await context.request2 + let createResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -56,7 +56,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let payload = JSON.stringify({ event: 'push', ref: 'refs/heads/main' }); - let response = await context.request2 + let response = await context.request .post(`/_webhooks/${webhookPath}`) .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', 'sha256=invalidsignature') @@ -74,7 +74,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let createResponse = await context.request2 + let createResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -108,7 +108,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { .update(payload, 'utf8') .digest('hex'); - let response = await context.request2 + let response = await context.request .post(`/_webhooks/${webhookPath}`) .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', signature) @@ -131,7 +131,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let createResponse = await context.request2 + let createResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -166,7 +166,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { .update(payload, 'utf8') .digest('base64'); - let response = await context.request2 + let response = await context.request .post(`/_webhooks/${webhookPath}`) .set('Content-Type', 'application/json') .set('X-Shopify-Hmac-SHA256', signature) @@ -207,7 +207,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { let payload = JSON.stringify({ event: 'test' }); - let response = await context.request2 + let response = await context.request .post('/_webhooks/whk_noheader') .set('Content-Type', 'application/json') .send(payload); @@ -228,7 +228,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { 'user@example.com', ); - let createResponse = await context.request2 + let createResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -263,7 +263,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { .digest('hex'); // No Authorization header set - this should still work - let response = await context.request2 + let response = await context.request .post(`/_webhooks/${webhookPath}`) .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', signature) @@ -287,7 +287,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { ); // Create webhook - let createWebhookResponse = await context.request2 + let createWebhookResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -317,7 +317,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { createWebhookResponse.body.data.attributes.signingSecret; // Register webhook command - await context.request2 + await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -350,7 +350,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { .update(payload, 'utf8') .digest('hex'); - let response = await context.request2 + let response = await context.request .post(`/_webhooks/${webhookPath}`) .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', signature) @@ -380,7 +380,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { ); // Create webhook - let createWebhookResponse = await context.request2 + let createWebhookResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -410,7 +410,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { createWebhookResponse.body.data.attributes.signingSecret; // Register command filtered to 'push' events only - await context.request2 + await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -443,7 +443,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { .update(payload, 'utf8') .digest('hex'); - let response = await context.request2 + let response = await context.request .post(`/_webhooks/${webhookPath}`) .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', signature) @@ -472,7 +472,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { realmSecretSeed, )}`; - let createWebhookResponse = await context.request2 + let createWebhookResponse = await context.request .post('/_incoming-webhooks') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -496,7 +496,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { createWebhookResponse.body.data.attributes.signingSecret; // Register command with roomId and realm in filter - await context.request2 + await context.request .post('/_webhook-commands') .set('Accept', 'application/vnd.api+json') .set('Content-Type', 'application/vnd.api+json') @@ -531,7 +531,7 @@ module(`server-endpoints/${basename(__filename)}`, function () { .update(payload, 'utf8') .digest('hex'); - let response = await context.request2 + let response = await context.request .post(`/_webhooks/${webhookPath}`) .set('Content-Type', 'application/json') .set('X-Hub-Signature-256', signature)