ci: run e2e tests on digital ocean droplet

This commit is contained in:
Nicolas Meienberger 2023-06-01 23:25:12 +02:00 committed by Nicolas Meienberger
parent 2aeacd7d14
commit c0d5e95d4c
20 changed files with 325 additions and 173 deletions

170
.github/workflows/e2e.yml vendored Normal file
View file

@ -0,0 +1,170 @@
name: E2E Tests
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
build:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push images
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64
push: true
tags: meienberger/runtipi:e2e
cache-from: type=registry,ref=meienberger/runtipi:buildcache
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
deploy:
timeout-minutes: 15
runs-on: ubuntu-latest
needs: [build]
outputs:
droplet_id: ${{ steps.create-droplet.outputs.droplet_id }}
droplet_ip: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: unnecessary
name: id_rsa
- name: Get sha of last commit
id: get-sha
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Create new Droplet
id: create-droplet
run: |
droplet_id=$(doctl compute droplet create runtipi-${{ steps.get-sha.outputs.sha }} \
--image ubuntu-20-04-x64 \
--size s-1vcpu-1gb \
--format ID \
--no-header \
--ssh-keys ${{ secrets.SSH_KEY_FINGERPRINT }})
echo "droplet_id=$droplet_id" >> $GITHUB_OUTPUT
- name: Wait for Droplet to become active
run: |
while ! doctl compute droplet get ${{ steps.create-droplet.outputs.droplet_id }} --format Status --no-header | grep -q "active"; do sleep 5; done
- name: Get Droplet IP address
id: get-droplet-ip
run: |
droplet_ip=$(doctl compute droplet get ${{ steps.create-droplet.outputs.droplet_id }} --format PublicIPv4 --no-header)
echo "droplet_ip=$droplet_ip" >> $GITHUB_OUTPUT
- name: Wait for SSH to be ready on Droplet
run: |
while ! ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa root@${{ steps.get-droplet-ip.outputs.droplet_ip }} "echo 'SSH is ready'"; do sleep 5; done
- name: Wait 1 minute for Droplet to be ready
run: sleep 60
- name: Deploy app to Droplet
uses: fifsky/ssh-action@master
with:
command: |
echo 'Cloning repo on branch ${{ github.head_ref }}'
git clone --single-branch --branch ${{ github.head_ref }} https://github.com/${{ github.repository }}
echo 'Waiting for dpkg lock to be released'
cd runtipi
echo 'Checking out branch ${{ github.head_ref }}'
git checkout ${{ github.head_ref }}
sudo ./scripts/start-e2e.sh latest
echo 'App deployed'
host: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
user: root
key: ${{ secrets.SSH_KEY }}
e2e:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: [deploy]
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Create .env.e2e file with Droplet IP
run: echo "SERVER_IP=${{ needs.deploy.outputs.droplet_ip }}" > .env.e2e
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
teardown:
runs-on: ubuntu-latest
if: always()
needs: [e2e, deploy]
steps:
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Delete Droplet
run: doctl compute droplet delete ${{ needs.deploy.outputs.droplet_id }} --force

View file

@ -1,59 +0,0 @@
name: Playwright Tests
on:
push:
branches: [develop, master, test/e2e-playwright]
pull_request:
branches: [develop, master]
jobs:
e2e:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: pnpm/action-setup@v2.2.2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Install fswatch
run: sudo apt-get update && sudo apt-get install fswatch
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Build and run the app
run: npm run start:e2e
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View file

@ -50,9 +50,7 @@ services:
- tipi_main_network - tipi_main_network
dashboard: dashboard:
build: image: meienberger/runtipi:${DOCKER_TAG}
context: .
dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
container_name: dashboard container_name: dashboard
networks: networks:

View file

