Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion .github/workflows/build-and-deploy-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ jobs:
run: bun install
- name: lint sources
run: bun run eslint src/
- name: lint tests
run: bun run eslint tests/
- name: lint build scripts
run: bun run eslint build/
- name: build code and icon css
run: bun run build/build
# https://pico.sh/pgs#-headers
Expand All @@ -66,7 +70,7 @@ jobs:
with:
name: whisper-bundle
path: bundle/
# Deploys the bundle build of any non-main branch to pgs-te project
# Deploys the bundle build of any non-main branch to pgs-te project
# linked to whisper.te.syncorix.com
# See https://github.com/picosh/pgs-action
deploy-to-pgs-te:
Expand Down Expand Up @@ -95,6 +99,28 @@ jobs:
retain: 'syncorix-whisper-build-te'
# retention policy: num of recently updated projects to keep
retain_num: 0
# Execute end-to-end tests against hosted environemt
e2e-tests-te:
runs-on: ubuntu-latest
name: Execute end to end tests (TE)
needs: deploy-to-pgs-te
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: install dependencies
run: bun install
- name: Install playwright (expensive :/)
run: bun playwright install --with-deps
- name: Run tests
run: bun playwright test --project='te*'
- name: Upload test report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
# Deploys the bundle build of the main branch to pgs-qa project
# linked to whisper.qa.syncorix.com
# See https://github.com/picosh/pgs-action
Expand Down Expand Up @@ -124,6 +150,28 @@ jobs:
retain: 'syncorix-whisper-build-qa'
# retention policy: num of recently updated projects to keep
retain_num: 0
# Execute end-to-end tests against hosted environemt
e2e-tests-qa:
runs-on: ubuntu-latest
name: Execute end to end tests (QA)
needs: deploy-to-pgs-qa
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: install dependencies
run: bun install
- name: Install playwright (expensive :/)
run: bun playwright install --with-deps
- name: Run tests
run: bun playwright test --project='qa*'
- name: Upload test report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
# Deploys the bundle build of release tags to pgs-live project
# linked to whisper.syncorix.com
# See https://github.com/picosh/pgs-action
Expand Down Expand Up @@ -153,3 +201,25 @@ jobs:
retain: 'syncorix-whisper-build-live'
# retention policy: num of recently updated projects to keep
retain_num: 0
# Execute end-to-end tests against hosted environemt
e2e-tests-live:
runs-on: ubuntu-latest
name: Execute end to end tests (LIVE)
needs: deploy-to-pgs-live
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: install dependencies
run: bun install
- name: Install playwright (expensive :/)
run: bun playwright install --with-deps
- name: Run tests
run: bun playwright test --project='live*'
- name: Upload test report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ dist
*.tgz
local
bundle
playwright-report
test-results

# code coverage
coverage
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,24 @@ Please note that usually
bun src/index.html
```
should do the same but it compiled the jose lib without support for ```kty: 'EC``` for some reason.

## Testing
A couple of functional UI tests across major browser engine is included in [tests/](tests/). To execute locally use one of the follwoing:
### Launch the Test UI
```sh
bun playwright test --ui
```
### Run tests against dev environment (localhost:8080)
```sh
bun playwright test --project='dev*'
```
### Run tests against hosted environments te, qa or live
```sh
bun playwright test --project='<te|qa|live>*'
```
### Run tests against all environments
```sh
bun playwright test
```

