From 63d91628c8f415583b3d3c635c556fd010cbf991 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Fri, 10 May 2024 18:12:07 +0200 Subject: [PATCH 01/10] test(e2e): test publishing functionality --- .github/workflows/springwolf-plugins.yml | 11 +++ settings.gradle | 1 + springwolf-examples/e2e/.gitignore | 5 + springwolf-examples/e2e/build.gradle | 44 +++++++++ springwolf-examples/e2e/package-lock.json | 92 +++++++++++++++++++ springwolf-examples/e2e/package.json | 18 ++++ springwolf-examples/e2e/playwright.config.ts | 78 ++++++++++++++++ springwolf-examples/e2e/tests/basic.spec.ts | 47 ++++++++++ springwolf-examples/e2e/tests/publish.spec.ts | 55 +++++++++++ .../e2e/util/external_process.ts | 48 ++++++++++ springwolf-examples/e2e/util/page_object.ts | 18 ++++ .../docker-compose.yml | 1 - .../docker-compose.yml | 1 - .../springwolf-jms-example/docker-compose.yml | 1 - .../docker-compose.yml | 1 - .../springwolf-sns-example/docker-compose.yml | 1 - .../springwolf-sqs-example/docker-compose.yml | 1 - 17 files changed, 417 insertions(+), 6 deletions(-) create mode 100644 springwolf-examples/e2e/.gitignore create mode 100644 springwolf-examples/e2e/build.gradle create mode 100644 springwolf-examples/e2e/package-lock.json create mode 100644 springwolf-examples/e2e/package.json create mode 100644 springwolf-examples/e2e/playwright.config.ts create mode 100644 springwolf-examples/e2e/tests/basic.spec.ts create mode 100644 springwolf-examples/e2e/tests/publish.spec.ts create mode 100644 springwolf-examples/e2e/util/external_process.ts create mode 100644 springwolf-examples/e2e/util/page_object.ts diff --git a/.github/workflows/springwolf-plugins.yml b/.github/workflows/springwolf-plugins.yml index 9942e23cc..9557c320d 100644 --- a/.github/workflows/springwolf-plugins.yml +++ b/.github/workflows/springwolf-plugins.yml @@ -52,6 +52,17 @@ jobs: - name: Run build, check, analyzeDependencies on example run: ./gradlew -p ${{ env.example }} build + - name: Run e2e tests + run: ./gradlew -p springwolf-examples/e2e test + env: + SPRINGWOLF_EXAMPLE: ${{ matrix.plugin }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.plugin }} + path: springwolf-examples/e2e/playwright-report/ + retention-days: 14 + - name: Publish docker image if: github.ref == 'refs/heads/master' run: ./gradlew -p ${{ env.example }} dockerBuildImage dockerPushImage diff --git a/settings.gradle b/settings.gradle index 9e0d09246..eb3f908a4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ include( 'springwolf-plugins:springwolf-kafka-plugin', 'springwolf-plugins:springwolf-sns-plugin', 'springwolf-plugins:springwolf-sqs-plugin', + 'springwolf-examples:e2e', 'springwolf-examples:springwolf-amqp-example', 'springwolf-examples:springwolf-cloud-stream-example', 'springwolf-examples:springwolf-jms-example', diff --git a/springwolf-examples/e2e/.gitignore b/springwolf-examples/e2e/.gitignore new file mode 100644 index 000000000..68c5d18f0 --- /dev/null +++ b/springwolf-examples/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/springwolf-examples/e2e/build.gradle b/springwolf-examples/e2e/build.gradle new file mode 100644 index 000000000..3281f3e64 --- /dev/null +++ b/springwolf-examples/e2e/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'java' + id 'com.github.node-gradle.node' version '7.0.2' +} + +node { + version = '18.16.1' + npmVersion = '9.5.1' + download = true +} + +npm_run_test { + dependsOn spotlessCheck + + inputs.files fileTree("tests") + inputs.files fileTree("util") + inputs.file 'playwright.config.ts' + inputs.file 'package.json' + inputs.file 'package-lock.json' +} + +spotless { + encoding 'UTF-8' + + def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm' + def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node' + + format 'styling', { + target 'tests/**/*.ts', 'tests/**/*.js','util/**/*.ts', 'util/**/*.js' + + prettier() + .npmExecutable("${tasks.named('npmSetup').get().npmDir.get()}${npmExec}") + .nodeExecutable("${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}") + + licenseHeader("/* SPDX-License-Identifier: Apache-2.0 */", "import|export|.* \\{") + + trimTrailingWhitespace() + endWithNewline() + } +} + +tasks.named('spotlessStyling').configure { + it.dependsOn('nodeSetup', 'npmSetup') +} diff --git a/springwolf-examples/e2e/package-lock.json b/springwolf-examples/e2e/package-lock.json new file mode 100644 index 000000000..821d1c68d --- /dev/null +++ b/springwolf-examples/e2e/package-lock.json @@ -0,0 +1,92 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "hasInstallScript": true, + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.44.0", + "@types/node": "^20.12.11" + } + }, + "node_modules/@playwright/test": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz", + "integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==", + "dev": true, + "dependencies": { + "playwright": "1.44.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", + "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz", + "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==", + "dev": true, + "dependencies": { + "playwright-core": "1.44.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz", + "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/springwolf-examples/e2e/package.json b/springwolf-examples/e2e/package.json new file mode 100644 index 000000000..e5cec13db --- /dev/null +++ b/springwolf-examples/e2e/package.json @@ -0,0 +1,18 @@ +{ + "name": "e2e", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "playwright test --ui", + "test": "playwright test", + "postinstall": "npx playwright install --with-deps" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.44.0", + "@types/node": "^20.12.11" + } +} diff --git a/springwolf-examples/e2e/playwright.config.ts b/springwolf-examples/e2e/playwright.config.ts new file mode 100644 index 000000000..2ec0260b1 --- /dev/null +++ b/springwolf-examples/e2e/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:8080/springwolf/asyncapi-ui.html', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + +// { +// name: 'firefox', +// use: { ...devices['Desktop Firefox'] }, +// }, +// +// { +// name: 'webkit', +// use: { ...devices['Desktop Safari'] }, +// }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your loal dev server before starting the tests */ + webServer: { + cwd: '../springwolf-' + (process.env.SPRINGWOLF_EXAMPLE || 'kafka') + '-example', + command: 'docker compose up', + url: 'http://127.0.0.1:8080/springwolf/docs.json', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/springwolf-examples/e2e/tests/basic.spec.ts b/springwolf-examples/e2e/tests/basic.spec.ts new file mode 100644 index 000000000..afe877f87 --- /dev/null +++ b/springwolf-examples/e2e/tests/basic.spec.ts @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { test, expect } from "@playwright/test"; +import { spawnProcess } from "../util/external_process"; +import { + locateChannelItems, + locateChannels, + locatePublishButton, + locateSnackbar, +} from "../util/page_object"; + +test.beforeEach(async ({ page }) => { + await page.goto(""); +}); + +test("has title", async ({ page }) => { + await expect(page).toHaveTitle(/Springwolf/); +}); + +test("can click download and get asyncapi.json in new tab", async ({ + page, +}) => { + const newPagePromise = page.waitForEvent("popup"); + + await page.click("text=Download AsyncAPI file"); + await page.waitForTimeout(500); + + const newPage = await newPagePromise; + const content = await newPage.textContent("body pre"); + const asyncApiJson = JSON.parse(content!!); + + expect(asyncApiJson.info.title).toContain("Springwolf example project"); +}); + +test("has channels and channel item", async ({ page }) => { + expect(await locateChannelItems(page).count()).toBeGreaterThan(0); +}); + +test("can publish", async ({ page }) => { + const channelEntry = locateChannelItems(page).first(); + await channelEntry.click(); + + const button = locatePublishButton(channelEntry); + await button.click(); + + const snackBar = locateSnackbar(page); + await expect(snackBar).toContainText("Example payload sent"); +}); diff --git a/springwolf-examples/e2e/tests/publish.spec.ts b/springwolf-examples/e2e/tests/publish.spec.ts new file mode 100644 index 000000000..e36c5e910 --- /dev/null +++ b/springwolf-examples/e2e/tests/publish.spec.ts @@ -0,0 +1,55 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { test, expect } from "@playwright/test"; +import { + monitorDockerLogs, + MonitorDockerLogsResponse, +} from "../util/external_process"; +import { + locateChannelItems, + locateChannels, + locatePublishButton, + locateSnackbar, +} from "../util/page_object"; + +let process: MonitorDockerLogsResponse; +test.beforeAll(async () => { + process = monitorDockerLogs(); +}); + +test.beforeEach(async ({ page }) => { + await page.goto(""); + + console.log("---\nProcessMessages---\n", process.messages.join("\n")); + process.clearMessages(); + process.log = true; +}); + +test.afterAll(async () => { + expect(process.errors).toHaveLength(0); +}); + +test("can publish and receive with backend", async ({ page }) => { + const channelEntries = await locateChannelItems(page).all(); + + const index = 0; + + const channelEntry = channelEntries[index]; + await channelEntry.click(); + + const button = locatePublishButton(channelEntry); + await button.click(); + + await expect + .poll( + async () => + process.messages.filter((m) => m.includes("Publishing to")).length + ) + .toBeGreaterThanOrEqual(1); + await expect + .poll( + async () => + process.messages.filter((m) => m.includes("Received new message")) + .length + ) + .toBeGreaterThanOrEqual(1); +}); diff --git a/springwolf-examples/e2e/util/external_process.ts b/springwolf-examples/e2e/util/external_process.ts new file mode 100644 index 000000000..0f9fbb120 --- /dev/null +++ b/springwolf-examples/e2e/util/external_process.ts @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { spawn } from "child_process"; + +export interface MonitorDockerLogsResponse { + messages: string[]; + errors: string[]; + log: boolean; + clearMessages: () => void; +} + +export function monitorDockerLogs(): MonitorDockerLogsResponse { + const process = spawn("docker", ["compose", "logs", "-f", "app"], { + cwd: "../springwolf-kafka-example", + }); + + const response: MonitorDockerLogsResponse = { + messages: [] as string[], + errors: [] as string[], + log: false, + clearMessages: () => { + response.messages.splice(0, response.messages.length); + }, + }; + + process.stdout.on("data", (data) => { + const strData = data.toString(); + response.messages.push(strData); + + if (response.log) { + console.log(strData); + } + }); + + process.stderr.on("data", (data) => { + const strData = data.toString(); + response.errors.push(strData); + + if (response.log) { + console.log(strData); + } + }); + + process.on("close", (code) => { + console.error("Child exited with", code, "and stdout has been saved"); + }); + + return response; +} diff --git a/springwolf-examples/e2e/util/page_object.ts b/springwolf-examples/e2e/util/page_object.ts new file mode 100644 index 000000000..aea7aee16 --- /dev/null +++ b/springwolf-examples/e2e/util/page_object.ts @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { Locator, Page } from "@playwright/test"; + +export function locateChannels(locator: Page | Locator) { + return locator.locator("app-channels > mat-accordion"); +} + +export function locateChannelItems(locator: Page | Locator) { + return locator.locator("app-channels > mat-accordion > mat-expansion-panel"); +} + +export function locatePublishButton(locator: Locator) { + return locator.getByRole("button", { name: "Publish" }); +} + +export function locateSnackbar(locator: Page | Locator) { + return locator.locator("simple-snack-bar"); +} diff --git a/springwolf-examples/springwolf-amqp-example/docker-compose.yml b/springwolf-examples/springwolf-amqp-example/docker-compose.yml index 8725c0e38..3f100b0d6 100644 --- a/springwolf-examples/springwolf-amqp-example/docker-compose.yml +++ b/springwolf-examples/springwolf-amqp-example/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: app: image: stavshamir/springwolf-amqp-example:${SPRINGWOLF_VERSION} diff --git a/springwolf-examples/springwolf-cloud-stream-example/docker-compose.yml b/springwolf-examples/springwolf-cloud-stream-example/docker-compose.yml index c65ed0a53..5826f7a51 100644 --- a/springwolf-examples/springwolf-cloud-stream-example/docker-compose.yml +++ b/springwolf-examples/springwolf-cloud-stream-example/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: app: image: stavshamir/springwolf-cloud-stream-example:${SPRINGWOLF_VERSION} diff --git a/springwolf-examples/springwolf-jms-example/docker-compose.yml b/springwolf-examples/springwolf-jms-example/docker-compose.yml index ecab0a081..568924a06 100644 --- a/springwolf-examples/springwolf-jms-example/docker-compose.yml +++ b/springwolf-examples/springwolf-jms-example/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: app: image: stavshamir/springwolf-jms-example:${SPRINGWOLF_VERSION} diff --git a/springwolf-examples/springwolf-kafka-example/docker-compose.yml b/springwolf-examples/springwolf-kafka-example/docker-compose.yml index f120a5b48..d14a986ab 100644 --- a/springwolf-examples/springwolf-kafka-example/docker-compose.yml +++ b/springwolf-examples/springwolf-kafka-example/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: app: image: stavshamir/springwolf-kafka-example:${SPRINGWOLF_VERSION} diff --git a/springwolf-examples/springwolf-sns-example/docker-compose.yml b/springwolf-examples/springwolf-sns-example/docker-compose.yml index b648eff3e..e93a38fd7 100644 --- a/springwolf-examples/springwolf-sns-example/docker-compose.yml +++ b/springwolf-examples/springwolf-sns-example/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: app: image: stavshamir/springwolf-sns-example:${SPRINGWOLF_VERSION} diff --git a/springwolf-examples/springwolf-sqs-example/docker-compose.yml b/springwolf-examples/springwolf-sqs-example/docker-compose.yml index 94e8770fe..9ce2e76a3 100644 --- a/springwolf-examples/springwolf-sqs-example/docker-compose.yml +++ b/springwolf-examples/springwolf-sqs-example/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: app: image: stavshamir/springwolf-sqs-example:${SPRINGWOLF_VERSION} From adf5f40807529d80caa9f22d0447ea5f97c47b92 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Fri, 10 May 2024 18:57:02 +0200 Subject: [PATCH 02/10] test(e2e): various improvements --- .github/workflows/springwolf-plugins.yml | 4 +- springwolf-examples/e2e/build.gradle | 2 +- springwolf-examples/e2e/playwright.config.ts | 8 +- springwolf-examples/e2e/tests/basic.spec.ts | 13 -- springwolf-examples/e2e/tests/publish.spec.ts | 128 ++++++++++++------ springwolf-examples/e2e/util/example.ts | 30 ++++ .../e2e/util/external_process.ts | 3 +- springwolf-examples/e2e/util/page_object.ts | 14 +- .../kafka/consumers/ExampleConsumer.java | 4 +- .../src/main/resources/application.properties | 5 + .../resources/application-test.properties | 4 - .../controller/SpringwolfAmqpController.java | 2 +- .../channels/channels.component.html | 1 + .../app/components/info/info.component.html | 4 +- 14 files changed, 157 insertions(+), 65 deletions(-) create mode 100644 springwolf-examples/e2e/util/example.ts delete mode 100644 springwolf-examples/springwolf-kafka-example/src/test/resources/application-test.properties diff --git a/.github/workflows/springwolf-plugins.yml b/.github/workflows/springwolf-plugins.yml index 9557c320d..0fb024f1b 100644 --- a/.github/workflows/springwolf-plugins.yml +++ b/.github/workflows/springwolf-plugins.yml @@ -53,7 +53,7 @@ jobs: run: ./gradlew -p ${{ env.example }} build - name: Run e2e tests - run: ./gradlew -p springwolf-examples/e2e test + run: ./gradlew -p springwolf-examples/e2e npm_run_test env: SPRINGWOLF_EXAMPLE: ${{ matrix.plugin }} - uses: actions/upload-artifact@v4 @@ -61,7 +61,7 @@ jobs: with: name: playwright-report-${{ matrix.plugin }} path: springwolf-examples/e2e/playwright-report/ - retention-days: 14 + retention-days: 30 - name: Publish docker image if: github.ref == 'refs/heads/master' diff --git a/springwolf-examples/e2e/build.gradle b/springwolf-examples/e2e/build.gradle index 3281f3e64..5368a2cd4 100644 --- a/springwolf-examples/e2e/build.gradle +++ b/springwolf-examples/e2e/build.gradle @@ -26,7 +26,7 @@ spotless { def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node' format 'styling', { - target 'tests/**/*.ts', 'tests/**/*.js','util/**/*.ts', 'util/**/*.js' + target 'tests/**/*.ts', 'tests/**/*.js', 'util/**/*.ts', 'util/**/*.js' prettier() .npmExecutable("${tasks.named('npmSetup').get().npmDir.get()}${npmExec}") diff --git a/springwolf-examples/e2e/playwright.config.ts b/springwolf-examples/e2e/playwright.config.ts index 2ec0260b1..cdf148efc 100644 --- a/springwolf-examples/e2e/playwright.config.ts +++ b/springwolf-examples/e2e/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, devices } from '@playwright/test'; +import { getExampleProject } from './util/example'; /** * Read environment variables from file. @@ -28,6 +29,11 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + + screenshot: { + mode: 'on', + fullPage: true + }, }, /* Configure projects for major browsers */ @@ -70,7 +76,7 @@ export default defineConfig({ /* Run your loal dev server before starting the tests */ webServer: { - cwd: '../springwolf-' + (process.env.SPRINGWOLF_EXAMPLE || 'kafka') + '-example', + cwd: '../springwolf-' + getExampleProject() + '-example', command: 'docker compose up', url: 'http://127.0.0.1:8080/springwolf/docs.json', reuseExistingServer: !process.env.CI, diff --git a/springwolf-examples/e2e/tests/basic.spec.ts b/springwolf-examples/e2e/tests/basic.spec.ts index afe877f87..499d46304 100644 --- a/springwolf-examples/e2e/tests/basic.spec.ts +++ b/springwolf-examples/e2e/tests/basic.spec.ts @@ -1,9 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { test, expect } from "@playwright/test"; -import { spawnProcess } from "../util/external_process"; import { locateChannelItems, - locateChannels, locatePublishButton, locateSnackbar, } from "../util/page_object"; @@ -34,14 +32,3 @@ test("can click download and get asyncapi.json in new tab", async ({ test("has channels and channel item", async ({ page }) => { expect(await locateChannelItems(page).count()).toBeGreaterThan(0); }); - -test("can publish", async ({ page }) => { - const channelEntry = locateChannelItems(page).first(); - await channelEntry.click(); - - const button = locatePublishButton(channelEntry); - await button.click(); - - const snackBar = locateSnackbar(page); - await expect(snackBar).toContainText("Example payload sent"); -}); diff --git a/springwolf-examples/e2e/tests/publish.spec.ts b/springwolf-examples/e2e/tests/publish.spec.ts index e36c5e910..cc164e9cf 100644 --- a/springwolf-examples/e2e/tests/publish.spec.ts +++ b/springwolf-examples/e2e/tests/publish.spec.ts @@ -5,51 +5,103 @@ import { MonitorDockerLogsResponse, } from "../util/external_process"; import { + locateChannel, locateChannelItems, - locateChannels, locatePublishButton, locateSnackbar, } from "../util/page_object"; +import { getExampleAsyncApi, getExampleProject } from "../util/example"; -let process: MonitorDockerLogsResponse; -test.beforeAll(async () => { - process = monitorDockerLogs(); -}); +test.describe("Publishing for " + getExampleProject() + " plugin", () => { + test.skip( + ["cloud-stream", "sns"].includes(getExampleProject()), + "Plugin does not support publishing" + ); -test.beforeEach(async ({ page }) => { - await page.goto(""); + let dockerLogs: MonitorDockerLogsResponse; + test.beforeAll(async () => { + dockerLogs = monitorDockerLogs(); + }); - console.log("---\nProcessMessages---\n", process.messages.join("\n")); - process.clearMessages(); - process.log = true; -}); + test.beforeEach(async ({ page }) => { + await page.goto(""); -test.afterAll(async () => { - expect(process.errors).toHaveLength(0); -}); + console.log("---\nProcessMessages---\n", dockerLogs.messages.join("\n")); + dockerLogs.clearMessages(); + dockerLogs.log = true; + }); + + test.afterAll(async () => { + expect(dockerLogs.errors).toHaveLength(0); + }); + + test("shows success notification when publishing", async ({ page }) => { + const channelEntry = locateChannelItems(page).first(); + await channelEntry.click(); + + const button = locatePublishButton(channelEntry); + await button.click(); + + const snackBar = locateSnackbar(page); + await expect(snackBar).toContainText("Example payload sent"); + }); + + const operations = getExampleAsyncApi().operations; + Object.keys(operations).forEach((key: string) => { + const operation = operations[key]; + const action = operation.action; + const protocol = Object.keys(operation.bindings)[0]; + const channelName = operation.channel.$ref.split("/").pop(); + const payload = operation.messages[0].$ref + .split("/") + .pop() + .split(".") + .pop(); + + if ( + payload === "AnotherPayloadAvroDto" || + payload === "XmlPayloadDto" || + payload === "YamlPayloadDto" || + payload === "MonetaryAmount" || + payload === "StringConsumer$StringEnvelope" || + payload === "ExamplePayloadProtobufDto$Message" + ) { + return; // publishing is not possible for these + } + + test(channelName + " with " + payload, async ({ page }) => { + const channel = locateChannel( + page, + protocol, + channelName, + action, + payload + ); + await channel.click(); + + const button = locatePublishButton(channel); + await button.click(); + + await expect + .poll( + async () => + dockerLogs.messages + .filter((m) => m.includes("Publishing to")) + .filter((m) => m.includes(channelName)) + .filter((m) => m.includes(payload)).length + ) + .toBeGreaterThanOrEqual(1); -test("can publish and receive with backend", async ({ page }) => { - const channelEntries = await locateChannelItems(page).all(); - - const index = 0; - - const channelEntry = channelEntries[index]; - await channelEntry.click(); - - const button = locatePublishButton(channelEntry); - await button.click(); - - await expect - .poll( - async () => - process.messages.filter((m) => m.includes("Publishing to")).length - ) - .toBeGreaterThanOrEqual(1); - await expect - .poll( - async () => - process.messages.filter((m) => m.includes("Received new message")) - .length - ) - .toBeGreaterThanOrEqual(1); + if (action === "receive") { + await expect + .poll( + async () => + dockerLogs.messages + .filter((m) => m.includes("Received new message in")) + .filter((m) => m.includes(channelName)).length + ) + .toBeGreaterThanOrEqual(1); + } + }); + }); }); diff --git a/springwolf-examples/e2e/util/example.ts b/springwolf-examples/e2e/util/example.ts new file mode 100644 index 000000000..4ba723dcb --- /dev/null +++ b/springwolf-examples/e2e/util/example.ts @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import mockSpringwolfAmqp from "../../../springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json"; +import mockSpringwolfCloudStream from "../../../springwolf-examples/springwolf-cloud-stream-example/src/test/resources/asyncapi.json"; +import mockSpringwolfKafka from "../../../springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json"; +import mockSpringwolfSns from "../../../springwolf-examples/springwolf-sns-example/src/test/resources/asyncapi.json"; +import mockSpringwolfSqs from "../../../springwolf-examples/springwolf-sqs-example/src/test/resources/asyncapi.json"; +import mockSpringwolfJms from "../../../springwolf-examples/springwolf-jms-example/src/test/resources/asyncapi.json"; + +type ExampleProject = "amqp" | "cloud-stream" | "kafka" | "sns" | "sqs" | "jms"; + +export function getExampleProject(): ExampleProject { + return process.env.SPRINGWOLF_EXAMPLE || "amqp"; +} + +export function getExampleAsyncApi() { + switch (getExampleProject()) { + case "amqp": + return mockSpringwolfAmqp; + case "cloud-stream": + return mockSpringwolfCloudStream; + case "jms": + return mockSpringwolfJms; + case "kafka": + return mockSpringwolfKafka; + case "sns": + return mockSpringwolfSns; + case "sqs": + return mockSpringwolfSqs; + } +} diff --git a/springwolf-examples/e2e/util/external_process.ts b/springwolf-examples/e2e/util/external_process.ts index 0f9fbb120..ef61c9f87 100644 --- a/springwolf-examples/e2e/util/external_process.ts +++ b/springwolf-examples/e2e/util/external_process.ts @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { spawn } from "child_process"; +import { getExampleProject } from "./example"; export interface MonitorDockerLogsResponse { messages: string[]; @@ -10,7 +11,7 @@ export interface MonitorDockerLogsResponse { export function monitorDockerLogs(): MonitorDockerLogsResponse { const process = spawn("docker", ["compose", "logs", "-f", "app"], { - cwd: "../springwolf-kafka-example", + cwd: "../springwolf-" + getExampleProject() + "-example", }); const response: MonitorDockerLogsResponse = { diff --git a/springwolf-examples/e2e/util/page_object.ts b/springwolf-examples/e2e/util/page_object.ts index aea7aee16..b86700d6e 100644 --- a/springwolf-examples/e2e/util/page_object.ts +++ b/springwolf-examples/e2e/util/page_object.ts @@ -9,8 +9,20 @@ export function locateChannelItems(locator: Page | Locator) { return locator.locator("app-channels > mat-accordion > mat-expansion-panel"); } +export function locateChannel( + locator: Page | Locator, + protocol: string, + channelName: string, + action: string, + payload: string +) { + return locator.getByTestId( + "#channel-" + protocol + "-" + channelName + "-" + action + "-" + payload + ); +} + export function locatePublishButton(locator: Locator) { - return locator.getByRole("button", { name: "Publish" }); + return locator.getByRole("button", { name: "Publish", exact: true }); } export function locateSnackbar(locator: Page | Locator) { diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/ExampleConsumer.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/ExampleConsumer.java index 4fb741690..3bb822c7f 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/ExampleConsumer.java +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/ExampleConsumer.java @@ -20,7 +20,7 @@ public class ExampleConsumer { @KafkaListener(topics = "example-topic") public void receiveExamplePayload(ExamplePayloadDto payload) { - log.info("Received new message in example-queue: {}", payload.toString()); + log.info("Received new message in example-topic: {}", payload.toString()); AnotherPayloadDto example = new AnotherPayloadDto(); example.setExample(payload); @@ -31,6 +31,6 @@ public void receiveExamplePayload(ExamplePayloadDto payload) { @KafkaListener(topicPattern = "another-topic", groupId = "example-group-id", batch = "true") public void receiveAnotherPayloadBatched(List payloads) { - log.info("Received new messages in another-topic: {}", payloads.toString()); + log.info("Received new message in another-topic: {}", payloads.toString()); } } diff --git a/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties b/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties index 89c7ee71d..da3902ced 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties +++ b/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties @@ -54,4 +54,9 @@ springwolf.plugin.kafka.publishing.producer.properties.sasl.mechanism=PLAIN # For debugging purposes logging.level.io.github.springwolf=DEBUG +# Reduce/Disable kafka logging +logging.level.org.apache.kafka=ERROR +logging.level.kafka=ERROR +logging.level.state.change.logger=ERROR + diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/application-test.properties b/springwolf-examples/springwolf-kafka-example/src/test/resources/application-test.properties deleted file mode 100644 index 627efb39b..000000000 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/application-test.properties +++ /dev/null @@ -1,4 +0,0 @@ -# Reduce/Disable kafka logging in test -logging.level.org.apache.kafka=ERROR -logging.level.kafka=ERROR -logging.level.state.change.logger=ERROR \ No newline at end of file diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/controller/SpringwolfAmqpController.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/controller/SpringwolfAmqpController.java index 275d5402d..8a70be637 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/controller/SpringwolfAmqpController.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/controller/SpringwolfAmqpController.java @@ -29,7 +29,7 @@ protected boolean isEnabled() { @Override protected void publishMessage(String topic, MessageDto message, Object payload) { - log.debug("Publishing to amqp queue {}: {}", topic, message.getPayload()); + log.debug("Publishing to amqp queue {}: {}", topic, message); producer.send(topic, payload); } } diff --git a/springwolf-ui/src/app/components/channels/channels.component.html b/springwolf-ui/src/app/components/channels/channels.component.html index e179e1fe8..3df3f6a80 100644 --- a/springwolf-ui/src/app/components/channels/channels.component.html +++ b/springwolf-ui/src/app/components/channels/channels.component.html @@ -5,6 +5,7 @@