@ -1,6 +1,12 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { loginUser } from './fixtures/fixtures'; import { registerUser } from './fixtures/fixtures';
import { testUser } from './helpers/constants'; import { testUser } from './helpers/constants';
import { clearDatabase } from './helpers/db';
test.beforeEach(async ({ page }) => {
await clearDatabase();
await registerUser(page);
});
test('user can login and is redirected to the dashboard', async ({ page }) => { test('user can login and is redirected to the dashboard', async ({ page }) => {
await page.goto('/login'); await page.goto('/login');
@ -13,7 +19,6 @@ test('user can login and is redirected to the dashboard', async ({ page }) => {
}); });
test('user can logout', async ({ page }) => { test('user can logout', async ({ page }) => {
await loginUser(page);
await page.getByTestId('logout-button').click(); await page.getByTestId('logout-button').click();
await expect(page.getByText('Login to your account')).toBeVisible(); await expect(page.getByText('Login to your account')).toBeVisible();

View file

@ -1,8 +1,10 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { loginUser } from './fixtures/fixtures'; import { registerUser } from './fixtures/fixtures';
import { clearDatabase } from './helpers/db';
test.beforeEach(async ({ page, isMobile }) => { test.beforeEach(async ({ page, isMobile }) => {
await loginUser(page); await clearDatabase();
await registerUser(page);
// Go to hello world app // Go to hello world app
if (isMobile) { if (isMobile) {

View file

@ -1,6 +1,18 @@
import { expect, Page } from '@playwright/test'; import { expect, Page } from '@playwright/test';
import { testUser } from '../helpers/constants'; import { testUser } from '../helpers/constants';
export const registerUser = async (page: Page) => {
await page.goto('/register');
await page.getByPlaceholder('you@example.com').click();
await page.getByPlaceholder('you@example.com').fill(testUser.email);
await page.getByPlaceholder('Enter your password', { exact: true }).fill(testUser.password);
await page.getByPlaceholder('Confirm your password').fill(testUser.password);
await page.getByRole('button', { name: 'Register' }).click();
await expect(page).toHaveTitle(/Dashboard/);
};
export const loginUser = async (page: Page) => { export const loginUser = async (page: Page) => {
await page.goto('/login'); await page.goto('/login');

View file

@ -1,13 +1,12 @@
import pg from 'pg'; import pg from 'pg';
import { getConfig } from '../../src/server/core/TipiConfig';
export const clearDatabase = async () => { export const clearDatabase = async () => {
const pgClient = new pg.Client({ const pgClient = new pg.Client({
user: getConfig().postgresUsername, user: 'tipi',
host: '127.0.0.1', host: process.env.SERVER_IP,
database: getConfig().postgresDatabase, database: 'tipi',
password: getConfig().postgresPassword, password: 'postgres',
port: getConfig().postgresPort, port: 5432,
}); });
await pgClient.connect(); await pgClient.connect();

View file

@ -5,7 +5,6 @@ import { clearDatabase } from './db';
*/ */
async function globalSetup() { async function globalSetup() {
await clearDatabase(); await clearDatabase();
console.log('Global setup...');
} }
export default globalSetup; export default globalSetup;

View file

@ -5,7 +5,7 @@
"scripts": { "scripts": {
"copy:migrations": "mkdir -p dist/migrations && cp -r ./src/server/migrations dist", "copy:migrations": "mkdir -p dist/migrations && cp -r ./src/server/migrations dist",
"test": "dotenv -e .env.test -- jest --colors", "test": "dotenv -e .env.test -- jest --colors",
"test:e2e": "NODE_ENV=test dotenv -e .env -- playwright test", "test:e2e": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test",
"test:e2e:ui": "NODE_ENV=test dotenv -e .env -- playwright test --ui", "test:e2e:ui": "NODE_ENV=test dotenv -e .env -- playwright test --ui",
"test:client": "jest --colors --selectProjects client --", "test:client": "jest --colors --selectProjects client --",
"test:server": "jest --colors --selectProjects server --", "test:server": "jest --colors --selectProjects server --",
@ -60,7 +60,6 @@
"next": "13.4.4", "next": "13.4.4",
"next-intl": "^2.14.2", "next-intl": "^2.14.2",
"node-cron": "^3.0.1", "node-cron": "^3.0.1",
"node-fetch-commonjs": "^3.2.4",
"pg": "^8.11.0", "pg": "^8.11.0",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "18.2.0", "react": "18.2.0",
@ -70,6 +69,7 @@
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-select": "^5.7.3", "react-select": "^5.7.3",
"react-tooltip": "^5.13.1", "react-tooltip": "^5.13.1",
"redaxios": "^0.5.1",
"redis": "^4.6.6", "redis": "^4.6.6",
"remark-breaks": "^3.0.3", "remark-breaks": "^3.0.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
@ -90,7 +90,6 @@
"@faker-js/faker": "^8.0.1", "@faker-js/faker": "^8.0.1",
"@testing-library/dom": "^9.3.0", "@testing-library/dom": "^9.3.0",
"@playwright/test": "^1.32.3", "@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.0.1",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",

View file

@ -26,7 +26,7 @@ export default defineConfig({
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000', baseURL: `http://${process.env.SERVER_IP}`,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
@ -42,21 +42,21 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] }, use: { ...devices['Desktop Chrome'] },
}, },
{ // {
name: 'firefox', // name: 'firefox',
use: { ...devices['Desktop Firefox'] }, // use: { ...devices['Desktop Firefox'] },
}, // },
{ // {
name: 'webkit', // name: 'webkit',
use: { ...devices['Desktop Safari'] }, // use: { ...devices['Desktop Safari'] },
}, // },
/* Test against mobile viewports. */ /* Test against mobile viewports. */
{ // {
name: 'Mobile Chrome', // name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }, // use: { ...devices['Pixel 5'] },
}, // },
// { // {
// name: 'Mobile Safari', // name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] }, // use: { ...devices['iPhone 12'] },
@ -74,9 +74,9 @@ export default defineConfig({
], ],
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
webServer: { // webServer: {
command: 'npm run start:e2e', // command: 'npm run start:e2e',
url: 'http://127.0.0.1:3000', // url: 'http://174.138.79.40',
reuseExistingServer: true, // reuseExistingServer: true,
}, // },
}); });

View file

@ -91,9 +91,6 @@ dependencies:
node-cron: node-cron:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.2 version: 3.0.2
node-fetch-commonjs:
specifier: ^3.2.4
version: 3.2.4
pg: pg:
specifier: ^8.11.0 specifier: ^8.11.0
version: 8.11.0 version: 8.11.0
@ -121,6 +118,9 @@ dependencies:
react-tooltip: react-tooltip:
specifier: ^5.13.1 specifier: ^5.13.1
version: 5.13.1(react-dom@18.2.0)(react@18.2.0) version: 5.13.1(react-dom@18.2.0)(react@18.2.0)
redaxios:
specifier: ^0.5.1
version: 0.5.1
redis: redis:
specifier: ^4.6.6 specifier: ^4.6.6
version: 4.6.6 version: 4.6.6
@ -175,7 +175,7 @@ devDependencies:
specifier: ^1.32.3 specifier: ^1.32.3
version: 1.32.3 version: 1.32.3
'@testing-library/dom': '@testing-library/dom':
specifier: ^9.0.1 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
'@testing-library/jest-dom': '@testing-library/jest-dom':
specifier: ^5.16.5 specifier: ^5.16.5
@ -1618,7 +1618,7 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
dependencies: dependencies:
'@types/node': 20.2.1 '@types/node': 20.2.5
playwright-core: 1.32.3 playwright-core: 1.32.3
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
@ -5100,14 +5100,6 @@ packages:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
dev: false dev: false
/fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.2.1
dev: false
/figures@3.2.0: /figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -5194,13 +5186,6 @@ packages:
mime-types: 2.1.35 mime-types: 2.1.35
dev: true dev: true
/formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
dependencies:
fetch-blob: 3.2.0
dev: false
/formidable@2.1.2: /formidable@2.1.2:
resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==}
dependencies: dependencies:
@ -7415,19 +7400,6 @@ packages:
uuid: 8.3.2 uuid: 8.3.2
dev: false dev: false
/node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
dev: false
/node-fetch-commonjs@3.2.4:
resolution: {integrity: sha512-bZW7+ldcuuMPLTJk8DufhT6qHDRdljYD0jqBjmrYfcInaYcReX5kK42SQsu/jvtit/tER28yYjnk63PEEmNPtg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
formdata-polyfill: 4.0.10
web-streams-polyfill: 3.2.1
dev: false
/node-fetch@2.6.9: /node-fetch@2.6.9:
resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@ -8209,6 +8181,10 @@ packages:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
/redaxios@0.5.1:
resolution: {integrity: sha512-FSD2AmfdbkYwl7KDExYQlVvIrFz6Yd83pGfaGjBzM9F6rpq8g652Q4Yq5QD4c+nf4g2AgeElv1y+8ajUPiOYMg==}
dev: false
/redent@3.0.0: /redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -9436,11 +9412,6 @@ packages:
- supports-color - supports-color
dev: false dev: false
/web-streams-polyfill@3.2.1:
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
engines: {node: '>= 8'}
dev: false
/webidl-conversions@3.0.1: /webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}

View file

@ -95,6 +95,7 @@ function kill_watcher() {
kill -9 $watcher_pid kill -9 $watcher_pid
fi fi
fi fi
# pkill -f "watcher.sh"
} }
@ -140,6 +141,7 @@ function generate_env_file() {
local postgres_port=$(get_json_field "$json_file" postgres_port) local postgres_port=$(get_json_field "$json_file" postgres_port)
local redis_host=$(get_json_field "$json_file" redis_host) local redis_host=$(get_json_field "$json_file" redis_host)
local demo_mode=$(get_json_field "$json_file" demo_mode) local demo_mode=$(get_json_field "$json_file" demo_mode)
local docker_tag=$(get_json_field "$json_file" docker_tag)
local root_folder=$(get_json_field "$json_file" root_folder | sed 's/\//\\\//g') local root_folder=$(get_json_field "$json_file" root_folder | sed 's/\//\\\//g')
local apps_repository=$(get_json_field "$json_file" apps_repository | sed 's/\//\\\//g') local apps_repository=$(get_json_field "$json_file" apps_repository | sed 's/\//\\\//g')
local storage_path=$(get_json_field "$json_file" storage_path | sed 's/\//\\\//g') local storage_path=$(get_json_field "$json_file" storage_path | sed 's/\//\\\//g')
@ -164,7 +166,7 @@ function generate_env_file() {
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoUrl)" != "null" ]]; then if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoUrl)" != "null" ]]; then
apps_repository_temp=$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoUrl) apps_repository_temp=$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoUrl)
apps_repository="$(echo "${apps_repository_temp}" | sed 's/\//\\\//g')" apps_repository="$(echo "${apps_repository_temp}" | sed 's/\//\\\//g')"
repo_id="$("${ROOT_FOLDER}"/scripts/git.sh get_hash "${apps_repository}")" repo_id="$("${ROOT_FOLDER}"/scripts/git.sh get_hash "${apps_repository_temp}")"
fi fi
# If port is set in settings.json, use it # If port is set in settings.json, use it
@ -233,6 +235,7 @@ function generate_env_file() {
sed "${sed_args[@]}" "s/<postgres_host>/${postgres_host}/g" "${template}" sed "${sed_args[@]}" "s/<postgres_host>/${postgres_host}/g" "${template}"
sed "${sed_args[@]}" "s/<redis_host>/${redis_host}/g" "${template}" sed "${sed_args[@]}" "s/<redis_host>/${redis_host}/g" "${template}"
sed "${sed_args[@]}" "s/<demo_mode>/${demo_mode}/g" "${template}" sed "${sed_args[@]}" "s/<demo_mode>/${demo_mode}/g" "${template}"
sed "${sed_args[@]}" "s/<docker_tag>/${docker_tag}/g" "${template}"
done done
mv -f "$env_file" "$ROOT_FOLDER/.env" mv -f "$env_file" "$ROOT_FOLDER/.env"

View file

@ -8,12 +8,10 @@ function install_generic() {
local os="${2}" local os="${2}"
if [[ "${os}" == "debian" ]]; then if [[ "${os}" == "debian" ]]; then
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${dependency}"
sudo apt-get install -y "${dependency}"
return 0 return 0
elif [[ "${os}" == "ubuntu" || "${os}" == "pop" ]]; then elif [[ "${os}" == "ubuntu" || "${os}" == "pop" ]]; then
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${dependency}"
sudo apt-get install -y "${dependency}"
return 0 return 0
elif [[ "${os}" == "centos" ]]; then elif [[ "${os}" == "centos" ]]; then
sudo yum install -y --allowerasing "${dependency}" sudo yum install -y --allowerasing "${dependency}"
@ -31,27 +29,23 @@ function install_generic() {
function install_docker() { function install_docker() {
local os="${1}" local os="${1}"
echo "Installing docker for os ${os}" >/dev/tty echo "Installing docker for os ${os}"
if [[ "${os}" == "debian" ]]; then if [[ "${os}" == "debian" ]]; then
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl gnupg lsb-release
sudo apt-get upgrade
sudo apt-get install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get update -y
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
return 0 return 0
elif [[ "${os}" == "ubuntu" || "${os}" == "pop" ]]; then elif [[ "${os}" == "ubuntu" || "${os}" == "pop" ]]; then
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl gnupg lsb-release
sudo apt-get upgrade
sudo apt-get install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get update -y
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
return 0 return 0
elif [[ "${os}" == "centos" ]]; then elif [[ "${os}" == "centos" ]]; then
sudo yum install -y yum-utils sudo yum install -y yum-utils
@ -82,12 +76,10 @@ function update_docker() {
echo "Updating Docker for os ${os}" >/dev/tty echo "Updating Docker for os ${os}" >/dev/tty
if [[ "${os}" == "debian" ]]; then if [[ "${os}" == "debian" ]]; then
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
return 0 return 0
elif [[ "${os}" == "ubuntu" || "${os}" == "pop" ]]; then elif [[ "${os}" == "ubuntu" || "${os}" == "pop" ]]; then
sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
return 0 return 0
elif [[ "${os}" == "centos" ]]; then elif [[ "${os}" == "centos" ]]; then
sudo yum install -y --allowerasing docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo yum install -y --allowerasing docker-ce docker-ce-cli containerd.io docker-compose-plugin
@ -103,7 +95,13 @@ function update_docker() {
fi fi
} }
echo "Updating system"
sudo DEBIAN_FRONTEND=noninteractive apt-get update -y
echo "Upgrading system"
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
if ! command -v docker >/dev/null; then if ! command -v docker >/dev/null; then
echo "Installing docker"
install_docker "${OS}" install_docker "${OS}"
docker_result=$? docker_result=$?

View file

@ -6,6 +6,8 @@ if [[ "${TRACE-0}" == "1" ]]; then
set -o xtrace set -o xtrace
fi fi
export DEBIAN_FRONTEND=noninteractive
source "${BASH_SOURCE%/*}/common.sh" source "${BASH_SOURCE%/*}/common.sh"
clean_logs clean_logs
@ -15,6 +17,26 @@ clean_logs
### -------------------------------- ### --------------------------------
ROOT_FOLDER="${PWD}" ROOT_FOLDER="${PWD}"
STATE_FOLDER="${ROOT_FOLDER}/state" STATE_FOLDER="${ROOT_FOLDER}/state"
## Comes from first argument
DOCKER_TAG="${1}"
echo "Starting e2e tests with tag meienberger/runtipi:${DOCKER_TAG}"
### --------------------------------
### Pre-configuration
### --------------------------------
sudo "${ROOT_FOLDER}/scripts/configure.sh"
mkdir -p "${ROOT_FOLDER}/state"
STATE_FOLDER="${ROOT_FOLDER}/state"
if [[ ! -f "${STATE_FOLDER}/seed" ]]; then
echo "Generating seed..."
mkdir -p "${STATE_FOLDER}"
touch "${STATE_FOLDER}/seed"
if ! tr </dev/urandom -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 >"${STATE_FOLDER}/seed"; then
echo "Created seed file..."
fi
fi
### -------------------------------- ### --------------------------------
### Apps repository configuration ### Apps repository configuration
@ -26,7 +48,7 @@ env_variables_json=$(cat <<EOF
"dns_ip": "9.9.9.9", "dns_ip": "9.9.9.9",
"domain": "tipi.localhost", "domain": "tipi.localhost",
"root_folder": "${ROOT_FOLDER}", "root_folder": "${ROOT_FOLDER}",
"nginx_port": 3000, "nginx_port": 80,
"nginx_port_ssl": 443, "nginx_port_ssl": 443,
"jwt_secret": "secret", "jwt_secret": "secret",
"postgres_password": "postgres", "postgres_password": "postgres",
@ -40,7 +62,8 @@ env_variables_json=$(cat <<EOF
"demo_mode": false, "demo_mode": false,
"apps_repository": "${apps_repository}", "apps_repository": "${apps_repository}",
"storage_path": "${ROOT_FOLDER}", "storage_path": "${ROOT_FOLDER}",
"repo_id": "$("${ROOT_FOLDER}"/scripts/git.sh get_hash ${apps_repository})" "repo_id": "$("${ROOT_FOLDER}"/scripts/git.sh get_hash ${apps_repository})",
"docker_tag": "${DOCKER_TAG}"
} }
EOF EOF
) )
@ -48,27 +71,32 @@ EOF
### -------------------------------- ### --------------------------------
### Watcher and system-info ### Watcher and system-info
### -------------------------------- ### --------------------------------
mkdir -p "${ROOT_FOLDER}/state"
echo "creating events file"
if [[ ! -f "${ROOT_FOLDER}/state/events" ]]; then if [[ ! -f "${ROOT_FOLDER}/state/events" ]]; then
touch "${ROOT_FOLDER}/state/events" touch "${ROOT_FOLDER}/state/events"
fi fi
echo "creating system-info file"
if [[ ! -f "${ROOT_FOLDER}/state/system-info.json" ]]; then if [[ ! -f "${ROOT_FOLDER}/state/system-info.json" ]]; then
echo "{}" >"${ROOT_FOLDER}/state/system-info.json" echo "{}" >"${ROOT_FOLDER}/state/system-info.json"
fi fi
chmod -R a+rwx "${ROOT_FOLDER}/state/events" chmod -R a+rwx "${ROOT_FOLDER}/state/events"
chmod -R a+rwx "${ROOT_FOLDER}/state/system-info.json" chmod -R a+rwx "${ROOT_FOLDER}/state/system-info.json"
echo "kill previous watcher"
kill_watcher kill_watcher
"${ROOT_FOLDER}/scripts/watcher.sh" & echo "starting watcher"
nohup "${ROOT_FOLDER}/scripts/watcher.sh" > /dev/null 2>&1 &
### -------------------------------- ### --------------------------------
### env file generation ### env file generation
### -------------------------------- ### --------------------------------
echo "Generating env file..."
generate_env_file "${env_variables_json}" generate_env_file "${env_variables_json}"
### -------------------------------- ### --------------------------------
### Start the project ### Start the project
### -------------------------------- ### --------------------------------
echo "Starting docker-compose..."
docker compose -f docker-compose.e2e.yml up -d --build docker compose -f docker-compose.e2e.yml up -d --build

View file

@ -22,7 +22,6 @@ clean_logs
"${ROOT_FOLDER}/scripts/configure.sh" "${ROOT_FOLDER}/scripts/configure.sh"
STATE_FOLDER="${ROOT_FOLDER}/state" STATE_FOLDER="${ROOT_FOLDER}/state"
# Create seed file with cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1
if [[ ! -f "${STATE_FOLDER}/seed" ]]; then if [[ ! -f "${STATE_FOLDER}/seed" ]]; then
echo "Generating seed..." echo "Generating seed..."
if ! tr </dev/urandom -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 >"${STATE_FOLDER}/seed"; then if ! tr </dev/urandom -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 >"${STATE_FOLDER}/seed"; then

View file

@ -50,7 +50,9 @@ nextApp.prepare().then(async () => {
EventDispatcher.clear(); EventDispatcher.clear();
// Run database migrations // Run database migrations
if (getConfig().NODE_ENV !== 'development') {
await runPostgresMigrations(); await runPostgresMigrations();
}
setConfig('status', 'RUNNING'); setConfig('status', 'RUNNING');
// Clone and update apps repo // Clone and update apps repo

View file

@ -1,17 +1,19 @@
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import fs from 'fs-extra'; import fs from 'fs-extra';
import semver from 'semver'; import semver from 'semver';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import fetch from 'node-fetch-commonjs';
import { EventDispatcher } from '../../core/EventDispatcher'; import { EventDispatcher } from '../../core/EventDispatcher';
import { setConfig } from '../../core/TipiConfig'; import { setConfig } from '../../core/TipiConfig';
import TipiCache from '../../core/TipiCache'; import TipiCache from '../../core/TipiCache';
import { SystemServiceClass } from '.'; import { SystemServiceClass } from '.';
jest.mock('redis'); jest.mock('redis');
jest.mock('node-fetch-commonjs');
const SystemService = new SystemServiceClass(); const SystemService = new SystemServiceClass();
const server = setupServer();
beforeEach(async () => { beforeEach(async () => {
jest.mock('fs-extra'); jest.mock('fs-extra');
jest.resetModules(); jest.resetModules();
@ -63,19 +65,28 @@ describe('Test: systemInfo', () => {
}); });
describe('Test: getVersion', () => { describe('Test: getVersion', () => {
beforeAll(() => {
server.listen();
});
beforeEach(() => { beforeEach(() => {
server.resetHandlers();
TipiCache.del('latestVersion'); TipiCache.del('latestVersion');
}); });
afterAll(() => { afterAll(() => {
server.close();
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
it('It should return version with body', async () => { it('It should return version with body', async () => {
// Arrange // Arrange
const body = faker.lorem.words(10); const body = faker.lorem.words(10);
// @ts-expect-error Mocking fetch server.use(
fetch.mockImplementationOnce(() => Promise.resolve({ json: () => Promise.resolve({ name: `v${faker.string.numeric(1)}.${faker.string.numeric(1)}.${faker.string.numeric()}`, body }) })); rest.get('https://api.github.com/repos/meienberger/runtipi/releases/latest', (_, res, ctx) => {
return res(ctx.json({ name: `v${faker.string.numeric(1)}.${faker.string.numeric(1)}.${faker.string.numeric()}`, body }));
}),
);
// Act // Act
const version = await SystemService.getVersion(); const version = await SystemService.getVersion();
@ -88,8 +99,11 @@ describe('Test: getVersion', () => {
}); });
it('Should return undefined for latest if request fails', async () => { it('Should return undefined for latest if request fails', async () => {
// @ts-expect-error Mocking fetch server.use(
fetch.mockImplementationOnce(() => Promise.reject(new Error('API is down'))); rest.get('https://api.github.com/repos/meienberger/runtipi/releases/latest', (_, res, ctx) => {
return res(ctx.status(500));
}),
);
const version = await SystemService.getVersion(); const version = await SystemService.getVersion();
@ -100,8 +114,11 @@ describe('Test: getVersion', () => {
it('Should return cached version', async () => { it('Should return cached version', async () => {
// Arrange // Arrange
// @ts-expect-error Mocking fetch server.use(
fetch.mockImplementationOnce(() => Promise.resolve({ json: () => Promise.resolve({ name: `v${faker.string.numeric(1)}.${faker.string.numeric(1)}.${faker.string.numeric()}` }) })); rest.get('https://api.github.com/repos/meienberger/runtipi/releases/latest', (_, res, ctx) => {
return res(ctx.json({ name: `v${faker.string.numeric(1)}.${faker.string.numeric(1)}.${faker.string.numeric()}` }));
}),
);
// Act // Act
const version = await SystemService.getVersion(); const version = await SystemService.getVersion();
@ -156,8 +173,11 @@ describe('Test: update', () => {
it('Should throw an error if latest version is not set', async () => { it('Should throw an error if latest version is not set', async () => {
// Arrange // Arrange
TipiCache.del('latestVersion'); TipiCache.del('latestVersion');
// @ts-expect-error Mocking fetch server.use(
fetch.mockImplementationOnce(() => Promise.resolve({ json: () => Promise.resolve({ name: null }) })); rest.get('https://api.github.com/repos/meienberger/runtipi/releases/latest', (_, res, ctx) => {
return res(ctx.json({ name: null }));
}),
);
setConfig('version', '0.0.1'); setConfig('version', '0.0.1');
// Act & Assert // Act & Assert

View file

@ -1,6 +1,6 @@
import semver from 'semver'; import semver from 'semver';
import { z } from 'zod'; import { z } from 'zod';
import fetch from 'node-fetch-commonjs'; import axios from 'redaxios';
import { TranslatedError } from '@/server/utils/errors'; import { TranslatedError } from '@/server/utils/errors';
import { readJsonFile } from '../../common/fs.helpers'; import { readJsonFile } from '../../common/fs.helpers';
import { EventDispatcher } from '../../core/EventDispatcher'; import { EventDispatcher } from '../../core/EventDispatcher';
@ -48,11 +48,10 @@ export class SystemServiceClass {
let body = await this.cache.get('latestVersionBody'); let body = await this.cache.get('latestVersionBody');
if (!version) { if (!version) {
const data = await fetch('https://api.github.com/repos/meienberger/runtipi/releases/latest'); const { data } = await axios.get<{ name: string; body: string }>('https://api.github.com/repos/meienberger/runtipi/releases/latest');
const release = (await data.json()) as { name: string; body: string };
version = release.name.replace('v', ''); version = data.name.replace('v', '');
body = release.body; body = data.body;
await this.cache.set('latestVersion', version?.replace('v', '') || '', 60 * 60); await this.cache.set('latestVersion', version?.replace('v', '') || '', 60 * 60);
await this.cache.set('latestVersionBody', body || '', 60 * 60); await this.cache.set('latestVersionBody', body || '', 60 * 60);

View file

@ -21,3 +21,4 @@ POSTGRES_PASSWORD=<postgres_password>
POSTGRES_PORT=<postgres_port> POSTGRES_PORT=<postgres_port>
REDIS_HOST=<redis_host> REDIS_HOST=<redis_host>
DEMO_MODE=<demo_mode> DEMO_MODE=<demo_mode>
DOCKER_TAG=<docker_tag>

View file

@ -1,6 +1,12 @@
import { fromPartial } from '@total-typescript/shoehorn';
import { EventDispatcher } from '../../src/server/core/EventDispatcher'; import { EventDispatcher } from '../../src/server/core/EventDispatcher';
global.fetch = jest.fn(); global.fetch = jest.fn();
// Mock global location
global.location = fromPartial({
hostname: 'localhost',
});
console.error = jest.fn(); console.error = jest.fn();
// Mock Logger // Mock Logger