feat: add integration tests (#6923)

### Summary

This PR introduces several integration tests, a mix of manually written
tests and those generated using the `generate-integration-tests` Python
script located in the `scripts` folder.

### Tests Added:
- **Authentication tests**: Validating login, registration, and token
handling.
- **FindMany queries**: Fetching multiple records for all existing
entities that do not require input arguments.

### How the Integration Tests Work:
- A `setupTest` function is called during the Jest test run. This
function initializes a test instance of the application and exposes it
on a dedicated port.
- Since tests are executed in isolated workers, they do not have direct
access to the in-memory app instance. Instead, the tests query the
application through the exposed port.
- A static accessToken is used, this one as a big expiration time so it
will never expire (365 years)
- The queries are executed, and the results are validated against
expected outcomes.

### Current State and Next Steps:
- These tests currently run using the existing development seed data. We
plan to introduce more comprehensive test data using `faker` to improve
coverage.
- At the moment, the only mutation tests implemented are for
authentication. Future updates should include broader mutation testing
for other entities.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2024-09-20 11:02:52 +02:00
committed by GitHub
parent 7781d70bb8
commit eef7f1661d
69 changed files with 2797 additions and 492 deletions

View File

@@ -17,7 +17,7 @@ concurrency:
cancel-in-progress: true
jobs:
server-test:
server-setup:
runs-on: ubuntu-latest
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
@@ -29,6 +29,10 @@ jobs:
POSTGRES_USER: postgres
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -36,7 +40,7 @@ jobs:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Tasks Cache
- name: Server / Restore Task Cache
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
@@ -45,14 +49,71 @@ jobs:
with:
tag: scope:backend
tasks: lint,typecheck
- name: Server / Run tests
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
tasks: test
- name: Server / Build
run: npx nx build twenty-server
- name: Server / Write .env
run: npx nx reset:env twenty-server
- name: Worker / Run
run: MESSAGE_QUEUE_TYPE=sync npx nx worker twenty-server
server-test:
runs-on: ubuntu-latest
needs: server-setup
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run Tests
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
tasks: test
server-integration-test:
runs-on: ubuntu-latest
needs: server-setup
services:
postgres:
image: twentycrm/twenty-postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run Integration Tests
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
tasks: "test:integration"
- name: Server / Upload reset-logs file
if: always()
uses: actions/upload-artifact@v4
with:
name: reset-logs
path: reset-logs.log

View File

@@ -20,6 +20,10 @@
"name": "packages/twenty-ui",
"path": "../packages/twenty-ui"
},
{
"name": "packages/twenty-emails",
"path": "../packages/twenty-emails"
},
{
"name": "packages/twenty-postgres",
"path": "../packages/twenty-postgres"

View File

@@ -1,26 +1,26 @@
PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/test
DEBUG_MODE=true
PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/test?connection_limit=1
# Use this for docker setup
# PG_DATABASE_URL=postgres://twenty:twenty@postgres:5432/default?connection_limit=1
# the URL of the front-end app
DEBUG_PORT=9000
FRONT_BASE_URL=http://localhost:3001
# random keys used to generate JWT tokens
ACCESS_TOKEN_SECRET=secret_jwt
LOGIN_TOKEN_SECRET=secret_login_tokens
REFRESH_TOKEN_SECRET=secret_refresh_token
FILE_TOKEN_SECRET=secret_file_token
ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
SIGN_IN_PREFILLED=true
EXCEPTION_HANDLER_DRIVER=console
SENTRY_DSN=https://ba869cb8fd72d5faeb6643560939cee0@o4505516959793152.ingest.sentry.io/4506660900306944
DEMO_WORKSPACE_IDS=63db4589-590f-42b3-bdf1-85268b3da02f,8de58f3f-7e86-4a0b-998d-b2cbe314ee3a,4d957b72-0b37-4bad-9468-8dc828ee082d,daa0b739-269e-49b6-9be5-5f0215941489,59c15f6a-909a-4495-9cf4-3ce1b0abbb6a,7202cc9d-92da-4b52-a323-d29d38cd3b4e,5f071b0d-646b-411a-94f1-5d9ba9d5c6ac,7bc10973-897b-4767-ab2f-35cdac3b2aec,4b3ba0be-2d29-4b1e-be66-8ac7eb65d000,edfb500d-cc4e-4f22-8e2b-f139a9758a68,eee459c9-9057-4459-ae0d-d51d14c01635,3dd2f505-0075-4217-ba33-fc4244aeaaa9,3d1a9165-3f3f-494e-a99d-f858eae95144,84db6ded-cfce-4aee-9160-6553b05c8143,96fb1540-269b-4d13-af21-2a8268eff8ca,b2463e69-d121-4ea5-80c9-bba82403e93e,5af30c15-867d-49ed-b939-d4856bed8514,b5677aa1-68fa-4818-aaaa-434a07ae2ed4,1ec7fa9a-d6bf-4fa2-a753-9a235d75ee3f,753a6fa2-df27-4c87-8c90-4da78fcb30dd,2138f2f2-bbe9-41df-b483-687a9075f94e,a885cfef-4636-4c3a-9788-1ff6e6b92df5,5458f7fb-9431-47a2-b7a0-32f31d115e23,6c09929f-11c3-4f92-9508-aa0e6b934d1e,57ae0a2c-7a4e-4c7d-8f43-68548e7f1206,cc7f0b85-6868-4c2d-85c5-3ce9977ea346,21871a7f-f067-45ea-989e-44339bb5ad07,c3efedab-84f5-4656-8297-55964b3d26cb,647dcdd1-4540-4003-9f58-fd84d4d759b7,fc5e6857-8d67-47b8-98f2-edeb0671e326,1ad8d72c-1826-40ed-8b44-d15a1d2aab70,eac6c90a-d25d-4c8c-a053-cfbc7cde0afb,023a70de-a85e-43fc-bbc6-757fbf6562f0,f3f0a7fb-1409-443b-8e39-4e58e628796e,62828804-97d4-40ec-82fa-2992a6ce4a81,af5441fe-b16f-4996-87f4-1a433ec53dd6,e8857860-f7b1-4478-9741-1eb9e7c11f2c,6bca9c44-c8c0-49f8-b0b5-1bb2ca7842b8,d50da092-09df-448f-84ea-3ebddfe1d9f6,9efd5d6d-db64-47d4-9ad3-5e4d8b65ff7f,6f089094-2dd2-4b0e-b5b7-8bb52b93ea8e,299b0822-68e9-4bfa-af35-da799012e80e,a3dd579c-93be-45a0-ad35-f518d8ed45dd,023b1b3e-4891-4061-aae0-f34368644f40,50174445-33c5-4482-bb8c-3ef6c511c8cd,9933c048-07a7-4735-9af2-940c2f9b6683,beadc568-3962-46bd-ad4d-06e23b37615b,0cdafc9f-d4c1-4576-837e-d7f6ec28643d,50bb24ce-1709-4928-a87b-d9d9e147a2ab,7690ed72-910d-4357-8e0e-17aa702b0b94,1ad0d69f-60fa-414f-bf79-4f94c2abba43,946d84a4-db4d-48cb-a5d3-03081b5c7e8e,1a080055-d2bf-4b14-8957-88a7d08769b8,ed343e38-e405-4fae-9486-27b09c98bdad,c8bdef75-a98c-4646-a372-3251340d2dea,87a8c6fa-f93e-4950-aff2-5f956ca1a6ba,604781ba-23c2-4220-a717-b5615431fcd9,31af6841-ad9f-4f28-a637-b5c5e6589447,cf067451-7b88-4ff2-a96d-3fc9c5d6fea0,26a8ad5e-29d9-4e7d-aa1f-e6221e8ea32a,fd14db29-e4df-44a7-9b3f-d00384458122,73b477a8-fcf4-4860-a685-65a0a79b8653,82e0f305-4c6c-4160-be1d-b0de834124e6,e38567ab-a6e2-4a94-99c5-a7db31c0aae8,faf3d6dc-66ff-4c1b-9658-f65a9cd9fcf1,6df6bb90-200e-4290-b73d-9bb374554229,2ff10cf4-a871-404a-9e7b-5ca7a232567e,06c614e2-0f36-4b72-8c82-59631680add2,5e508c81-3453-4185-ae8c-4c9b841f8c15,21b5c371-6010-4b1b-be67-7538eb877efb,54e61442-e291-4eea-8d49-7f11b5f85bd2,b6b7260a-4eea-40b0-9f7f-1dfd4c3cc7a8,e163fe76-30fb-44fb-b51a-50cc78745a21,4da672f2-29b4-4a98-b27c-b39a4aecc858,2fdb0601-c882-4aaf-ad49-ae17e530d47a,49525e1b-1b47-4545-a98c-0ba58778179f,f958ab32-b152-4004-9228-18148f7380f1,0ff5025a-62cd-4a10-a722-79f7cf360f01,642df445-e314-409a-a97d-64fc2aa2a15e,38b0dab5-d4fb-44f9-8cf9-bb35cf82e91d,62054133-f35a-4f64-a2ee-a31e48952835,536dbe8c-af33-4eab-a0a8-8d039a00db40,a04998ba-52c9-4538-b6d9-6d04408dbaf2,89016c7a-3d36-4619-a5c6-4f31795eebf7,7708b9a9-776c-46fc-94a4-dc28e7880958,5c92bc69-b328-4c66-a791-a05dbaf7a6f8,ad580a50-80b4-44be-9bc4-f2b57cd23207,36c0241c-891e-4b74-bd10-5e99df96bbc8,a96842ff-18be-4536-a23d-20d973d91621,0ea549b0-9558-4bdf-9944-5abc707c7660,0186c353-5ed2-4c94-b71a-fc0b48c90288,1508a165-2217-4911-b31c-1ea42a08f097,1731e392-dfdf-4fc4-863b-27ae62b0e374,0b245cea-96a6-4a3a-af6a-ef43496c239c,a844e208-7078-43a2-8bd0-86f31498cd3f,53d112b5-87f2-490b-a788-df1f4624f9ad,0d5794d4-3a52-482b-9a6a-f8185018bad1,df877aa6-231c-47fb-9be0-906e61677356,c56c6d1a-3418-49d2-82ce-bd9370668043,6e0b6f34-3cd0-4aa0-ae1f-25f5545dca68
MUTATION_MAXIMUM_RECORD_AFFECTED=100
MESSAGE_QUEUE_TYPE=pg-boss
CACHE_STORAGE_TYPE=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_USERNAME=default
REDIS_PASSWORD=
# ———————— Optional ————————
# DEBUG_MODE=false
# SIGN_IN_PREFILLED=false
# ACCESS_TOKEN_EXPIRES_IN=30m
# LOGIN_TOKEN_EXPIRES_IN=15m
# REFRESH_TOKEN_EXPIRES_IN=90d
# FILE_TOKEN_EXPIRES_IN=1d
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
# AUTH_GOOGLE_ENABLED=false
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
# STORAGE_TYPE=local
# STORAGE_LOCAL_PATH=.local-storage
# MUTATION_MAXIMUM_AFFECTED_RECORDS=100
AUTH_GOOGLE_ENABLED=false
MESSAGING_PROVIDER_GMAIL_ENABLED=false
CALENDAR_PROVIDER_GOOGLE_ENABLED=false
AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token
MESSAGING_PROVIDER_GMAIL_CALLBACK_URL=http://localhost:3000/auth/google-gmail/get-access-token

View File

@@ -93,5 +93,11 @@ module.exports = {
'@nx/workspace-inject-workspace-repository': 'warn',
},
},
{
files: ['scripts/**/*.ts'],
parserOptions: {
project: ['packages/twenty-server/tsconfig.scripts.json'],
},
},
],
};

