Skip to content
Open
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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ jobs:
- name: Verify production build
run: npm run build -- --configuration production --aot=false

- name: Install Chromium
run: ./node_modules/.bin/playwright install --with-deps chromium --only-shell

- name: Run E2E tests
run: npm run test:e2e

- name: Upload E2E test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-test-results
path: nav-app/tmp/playwright/test-results/
retention-days: 7

- name: Archive code coverage results
uses: actions/upload-artifact@v4
if: always()
Expand Down
21 changes: 21 additions & 0 deletions nav-app/e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM node:22.22.2-bookworm
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

WORKDIR /app/nav-app
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
COPY nav-app/package.json nav-app/package-lock.json ./
RUN npm ci --ignore-scripts
RUN --mount=type=secret,id=extra_ca \
if [ -f /run/secrets/extra_ca ]; then export NODE_EXTRA_CA_CERTS=/run/secrets/extra_ca; fi; \
./node_modules/.bin/playwright install --with-deps chromium --only-shell

COPY nav-app/angular.json nav-app/tsconfig*.json nav-app/playwright.config.ts ./
COPY nav-app/src ./src
COPY nav-app/e2e ./e2e
COPY layers /app/layers
COPY *.md /app/
RUN npm run build -- --configuration production --aot=false

Check warning on line 16 in nav-app/e2e/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=mitre-attack_attack-navigator&issues=AaChFbY7QT1qxmW0tJTW&open=AaChFbY7QT1qxmW0tJTW&pullRequest=830

RUN mkdir -p tmp && chown node:node tmp
USER node

CMD ["npm", "run", "test:e2e"]
14 changes: 14 additions & 0 deletions nav-app/e2e/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
**
!*.md
!layers/
!layers/**
!nav-app/
!nav-app/package.json
!nav-app/package-lock.json
!nav-app/angular.json
!nav-app/tsconfig*.json
!nav-app/src/
!nav-app/src/**
!nav-app/playwright.config.ts
!nav-app/e2e/
!nav-app/e2e/**
62 changes: 62 additions & 0 deletions nav-app/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# End-to-end tests

Playwright runs the E2E suite in Chromium.

Run from `nav-app` with Node 22, npm, and Python 3 available:

```sh
npm ci --ignore-scripts
npx playwright install chromium --only-shell
npm run build -- --configuration production --aot=false
npm run test:e2e
```

Playwright starts and stops a local static server on port 4173. The suite uses the
production build in `dist/browser`; rebuild after application changes.

Downloaded JSON and failure traces are saved under `tmp/playwright/test-results`.
There are no retries. The CI quality gate runs the suite after the production
build on PRs targeting `develop` or `master`, and on pushes to `develop`. Failed runs upload
`tmp/playwright/test-results` as an artifact retained for seven days.

## Adding tests

Tag smoke tests with `{ tag: '@smoke' }`; run only those with `npm run test:e2e -- --grep @smoke`.

Add Playwright tests as `*.spec.ts` files in this directory.
Protractor `*.e2e-spec.ts` files are excluded from Playwright runs.

For tests that do not check font rendering, stub remote font stylesheet requests
with an empty CSS response so external font-service availability cannot affect
the result. See `layer.spec.ts` for an example.

## Docker

Run from the repository root:

```sh
docker build -f nav-app/e2e/Dockerfile -t attack-navigator-e2e:local .
docker run --rm --init attack-navigator-e2e:local
```

The image installs dependencies and Chromium, builds Navigator, and runs the same
suite. Source files are copied into the image; no host directories
are mounted. The local build and `node_modules` are not used. Test artifacts are
inside the container and are discarded by `--rm`.

Optional troubleshooting flags:

- Add `--platform linux/amd64` to both commands to match the GitHub runner's CPU
architecture when investigating differences between local and CI runs.
- Add `--shm-size=1g` to `docker run` to increase shared memory if Chromium crashes
because the container's default shared memory is insufficient.

If browser downloads fail with `SELF_SIGNED_CERT_IN_CHAIN`, set `CA_BUNDLE` to a
trusted PEM certificate bundle and supply it when building:

```sh
docker build --secret "id=extra_ca,src=$CA_BUNDLE" -f nav-app/e2e/Dockerfile -t attack-navigator-e2e:local .
```

The optional build secret sets `NODE_EXTRA_CA_CERTS` only for browser installation.
The bundle is not stored in the image, and TLS verification is enabled.
54 changes: 54 additions & 0 deletions nav-app/e2e/layer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { test, expect } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import config from '../src/assets/config.json';
import { configData, matrixSDO, TA0000, T0001, T0003 } from '../src/tests/utils/mock-data';

test('create, score, rename, and export an Enterprise layer', { tag: '@smoke' }, async ({ page }, testInfo) => {
const errors: string[] = [];
page.on('pageerror', error => errors.push(error.message));

// Reuse existing synthetic STIX fixtures; only data responses are replaced.
await page.route('**/assets/config.json', route => route.fulfill({
json: { ...config, collection_index_url: '', versions: configData },
}));
await page.route(configData.entries[0].domains[0].data[0], route => route.fulfill({
json: {
type: 'bundle',
id: 'bundle-smoke',
objects: [matrixSDO, TA0000, { ...T0001, name: 'Smoke technique' }, { ...T0003, name: 'Untouched technique' }],
},
}));
Comment on lines +10 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, but to me it is highlighting that our mock data that it uses here hasn't been updated in 2+ years and probably longer than that, since https://github.com/mitre-attack/attack-navigator/blob/master/nav-app/src/tests/utils/mock-data.ts has ATT&CK v13 in it which is from April 2023.

