Compare commits

...

9 Commits

Author SHA1 Message Date
a.bouhuolia
6bd30abddb fix(server): prevent delete base currency 2023-04-14 03:45:09 +02:00
Ahmed Bouhuolia
cc863f774a Merge pull request #108 from bigcapitalhq/e2e-init
feat: setup e2e
2023-04-13 02:56:16 +02:00
a.bouhuolia
bcd08284b4 chore: add vercel rewrite 2023-04-13 02:50:55 +02:00
a.bouhuolia
8e8161f207 feat: add playwright base url 2023-04-13 02:38:22 +02:00
a.bouhuolia
7b4b50cf4b feat: setup e2e 2023-04-13 02:31:56 +02:00
Lars Scheibling
bca3e51fdf fix: typo in docker-compose.prod.yml (#107)
MAIL_USERNAME instead of MAIL_USERNAM
2023-04-12 19:29:04 +02:00
Ahmed Bouhuolia
6faa378577 Merge pull request #105 from bigcapitalhq/docker-compose-user-permissions
fix: docker-compose environment values
2023-04-07 18:57:47 +02:00
a.bouhuolia
012b13ad4a add comments 2023-04-07 18:50:45 +02:00
a.bouhuolia
ad8770f12c fix: docker-compose enviroment values 2023-04-07 18:41:03 +02:00
14 changed files with 301 additions and 58 deletions

68
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
name: E2E
on:
push:
branches:
- main
- develop
paths:
- '**.ts'
- '**.tsx'
- '**/tsconfig.json'
- 'yarn.lock'
- '.github/workflows/e2e.yml'
pull_request:
paths:
- '**.ts'
- '**.tsx'
- '**/tsconfig.json'
- 'yarn.lock'
- '.github/workflows/e2e.yml'
defaults:
run:
shell: 'bash'
jobs:
test_setup:
name: Test setup
runs-on: ubuntu-latest
outputs:
preview_url: ${{ steps.waitForVercelPreviewDeployment.outputs.url }}
steps:
- name: Wait for Vercel preview deployment to be ready
uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
id: waitForVercelPreviewDeployment
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 3000
test_e2e:
runs-on: ubuntu-latest
needs: test_setup
name: Playwright tests
timeout-minutes: 15
environment: ${{ vars.ENVIRONMENT_STAGE }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14 # Need for npm >=7.7
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Install Playwright with deps
run: npx playwright install --with-deps
- name: Run tests
run: npm run test:e2e
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ needs.test_setup.outputs.preview_url }}
- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: test-results/
retention-days: 30

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules/ node_modules/
data data
.env .env
test-results/

View File

@@ -40,7 +40,7 @@ services:
environment: environment:
# Mail # Mail
- MAIL_HOST=${MAIL_HOST} - MAIL_HOST=${MAIL_HOST}
- MAIL_USERNAME=${MAIL_USERNAM} - MAIL_USERNAME=${MAIL_USERNAME}
- MAIL_PASSWORD=${MAIL_PASSWORD} - MAIL_PASSWORD=${MAIL_PASSWORD}
- MAIL_PORT=${MAIL_PORT} - MAIL_PORT=${MAIL_PORT}
- MAIL_SECURE=${MAIL_SECURE} - MAIL_SECURE=${MAIL_SECURE}
@@ -77,22 +77,24 @@ services:
build: build:
context: ./ context: ./
dockerfile: docker/migration/Dockerfile dockerfile: docker/migration/Dockerfile
args: environment:
- DB_HOST=mysql - DB_HOST=mysql
- DB_USER=${DB_USER} - DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD} - DB_PASSWORD=${DB_PASSWORD}
- DB_CHARSET=${DB_CHARSET} - DB_CHARSET=${DB_CHARSET}
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME} - SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
depends_on:
- mysql
mysql: mysql:
container_name: bigcapital-mysql container_name: bigcapital-mysql
build: build:
context: ./docker/mysql context: ./docker/mysql
args: environment:
- MYSQL_DATABASE=${SYSTEM_DB_NAME} - MYSQL_DATABASE=${SYSTEM_DB_NAME}
- MYSQL_USER=${DB_NAME} - MYSQL_USER=${DB_USER}
- MYSQL_PASSWORD=${DB_PASSWORD} - MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
expose: expose:
@@ -127,4 +129,4 @@ volumes:
redis: redis:
name: bigcapital_prod_redis name: bigcapital_prod_redis
driver: local driver: local