Channels

diff --git a/springwolf-ui/src/app/components/info/info.component.html b/springwolf-ui/src/app/components/info/info.component.html index 7bff0472b..2865a4192 100644 --- a/springwolf-ui/src/app/components/info/info.component.html +++ b/springwolf-ui/src/app/components/info/info.component.html @@ -3,7 +3,9 @@

{{ info?.title }}

API version {{ info?.version || "not specified" }} - - Download AsyncAPI file + Download AsyncAPI file

From bda94e03a3356f52c71bc0b9252d219ff867494a Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sun, 12 May 2024 02:46:51 +0200 Subject: [PATCH 03/10] test(e2e): various improvements --- .../core/controller/dtos/MessageDto.java | 4 +- springwolf-examples/e2e/tests/publish.spec.ts | 2 +- .../RabbitLifecycleConfiguration.java | 65 +++++++++++++++++++ .../amqp/producers/AnotherProducer.java | 2 +- .../examples/amqp/ApiIntegrationTest.java | 2 + .../examples/amqp/ProducerSystemTest.java | 2 + .../amqp/SpringContextIntegrationTest.java | 3 + ...AmqpExampleApplicationIntegrationTest.java | 2 + .../src/test/resources/asyncapi.json | 52 +++++++-------- .../controller/SpringwolfJmsController.java | 2 +- .../controller/SpringwolfSqsController.java | 2 +- 11 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java b/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java index d69523e09..74cc03e20 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java @@ -18,8 +18,8 @@ public class MessageDto { private final Map headers; @Builder.Default - private final String payload = EMPTY; + private final String payloadType = String.class.getCanonicalName(); @Builder.Default - private final String payloadType = String.class.getCanonicalName(); + private final String payload = EMPTY; } diff --git a/springwolf-examples/e2e/tests/publish.spec.ts b/springwolf-examples/e2e/tests/publish.spec.ts index cc164e9cf..0fafb3c55 100644 --- a/springwolf-examples/e2e/tests/publish.spec.ts +++ b/springwolf-examples/e2e/tests/publish.spec.ts @@ -69,7 +69,7 @@ test.describe("Publishing for " + getExampleProject() + " plugin", () => { return; // publishing is not possible for these } - test(channelName + " with " + payload, async ({ page }) => { + test(action + " " + channelName + " with " + payload, async ({ page }) => { const channel = locateChannel( page, protocol, diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java new file mode 100644 index 000000000..eec41e4bc --- /dev/null +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.examples.amqp.configuration; + +import org.springframework.amqp.AmqpConnectException; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("!test") +public class RabbitLifecycleConfiguration implements SmartLifecycle { + + private final ConnectionFactory connectionFactory; + private boolean isRunning = false; + + public RabbitLifecycleConfiguration(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + start(); + } + + @Override + public void start() { + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < 60000) { // 60000 milliseconds = 1 minute + try { + RabbitAdmin admin = new RabbitAdmin(connectionFactory); + admin.afterPropertiesSet(); // this will block until the connection is ready + + // Create a RabbitTemplate and execute a dummy operation to ensure the connection is open and ready + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.execute(channel -> { + // This is a dummy operation + return null; + }); + + isRunning = true; + break; // exit the loop if the connection is successfully opened + } catch (AmqpConnectException e) { + try { + Thread.sleep(5000); // 10000 milliseconds = 10 seconds + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Thread was interrupted", ie); + } + } + } + + if (!isRunning) { + throw new IllegalStateException("Unable to connect to amqp (Did you start all docker containers?)"); + } + } + + @Override + public void stop() { + isRunning = false; + } + + @Override + public boolean isRunning() { + return isRunning; + } +} diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java index 845c67c40..52cff853d 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/producers/AnotherProducer.java @@ -17,7 +17,7 @@ public class AnotherProducer { @AsyncPublisher( operation = @AsyncOperation( - channelName = "example-producer-channel-publisher", + channelName = "example-topic-exchange", description = "Custom, optional description defined in the AsyncPublisher annotation")) @AmqpAsyncOperationBinding() public void sendMessage(AnotherPayloadDto msg) { diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java index 37fba0302..26025d272 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ApiIntegrationTest.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.test.context.ActiveProfiles; import java.io.IOException; import java.io.InputStream; @@ -17,6 +18,7 @@ @SpringBootTest( classes = {SpringwolfAmqpExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") class ApiIntegrationTest { @Autowired diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java index d0b6cde9d..38554f727 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/ProducerSystemTest.java @@ -15,6 +15,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.junit.jupiter.Container; @@ -36,6 +37,7 @@ @SpringBootTest( classes = {SpringwolfAmqpExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") @Testcontainers @DirtiesContext @TestMethodOrder(OrderAnnotation.class) diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java index bb73de5de..1554babb9 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -25,6 +26,7 @@ public class SpringContextIntegrationTest { "springwolf.docket.servers.test-protocol.protocol=amqp", "springwolf.docket.servers.test-protocol.host=some-server:1234", }) + @ActiveProfiles("test") class ApplicationPropertiesConfigurationTest { @Autowired @@ -56,6 +58,7 @@ void testAllChannelsAreFound() { "springwolf.scanner.producer-data.enabled=false", "springwolf.plugin.amqp.scanner.rabbit-listener.enabled=false", }) + @ActiveProfiles("test") class DisabledScannerTest { @Autowired diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringwolfAmqpExampleApplicationIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringwolfAmqpExampleApplicationIntegrationTest.java index e8a6da493..f776b6a4b 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringwolfAmqpExampleApplicationIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringwolfAmqpExampleApplicationIntegrationTest.java @@ -5,10 +5,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest +@ActiveProfiles("test") class SpringwolfAmqpExampleApplicationIntegrationTest { @Autowired diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json index 2b64ebf1c..96162e3db 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json @@ -45,13 +45,6 @@ } } }, - "example-producer-channel-publisher": { - "messages": { - "io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto": { - "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" - } - } - }, "example-queue": { "messages": { "io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto": { @@ -72,6 +65,13 @@ } } }, + "example-topic-exchange": { + "messages": { + "io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + } + } + }, "example-topic-routing-key": { "messages": { "io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto": { @@ -268,48 +268,48 @@ } ] }, - "example-producer-channel-publisher_send_sendMessage": { - "action": "send", + "example-queue_receive_receiveExamplePayload": { + "action": "receive", "channel": { - "$ref": "#/channels/example-producer-channel-publisher" + "$ref": "#/channels/example-queue" }, - "title": "example-producer-channel-publisher_send", - "description": "Custom, optional description defined in the AsyncPublisher annotation", "bindings": { "amqp": { "expiration": 0, - "cc": [ ], - "priority": 0, - "deliveryMode": 1, - "mandatory": false, - "timestamp": false, - "ack": false, + "cc": [ + "example-queue" + ], "bindingVersion": "0.3.0" } }, "messages": [ { - "$ref": "#/channels/example-producer-channel-publisher/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + "$ref": "#/channels/example-queue/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" } ] }, - "example-queue_receive_receiveExamplePayload": { - "action": "receive", + "example-topic-exchange_send_sendMessage": { + "action": "send", "channel": { - "$ref": "#/channels/example-queue" + "$ref": "#/channels/example-topic-exchange" }, + "title": "example-topic-exchange_send", + "description": "Custom, optional description defined in the AsyncPublisher annotation", "bindings": { "amqp": { "expiration": 0, - "cc": [ - "example-queue" - ], + "cc": [ ], + "priority": 0, + "deliveryMode": 1, + "mandatory": false, + "timestamp": false, + "ack": false, "bindingVersion": "0.3.0" } }, "messages": [ { - "$ref": "#/channels/example-queue/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + "$ref": "#/channels/example-topic-exchange/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" } ] }, diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/springwolf/plugins/jms/controller/SpringwolfJmsController.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/springwolf/plugins/jms/controller/SpringwolfJmsController.java index d60a12213..6ae618150 100644 --- a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/springwolf/plugins/jms/controller/SpringwolfJmsController.java +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/springwolf/plugins/jms/controller/SpringwolfJmsController.java @@ -28,7 +28,7 @@ protected boolean isEnabled() { @Override protected void publishMessage(String topic, MessageDto message, Object payload) { - log.debug("Publishing to JMS queue {}: {}", topic, message); + log.debug("Publishing to jms queue {}: {}", topic, message); producer.send(topic, message.getHeaders(), payload); } } diff --git a/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/springwolf/plugins/sqs/controller/SpringwolfSqsController.java b/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/springwolf/plugins/sqs/controller/SpringwolfSqsController.java index 42b4c4f05..2d68cbb38 100644 --- a/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/springwolf/plugins/sqs/controller/SpringwolfSqsController.java +++ b/springwolf-plugins/springwolf-sqs-plugin/src/main/java/io/github/springwolf/plugins/sqs/controller/SpringwolfSqsController.java @@ -28,7 +28,7 @@ protected boolean isEnabled() { @Override protected void publishMessage(String topic, MessageDto message, Object payload) { - log.debug("Publishing to SQS queue {}: {}", topic, message); + log.debug("Publishing to sqs queue {}: {}", topic, message); producer.send(topic, payload); } } From cbd14364143435cfd2c580e41f09538d9643cf7d Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sun, 12 May 2024 02:56:53 +0200 Subject: [PATCH 04/10] test(e2e): various improvements --- springwolf-examples/e2e/README.md | 27 ++++++++ springwolf-examples/e2e/tests/basic.spec.ts | 10 ++- .../e2e/tests/exception.spec.ts | 23 +++++++ springwolf-examples/e2e/tests/publish.spec.ts | 39 ++++++----- .../e2e/util/external_process.ts | 19 ++++-- .../springwolf-amqp-example/build.gradle | 1 + .../configuration/RabbitConfiguration.java | 6 +- .../RabbitLifecycleConfiguration.java | 65 ------------------- .../RabbitMqReadinessVerifier.java | 46 +++++++++++++ .../amqp/consumers/ExampleConsumer.java | 7 +- .../src/test/resources/asyncapi.json | 12 ++-- 11 files changed, 154 insertions(+), 101 deletions(-) create mode 100644 springwolf-examples/e2e/README.md create mode 100644 springwolf-examples/e2e/tests/exception.spec.ts delete mode 100644 springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java create mode 100644 springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitMqReadinessVerifier.java diff --git a/springwolf-examples/e2e/README.md b/springwolf-examples/e2e/README.md new file mode 100644 index 000000000..f6132fb11 --- /dev/null +++ b/springwolf-examples/e2e/README.md @@ -0,0 +1,27 @@ +# End-to-End tests for Springwolf + +The end-to-end tests cover test cases that cannot be verified using junitk/jest unit/integration/system tests. +Typical examples are click interactions through `springwolf-ui`. + +## Usage + +This project uses playwright. + +To use the playwright ui, run +```bash +npm run start +``` + +For ci/cd or cli environments, use +```bash +npm run test +``` + +### Example project +The end-to-end tests are run against one example project. + +To test against the non-default project, specify the environment variable `SPRINGWOLF_EXAMPLE` +For example: +```bash +SPRINGWOLF_EXAMPLE=kafka npm run start +``` diff --git a/springwolf-examples/e2e/tests/basic.spec.ts b/springwolf-examples/e2e/tests/basic.spec.ts index 499d46304..e95495310 100644 --- a/springwolf-examples/e2e/tests/basic.spec.ts +++ b/springwolf-examples/e2e/tests/basic.spec.ts @@ -1,10 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { test, expect } from "@playwright/test"; -import { - locateChannelItems, - locatePublishButton, - locateSnackbar, -} from "../util/page_object"; +import { locateChannelItems } from "../util/page_object"; +import { getExampleAsyncApi } from "../util/example"; test.beforeEach(async ({ page }) => { await page.goto(""); @@ -14,7 +11,7 @@ test("has title", async ({ page }) => { await expect(page).toHaveTitle(/Springwolf/); }); -test("can click download and get asyncapi.json in new tab", async ({ +test("can click download and get original asyncapi.json in new tab", async ({ page, }) => { const newPagePromise = page.waitForEvent("popup"); @@ -27,6 +24,7 @@ test("can click download and get asyncapi.json in new tab", async ({ const asyncApiJson = JSON.parse(content!!); expect(asyncApiJson.info.title).toContain("Springwolf example project"); + expect(asyncApiJson).toStrictEqual(getExampleAsyncApi()); }); test("has channels and channel item", async ({ page }) => { diff --git a/springwolf-examples/e2e/tests/exception.spec.ts b/springwolf-examples/e2e/tests/exception.spec.ts new file mode 100644 index 000000000..a0610d4e5 --- /dev/null +++ b/springwolf-examples/e2e/tests/exception.spec.ts @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { test, expect } from "@playwright/test"; +import { + monitorDockerLogs, + MonitorDockerLogsResponse, + verifyNoErrorLogs, +} from "../util/external_process"; + +let dockerLogs: MonitorDockerLogsResponse; +test.beforeAll(async () => { + dockerLogs = monitorDockerLogs(); +}); +test.afterAll(async () => { + console.debug("---\nProcessMessages---\n", dockerLogs.messages.join("\n")); +}); + +test("no error nor warn log messages in logs", async ({ page }) => { + // Ensure that logs are available by calling the docs endpoint + await page.goto(""); + + verifyNoErrorLogs(dockerLogs); + expect(dockerLogs.errors).toHaveLength(0); +}); diff --git a/springwolf-examples/e2e/tests/publish.spec.ts b/springwolf-examples/e2e/tests/publish.spec.ts index 0fafb3c55..a2a7d6e9d 100644 --- a/springwolf-examples/e2e/tests/publish.spec.ts +++ b/springwolf-examples/e2e/tests/publish.spec.ts @@ -3,6 +3,7 @@ import { test, expect } from "@playwright/test"; import { monitorDockerLogs, MonitorDockerLogsResponse, + verifyNoErrorLogs, } from "../util/external_process"; import { locateChannel, @@ -12,13 +13,13 @@ import { } from "../util/page_object"; import { getExampleAsyncApi, getExampleProject } from "../util/example"; -test.describe("Publishing for " + getExampleProject() + " plugin", () => { +let dockerLogs: MonitorDockerLogsResponse; +test.describe("Publishing in " + getExampleProject() + " example", () => { test.skip( ["cloud-stream", "sns"].includes(getExampleProject()), - "Plugin does not support publishing" + "Example/Plugin does not support publishing" ); - let dockerLogs: MonitorDockerLogsResponse; test.beforeAll(async () => { dockerLogs = monitorDockerLogs(); }); @@ -26,13 +27,14 @@ test.describe("Publishing for " + getExampleProject() + " plugin", () => { test.beforeEach(async ({ page }) => { await page.goto(""); - console.log("---\nProcessMessages---\n", dockerLogs.messages.join("\n")); dockerLogs.clearMessages(); - dockerLogs.log = true; }); test.afterAll(async () => { expect(dockerLogs.errors).toHaveLength(0); + verifyNoErrorLogs(dockerLogs); + + console.debug("---\nProcessMessages---\n", dockerLogs.messages.join("\n")); }); test("shows success notification when publishing", async ({ page }) => { @@ -46,6 +48,10 @@ test.describe("Publishing for " + getExampleProject() + " plugin", () => { await expect(snackBar).toContainText("Example payload sent"); }); + testPublishingEveryChannelItem(); +}); + +function testPublishingEveryChannelItem() { const operations = getExampleAsyncApi().operations; Object.keys(operations).forEach((key: string) => { const operation = operations[key]; @@ -59,14 +65,15 @@ test.describe("Publishing for " + getExampleProject() + " plugin", () => { .pop(); if ( - payload === "AnotherPayloadAvroDto" || - payload === "XmlPayloadDto" || - payload === "YamlPayloadDto" || - payload === "MonetaryAmount" || - payload === "StringConsumer$StringEnvelope" || - payload === "ExamplePayloadProtobufDto$Message" + payload === "AnotherPayloadAvroDto" || // Avro publishing is not supported + payload === "XmlPayloadDto" || // Unable to create correct xml payload + payload === "YamlPayloadDto" || // Unable to create correct yaml payload + payload === "MonetaryAmount" || // Issue with either MonetaryAmount of ModelConverters + payload === "StringConsumer$StringEnvelope" || // Unable to instantiate class + payload === "ExamplePayloadProtobufDto$Message" || // Unable to instantiate class + channelName === "example-topic-routing-key" // Publishing through amqp exchange is not supported GH-366 ) { - return; // publishing is not possible for these + return; // skip } test(action + " " + channelName + " with " + payload, async ({ page }) => { @@ -88,7 +95,8 @@ test.describe("Publishing for " + getExampleProject() + " plugin", () => { dockerLogs.messages .filter((m) => m.includes("Publishing to")) .filter((m) => m.includes(channelName)) - .filter((m) => m.includes(payload)).length + .filter((m) => m.includes(payload)).length, + { message: "Expected publishing message in application logs" } ) .toBeGreaterThanOrEqual(1); @@ -98,10 +106,11 @@ test.describe("Publishing for " + getExampleProject() + " plugin", () => { async () => dockerLogs.messages .filter((m) => m.includes("Received new message in")) - .filter((m) => m.includes(channelName)).length + .filter((m) => m.includes(channelName)).length, + { message: "Expected receiving message in appliation logs" } ) .toBeGreaterThanOrEqual(1); } }); }); -}); +} diff --git a/springwolf-examples/e2e/util/external_process.ts b/springwolf-examples/e2e/util/external_process.ts index ef61c9f87..e9986be03 100644 --- a/springwolf-examples/e2e/util/external_process.ts +++ b/springwolf-examples/e2e/util/external_process.ts @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { spawn } from "child_process"; import { getExampleProject } from "./example"; +import { expect } from "@playwright/test"; export interface MonitorDockerLogsResponse { messages: string[]; @@ -24,8 +25,8 @@ export function monitorDockerLogs(): MonitorDockerLogsResponse { }; process.stdout.on("data", (data) => { - const strData = data.toString(); - response.messages.push(strData); + const strData = data.toString().split("\n") as string[]; + response.messages.push(...strData); if (response.log) { console.log(strData); @@ -33,8 +34,8 @@ export function monitorDockerLogs(): MonitorDockerLogsResponse { }); process.stderr.on("data", (data) => { - const strData = data.toString(); - response.errors.push(strData); + const strData = data.toString().split("\n") as string[]; + response.errors.push(...strData); if (response.log) { console.log(strData); @@ -47,3 +48,13 @@ export function monitorDockerLogs(): MonitorDockerLogsResponse { return response; } + +export function verifyNoErrorLogs(dockerLogs: MonitorDockerLogsResponse) { + const errorMessages = dockerLogs.messages + .filter((message) => message.includes("i.g.s")) // io.github.springwolf + .filter((message) => message.includes("ERROR") || message.includes("WARN")); + + expect(errorMessages, { + message: "Unexpected Springwolf ERROR or WARN log messages found", + }).toHaveLength(0); +} diff --git a/springwolf-examples/springwolf-amqp-example/build.gradle b/springwolf-examples/springwolf-amqp-example/build.gradle index a36dfaf0f..deab1538a 100644 --- a/springwolf-examples/springwolf-amqp-example/build.gradle +++ b/springwolf-examples/springwolf-amqp-example/build.gradle @@ -21,6 +21,7 @@ dependencies { runtimeOnly "org.springframework.boot:spring-boot-starter-web" implementation "org.springframework.amqp:spring-rabbit" + permitUsedUndeclared 'com.rabbitmq:amqp-client' implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" implementation "io.swagger.core.v3:swagger-annotations-jakarta:${swaggerVersion}" diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java index 4d3be59bc..2e92ce5e8 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java @@ -42,13 +42,13 @@ public Exchange exampleTopicExchange() { } @Bean - public Queue exampleTopicQueue() { + public Queue multiPayloadQueue() { return new Queue("multi-payload-queue"); } @Bean - public Binding exampleTopicBinding(Queue exampleTopicQueue, Exchange exampleTopicExchange) { - return BindingBuilder.bind(exampleTopicQueue) + public Binding exampleTopicBinding(Queue exampleBindingsQueue, Exchange exampleTopicExchange) { + return BindingBuilder.bind(exampleBindingsQueue) .to(exampleTopicExchange) .with("example-topic-routing-key") .noargs(); diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java deleted file mode 100644 index eec41e4bc..000000000 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitLifecycleConfiguration.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package io.github.springwolf.examples.amqp.configuration; - -import org.springframework.amqp.AmqpConnectException; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.context.SmartLifecycle; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; - -@Configuration -@Profile("!test") -public class RabbitLifecycleConfiguration implements SmartLifecycle { - - private final ConnectionFactory connectionFactory; - private boolean isRunning = false; - - public RabbitLifecycleConfiguration(ConnectionFactory connectionFactory) { - this.connectionFactory = connectionFactory; - start(); - } - - @Override - public void start() { - long startTime = System.currentTimeMillis(); - while (System.currentTimeMillis() - startTime < 60000) { // 60000 milliseconds = 1 minute - try { - RabbitAdmin admin = new RabbitAdmin(connectionFactory); - admin.afterPropertiesSet(); // this will block until the connection is ready - - // Create a RabbitTemplate and execute a dummy operation to ensure the connection is open and ready - RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); - rabbitTemplate.execute(channel -> { - // This is a dummy operation - return null; - }); - - isRunning = true; - break; // exit the loop if the connection is successfully opened - } catch (AmqpConnectException e) { - try { - Thread.sleep(5000); // 10000 milliseconds = 10 seconds - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Thread was interrupted", ie); - } - } - } - - if (!isRunning) { - throw new IllegalStateException("Unable to connect to amqp (Did you start all docker containers?)"); - } - } - - @Override - public void stop() { - isRunning = false; - } - - @Override - public boolean isRunning() { - return isRunning; - } -} diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitMqReadinessVerifier.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitMqReadinessVerifier.java new file mode 100644 index 000000000..5fd036021 --- /dev/null +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitMqReadinessVerifier.java @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.examples.amqp.configuration; + +import org.springframework.amqp.AmqpConnectException; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("!test") +public class RabbitMqReadinessVerifier { + + private static final int RABBITMQ_MAX_CONNECT_TIMEOUT_MS = 60000; + private static final int RABBITMQ_INTERVAL_WAIT_MS = 5000; + private final ConnectionFactory connectionFactory; + + public RabbitMqReadinessVerifier(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + + verifyRabbitMqIsUp(); + } + + private void verifyRabbitMqIsUp() { + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < RABBITMQ_MAX_CONNECT_TIMEOUT_MS) { + try { + RabbitAdmin admin = new RabbitAdmin(connectionFactory); + admin.afterPropertiesSet(); + + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.execute(channel -> null); // dummy operation throws exception when amqp is not up + + break; + } catch (AmqpConnectException e) { + try { + Thread.sleep(RABBITMQ_INTERVAL_WAIT_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("RabbitMQ is not running", ie); + } + } + } + } +} diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java index e764d2289..fddf77bc0 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java @@ -39,7 +39,7 @@ public void receiveAnotherPayload(AnotherPayloadDto payload) { @RabbitListener( bindings = { @QueueBinding( - exchange = @Exchange(name = "example-bindings-exchange-name", type = ExchangeTypes.TOPIC), + exchange = @Exchange(name = "example-topic-exchange", type = ExchangeTypes.TOPIC), value = @Queue( name = "example-bindings-queue", @@ -49,7 +49,10 @@ public void receiveAnotherPayload(AnotherPayloadDto payload) { key = "example-topic-routing-key") }) public void bindingsExample(AnotherPayloadDto payload) { - log.info("Received new message in example-bindings-queue: {}", payload.toString()); + log.info( + "Received new message in example-bindings-queue" + + " through exchange example-topic-exchange using routing key example-topic-routing-key: {}", + payload.toString()); } @RabbitListener(queues = "multi-payload-queue") diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json index 96162e3db..ae13a4eea 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json @@ -82,7 +82,7 @@ "amqp": { "is": "routingKey", "exchange": { - "name": "example-bindings-exchange-name", + "name": "example-topic-exchange", "type": "topic", "durable": true, "autoDelete": false, @@ -103,11 +103,11 @@ }, "bindings": { "amqp": { - "is": "routingKey", - "exchange": { - "name": "example-topic-exchange", - "type": "topic", + "is": "queue", + "queue": { + "name": "multi-payload-queue", "durable": true, + "exclusive": false, "autoDelete": false, "vhost": "/" }, @@ -342,7 +342,7 @@ "amqp": { "expiration": 0, "cc": [ - "example-topic-routing-key" + "multi-payload-queue" ], "bindingVersion": "0.3.0" } From cdb7bcb64c29396bd680787897de5ace256a6beb Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sun, 12 May 2024 14:14:08 +0200 Subject: [PATCH 05/10] test(e2e): various improvements --- .github/workflows/springwolf-plugins.yml | 2 +- springwolf-examples/e2e/README.md | 10 +- ...eption.spec.ts => application-log.spec.ts} | 0 springwolf-examples/e2e/tests/publish.spec.ts | 116 ---------------- .../e2e/tests/publishing.spec.ts | 128 ++++++++++++++++++ springwolf-examples/e2e/util/example.ts | 2 +- .../e2e/util/external_process.ts | 10 +- 7 files changed, 143 insertions(+), 125 deletions(-) rename springwolf-examples/e2e/tests/{exception.spec.ts => application-log.spec.ts} (100%) delete mode 100644 springwolf-examples/e2e/tests/publish.spec.ts create mode 100644 springwolf-examples/e2e/tests/publishing.spec.ts diff --git a/.github/workflows/springwolf-plugins.yml b/.github/workflows/springwolf-plugins.yml index 0fb024f1b..326c931a6 100644 --- a/.github/workflows/springwolf-plugins.yml +++ b/.github/workflows/springwolf-plugins.yml @@ -61,7 +61,7 @@ jobs: with: name: playwright-report-${{ matrix.plugin }} path: springwolf-examples/e2e/playwright-report/ - retention-days: 30 + retention-days: 14 - name: Publish docker image if: github.ref == 'refs/heads/master' diff --git a/springwolf-examples/e2e/README.md b/springwolf-examples/e2e/README.md index f6132fb11..3bea6cc5a 100644 --- a/springwolf-examples/e2e/README.md +++ b/springwolf-examples/e2e/README.md @@ -5,23 +5,27 @@ Typical examples are click interactions through `springwolf-ui`. ## Usage -This project uses playwright. +This project uses [playwright](https://playwright.dev). + +To install the dependencies, run `npm install` (or `../gradlew npmInstall`) To use the playwright ui, run ```bash npm run start ``` -For ci/cd or cli environments, use +For ci/cd or cli environments, use `../gradlew npm_run_test` or ```bash npm run test ``` ### Example project -The end-to-end tests are run against one example project. +The end-to-end tests are run against one example project, which is automatically started using docker. To test against the non-default project, specify the environment variable `SPRINGWOLF_EXAMPLE` For example: ```bash SPRINGWOLF_EXAMPLE=kafka npm run start ``` + +_Note: The example is re-used between tests. When switching example projects, the docker containers need to be stopped._ diff --git a/springwolf-examples/e2e/tests/exception.spec.ts b/springwolf-examples/e2e/tests/application-log.spec.ts similarity index 100% rename from springwolf-examples/e2e/tests/exception.spec.ts rename to springwolf-examples/e2e/tests/application-log.spec.ts diff --git a/springwolf-examples/e2e/tests/publish.spec.ts b/springwolf-examples/e2e/tests/publish.spec.ts deleted file mode 100644 index a2a7d6e9d..000000000 --- a/springwolf-examples/e2e/tests/publish.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* SPDX-License-Identifier: Apache-2.0 */ -import { test, expect } from "@playwright/test"; -import { - monitorDockerLogs, - MonitorDockerLogsResponse, - verifyNoErrorLogs, -} from "../util/external_process"; -import { - locateChannel, - locateChannelItems, - locatePublishButton, - locateSnackbar, -} from "../util/page_object"; -import { getExampleAsyncApi, getExampleProject } from "../util/example"; - -let dockerLogs: MonitorDockerLogsResponse; -test.describe("Publishing in " + getExampleProject() + " example", () => { - test.skip( - ["cloud-stream", "sns"].includes(getExampleProject()), - "Example/Plugin does not support publishing" - ); - - test.beforeAll(async () => { - dockerLogs = monitorDockerLogs(); - }); - - test.beforeEach(async ({ page }) => { - await page.goto(""); - - dockerLogs.clearMessages(); - }); - - test.afterAll(async () => { - expect(dockerLogs.errors).toHaveLength(0); - verifyNoErrorLogs(dockerLogs); - - console.debug("---\nProcessMessages---\n", dockerLogs.messages.join("\n")); - }); - - test("shows success notification when publishing", async ({ page }) => { - const channelEntry = locateChannelItems(page).first(); - await channelEntry.click(); - - const button = locatePublishButton(channelEntry); - await button.click(); - - const snackBar = locateSnackbar(page); - await expect(snackBar).toContainText("Example payload sent"); - }); - - testPublishingEveryChannelItem(); -}); - -function testPublishingEveryChannelItem() { - const operations = getExampleAsyncApi().operations; - Object.keys(operations).forEach((key: string) => { - const operation = operations[key]; - const action = operation.action; - const protocol = Object.keys(operation.bindings)[0]; - const channelName = operation.channel.$ref.split("/").pop(); - const payload = operation.messages[0].$ref - .split("/") - .pop() - .split(".") - .pop(); - - if ( - payload === "AnotherPayloadAvroDto" || // Avro publishing is not supported - payload === "XmlPayloadDto" || // Unable to create correct xml payload - payload === "YamlPayloadDto" || // Unable to create correct yaml payload - payload === "MonetaryAmount" || // Issue with either MonetaryAmount of ModelConverters - payload === "StringConsumer$StringEnvelope" || // Unable to instantiate class - payload === "ExamplePayloadProtobufDto$Message" || // Unable to instantiate class - channelName === "example-topic-routing-key" // Publishing through amqp exchange is not supported GH-366 - ) { - return; // skip - } - - test(action + " " + channelName + " with " + payload, async ({ page }) => { - const channel = locateChannel( - page, - protocol, - channelName, - action, - payload - ); - await channel.click(); - - const button = locatePublishButton(channel); - await button.click(); - - await expect - .poll( - async () => - dockerLogs.messages - .filter((m) => m.includes("Publishing to")) - .filter((m) => m.includes(channelName)) - .filter((m) => m.includes(payload)).length, - { message: "Expected publishing message in application logs" } - ) - .toBeGreaterThanOrEqual(1); - - if (action === "receive") { - await expect - .poll( - async () => - dockerLogs.messages - .filter((m) => m.includes("Received new message in")) - .filter((m) => m.includes(channelName)).length, - { message: "Expected receiving message in appliation logs" } - ) - .toBeGreaterThanOrEqual(1); - } - }); - }); -} diff --git a/springwolf-examples/e2e/tests/publishing.spec.ts b/springwolf-examples/e2e/tests/publishing.spec.ts new file mode 100644 index 000000000..635d43abf --- /dev/null +++ b/springwolf-examples/e2e/tests/publishing.spec.ts @@ -0,0 +1,128 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { test, expect } from "@playwright/test"; +import { + monitorDockerLogs, + MonitorDockerLogsResponse, + verifyNoErrorLogs, +} from "../util/external_process"; +import { + locateChannel, + locateChannelItems, + locatePublishButton, + locateSnackbar, +} from "../util/page_object"; +import { getExampleAsyncApi, getExampleProject } from "../util/example"; + +let dockerLogs: MonitorDockerLogsResponse; +test.describe("Publishing in " + getExampleProject() + " example", () => { + test.slow(); + test.skip( + ["cloud-stream", "sns"].includes(getExampleProject()), + "Example/Plugin does not support publishing" + ); + + test.beforeAll(async () => { + dockerLogs = monitorDockerLogs(); + }); + + test.beforeEach(async ({ page }) => { + await page.goto(""); + + dockerLogs.log = true; + }); + + test.afterEach(async () => { + verifyNoErrorLogs(dockerLogs); + + console.debug("---\nProcessMessages---\n", dockerLogs.messages.join("\n")); + }); + + test("shows success notification when publishing", async ({ page }) => { + const channelEntry = locateChannelItems(page).first(); + await channelEntry.click(); + + const button = locatePublishButton(channelEntry); + await button.click(); + + const snackBar = locateSnackbar(page); + await expect(snackBar).toContainText("Example payload sent"); + }); + + testPublishingEveryChannelItem(); +}); + +function testPublishingEveryChannelItem() { + const operations = getExampleAsyncApi().operations; + Object.keys(operations).forEach((key: string) => { + const operation = operations[key]; + operation.messages.forEach((messageReference) => { + const action = operation.action; + const protocol = Object.keys(operation.bindings)[0]; + const channelName = operation.channel.$ref.split("/").pop(); + const payload = messageReference.$ref.split("/").pop().split(".").pop(); // TODO: forEach payload + + if ( + payload === "AnotherPayloadAvroDto" || // Avro publishing is not supported + payload === "XmlPayloadDto" || // Unable to create correct xml payload + payload === "YamlPayloadDto" || // Unable to create correct yaml payload + payload === "MonetaryAmount" || // Issue with either MonetaryAmount of ModelConverters + payload === "StringConsumer$StringEnvelope" || // Unable to instantiate class + payload === "ExamplePayloadProtobufDto$Message" || // Unable to instantiate class + channelName === "example-topic-routing-key" // Publishing through amqp exchange is not supported, see GH-366 + ) { + return; // skip + } + + test( + action + " " + channelName + " with " + payload, + async ({ page }) => { + const channel = locateChannel( + page, + protocol, + channelName, + action, + payload + ); + await channel.click(); + + const button = locatePublishButton(channel); + await button.click(); + + await expect + .poll( + async () => { + const found = dockerLogs.messages + .filter((m) => m.includes("Publishing to")) + .filter((m) => m.includes(channelName)) + .filter((m) => m.includes(payload)).length; + console.debug("Polling for publish message and found=" + found); + return found; + }, + { message: "Expected publishing message in application logs" } + ) + .toBeGreaterThanOrEqual(1); + + if (action === "receive") { + await expect + .poll( + async () => { + const found = dockerLogs.messages + .filter((m) => m.includes("Received new message in")) + .filter((m) => m.includes(channelName)).length; + console.debug( + "Polling for receive message and found=" + found + ); + return found; + }, + { + message: "Expected receiving message in appliation logs", + timeout: 10_000, + } + ) + .toBeGreaterThanOrEqual(1); + } + } + ); + }); + }); +} diff --git a/springwolf-examples/e2e/util/example.ts b/springwolf-examples/e2e/util/example.ts index 4ba723dcb..4e0cc1f63 100644 --- a/springwolf-examples/e2e/util/example.ts +++ b/springwolf-examples/e2e/util/example.ts @@ -9,7 +9,7 @@ import mockSpringwolfJms from "../../../springwolf-examples/springwolf-jms-examp type ExampleProject = "amqp" | "cloud-stream" | "kafka" | "sns" | "sqs" | "jms"; export function getExampleProject(): ExampleProject { - return process.env.SPRINGWOLF_EXAMPLE || "amqp"; + return process.env.SPRINGWOLF_EXAMPLE || "kafka"; } export function getExampleAsyncApi() { diff --git a/springwolf-examples/e2e/util/external_process.ts b/springwolf-examples/e2e/util/external_process.ts index e9986be03..55d3c82ce 100644 --- a/springwolf-examples/e2e/util/external_process.ts +++ b/springwolf-examples/e2e/util/external_process.ts @@ -29,7 +29,7 @@ export function monitorDockerLogs(): MonitorDockerLogsResponse { response.messages.push(...strData); if (response.log) { - console.log(strData); + console.debug(strData); } }); @@ -38,23 +38,25 @@ export function monitorDockerLogs(): MonitorDockerLogsResponse { response.errors.push(...strData); if (response.log) { - console.log(strData); + console.debug(strData); } }); process.on("close", (code) => { - console.error("Child exited with", code, "and stdout has been saved"); + console.error("Child exited with ", code, " and stdout has been saved"); }); return response; } export function verifyNoErrorLogs(dockerLogs: MonitorDockerLogsResponse) { + expect(dockerLogs.errors).toHaveLength(0); + const errorMessages = dockerLogs.messages .filter((message) => message.includes("i.g.s")) // io.github.springwolf .filter((message) => message.includes("ERROR") || message.includes("WARN")); expect(errorMessages, { - message: "Unexpected Springwolf ERROR or WARN log messages found", + message: "expect: No Springwolf ERROR or WARN log messages found", }).toHaveLength(0); } From 72b45ca52044ed63cccc6790264e5cfb6b734dc2 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sun, 12 May 2024 14:34:12 +0200 Subject: [PATCH 06/10] test(e2e): various improvements (StringEnvelope) --- springwolf-examples/e2e/tests/publishing.spec.ts | 12 ++++++++---- .../examples/kafka/consumers/StringConsumer.java | 6 +++++- .../src/main/resources/application.properties | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/springwolf-examples/e2e/tests/publishing.spec.ts b/springwolf-examples/e2e/tests/publishing.spec.ts index 635d43abf..a0e6373f8 100644 --- a/springwolf-examples/e2e/tests/publishing.spec.ts +++ b/springwolf-examples/e2e/tests/publishing.spec.ts @@ -15,7 +15,6 @@ import { getExampleAsyncApi, getExampleProject } from "../util/example"; let dockerLogs: MonitorDockerLogsResponse; test.describe("Publishing in " + getExampleProject() + " example", () => { - test.slow(); test.skip( ["cloud-stream", "sns"].includes(getExampleProject()), "Example/Plugin does not support publishing" @@ -59,15 +58,20 @@ function testPublishingEveryChannelItem() { const action = operation.action; const protocol = Object.keys(operation.bindings)[0]; const channelName = operation.channel.$ref.split("/").pop(); - const payload = messageReference.$ref.split("/").pop().split(".").pop(); // TODO: forEach payload + const payload = messageReference.$ref + .split("/") + .pop() + .split(".") + .pop() + .split("$") + .pop(); if ( payload === "AnotherPayloadAvroDto" || // Avro publishing is not supported payload === "XmlPayloadDto" || // Unable to create correct xml payload payload === "YamlPayloadDto" || // Unable to create correct yaml payload payload === "MonetaryAmount" || // Issue with either MonetaryAmount of ModelConverters - payload === "StringConsumer$StringEnvelope" || // Unable to instantiate class - payload === "ExamplePayloadProtobufDto$Message" || // Unable to instantiate class + payload === "Message" || // Unable to instantiate ExamplePayloadProtobufDto$Message class channelName === "example-topic-routing-key" // Publishing through amqp exchange is not supported, see GH-366 ) { return; // skip diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/StringConsumer.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/StringConsumer.java index 885c8cb1b..117d3fc4a 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/StringConsumer.java +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/consumers/StringConsumer.java @@ -6,7 +6,9 @@ import io.github.springwolf.core.asyncapi.annotations.AsyncOperation; import io.github.springwolf.plugins.kafka.asyncapi.annotations.KafkaAsyncOperationBinding; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.annotation.KafkaListener; @@ -33,11 +35,13 @@ public void receiveStringPayload(String stringPayload) { } @Data + @AllArgsConstructor + @NoArgsConstructor static class StringEnvelope { @AsyncApiPayload @Schema( description = "Payload description using @Schema annotation and @AsyncApiPayload within envelope class", maxLength = 100) - private final String payload; + private String payload; } } diff --git a/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties b/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties index da3902ced..215a84849 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties +++ b/springwolf-examples/springwolf-kafka-example/src/main/resources/application.properties @@ -17,6 +17,8 @@ spring.kafka.consumer.group-id=example-group-id spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer spring.kafka.consumer.properties.spring.json.trusted.packages=io.github.springwolf.* +# when using KafkaListener topicPattern, spring will discover (new) topics only after a metadata refresh. Required for e2e +spring.kafka.consumer.properties.metadata.max.age.ms=5000 # if needed, authentication can be enabled as well: #spring.kafka.consumer.security.protocol=SASL_PLAINTEXT #spring.kafka.consumer.properties.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="broker" password="broker-secret"; From d461375abebc9617231c616750e911680e8ebbf4 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sun, 12 May 2024 22:34:25 +0200 Subject: [PATCH 07/10] test(e2e): cleanup --- springwolf-examples/e2e/playwright.config.ts | 5 --- .../e2e/tests/publishing.spec.ts | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/springwolf-examples/e2e/playwright.config.ts b/springwolf-examples/e2e/playwright.config.ts index cdf148efc..0186d0b2d 100644 --- a/springwolf-examples/e2e/playwright.config.ts +++ b/springwolf-examples/e2e/playwright.config.ts @@ -29,11 +29,6 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', - - screenshot: { - mode: 'on', - fullPage: true - }, }, /* Configure projects for major browsers */ diff --git a/springwolf-examples/e2e/tests/publishing.spec.ts b/springwolf-examples/e2e/tests/publishing.spec.ts index a0e6373f8..20af79c01 100644 --- a/springwolf-examples/e2e/tests/publishing.spec.ts +++ b/springwolf-examples/e2e/tests/publishing.spec.ts @@ -92,21 +92,29 @@ function testPublishingEveryChannelItem() { const button = locatePublishButton(channel); await button.click(); - await expect - .poll( - async () => { - const found = dockerLogs.messages - .filter((m) => m.includes("Publishing to")) - .filter((m) => m.includes(channelName)) - .filter((m) => m.includes(payload)).length; - console.debug("Polling for publish message and found=" + found); - return found; - }, - { message: "Expected publishing message in application logs" } - ) - .toBeGreaterThanOrEqual(1); - + await verifyMessageIsPublished(); if (action === "receive") { + await verifyMessageIsReceived(); + } + + async function verifyMessageIsPublished() { + await expect + .poll( + async () => { + const found = dockerLogs.messages + .filter((m) => m.includes("Publishing to")) + .filter((m) => m.includes(channelName)) + .filter((m) => m.includes(payload)).length; + console.debug( + "Polling for publish message and found=" + found + ); + return found; + }, + { message: "Expected publishing message in application logs" } + ) + .toBeGreaterThanOrEqual(1); + } + async function verifyMessageIsReceived() { await expect .poll( async () => { From e163c7120072394448478e519ff6b83b263212fb Mon Sep 17 00:00:00 2001 From: Timon Back Date: Sun, 12 May 2024 23:12:25 +0200 Subject: [PATCH 08/10] test(e2e): cleanup --- springwolf-examples/e2e/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/springwolf-examples/e2e/package.json b/springwolf-examples/e2e/package.json index e5cec13db..a16d7a77b 100644 --- a/springwolf-examples/e2e/package.json +++ b/springwolf-examples/e2e/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "playwright test --ui", "test": "playwright test", - "postinstall": "npx playwright install --with-deps" + "postinstall": "npx playwright install --with-deps chromium" }, "keywords": [], "author": "", From 2a6036763902d6f49fc0fe1621e98f4cf57de1cf Mon Sep 17 00:00:00 2001 From: Timon Back Date: Fri, 17 May 2024 15:10:09 +0200 Subject: [PATCH 09/10] test(ui): update info component test --- springwolf-ui/src/app/components/info/info.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/springwolf-ui/src/app/components/info/info.component.spec.ts b/springwolf-ui/src/app/components/info/info.component.spec.ts index 7a50dd59e..cab8e9a0c 100644 --- a/springwolf-ui/src/app/components/info/info.component.spec.ts +++ b/springwolf-ui/src/app/components/info/info.component.spec.ts @@ -58,7 +58,7 @@ describe("InfoComponent", function () { const compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector("h1").textContent).toContain("title"); expect(compiled.querySelector("h5").textContent).toContain( - " API version 1.0.0 - Download AsyncAPI file" + "API version 1.0.0" ); }); From dfa956289c5829420f45dc25bbf085ef66bde31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Fri, 17 May 2024 16:22:46 +0200 Subject: [PATCH 10/10] fix(e2e): fix after rebase Co-authored-by: Timon Back --- springwolf-examples/e2e/README.md | 4 ++-- springwolf-examples/e2e/tests/publishing.spec.ts | 14 +++++--------- springwolf-ui/src/app/models/example.model.ts | 12 +++++++----- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/springwolf-examples/e2e/README.md b/springwolf-examples/e2e/README.md index 3bea6cc5a..68fb94fc3 100644 --- a/springwolf-examples/e2e/README.md +++ b/springwolf-examples/e2e/README.md @@ -7,14 +7,14 @@ Typical examples are click interactions through `springwolf-ui`. This project uses [playwright](https://playwright.dev). -To install the dependencies, run `npm install` (or `../gradlew npmInstall`) +To install the dependencies, run `npm install` (or `../../gradlew npmInstall`) To use the playwright ui, run ```bash npm run start ``` -For ci/cd or cli environments, use `../gradlew npm_run_test` or +For ci/cd or cli environments, use `../../gradlew npm_run_test` or ```bash npm run test ``` diff --git a/springwolf-examples/e2e/tests/publishing.spec.ts b/springwolf-examples/e2e/tests/publishing.spec.ts index 20af79c01..373b403d3 100644 --- a/springwolf-examples/e2e/tests/publishing.spec.ts +++ b/springwolf-examples/e2e/tests/publishing.spec.ts @@ -51,20 +51,16 @@ test.describe("Publishing in " + getExampleProject() + " example", () => { }); function testPublishingEveryChannelItem() { - const operations = getExampleAsyncApi().operations; + const asyncApiDoc = getExampleAsyncApi(); + const operations = asyncApiDoc.operations; Object.keys(operations).forEach((key: string) => { const operation = operations[key]; operation.messages.forEach((messageReference) => { const action = operation.action; const protocol = Object.keys(operation.bindings)[0]; const channelName = operation.channel.$ref.split("/").pop(); - const payload = messageReference.$ref - .split("/") - .pop() - .split(".") - .pop() - .split("$") - .pop(); + const schemaType = messageReference.$ref.split("/").pop(); + const payload = asyncApiDoc.components.messages[schemaType]?.title; if ( payload === "AnotherPayloadAvroDto" || // Avro publishing is not supported @@ -104,7 +100,7 @@ function testPublishingEveryChannelItem() { const found = dockerLogs.messages .filter((m) => m.includes("Publishing to")) .filter((m) => m.includes(channelName)) - .filter((m) => m.includes(payload)).length; + .filter((m) => m.includes(schemaType)).length; console.debug( "Polling for publish message and found=" + found ); diff --git a/springwolf-ui/src/app/models/example.model.ts b/springwolf-ui/src/app/models/example.model.ts index 89f8c80f4..8192babcf 100644 --- a/springwolf-ui/src/app/models/example.model.ts +++ b/springwolf-ui/src/app/models/example.model.ts @@ -7,12 +7,14 @@ export class Example { constructor(exampleObject: object | string) { this.rawValue = exampleObject; - if (typeof exampleObject === "string") { - this.value = exampleObject; - } else if (Object.keys(exampleObject).length === 0) { - this.value = ""; + if (typeof exampleObject === "object") { + if (Object.keys(exampleObject).length > 0) { + this.value = JSON.stringify(exampleObject, null, 2); + } else { + this.value = ""; + } } else { - this.value = JSON.stringify(exampleObject, null, 2); + this.value = "" + exampleObject; } this.lineCount = this.value.split("\n").length;