From 25db7fbc8e0906031529947d1347ad0a3762b31b Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Mon, 1 Jun 2026 14:15:20 +0200 Subject: [PATCH 1/3] CONSOLE-5279: Migrate secrets e2e tests from Cypress to Playwright Migrate 12 secrets tests across 5 spec files to Playwright: - key-value.spec.ts (4 tests): binary/ascii/unicode file secrets, TLS edit - image-pull.spec.ts (3 tests): registry credentials, config file upload, password obfuscation - source.spec.ts (2 tests): basic auth and SSH auth secrets - webhook.spec.ts (1 test): create, regenerate, and delete - add-to-workload.spec.ts (2 tests): env vars and volume mount Adds SecretPage page object and extends KubernetesClient with createSecret, getSecret, and getDeployment methods. Co-Authored-By: Claude Opus 4.6 --- frontend/e2e/clients/kubernetes-client.ts | 26 +++ frontend/e2e/fixtures/secrets/asciisecret.txt | 1 + .../e2e/fixtures/secrets/binarysecret.bin | Bin 0 -> 497 bytes .../e2e/fixtures/secrets/unicodesecret.utf8 | 1 + frontend/e2e/pages/secret-page.ts | 213 ++++++++++++++++++ .../crud/secrets/add-to-workload.spec.ts | 118 ++++++++++ .../console/crud/secrets/image-pull.spec.ts | 152 +++++++++++++ .../console/crud/secrets/key-value.spec.ts | 149 ++++++++++++ .../tests/console/crud/secrets/source.spec.ts | 115 ++++++++++ .../console/crud/secrets/webhook.spec.ts | 53 +++++ 10 files changed, 828 insertions(+) create mode 100644 frontend/e2e/fixtures/secrets/asciisecret.txt create mode 100644 frontend/e2e/fixtures/secrets/binarysecret.bin create mode 100644 frontend/e2e/fixtures/secrets/unicodesecret.utf8 create mode 100644 frontend/e2e/pages/secret-page.ts create mode 100644 frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts create mode 100644 frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts create mode 100644 frontend/e2e/tests/console/crud/secrets/key-value.spec.ts create mode 100644 frontend/e2e/tests/console/crud/secrets/source.spec.ts create mode 100644 frontend/e2e/tests/console/crud/secrets/webhook.spec.ts diff --git a/frontend/e2e/clients/kubernetes-client.ts b/frontend/e2e/clients/kubernetes-client.ts index 9b3bbc49b4f..c53ba09524f 100644 --- a/frontend/e2e/clients/kubernetes-client.ts +++ b/frontend/e2e/clients/kubernetes-client.ts @@ -479,6 +479,14 @@ export default class KubernetesClient { } } + async createSecret(namespace: string, body: k8s.V1Secret): Promise { + return this.k8sApi.createNamespacedSecret({ namespace, body }); + } + + async getSecret(name: string, namespace: string): Promise { + return this.k8sApi.readNamespacedSecret({ name, namespace }); + } + async patchSecret(name: string, namespace: string, patch: object[]): Promise { await this.k8sApi.patchNamespacedSecret({ name, namespace, body: patch }); } @@ -573,6 +581,24 @@ export default class KubernetesClient { return response; } + async createDeployment(namespace: string, body: k8s.V1Deployment): Promise { + return this.appsApi.createNamespacedDeployment({ namespace, body }); + } + + async getDeployment(name: string, namespace: string): Promise { + return this.appsApi.readNamespacedDeployment({ name, namespace }); + } + + async deleteDeployment(name: string, namespace: string): Promise { + try { + await this.appsApi.deleteNamespacedDeployment({ name, namespace }); + } catch (err) { + if (!isNotFound(err)) { + throw err; + } + } + } + async listCustomResources( group: string, version: string, diff --git a/frontend/e2e/fixtures/secrets/asciisecret.txt b/frontend/e2e/fixtures/secrets/asciisecret.txt new file mode 100644 index 00000000000..7c3fb9387ab --- /dev/null +++ b/frontend/e2e/fixtures/secrets/asciisecret.txt @@ -0,0 +1 @@ +This is ascii \ No newline at end of file diff --git a/frontend/e2e/fixtures/secrets/binarysecret.bin b/frontend/e2e/fixtures/secrets/binarysecret.bin new file mode 100644 index 0000000000000000000000000000000000000000..994e78bd5b2d8ccbe25d8bd248e2e4675b4f7c38 GIT binary patch literal 497 zcmZQ&VqjnhXJBFwa&q_f~2f&5UAnJT+Y{&L9vX`R2NC z%pnuoTHmib%BrP+uFt2Y6+#ROY5IIOEY_I2J($J%fxSXlL4YYms{T5M>9LhBSfxaR evVm4o)mMTH0v5|uGB>g~Wco?G`)GV9R}}z>0gEO8 literal 0 HcmV?d00001 diff --git a/frontend/e2e/fixtures/secrets/unicodesecret.utf8 b/frontend/e2e/fixtures/secrets/unicodesecret.utf8 new file mode 100644 index 00000000000..aaaf48d6610 --- /dev/null +++ b/frontend/e2e/fixtures/secrets/unicodesecret.utf8 @@ -0,0 +1 @@ +ȹȻɣʄɣɱɰ \ No newline at end of file diff --git a/frontend/e2e/pages/secret-page.ts b/frontend/e2e/pages/secret-page.ts new file mode 100644 index 00000000000..ad4e4fb53d9 --- /dev/null +++ b/frontend/e2e/pages/secret-page.ts @@ -0,0 +1,213 @@ +import { expect } from '@playwright/test'; + +import BasePage from './base-page'; + +export class SecretPage extends BasePage { + private readonly secretNameInput = this.page.getByTestId('secret-name'); + private readonly saveButton = this.page.getByTestId('save-changes'); + private readonly revealValuesButton = this.page.getByTestId('reveal-values'); + private readonly secretDataContainer = this.page.getByTestId('secret-data'); + private readonly addCredentialsButton = this.page.getByTestId('add-credentials-button'); + private readonly removeEntryButton = this.page.getByTestId('remove-entry-button'); + private readonly authTypeToggle = this.page.getByTestId( + 'console-select-auth-type-menu-toggle', + ); + private readonly webhookGenerateButton = this.page.getByTestId('webhook-generate-button'); + private readonly secretKeyInput = this.page.getByTestId('secret-key'); + private readonly fileInputTextarea = this.page.locator('[data-test-id="file-input-textarea"]'); + private readonly pageHeading = this.page.getByTestId('page-heading'); + private readonly createDropdown = this.page.getByTestId('item-create'); + + async clickCreateSecretDropdownButton(secretType: string): Promise { + await this.robustClick(this.createDropdown); + const menuItem = this.page.locator( + `[data-test-dropdown-menu="${secretType}"] [role="menuitem"]`, + ); + await this.robustClick(menuItem); + await this.waitForLoadingComplete(); + await this.secretNameInput.waitFor({ state: 'visible', timeout: 30_000 }); + } + + async fillName(name: string): Promise { + await this.secretNameInput.waitFor({ state: 'visible', timeout: 10_000 }); + await this.secretNameInput.fill(name); + } + + async save(): Promise { + await this.saveButton.waitFor({ state: 'visible', timeout: 10_000 }); + await expect(this.saveButton).toBeEnabled(); + await this.robustClick(this.saveButton); + await this.saveButton.waitFor({ state: 'detached', timeout: 30_000 }); + } + + async revealValues(): Promise { + await this.waitForLoadingComplete(); + await this.revealValuesButton.waitFor({ state: 'visible', timeout: 30_000 }); + await this.robustClick(this.revealValuesButton); + await this.secretDataContainer.waitFor({ state: 'visible', timeout: 10_000 }); + } + + async verifySecretData(expected: Record, json = false): Promise { + await this.revealValues(); + const terms = this.page.getByTestId('secret-data-term'); + const clipboards = this.page.getByTestId('copy-to-clipboard'); + const count = await terms.count(); + const rendered: Record = {}; + for (let i = 0; i < count; i++) { + const key = (await terms.nth(i).textContent()) ?? ''; + const value = (await clipboards.nth(i).textContent()) ?? ''; + rendered[key] = json ? JSON.parse(value) : value; + } + expect(rendered).toEqual(expected); + } + + async checkKeyValueExist(key: string, value: string): Promise { + await this.revealValues(); + const terms = this.page.getByTestId('secret-data-term'); + const clipboards = this.page.getByTestId('copy-to-clipboard'); + const count = await terms.count(); + for (let i = 0; i < count; i++) { + const termText = (await terms.nth(i).textContent()) ?? ''; + if (termText === key) { + await expect(clipboards.nth(i)).toContainText(value); + return; + } + } + throw new Error(`Secret key "${key}" not found among ${count} entries`); + } + + async addKeyValueEntry(key: string, value: string): Promise { + await this.robustClick(this.addCredentialsButton); + await this.secretKeyInput.last().clear(); + await this.secretKeyInput.last().fill(key); + await this.fileInputTextarea.last().clear(); + await this.fileInputTextarea.last().fill(value); + } + + async fillBasicAuth(username: string, password: string): Promise { + await this.page.getByTestId('secret-username').fill(username); + await this.page.getByTestId('secret-password').fill(password); + } + + async fillImagePullCredential( + index: number, + cred: { address: string; username: string; password: string; email: string }, + ): Promise { + const form = this.page.locator('[data-test-id="create-image-secret-form"]').nth(index); + await form.locator('[data-test="image-secret-address"]').fill(cred.address); + await form.locator('[data-test="image-secret-username"]').fill(cred.username); + await form.locator('[data-test="image-secret-password"]').fill(cred.password); + await form.locator('[data-test="image-secret-email"]').fill(cred.email); + } + + async removeEntry(index = 0): Promise { + await this.robustClick(this.removeEntryButton.nth(index)); + } + + async selectAuthType(type: string): Promise { + const option = this.page.locator(`[data-test-dropdown-menu="${type}"] [role="option"]`); + for (let attempt = 0; attempt < 3; attempt++) { + await this.authTypeToggle.click(); + try { + await option.waitFor({ state: 'visible', timeout: 5_000 }); + await option.click(); + await this.authTypeToggle.waitFor({ state: 'visible', timeout: 5_000 }); + return; + } catch { + // Dropdown may have closed — retry + } + } + throw new Error(`Auth type option "${type}" not found after 3 attempts`); + } + + async generateWebhookKey(): Promise { + await this.webhookGenerateButton.waitFor({ state: 'visible' }); + await this.robustClick(this.webhookGenerateButton); + } + + async detailsPageIsLoaded(secretName: string): Promise { + await this.waitForLoadingComplete(); + await expect(this.pageHeading).toContainText(secretName, { timeout: 30_000 }); + const dataOrEmpty = this.page.locator( + '[data-test="secret-data"], .pf-v6-c-empty-state', + ); + const tryAgain = this.page.getByRole('button', { name: 'Try again' }); + for (let attempt = 0; attempt < 5; attempt++) { + if (await tryAgain.isVisible({ timeout: 2_000 }).catch(() => false)) { + await tryAgain.click(); + await this.waitForLoadingComplete(); + continue; + } + break; + } + await dataOrEmpty.first().waitFor({ state: 'visible', timeout: 30_000 }); + } + + private async clickAction(actionName: string): Promise { + const actionsButton = this.page.locator('[data-test-id="actions-menu-button"]'); + const action = this.page.locator(`[data-test-action="${actionName}"]`); + for (let attempt = 0; attempt < 3; attempt++) { + await this.robustClick(actionsButton); + try { + await action.waitFor({ state: 'visible', timeout: 5_000 }); + await action.click(); + return; + } catch { + // Menu may have closed or items not loaded yet — retry + } + } + throw new Error(`Action "${actionName}" not found after 3 attempts`); + } + + async editSecret(): Promise { + await this.clickAction('Edit Secret'); + } + + async deleteSecret(_secretName: string): Promise { + await this.clickAction('Delete Secret'); + const submitButton = this.page.locator('button[type=submit]'); + await submitButton.waitFor({ state: 'visible', timeout: 10_000 }); + await submitButton.click(); + await submitButton.waitFor({ state: 'detached', timeout: 30_000 }); + } + + async addToWorkload( + workloadName: string, + asType: 'environment' | 'volume', + options?: { prefix?: string; mountPath?: string }, + ): Promise { + await this.page.getByTestId('Add Secret to workload').click(); + const cancelButton = this.page.locator('[data-test-id="modal-cancel-action"]'); + await cancelButton.waitFor({ state: 'visible', timeout: 20_000 }); + + await this.page.locator('#co-add-secret-to-workload__workload').click(); + await this.page.getByTestId('console-select-search-input').locator('input').fill(workloadName); + await this.page.getByTestId('console-select-item').click(); + + if (asType === 'environment') { + await this.page.getByTestId('Environment variables-radio-input').click(); + if (options?.prefix) { + await this.page.getByTestId('add-secret-to-workload-prefix').fill(options.prefix); + } + } else { + await this.page.getByTestId('Volume-radio-input').click(); + if (options?.mountPath) { + await this.page.getByTestId('add-secret-to-workload-mountpath').fill(options.mountPath); + } + } + + const confirmButton = this.page.locator('[data-test="confirm-action"]'); + await expect(confirmButton).toBeEnabled(); + await confirmButton.click(); + await cancelButton.waitFor({ state: 'detached', timeout: 30_000 }); + } + + async uploadFile(filePath: string): Promise { + const fileInput = this.page.locator('.co-file-input input[type="file"]'); + await fileInput.setInputFiles(filePath); + } + + static encode(username: string, password: string): string { + return Buffer.from(`${username}:${password}`).toString('base64'); + } +} diff --git a/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts b/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts new file mode 100644 index 00000000000..7cb17e3a4be --- /dev/null +++ b/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts @@ -0,0 +1,118 @@ +import { test, expect } from '../../../../fixtures'; +import { SecretPage } from '../../../../pages/secret-page'; + +test.describe('Add Secret to Workloads', { tag: ['@admin', '@crud'] }, () => { + test('adds secret to deployment as environment variables', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-secret-env-${Date.now()}`; + const secretName = 'test-secret'; + const deployName = 'test-deploy'; + const envPrefix = 'env-'; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace, deployment, and secret', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createDeployment(ns, { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: deployName, namespace: ns }, + spec: { + selector: { matchLabels: { test: 'add-secret-to-workload' } }, + template: { + metadata: { labels: { test: 'add-secret-to-workload' } }, + spec: { + containers: [ + { + name: 'httpd', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + }, + ], + }, + }, + }, + } as any); + await k8sClient.createSecret(ns, { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: secretName, namespace: ns }, + stringData: { key1: 'supersecret' }, + } as any); + }); + + await test.step('Add secret as env vars', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await page.getByTestId('page-heading').waitFor({ state: 'visible', timeout: 30_000 }); + await page.goto(`/k8s/ns/${ns}/secrets/${secretName}`); + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.addToWorkload(deployName, 'environment', { prefix: envPrefix }); + }); + + await test.step('Verify env vars via API', async () => { + const deploy = (await k8sClient.getDeployment(deployName, ns)) as any; + const envFrom = deploy.spec.template.spec.containers[0].envFrom; + expect(envFrom).toBeDefined(); + const secretEnv = envFrom.find((e: any) => e.secretRef?.name === secretName); + expect(secretEnv).toBeDefined(); + expect(secretEnv.prefix).toBe(envPrefix); + }); + }); + + test('adds secret to deployment as volume', async ({ page, k8sClient, cleanup }) => { + const ns = `test-secret-vol-${Date.now()}`; + const secretName = 'test-secret'; + const deployName = 'test-deploy'; + const mountPath = '/tmp/testdata'; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace, deployment, and secret', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createDeployment(ns, { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: deployName, namespace: ns }, + spec: { + selector: { matchLabels: { test: 'add-secret-to-workload' } }, + template: { + metadata: { labels: { test: 'add-secret-to-workload' } }, + spec: { + containers: [ + { + name: 'httpd', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + }, + ], + }, + }, + }, + } as any); + await k8sClient.createSecret(ns, { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: secretName, namespace: ns }, + stringData: { key1: 'supersecret' }, + } as any); + }); + + await test.step('Add secret as volume', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await page.getByTestId('page-heading').waitFor({ state: 'visible', timeout: 30_000 }); + await page.goto(`/k8s/ns/${ns}/secrets/${secretName}`); + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.addToWorkload(deployName, 'volume', { mountPath }); + }); + + await test.step('Verify volume mount via API', async () => { + const deploy = (await k8sClient.getDeployment(deployName, ns)) as any; + const volumeMounts = deploy.spec.template.spec.containers[0].volumeMounts; + expect(volumeMounts).toBeDefined(); + const secretMount = volumeMounts.find((m: any) => m.name === secretName); + expect(secretMount).toBeDefined(); + expect(secretMount.mountPath).toBe(mountPath); + }); + }); +}); diff --git a/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts b/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts new file mode 100644 index 00000000000..77b7c312b0e --- /dev/null +++ b/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts @@ -0,0 +1,152 @@ +import { test, expect } from '../../../../fixtures'; +import { SecretPage } from '../../../../pages/secret-page'; + +test.describe('Image pull secrets', { tag: ['@admin', '@crud'] }, () => { + test('creates and edits registry credentials', async ({ page, k8sClient, cleanup }) => { + const ns = `test-secret-img-cred-${Date.now()}`; + const secretName = `registry-cred-secret`; + const secretPage = new SecretPage(page); + const address = 'https://index.openshift.io/v'; + const addressUpdated = 'https://index.openshift.io/updated/v1'; + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create image secret with two credential entries', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('image'); + await expect(page.getByTestId('page-heading')).toContainText('Create image pull secret'); + await secretPage.fillName(secretName); + await page.getByTestId('add-credentials-button').click(); + + const forms = page.locator('[data-test-id="create-image-secret-form"]'); + const count = await forms.count(); + for (let i = 0; i < count; i++) { + const form = forms.nth(i); + await form.locator('[data-test="image-secret-address"]').fill(`${address}${i}`); + await form.locator('[data-test="image-secret-username"]').fill(`username${i}`); + await form.locator('[data-test="image-secret-password"]').fill(`password${i}`); + await form.locator('[data-test="image-secret-email"]').fill(`test@secret.com${i}`); + } + await secretPage.save(); + }); + + await test.step('Verify credentials via API', async () => { + await secretPage.detailsPageIsLoaded(secretName); + const secret = await k8sClient.getSecret(secretName, ns); + const dockerConfig = JSON.parse( + Buffer.from((secret as any).data['.dockerconfigjson'], 'base64').toString('utf-8'), + ); + expect(dockerConfig.auths[`${address}0`].username).toBe('username0'); + expect(dockerConfig.auths[`${address}0`].password).toBe('password0'); + expect(dockerConfig.auths[`${address}0`].auth).toBe(SecretPage.encode('username0', 'password0')); + expect(dockerConfig.auths[`${address}1`].username).toBe('username1'); + expect(dockerConfig.auths[`${address}1`].auth).toBe(SecretPage.encode('username1', 'password1')); + }); + + await test.step('Edit secret: remove entry, update with whitespace', async () => { + await secretPage.editSecret(); + await expect(page.getByTestId('page-heading')).toContainText('Edit image pull secret'); + await expect(page.locator('[data-test-id="create-image-secret-form"]')).toHaveCount(2); + await secretPage.removeEntry(0); + await page.getByTestId('image-secret-address').clear(); + await page.getByTestId('image-secret-address').fill(` ${addressUpdated} `); + await page.getByTestId('image-secret-username').clear(); + await page.getByTestId('image-secret-username').fill(' usernameUpdated '); + await page.getByTestId('image-secret-password').clear(); + await page.getByTestId('image-secret-password').fill(' passwordUpdated '); + await page.getByTestId('image-secret-email').clear(); + await page.getByTestId('image-secret-email').fill(' testUpdated@secret.com '); + await secretPage.save(); + }); + + await test.step('Verify whitespace trimmed in API', async () => { + await secretPage.detailsPageIsLoaded(secretName); + const secret = await k8sClient.getSecret(secretName, ns); + const dockerConfig = JSON.parse( + Buffer.from((secret as any).data['.dockerconfigjson'], 'base64').toString('utf-8'), + ); + expect(dockerConfig.auths[addressUpdated].username).toBe('usernameUpdated'); + expect(dockerConfig.auths[addressUpdated].password).toBe('passwordUpdated'); + expect(dockerConfig.auths[addressUpdated].email).toBe('testUpdated@secret.com'); + expect(dockerConfig.auths[addressUpdated].auth).toBe( + SecretPage.encode('usernameUpdated', 'passwordUpdated'), + ); + }); + }); + + test('creates an upload config file image pull secret', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-secret-img-cfg-${Date.now()}`; + const secretName = `config-file-secret`; + const secretPage = new SecretPage(page); + const configFile = { + auths: { + 'https://index.openshift.io/v1': { + username: 'username', + password: 'password', + auth: SecretPage.encode('username', 'password'), + email: 'test@secret.com', + }, + }, + }; + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create config file secret', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('image'); + await expect(page.getByTestId('page-heading')).toContainText('Create image pull secret'); + await secretPage.fillName(secretName); + await secretPage.selectAuthType('config-file'); + + const textarea = page.locator('[data-test-id="file-input-textarea"]'); + await textarea.clear(); + await textarea.fill(JSON.stringify(configFile)); + await secretPage.save(); + }); + + await test.step('Verify config file secret via API', async () => { + await secretPage.detailsPageIsLoaded(secretName); + const secret = await k8sClient.getSecret(secretName, ns); + const dockerConfig = JSON.parse( + Buffer.from((secret as any).data['.dockerconfigjson'], 'base64').toString('utf-8'), + ); + expect(dockerConfig).toEqual(configFile); + }); + }); + + test('obfuscates password fields', async ({ page, k8sClient, cleanup }) => { + const ns = `test-secret-img-pw-${Date.now()}`; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Verify image pull password is obfuscated', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('image'); + await expect( + page.locator('input[data-test="image-secret-password"]'), + ).toHaveAttribute('type', 'password'); + await page.locator('button#cancel').click(); + }); + + await test.step('Verify source secret password is obfuscated', async () => { + await secretPage.clickCreateSecretDropdownButton('source'); + await expect( + page.locator('input[data-test="secret-password"]'), + ).toHaveAttribute('type', 'password'); + }); + }); +}); diff --git a/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts b/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts new file mode 100644 index 00000000000..f5ad9a88267 --- /dev/null +++ b/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts @@ -0,0 +1,149 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { test, expect } from '../../../../fixtures'; +import { SecretPage } from '../../../../pages/secret-page'; + +const fixturesDir = path.resolve(import.meta.dirname, '..', '..', '..', '..', 'fixtures', 'secrets'); + +test.describe('Key/value secrets', { tag: ['@admin', '@crud'] }, () => { + test('creates a binary file secret', async ({ page, k8sClient, cleanup }) => { + const ns = `test-secret-kv-bin-${Date.now()}`; + const secretName = `binary-secret`; + const secretKey = 'secretkey'; + const modifiedKey = 'modifiedsecretkey'; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create binary file secret', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('generic'); + await expect(page.getByTestId('page-heading')).toContainText('Create key/value secret'); + await secretPage.fillName(secretName); + await page.getByTestId('secret-key').fill(secretKey); + await secretPage.uploadFile(path.join(fixturesDir, 'binarysecret.bin')); + await expect(page.getByTestId('file-input-binary-alert')).toBeVisible(); + await secretPage.save(); + }); + + await test.step('Verify binary secret via API', async () => { + await secretPage.detailsPageIsLoaded(secretName); + const secret = await k8sClient.getSecret(secretName, ns); + const expectedBase64 = fs.readFileSync(path.join(fixturesDir, 'binarysecret.bin')).toString('base64'); + expect((secret as any).data[secretKey]).toBe(expectedBase64); + }); + + await test.step('Edit secret key and verify data preserved', async () => { + await secretPage.editSecret(); + await expect(page.getByTestId('page-heading')).toContainText('Edit key/value secret'); + await page.getByTestId('secret-key').clear(); + await page.getByTestId('secret-key').fill(modifiedKey); + await expect(page.getByTestId('file-input-binary-alert')).toBeVisible(); + await secretPage.save(); + await secretPage.detailsPageIsLoaded(secretName); + const secret = await k8sClient.getSecret(secretName, ns); + const expectedBase64 = fs.readFileSync(path.join(fixturesDir, 'binarysecret.bin')).toString('base64'); + expect((secret as any).data[modifiedKey]).toBe(expectedBase64); + }); + }); + + test('creates an ascii file secret', async ({ page, k8sClient, cleanup }) => { + const ns = `test-secret-kv-ascii-${Date.now()}`; + const secretName = `ascii-secret`; + const secretKey = 'secretkey'; + const secretPage = new SecretPage(page); + const asciiContent = fs.readFileSync(path.join(fixturesDir, 'asciisecret.txt'), 'utf-8'); + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create ascii file secret', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('generic'); + await secretPage.fillName(secretName); + await page.getByTestId('secret-key').fill(secretKey); + await secretPage.uploadFile(path.join(fixturesDir, 'asciisecret.txt')); + await expect(page.locator('[data-test-id="file-input-textarea"]')).toContainText(asciiContent); + await expect(page.getByTestId('file-input-binary-alert')).toBeHidden(); + await secretPage.save(); + }); + + await test.step('Verify ascii secret via API', async () => { + await secretPage.detailsPageIsLoaded(secretName); + const secret = await k8sClient.getSecret(secretName, ns); + const decoded = Buffer.from((secret as any).data[secretKey], 'base64').toString('utf-8'); + expect(decoded).toBe(asciiContent); + }); + }); + + test('creates a unicode file secret', async ({ page, k8sClient, cleanup }) => { + const ns = `test-secret-kv-unicode-${Date.now()}`; + const secretName = `unicode-secret`; + const secretKey = 'secretkey'; + const secretPage = new SecretPage(page); + const unicodeContent = fs.readFileSync(path.join(fixturesDir, 'unicodesecret.utf8'), 'utf-8'); + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create unicode file secret', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('generic'); + await secretPage.fillName(secretName); + await page.getByTestId('secret-key').fill(secretKey); + await secretPage.uploadFile(path.join(fixturesDir, 'unicodesecret.utf8')); + await expect(page.locator('[data-test-id="file-input-textarea"]')).toContainText(unicodeContent); + await expect(page.getByTestId('file-input-binary-alert')).toBeHidden(); + await secretPage.save(); + }); + + await test.step('Verify unicode secret via API', async () => { + await secretPage.detailsPageIsLoaded(secretName); + const secret = await k8sClient.getSecret(secretName, ns); + const decoded = Buffer.from((secret as any).data[secretKey], 'base64').toString('utf-8'); + expect(decoded).toBe(unicodeContent); + }); + }); + + test('edits a TLS secret', async ({ page, k8sClient, cleanup }) => { + const ns = `test-secret-kv-tls-${Date.now()}`; + const secretName = `tls-secret`; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace and TLS secret', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createSecret(ns, { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: secretName, namespace: ns }, + type: 'kubernetes.io/tls', + data: { + 'tls.crt': 'QUFBCG==', + 'tls.key': 'QkJCCg==', + }, + } as any); + }); + + await test.step('Edit TLS secret and add key', async () => { + await page.goto(`/k8s/ns/${ns}/secrets/${secretName}/edit`); + await secretPage.addKeyValueEntry('keyfortest', 'valuefortest'); + await secretPage.save(); + }); + + await test.step('Verify new key appears on details page', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.checkKeyValueExist('keyfortest', 'valuefortest'); + const secret = await k8sClient.getSecret(secretName, ns); + expect((secret as any).data).toBeDefined(); + }); + }); +}); diff --git a/frontend/e2e/tests/console/crud/secrets/source.spec.ts b/frontend/e2e/tests/console/crud/secrets/source.spec.ts new file mode 100644 index 00000000000..d26118bba1e --- /dev/null +++ b/frontend/e2e/tests/console/crud/secrets/source.spec.ts @@ -0,0 +1,115 @@ +import { test, expect } from '../../../../fixtures'; +import { SecretPage } from '../../../../pages/secret-page'; + +test.describe('Source secrets', { tag: ['@admin', '@crud'] }, () => { + test('creates, edits, and deletes a basic source secret', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-secret-src-basic-${Date.now()}`; + const secretName = `basic-source-secret`; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create basic source secret', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('source'); + await expect(page.getByTestId('page-heading')).toContainText('Create source secret'); + await secretPage.fillName(secretName); + await secretPage.fillBasicAuth('username', 'password'); + await secretPage.save(); + }); + + await test.step('Verify secret data', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.verifySecretData({ + password: 'password', + username: 'username', + }); + }); + + await test.step('Edit secret', async () => { + await secretPage.editSecret(); + await expect(page.getByTestId('page-heading')).toContainText('Edit source secret'); + await expect(page.getByTestId('secret-username')).toHaveValue('username'); + await expect(page.getByTestId('secret-password')).toHaveValue('password'); + await page.getByTestId('secret-username').clear(); + await page.getByTestId('secret-username').fill('usernameUpdated'); + await page.getByTestId('secret-password').clear(); + await page.getByTestId('secret-password').fill('passwordUpdated'); + await secretPage.save(); + }); + + await test.step('Verify edit', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.verifySecretData({ + password: 'passwordUpdated', + username: 'usernameUpdated', + }); + }); + + await test.step('Delete secret', async () => { + await secretPage.deleteSecret(secretName); + }); + }); + + test('creates, edits, and deletes an SSH source secret', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-secret-src-ssh-${Date.now()}`; + const secretName = `ssh-source-secret`; + const sshKey = 'sshKey'; + const sshKeyUpdated = 'sshKeyUpdated'; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create SSH source secret', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('source'); + await expect(page.getByTestId('page-heading')).toContainText('Create source secret'); + await secretPage.fillName(secretName); + await secretPage.selectAuthType('kubernetes.io/ssh-auth'); + await page.locator('[data-test-id="file-input-textarea"]').fill(sshKey); + await secretPage.save(); + }); + + await test.step('Verify secret data', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.verifySecretData({ + 'ssh-privatekey': `${sshKey}\n`, + }); + }); + + await test.step('Edit secret', async () => { + await secretPage.editSecret(); + await expect(page.getByTestId('page-heading')).toContainText('Edit source secret'); + const textarea = page.locator('[data-test-id="file-input-textarea"]'); + await expect(textarea).toContainText(sshKey); + await textarea.clear(); + await textarea.fill(sshKeyUpdated); + await secretPage.save(); + }); + + await test.step('Verify edit', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.verifySecretData({ + 'ssh-privatekey': `${sshKeyUpdated}\n`, + }); + }); + + await test.step('Delete secret', async () => { + await secretPage.deleteSecret(secretName); + }); + }); +}); diff --git a/frontend/e2e/tests/console/crud/secrets/webhook.spec.ts b/frontend/e2e/tests/console/crud/secrets/webhook.spec.ts new file mode 100644 index 00000000000..d0938abfd8c --- /dev/null +++ b/frontend/e2e/tests/console/crud/secrets/webhook.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '../../../../fixtures'; +import { SecretPage } from '../../../../pages/secret-page'; + +test.describe('Webhook secret', { tag: ['@admin', '@crud'] }, () => { + test('creates, regenerates, and deletes a webhook secret', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-secret-webhook-${Date.now()}`; + const secretName = `webhook-secret`; + const webhookKey = 'webhookValue'; + const secretPage = new SecretPage(page); + + await test.step('Set up namespace', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + }); + + await test.step('Create webhook secret', async () => { + await page.goto(`/k8s/ns/${ns}/secrets`); + await secretPage.clickCreateSecretDropdownButton('webhook'); + await expect(page.getByTestId('page-heading')).toContainText('Create webhook secret'); + await secretPage.fillName(secretName); + await page.getByTestId('secret-key').fill(webhookKey); + await secretPage.save(); + }); + + await test.step('Verify webhook secret', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.verifySecretData({ + WebHookSecretKey: webhookKey, + }); + }); + + await test.step('Edit and regenerate key', async () => { + await secretPage.editSecret(); + await expect(page.getByTestId('page-heading')).toContainText('Edit webhook secret'); + await secretPage.generateWebhookKey(); + await secretPage.save(); + }); + + await test.step('Verify key changed', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.revealValues(); + await expect(page.getByTestId('copy-to-clipboard').first()).not.toHaveText(webhookKey); + }); + + await test.step('Delete secret', async () => { + await secretPage.deleteSecret(secretName); + }); + }); +}); From f96da4eacfdabc85149569aed46d92af8c8fb502 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Wed, 1 Jul 2026 13:00:23 -0400 Subject: [PATCH 2/3] CONSOLE-5279: Migrate secrets e2e tests from Cypress to Playwright Remove Cypress secrets tests, fixtures, and page object now that all 13 test scenarios have feature-parity Playwright equivalents. Move inline locators from spec files into the SecretPage page object, fix robustClick usage, and add warmupSPA to prevent "Model does not exist" errors. Co-Authored-By: Claude Opus 4.6 --- frontend/e2e/pages/secret-page.ts | 170 +++++++++----- .../crud/secrets/add-to-workload.spec.ts | 7 +- .../console/crud/secrets/image-pull.spec.ts | 48 ++-- .../console/crud/secrets/key-value.spec.ts | 77 +++++-- .../tests/console/crud/secrets/source.spec.ts | 29 ++- .../console/crud/secrets/webhook.spec.ts | 8 +- .../fixtures/asciisecret.txt | 1 - .../fixtures/binarysecret.bin | Bin 497 -> 0 bytes .../fixtures/unicodesecret.utf8 | 1 - .../tests/crud/secrets/add-to-workload.cy.ts | 116 ---------- .../tests/crud/secrets/image-pull.cy.ts | 211 ----------------- .../tests/crud/secrets/key-value.cy.ts | 218 ------------------ .../tests/crud/secrets/source.cy.ts | 113 --------- .../tests/crud/secrets/webhook.cy.ts | 66 ------ .../integration-tests/views/secret.ts | 96 -------- .../modals/add-secret-to-workload.tsx | 2 +- .../secrets/create-secret/AuthSecretForm.tsx | 7 +- .../PullSecretCredentialEntry.tsx | 6 +- .../secrets/create-secret/PullSecretForm.tsx | 2 + 19 files changed, 238 insertions(+), 940 deletions(-) delete mode 100644 frontend/packages/integration-tests/fixtures/asciisecret.txt delete mode 100644 frontend/packages/integration-tests/fixtures/binarysecret.bin delete mode 100644 frontend/packages/integration-tests/fixtures/unicodesecret.utf8 delete mode 100644 frontend/packages/integration-tests/tests/crud/secrets/add-to-workload.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/crud/secrets/image-pull.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/crud/secrets/key-value.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/crud/secrets/source.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/crud/secrets/webhook.cy.ts delete mode 100644 frontend/packages/integration-tests/views/secret.ts diff --git a/frontend/e2e/pages/secret-page.ts b/frontend/e2e/pages/secret-page.ts index ad4e4fb53d9..cc457b60113 100644 --- a/frontend/e2e/pages/secret-page.ts +++ b/frontend/e2e/pages/secret-page.ts @@ -1,6 +1,8 @@ +import type { Locator } from '@playwright/test'; import { expect } from '@playwright/test'; import BasePage from './base-page'; +import { ModalPage } from './modal-page'; export class SecretPage extends BasePage { private readonly secretNameInput = this.page.getByTestId('secret-name'); @@ -14,48 +16,104 @@ export class SecretPage extends BasePage { ); private readonly webhookGenerateButton = this.page.getByTestId('webhook-generate-button'); private readonly secretKeyInput = this.page.getByTestId('secret-key'); - private readonly fileInputTextarea = this.page.locator('[data-test-id="file-input-textarea"]'); + private readonly fileInputTextarea = this.page.getByRole('textbox', { + name: 'Value', + exact: true, + }); + private readonly binaryAlert = this.page.getByTestId('file-input-binary-alert'); private readonly pageHeading = this.page.getByTestId('page-heading'); private readonly createDropdown = this.page.getByTestId('item-create'); + private readonly actionsButton = this.page.getByTestId('actions-menu-button'); + private readonly secretDataTerms = this.page.getByTestId('secret-data-term'); + private readonly clipboards = this.page.getByTestId('copy-to-clipboard'); + private readonly imagePullForm = this.page.getByTestId('create-image-secret-form'); + private readonly usernameInput = this.page.getByTestId('secret-username'); + private readonly passwordInput = this.page.getByTestId('secret-password'); + private readonly sshKeyTextarea = this.page.getByTestId('ssh-privatekey-textarea'); + private readonly dockerConfigTextarea = this.page.getByTestId('docker-config-textarea'); + private readonly imagePasswordInput = this.page.getByTestId('image-secret-password'); + private readonly cancelButton = this.page.getByRole('button', { name: 'Cancel' }); + private readonly modal = new ModalPage(this.page); + + getPageHeading(): Locator { + return this.pageHeading; + } + + getBinaryAlert(): Locator { + return this.binaryAlert; + } + + getClipboards(): Locator { + return this.clipboards; + } + + getImagePullForms(): Locator { + return this.imagePullForm; + } + + getUsernameInput(): Locator { + return this.usernameInput; + } + + getPasswordInput(): Locator { + return this.passwordInput; + } + + getSshKeyTextarea(): Locator { + return this.sshKeyTextarea; + } + + getDockerConfigTextarea(): Locator { + return this.dockerConfigTextarea; + } + + getImagePasswordInput(): Locator { + return this.imagePasswordInput; + } + + getFileInputTextarea(): Locator { + return this.fileInputTextarea; + } + + getSecretKeyInput(): Locator { + return this.secretKeyInput; + } async clickCreateSecretDropdownButton(secretType: string): Promise { await this.robustClick(this.createDropdown); - const menuItem = this.page.locator( - `[data-test-dropdown-menu="${secretType}"] [role="menuitem"]`, - ); + const menuItem = this.page.getByTestId(`dropdown-menu-${secretType}`).getByRole('menuitem'); await this.robustClick(menuItem); await this.waitForLoadingComplete(); - await this.secretNameInput.waitFor({ state: 'visible', timeout: 30_000 }); } async fillName(name: string): Promise { - await this.secretNameInput.waitFor({ state: 'visible', timeout: 10_000 }); await this.secretNameInput.fill(name); } + async fillSecretKey(key: string): Promise { + await this.secretKeyInput.fill(key); + } + async save(): Promise { - await this.saveButton.waitFor({ state: 'visible', timeout: 10_000 }); await expect(this.saveButton).toBeEnabled(); await this.robustClick(this.saveButton); + // eslint-disable-next-line no-restricted-syntax await this.saveButton.waitFor({ state: 'detached', timeout: 30_000 }); } async revealValues(): Promise { await this.waitForLoadingComplete(); - await this.revealValuesButton.waitFor({ state: 'visible', timeout: 30_000 }); - await this.robustClick(this.revealValuesButton); - await this.secretDataContainer.waitFor({ state: 'visible', timeout: 10_000 }); + await this.robustClick(this.revealValuesButton, { timeout: 30_000 }); + await expect(this.secretDataContainer).toBeVisible(); } async verifySecretData(expected: Record, json = false): Promise { await this.revealValues(); - const terms = this.page.getByTestId('secret-data-term'); - const clipboards = this.page.getByTestId('copy-to-clipboard'); - const count = await terms.count(); + const count = await this.secretDataTerms.count(); const rendered: Record = {}; for (let i = 0; i < count; i++) { - const key = (await terms.nth(i).textContent()) ?? ''; - const value = (await clipboards.nth(i).textContent()) ?? ''; + const key = (await this.secretDataTerms.nth(i).textContent()) ?? ''; + const value = (await this.clipboards.nth(i).textContent()) ?? ''; rendered[key] = json ? JSON.parse(value) : value; } expect(rendered).toEqual(expected); @@ -63,13 +121,11 @@ export class SecretPage extends BasePage { async checkKeyValueExist(key: string, value: string): Promise { await this.revealValues(); - const terms = this.page.getByTestId('secret-data-term'); - const clipboards = this.page.getByTestId('copy-to-clipboard'); - const count = await terms.count(); + const count = await this.secretDataTerms.count(); for (let i = 0; i < count; i++) { - const termText = (await terms.nth(i).textContent()) ?? ''; + const termText = (await this.secretDataTerms.nth(i).textContent()) ?? ''; if (termText === key) { - await expect(clipboards.nth(i)).toContainText(value); + await expect(this.clipboards.nth(i)).toContainText(value); return; } } @@ -85,33 +141,47 @@ export class SecretPage extends BasePage { } async fillBasicAuth(username: string, password: string): Promise { - await this.page.getByTestId('secret-username').fill(username); - await this.page.getByTestId('secret-password').fill(password); + await this.usernameInput.fill(username); + await this.passwordInput.fill(password); + } + + async fillSshKey(key: string): Promise { + await this.sshKeyTextarea.fill(key); + } + + async fillDockerConfig(config: string): Promise { + await this.dockerConfigTextarea.fill(config); } async fillImagePullCredential( index: number, cred: { address: string; username: string; password: string; email: string }, ): Promise { - const form = this.page.locator('[data-test-id="create-image-secret-form"]').nth(index); - await form.locator('[data-test="image-secret-address"]').fill(cred.address); - await form.locator('[data-test="image-secret-username"]').fill(cred.username); - await form.locator('[data-test="image-secret-password"]').fill(cred.password); - await form.locator('[data-test="image-secret-email"]').fill(cred.email); + const form = this.imagePullForm.nth(index); + await form.getByTestId('image-secret-address').fill(cred.address); + await form.getByTestId('image-secret-username').fill(cred.username); + await form.getByTestId('image-secret-password').fill(cred.password); + await form.getByTestId('image-secret-email').fill(cred.email); + } + + async addCredentialEntry(): Promise { + await this.robustClick(this.addCredentialsButton); } async removeEntry(index = 0): Promise { await this.robustClick(this.removeEntryButton.nth(index)); } + async cancel(): Promise { + await this.robustClick(this.cancelButton); + } + async selectAuthType(type: string): Promise { - const option = this.page.locator(`[data-test-dropdown-menu="${type}"] [role="option"]`); + const option = this.page.getByTestId(type).getByRole('option'); for (let attempt = 0; attempt < 3; attempt++) { - await this.authTypeToggle.click(); + await this.robustClick(this.authTypeToggle); try { - await option.waitFor({ state: 'visible', timeout: 5_000 }); - await option.click(); - await this.authTypeToggle.waitFor({ state: 'visible', timeout: 5_000 }); + await option.click({ timeout: 5_000 }); return; } catch { // Dropdown may have closed — retry @@ -121,15 +191,14 @@ export class SecretPage extends BasePage { } async generateWebhookKey(): Promise { - await this.webhookGenerateButton.waitFor({ state: 'visible' }); await this.robustClick(this.webhookGenerateButton); } async detailsPageIsLoaded(secretName: string): Promise { await this.waitForLoadingComplete(); await expect(this.pageHeading).toContainText(secretName, { timeout: 30_000 }); - const dataOrEmpty = this.page.locator( - '[data-test="secret-data"], .pf-v6-c-empty-state', + const dataOrEmpty = this.secretDataContainer.or( + this.page.locator('.pf-v6-c-empty-state'), ); const tryAgain = this.page.getByRole('button', { name: 'Try again' }); for (let attempt = 0; attempt < 5; attempt++) { @@ -140,17 +209,15 @@ export class SecretPage extends BasePage { } break; } - await dataOrEmpty.first().waitFor({ state: 'visible', timeout: 30_000 }); + await expect(dataOrEmpty.first()).toBeVisible({ timeout: 30_000 }); } private async clickAction(actionName: string): Promise { - const actionsButton = this.page.locator('[data-test-id="actions-menu-button"]'); - const action = this.page.locator(`[data-test-action="${actionName}"]`); + const action = this.page.getByTestId(actionName); for (let attempt = 0; attempt < 3; attempt++) { - await this.robustClick(actionsButton); + await this.robustClick(this.actionsButton); try { - await action.waitFor({ state: 'visible', timeout: 5_000 }); - await action.click(); + await action.click({ timeout: 5_000 }); return; } catch { // Menu may have closed or items not loaded yet — retry @@ -165,10 +232,9 @@ export class SecretPage extends BasePage { async deleteSecret(_secretName: string): Promise { await this.clickAction('Delete Secret'); - const submitButton = this.page.locator('button[type=submit]'); - await submitButton.waitFor({ state: 'visible', timeout: 10_000 }); - await submitButton.click(); - await submitButton.waitFor({ state: 'detached', timeout: 30_000 }); + await this.modal.waitForOpen(); + await this.modal.submit(); + await this.modal.waitForClosed(); } async addToWorkload( @@ -176,11 +242,10 @@ export class SecretPage extends BasePage { asType: 'environment' | 'volume', options?: { prefix?: string; mountPath?: string }, ): Promise { - await this.page.getByTestId('Add Secret to workload').click(); - const cancelButton = this.page.locator('[data-test-id="modal-cancel-action"]'); - await cancelButton.waitFor({ state: 'visible', timeout: 20_000 }); + await this.clickAction('Add Secret to workload'); + await this.modal.waitForOpen(); - await this.page.locator('#co-add-secret-to-workload__workload').click(); + await this.robustClick(this.page.getByTestId('add-secret-to-workload-button')); await this.page.getByTestId('console-select-search-input').locator('input').fill(workloadName); await this.page.getByTestId('console-select-item').click(); @@ -196,10 +261,9 @@ export class SecretPage extends BasePage { } } - const confirmButton = this.page.locator('[data-test="confirm-action"]'); - await expect(confirmButton).toBeEnabled(); - await confirmButton.click(); - await cancelButton.waitFor({ state: 'detached', timeout: 30_000 }); + await expect(this.modal.getSubmitButton()).toBeEnabled(); + await this.modal.submit(); + await this.modal.waitForClosed(); } async uploadFile(filePath: string): Promise { diff --git a/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts b/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts index 7cb17e3a4be..6c86e05facf 100644 --- a/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from '../../../../fixtures'; +import { warmupSPA } from '../../../../pages/base-page'; import { SecretPage } from '../../../../pages/secret-page'; test.describe('Add Secret to Workloads', { tag: ['@admin', '@crud'] }, () => { @@ -44,8 +45,7 @@ test.describe('Add Secret to Workloads', { tag: ['@admin', '@crud'] }, () => { }); await test.step('Add secret as env vars', async () => { - await page.goto(`/k8s/ns/${ns}/secrets`); - await page.getByTestId('page-heading').waitFor({ state: 'visible', timeout: 30_000 }); + await warmupSPA(page); await page.goto(`/k8s/ns/${ns}/secrets/${secretName}`); await secretPage.detailsPageIsLoaded(secretName); await secretPage.addToWorkload(deployName, 'environment', { prefix: envPrefix }); @@ -99,8 +99,7 @@ test.describe('Add Secret to Workloads', { tag: ['@admin', '@crud'] }, () => { }); await test.step('Add secret as volume', async () => { - await page.goto(`/k8s/ns/${ns}/secrets`); - await page.getByTestId('page-heading').waitFor({ state: 'visible', timeout: 30_000 }); + await warmupSPA(page); await page.goto(`/k8s/ns/${ns}/secrets/${secretName}`); await secretPage.detailsPageIsLoaded(secretName); await secretPage.addToWorkload(deployName, 'volume', { mountPath }); diff --git a/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts b/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts index 77b7c312b0e..9d7b7df6f7d 100644 --- a/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts @@ -17,18 +17,18 @@ test.describe('Image pull secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Create image secret with two credential entries', async () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('image'); - await expect(page.getByTestId('page-heading')).toContainText('Create image pull secret'); + await expect(secretPage.getPageHeading()).toContainText('Create image pull secret'); await secretPage.fillName(secretName); - await page.getByTestId('add-credentials-button').click(); + await secretPage.addCredentialEntry(); - const forms = page.locator('[data-test-id="create-image-secret-form"]'); - const count = await forms.count(); + const count = await secretPage.getImagePullForms().count(); for (let i = 0; i < count; i++) { - const form = forms.nth(i); - await form.locator('[data-test="image-secret-address"]').fill(`${address}${i}`); - await form.locator('[data-test="image-secret-username"]').fill(`username${i}`); - await form.locator('[data-test="image-secret-password"]').fill(`password${i}`); - await form.locator('[data-test="image-secret-email"]').fill(`test@secret.com${i}`); + await secretPage.fillImagePullCredential(i, { + address: `${address}${i}`, + username: `username${i}`, + password: `password${i}`, + email: `test@secret.com${i}`, + }); } await secretPage.save(); }); @@ -48,17 +48,15 @@ test.describe('Image pull secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Edit secret: remove entry, update with whitespace', async () => { await secretPage.editSecret(); - await expect(page.getByTestId('page-heading')).toContainText('Edit image pull secret'); - await expect(page.locator('[data-test-id="create-image-secret-form"]')).toHaveCount(2); + await expect(secretPage.getPageHeading()).toContainText('Edit image pull secret'); + await expect(secretPage.getImagePullForms()).toHaveCount(2); await secretPage.removeEntry(0); - await page.getByTestId('image-secret-address').clear(); - await page.getByTestId('image-secret-address').fill(` ${addressUpdated} `); - await page.getByTestId('image-secret-username').clear(); - await page.getByTestId('image-secret-username').fill(' usernameUpdated '); - await page.getByTestId('image-secret-password').clear(); - await page.getByTestId('image-secret-password').fill(' passwordUpdated '); - await page.getByTestId('image-secret-email').clear(); - await page.getByTestId('image-secret-email').fill(' testUpdated@secret.com '); + await secretPage.fillImagePullCredential(0, { + address: ` ${addressUpdated} `, + username: ' usernameUpdated ', + password: ' passwordUpdated ', + email: ' testUpdated@secret.com ', + }); await secretPage.save(); }); @@ -104,13 +102,11 @@ test.describe('Image pull secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Create config file secret', async () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('image'); - await expect(page.getByTestId('page-heading')).toContainText('Create image pull secret'); + await expect(secretPage.getPageHeading()).toContainText('Create image pull secret'); await secretPage.fillName(secretName); await secretPage.selectAuthType('config-file'); - const textarea = page.locator('[data-test-id="file-input-textarea"]'); - await textarea.clear(); - await textarea.fill(JSON.stringify(configFile)); + await secretPage.fillDockerConfig(JSON.stringify(configFile)); await secretPage.save(); }); @@ -137,15 +133,15 @@ test.describe('Image pull secrets', { tag: ['@admin', '@crud'] }, () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('image'); await expect( - page.locator('input[data-test="image-secret-password"]'), + secretPage.getImagePasswordInput(), ).toHaveAttribute('type', 'password'); - await page.locator('button#cancel').click(); + await secretPage.cancel(); }); await test.step('Verify source secret password is obfuscated', async () => { await secretPage.clickCreateSecretDropdownButton('source'); await expect( - page.locator('input[data-test="secret-password"]'), + secretPage.getPasswordInput(), ).toHaveAttribute('type', 'password'); }); }); diff --git a/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts b/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts index f5ad9a88267..f5f2bc40541 100644 --- a/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { test, expect } from '../../../../fixtures'; +import { warmupSPA } from '../../../../pages/base-page'; import { SecretPage } from '../../../../pages/secret-page'; const fixturesDir = path.resolve(import.meta.dirname, '..', '..', '..', '..', 'fixtures', 'secrets'); @@ -22,11 +23,11 @@ test.describe('Key/value secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Create binary file secret', async () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('generic'); - await expect(page.getByTestId('page-heading')).toContainText('Create key/value secret'); + await expect(secretPage.getPageHeading()).toContainText('Create key/value secret'); await secretPage.fillName(secretName); - await page.getByTestId('secret-key').fill(secretKey); + await secretPage.fillSecretKey(secretKey); await secretPage.uploadFile(path.join(fixturesDir, 'binarysecret.bin')); - await expect(page.getByTestId('file-input-binary-alert')).toBeVisible(); + await expect(secretPage.getBinaryAlert()).toBeVisible(); await secretPage.save(); }); @@ -39,10 +40,10 @@ test.describe('Key/value secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Edit secret key and verify data preserved', async () => { await secretPage.editSecret(); - await expect(page.getByTestId('page-heading')).toContainText('Edit key/value secret'); - await page.getByTestId('secret-key').clear(); - await page.getByTestId('secret-key').fill(modifiedKey); - await expect(page.getByTestId('file-input-binary-alert')).toBeVisible(); + await expect(secretPage.getPageHeading()).toContainText('Edit key/value secret'); + await secretPage.getSecretKeyInput().clear(); + await secretPage.fillSecretKey(modifiedKey); + await expect(secretPage.getBinaryAlert()).toBeVisible(); await secretPage.save(); await secretPage.detailsPageIsLoaded(secretName); const secret = await k8sClient.getSecret(secretName, ns); @@ -67,10 +68,10 @@ test.describe('Key/value secrets', { tag: ['@admin', '@crud'] }, () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('generic'); await secretPage.fillName(secretName); - await page.getByTestId('secret-key').fill(secretKey); + await secretPage.fillSecretKey(secretKey); await secretPage.uploadFile(path.join(fixturesDir, 'asciisecret.txt')); - await expect(page.locator('[data-test-id="file-input-textarea"]')).toContainText(asciiContent); - await expect(page.getByTestId('file-input-binary-alert')).toBeHidden(); + await expect(secretPage.getFileInputTextarea()).toContainText(asciiContent); + await expect(secretPage.getBinaryAlert()).toBeHidden(); await secretPage.save(); }); @@ -98,10 +99,10 @@ test.describe('Key/value secrets', { tag: ['@admin', '@crud'] }, () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('generic'); await secretPage.fillName(secretName); - await page.getByTestId('secret-key').fill(secretKey); + await secretPage.fillSecretKey(secretKey); await secretPage.uploadFile(path.join(fixturesDir, 'unicodesecret.utf8')); - await expect(page.locator('[data-test-id="file-input-textarea"]')).toContainText(unicodeContent); - await expect(page.getByTestId('file-input-binary-alert')).toBeHidden(); + await expect(secretPage.getFileInputTextarea()).toContainText(unicodeContent); + await expect(secretPage.getBinaryAlert()).toBeHidden(); await secretPage.save(); }); @@ -146,4 +147,54 @@ test.describe('Key/value secrets', { tag: ['@admin', '@crud'] }, () => { expect((secret as any).data).toBeDefined(); }); }); + + test('editing text field does not corrupt binary data (OCPBUGS-70273)', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-secret-kv-mixed-${Date.now()}`; + const secretName = 'mixed-secret'; + const textKey = 'textfield'; + const textValue = 'original-password'; + const updatedTextValue = 'updated-password'; + const binaryKey = 'binaryfield'; + const binaryBase64 = fs.readFileSync(path.join(fixturesDir, 'binarysecret.bin')).toString('base64'); + const secretPage = new SecretPage(page); + + await test.step('Set up namespace and mixed secret', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createSecret(ns, { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: secretName, namespace: ns }, + data: { + [textKey]: Buffer.from(textValue).toString('base64'), + [binaryKey]: binaryBase64, + }, + } as any); + }); + + await test.step('Edit only the text field', async () => { + await warmupSPA(page); + await page.goto(`/k8s/ns/${ns}/secrets/${secretName}`); + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.editSecret(); + await expect(secretPage.getPageHeading()).toContainText('Edit key/value secret'); + await expect(secretPage.getBinaryAlert()).toBeVisible(); + const textArea = secretPage.getFileInputTextarea().first(); + await textArea.clear(); + await textArea.fill(updatedTextValue); + await secretPage.save(); + }); + + await test.step('Verify text updated and binary preserved', async () => { + await secretPage.detailsPageIsLoaded(secretName); + await secretPage.revealValues(); + await expect(secretPage.getClipboards().first()).toContainText(updatedTextValue); + const secret = await k8sClient.getSecret(secretName, ns); + expect((secret as any).data[binaryKey]).toBe(binaryBase64); + }); + }); }); diff --git a/frontend/e2e/tests/console/crud/secrets/source.spec.ts b/frontend/e2e/tests/console/crud/secrets/source.spec.ts index d26118bba1e..6820b8139bd 100644 --- a/frontend/e2e/tests/console/crud/secrets/source.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/source.spec.ts @@ -19,7 +19,7 @@ test.describe('Source secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Create basic source secret', async () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('source'); - await expect(page.getByTestId('page-heading')).toContainText('Create source secret'); + await expect(secretPage.getPageHeading()).toContainText('Create source secret'); await secretPage.fillName(secretName); await secretPage.fillBasicAuth('username', 'password'); await secretPage.save(); @@ -35,13 +35,13 @@ test.describe('Source secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Edit secret', async () => { await secretPage.editSecret(); - await expect(page.getByTestId('page-heading')).toContainText('Edit source secret'); - await expect(page.getByTestId('secret-username')).toHaveValue('username'); - await expect(page.getByTestId('secret-password')).toHaveValue('password'); - await page.getByTestId('secret-username').clear(); - await page.getByTestId('secret-username').fill('usernameUpdated'); - await page.getByTestId('secret-password').clear(); - await page.getByTestId('secret-password').fill('passwordUpdated'); + await expect(secretPage.getPageHeading()).toContainText('Edit source secret'); + await expect(secretPage.getUsernameInput()).toHaveValue('username'); + await expect(secretPage.getPasswordInput()).toHaveValue('password'); + await secretPage.getUsernameInput().clear(); + await secretPage.getUsernameInput().fill('usernameUpdated'); + await secretPage.getPasswordInput().clear(); + await secretPage.getPasswordInput().fill('passwordUpdated'); await secretPage.save(); }); @@ -77,10 +77,10 @@ test.describe('Source secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Create SSH source secret', async () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('source'); - await expect(page.getByTestId('page-heading')).toContainText('Create source secret'); + await expect(secretPage.getPageHeading()).toContainText('Create source secret'); await secretPage.fillName(secretName); await secretPage.selectAuthType('kubernetes.io/ssh-auth'); - await page.locator('[data-test-id="file-input-textarea"]').fill(sshKey); + await secretPage.fillSshKey(sshKey); await secretPage.save(); }); @@ -93,11 +93,10 @@ test.describe('Source secrets', { tag: ['@admin', '@crud'] }, () => { await test.step('Edit secret', async () => { await secretPage.editSecret(); - await expect(page.getByTestId('page-heading')).toContainText('Edit source secret'); - const textarea = page.locator('[data-test-id="file-input-textarea"]'); - await expect(textarea).toContainText(sshKey); - await textarea.clear(); - await textarea.fill(sshKeyUpdated); + await expect(secretPage.getPageHeading()).toContainText('Edit source secret'); + await expect(secretPage.getSshKeyTextarea()).toContainText(sshKey); + await secretPage.getSshKeyTextarea().clear(); + await secretPage.fillSshKey(sshKeyUpdated); await secretPage.save(); }); diff --git a/frontend/e2e/tests/console/crud/secrets/webhook.spec.ts b/frontend/e2e/tests/console/crud/secrets/webhook.spec.ts index d0938abfd8c..41abfee094e 100644 --- a/frontend/e2e/tests/console/crud/secrets/webhook.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/webhook.spec.ts @@ -20,9 +20,9 @@ test.describe('Webhook secret', { tag: ['@admin', '@crud'] }, () => { await test.step('Create webhook secret', async () => { await page.goto(`/k8s/ns/${ns}/secrets`); await secretPage.clickCreateSecretDropdownButton('webhook'); - await expect(page.getByTestId('page-heading')).toContainText('Create webhook secret'); + await expect(secretPage.getPageHeading()).toContainText('Create webhook secret'); await secretPage.fillName(secretName); - await page.getByTestId('secret-key').fill(webhookKey); + await secretPage.fillSecretKey(webhookKey); await secretPage.save(); }); @@ -35,7 +35,7 @@ test.describe('Webhook secret', { tag: ['@admin', '@crud'] }, () => { await test.step('Edit and regenerate key', async () => { await secretPage.editSecret(); - await expect(page.getByTestId('page-heading')).toContainText('Edit webhook secret'); + await expect(secretPage.getPageHeading()).toContainText('Edit webhook secret'); await secretPage.generateWebhookKey(); await secretPage.save(); }); @@ -43,7 +43,7 @@ test.describe('Webhook secret', { tag: ['@admin', '@crud'] }, () => { await test.step('Verify key changed', async () => { await secretPage.detailsPageIsLoaded(secretName); await secretPage.revealValues(); - await expect(page.getByTestId('copy-to-clipboard').first()).not.toHaveText(webhookKey); + await expect(secretPage.getClipboards().first()).not.toHaveText(webhookKey); }); await test.step('Delete secret', async () => { diff --git a/frontend/packages/integration-tests/fixtures/asciisecret.txt b/frontend/packages/integration-tests/fixtures/asciisecret.txt deleted file mode 100644 index 7c3fb9387ab..00000000000 --- a/frontend/packages/integration-tests/fixtures/asciisecret.txt +++ /dev/null @@ -1 +0,0 @@ -This is ascii \ No newline at end of file diff --git a/frontend/packages/integration-tests/fixtures/binarysecret.bin b/frontend/packages/integration-tests/fixtures/binarysecret.bin deleted file mode 100644 index 994e78bd5b2d8ccbe25d8bd248e2e4675b4f7c38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 497 zcmZQ&VqjnhXJBFwa&q_f~2f&5UAnJT+Y{&L9vX`R2NC z%pnuoTHmib%BrP+uFt2Y6+#ROY5IIOEY_I2J($J%fxSXlL4YYms{T5M>9LhBSfxaR evVm4o)mMTH0v5|uGB>g~Wco?G`)GV9R}}z>0gEO8 diff --git a/frontend/packages/integration-tests/fixtures/unicodesecret.utf8 b/frontend/packages/integration-tests/fixtures/unicodesecret.utf8 deleted file mode 100644 index aaaf48d6610..00000000000 --- a/frontend/packages/integration-tests/fixtures/unicodesecret.utf8 +++ /dev/null @@ -1 +0,0 @@ -ȹȻɣʄɣɱɰ \ No newline at end of file diff --git a/frontend/packages/integration-tests/tests/crud/secrets/add-to-workload.cy.ts b/frontend/packages/integration-tests/tests/crud/secrets/add-to-workload.cy.ts deleted file mode 100644 index 61bf5825f09..00000000000 --- a/frontend/packages/integration-tests/tests/crud/secrets/add-to-workload.cy.ts +++ /dev/null @@ -1,116 +0,0 @@ -import * as _ from 'lodash'; -import type { DeploymentKind } from '@console/internal/module/k8s'; -import { checkErrors, testName } from '../../../support'; -import { modal } from '../../../views/modal'; -import { secrets } from '../../../views/secret'; - -const secretName = 'test-secret'; -const resourceName = 'test-deploy'; -const resourceKind = 'deployment'; -const envPrefix = 'env-'; -const mountPath = '/tmp/testdata'; -const deployment: DeploymentKind = { - apiVersion: 'apps/v1', - kind: 'Deployment', - metadata: { - name: resourceName, - namespace: testName, - }, - spec: { - selector: { - matchLabels: { - test: 'add-secret-to-workload', - }, - }, - template: { - metadata: { - labels: { - test: 'add-secret-to-workload', - }, - }, - spec: { - containers: [ - { - name: 'httpd', - image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', - }, - ], - }, - }, - }, -}; - -describe('Add Secret to Workloads', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - cy.exec(`echo '${JSON.stringify(deployment)}' | oc create -n ${testName} -f -`); - cy.exec( - `oc create secret generic ${secretName} --from-literal=key1=supersecret -n ${testName}`, - ); - }); - - beforeEach(() => { - cy.visit(`/k8s/ns/${testName}/secrets/${secretName}`); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.deleteProjectWithCLI(testName); - }); - - it(`Adds Secret to Deployment as Environment Variables`, () => { - cy.log('Add Secret'); - secrets.addSecretToWorkload(resourceName); - cy.byTestID('Environment variables-radio-input').click(); - cy.byTestID('add-secret-to-workload-prefix').type(envPrefix); - modal.submitShouldBeEnabled(); - modal.submit(); - - cy.log('Verify Secret'); - secrets.getResourceJSON(resourceName, testName, resourceKind).then((resourceJSON) => { - const resource = JSON.parse(resourceJSON.stdout); - const name = _.get( - resource, - 'spec.template.spec.containers[0].envFrom[0].secretRef.name', - undefined, - ); - expect(name).to.equal(secretName); - const prefix = _.get( - resource, - 'spec.template.spec.containers[0].envFrom[0].prefix', - undefined, - ); - expect(prefix).to.equal(envPrefix); - }); - }); - - it(`Adds Secret to Deployment as Volume`, () => { - cy.log('Add Secret'); - secrets.addSecretToWorkload(resourceName); - cy.byTestID('Volume-radio-input').click(); - cy.byTestID('add-secret-to-workload-mountpath').type(mountPath); - modal.submitShouldBeEnabled(); - modal.submit(); - - cy.log('Verify Secret'); - secrets.getResourceJSON(resourceName, testName, resourceKind).then((resourceJSON) => { - const resource = JSON.parse(resourceJSON.stdout); - const name = _.get( - resource, - 'spec.template.spec.containers[0].volumeMounts[0].name', - undefined, - ); - expect(name).to.equal(secretName); - const mp = _.get( - resource, - 'spec.template.spec.containers[0].volumeMounts[0].mountPath', - undefined, - ); - expect(mp).to.equal(mountPath); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/crud/secrets/image-pull.cy.ts b/frontend/packages/integration-tests/tests/crud/secrets/image-pull.cy.ts deleted file mode 100644 index 018ba794fa6..00000000000 --- a/frontend/packages/integration-tests/tests/crud/secrets/image-pull.cy.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { checkErrors, testName } from '../../../support'; -import { detailsPage } from '../../../views/details-page'; -import { secrets } from '../../../views/secret'; - -const heading = 'Create image pull secret'; - -describe('Image pull secrets', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - beforeEach(function () { - // Skip beforeEach for the obfuscated passwords test - if (this.currentTest?.title === 'Passwords entered on the console are obfuscated') { - return; - } - // ensure the test project is selected to avoid flakes - cy.visit(`/k8s/cluster/projects/${testName}`); - cy.visit(`/k8s/ns/${testName}/secrets/`); - secrets.clickCreateSecretDropdownButton('image'); - }); - - afterEach(() => { - const credentialsImageSecretName = `registry-credentials-image-secret-${testName}`; - const uploadConfigFileImageSecretName = `upload-configuration-file-image-secret-${testName}`; - cy.exec( - `oc delete secret -n ${testName} ${credentialsImageSecretName} ${uploadConfigFileImageSecretName}`, - { - failOnNonZeroExit: false, - }, - ); - checkErrors(); - }); - - after(() => { - cy.exec(`oc delete project ${testName} --wait=false`); - }); - - it(`Creates, edits, and deletes an image registry credentials pull secret`, () => { - const credentialsImageSecretName = `registry-credentials-image-secret-${testName}`; - const address = 'https://index.openshift.io/v'; - const addressUpdated = 'https://index.openshift.io/updated/v1'; - const username = 'username'; - const password = 'password'; - const username0 = `${username}0`; - const password0 = `${password}0`; - const username1 = `${username}1`; - const password1 = `${password}1`; - const usernameUpdated = `${username}Updated`; - const passwordUpdated = `${password}Updated`; - const mail = 'test@secret.com'; - const mail0 = `${mail}0`; - const mail1 = `${mail}1`; - const mailUpdated = 'testUpdated@secret.com'; - - const credentialsToCheck = { - '.dockerconfigjson': { - auths: { - 'https://index.openshift.io/v0': { - username: username0, - password: password0, - auth: secrets.encode(username0, password0), - email: mail0, - }, - 'https://index.openshift.io/v1': { - username: username1, - password: password1, - auth: secrets.encode(username1, password1), - email: mail1, - }, - }, - }, - }; - const updatedCredentialsToCheck = { - '.dockerconfigjson': { - auths: { - 'https://index.openshift.io/updated/v1': { - username: usernameUpdated, - password: passwordUpdated, - auth: secrets.encode(usernameUpdated, passwordUpdated), - email: mailUpdated, - }, - }, - }, - }; - - cy.log('Create secret'); - cy.byTestID('page-heading').contains(heading); - secrets.enterSecretName(credentialsImageSecretName); - secrets.clickAddCredentialsButton(); - cy.get('[data-test-id="create-image-secret-form"]').each(($el, index) => { - cy.wrap($el).find('[data-test="image-secret-address"]').type(`${address}${index}`); - cy.wrap($el).find('[data-test="image-secret-username"]').type(`${username}${index}`); - cy.wrap($el).find('[data-test="image-secret-password"]').type(`${password}${index}`); - cy.wrap($el).find('[data-test="image-secret-email"]').type(`${mail}${index}`); - }); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - - // Navigate to secret details page (save may go to list page) - cy.url({ timeout: 30000 }).then((url) => { - if (!url.includes(`/secrets/${credentialsImageSecretName}`)) { - cy.visit(`/k8s/ns/${testName}/secrets/${credentialsImageSecretName}`); - } - }); - secrets.detailsPageIsLoaded(credentialsImageSecretName); - - cy.log('Verify secret'); - secrets.checkSecret(credentialsToCheck, true); - - cy.log('Edit secret with whitespace in input values'); - detailsPage.clickPageActionFromDropdown('Edit Secret'); - // Wait for form to load - cy.byTestID('page-heading').contains('Edit image pull secret'); - cy.get('[data-test-id="create-image-secret-form"]').should('have.length', 2); - secrets.clickRemoveEntryButton(); - cy.byTestID('image-secret-address').clear(); - cy.byTestID('image-secret-address').type(` ${addressUpdated} `); - cy.byTestID('image-secret-username').clear(); - cy.byTestID('image-secret-username').type(` ${usernameUpdated} `); - cy.byTestID('image-secret-password').clear(); - cy.byTestID('image-secret-password').type(` ${passwordUpdated} `); - cy.byTestID('image-secret-email').clear(); - cy.byTestID('image-secret-email').type(` ${mailUpdated} `); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - - // Navigate to secret details page (save may go to list page) - cy.url({ timeout: 30000 }).then((url) => { - if (!url.includes(`/secrets/${credentialsImageSecretName}`)) { - cy.visit(`/k8s/ns/${testName}/secrets/${credentialsImageSecretName}`); - } - }); - - cy.log('Verify edit, whitespace in input values are removed'); - secrets.detailsPageIsLoaded(credentialsImageSecretName); - secrets.checkSecret(updatedCredentialsToCheck, true); - - cy.log('Delete secret'); - secrets.deleteSecret(credentialsImageSecretName); - }); - - it(`Creates and deletes an upload configuration file image pull secret`, () => { - const uploadConfigFileImageSecretName = `upload-configuration-file-image-secret-${testName}`; - const username = 'username'; - const password = 'password'; - const configFile = { - auths: { - 'https://index.openshift.io/v1': { - username, - password, - auth: secrets.encode(username, password), - email: 'test@secret.com', - }, - }, - }; - - cy.log('Create secret'); - cy.byTestID('page-heading').contains(heading); - secrets.enterSecretName(uploadConfigFileImageSecretName); - cy.byTestID('console-select-auth-type-menu-toggle').click(); - cy.byTestDropDownMenu('config-file').click(); - - // Type the JSON config to properly trigger React state updates and Yup validation - const configJson = JSON.stringify(configFile); - cy.byLegacyTestID('file-input-textarea') - .clear() - .type(configJson, { delay: 0, parseSpecialCharSequences: false }); - - // Wait for validation to complete and save button to be enabled - cy.byTestID('save-changes', { timeout: 30000 }).should('be.visible').and('be.enabled'); - - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - - // Navigate to secret details page (save may go to list page) - cy.url({ timeout: 30000 }).then((url) => { - if (!url.includes(`/secrets/${uploadConfigFileImageSecretName}`)) { - // If we're on list page, click on the secret to go to details - cy.visit(`/k8s/ns/${testName}/secrets/${uploadConfigFileImageSecretName}`); - } - }); - secrets.detailsPageIsLoaded(uploadConfigFileImageSecretName); - - cy.log('Verify secret'); - secrets.checkSecret( - { - '.dockerconfigjson': configFile, - }, - true, - ); - - cy.log('Delete secret'); - secrets.deleteSecret(uploadConfigFileImageSecretName); - }); - it(`Passwords entered on the console are obfuscated`, () => { - // Navigate to secrets page and open image secret form - cy.visit(`/k8s/ns/${testName}/secrets/`); - secrets.clickCreateSecretDropdownButton('image'); - cy.get('input[data-test="image-secret-password"]').should('have.attr', 'type', 'password'); - cy.get('button[id="cancel"]').click(); - - // Open source secret form - secrets.clickCreateSecretDropdownButton('source'); - cy.get('input[data-test="secret-password"]').should('have.attr', 'type', 'password'); - - // Clean up - navigate back to secrets list to close any open forms - cy.visit(`/k8s/ns/${testName}/secrets/`); - }); -}); diff --git a/frontend/packages/integration-tests/tests/crud/secrets/key-value.cy.ts b/frontend/packages/integration-tests/tests/crud/secrets/key-value.cy.ts deleted file mode 100644 index 814f0f45851..00000000000 --- a/frontend/packages/integration-tests/tests/crud/secrets/key-value.cy.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { checkErrors, testName } from '../../../support'; -import { detailsPage } from '../../../views/details-page'; -import { listPage } from '../../../views/list-page'; -import { nav } from '../../../views/nav'; -import { secrets } from '../../../views/secret'; - -const populateSecretForm = (name: string, key: string, fileName: string) => { - cy.get('[data-test="page-heading"] h1').contains('Create key/value secret'); - cy.byTestID('secret-name').should('exist'); - cy.byLegacyTestID('file-input-textarea').should('exist'); - secrets.enterSecretName(name); - cy.byTestID('secret-key').type(key); - cy.get('.co-file-input').selectFile( - `${Cypress.config('fileServerFolder')}/fixtures/${fileName}`, - { - action: 'drag-drop', - force: true, - }, - ); -}; - -const modifySecretForm = (key: string) => { - detailsPage.clickPageActionFromDropdown('Edit Secret'); - cy.get('[data-test="page-heading"] h1').contains('Edit key/value secret'); - cy.byTestID('secret-key').clear().type(key); -}; - -describe('Create key/value secrets', () => { - const binarySecretName = `key-value-binary-secret-${testName}`; - const asciiSecretName = `key-value-ascii-secret-${testName}`; - const unicodeSecretName = `key-value-unicode-secret-${testName}`; - const tlsSecretName = `key-value-tls-secret-${testName}`; - const binaryFilename = 'binarysecret.bin'; - const asciiFilename = 'asciisecret.txt'; - const unicodeFilename = 'unicodesecret.utf8'; - const secretKey = `secretkey`; - const modifiedSecretKey = 'modifiedsecretkey'; - const tlsSecretYaml = ` -apiVersion: v1 -kind: Secret -metadata: - name: ${tlsSecretName} -type: kubernetes.io/tls -data: - tls.crt: QUFBCg== - tls.key: QkJCCg== -`; - - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - cy.exec(`echo '${tlsSecretYaml}' | oc create -f - -n ${testName}`); - }); - - beforeEach(() => { - // ensure the test project is selected to avoid flakes - cy.visit(`/k8s/cluster/projects/${testName}`); - nav.sidenav.clickNavLink(['Workloads', 'Secrets']); - listPage.titleShouldHaveText('Secrets'); - secrets.clickCreateSecretDropdownButton('generic'); - }); - - afterEach(() => { - cy.exec( - `oc delete secret -n ${testName} ${binarySecretName} ${asciiSecretName} ${unicodeSecretName}`, - { - failOnNonZeroExit: false, - }, - ); - checkErrors(); - }); - - after(() => { - cy.deleteProjectWithCLI(testName); - }); - - it(`Validate create and edit of a key/value secret whose value is a binary file`, () => { - populateSecretForm(binarySecretName, secretKey, binaryFilename); - cy.byLegacyTestID('file-input-textarea').should('not.exist'); - cy.byTestID('file-input-binary-alert').should('exist'); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(binarySecretName); - cy.exec( - `oc get secret -n ${testName} ${binarySecretName} --template '{{.data.${secretKey}}}'`, - { - failOnNonZeroExit: false, - }, - ).then((value) => { - cy.fixture(binaryFilename, 'base64').then((binarySecret) => { - expect(binarySecret).toEqual(value.stdout); - }); - }); - modifySecretForm(modifiedSecretKey); - cy.byTestID('file-input-binary-alert').should('exist'); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(binarySecretName); - cy.exec( - `oc get secret -n ${testName} ${binarySecretName} --template '{{.data.${modifiedSecretKey}}}'`, - { - failOnNonZeroExit: false, - }, - ).then((value) => { - cy.fixture(binaryFilename, 'base64').then((binarySecret) => { - expect(binarySecret).toEqual(value.stdout); - }); - }); - }); - - it(`Validate a key/value secret whose value is an ascii file`, () => { - populateSecretForm(asciiSecretName, secretKey, asciiFilename); - cy.fixture(asciiFilename, 'ascii').then((asciiSecret) => { - cy.byLegacyTestID('file-input-textarea').should('contain.text', asciiSecret); - cy.byTestID('file-input-binary-alert').should('not.exist'); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(asciiSecretName); - cy.exec( - `oc get secret -n ${testName} ${asciiSecretName} --template '{{.data.${secretKey}}}' | base64 -d`, - { - failOnNonZeroExit: false, - }, - ).then((value) => { - expect(asciiSecret).toEqual(value.stdout); - }); - }); - }); - - it(`Validate a key/value secret whose value is a unicode file`, () => { - populateSecretForm(unicodeSecretName, secretKey, unicodeFilename); - cy.fixture(unicodeFilename, 'utf8').then((unicodeSecret) => { - cy.byLegacyTestID('file-input-textarea').should('contain.text', unicodeSecret); - cy.byTestID('file-input-binary-alert').should('not.exist'); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(unicodeSecretName); - cy.exec( - `oc get secret -n ${testName} ${unicodeSecretName} --template '{{.data.${secretKey}}}' | base64 -d`, - { - failOnNonZeroExit: false, - }, - ).then((value) => { - expect(unicodeSecret).toEqual(value.stdout); - }); - }); - }); - - it('Validate tls secret is editable', () => { - cy.visit(`/k8s/ns/${testName}/secrets/${tlsSecretName}/edit`); - secrets.addKeyValue('keyfortest', 'valuefortest'); - secrets.save(); - secrets.detailsPageIsLoaded(tlsSecretName); - secrets.checkKeyValueExist('keyfortest', 'valuefortest'); - }); - - it('Validate editing text field does not corrupt binary data (OCPBUGS-70273)', () => { - const mixedSecretName = `key-value-mixed-secret-${testName}`; - const textKey = 'textfield'; - const textValue = 'original-password'; - const updatedTextValue = 'updated-password'; - const binaryKey = 'binaryfield'; - - // Create a secret with both text and binary data using CLI - cy.exec( - `oc create secret generic ${mixedSecretName} -n ${testName} --from-literal=${textKey}=${textValue} --from-file=${binaryKey}=${Cypress.config( - 'fileServerFolder', - )}/fixtures/${binaryFilename}`, - ); - - // Capture the original binary data - cy.exec( - `oc get secret -n ${testName} ${mixedSecretName} --template '{{.data.${binaryKey}}}'`, - ).then((originalBinary) => { - // Edit the secret via the console - cy.visit(`/k8s/ns/${testName}/secrets/${mixedSecretName}`); - detailsPage.isLoaded(); - detailsPage.clickPageActionFromDropdown('Edit Secret'); - - // Modify only the text field - cy.byTestID('secret-key') - .should('have.length', 2) - .each(($el) => { - if ($el.val() === textKey) { - // Find the corresponding value textarea and update it - cy.byLegacyTestID('file-input-textarea').first().clear().type(updatedTextValue); - } - }); - - // Verify binary field shows the binary alert (indicates it's still treated as binary) - cy.byTestID('file-input-binary-alert').should('exist'); - - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - - // Verify the text field was updated - secrets.clickRevealValues(); - cy.byTestID('copy-to-clipboard').should('contain.text', updatedTextValue); - - // Verify the binary data was NOT corrupted - cy.exec( - `oc get secret -n ${testName} ${mixedSecretName} --template '{{.data.${binaryKey}}}'`, - ).then((updatedBinary) => { - expect(updatedBinary.stdout).to.equal(originalBinary.stdout); - }); - - // Cleanup - cy.exec(`oc delete secret -n ${testName} ${mixedSecretName}`, { - failOnNonZeroExit: false, - }); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/crud/secrets/source.cy.ts b/frontend/packages/integration-tests/tests/crud/secrets/source.cy.ts deleted file mode 100644 index 293b649aece..00000000000 --- a/frontend/packages/integration-tests/tests/crud/secrets/source.cy.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { checkErrors, testName } from '../../../support'; -import { detailsPage } from '../../../views/details-page'; -import { secrets } from '../../../views/secret'; - -describe('Source secrets', () => { - const basicSourceSecretName = `basic-source-secret-${testName}`; - const basicSourceSecretUsername = 'username'; - const basicSourceSecretUsernameUpdated = 'usernameUpdated'; - const basicSourceSecretPassword = 'password'; - const basicSourceSecretPasswordUpdated = 'passwordUpdated'; - const sshSourceSecretName = `ssh-source-secret-${testName}`; - const sshSourceSecretSSHKey = 'sshKey'; - const sshSourceSecretSSHKeUpdated = 'sshKeyUpdated'; - - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - beforeEach(() => { - // ensure the test project is selected to avoid flakes - cy.visit(`/k8s/cluster/projects/${testName}`); - cy.visit(`/k8s/ns/${testName}/secrets/`); - secrets.clickCreateSecretDropdownButton('source'); - }); - - afterEach(() => { - cy.exec(`oc delete secret -n ${testName} ${basicSourceSecretName} ${sshSourceSecretName}`, { - failOnNonZeroExit: false, - }); - checkErrors(); - }); - - after(() => { - cy.deleteProjectWithCLI(testName); - }); - - it(`Creates, edits, and deletes a basic source secret`, () => { - cy.log('Create secret'); - cy.byTestID('page-heading').contains('Create source secret'); - secrets.enterSecretName(basicSourceSecretName); - cy.byTestID('secret-username').type(basicSourceSecretUsername); - cy.byTestID('secret-password').type(basicSourceSecretPassword); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - secrets.detailsPageIsLoaded(basicSourceSecretName); - - cy.log('Verify secret'); - secrets.checkSecret({ - password: basicSourceSecretPassword, - username: basicSourceSecretUsername, - }); - - cy.log('Edit secret'); - detailsPage.clickPageActionFromDropdown('Edit Secret'); - // Wait for form to load and hydrate with current values - cy.byTestID('page-heading').contains('Edit source secret'); - cy.byTestID('secret-username').should('have.value', basicSourceSecretUsername); - cy.byTestID('secret-password').should('have.value', basicSourceSecretPassword); - cy.byTestID('secret-username').clear(); - cy.byTestID('secret-username').type(basicSourceSecretUsernameUpdated); - cy.byTestID('secret-password').clear(); - cy.byTestID('secret-password').type(basicSourceSecretPasswordUpdated); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - - cy.log('Verify edit'); - secrets.detailsPageIsLoaded(basicSourceSecretName); - secrets.checkSecret({ - password: basicSourceSecretPasswordUpdated, - username: basicSourceSecretUsernameUpdated, - }); - - cy.log('Delete secret'); - secrets.deleteSecret(basicSourceSecretName); - }); - - it(`Creates, edits, and deletes a SSH source secret`, () => { - cy.log('Create secret'); - cy.byTestID('page-heading').contains('Create source secret'); - secrets.enterSecretName(sshSourceSecretName); - cy.byTestID('console-select-auth-type-menu-toggle').click(); - cy.byTestDropDownMenu('kubernetes.io/ssh-auth').click(); - cy.byLegacyTestID('file-input-textarea').type(sshSourceSecretSSHKey); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - secrets.detailsPageIsLoaded(sshSourceSecretName); - - cy.log('Verify secret'); - secrets.checkSecret({ - 'ssh-privatekey': `${sshSourceSecretSSHKey}\n`, - }); - - cy.log('Edit secret'); - detailsPage.clickPageActionFromDropdown('Edit Secret'); - // Wait for form to load and hydrate with current values - cy.byTestID('page-heading').contains('Edit source secret'); - cy.byLegacyTestID('file-input-textarea').should('contain.value', sshSourceSecretSSHKey); - cy.byLegacyTestID('file-input-textarea').clear(); - cy.byLegacyTestID('file-input-textarea').type(sshSourceSecretSSHKeUpdated); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - - cy.log('Verify edit'); - secrets.detailsPageIsLoaded(sshSourceSecretName); - secrets.checkSecret({ - 'ssh-privatekey': `${sshSourceSecretSSHKeUpdated}\n`, - }); - - cy.log('Delete secret'); - secrets.deleteSecret(sshSourceSecretName); - }); -}); diff --git a/frontend/packages/integration-tests/tests/crud/secrets/webhook.cy.ts b/frontend/packages/integration-tests/tests/crud/secrets/webhook.cy.ts deleted file mode 100644 index e611f3bca22..00000000000 --- a/frontend/packages/integration-tests/tests/crud/secrets/webhook.cy.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { checkErrors, testName } from '../../../support'; -import { detailsPage } from '../../../views/details-page'; -import { secrets } from '../../../views/secret'; - -describe('Webhook secret', () => { - const webhookSecretName = `webhook-secret-${testName}`; - const webhookSecretKey = 'webhookValue'; - - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - beforeEach(() => { - // ensure the test project is selected to avoid flakes - cy.visit(`/k8s/cluster/projects/${testName}`); - cy.visit(`/k8s/ns/${testName}/secrets/`); - secrets.clickCreateSecretDropdownButton('webhook'); - }); - - afterEach(() => { - cy.exec(`oc delete secret -n ${testName} ${webhookSecretName}`, { - failOnNonZeroExit: false, - }); - checkErrors(); - }); - - after(() => { - cy.deleteProjectWithCLI(testName); - }); - - it(`Create, edit, and delete a webhook secret`, () => { - cy.log('Create secret'); - cy.byTestID('page-heading').contains('Create webhook secret'); - secrets.enterSecretName(webhookSecretName); - cy.byTestID('secret-key').type(webhookSecretKey); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - secrets.detailsPageIsLoaded(webhookSecretName); - - cy.log('Verify secret'); - secrets.checkSecret({ - WebHookSecretKey: webhookSecretKey, - }); - - cy.log('Edit secret'); - detailsPage.clickPageActionFromDropdown('Edit Secret'); - // Wait for form to load - cy.byTestID('page-heading').contains('Edit webhook secret'); - cy.byTestID('webhook-generate-button').should('be.visible'); - cy.byTestID('webhook-generate-button').click(); - secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - - cy.log('Verify edit'); - secrets.detailsPageIsLoaded(webhookSecretName); - secrets.clickRevealValues(); - cy.get('.co-copy-to-clipboard__text .co-copy-to-clipboard__code') - .eq(0) - .invoke('text') - .should('not.equal', webhookSecretKey); - - cy.log('Delete secret'); - secrets.deleteSecret(webhookSecretName); - }); -}); diff --git a/frontend/packages/integration-tests/views/secret.ts b/frontend/packages/integration-tests/views/secret.ts deleted file mode 100644 index 8b41473d1cf..00000000000 --- a/frontend/packages/integration-tests/views/secret.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Base64 } from 'js-base64'; -import { detailsPage } from './details-page'; -import { listPage } from './list-page'; -import { modal } from './modal'; - -export const secrets = { - addSecretToWorkload: (resourceName: string) => { - cy.byTestID('Add Secret to workload').click(); - modal.shouldBeOpened(); - modal.modalTitleShouldContain('Add secret to workload'); - cy.get('#co-add-secret-to-workload__workload').click(); - cy.byTestID('console-select-search-input').type(resourceName); - cy.byTestID('console-select-item').click(); - }, - addKeyValue: (key: string, value: string) => { - cy.byTestID('add-credentials-button').click(); - cy.byTestID('secret-key').last().clear().type(key); - cy.byLegacyTestID('file-input-textarea').last().clear().type(value); - }, - checkSecret: (keyValuesToCheck: object, jsonOutput: boolean = false) => { - secrets.clickRevealValues(); - const renderedKeyValues = {}; - cy.byTestID('secret-data') - .find('[data-test="secret-data-term"]') - .each(($el, index) => { - const key = $el.text(); - cy.get('[data-test="copy-to-clipboard"]') - .eq(index) - .invoke('text') - .then(($text) => { - renderedKeyValues[key] = jsonOutput ? JSON.parse($text) : $text; - }); - }) - .then(() => { - expect(renderedKeyValues).toEqual(keyValuesToCheck); - }); - }, - checkKeyValueExist: (key: string, value: string) => { - // Just for one new added key/value - secrets.clickRevealValues(); - cy.byTestID('secret-data-term').first().should('have.text', key); - cy.get('code').first().should('have.text', value); - }, - clickAddCredentialsButton: () => cy.byTestID('add-credentials-button').click(), - clickRemoveEntryButton: () => cy.byTestID('remove-entry-button').first().click(), - clickRevealValues: () => { - // Wait for page to fully stabilize - cy.byTestID('loading-indicator', { timeout: 5000 }).should('not.exist'); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1000); - // Click reveal-values button with force to handle re-renders - cy.byTestID('reveal-values', { timeout: 30000 }).should('be.visible').click({ force: true }); - // Wait for data to be revealed - cy.byTestID('secret-data', { timeout: 10000 }).should('be.visible'); - }, - clickCreateSecretDropdownButton: (secretType: string) => { - cy.byTestID('item-create') - .click({ force: true }) - .get('body') - .then(($body) => { - if ($body.find(`[data-test-dropdown-menu=${secretType}]`).length) { - cy.get(`[data-test-dropdown-menu=${secretType}]`).click(); - } - }); - }, - deleteSecret: (secretName: string) => { - detailsPage.clickPageActionFromDropdown('Delete Secret'); - modal.shouldBeOpened(); - modal.submit(); - modal.shouldBeClosed(); - listPage.rows.shouldNotExist(secretName); - }, - detailsPageIsLoaded: (secretName: string) => { - // Wait for loading to complete - cy.byTestID('loading-indicator', { timeout: 5000 }).should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(secretName); - // Wait for either secret-data (has data) or empty-box (no data) to be visible - cy.get('[data-test="secret-data"], .pf-v6-c-empty-state', { timeout: 30000 }) - .should('exist') - .and('be.visible'); - }, - encode: (username, password) => Base64.encode(`${username}:${password}`), - enterSecretName: (secretName: string) => cy.byTestID('secret-name').type(secretName), - getResourceJSON: (name: string, namespace: string, kind: string) => { - return cy.exec(`oc get -o json -n ${namespace} ${kind} ${name}`); - }, - save: () => { - cy.byTestID('save-changes', { timeout: 10000 }) - .should('be.visible') - .and('not.be.disabled') - .click(); - // Wait for navigation away from create/edit page - cy.byTestID('save-changes').should('not.exist'); - }, -}; diff --git a/frontend/public/components/modals/add-secret-to-workload.tsx b/frontend/public/components/modals/add-secret-to-workload.tsx index 210558aa6de..d2867bc047f 100644 --- a/frontend/public/components/modals/add-secret-to-workload.tsx +++ b/frontend/public/components/modals/add-secret-to-workload.tsx @@ -207,7 +207,7 @@ const AddSecretToWorkloadModal: FC = (props) => { autocompleteFilter={autocompleteFilter} autocompletePlaceholder={selectWorkloadPlaceholder} id="co-add-secret-to-workload__workload" - data-test="add-secret-to-workload-button" + dataTest="add-secret-to-workload-button" /> = ({ {t('Basic authentication')} - + {t('SSH key')} diff --git a/frontend/public/components/secrets/create-secret/PullSecretCredentialEntry.tsx b/frontend/public/components/secrets/create-secret/PullSecretCredentialEntry.tsx index 87a24943262..264fbd90a51 100644 --- a/frontend/public/components/secrets/create-secret/PullSecretCredentialEntry.tsx +++ b/frontend/public/components/secrets/create-secret/PullSecretCredentialEntry.tsx @@ -48,7 +48,11 @@ export const PullSecretCredentialEntry: FC = ({ ); return ( - + {showRemoveButton && (