View File

@@ -9,11 +9,11 @@ services:
mysql: mysql:
build: build:
context: ./docker/mysql context: ./docker/mysql
args: environment:
- MYSQL_DATABASE=${SYSTEM_DB_NAME} - MYSQL_DATABASE=${SYSTEM_DB_NAME}
- MYSQL_USER=${DB_NAME} - MYSQL_USER=${DB_USER}
- MYSQL_PASSWORD=${DB_PASSWORD} - MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
expose: expose:

View File

@@ -1,5 +1,6 @@
FROM mysql:5.7 FROM mysql:5.7
USER root
ADD my.cnf /etc/mysql/conf.d/my.cnf ADD my.cnf /etc/mysql/conf.d/my.cnf
ARG MYSQL_DATABASE=default_database ARG MYSQL_DATABASE=default_database
@@ -12,5 +13,14 @@ ENV MYSQL_USER=$MYSQL_USER
ENV MYSQL_PASSWORD=$MYSQL_PASSWORD ENV MYSQL_PASSWORD=$MYSQL_PASSWORD
ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
# Copy init sql file with env vars and then the script will substitute the variables.
COPY ./init.sql /scripts/init.template.sql
COPY ./docker-entrypoint.sh /docker-entrypoint-initdb.d/docker-initialize.sh
# The scripts in the docker-entrypoint-initdb.d/ directory are executed as
# the mysql user inside the MySQL Docker container.
RUN chown -R mysql:root /docker-entrypoint-initdb.d
RUN chown -R mysql:root /scripts
CMD ["mysqld"] CMD ["mysqld"]
EXPOSE 3306 EXPOSE 3306

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# chmod u+rwx /scripts/init.template.sql
cp /scripts/init.template.sql /scripts/init.sql
# Replace environment variables in SQL files with their values
if [ -n "$MYSQL_USER" ]; then
sed -i "s/{MYSQL_USER}/$MYSQL_USER/g" /scripts/init.sql
fi
if [ -n "$MYSQL_PASSWORD" ]; then
sed -i "s/{MYSQL_PASSWORD}/$MYSQL_PASSWORD/g" /scripts/init.sql
fi
if [ -n "$MYSQL_DATABASE" ]; then
sed -i "s/{MYSQL_DATABASE}/$MYSQL_DATABASE/g" /scripts/init.sql
fi
# Execute SQL file
mysql -u root -p$MYSQL_ROOT_PASSWORD < /scripts/init.sql

2
docker/mysql/init.sql Normal file
View File

@@ -0,0 +1,2 @@
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
FLUSH PRIVILEGES;

View File

@@ -0,0 +1,68 @@
import { test, expect, Page } from '@playwright/test';
let authPage: Page;
test.describe('authentication', () => {
test.beforeAll(async ({ browser }) => {
authPage = await browser.newPage();
});
test.describe('login', () => {
test.beforeAll(async () => {
await authPage.goto('/auth/login');
});
test('should show the login page.', async () => {
await expect(authPage.locator('body')).toContainText(
"Don't have an account? Sign up"
);
});
test('should email and password be required.', async () => {
await authPage.getByRole('button', { name: 'Log in' }).click();
await expect(authPage.locator('form')).toContainText(
'Email is a required field'
);
await expect(authPage.locator('form')).toContainText(
'Password is a required field'
);
});
test('should go to the register page when click on sign up link', async () => {
await authPage.getByRole('link', { name: 'Sign up' }).click();
await expect(authPage.url()).toContain('/auth/register');
});
});
test.describe('register', () => {
test.beforeAll(async () => {
await authPage.goto('/auth/register');
});
test('should first name, last name, email and password be required.', async () => {
await authPage.getByRole('button', { name: 'Register' }).click();
await expect(authPage.locator('form')).toContainText(
'First name is a required field'
);
await expect(authPage.locator('form')).toContainText(
'Last name is a required field'
);
await expect(authPage.locator('form')).toContainText(
'Email is a required field'
);
await expect(authPage.locator('form')).toContainText(
'Password is a required field'
);
});
});
test.describe('reset password', () => {
test.beforeAll(async () => {
await authPage.goto('/auth/send_reset_password');
});
test('should email be required.', async () => {
await authPage.getByRole('button', { name: 'Reset Password' }).click();
await expect(authPage.locator('form')).toContainText(
'Email is a required field'
);
});
});
});