// Avoid an external font dependency; assertions use text and attributes, not icon appearance.
await page.route('https://fonts.googleapis.com/**', route => route.fulfill({ body: '', contentType: 'text/css' }));

await page.goto('/');
await page.getByText('Create New Layer', { exact: true }).click();
await page.getByRole('button', { name: 'Enterprise', exact: true }).click();
await expect(page.locator('technique-cell')).toHaveCount(2);
await page.locator('technique-cell').getByText('Smoke technique', { exact: true }).click();

await page.getByText('Technique Controls', { exact: true }).click();
await page.locator('span[alt="score"]').click();
await page.getByLabel('score', { exact: true }).fill('1');

await page.getByText('Layer Controls', { exact: true }).click();
await page.locator('span[alt="layer information"]').click();
await page.getByLabel('Name', { exact: true }).fill('Smoke layer');
await expect(page.locator('.tab-title.active')).toContainText('Smoke layer');

await page.locator('span[alt="export"]').click();
const downloadPromise = page.waitForEvent('download');
await page.locator('span[alt="save layer"]').filter({ hasText: /^code$/ }).click();
const download = await downloadPromise;
const file = testInfo.outputPath('layer.json');
await download.saveAs(file);
const layer = JSON.parse(await readFile(file, 'utf8'));
expect(layer).toMatchObject({
name: 'Smoke layer',
domain: 'enterprise-attack',
versions: { attack: '13', layer: '4.5', navigator: expect.any(String) },
});
expect(layer.techniques).toHaveLength(1);
expect(layer.techniques[0]).toMatchObject({ techniqueID: 'T0001', tactic: 'tactic-name', score: 1 });
expect(errors).toEqual([]);
});
46 changes: 46 additions & 0 deletions nav-app/package-lock.json

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

2 changes: 2 additions & 0 deletions nav-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"start": "ng serve --host 0.0.0.0",
"build": "ng build",
"test": "ng test",
"test:e2e": "playwright test",
"lint": "ng lint",
"prettier": "npx prettier src --check",
"prettier:fix": "npm run prettier -- --write"
Expand Down Expand Up @@ -55,6 +56,7 @@
"@angular/cli": "^19.2.23",
"@angular/compiler-cli": "^19.2.20",
"@angular/language-service": "^19.2.20",
"@playwright/test": "1.63.0",
"@types/jasmine": "^6.0.0",
"@types/jasminewd2": "^2.0.8",
"@types/node": "^22.19.17",
Expand Down
18 changes: 18 additions & 0 deletions nav-app/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
testMatch: '**/*.spec.ts',
outputDir: './tmp/playwright/test-results',
workers: 1,
retries: 0,
use: {
browserName: 'chromium',
baseURL: 'http://127.0.0.1:4173',
trace: 'retain-on-failure',
},
webServer: {
command: 'python3 -m http.server 4173 --bind 127.0.0.1 --directory dist/browser',
url: 'http://127.0.0.1:4173',
},
});
Loading