diff --git a/.env b/.env index d982682..6c92d11 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -CONSOLE_IMAGE=quay.io/securesign/rhtas-console@sha256:75966d60ed709af33efd48c53b96ea7b2fcd4608f90ccc56885bf224e34b55f5 -CONSOLE_UI_IMAGE=quay.io/securesign/rhtas-console-ui@sha256:c0b0b2d76548c05efadb2425baf93609cf6c40180f170cb531fbb7689a91db31 -CONSOLE_DB_IMAGE=registry.redhat.io/rhel9/mariadb-105@sha256:050dd5a7a32395b73b8680570e967e55050b152727412fdd73a25d8816e62d53 +CONSOLE_IMAGE=ghcr.io/securesign/rhtas-console:latest +CONSOLE_UI_IMAGE=ghcr.io/securesign/rhtas-console-ui:latest +CONSOLE_DB_IMAGE=docker.io/library/mariadb:10.5 diff --git a/.github/actions/start-console/action.yml b/.github/actions/start-console/action.yml new file mode 100644 index 0000000..3f8b242 --- /dev/null +++ b/.github/actions/start-console/action.yml @@ -0,0 +1,81 @@ +name: Start console +description: Start console using docker compose. +inputs: + ui_image: + description: image uri for the ui (ie. ghcr.io//:) + type: string + required: false + default: "" + server_image: + description: image uri for the server (ie. ghcr.io//:) + type: string + required: false + default: "" + server_db_image: + description: image uri for server-postgres (ie. ghcr.io//:) + type: string + required: false + default: "" + playwright_version: + description: version of the playwright image to run + type: string + required: false + default: "" +outputs: + server_port: + description: Port where the server is running + value: ${{ steps.set-output.outputs.server_port }} + ui_port: + description: Port where the UI is running + value: ${{ steps.set-output.outputs.ui_port }} + playwright_port: + description: Port where the UI is running + value: ${{ steps.set-output.outputs.playwright_port }} +runs: + using: "composite" + steps: + - name: Start console + working-directory: ${{ github.action_path }}/../../.. + shell: bash + run: | + opts="" + + if [ -n "${{ inputs.server_image }}" ]; then + opts="${opts} CONSOLE_IMAGE=${{ inputs.server_image }}" + fi + if [ -n "${{ inputs.ui_image }}" ]; then + opts="${opts} CONSOLE_UI_IMAGE=${{ inputs.ui_image }}" + fi + if [ -n "${{ inputs.server_db_image }}" ]; then + opts="${opts} POSTGRESQL_IMAGE=${{ inputs.server_db_image }}" + fi + + if [ -n "${{ inputs.playwright_version }}" ]; then + opts="${opts} PLAYWRIGHT_VERSION=${{ inputs.playwright_version }}" + fi + + echo "opts: $opts" + + eval "${opts} docker compose up -d" + + - name: Wait for services to be ready + shell: bash + run: | + # Wait for backend + until curl -s http://localhost:8087/healthz | jq -e '.status == "ok"' >/dev/null 2>&1; do + echo "Waiting for healthy service response on port 8087..." + sleep 2 + done + + # Wait for ui + until curl -s http://localhost:8088 | grep -qi "> $GITHUB_OUTPUT + echo "ui_port=8088" >> $GITHUB_OUTPUT + echo "playwright_port=5000" >> $GITHUB_OUTPUT diff --git a/.github/workflows/ci-e2e-template.yaml b/.github/workflows/ci-e2e-template.yaml new file mode 100644 index 0000000..6d66f3e --- /dev/null +++ b/.github/workflows/ci-e2e-template.yaml @@ -0,0 +1,147 @@ +name: Run e2e RHTAS Console CI tests + +on: + workflow_call: + inputs: + artifact: + description: | + The name of the component being tested, ie server etc. + Must correspond to an artifact storing the custom built image, named , + and should contain the file .tar inside. + required: false + type: string + ui_image: + description: image uri for the ui (ie. ghcr.io//:) + type: string + required: false + default: "" + server_image: + description: image uri for the server (ie. ghcr.io//:) + type: string + required: false + default: "" + server_db_image: + description: image uri for server-postgres (ie. ghcr.io//:) + type: string + required: false + default: "" + workflow_dispatch: + inputs: + artifact: + description: | + The name of the component being tested, ie server etc. + Must correspond to an artifact storing the custom built image, named , + and should contain the file .tar inside. + required: false + type: string + ui_image: + description: image uri for the ui (ie. ghcr.io//:) + type: string + required: false + default: "" + server_image: + description: image uri for the server (ie. ghcr.io//:) + type: string + required: false + default: "" + server_db_image: + description: image uri for server-postgres (ie. ghcr.io//:) + type: string + required: false + default: "" + +jobs: + check-images: + runs-on: ubuntu-latest + steps: + - name: Log in to registry + uses: docker/login-action@v3 + with: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io + + - name: Download artifact + if: "${{ inputs.artifact != '' }}" + uses: actions/download-artifact@v5 + with: + name: ${{ inputs.artifact }} + path: /tmp + - name: Load images + if: ${{ inputs.artifact != '' }} + run: | + docker load --input /tmp/${{ inputs.artifact }}.tar + - name: Check ui image exists + if: ${{ inputs.ui_image != '' }} + run: | + if docker image inspect ${{ inputs.ui_image }} >/dev/null 2>&1; then + echo "Image exists locally" + docker image inspect ${{ inputs.ui_image }} + else + echo "Image does not exist locally" + docker manifest inspect ${{ inputs.ui_image }} + fi + - name: Check server image exists + if: ${{ inputs.server_image != '' }} + run: | + if docker image inspect ${{ inputs.server_image }} >/dev/null 2>&1; then + echo "Image exists locally" + docker image inspect ${{ inputs.server_image }} + else + echo "Image does not exist locally" + docker manifest inspect ${{ inputs.server_image }} + fi + - name: Check server_db_image image exists + if: ${{ inputs.server_db_image != '' }} + run: | + if docker image inspect ${{ inputs.server_db_image }} >/dev/null 2>&1; then + echo "Image exists locally" + docker image inspect ${{ inputs.server_db_image }} + else + echo "Image does not exist locally" + docker manifest inspect ${{ inputs.server_db_image }} + fi + + e2e-integration-tests: + needs: check-images + runs-on: ubuntu-latest + steps: + - name: Download artifact + if: "${{ inputs.artifact != '' }}" + uses: actions/download-artifact@v5 + with: + name: ${{ inputs.artifact }} + path: /tmp + - name: Load images + if: ${{ inputs.artifact != '' }} + run: | + docker load --input /tmp/${{ inputs.artifact }}.tar + + - name: Checkout ui repo + uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "npm" + - name: Install dependencies + run: npm ci --verbose --ignore-scripts --no-audit + + - name: Start rhtas-console + uses: ./.github/actions/start-console + with: + ui_image: ${{ inputs.ui_image }} + server_image: ${{ inputs.server_image }} + server_db_image: ${{ inputs.server_db_image }} + + - name: Run Playwright tests + run: | + PW_TEST_CONNECT_WS_ENDPOINT=ws://localhost:5000/ CONSOLE_UI_URL=http://localhost:8088 AUTH_REQUIRED=false npm run -w e2e test + + - name: Upload Playwright artifacts + if: failure() # only upload if tests failed + uses: actions/upload-artifact@v4 + with: + name: playwright-artifacts + path: | + e2e/test-results + e2e/playwright-report diff --git a/.github/workflows/ci-e2e.yaml b/.github/workflows/ci-e2e.yaml new file mode 100644 index 0000000..1b1f52e --- /dev/null +++ b/.github/workflows/ci-e2e.yaml @@ -0,0 +1,79 @@ +name: CI (e2e) + +on: + push: + branches: + - "main" + - "release/*" + pull_request: + branches: + - "main" + - "release/*" + workflow_call: + merge_group: + +concurrency: + group: ci-e2e-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-upload-for-e2e-ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: save rhtas-console-ui image + run: | + docker build . -t ghcr.io/securesign/rhtas-console-ui:pr-test -f Dockerfile + docker save -o /tmp/rhtas-console-ui.tar ghcr.io/securesign/rhtas-console-ui:pr-test + + - name: Upload console-ui image as artifact + uses: actions/upload-artifact@v4 + with: + name: rhtas-console-ui + path: /tmp/rhtas-console-ui.tar + retention-days: 1 + + discover-envs-for-e2e-ci: + runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.set-outputs.outputs.image_tag }} + steps: + - name: Extract vars for Pull Request + shell: bash + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + env: + base_ref: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }} + run: | + branch=$base_ref + branch=$(echo ${branch#refs/heads/}) + image_tag="latest" + if [[ "$branch" != "main" ]]; then + image_tag="${branch#release/}" + fi + echo "image_tag=$image_tag" >> $GITHUB_ENV + - name: Extract vars for Push + shell: bash + if: ${{ github.event_name != 'pull_request' && github.event_name != 'merge_group' }} + run: | + branch=$(echo ${GITHUB_REF#refs/heads/}) + image_tag="latest" + if [[ "$branch" != "main" ]]; then + image_tag="${branch#release/}" + fi + echo "image_tag=$image_tag" >> $GITHUB_ENV + - name: Set outputs + id: set-outputs + run: | + echo ${{ env.image_tag }} + echo "image_tag=${{ env.image_tag }}" >> "$GITHUB_OUTPUT" + + run-e2e-ci: + needs: + - build-and-upload-for-e2e-ci + - discover-envs-for-e2e-ci + uses: ./.github/workflows/ci-e2e-template.yaml + with: + artifact: rhtas-console-ui + ui_image: ghcr.io/securesign/rhtas-console-ui:pr-test + server_image: ghcr.io/securesign/rhtas-console:${{ needs.discover-envs-for-e2e-ci.outputs.image_tag }} diff --git a/README.md b/README.md index 0824b00..c601321 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ npm run start | --------------- | ----------------------------- | -------------------------------------- | | MOCK | Enables or disables mock data | `off` | | AUTH_REQUIRED | Enable/Disable authentication | false | +| CONSOLE_API_URL | Set Console API URL | http://localhost:8080 | | OIDC_CLIENT_ID | Set Oidc Client | frontend | | OIDC_SERVER_URL | Set Oidc Server URL | `http://localhost:8090/realms/console` | | OIDC_SCOPE | Set Oidc Scope | openid | @@ -90,7 +91,7 @@ podman run -it $BASE_IMAGE cat /etc/yum.repos.d/ubi.repo > ubi.repo Make sure the `ubi.repo` file has all repositories enabled `enabled = 1` and then: -Also make sure the `ubi.repo` contains only repositories from https://github.com/release-engineering/rhtap-ec-policy/blob/main/data/known_rpm_repositories.yml . Change the repository names manually if needed. E.g. +Also make sure the `ubi.repo` contains only repositories from https://github.com/release-engineering/rhtap-ec-policy/blob/main/data/known_rpm_repositories.yml . Change the repository names manually if needed. E.g. - `ubi-9-for-baseos-rpms` change it to `ubi-9-for-x86_64-baseos-rpms` as only the latter is an accepted repository in Konflux. @@ -128,13 +129,14 @@ The `overlays/dev/` directory contains a `kustomization.yaml` for environment-sp 1. **Set TUF_REPO_URL using a ConfigMap**: Before deploying, you need to retrieve the TUF repository URL from your running RHTAS instance. This value should be stored in a ConfigMap that the console backend can consume. - - * Retrieve the TUF route URL from your running RHTAS instance: + - Retrieve the TUF route URL from your running RHTAS instance: + ```bash oc get tuf -o jsonpath='{.items[0].status.url}' ``` - - * Create a ConfigMap with the retrieved URL: + + - Create a ConfigMap with the retrieved URL: + ```bash oc create configmap tuf-repo-config \ --from-literal=TUF_REPO_URL= \ @@ -151,7 +153,7 @@ The `overlays/dev/` directory contains a `kustomization.yaml` for environment-sp oc apply -k https://github.com/securesign/rhtas-console-ui/deployment/overlays/dev?ref=v0.1.0 ``` -4. **Verify the Deployment**: +3. **Verify the Deployment**: Check the status of the deployed resources: @@ -160,11 +162,12 @@ The `overlays/dev/` directory contains a `kustomization.yaml` for environment-sp ``` You can access the console via a browser using the UI route: + ```bash oc get route console-ui -o jsonpath='https://{.spec.host}{"\n"}' ``` -5. **Deletion**: +4. **Deletion**: To delete the deployed resources: diff --git a/docker-compose.yaml b/docker-compose.yaml index e2b41cd..9f06e2e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,3 +43,15 @@ services: depends_on: console: condition: service_started + + playwright: + image: mcr.microsoft.com/playwright:v1.56.1-jammy + ports: + - "5000:5000" + network_mode: host + working_dir: /home/pwuser + command: + - /bin/sh + - -c + - npx -y playwright run-server --port 5000 + diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..335bd46 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,8 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..e3c0322 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,23 @@ +{ + "name": "@console-ui/e2e", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "clean": "rimraf ./test-results ./playwright-report", + "clean:all": "rimraf ./dist ./node_modules", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --check './tests/**/*.{ts,tsx,js,json}'", + "format:fix": "prettier --write './tests/**/*.{ts,tsx,js,json}'", + "test": "npx playwright test --project='chromium'", + "test:trace": "npx playwright test --project='chromium' --trace on", + "test:ui:host": "npx playwright test --ui-host 127.0.0.1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.56.1" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..63188ad --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,87 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +const DESKTOP_CONFIG = { + viewport: { height: 961, width: 1920 }, +}; + +/** + * 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: process.env.CONSOLE_UI_URL ?? "http://localhost:3000/", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + + // Capture screenshot after each test failure. + screenshot: "only-on-failure", + ignoreHTTPSErrors: true, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"], ...DESKTOP_CONFIG }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"], ...DESKTOP_CONFIG }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"], ...DESKTOP_CONFIG }, + }, + + /* 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 local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/e2e/tests/example.spec.ts b/e2e/tests/example.spec.ts new file mode 100644 index 0000000..839cef5 --- /dev/null +++ b/e2e/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test"; + +test("has title", async ({ page }) => { + await page.goto("https://playwright.dev/"); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test("get started link", async ({ page }) => { + await page.goto("https://playwright.dev/"); + + // Click the get started link. + await page.getByRole("link", { name: "Get started" }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible(); +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..72539eb --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "moduleResolution": "bundler", + "types": ["playwright", "node"] + }, + "include": ["playwright.config.ts", "tests/**/*"], + "exclude": ["node_modules"] +} diff --git a/eslint.config.mjs b/eslint.config.mjs index e323cd5..02123c6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,7 @@ export default tseslint.config([ ecmaVersion: 2020, globals: globals.browser, parserOptions: { - project: ["./common/tsconfig.json", "./client/tsconfig.node.json", "./client/tsconfig.app.json"], + project: ["./common/tsconfig.json", "./client/tsconfig.node.json", "./client/tsconfig.app.json", "./e2e/tsconfig.json"], tsconfigRootDir: import.meta.dirname, }, }, diff --git a/package-lock.json b/package-lock.json index d7b4b98..98bc8d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "workspaces": [ "common", "client", - "server" + "server", + "e2e" ], "dependencies": { "@types/express": "^5.0.3", @@ -104,6 +105,14 @@ "version": "0.1.0", "license": "Apache-2.0" }, + "e2e": { + "name": "@console-ui/e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.56.1" + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -518,6 +527,10 @@ "resolved": "common", "link": true }, + "node_modules/@console-ui/e2e": { + "resolved": "e2e", + "link": true + }, "node_modules/@console-ui/server": { "resolved": "server", "link": true @@ -1984,6 +1997,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -8753,15 +8782,13 @@ "license": "MIT" }, "node_modules/playwright": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", - "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", - "optional": true, - "peer": true, "dependencies": { - "playwright-core": "1.55.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -8774,13 +8801,11 @@ } }, "node_modules/playwright-core": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", - "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", - "optional": true, - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -8799,7 +8824,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/package.json b/package.json index 24ef43d..451bb75 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,16 @@ "start:dev:client": "npm run start:dev -w client", "start:dev": "npm run build -w common && concurrently -n common,client -c 'white.bold.inverse,green.bold.inverse,blue.bold.inverse' 'npm:start:dev:common' 'npm:start:dev:client'", "start": "npm run build -w common -w client && npm run start -w server", - "test": "npm run build -w common && npm run test -w common -w client -w server --if-present --" + "test": "npm run build -w common && npm run test -w common -w client -w server --if-present --", + "e2e:test": "npm run test -w e2e --if-present --", + "e2e:test:trace": "npm run test:trace -w e2e --if-present --", + "e2e:test:host": "npm run test:ui:host -w e2e --if-present --" }, "workspaces": [ "common", "client", - "server" + "server", + "e2e" ], "dependencies": { "@types/express": "^5.0.3",