26
package-lock.json generated
View File

@@ -941,6 +941,23 @@
"esquery": "^1.0.1" "esquery": "^1.0.1"
} }
}, },
"@playwright/test": {
"version": "1.32.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz",
"integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==",
"requires": {
"@types/node": "*",
"fsevents": "2.3.2",
"playwright-core": "1.32.3"
},
"dependencies": {
"playwright-core": {
"version": "1.32.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz",
"integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg=="
}
}
},
"@tootallnate/once": { "@tootallnate/once": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -986,8 +1003,7 @@
"@types/node": { "@types/node": {
"version": "18.14.6", "version": "18.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz",
"integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==", "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA=="
"dev": true
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.1", "version": "2.4.1",
@@ -2304,6 +2320,12 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true "dev": true
}, },
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",

View File

@@ -10,6 +10,7 @@
"dev:server": "lerna run dev --scope \"@bigcapital/server\"", "dev:server": "lerna run dev --scope \"@bigcapital/server\"",
"build:server": "lerna run build --scope \"@bigcapital/server\"", "build:server": "lerna run build --scope \"@bigcapital/server\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\"", "serve:server": "lerna run serve --scope \"@bigcapital/server\"",
"test:e2e": "playwright test",
"prepare": "husky install" "prepare": "husky install"
}, },
"workspaces": [ "workspaces": [
@@ -21,7 +22,8 @@
"@commitlint/config-lerna-scopes": "^17.4.2", "@commitlint/config-lerna-scopes": "^17.4.2",
"husky": "^8.0.3", "husky": "^8.0.3",
"lerna": "^6.4.1", "lerna": "^6.4.1",
"@commitlint/cli": "^17.4.2" "@commitlint/cli": "^17.4.2",
"@playwright/test": "^1.32.3"
}, },
"engines": { "engines": {
"node": "14.x" "node": "14.x"
@@ -30,6 +32,5 @@
"hooks": { "hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
} }
}, }
"dependencies": {}
} }

View File