See [test config](playwright.config.ts) for complete list of projects to execute and [Playwright](https://playwright.dev/) for doc
4 changes: 2 additions & 2 deletions build/build-iconify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { getIconsCSS } from '@iconify/utils';
import { locate } from '@iconify/json';
import { IconifyJSON } from '@iconify/types';

const iconSetName: string = 'mdi';
const iconSetName = 'mdi';
const targetFileName = 'src/css/' + iconSetName + '-icons.css'
const chosenIcons: Array<string> = [
const chosenIcons: string[] = [
'account-circle-outline',
'call-made',
'call-received',
Expand Down
9 changes: 9 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@iconify/json": "^2.2.319",
"@iconify/types": "^2.0.0",
"@iconify/utils": "^2.3.0",
"@playwright/test": "^1.51.1",
"@types/alpinejs": "^3.13.11",
"@types/bun": "latest",
"typescript-eslint": "^8.28.0",
Expand Down
61 changes: 61 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { defineConfig, devices } from '@playwright/test';

const localDevUrl = 'http://localhost:8080';
const teUrl = 'https://whisper.te.syncorix.com';
const qaUrl = 'https://whisper.qa.syncorix.com';
const liveUrl = 'https://whisper.syncorix.com';

export default defineConfig({
testDir: './tests',
reporter: 'html',
projects: [
{
name: 'dev - chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium', baseURL: localDevUrl },
},
{
name: 'dev - firefox',
use: { ...devices['Desktop Firefox'], baseURL: localDevUrl },
},
{
name: 'dev - webkit',
use: { ...devices['Desktop Safari'], baseURL: localDevUrl },
},
{
name: 'te - chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium', baseURL: teUrl },
},
{
name: 'te - firefox',
use: { ...devices['Desktop Firefox'], baseURL: teUrl },
},
{
name: 'te - webkit',
use: { ...devices['Desktop Safari'], baseURL: teUrl },
},
{
name: 'qa - chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium', baseURL: qaUrl },
},
{
name: 'qa - firefox',
use: { ...devices['Desktop Firefox'], baseURL: qaUrl },
},
{
name: 'qa - webkit',
use: { ...devices['Desktop Safari'], baseURL: qaUrl },
},
{
name: 'live - chromium',
use: { ...devices['Desktop Chrome'], channel: 'chromium', baseURL: liveUrl },
},
{
name: 'live - firefox',
use: { ...devices['Desktop Firefox'], baseURL: liveUrl },
},
{
name: 'live - webkit',
use: { ...devices['Desktop Safari'], baseURL: liveUrl },
},
],
});
11 changes: 11 additions & 0 deletions tests/menu-items-present.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test';

// Simple test for the expected menu items
test('test menu items are present', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('list').getByRole('link', { name: 'Send' })).toBeVisible();
await expect(page.getByRole('list').getByRole('link', { name: 'Receive' })).toBeVisible();
await expect(page.getByRole('list').getByRole('link', { name: 'You' })).toBeVisible();
await expect(page.getByRole('list').getByRole('link', { name: 'Info' })).toBeVisible();
await expect(page.getByRole('list').getByRole('link', { name: 'to GitHub' })).toBeVisible();
});
110 changes: 110 additions & 0 deletions tests/roundtrip-enc-dec.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { test, expect } from '@playwright/test';
import { Readable } from 'stream';

const publicKeyLabel = "__Playwright";

// a complete roundtrip test with encryption and decryption
test('test encrypt decrypt roundtrip', async ({ page }, testInfo) => {
await page.goto('/');

// Fill in key label
await expect(page.getByRole('textbox', { name: 'Hint about your identity' })).toBeVisible();
await page.getByRole('textbox', { name: 'Hint about your identity' }).click();
await page.getByRole('textbox', { name: 'Hint about your identity' }).pressSequentially(publicKeyLabel);

// Check and click OK button
await expect(page.getByRole('button', { name: 'Ok' })).toBeVisible();
await page.getByRole('button', { name: 'Ok' }).click();

// Check and click download button
await expect(page.getByRole('link', { name: publicKeyLabel })).toBeVisible();
const publicKeyDownloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: publicKeyLabel }).click();
const publicKeyDownload = await publicKeyDownloadPromise;
const publicKeyDownloadPath = await publicKeyDownload.path();
console.log('Saved public key %s as %s.', publicKeyDownload.suggestedFilename(), publicKeyDownloadPath);
// Screenshot - looks nice in report
await page.screenshot().then(sc => testInfo.attach('Public key download', { body: sc, contentType: 'image/png' }));

// Goto Send page
await page.getByRole('list').getByRole('link', { name: 'Send' }).click();
// Upload own public key as recipient
await expect(page.getByText('Drag & Drop your recipient')).toBeVisible();
await page.locator('id=add-recipient-dropzone').locator('id=add-recipient-dropzone-input').setInputFiles(publicKeyDownloadPath);
// Check label
await expect(page.getByText(publicKeyLabel).nth(0)).toBeVisible();