View File

@@ -1,3 +1,4 @@
dist/*
.local-storage
logs/**/*
*.log

17
packages/twenty-server/@types/jest.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import 'jest';
declare module '@jest/types' {
namespace Config {
interface ConfigGlobals {
APP_PORT: number;
ACCESS_TOKEN: string;
}
}
}
declare global {
const APP_PORT: number;
const ACCESS_TOKEN: string;
}
export {};

View File

@@ -0,0 +1,34 @@
import { JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tsConfig = require('./tsconfig.json');
const jestConfig: JestConfigWithTsJest = {
silent: false,
verbose: true,
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testEnvironment: 'node',
testRegex: '.integration-spec.ts$',
modulePathIgnorePatterns: ['<rootDir>/dist'],
globalSetup: '<rootDir>/test/utils/setup-test.ts',
globalTeardown: '<rootDir>/test/utils/teardown-test.ts',
testTimeout: 15000,
moduleNameMapper: {
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths),
'twenty-emails': '<rootDir>/../twenty-emails/dist/index.js',
},
fakeTimers: {
enableGlobally: true,
},
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
globals: {
APP_PORT: 4000,
ACCESS_TOKEN:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzI2NDkyNTAyLCJleHAiOjEzMjQ1MDE2NTAyfQ.zM6TbfeOqYVH5Sgryc2zf02hd9uqUOSL1-iJlMgwzsI',
},
};
export default jestConfig;

View File