@@ -1,18 +1,15 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { uniq } from 'lodash';
import { import {
ICurrencyEditDTO, ICurrencyEditDTO,
ICurrencyDTO, ICurrencyDTO,
ICurrenciesService, ICurrenciesService,
ICurrency, ICurrency,
} from '@/interfaces'; } from '@/interfaces';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { Tenant } from '@/system/models';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { CurrencyTransformer } from './CurrencyTransformer';
const ERRORS = { const ERRORS = {
CURRENCY_NOT_FOUND: 'currency_not_found', CURRENCY_NOT_FOUND: 'currency_not_found',
@@ -23,14 +20,11 @@ const ERRORS = {
@Service() @Service()
export default class CurrenciesService implements ICurrenciesService { export default class CurrenciesService implements ICurrenciesService {
@Inject('logger') @Inject()
logger: any; private tenancy: TenancyService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject() @Inject()
tenancy: TenancyService; private transformer: TransformerInjectable;
/** /**
* Retrieve currency by given currency code or throw not found error. * Retrieve currency by given currency code or throw not found error.
@@ -105,7 +99,7 @@ export default class CurrenciesService implements ICurrenciesService {
*/ */
public async newCurrency(tenantId: number, currencyDTO: ICurrencyDTO) { public async newCurrency(tenantId: number, currencyDTO: ICurrencyDTO) {
const { Currency } = this.tenancy.models(tenantId); const { Currency } = this.tenancy.models(tenantId);
// Validate currency code uniquiness. // Validate currency code uniquiness.
await this.validateCurrencyCodeUniquiness( await this.validateCurrencyCodeUniquiness(
tenantId, tenantId,
@@ -141,13 +135,15 @@ export default class CurrenciesService implements ICurrenciesService {
* @param {number} tenantId * @param {number} tenantId
* @param {string} currencyCode * @param {string} currencyCode
*/ */
validateCannotDeleteBaseCurrency(tenantId: number, currencyCode: string) { private async validateCannotDeleteBaseCurrency(
const settings = this.tenancy.settings(tenantId); tenantId: number,
const baseCurrency = settings.get({ currencyCode: string
group: 'organization', ) {
key: 'base_currency', const tenant = await Tenant.query()
}); .findById(tenantId)
if (baseCurrency === currencyCode) { .withGraphFetched('metadata');
if (tenant.metadata.baseCurrency === currencyCode) {
throw new ServiceError(ERRORS.CANNOT_DELETE_BASE_CURRENCY); throw new ServiceError(ERRORS.CANNOT_DELETE_BASE_CURRENCY);
} }
} }
@@ -156,7 +152,7 @@ export default class CurrenciesService implements ICurrenciesService {
* Delete the given currency code. * Delete the given currency code.
* @param {number} tenantId * @param {number} tenantId
* @param {string} currencyCode * @param {string} currencyCode
* @return {Promise<} * @return {Promise<void>}
*/ */
public async deleteCurrency( public async deleteCurrency(
tenantId: number, tenantId: number,
@@ -180,19 +176,13 @@ export default class CurrenciesService implements ICurrenciesService {
public async listCurrencies(tenantId: number): Promise<ICurrency[]> { public async listCurrencies(tenantId: number): Promise<ICurrency[]> {
const { Currency } = this.tenancy.models(tenantId); const { Currency } = this.tenancy.models(tenantId);
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
const currencies = await Currency.query().onBuild((query) => { const currencies = await Currency.query().onBuild((query) => {
query.orderBy('createdAt', 'ASC'); query.orderBy('createdAt', 'ASC');
}); });
const formattedCurrencies = currencies.map((currency) => ({ return this.transformer.transform(
isBaseCurrency: baseCurrency === currency.currencyCode, tenantId,
...currency, currencies,
})); new CurrencyTransformer()
return formattedCurrencies; );
} }
} }

View File

@@ -0,0 +1,19 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class CurrencyTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['isBaseCurrency'];
};
/**
* Detarmines whether the currency is base currency.
* @returns {boolean}
*/
public isBaseCurrency(currency): boolean {
return this.context.organization.baseCurrency === currency.currencyCode;
}
}

38
playwright.config.ts Normal file
View File

@@ -0,0 +1,38 @@
import path from 'path';
import { PlaywrightTestConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
// Reference: https://playwright.dev/docs/test-configuration
const config: PlaywrightTestConfig = {
// Timeout per test
timeout: 60 * 1000,
workers: 1,
// Test directory
testDir: path.join(__dirname, 'e2e'),
// If a test fails, retry it additional 2 times
retries: 0,
// Artifacts folder where screenshots, videos, and traces are stored.
outputDir: 'test-results/',
use: {
// Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc.
// More information: https://playwright.dev/docs/trace-viewer
trace: 'retain-on-failure',
// All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context
// contextOptions: {
// ignoreHTTPSErrors: true,
// },
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:4000',
},
projects: [
{
name: 'Desktop Chrome',
use: {
...devices['Desktop Chrome'],
},
},
],
};
export default config;

View File

@@ -1,5 +1,9 @@
{ {
"installCommand": "npm install && npm run bootstrap", "installCommand": "npm install && npm run bootstrap",
"buildCommand": "CI='' npm run build:webapp", "buildCommand": "CI='' npm run build:webapp",
"outputDirectory": "packages/webapp/build" "outputDirectory": "packages/webapp/build",
} "rewrites": [{
"source": "/(.*)",
"destination": "/"
}]
}