diff --git a/packages/create-twenty-app/src/constants/template/src/__tests__/app-install.integration-test.ts b/packages/create-twenty-app/src/constants/template/src/__tests__/app-install.integration-test.ts deleted file mode 100644 index a51ac84432..0000000000 --- a/packages/create-twenty-app/src/constants/template/src/__tests__/app-install.integration-test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers'; -import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; -import { MetadataApiClient } from 'twenty-client-sdk/metadata'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - -const APP_PATH = process.cwd(); - -describe('App installation', () => { - beforeAll(async () => { - const buildResult = await appBuild({ - appPath: APP_PATH, - tarball: true, - onProgress: (message: string) => console.log(`[build] ${message}`), - }); - - if (!buildResult.success) { - throw new Error( - `Build failed: ${buildResult.error?.message ?? 'Unknown error'}`, - ); - } - - const deployResult = await appDeploy({ - tarballPath: buildResult.data.tarballPath!, - onProgress: (message: string) => console.log(`[deploy] ${message}`), - }); - - if (!deployResult.success) { - throw new Error( - `Deploy failed: ${deployResult.error?.message ?? 'Unknown error'}`, - ); - } - - const installResult = await appInstall({ appPath: APP_PATH }); - - if (!installResult.success) { - throw new Error( - `Install failed: ${installResult.error?.message ?? 'Unknown error'}`, - ); - } - }); - - afterAll(async () => { - const uninstallResult = await appUninstall({ appPath: APP_PATH }); - - if (!uninstallResult.success) { - console.warn( - `App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`, - ); - } - }); - - it('should find the installed app in the applications list', async () => { - const metadataClient = new MetadataApiClient(); - - const result = await metadataClient.query({ - findManyApplications: { - id: true, - name: true, - universalIdentifier: true, - }, - }); - - const installedApp = result.findManyApplications.find( - (application: { universalIdentifier: string }) => - application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, - ); - - expect(installedApp).toBeDefined(); - }); -}); diff --git a/packages/create-twenty-app/src/constants/template/src/__tests__/global-setup.ts b/packages/create-twenty-app/src/constants/template/src/__tests__/global-setup.ts new file mode 100644 index 0000000000..76d5993737 --- /dev/null +++ b/packages/create-twenty-app/src/constants/template/src/__tests__/global-setup.ts @@ -0,0 +1,87 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { appDevOnce, appUninstall } from 'twenty-sdk/cli'; + +const APP_PATH = process.cwd(); +const CONFIG_DIR = path.join(os.homedir(), '.twenty'); + +function validateEnv(): { apiUrl: string; apiKey: string } { + const apiUrl = process.env.TWENTY_API_URL; + const apiKey = process.env.TWENTY_API_KEY; + + if (!apiUrl || !apiKey) { + throw new Error( + 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + + 'Start a local server: yarn twenty server start\n' + + 'Or set them in vitest env config.', + ); + } + + return { apiUrl, apiKey }; +} + +async function checkServer(apiUrl: string) { + let response: Response; + + try { + response = await fetch(`${apiUrl}/healthz`); + } catch { + throw new Error( + `Twenty server is not reachable at ${apiUrl}. ` + + 'Make sure the server is running before executing integration tests.', + ); + } + + if (!response.ok) { + throw new Error(`Server at ${apiUrl} returned ${response.status}`); + } +} + +function writeConfig(apiUrl: string, apiKey: string) { + const payload = JSON.stringify( + { + remotes: { + local: { apiUrl, apiKey, accessToken: apiKey }, + }, + defaultRemote: 'local', + }, + null, + 2, + ); + + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload); +} + +export async function setup() { + const { apiUrl, apiKey } = validateEnv(); + + await checkServer(apiUrl); + + writeConfig(apiUrl, apiKey); + + await appUninstall({ appPath: APP_PATH }).catch(() => {}); + + const result = await appDevOnce({ + appPath: APP_PATH, + onProgress: (message: string) => console.log(`[dev] ${message}`), + }); + + if (!result.success) { + throw new Error( + `Dev sync failed: ${result.error?.message ?? 'Unknown error'}`, + ); + } +} + +export async function teardown() { + const uninstallResult = await appUninstall({ appPath: APP_PATH }); + + if (!uninstallResult.success) { + console.warn( + `App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`, + ); + } +} diff --git a/packages/create-twenty-app/src/constants/template/src/__tests__/schema.integration-test.ts b/packages/create-twenty-app/src/constants/template/src/__tests__/schema.integration-test.ts new file mode 100644 index 0000000000..31007e28e9 --- /dev/null +++ b/packages/create-twenty-app/src/constants/template/src/__tests__/schema.integration-test.ts @@ -0,0 +1,46 @@ +import { CoreApiClient } from 'twenty-client-sdk/core'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers'; +import { describe, expect, it } from 'vitest'; + +describe('App installation', () => { + it('should find the installed app in the applications list', async () => { + const client = new MetadataApiClient(); + + const result = await client.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const app = result.findManyApplications.find( + (a: { universalIdentifier: string }) => + a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(app).toBeDefined(); + }); +}); + +describe('CoreApiClient', () => { + it('should support CRUD on standard objects', async () => { + const client = new CoreApiClient(); + + const created = await client.mutation({ + createNote: { + __args: { data: { title: 'Integration test note' } }, + id: true, + }, + }); + expect(created.createNote.id).toBeDefined(); + + await client.mutation({ + destroyNote: { + __args: { id: created.createNote.id }, + id: true, + }, + }); + }); +}); diff --git a/packages/create-twenty-app/src/constants/template/src/__tests__/setup-test.ts b/packages/create-twenty-app/src/constants/template/src/__tests__/setup-test.ts deleted file mode 100644 index 40cb3888db..0000000000 --- a/packages/create-twenty-app/src/constants/template/src/__tests__/setup-test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { beforeAll } from 'vitest'; - -const CONFIG_DIR = path.join(os.homedir(), '.twenty'); -const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json'); - -beforeAll(async () => { - const apiUrl = process.env.TWENTY_API_URL!; - const token = process.env.TWENTY_API_KEY!; - - if (!apiUrl || !token) { - throw new Error( - 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + - 'Start a local server: yarn twenty server start\n' + - 'Or set them in vitest env config.', - ); - } - - let response: Response; - - try { - response = await fetch(`${apiUrl}/healthz`); - } catch { - throw new Error( - `Twenty server is not reachable at ${apiUrl}. ` + - 'Make sure the server is running before executing integration tests.', - ); - } - - if (!response.ok) { - throw new Error(`Server at ${apiUrl} returned ${response.status}`); - } - - fs.mkdirSync(CONFIG_DIR, { recursive: true }); - - fs.writeFileSync( - CONFIG_PATH, - JSON.stringify( - { - remotes: { - local: { apiUrl, apiKey: token }, - }, - defaultRemote: 'local', - }, - null, - 2, - ), - ); - - process.env.TWENTY_APP_ACCESS_TOKEN ??= token; -}); diff --git a/packages/create-twenty-app/src/constants/template/vitest.config.ts b/packages/create-twenty-app/src/constants/template/vitest.config.ts index 243b9dbc58..055af77dbe 100644 --- a/packages/create-twenty-app/src/constants/template/vitest.config.ts +++ b/packages/create-twenty-app/src/constants/template/vitest.config.ts @@ -1,6 +1,15 @@ import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TWENTY_API_KEY = + process.env.TWENTY_API_KEY ?? + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc'; + +// Make env vars available to globalSetup (test.env only applies to workers) +process.env.TWENTY_API_URL = TWENTY_API_URL; +process.env.TWENTY_API_KEY = TWENTY_API_KEY; + export default defineConfig({ plugins: [ tsconfigPaths({ @@ -11,13 +20,12 @@ export default defineConfig({ test: { testTimeout: 120_000, hookTimeout: 120_000, + fileParallelism: false, include: ['src/**/*.integration-test.ts'], - setupFiles: ['src/__tests__/setup-test.ts'], + globalSetup: ['src/__tests__/global-setup.ts'], env: { - TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020', - TWENTY_API_KEY: - process.env.TWENTY_API_KEY ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc', + TWENTY_API_URL, + TWENTY_API_KEY, }, }, }); diff --git a/packages/twenty-apps/examples/postcard/src/__tests__/app-install.integration-test.ts b/packages/twenty-apps/examples/postcard/src/__tests__/app-install.integration-test.ts deleted file mode 100644 index dc21ab8778..0000000000 --- a/packages/twenty-apps/examples/postcard/src/__tests__/app-install.integration-test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config'; -import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; -import { MetadataApiClient } from 'twenty-client-sdk/metadata'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - -const APP_PATH = process.cwd(); - -describe('App installation', () => { - beforeAll(async () => { - const buildResult = await appBuild({ - appPath: APP_PATH, - tarball: true, - onProgress: (message: string) => console.log(`[build] ${message}`), - }); - - if (!buildResult.success) { - throw new Error( - `Build failed: ${buildResult.error?.message ?? 'Unknown error'}`, - ); - } - - const deployResult = await appDeploy({ - tarballPath: buildResult.data.tarballPath!, - onProgress: (message: string) => console.log(`[deploy] ${message}`), - }); - - if (!deployResult.success) { - throw new Error( - `Deploy failed: ${deployResult.error?.message ?? 'Unknown error'}`, - ); - } - - const installResult = await appInstall({ appPath: APP_PATH }); - - if (!installResult.success) { - throw new Error( - `Install failed: ${installResult.error?.message ?? 'Unknown error'}`, - ); - } - }); - - afterAll(async () => { - const uninstallResult = await appUninstall({ appPath: APP_PATH }); - - if (!uninstallResult.success) { - console.warn( - `App uninstall failed: ${ - uninstallResult.error?.message ?? 'Unknown error' - }`, - ); - } - }); - - it('should find the installed app in the applications list', async () => { - const metadataClient = new MetadataApiClient(); - - const result = await metadataClient.query({ - findManyApplications: { - id: true, - name: true, - universalIdentifier: true, - }, - }); - - const installedApp = result.findManyApplications.find( - (application: { universalIdentifier: string }) => - application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, - ); - - expect(installedApp).toBeDefined(); - }); -}); diff --git a/packages/twenty-apps/examples/postcard/src/__tests__/global-setup.ts b/packages/twenty-apps/examples/postcard/src/__tests__/global-setup.ts new file mode 100644 index 0000000000..76d5993737 --- /dev/null +++ b/packages/twenty-apps/examples/postcard/src/__tests__/global-setup.ts @@ -0,0 +1,87 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { appDevOnce, appUninstall } from 'twenty-sdk/cli'; + +const APP_PATH = process.cwd(); +const CONFIG_DIR = path.join(os.homedir(), '.twenty'); + +function validateEnv(): { apiUrl: string; apiKey: string } { + const apiUrl = process.env.TWENTY_API_URL; + const apiKey = process.env.TWENTY_API_KEY; + + if (!apiUrl || !apiKey) { + throw new Error( + 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + + 'Start a local server: yarn twenty server start\n' + + 'Or set them in vitest env config.', + ); + } + + return { apiUrl, apiKey }; +} + +async function checkServer(apiUrl: string) { + let response: Response; + + try { + response = await fetch(`${apiUrl}/healthz`); + } catch { + throw new Error( + `Twenty server is not reachable at ${apiUrl}. ` + + 'Make sure the server is running before executing integration tests.', + ); + } + + if (!response.ok) { + throw new Error(`Server at ${apiUrl} returned ${response.status}`); + } +} + +function writeConfig(apiUrl: string, apiKey: string) { + const payload = JSON.stringify( + { + remotes: { + local: { apiUrl, apiKey, accessToken: apiKey }, + }, + defaultRemote: 'local', + }, + null, + 2, + ); + + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload); +} + +export async function setup() { + const { apiUrl, apiKey } = validateEnv(); + + await checkServer(apiUrl); + + writeConfig(apiUrl, apiKey); + + await appUninstall({ appPath: APP_PATH }).catch(() => {}); + + const result = await appDevOnce({ + appPath: APP_PATH, + onProgress: (message: string) => console.log(`[dev] ${message}`), + }); + + if (!result.success) { + throw new Error( + `Dev sync failed: ${result.error?.message ?? 'Unknown error'}`, + ); + } +} + +export async function teardown() { + const uninstallResult = await appUninstall({ appPath: APP_PATH }); + + if (!uninstallResult.success) { + console.warn( + `App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`, + ); + } +} diff --git a/packages/twenty-apps/examples/postcard/src/__tests__/schema.integration-test.ts b/packages/twenty-apps/examples/postcard/src/__tests__/schema.integration-test.ts new file mode 100644 index 0000000000..0e8bbb9ec8 --- /dev/null +++ b/packages/twenty-apps/examples/postcard/src/__tests__/schema.integration-test.ts @@ -0,0 +1,63 @@ +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config'; +import { describe, expect, it } from 'vitest'; + +describe('App installation', () => { + it('should find the installed app in the applications list', async () => { + const client = new MetadataApiClient(); + + const result = await client.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const app = result.findManyApplications.find( + (a: { universalIdentifier: string }) => + a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(app).toBeDefined(); + }); +}); + +describe('PostCard object', () => { + it('should exist with expected fields and relations', async () => { + const client = new MetadataApiClient(); + + const { objects } = await client.query({ + objects: { + __args: { + filter: { isCustom: { is: true } }, + paging: { first: 50 }, + }, + edges: { + node: { + nameSingular: true, + fields: { + __args: { paging: { first: 500 } }, + edges: { node: { name: true } }, + }, + }, + }, + }, + }); + + const obj = objects.edges + .map((e: { node: { nameSingular: string } }) => e.node) + .find((n: { nameSingular: string }) => n.nameSingular === 'postCard'); + expect(obj).toBeDefined(); + + const names = obj!.fields.edges.map( + (e: { node: { name: string } }) => e.node.name, + ); + expect(names).toContain('name'); + expect(names).toContain('content'); + expect(names).toContain('status'); + expect(names).toContain('deliveredAt'); + expect(names).toContain('recipient'); + }); + +}); diff --git a/packages/twenty-apps/examples/postcard/src/__tests__/setup-test.ts b/packages/twenty-apps/examples/postcard/src/__tests__/setup-test.ts deleted file mode 100644 index 40cb3888db..0000000000 --- a/packages/twenty-apps/examples/postcard/src/__tests__/setup-test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { beforeAll } from 'vitest'; - -const CONFIG_DIR = path.join(os.homedir(), '.twenty'); -const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json'); - -beforeAll(async () => { - const apiUrl = process.env.TWENTY_API_URL!; - const token = process.env.TWENTY_API_KEY!; - - if (!apiUrl || !token) { - throw new Error( - 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + - 'Start a local server: yarn twenty server start\n' + - 'Or set them in vitest env config.', - ); - } - - let response: Response; - - try { - response = await fetch(`${apiUrl}/healthz`); - } catch { - throw new Error( - `Twenty server is not reachable at ${apiUrl}. ` + - 'Make sure the server is running before executing integration tests.', - ); - } - - if (!response.ok) { - throw new Error(`Server at ${apiUrl} returned ${response.status}`); - } - - fs.mkdirSync(CONFIG_DIR, { recursive: true }); - - fs.writeFileSync( - CONFIG_PATH, - JSON.stringify( - { - remotes: { - local: { apiUrl, apiKey: token }, - }, - defaultRemote: 'local', - }, - null, - 2, - ), - ); - - process.env.TWENTY_APP_ACCESS_TOKEN ??= token; -}); diff --git a/packages/twenty-apps/examples/postcard/vitest.config.ts b/packages/twenty-apps/examples/postcard/vitest.config.ts index 243b9dbc58..055af77dbe 100644 --- a/packages/twenty-apps/examples/postcard/vitest.config.ts +++ b/packages/twenty-apps/examples/postcard/vitest.config.ts @@ -1,6 +1,15 @@ import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TWENTY_API_KEY = + process.env.TWENTY_API_KEY ?? + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc'; + +// Make env vars available to globalSetup (test.env only applies to workers) +process.env.TWENTY_API_URL = TWENTY_API_URL; +process.env.TWENTY_API_KEY = TWENTY_API_KEY; + export default defineConfig({ plugins: [ tsconfigPaths({ @@ -11,13 +20,12 @@ export default defineConfig({ test: { testTimeout: 120_000, hookTimeout: 120_000, + fileParallelism: false, include: ['src/**/*.integration-test.ts'], - setupFiles: ['src/__tests__/setup-test.ts'], + globalSetup: ['src/__tests__/global-setup.ts'], env: { - TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020', - TWENTY_API_KEY: - process.env.TWENTY_API_KEY ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc', + TWENTY_API_URL, + TWENTY_API_KEY, }, }, });