@@ -1,22 +1,27 @@
module.exports = {
import { JestConfigWithTsJest } from 'ts-jest';
const jestConfig: JestConfigWithTsJest = {
// to enable logs, comment out the following line
silent: true,
clearMocks: true,
preset: 'ts-jest',
testEnvironment: 'node',
modulePathIgnorePatterns: ['<rootDir>/dist'],
moduleFileExtensions: ['js', 'json', 'ts'],
moduleNameMapper: {
'^src/(.*)': '<rootDir>/src/$1',
},
displayName: 'twenty-server',
rootDir: './',
testEnvironment: 'node',
transformIgnorePatterns: ['../../node_modules/'],
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
moduleNameMapper: {
'^src/(.*)': '<rootDir>/src/$1',
},
moduleFileExtensions: ['js', 'json', 'ts'],
modulePathIgnorePatterns: ['<rootDir>/dist'],
fakeTimers: {
enableGlobally: true,
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
};
export default jestConfig;

View File

@@ -11,6 +11,16 @@
"commands": ["rimraf dist", "nest build --path ./tsconfig.build.json"]
}
},
"test:integration": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/twenty-server",
"commands": [
"NODE_ENV=test nx database:reset > reset-logs.log && NODE_ENV=test nx jest --config ./jest-integration.config.ts"
]
},
"parallel": false
},
"build:packageJson": {
"executor": "@nx/js:tsc",
"options": {
@@ -108,12 +118,11 @@
"command": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register ../../node_modules/.bin/jest --runInBand"
}
},
"test:e2e": {
"jest": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-server",
"command": "./scripts/run-integration.sh"
"command": "jest"
}
},
"database:migrate": {
@@ -140,6 +149,16 @@
"parallel": false
}
},
"generate:integration-test": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/twenty-server",
"commands": [
"nx ts-node-no-deps -- ./scripts/generate-integration-tests/index.ts"
],
"parallel": false
}
},
"database:reset": {
"executor": "nx:run-commands",
"dependsOn": ["build"],

View File

@@ -0,0 +1,213 @@
import * as fs from 'fs';
import * as path from 'path';
import * as process from 'process';
import { INTROSPECTION_QUERY } from './introspection-query';
import {
Field,
InputValue,
IntrospectionResponse,
TypeRef,
} from './introspection.interface';
const GRAPHQL_URL = 'http://localhost:3000/graphql';
const BEARER_TOKEN =
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzI2NDkyNTAyLCJleHAiOjEzMjQ1MDE2NTAyfQ.zM6TbfeOqYVH5Sgryc2zf02hd9uqUOSL1-iJlMgwzsI';
const TEST_OUTPUT_DIR = './test';
const fetchGraphQLSchema = async (): Promise<IntrospectionResponse> => {
const headers = {
Authorization: BEARER_TOKEN,
'Content-Type': 'application/json',
};
const response = await fetch(GRAPHQL_URL, {
method: 'POST',
headers,
body: JSON.stringify({ query: INTROSPECTION_QUERY }),
});
if (!response.ok) {
throw new Error(`Failed to fetch schema: ${response.statusText}`);
}
return response.json();
};
const toKebabCase = (name: string): string => {
return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
};
const unwrapType = (typeInfo: TypeRef): any => {
while (typeInfo.ofType) {
typeInfo = typeInfo.ofType;
}
return typeInfo;
};
const hasRequiredArgs = (args: InputValue[]): boolean => {
return args.some((arg) => unwrapType(arg.type).kind === 'NON_NULL');
};
const generateTestContent = (
queryName: string,
fields: Field[],
): string | null => {
const fieldNames = fields
.filter((f) => ['SCALAR', 'ENUM'].includes(unwrapType(f.type).kind))
.map((f) => f.name);
if (fieldNames.length === 0) {
console.log(`Skipping ${queryName}: No usable fields found.`);
return null;
}
const fieldSelection = fieldNames.join('\n ');
const expectSelection = fieldNames
.map((f) => `expect(${queryName}).toHaveProperty('${f}');`)
.join('\n ');
return `import request from 'supertest';
const client = request(\`http://localhost:\${APP_PORT}\`);
describe('${queryName}Resolver (e2e)', () => {
it('should find many ${queryName}', () => {
const queryData = {
query: \`
query ${queryName} {
${queryName} {
edges {
node {
${fieldSelection}
}
}
}
}
\`,
};
return client
.post('/graphql')
.set('Authorization', \`Bearer \${ACCESS_TOKEN}\`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.${queryName};
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const ${queryName} = edges[0].node;
${expectSelection}
}
});
});
});
`;
};
const writeTestFile = (
queryName: string,
content: string | null,
force = false,
): string => {
if (!content) return 'skipped';
const fileName = `${toKebabCase(queryName)}.integration-spec.ts`;
const filePath = path.join(TEST_OUTPUT_DIR, fileName);
if (fs.existsSync(filePath) && !force) {
return 'skipped';
}
fs.writeFileSync(filePath, content);
return force ? 'updated' : 'created';
};
const generateTests = async (force = false) => {
fs.mkdirSync(TEST_OUTPUT_DIR, { recursive: true });
const schemaData = await fetchGraphQLSchema();
const types = schemaData.data.__schema.types;
const queryTypeName = schemaData.data.__schema.queryType.name;
const queryType = types.find((t: any) => t.name === queryTypeName);
let createdCount = 0;
let updatedCount = 0;
let totalCount = 0;
if (!queryType?.fields) {
console.log('No query fields found.');
return;
}
for (const query of queryType.fields) {
const queryName = query.name;
if (hasRequiredArgs(query.args)) continue;
if (queryName.includes('Duplicates')) continue;
const queryReturnType = unwrapType(query.type);
if (
queryReturnType.kind === 'OBJECT' &&
queryReturnType.name.includes('Connection')
) {
totalCount++;
const connectionTypeInfo = types.find(
(f: any) => f.name === queryReturnType.name,
);
const edgeTypeInfo = connectionTypeInfo?.fields?.find(
(f: any) => f.name === 'edges',
);
if (edgeTypeInfo) {
const returnType = unwrapType(edgeTypeInfo.type);
const returnTypeInfo = types.find(
(t: any) => t.name === returnType.name,
);
const returnNodeTypeInfo = returnTypeInfo?.fields?.find(
(f: any) => f.name === 'node',
);
if (returnNodeTypeInfo) {
const nodeType = unwrapType(returnNodeTypeInfo.type);
const nodeTypeInfo = types.find((t: any) => t.name === nodeType.name);
if (!nodeTypeInfo?.fields) {
continue;
}
const content = generateTestContent(queryName, nodeTypeInfo?.fields);
const result = writeTestFile(queryName, content, force);
if (result === 'created') createdCount++;
if (result === 'updated') updatedCount++;
}
}
}
}
console.log(`Number of tests created: ${createdCount}/${totalCount}`);
if (force) {
console.log(`Number of tests updated: ${updatedCount}/${totalCount}`);
}
};
// Basic command-line argument parsing
const forceArg = process.argv.includes('--force');
// Call the function with the parsed argument
generateTests(forceArg);

View File

@@ -0,0 +1,89 @@
export const INTROSPECTION_QUERY = `
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
`;

View File

@@ -0,0 +1,60 @@
export interface IntrospectionResponse {
data: {
__schema: Schema;
};
}
export interface Schema {
queryType: { name: string };
mutationType: { name: string | null };
subscriptionType: { name: string | null };
types: GraphQLType[];
directives: Directive[];
}
export interface Directive {
name: string;
description: string | null;
locations: string[];
args: InputValue[];
}
export interface GraphQLType {
kind: string;
name: string;
description: string | null;
fields?: Field[];
inputFields?: InputValue[];
interfaces?: TypeRef[];
enumValues?: EnumValue[];
possibleTypes?: TypeRef[];
}
export interface Field {
name: string;
description: string | null;
args: InputValue[];
type: TypeRef;
isDeprecated: boolean;
deprecationReason: string | null;
}
export interface InputValue {
name: string;
description: string | null;
type: TypeRef;
defaultValue: string | null;
}
export interface TypeRef {
kind: string;
name: string | null;
ofType: TypeRef | null;
}
export interface EnumValue {
name: string;
description: string | null;
isDeprecated: boolean;
deprecationReason: string | null;
}

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env bash
# scripts/run-integration.sh
DIR="$(cd "$(dirname "$0")" && pwd)"
source $DIR/set-env-test.sh
npx nx database:reset
npx nx jest --config ./test/jest-e2e.json

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env bash
# scripts/set-env-test.sh
# Get script's directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Construct the absolute path of .env file in the project root directory
ENV_PATH="${SCRIPT_DIR}/../.env.test"
# Check if the file exists
if [ -f "${ENV_PATH}" ]; then
echo "🔵 - Loading environment variables from "${ENV_PATH}"..."
# Export env vars
while IFS= read -r line || [ -n "$line" ]; do
if echo "$line" | grep -F = &>/dev/null
then
varname=$(echo "$line" | cut -d '=' -f 1)
varvalue=$(echo "$line" | cut -d '=' -f 2- | cut -d '#' -f 1)
export "$varname"="$varvalue"
fi
done < <(grep -v '^#' "${ENV_PATH}")
else
echo "Error: ${ENV_PATH} does not exist."
exit 1
fi

View File

@@ -26,23 +26,15 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
import { ModulesModule } from 'src/modules/modules.module';
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
@Module({
imports: [
// Nest.js devtools, use devtools.nestjs.com to debug
// DevtoolsModule.registerAsync({
// useFactory: (environmentService: EnvironmentService) => ({
// http: environmentService.get('DEBUG_MODE'),
// port: environmentService.get('DEBUG_PORT'),
// }),
// inject: [EnvironmentService],
// }),
ConfigModule.forRoot({
isGlobal: true,
envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
}),
GraphQLModule.forRootAsync<YogaDriverConfig>({
driver: YogaDriver,
imports: [CoreEngineModule, GraphQLConfigModule],
imports: [GraphQLConfigModule],
useClass: GraphQLConfigService,
}),
TwentyORMModule,

View File

@@ -2,18 +2,24 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from 'dotenv';
import { DataSource, DataSourceOptions } from 'typeorm';
config();
config({ path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env' });
const isJest = process.argv.some((arg) => arg.includes('jest'));
export const typeORMCoreModuleOptions: TypeOrmModuleOptions = {
url: process.env.PG_DATABASE_URL,
type: 'postgres',
logging: ['error'],
schema: 'core',
entities: ['dist/src/engine/core-modules/**/*.entity{.ts,.js}'],
entities: [
`${isJest ? '' : 'dist/'}src/engine/core-modules/**/*.entity{.ts,.js}`,
],
synchronize: false,
migrationsRun: false,
migrationsTableName: '_typeorm_migrations',
migrations: ['dist/src/database/typeorm/core/migrations/*{.ts,.js}'],
migrations: [
`${isJest ? '' : 'dist/'}src/database/typeorm/core/migrations/*{.ts,.js}`,
],
ssl:
process.env.PG_SSL_ALLOW_SELF_SIGNED === 'true'
? {

View File

@@ -2,18 +2,24 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from 'dotenv';
import { DataSource, DataSourceOptions } from 'typeorm';
config();
config({ path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env' });
const isJest = process.argv.some((arg) => arg.includes('jest'));
export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = {
url: process.env.PG_DATABASE_URL,
type: 'postgres',
logging: ['error'],
schema: 'metadata',
entities: ['dist/src/engine/metadata-modules/**/*.entity{.ts,.js}'],
entities: [
`${isJest ? '' : 'dist/'}src/engine/metadata-modules/**/*.entity{.ts,.js}`,
],
synchronize: false,
migrationsRun: false,
migrationsTableName: '_typeorm_migrations',
migrations: ['dist/src/database/typeorm/metadata/migrations/*{.ts,.js}'],
migrations: [
`${isJest ? '' : 'dist/'}src/database/typeorm/metadata/migrations/*{.ts,.js}`,
],
ssl:
process.env.PG_SSL_ALLOW_SELF_SIGNED === 'true'
? {
@@ -24,6 +30,7 @@ export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = {
query_timeout: 10000,
},
};
export const connectionSource = new DataSource(
typeORMMetadataModuleOptions as DataSourceOptions,
);

View File

@@ -1,6 +1,6 @@
import { config } from 'dotenv';
import { DataSource, DataSourceOptions } from 'typeorm';
config();
config({ path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env' });
const typeORMRawModuleOptions: DataSourceOptions = {
url: process.env.PG_DATABASE_URL,

View File

@@ -4,15 +4,15 @@ import { DataSource } from 'typeorm';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Injectable()
export class TypeORMService implements OnModuleInit, OnModuleDestroy {

View File

@@ -5,6 +5,6 @@ import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
@Module({
imports: [CoreEngineModule],
providers: [],
exports: [],
exports: [CoreEngineModule],
})
export class GraphQLConfigModule {}

View File

@@ -1,9 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ConfigurableModuleClass } from 'src/engine/core-modules/environment/environment.module-definition';
import { validate } from 'src/engine/core-modules/environment/environment-variables';
import { ConfigurableModuleClass } from 'src/engine/core-modules/environment/environment.module-definition';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Global()
@Module({
@@ -12,6 +12,7 @@ import { validate } from 'src/engine/core-modules/environment/environment-variab
isGlobal: true,
expandVariables: true,
validate,
envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
}),
],
providers: [EnvironmentService],

View File

@@ -1,4 +1,5 @@
export enum NodeEnvironment {
test = 'test',
development = 'development',
production = 'production',
}

View File

@@ -8,7 +8,6 @@ import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.typ
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
class GraphqlTokenValidationProxy {
private tokenService: TokenService;

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { config } from 'dotenv';
config();
config({ path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env' });
export function generateFrontConfig(): void {
const configObject = {

View File

@@ -0,0 +1,67 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('activitiesResolver (integration)', () => {
it('should find many activities', () => {
const queryData = {
query: `
query activities {
activities {
edges {
node {
title
body
type
reminderAt
dueAt
completedAt
id
createdAt
updatedAt
deletedAt
authorId
assigneeId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.activities;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const activities = edges[0].node;
expect(activities).toHaveProperty('title');
expect(activities).toHaveProperty('body');
expect(activities).toHaveProperty('type');
expect(activities).toHaveProperty('reminderAt');
expect(activities).toHaveProperty('dueAt');
expect(activities).toHaveProperty('completedAt');
expect(activities).toHaveProperty('id');
expect(activities).toHaveProperty('createdAt');
expect(activities).toHaveProperty('updatedAt');
expect(activities).toHaveProperty('deletedAt');
expect(activities).toHaveProperty('authorId');
expect(activities).toHaveProperty('assigneeId');
}
});
});
});

View File

@@ -0,0 +1,59 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('activityTargetsResolver (integration)', () => {
it('should find many activityTargets', () => {
const queryData = {
query: `
query activityTargets {
activityTargets {
edges {
node {
id
createdAt
updatedAt
deletedAt
activityId
personId
companyId
opportunityId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.activityTargets;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const activityTargets = edges[0].node;
expect(activityTargets).toHaveProperty('id');
expect(activityTargets).toHaveProperty('createdAt');
expect(activityTargets).toHaveProperty('updatedAt');
expect(activityTargets).toHaveProperty('deletedAt');
expect(activityTargets).toHaveProperty('activityId');
expect(activityTargets).toHaveProperty('personId');
expect(activityTargets).toHaveProperty('companyId');
expect(activityTargets).toHaveProperty('opportunityId');
}
});
});
});

View File

@@ -0,0 +1,57 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('apiKeysResolver (integration)', () => {
it('should find many apiKeys', () => {
const queryData = {
query: `
query apiKeys {
apiKeys {
edges {
node {
name
expiresAt
revokedAt
id
createdAt
updatedAt
deletedAt
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.apiKeys;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const apiKeys = edges[0].node;
expect(apiKeys).toHaveProperty('name');
expect(apiKeys).toHaveProperty('expiresAt');
expect(apiKeys).toHaveProperty('revokedAt');
expect(apiKeys).toHaveProperty('id');
expect(apiKeys).toHaveProperty('createdAt');
expect(apiKeys).toHaveProperty('updatedAt');
expect(apiKeys).toHaveProperty('deletedAt');
}
});
});
});

View File

@@ -1,31 +0,0 @@
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { createApp } from './utils/create-app';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
[app] = await createApp();
});
afterEach(async () => {
await app.close();
});
it('/healthz (GET)', () => {
return request(app.getHttpServer())
.get('/healthz')
.expect(200)
.expect((response) => {
expect(response.body).toEqual({
status: 'ok',
info: { database: { status: 'up' } },
error: {},
details: { database: { status: 'up' } },
});
});
});
});

View File

@@ -0,0 +1,71 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('attachmentsResolver (integration)', () => {
it('should find many attachments', () => {
const queryData = {
query: `
query attachments {
attachments {
edges {
node {
name
fullPath
type
id
createdAt
updatedAt
deletedAt
authorId
activityId
taskId
noteId
personId
companyId
opportunityId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.attachments;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const attachments = edges[0].node;
expect(attachments).toHaveProperty('name');
expect(attachments).toHaveProperty('fullPath');
expect(attachments).toHaveProperty('type');
expect(attachments).toHaveProperty('id');
expect(attachments).toHaveProperty('createdAt');
expect(attachments).toHaveProperty('updatedAt');
expect(attachments).toHaveProperty('deletedAt');
expect(attachments).toHaveProperty('authorId');
expect(attachments).toHaveProperty('activityId');
expect(attachments).toHaveProperty('taskId');
expect(attachments).toHaveProperty('noteId');
expect(attachments).toHaveProperty('personId');
expect(attachments).toHaveProperty('companyId');
expect(attachments).toHaveProperty('opportunityId');
}
});
});
});

View File

@@ -0,0 +1,65 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('auditLogsResolver (integration)', () => {
it('should find many auditLogs', () => {
const queryData = {
query: `
query auditLogs {
auditLogs {
edges {
node {
name
properties
context
objectName
objectMetadataId
recordId
id
createdAt
updatedAt
deletedAt
workspaceMemberId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.auditLogs;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const auditLogs = edges[0].node;
expect(auditLogs).toHaveProperty('name');
expect(auditLogs).toHaveProperty('properties');
expect(auditLogs).toHaveProperty('context');
expect(auditLogs).toHaveProperty('objectName');
expect(auditLogs).toHaveProperty('objectMetadataId');
expect(auditLogs).toHaveProperty('recordId');
expect(auditLogs).toHaveProperty('id');
expect(auditLogs).toHaveProperty('createdAt');
expect(auditLogs).toHaveProperty('updatedAt');
expect(auditLogs).toHaveProperty('deletedAt');
expect(auditLogs).toHaveProperty('workspaceMemberId');
}
});
});
});

View File

@@ -0,0 +1,80 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
const auth = {
email: 'tim@apple.dev',
password: 'Applecar2025',
};
describe('AuthResolve (integration)', () => {
let loginToken: string;
it('should challenge with email and password', () => {
const queryData = {
query: `
mutation Challenge {
challenge(email: "${auth.email}", password: "${auth.password}") {
loginToken {
token
expiresAt
}
}
}
`,
};
return client
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.challenge;
expect(data).toBeDefined();
expect(data.loginToken).toBeDefined();
loginToken = data.loginToken.token;
});
});
it('should verify with login token', () => {
const queryData = {
query: `
mutation Verify {
verify(loginToken: "${loginToken}") {
tokens {
accessToken {
token
}
}
}
}
`,
};
return client
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.verify;
expect(data).toBeDefined();
expect(data.tokens).toBeDefined();
const accessToken = data.tokens.accessToken;
expect(accessToken).toBeDefined();
expect(accessToken.token).toBeDefined();
});
});
});

View File

@@ -0,0 +1,55 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('blocklistsResolver (integration)', () => {
it('should find many blocklists', () => {
const queryData = {
query: `
query blocklists {
blocklists {
edges {
node {
handle
id
createdAt
updatedAt
deletedAt
workspaceMemberId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.blocklists;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const blocklists = edges[0].node;
expect(blocklists).toHaveProperty('handle');
expect(blocklists).toHaveProperty('id');
expect(blocklists).toHaveProperty('createdAt');
expect(blocklists).toHaveProperty('updatedAt');
expect(blocklists).toHaveProperty('deletedAt');
expect(blocklists).toHaveProperty('workspaceMemberId');
}
});
});
});

View File

@@ -0,0 +1,63 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('calendarChannelEventAssociationsResolver (integration)', () => {
it('should find many calendarChannelEventAssociations', () => {
const queryData = {
query: `
query calendarChannelEventAssociations {
calendarChannelEventAssociations {
edges {
node {
eventExternalId
id
createdAt
updatedAt
deletedAt
calendarChannelId
calendarEventId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.calendarChannelEventAssociations;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const calendarChannelEventAssociations = edges[0].node;
expect(calendarChannelEventAssociations).toHaveProperty(
'eventExternalId',
);
expect(calendarChannelEventAssociations).toHaveProperty('id');
expect(calendarChannelEventAssociations).toHaveProperty('createdAt');
expect(calendarChannelEventAssociations).toHaveProperty('updatedAt');
expect(calendarChannelEventAssociations).toHaveProperty('deletedAt');
expect(calendarChannelEventAssociations).toHaveProperty(
'calendarChannelId',
);
expect(calendarChannelEventAssociations).toHaveProperty(
'calendarEventId',
);
}
});
});
});

View File

@@ -0,0 +1,75 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('calendarChannelsResolver (integration)', () => {
it('should find many calendarChannels', () => {
const queryData = {
query: `
query calendarChannels {
calendarChannels {
edges {
node {
handle
syncStatus
syncStage
visibility
isContactAutoCreationEnabled
contactAutoCreationPolicy
isSyncEnabled
syncCursor
syncStageStartedAt
throttleFailureCount
id
createdAt
updatedAt
deletedAt
connectedAccountId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.calendarChannels;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const calendarChannels = edges[0].node;
expect(calendarChannels).toHaveProperty('handle');
expect(calendarChannels).toHaveProperty('syncStatus');
expect(calendarChannels).toHaveProperty('syncStage');
expect(calendarChannels).toHaveProperty('visibility');
expect(calendarChannels).toHaveProperty(
'isContactAutoCreationEnabled',
);
expect(calendarChannels).toHaveProperty('contactAutoCreationPolicy');
expect(calendarChannels).toHaveProperty('isSyncEnabled');
expect(calendarChannels).toHaveProperty('syncCursor');
expect(calendarChannels).toHaveProperty('syncStageStartedAt');
expect(calendarChannels).toHaveProperty('throttleFailureCount');
expect(calendarChannels).toHaveProperty('id');
expect(calendarChannels).toHaveProperty('createdAt');
expect(calendarChannels).toHaveProperty('updatedAt');
expect(calendarChannels).toHaveProperty('deletedAt');
expect(calendarChannels).toHaveProperty('connectedAccountId');
}
});
});
});

View File

@@ -0,0 +1,65 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('calendarEventParticipantsResolver (integration)', () => {
it('should find many calendarEventParticipants', () => {
const queryData = {
query: `
query calendarEventParticipants {
calendarEventParticipants {
edges {
node {
handle
displayName
isOrganizer
responseStatus
id
createdAt
updatedAt
deletedAt
calendarEventId
personId
workspaceMemberId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.calendarEventParticipants;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const calendarEventParticipants = edges[0].node;
expect(calendarEventParticipants).toHaveProperty('handle');
expect(calendarEventParticipants).toHaveProperty('displayName');
expect(calendarEventParticipants).toHaveProperty('isOrganizer');
expect(calendarEventParticipants).toHaveProperty('responseStatus');
expect(calendarEventParticipants).toHaveProperty('id');
expect(calendarEventParticipants).toHaveProperty('createdAt');
expect(calendarEventParticipants).toHaveProperty('updatedAt');
expect(calendarEventParticipants).toHaveProperty('deletedAt');
expect(calendarEventParticipants).toHaveProperty('calendarEventId');
expect(calendarEventParticipants).toHaveProperty('personId');
expect(calendarEventParticipants).toHaveProperty('workspaceMemberId');
}
});
});
});

View File

@@ -0,0 +1,57 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('commentsResolver (integration)', () => {
it('should find many comments', () => {
const queryData = {
query: `
query comments {
comments {
edges {
node {
body
id
createdAt
updatedAt
deletedAt
authorId
activityId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.comments;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const comments = edges[0].node;
expect(comments).toHaveProperty('body');
expect(comments).toHaveProperty('id');
expect(comments).toHaveProperty('createdAt');
expect(comments).toHaveProperty('updatedAt');
expect(comments).toHaveProperty('deletedAt');
expect(comments).toHaveProperty('authorId');
expect(comments).toHaveProperty('activityId');
}
});
});
});

View File

@@ -0,0 +1,67 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('companiesResolver (integration)', () => {
it('should find many companies', () => {
const queryData = {
query: `
query companies {
companies {
edges {
node {
name
employees
idealCustomerProfile
position
id
createdAt
updatedAt
deletedAt
accountOwnerId
tagline
workPolicy
visaSponsorship
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.companies;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const companies = edges[0].node;
expect(companies).toHaveProperty('name');
expect(companies).toHaveProperty('employees');
expect(companies).toHaveProperty('idealCustomerProfile');
expect(companies).toHaveProperty('position');
expect(companies).toHaveProperty('id');
expect(companies).toHaveProperty('createdAt');
expect(companies).toHaveProperty('updatedAt');
expect(companies).toHaveProperty('deletedAt');
expect(companies).toHaveProperty('accountOwnerId');
expect(companies).toHaveProperty('tagline');
expect(companies).toHaveProperty('workPolicy');
expect(companies).toHaveProperty('visaSponsorship');
}
});
});
});

View File

@@ -1,310 +0,0 @@
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { createApp } from './utils/create-app';
describe('CompanyResolver (e2e)', () => {
let app: INestApplication;
let companyId: string | undefined;
const authGuardMock = { canActivate: (): any => true };
beforeEach(async () => {
[app] = await createApp({
moduleBuilderHook: (moduleBuilder) =>
moduleBuilder.overrideGuard(WorkspaceAuthGuard).useValue(authGuardMock),
});
});
afterEach(async () => {
await app.close();
});
it('should create a company', () => {
const queryData = {
query: `
mutation CreateOneCompany($data: CompanyCreateInput!) {
createOneCompany(data: $data) {
id
name
domainName
address {
addressCity
}
}
}
`,
variables: {
data: {
name: 'New Company',
domainName: 'new-company.com',
address: { addressCity: 'Paris' },
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.createOneCompany;
companyId = data.id;
expect(data).toBeDefined();
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('name', 'New Company');
expect(data).toHaveProperty('domainName', 'new-company.com');
expect(data).toHaveProperty('address', { addressCity: 'Paris' });
});
});
it('should find many companies', () => {
const queryData = {
query: `
query FindManyCompany {
findManyCompany {
id
name
domainName
address {
addressCity
}
}
}
`,
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.findManyCompany;
expect(data).toBeDefined();
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBeGreaterThan(0);
const company = data.find((c) => c.id === companyId);
expect(company).toBeDefined();
expect(company).toHaveProperty('id');
expect(company).toHaveProperty('name', 'New Company');
expect(company).toHaveProperty('domainName', 'new-company.com');
expect(company).toHaveProperty('address', { addressCity: 'Paris' });
// Check if we have access to ressources outside of our workspace
const instagramCompany = data.find((c) => c.name === 'Instagram');
expect(instagramCompany).toBeUndefined();
});
});
it('should find unique company', () => {
const queryData = {
query: `
query FindUniqueCompany($where: CompanyWhereUniqueInput!) {
findUniqueCompany(where: $where) {
id
name
domainName
address {
addressCity
}
}
}
`,
variables: {
where: {
id: companyId,
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.findUniqueCompany;
expect(data).toBeDefined();
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('name', 'New Company');
expect(data).toHaveProperty('domainName', 'new-company.com');
expect(data).toHaveProperty('address', { addressCity: 'Paris' });
});
});
it('should not find unique company (forbidden because outside workspace)', () => {
const queryData = {
query: `
query FindUniqueCompany($where: CompanyWhereUniqueInput!) {
findUniqueCompany(where: $where) {
id
name
domainName
address {
addressCity
}
}
}
`,
variables: {
where: {
id: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e',
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const errors = res.body.errors;
const error = errors?.[0];
expect(error).toBeDefined();
expect(error.message).toBe('Forbidden resource');
});
});
it('should update a company', () => {
const queryData = {
query: `
mutation UpdateOneCompany($where: CompanyWhereUniqueInput!, $data: CompanyUpdateInput!) {
updateOneCompany(data: $data, where: $where) {
id
name
domainName
address {
addressCity
}
}
}
`,
variables: {
where: {
id: companyId,
},
data: {
name: 'Updated Company',
domainName: 'updated-company.com',
address: { addressCity: 'Updated City' },
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.updateOneCompany;
expect(data).toBeDefined();
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('name', 'Updated Company');
expect(data).toHaveProperty('domainName', 'updated-company.com');
expect(data).toHaveProperty('address', { addressCity: 'Updated City' });
});
});
it('should not update a company (forbidden because outside workspace)', () => {
const queryData = {
query: `
mutation UpdateOneCompany($where: CompanyWhereUniqueInput!, $data: CompanyUpdateInput!) {
updateOneCompany(data: $data, where: $where) {
id
name
domainName
address {
addressCity
}
}
}
`,
variables: {
where: {
id: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e',
},
data: {
name: 'Updated Instagram',
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const errors = res.body.errors;
const error = errors?.[0];
expect(error).toBeDefined();
expect(error.message).toBe('Forbidden resource');
});
});
it('should delete a company', () => {
const queryData = {
query: `
mutation DeleteManyCompany($ids: [String!]) {
deleteManyCompany(where: {id: {in: $ids}}) {
count
}
}
`,
variables: {
ids: [companyId],
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.deleteManyCompany;
companyId = undefined;
expect(data).toBeDefined();
expect(data).toHaveProperty('count', 1);
});
});
it('should not delete a company (forbidden because outside workspace)', () => {
const queryData = {
query: `
mutation DeleteManyCompany($ids: [String!]) {
deleteManyCompany(where: {id: {in: $ids}}) {
count
}
}
`,
variables: {
ids: ['twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e'],
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const errors = res.body.errors;
const error = errors?.[0];
expect(error).toBeDefined();
expect(error.message).toBe('Forbidden resource');
});
});
});

View File

@@ -0,0 +1,48 @@
import request from 'supertest';
const graphqlClient = request(`http://localhost:${APP_PORT}`);
describe('CompanyResolver (integration)', () => {
it('should find many companies', () => {
const queryData = {
query: `
query Companies {
companies {
edges {
node {
id
name
}
}
}
}
`,
};
return graphqlClient
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.companies;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const company = edges[0].node;
expect(company).toBeDefined();
expect(company).toHaveProperty('id');
expect(company).toHaveProperty('name');
}
});
});
});

View File

@@ -0,0 +1,67 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('connectedAccountsResolver (integration)', () => {
it('should find many connectedAccounts', () => {
const queryData = {
query: `
query connectedAccounts {
connectedAccounts {
edges {
node {
handle
provider
accessToken
refreshToken
lastSyncHistoryId
authFailedAt
handleAliases
id
createdAt
updatedAt
deletedAt
accountOwnerId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.connectedAccounts;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const connectedAccounts = edges[0].node;
expect(connectedAccounts).toHaveProperty('handle');
expect(connectedAccounts).toHaveProperty('provider');
expect(connectedAccounts).toHaveProperty('accessToken');
expect(connectedAccounts).toHaveProperty('refreshToken');
expect(connectedAccounts).toHaveProperty('lastSyncHistoryId');
expect(connectedAccounts).toHaveProperty('authFailedAt');
expect(connectedAccounts).toHaveProperty('handleAliases');
expect(connectedAccounts).toHaveProperty('id');
expect(connectedAccounts).toHaveProperty('createdAt');
expect(connectedAccounts).toHaveProperty('updatedAt');
expect(connectedAccounts).toHaveProperty('deletedAt');
expect(connectedAccounts).toHaveProperty('accountOwnerId');
}
});
});
});

View File

@@ -0,0 +1,67 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('favoritesResolver (integration)', () => {
it('should find many favorites', () => {
const queryData = {
query: `
query favorites {
favorites {
edges {
node {
position
id
createdAt
updatedAt
deletedAt
workspaceMemberId
personId
companyId
opportunityId
taskId
noteId
viewId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.favorites;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const favorites = edges[0].node;
expect(favorites).toHaveProperty('position');
expect(favorites).toHaveProperty('id');
expect(favorites).toHaveProperty('createdAt');
expect(favorites).toHaveProperty('updatedAt');
expect(favorites).toHaveProperty('deletedAt');
expect(favorites).toHaveProperty('workspaceMemberId');
expect(favorites).toHaveProperty('personId');
expect(favorites).toHaveProperty('companyId');
expect(favorites).toHaveProperty('opportunityId');
expect(favorites).toHaveProperty('taskId');
expect(favorites).toHaveProperty('noteId');
expect(favorites).toHaveProperty('viewId');
}
});
});
});

View File

@@ -1,14 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"setupFilesAfterEnv": ["<rootDir>/utils/setup-tests.ts"],
"moduleNameMapper": {
"^src/(.*)": "<rootDir>/../src/$1",
"^test/(.*)": "<rootDir>/$1"
},
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,67 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('messageChannelMessageAssociationsResolver (integration)', () => {
it('should find many messageChannelMessageAssociations', () => {
const queryData = {
query: `
query messageChannelMessageAssociations {
messageChannelMessageAssociations {
edges {
node {
createdAt
messageExternalId
messageThreadExternalId
direction
id
updatedAt
deletedAt
messageChannelId
messageId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.messageChannelMessageAssociations;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const messageChannelMessageAssociations = edges[0].node;
expect(messageChannelMessageAssociations).toHaveProperty('createdAt');
expect(messageChannelMessageAssociations).toHaveProperty(
'messageExternalId',
);
expect(messageChannelMessageAssociations).toHaveProperty(
'messageThreadExternalId',
);
expect(messageChannelMessageAssociations).toHaveProperty('direction');
expect(messageChannelMessageAssociations).toHaveProperty('id');
expect(messageChannelMessageAssociations).toHaveProperty('updatedAt');
expect(messageChannelMessageAssociations).toHaveProperty('deletedAt');
expect(messageChannelMessageAssociations).toHaveProperty(
'messageChannelId',
);
expect(messageChannelMessageAssociations).toHaveProperty('messageId');
}
});
});
});

View File

@@ -0,0 +1,85 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('messageChannelsResolver (integration)', () => {
it('should find many messageChannels', () => {
const queryData = {
query: `
query messageChannels {
messageChannels {
edges {
node {
visibility
handle
type
isContactAutoCreationEnabled
contactAutoCreationPolicy
excludeNonProfessionalEmails
excludeGroupEmails
isSyncEnabled
syncCursor
syncedAt
syncStatus
syncStage
syncStageStartedAt
throttleFailureCount
id
createdAt
updatedAt
deletedAt
connectedAccountId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.messageChannels;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const messageChannels = edges[0].node;
expect(messageChannels).toHaveProperty('visibility');
expect(messageChannels).toHaveProperty('handle');
expect(messageChannels).toHaveProperty('type');
expect(messageChannels).toHaveProperty(
'isContactAutoCreationEnabled',
);
expect(messageChannels).toHaveProperty('contactAutoCreationPolicy');
expect(messageChannels).toHaveProperty(
'excludeNonProfessionalEmails',
);
expect(messageChannels).toHaveProperty('excludeGroupEmails');
expect(messageChannels).toHaveProperty('isSyncEnabled');
expect(messageChannels).toHaveProperty('syncCursor');
expect(messageChannels).toHaveProperty('syncedAt');
expect(messageChannels).toHaveProperty('syncStatus');
expect(messageChannels).toHaveProperty('syncStage');
expect(messageChannels).toHaveProperty('syncStageStartedAt');
expect(messageChannels).toHaveProperty('throttleFailureCount');
expect(messageChannels).toHaveProperty('id');
expect(messageChannels).toHaveProperty('createdAt');
expect(messageChannels).toHaveProperty('updatedAt');
expect(messageChannels).toHaveProperty('deletedAt');
expect(messageChannels).toHaveProperty('connectedAccountId');
}
});
});
});

View File

@@ -0,0 +1,63 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('messageParticipantsResolver (integration)', () => {
it('should find many messageParticipants', () => {
const queryData = {
query: `
query messageParticipants {
messageParticipants {
edges {
node {
role
handle
displayName
id
createdAt
updatedAt
deletedAt
messageId
personId
workspaceMemberId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.messageParticipants;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const messageParticipants = edges[0].node;
expect(messageParticipants).toHaveProperty('role');
expect(messageParticipants).toHaveProperty('handle');
expect(messageParticipants).toHaveProperty('displayName');
expect(messageParticipants).toHaveProperty('id');
expect(messageParticipants).toHaveProperty('createdAt');
expect(messageParticipants).toHaveProperty('updatedAt');
expect(messageParticipants).toHaveProperty('deletedAt');
expect(messageParticipants).toHaveProperty('messageId');
expect(messageParticipants).toHaveProperty('personId');
expect(messageParticipants).toHaveProperty('workspaceMemberId');
}
});
});
});

View File

@@ -0,0 +1,51 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('messageThreadsResolver (integration)', () => {
it('should find many messageThreads', () => {
const queryData = {
query: `
query messageThreads {
messageThreads {
edges {
node {
id
createdAt
updatedAt
deletedAt
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.messageThreads;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const messageThreads = edges[0].node;
expect(messageThreads).toHaveProperty('id');
expect(messageThreads).toHaveProperty('createdAt');
expect(messageThreads).toHaveProperty('updatedAt');
expect(messageThreads).toHaveProperty('deletedAt');
}
});
});
});

View File

@@ -1,9 +0,0 @@
{
"id": "20202020-a838-4fa9-b59b-96409b9a1c30",
"firstName": "Tim",
"lastName": "Apple",
"email": "tim@apple.dev",
"locale": "en",
"passwordHash": "$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6",
"avatarUrl": null
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,59 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('noteTargetsResolver (integration)', () => {
it('should find many noteTargets', () => {
const queryData = {
query: `
query noteTargets {
noteTargets {
edges {
node {
id
createdAt
updatedAt
deletedAt
noteId
personId
companyId
opportunityId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.noteTargets;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const noteTargets = edges[0].node;
expect(noteTargets).toHaveProperty('id');
expect(noteTargets).toHaveProperty('createdAt');
expect(noteTargets).toHaveProperty('updatedAt');
expect(noteTargets).toHaveProperty('deletedAt');
expect(noteTargets).toHaveProperty('noteId');
expect(noteTargets).toHaveProperty('personId');
expect(noteTargets).toHaveProperty('companyId');
expect(noteTargets).toHaveProperty('opportunityId');
}
});
});
});

View File

@@ -0,0 +1,57 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('notesResolver (integration)', () => {
it('should find many notes', () => {
const queryData = {
query: `
query notes {
notes {
edges {
node {
position
title
body
id
createdAt
updatedAt
deletedAt
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.notes;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const notes = edges[0].node;
expect(notes).toHaveProperty('position');
expect(notes).toHaveProperty('title');
expect(notes).toHaveProperty('body');
expect(notes).toHaveProperty('id');
expect(notes).toHaveProperty('createdAt');
expect(notes).toHaveProperty('updatedAt');
expect(notes).toHaveProperty('deletedAt');
}
});
});
});

View File

@@ -0,0 +1,75 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('objectsResolver (integration)', () => {
it('should find many objects', () => {
const queryData = {
query: `
query objects {
objects {
edges {
node {
id
dataSourceId
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isRemote
isActive
isSystem
createdAt
updatedAt
labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.objects;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const objects = edges[0].node;
expect(objects).toHaveProperty('id');
expect(objects).toHaveProperty('dataSourceId');
expect(objects).toHaveProperty('nameSingular');
expect(objects).toHaveProperty('namePlural');
expect(objects).toHaveProperty('labelSingular');
expect(objects).toHaveProperty('labelPlural');
expect(objects).toHaveProperty('description');
expect(objects).toHaveProperty('icon');
expect(objects).toHaveProperty('isCustom');
expect(objects).toHaveProperty('isRemote');
expect(objects).toHaveProperty('isActive');
expect(objects).toHaveProperty('isSystem');
expect(objects).toHaveProperty('createdAt');
expect(objects).toHaveProperty('updatedAt');
expect(objects).toHaveProperty('labelIdentifierFieldMetadataId');
expect(objects).toHaveProperty('imageIdentifierFieldMetadataId');
}
});
});
});

View File

@@ -0,0 +1,63 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('opportunitiesResolver (integration)', () => {
it('should find many opportunities', () => {
const queryData = {
query: `
query opportunities {
opportunities {
edges {
node {
name
closeDate
stage
position
id
createdAt
updatedAt
deletedAt
pointOfContactId
companyId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.opportunities;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const opportunities = edges[0].node;
expect(opportunities).toHaveProperty('name');
expect(opportunities).toHaveProperty('closeDate');
expect(opportunities).toHaveProperty('stage');
expect(opportunities).toHaveProperty('position');
expect(opportunities).toHaveProperty('id');
expect(opportunities).toHaveProperty('createdAt');
expect(opportunities).toHaveProperty('updatedAt');
expect(opportunities).toHaveProperty('deletedAt');
expect(opportunities).toHaveProperty('pointOfContactId');
expect(opportunities).toHaveProperty('companyId');
}
});
});
});

View File

@@ -0,0 +1,71 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('peopleResolver (integration)', () => {
it('should find many people', () => {
const queryData = {
query: `
query people {
people {
edges {
node {
jobTitle
phone
city
avatarUrl
position
id
createdAt
updatedAt
deletedAt
companyId
intro
whatsapp
workPrefereance
performanceRating
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.people;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const people = edges[0].node;
expect(people).toHaveProperty('jobTitle');
expect(people).toHaveProperty('phone');
expect(people).toHaveProperty('city');
expect(people).toHaveProperty('avatarUrl');
expect(people).toHaveProperty('position');
expect(people).toHaveProperty('id');
expect(people).toHaveProperty('createdAt');
expect(people).toHaveProperty('updatedAt');
expect(people).toHaveProperty('deletedAt');
expect(people).toHaveProperty('companyId');
expect(people).toHaveProperty('intro');
expect(people).toHaveProperty('whatsapp');
expect(people).toHaveProperty('workPrefereance');
expect(people).toHaveProperty('performanceRating');
}
});
});
});

View File

@@ -0,0 +1,61 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('serverlessFunctionsResolver (integration)', () => {
it('should find many serverlessFunctions', () => {
const queryData = {
query: `
query serverlessFunctions {
serverlessFunctions {
edges {
node {
id
name
description
sourceCodeHash
runtime
latestVersion
syncStatus
createdAt
updatedAt
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.serverlessFunctions;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const serverlessFunctions = edges[0].node;
expect(serverlessFunctions).toHaveProperty('id');
expect(serverlessFunctions).toHaveProperty('name');
expect(serverlessFunctions).toHaveProperty('description');
expect(serverlessFunctions).toHaveProperty('sourceCodeHash');
expect(serverlessFunctions).toHaveProperty('runtime');
expect(serverlessFunctions).toHaveProperty('latestVersion');
expect(serverlessFunctions).toHaveProperty('syncStatus');
expect(serverlessFunctions).toHaveProperty('createdAt');
expect(serverlessFunctions).toHaveProperty('updatedAt');
}
});
});
});

View File

@@ -0,0 +1,59 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('taskTargetsResolver (integration)', () => {
it('should find many taskTargets', () => {
const queryData = {
query: `
query taskTargets {
taskTargets {
edges {
node {
id
createdAt
updatedAt
deletedAt
taskId
personId
companyId
opportunityId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.taskTargets;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const taskTargets = edges[0].node;
expect(taskTargets).toHaveProperty('id');
expect(taskTargets).toHaveProperty('createdAt');
expect(taskTargets).toHaveProperty('updatedAt');
expect(taskTargets).toHaveProperty('deletedAt');
expect(taskTargets).toHaveProperty('taskId');
expect(taskTargets).toHaveProperty('personId');
expect(taskTargets).toHaveProperty('companyId');
expect(taskTargets).toHaveProperty('opportunityId');
}
});
});
});

View File

@@ -0,0 +1,63 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('tasksResolver (integration)', () => {
it('should find many tasks', () => {
const queryData = {
query: `
query tasks {
tasks {
edges {
node {
position
title
body
dueAt
status
id
createdAt
updatedAt
deletedAt
assigneeId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.tasks;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const tasks = edges[0].node;
expect(tasks).toHaveProperty('position');
expect(tasks).toHaveProperty('title');
expect(tasks).toHaveProperty('body');
expect(tasks).toHaveProperty('dueAt');
expect(tasks).toHaveProperty('status');
expect(tasks).toHaveProperty('id');
expect(tasks).toHaveProperty('createdAt');
expect(tasks).toHaveProperty('updatedAt');
expect(tasks).toHaveProperty('deletedAt');
expect(tasks).toHaveProperty('assigneeId');
}
});
});
});

View File

@@ -0,0 +1,75 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('timelineActivitiesResolver (integration)', () => {
it('should find many timelineActivities', () => {
const queryData = {
query: `
query timelineActivities {
timelineActivities {
edges {
node {
happensAt
name
properties
linkedRecordCachedName
linkedRecordId
linkedObjectMetadataId
id
createdAt
updatedAt
deletedAt
workspaceMemberId
personId
companyId
opportunityId
noteId
taskId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.timelineActivities;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const timelineActivities = edges[0].node;
expect(timelineActivities).toHaveProperty('happensAt');
expect(timelineActivities).toHaveProperty('name');
expect(timelineActivities).toHaveProperty('properties');
expect(timelineActivities).toHaveProperty('linkedRecordCachedName');
expect(timelineActivities).toHaveProperty('linkedRecordId');
expect(timelineActivities).toHaveProperty('linkedObjectMetadataId');
expect(timelineActivities).toHaveProperty('id');
expect(timelineActivities).toHaveProperty('createdAt');
expect(timelineActivities).toHaveProperty('updatedAt');
expect(timelineActivities).toHaveProperty('deletedAt');
expect(timelineActivities).toHaveProperty('workspaceMemberId');
expect(timelineActivities).toHaveProperty('personId');
expect(timelineActivities).toHaveProperty('companyId');
expect(timelineActivities).toHaveProperty('opportunityId');
expect(timelineActivities).toHaveProperty('noteId');
expect(timelineActivities).toHaveProperty('taskId');
}
});
});
});

View File

@@ -1,10 +1,6 @@
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing';
import mockUser from 'test/mock-data/user.json';
import mockWorkspace from 'test/mock-data/workspace.json';
import { RequestHandler } from 'express';
import { AppModule } from 'src/app.module';
interface TestingModuleCreatePreHook {
@@ -19,14 +15,14 @@ export type TestingAppCreatePreHook = (
) => Promise<void>;
/**
* Sets basic e2e testing module of app
* Sets basic integration testing module of app
*/
export const createApp = async (
config: {
moduleBuilderHook?: TestingModuleCreatePreHook;
appInitHook?: TestingAppCreatePreHook;
} = {},
): Promise<[NestExpressApplication, TestingModule]> => {
): Promise<NestExpressApplication> => {
let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({
imports: [AppModule],
});
@@ -36,21 +32,14 @@ export const createApp = async (
}
const moduleFixture: TestingModule = await moduleBuilder.compile();
const app = moduleFixture.createNestApplication<NestExpressApplication>();
if (config.appInitHook) {
await config.appInitHook(app);
}
const mockAuthHandler: RequestHandler = (req, _res, next) => {
req.user = {
user: mockUser,
workspace: mockWorkspace,
};
next();
};
await app.init();
app.use(mockAuthHandler);
return [await app.init(), moduleFixture];
return app;
};

View File

@@ -0,0 +1,16 @@
import 'tsconfig-paths/register';
import { JestConfigWithTsJest } from 'ts-jest';
import { createApp } from './create-app';
export default async (_, projectConfig: JestConfigWithTsJest) => {
const app = await createApp({});
if (!projectConfig.globals) {
throw new Error('No globals found in project config');
}
await app.listen(projectConfig.globals.APP_PORT);
global.app = app;
};

View File

@@ -1,3 +0,0 @@
global.beforeEach(() => {
// resetDb();
});

View File

@@ -0,0 +1,5 @@
import 'tsconfig-paths/register';
export default async () => {
global.app.close();
};

View File

@@ -0,0 +1,61 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('viewFieldsResolver (integration)', () => {
it('should find many viewFields', () => {
const queryData = {
query: `
query viewFields {
viewFields {
edges {
node {
fieldMetadataId
isVisible
size
position
id
createdAt
updatedAt
deletedAt
viewId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.viewFields;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const viewFields = edges[0].node;
expect(viewFields).toHaveProperty('fieldMetadataId');
expect(viewFields).toHaveProperty('isVisible');
expect(viewFields).toHaveProperty('size');
expect(viewFields).toHaveProperty('position');
expect(viewFields).toHaveProperty('id');
expect(viewFields).toHaveProperty('createdAt');
expect(viewFields).toHaveProperty('updatedAt');
expect(viewFields).toHaveProperty('deletedAt');
expect(viewFields).toHaveProperty('viewId');
}
});
});
});

View File

@@ -0,0 +1,61 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('viewFiltersResolver (integration)', () => {
it('should find many viewFilters', () => {
const queryData = {
query: `
query viewFilters {
viewFilters {
edges {
node {
fieldMetadataId
operand
value
displayValue
id
createdAt
updatedAt
deletedAt
viewId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.viewFilters;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const viewFilters = edges[0].node;
expect(viewFilters).toHaveProperty('fieldMetadataId');
expect(viewFilters).toHaveProperty('operand');
expect(viewFilters).toHaveProperty('value');
expect(viewFilters).toHaveProperty('displayValue');
expect(viewFilters).toHaveProperty('id');
expect(viewFilters).toHaveProperty('createdAt');
expect(viewFilters).toHaveProperty('updatedAt');
expect(viewFilters).toHaveProperty('deletedAt');
expect(viewFilters).toHaveProperty('viewId');
}
});
});
});

View File

@@ -0,0 +1,57 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('viewSortsResolver (integration)', () => {
it('should find many viewSorts', () => {
const queryData = {
query: `
query viewSorts {
viewSorts {
edges {
node {
fieldMetadataId
direction
id
createdAt
updatedAt
deletedAt
viewId
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.viewSorts;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const viewSorts = edges[0].node;
expect(viewSorts).toHaveProperty('fieldMetadataId');
expect(viewSorts).toHaveProperty('direction');
expect(viewSorts).toHaveProperty('id');
expect(viewSorts).toHaveProperty('createdAt');
expect(viewSorts).toHaveProperty('updatedAt');
expect(viewSorts).toHaveProperty('deletedAt');
expect(viewSorts).toHaveProperty('viewId');
}
});
});
});

View File

@@ -0,0 +1,67 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('viewsResolver (integration)', () => {
it('should find many views', () => {
const queryData = {
query: `
query views {
views {
edges {
node {
position
name
objectMetadataId
type
key
icon
kanbanFieldMetadataId
isCompact
id
createdAt
updatedAt
deletedAt
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.views;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const views = edges[0].node;
expect(views).toHaveProperty('position');
expect(views).toHaveProperty('name');
expect(views).toHaveProperty('objectMetadataId');
expect(views).toHaveProperty('type');
expect(views).toHaveProperty('key');
expect(views).toHaveProperty('icon');
expect(views).toHaveProperty('kanbanFieldMetadataId');
expect(views).toHaveProperty('isCompact');
expect(views).toHaveProperty('id');
expect(views).toHaveProperty('createdAt');
expect(views).toHaveProperty('updatedAt');
expect(views).toHaveProperty('deletedAt');
}
});
});
});

View File

@@ -0,0 +1,57 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('webhooksResolver (integration)', () => {
it('should find many webhooks', () => {
const queryData = {
query: `
query webhooks {
webhooks {
edges {
node {
targetUrl
operation
description
id
createdAt
updatedAt
deletedAt
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.webhooks;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const webhooks = edges[0].node;
expect(webhooks).toHaveProperty('targetUrl');
expect(webhooks).toHaveProperty('operation');
expect(webhooks).toHaveProperty('description');
expect(webhooks).toHaveProperty('id');
expect(webhooks).toHaveProperty('createdAt');
expect(webhooks).toHaveProperty('updatedAt');
expect(webhooks).toHaveProperty('deletedAt');
}
});
});
});

View File

@@ -0,0 +1,67 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
describe('workspaceMembersResolver (integration)', () => {
it('should find many workspaceMembers', () => {
const queryData = {
query: `
query workspaceMembers {
workspaceMembers {
edges {
node {
id
colorScheme
avatarUrl
locale
timeZone
dateFormat
timeFormat
userEmail
userId
createdAt
updatedAt
deletedAt
}
}
}
}
`,
};
return client
.post('/graphql')
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeDefined();
expect(res.body.errors).toBeUndefined();
})
.expect((res) => {
const data = res.body.data.workspaceMembers;
expect(data).toBeDefined();
expect(Array.isArray(data.edges)).toBe(true);
const edges = data.edges;
if (edges.length > 0) {
const workspaceMembers = edges[0].node;
expect(workspaceMembers).toHaveProperty('id');
expect(workspaceMembers).toHaveProperty('colorScheme');
expect(workspaceMembers).toHaveProperty('avatarUrl');
expect(workspaceMembers).toHaveProperty('locale');
expect(workspaceMembers).toHaveProperty('timeZone');
expect(workspaceMembers).toHaveProperty('dateFormat');
expect(workspaceMembers).toHaveProperty('timeFormat');
expect(workspaceMembers).toHaveProperty('userEmail');
expect(workspaceMembers).toHaveProperty('userId');
expect(workspaceMembers).toHaveProperty('createdAt');
expect(workspaceMembers).toHaveProperty('updatedAt');
expect(workspaceMembers).toHaveProperty('deletedAt');
}
});
});
});

View File

@@ -27,7 +27,7 @@
"paths": {
"src/*": ["packages/twenty-server/src/*"],
"test/*": ["packages/twenty-server/test/*"],
"twenty-emails": ["packages/twenty-emails/src/index.ts"]
"twenty-emails": ["packages/twenty-emails/dist"]
}
},
"ts-node": {

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["scripts/**/*.ts"],
"exclude": ["node_modules", "dist"]
}