// Upload file to be encrypted
await expect(page.locator('id=whisper-dropzone').getByText('Drag & Drop your file(s) to')).toBeVisible();
await page.locator('id=whisper-dropzone').locator('id=whisper-dropzone-input').setInputFiles(publicKeyDownloadPath);

// Perform encryption
await expect(page.getByText('Ready? Your data will be encrypted locally on your device and can only be')).toBeVisible();
await expect(page.getByRole('link', { name: 'Go!' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Reset' })).toBeVisible();
await page.getByRole('link', { name: 'Go!' }).click();

// Download the encrypted file
const cryptogramDownloadLinkName = toAbbreviatedFileName(publicKeyDownloadPath);
await expect(page.getByRole('heading', { name: 'Complete!' })).toBeVisible();
await expect(page.getByRole('link', { name: cryptogramDownloadLinkName })).toBeVisible();
await expect(page.getByRole('link', { name: 'Clear' })).toBeVisible();
// Label from recipients should be hidden (1) but visible in dowload section (0)
await expect(page.getByRole('listitem').filter({ hasText: publicKeyLabel }).nth(0)).toBeVisible();
await expect(page.getByRole('listitem').filter({ hasText: publicKeyLabel }).nth(1)).toBeHidden();

const cryptogramDownloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: cryptogramDownloadLinkName }).click();
const cryptogramDownload = await cryptogramDownloadPromise;
const cryptogramDownloadPath = await cryptogramDownload.path();
console.log('Saved cryptogram %s as %s.', cryptogramDownload.suggestedFilename(), cryptogramDownloadPath);
// Screenshot - looks nice in report
await page.screenshot().then(sc => testInfo.attach('Encryption complete', { body: sc, contentType: 'image/png' }));

// Goto Receive page
await page.getByRole('list').getByRole('link', { name: 'Receive' }).click();
await expect(page.getByRole('heading', { name: 'What have you received?' })).toBeVisible();

// Upload encrypted file
await expect(page.locator('id=whisper-received-dropzone').getByText('Drag & Drop your file(s) to')).toBeVisible();
await page.locator('id=whisper-received-dropzone').locator('id=whisper-received-dropzone-input').setInputFiles(cryptogramDownloadPath);

// Perform descryption
await expect(page.getByRole('heading', { name: 'Ready?' })).toBeVisible();
await expect(page.getByText('Ready? Your data will be decrypted locally on your device and is never')).toBeVisible();
await expect(page.getByRole('link', { name: 'Go!' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Reset' })).toBeVisible();
await page.getByRole('link', { name: 'Go!' }).click();
await expect(page.getByRole('heading', { name: 'Complete!' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Clear' })).toBeVisible();

const decryptedDownloadLinkName = toAbbreviatedFileName(cryptogramDownloadPath);
const decryptedDownloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: decryptedDownloadLinkName }).click();
const decryptedDownload = await decryptedDownloadPromise;
const decryptedDownloadPath = await decryptedDownload.path();
console.log('Saved decrypted file %s as %s.', decryptedDownload.suggestedFilename(), decryptedDownloadPath);

// Screenshot - looks nice in report
await page.screenshot().then(sc => testInfo.attach('Decryption complete', { body: sc, contentType: 'image/png' }));

// Since we used the keyfile as content, we can now check whether the decrypted file is the public key
const publicKeyDownloadContent = await publicKeyDownload.createReadStream().then(readFromStream);
const decryptedDownloadContent = await decryptedDownload.createReadStream().then(readFromStream);
expect(decryptedDownloadContent).toBe(publicKeyDownloadContent);
});

function toAbbreviatedFileName(path: string): string {
return path.split('/').findLast(() => true)?.slice(0, 8) + '...';
}

async function readFromStream(stream: Readable): Promise<string> {
// Convert the stream into a string (file content)
let fileContent = '';
stream.on('data', (chunk) => {
fileContent += chunk.toString(); // Append chunk as string
});

// Wait for stream to finish and return content when promise resolves
return new Promise((resolve) => stream.on('end', resolve)).then(() => fileContent);
}