From 1b7d99c480103eba19460bca4414b5edaaa3dc38 Mon Sep 17 00:00:00 2001 From: Tiffany Forkner Date: Tue, 8 Oct 2024 10:15:20 -0400 Subject: [PATCH 001/105] in progress commit --- .github/workflows/e2e-test.yml | 37 +++++++++++++++ tests/smoke/playwright/.gitignore | 1 + tests/smoke/playwright/package.json | 3 +- tests/smoke/playwright/playwright.config.ts | 52 ++++++++++++++++++--- tests/smoke/playwright/tsconfig.json | 4 +- 5 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/e2e-test.yml diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 000000000..52de0807a --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,37 @@ +name: Template - Smoke Test + +on: + workflow_call: + # inputs: + # ENVIRONMENT: + # type: string + # description: "Github environment (dev|tst|stg|prod)" + # required: true +# pull_request: +# paths: +# - upload-server/** +# - tests/smoke/playwright/** + +defaults: + run: + working-directory: tests/smoke/playwright + +jobs: + e2e-test: + name: Playwright Tests + timeout-minutes: 60 + runs-on: ubuntu-latest + # environment: ${{ inputs.ENVIRONMENT }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v3 + - name: Install dependencies + run: npm ci + - name: Install playwright browsers + run: npx playwright install --with-deps + - name: Run tests + env: + CI: true + run: npx playwright test diff --git a/tests/smoke/playwright/.gitignore b/tests/smoke/playwright/.gitignore index b83ea2fae..ad13fe2ae 100644 --- a/tests/smoke/playwright/.gitignore +++ b/tests/smoke/playwright/.gitignore @@ -1,2 +1,3 @@ # Playwright test results test-results/ +test-reports/ diff --git a/tests/smoke/playwright/package.json b/tests/smoke/playwright/package.json index fec1cf736..d1068b93e 100644 --- a/tests/smoke/playwright/package.json +++ b/tests/smoke/playwright/package.json @@ -5,7 +5,8 @@ "main": "dist/main.js", "scripts": { "build": "npx tsc", - "test": "npx playwright test" + "test": "npx playwright test", + "server": "cd ../../../upload-server/ && go run ./... && cd ../tests/smoke/playwright/" }, "dependencies": { "dotenv": "^16.4.5", diff --git a/tests/smoke/playwright/playwright.config.ts b/tests/smoke/playwright/playwright.config.ts index e2ba410bd..469d6f17c 100644 --- a/tests/smoke/playwright/playwright.config.ts +++ b/tests/smoke/playwright/playwright.config.ts @@ -1,19 +1,58 @@ import { PlaywrightTestConfig, devices } from "@playwright/test"; +const baseURL = process.env.UI_URL ?? 'http://localhost:8081'; + const config: PlaywrightTestConfig = { // Specify the directory where your tests are located testDir: "./test", // Use this to change the number of browsers/contexts to run in parallel // Setting this to 1 will run tests serially which can help if you're seeing issues with parallel execution - workers: 1, + // Opt out of parallel tests on CI. + workers: process.env.CI ? 1 : undefined, + + // Fail the build on CI if you accidentally left test.only in the source code. + forbidOnly: !!process.env.CI, - // Configure retries for flaky tests - retries: 0, + // If a test fails, retry it additional 2 times + // Retry on CI only. + retries: process.env.CI ? 2 : 0, // Configure test timeout timeout: 30000, + // Reporter to use + reporter: process.env.CI + ? 'dot' + : [ + ['list'], + [ + 'html', + { + outputFolder: './test-reports/html', + open: 'never', + }, + ], + [ + 'json', + { + outputFile: './test-reports/test-report.json', + }, + ], + ], + + // Artifacts folder where screenshots, videos, and traces are stored. + outputDir: './test-results/', + + // Run your local dev server before starting the tests: + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + webServer: { + command: 'npm run server', + url: baseURL, + timeout: 360 * 1000, + reuseExistingServer: !process.env.CI, + }, + // Specify browser to use use: { // Specify browser to use. You can also use 'firefox' or 'webkit'. @@ -27,8 +66,10 @@ const config: PlaywrightTestConfig = { // Specify viewport size viewport: { width: 1280, height: 720 }, + // Specify the server url + baseURL, + // More options can be set here - baseURL: "http://localhost:8081", }, // Add any global setup or teardown in here @@ -43,9 +84,6 @@ const config: PlaywrightTestConfig = { }, // More projects can be configured here ], - - // Configure reporter here. 'dot', 'list', 'junit', etc. - reporter: [['list']] }; export default config; diff --git a/tests/smoke/playwright/tsconfig.json b/tests/smoke/playwright/tsconfig.json index 24200c83e..fafb6c779 100644 --- a/tests/smoke/playwright/tsconfig.json +++ b/tests/smoke/playwright/tsconfig.json @@ -1,11 +1,13 @@ { "compilerOptions": { + "strict": true, "module": "commonjs", "esModuleInterop": true, "target": "es6", "moduleResolution": "node", "sourceMap": true, - "outDir": "dist" + "outDir": "dist", + "forceConsistentCasingInFileNames": true }, "exclude": ["node_modules"] } From 686adaf9694e62a3862a9a3e63127a16273e2a44 Mon Sep 17 00:00:00 2001 From: Tiffany Forkner Date: Tue, 8 Oct 2024 12:57:07 -0400 Subject: [PATCH 002/105] temp --- tests/smoke/playwright/package.json | 2 +- upload-server/docker-compose.e2e.yml | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 upload-server/docker-compose.e2e.yml diff --git a/tests/smoke/playwright/package.json b/tests/smoke/playwright/package.json index d1068b93e..201131cc1 100644 --- a/tests/smoke/playwright/package.json +++ b/tests/smoke/playwright/package.json @@ -18,7 +18,7 @@ "license": "ISC", "devDependencies": { "@axe-core/playwright": "^4.10.0", - "@playwright/test": "^1.42.0", + "@playwright/test": "1.42.1", "@types/express": "^4.17.1", "@types/node": "^20.11.22", "ts-node": "^10.9.2", diff --git a/upload-server/docker-compose.e2e.yml b/upload-server/docker-compose.e2e.yml new file mode 100644 index 000000000..350ebb455 --- /dev/null +++ b/upload-server/docker-compose.e2e.yml @@ -0,0 +1,10 @@ +services: + playwright: + image: mcr.microsoft.com/playwright:v1.42.1-focal + volumes: + - ../tests/smoke/playwright:/app + ports: + - 9222:9222 + command: npx playwright test + depends_on: + - upload-server From df4456478af250fbc512064a22c29a241856eea8 Mon Sep 17 00:00:00 2001 From: Tiffany Forkner Date: Wed, 9 Oct 2024 12:47:20 -0400 Subject: [PATCH 003/105] created a docker-compose file for running playwright tests; set the playwright version to a specific value; updated tests to be more robust; added playwright tests to tus-upload-server-ci workflow; removed the endpoint field from the form because setting it is unnecessary and possibly destructive --- .github/workflows/tus-upload-server-ci.yml | 28 ++++++++++ tests/smoke/playwright/package-lock.json | 30 +++++------ tests/smoke/playwright/package.json | 5 +- tests/smoke/playwright/playwright.config.ts | 30 +++++------ .../test/upload-test-accessibility.spec.ts | 12 ++--- .../playwright/test/upload-test-e2e.spec.ts | 4 +- .../playwright/test/upload-test-pages.spec.ts | 51 ++++++++----------- upload-server/cmd/main.go | 2 +- upload-server/docker-compose.e2e.yml | 26 ++++++++-- upload-server/internal/appconfig/appconfig.go | 23 ++++++--- upload-server/internal/ui/assets/tusclient.js | 12 ++--- upload-server/internal/ui/ui.go | 24 +++++---- upload-server/internal/ui/upload.html | 13 +---- 13 files changed, 145 insertions(+), 115 deletions(-) diff --git a/.github/workflows/tus-upload-server-ci.yml b/.github/workflows/tus-upload-server-ci.yml index 06ec1f4dd..473c8b3ce 100644 --- a/.github/workflows/tus-upload-server-ci.yml +++ b/.github/workflows/tus-upload-server-ci.yml @@ -41,6 +41,34 @@ jobs: run: podman-compose -f docker-compose.yml -f docker-compose.azurite.yml -f docker-compose.minio.yml -f docker-compose.testing.yml up --exit-code-from upload-server - name: Show coverage run: go tool cover -func=c.out + e2e-test: + name: E2E Test + runs-on: ubuntu-latest + env: + CI: true + TEST_REPORTS_DIR: ./playwright-report + steps: + - uses: actions/checkout@v4 + - name: Install podman compse + run: pip3 install podman-compose + - name: Run Tests - File System + run: PLAYWRIGHT_JSON_OUTPUT_NAME=file-system-report.json podman-compose -f docker-compose.yml -f docker-compose.e2e.yml up --exit-code-from playwright + - name: Run Tests - Azure + run: PLAYWRIGHT_JSON_OUTPUT_NAME=azure-report.json podman-compose -f docker-compose.yml -f docker-compose.azurite.yml -f docker-compose.e2e.yml up --exit-code-from playwright + - name: Run Tests - AWS + run: PLAYWRIGHT_JSON_OUTPUT_NAME=aws-report.json podman-compose -f docker-compose.yml -f docker-compose.minio.yml -f docker-compose.e2e.yml up --exit-code-from playwright + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + - name: Concatenate reports + run: cat playwright-report/*-report.json > playwright-report.json + - uses: daun/playwright-report-summary@v3 + if: always() + with: + report-file: playwright-report.json run-fortify-scan: uses: ./.github/workflows/remote-cd-trigger-template.yml with: diff --git a/tests/smoke/playwright/package-lock.json b/tests/smoke/playwright/package-lock.json index 6a869369d..d04f267aa 100644 --- a/tests/smoke/playwright/package-lock.json +++ b/tests/smoke/playwright/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@axe-core/playwright": "^4.10.0", - "@playwright/test": "^1.42.0", + "@playwright/test": "1.48.0", "@types/express": "^4.17.1", "@types/node": "^20.11.22", "ts-node": "^10.9.2", @@ -72,18 +72,18 @@ "dev": true }, "node_modules/@playwright/test": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.0.tgz", - "integrity": "sha512-2k1HzC28Fs+HiwbJOQDUwrWMttqSLUVdjCqitBOjdCD0svWOMQUVqrXX6iFD7POps6xXAojsX/dGBpKnjZctLA==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0.tgz", + "integrity": "sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==", "dev": true, "dependencies": { - "playwright": "1.42.0" + "playwright": "1.48.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@tsconfig/node10": { @@ -374,33 +374,33 @@ "dev": true }, "node_modules/playwright": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.0.tgz", - "integrity": "sha512-Ko7YRUgj5xBHbntrgt4EIw/nE//XBHOKVKnBjO1KuZkmkhlbgyggTe5s9hjqQ1LpN+Xg+kHsQyt5Pa0Bw5XpvQ==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz", + "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==", "dev": true, "dependencies": { - "playwright-core": "1.42.0" + "playwright-core": "1.48.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.0.tgz", - "integrity": "sha512-0HD9y8qEVlcbsAjdpBaFjmaTHf+1FeIddy8VJLeiqwhcNqGCBe4Wp2e8knpqiYbzxtxarxiXyNDw2cG8sCaNMQ==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz", + "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/proper-lockfile": { diff --git a/tests/smoke/playwright/package.json b/tests/smoke/playwright/package.json index 201131cc1..3a09c3ab2 100644 --- a/tests/smoke/playwright/package.json +++ b/tests/smoke/playwright/package.json @@ -5,8 +5,7 @@ "main": "dist/main.js", "scripts": { "build": "npx tsc", - "test": "npx playwright test", - "server": "cd ../../../upload-server/ && go run ./... && cd ../tests/smoke/playwright/" + "test": "npx playwright test" }, "dependencies": { "dotenv": "^16.4.5", @@ -18,7 +17,7 @@ "license": "ISC", "devDependencies": { "@axe-core/playwright": "^4.10.0", - "@playwright/test": "1.42.1", + "@playwright/test": "1.48.0", "@types/express": "^4.17.1", "@types/node": "^20.11.22", "ts-node": "^10.9.2", diff --git a/tests/smoke/playwright/playwright.config.ts b/tests/smoke/playwright/playwright.config.ts index 469d6f17c..192c56eb5 100644 --- a/tests/smoke/playwright/playwright.config.ts +++ b/tests/smoke/playwright/playwright.config.ts @@ -1,6 +1,8 @@ import { PlaywrightTestConfig, devices } from "@playwright/test"; const baseURL = process.env.UI_URL ?? 'http://localhost:8081'; +const testReportDir = process.env.TEST_REPORTS_DIR ?? './test-reports'; +const testResultsDir = process.env.TEST_RESULTS_DIR ?? './test-results'; const config: PlaywrightTestConfig = { // Specify the directory where your tests are located @@ -9,7 +11,7 @@ const config: PlaywrightTestConfig = { // Use this to change the number of browsers/contexts to run in parallel // Setting this to 1 will run tests serially which can help if you're seeing issues with parallel execution // Opt out of parallel tests on CI. - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 1 : 4, // Fail the build on CI if you accidentally left test.only in the source code. forbidOnly: !!process.env.CI, @@ -23,35 +25,33 @@ const config: PlaywrightTestConfig = { // Reporter to use reporter: process.env.CI - ? 'dot' - : [ + ? [ + ['github'], + [ + 'json', + { + outputFile: `${testReportDir}/test-report.json`, + }, + ], + ] : [ ['list'], [ 'html', { - outputFolder: './test-reports/html', + outputFolder: `${testReportDir}/html`, open: 'never', }, ], [ 'json', { - outputFile: './test-reports/test-report.json', + outputFile: `${testReportDir}/test-report.json`, }, ], ], // Artifacts folder where screenshots, videos, and traces are stored. - outputDir: './test-results/', - - // Run your local dev server before starting the tests: - // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests - webServer: { - command: 'npm run server', - url: baseURL, - timeout: 360 * 1000, - reuseExistingServer: !process.env.CI, - }, + outputDir: testResultsDir, // Specify browser to use use: { diff --git a/tests/smoke/playwright/test/upload-test-accessibility.spec.ts b/tests/smoke/playwright/test/upload-test-accessibility.spec.ts index d5c9c644a..42fc12384 100644 --- a/tests/smoke/playwright/test/upload-test-accessibility.spec.ts +++ b/tests/smoke/playwright/test/upload-test-accessibility.spec.ts @@ -7,8 +7,8 @@ const manifests = JSON.parse(JSON.stringify(require("./manifests.json"))) const axeRuleTags = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]; test.describe('Upload Landing Page', () => { - test('has accessible features when loaded', async ({ page }, testInfo) => { - await page.goto(`/`) + test('has accessible features when loaded', async ({ page }) => { + await page.goto(`/`, { waitUntil: 'load' }) const results = await new AxeBuilder({ page }) .withTags(axeRuleTags) .analyze(); @@ -18,9 +18,9 @@ test.describe('Upload Landing Page', () => { }); test.describe('Upload Manifest Page', () => { - manifests.forEach(({ dataStream, route }) => { - test(`Checks accessibility for individual mainfest page: ${dataStream} / ${route}`, async ({ page }) => { - await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`); + manifests.forEach(({ dataStream, route }: { dataStream: string, route: string}) => { + test(`Checks accessibility for individual manifest page: ${dataStream} / ${route}`, async ({ page }) => { + await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`, { waitUntil: 'load' }); const results = await new AxeBuilder({ page }) .withTags(axeRuleTags) .analyze(); @@ -31,7 +31,7 @@ test.describe('Upload Manifest Page', () => { test.describe('File Upload Page', () => { test(`Checks accessibliity for the upload page for the dextesting/testevent1 manifest`, async ({ page }) => { - await page.goto(`/manifest?data_stream_id=dextesting&data_stream_route=testevent1`); + await page.goto(`/manifest?data_stream_id=dextesting&data_stream_route=testevent1`, { waitUntil: 'load' }); await page.getByLabel('Sender Id').fill('Sender123'); await page.getByLabel('Data Producer Id').fill('Producer123'); await page.getByLabel('Jurisdiction').fill('Jurisdiction123'); diff --git a/tests/smoke/playwright/test/upload-test-e2e.spec.ts b/tests/smoke/playwright/test/upload-test-e2e.spec.ts index 96c997c70..badfd26ca 100644 --- a/tests/smoke/playwright/test/upload-test-e2e.spec.ts +++ b/tests/smoke/playwright/test/upload-test-e2e.spec.ts @@ -5,10 +5,10 @@ test.describe.configure({ mode: 'parallel' }); const manifests = JSON.parse(JSON.stringify(require("./manifests.json"))) test.describe("Upload API/UI", () => { - manifests.forEach(({ dataStream, route }) => { + manifests.forEach(({ dataStream, route }: { dataStream: string, route: string }) => { test(`can use the UI to upload a file for Data stream: ${dataStream} / Route: ${route}`, async ({ page }) => { - await page.goto(`/`); + await page.goto(`/`, { waitUntil: 'load' }); await page.getByLabel('Data Stream', {exact: true}).fill(dataStream); await page.getByLabel('Data Stream Route').fill(route); await page.getByRole('button', {name: /next/i }).click(); diff --git a/tests/smoke/playwright/test/upload-test-pages.spec.ts b/tests/smoke/playwright/test/upload-test-pages.spec.ts index 2d045f7f4..3e718ecab 100644 --- a/tests/smoke/playwright/test/upload-test-pages.spec.ts +++ b/tests/smoke/playwright/test/upload-test-pages.spec.ts @@ -6,7 +6,7 @@ const manifests = JSON.parse(JSON.stringify(require("./manifests.json"))) test.describe("Upload Landing Page", () => { test("has the expected elements to start a file upload process", async ({page}) => { - await page.goto(`/`); + await page.goto(`/`, { waitUntil: 'load' }); const nav = page.getByRole('navigation') await expect(nav.getByRole("link").and(nav.getByText('Skip to main content Upload'))).toBeHidden() await expect(nav.getByRole("link").and(nav.getByText('Upload'))).toBeVisible() @@ -19,9 +19,9 @@ test.describe("Upload Landing Page", () => { }); test.describe("Upload Manifest Page", () => { - manifests.forEach(({ dataStream, route }) => { + manifests.forEach(({ dataStream, route }: { dataStream: string, route: string }) => { test(`has the expected metadata elements for Data stream: ${dataStream} / Route: ${route}`, async ({ page }) => { - await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`); + await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`, { waitUntil: 'load' }); const nav = page.locator('nav') await expect(nav.getByRole("link").and(nav.getByText('Skip to main content'))).toBeHidden() await expect(nav.getByRole("link").and(nav.getByText('Upload'))).toBeVisible() @@ -59,12 +59,11 @@ test.describe("Upload Manifest Page", () => { }); test.describe("File Uploader Page", () => { - test("has the expected elements to prepare to upload a file", async ({ page, baseURL }) => { - const apiURL = baseURL.replace('8081', '8080') + test("has the expected elements to prepare to upload a file", async ({ page }) => { const dataStream = 'dextesting'; const route = 'testevent1'; - await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`); + await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`, { waitUntil: 'load' }); await page.getByLabel('Sender Id').fill('Sender123') await page.getByLabel('Data Producer Id').fill('Producer123') @@ -73,10 +72,7 @@ test.describe("File Uploader Page", () => { await page.getByRole('button', { name: /next/i }).click(); await expect(page.getByRole('heading', { level: 1, includeHidden: false }).nth(0)).toHaveText('File Uploader') - const uploadEndpoint = page.getByRole('textbox', { name: 'Upload Endpoint' }); - // not the greatest way to interpret the endpoint value here, but this will have to work for now... - await expect(uploadEndpoint).toHaveValue(`${apiURL}/files/`) - + const chunkSize = page.getByLabel('Chunk size (bytes)'); const chunkSizeLabel = page.locator('label', { hasText: 'Chunk size (bytes)' }) await expect(chunkSizeLabel).toContainText('Note: Chunksize should be set on the client for uploading files of large size (1GB or over).') @@ -99,8 +95,9 @@ test.describe("Upload Status Page", () => { const expectedSender = 'Sender123' const expectedDataProducer = 'Producer123' const expectedJurisdiction = 'Jurisdiction123' + const targets = ['edav', 'ehdi', 'eicr', 'ncird'] - await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`); + await page.goto(`/manifest?data_stream_id=${dataStream}&data_stream_route=${route}`, { waitUntil: 'load'}); await page.getByLabel('Sender Id').fill(expectedSender) await page.getByLabel('Data Producer Id').fill(expectedDataProducer) @@ -130,6 +127,11 @@ test.describe("Upload Status Page", () => { await expect((await uploadHeadResponsePromise).ok()).toBeTruthy() await page.reload(); + var refreshes = 0; + while (await page.locator('.file-delivery-container').count() < targets.length && refreshes < 3) { + await page.reload(); + refreshes++ + } const fileHeaderContainer= page.locator('.file-header-container') await expect(fileHeaderContainer.getByRole('heading', { level: 1 }).nth(0)).toHaveText(expectedFileName) @@ -139,25 +141,14 @@ test.describe("Upload Status Page", () => { const fileDeliveriesContainer = page.locator('.file-deliveries-container'); await expect(fileDeliveriesContainer.getByRole('heading', { level: 2 }).nth(0)).toHaveText('Delivery Status') - const fileDeliveryEdavContainer = page.locator('.file-delivery-container').nth(0) - await expect(fileDeliveryEdavContainer.getByRole('heading', { level: 2 })).toHaveText('EDAV') - await expect(fileDeliveryEdavContainer.getByRole('heading', { level: 3 })).toHaveText('Delivery Status: SUCCESS') - await expect(fileDeliveryEdavContainer).toContainText(`Location: uploads/edav/${uploadId}`) - - const fileDeliveryEhdiContainer = page.locator('.file-delivery-container').nth(1) - await expect(fileDeliveryEhdiContainer.getByRole('heading', { level: 2 })).toHaveText('EHDI') - await expect(fileDeliveryEhdiContainer.getByRole('heading', { level: 3 })).toHaveText('Delivery Status: SUCCESS') - await expect(fileDeliveryEhdiContainer).toContainText(`Location: uploads/ehdi/${uploadId}`) - - const fileDeliveryEicrContainer = page.locator('.file-delivery-container').nth(2) - await expect(fileDeliveryEicrContainer.getByRole('heading', { level: 2 })).toHaveText('EICR') - await expect(fileDeliveryEicrContainer.getByRole('heading', { level: 3 })).toHaveText('Delivery Status: SUCCESS') - await expect(fileDeliveryEicrContainer).toContainText(`Location: uploads/eicr/${uploadId}`) - - const fileDeliveryNcirdContainer = page.locator('.file-delivery-container').nth(3) - await expect(fileDeliveryNcirdContainer.getByRole('heading', { level: 2 })).toHaveText('NCIRD') - await expect(fileDeliveryNcirdContainer.getByRole('heading', { level: 3 })).toHaveText('Delivery Status: SUCCESS') - await expect(fileDeliveryNcirdContainer).toContainText(`Location: uploads/ncird/${uploadId}`) + targets.forEach((target, index) => { + (async () => { + const fileDeliveryContainer = page.locator('.file-delivery-container').nth(index) + await expect(fileDeliveryContainer.getByRole('heading', { level: 2 })).toHaveText(target.toUpperCase()) + await expect(fileDeliveryContainer.getByRole('heading', { level: 3 })).toHaveText('Delivery Status: SUCCESS') + await expect(fileDeliveryContainer).toContainText(`Location: uploads/${target}/${uploadId}`) + })() + }) const uploadDetailsContainer = page.locator('.file-details-container') await expect(uploadDetailsContainer.getByRole('heading', { level: 2 })).toHaveText('Upload Details') diff --git a/upload-server/cmd/main.go b/upload-server/cmd/main.go index aa623b67c..13c0f8ad1 100644 --- a/upload-server/cmd/main.go +++ b/upload-server/cmd/main.go @@ -130,7 +130,7 @@ func main() { mainWaitGroup.Add(1) go func() { defer mainWaitGroup.Done() - if err := ui.Start(appConfig.UIPort, appConfig.CsrfToken, appConfig.TusUIFileEndpointUrl, appConfig.TusUIInfoEndpointUrl); err != nil { + if err := ui.Start(appConfig.UIPort, appConfig.CsrfToken, appConfig.ServerFileEndpointUrl, appConfig.ServerInfoEndpointUrl); err != nil { slog.Error("failed to start ui", "error", err) os.Exit(appMainExitCode) } diff --git a/upload-server/docker-compose.e2e.yml b/upload-server/docker-compose.e2e.yml index 350ebb455..cd13707f2 100644 --- a/upload-server/docker-compose.e2e.yml +++ b/upload-server/docker-compose.e2e.yml @@ -1,10 +1,28 @@ services: playwright: - image: mcr.microsoft.com/playwright:v1.42.1-focal + image: mcr.microsoft.com/playwright:v1.48.0-focal volumes: - - ../tests/smoke/playwright:/app + - ../tests/smoke:/app + working_dir: /app/playwright ports: - 9222:9222 - command: npx playwright test + command: npx playwright test || exit 0 + # appending `|| exit 0` will wait until all of the workers have completed before exiting + environment: + - CI=${CI} + - UI_URL=http://upload:${UI_PORT:-8081} + - TEST_REPORTS_DIR=${TEST_REPORTS_DIR} + - TEST_RESULTS_DIR=${TEST_RESULTS_DIR} depends_on: - - upload-server + upload-server: + condition: service_healthy + upload-server: + hostname: upload + environment: + - SERVER_HOSTNAME=upload + healthcheck: + test: wget --no-verbose --tries=1 --spider http://upload:${UI_PORT:-8081} || exit 1 + interval: 60s + retries: 5 + start_period: 20s + timeout: 10s diff --git a/upload-server/internal/appconfig/appconfig.go b/upload-server/internal/appconfig/appconfig.go index eb06dcd0d..7978e94f1 100644 --- a/upload-server/internal/appconfig/appconfig.go +++ b/upload-server/internal/appconfig/appconfig.go @@ -44,7 +44,14 @@ type AppConfig struct { LoggerDebugOn bool `env:"LOGGER_DEBUG_ON"` // Server - ServerPort string `env:"SERVER_PORT, default=8080"` + ServerProtocol string `env:"SERVER_PROTOCOL, default=http"` + ServerHostname string `env:"SERVER_HOSTNAME, default=localhost"` + ServerPort string `env:"SERVER_PORT, default=8080"` + TusdHandlerBasePath string `env:"TUSD_HANDLER_BASE_PATH, default=/files/"` + TusdHandlerInfoPath string `env:"TUSD_HANDLER_INFO_PATH, default=/info/"` + ServerUrl string + ServerFileEndpointUrl string + ServerInfoEndpointUrl string //QUESTION: this is arbitrary so is it useful? Environment string `env:"ENVIRONMENT, default=DEV"` @@ -62,14 +69,9 @@ type AppConfig struct { LocalEicrFolder string `env:"LOCAL_EICR_FOLDER, default=./uploads/eicr"` LocalNcirdFolder string `env:"LOCAL_NCIRD_FOLDER, default=./uploads/ncird"` - // TUSD - TusdHandlerBasePath string `env:"TUSD_HANDLER_BASE_PATH, default=/files/"` - // UI - TusUIFileEndpointUrl string `env:"TUS_UI_FILE_ENDPOINT_URL, default=http://localhost:8080/files/"` - TusUIInfoEndpointUrl string `env:"TUS_UI_INFO_ENDPOINT_URL, default=http://localhost:8080/info/"` - UIPort string `env:"UI_PORT, default=:8081"` - CsrfToken string `env:"CSRF_TOKEN, default=SwVgY4SfiXNyXCT4U6AvLNURDYS7J+Y/V2j4ng2UVp0XwQY0IUELUT5J5b/FATcE"` + UIPort string `env:"UI_PORT, default=:8081"` + CsrfToken string `env:"CSRF_TOKEN, default=SwVgY4SfiXNyXCT4U6AvLNURDYS7J+Y/V2j4ng2UVp0XwQY0IUELUT5J5b/FATcE"` // WARNING: the default CsrfToken value is for local development use only, it needs to be replaced by a secret 32 byte string before being used in production // Processing Status @@ -281,6 +283,11 @@ func ParseConfig(ctx context.Context) (AppConfig, error) { ac.AzureConnection.ContainerEndpoint = fmt.Sprintf("https://%s.blob.core.windows.net", ac.AzureConnection.StorageName) } } + + ac.ServerUrl = fmt.Sprintf("%s://%s:%s", ac.ServerProtocol, ac.ServerHostname, ac.ServerPort) + ac.ServerFileEndpointUrl = ac.ServerUrl + ac.TusdHandlerBasePath + ac.ServerInfoEndpointUrl = ac.ServerUrl + ac.TusdHandlerInfoPath + LoadedConfig = &ac return ac, nil } // .ParseConfig diff --git a/upload-server/internal/ui/assets/tusclient.js b/upload-server/internal/ui/assets/tusclient.js index cbef7861b..3795dbf1d 100644 --- a/upload-server/internal/ui/assets/tusclient.js +++ b/upload-server/internal/ui/assets/tusclient.js @@ -181,7 +181,6 @@ async function submitUploadForm() { // one file at a time file = fileList[0]; - let endpoint; let chunkSize; let parallelUploads; if (previousUpload) { @@ -207,12 +206,8 @@ async function submitUploadForm() { ); return; } - ({ endpoint, chunkSize, parallelUploads } = metadata); + ({ chunkSize, parallelUploads } = metadata); } else { - // retrieve the values entered in the form - const endpointInput = document.querySelector("#endpoint"); - endpoint = endpointInput.value; - const chunkInput = document.querySelector("#chunksize"); chunkSize = parseInt(chunkInput.value, 10); if (Number.isNaN(chunkSize)) { @@ -229,13 +224,13 @@ async function submitUploadForm() { _toggleFormContainer(false); // Upload the file - await uploadFile(file, { endpoint, chunkSize, parallelUploads }); + await uploadFile(file, { chunkSize, parallelUploads }); } // Creates the tus client and uploads the file. // Handles onProgress, onSuccess, and onError. // Will resume an upload if one has already been started. -async function uploadFile(file, { endpoint, chunkSize, parallelUploads }) { +async function uploadFile(file, { chunkSize, parallelUploads }) { console.log(`start uploading file: ${file.name}`); // used to determine the duration @@ -256,6 +251,7 @@ async function uploadFile(file, { endpoint, chunkSize, parallelUploads }) { fileSize: file.size, fileLastModified: file.lastModified, endpoint, + uploadUrl, chunkSize, parallelUploads, }, diff --git a/upload-server/internal/ui/ui.go b/upload-server/internal/ui/ui.go index 21de361ff..609e1b3bd 100644 --- a/upload-server/internal/ui/ui.go +++ b/upload-server/internal/ui/ui.go @@ -97,11 +97,12 @@ type ManifestTemplateData struct { } type UploadTemplateData struct { - UploadUrl string - UploadStatus string - Info info.InfoResponse - Navbar components.Navbar - NewUploadBtn components.NewUploadBtn + UploadEndpoint string + UploadUrl string + UploadStatus string + Info info.InfoResponse + Navbar components.Navbar + NewUploadBtn components.NewUploadBtn } var StaticHandler = http.FileServer(http.FS(content)) @@ -240,18 +241,19 @@ func GetRouter(uploadUrl string, infoUrl string) *mux.Router { return } - uploadUrl, err := url.JoinPath(uploadUrl, id) + uploadDestinationUrl, err := url.JoinPath(uploadUrl, id) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } err = uploadTemplate.Execute(rw, &UploadTemplateData{ - UploadUrl: uploadUrl, - Info: fileInfo, - UploadStatus: fileInfo.UploadStatus.Status, - Navbar: components.NewNavbar(true), - NewUploadBtn: components.NewUploadBtn{}, + UploadEndpoint: uploadUrl, + UploadUrl: uploadDestinationUrl, + Info: fileInfo, + UploadStatus: fileInfo.UploadStatus.Status, + Navbar: components.NewNavbar(true), + NewUploadBtn: components.NewUploadBtn{}, }) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) diff --git a/upload-server/internal/ui/upload.html b/upload-server/internal/ui/upload.html index 1e501cc52..de4710f3b 100644 --- a/upload-server/internal/ui/upload.html +++ b/upload-server/internal/ui/upload.html @@ -23,18 +23,6 @@

File Uploader

Resume Upload

-
- - - - - -