This is an automated email from the ASF dual-hosted git repository.
baoyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 1ccd78a40 test: add comprehensive E2E tests for Secrets resource
(#3245)
1ccd78a40 is described below
commit 1ccd78a4089a6643e5f6518dfca67fc3099f9a36
Author: Deep Shekhar Singh <[email protected]>
AuthorDate: Tue Dec 16 15:41:43 2025 +0530
test: add comprehensive E2E tests for Secrets resource (#3245)
---
e2e/pom/secrets.ts | 60 ++++++++++
e2e/tests/secrets.crud-all-fields.spec.ts | 159 +++++++++++++++++++++++++
e2e/tests/secrets.crud-required-fields.spec.ts | 147 +++++++++++++++++++++++
e2e/tests/secrets.list.spec.ts | 100 ++++++++++++++++
4 files changed, 466 insertions(+)
diff --git a/e2e/pom/secrets.ts b/e2e/pom/secrets.ts
new file mode 100644
index 000000000..d89b39bf4
--- /dev/null
+++ b/e2e/pom/secrets.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { uiGoto } from '@e2e/utils/ui';
+import { expect, type Page } from '@playwright/test';
+
+const locator = {
+ getSecretNavBtn: (page: Page) =>
+ page.getByRole('link', { name: 'Secrets' }),
+ getAddSecretBtn: (page: Page) =>
+ page.getByRole('button', { name: 'Add Secret' }),
+ getAddBtn: (page: Page) =>
+ page.getByRole('button', { name: 'Add', exact: true }),
+};
+
+const assert = {
+ isIndexPage: async (page: Page) => {
+ await expect(page).toHaveURL((url) => url.pathname.endsWith('/secrets'));
+ const title = page.getByRole('heading', { name: 'Secrets' });
+ await expect(title).toBeVisible();
+ },
+ isAddPage: async (page: Page) => {
+ await expect(page).toHaveURL((url) =>
+ url.pathname.endsWith('/secrets/add')
+ );
+ const title = page.getByRole('heading', { name: 'Add Secret' });
+ await expect(title).toBeVisible();
+ },
+ isDetailPage: async (page: Page) => {
+ await expect(page).toHaveURL((url) =>
+ url.pathname.includes('/secrets/detail')
+ );
+ const title = page.getByRole('heading', { name: 'Secret Detail' });
+ await expect(title).toBeVisible();
+ },
+};
+
+const goto = {
+ toIndex: (page: Page) => uiGoto(page, '/secrets'),
+ toAdd: (page: Page) => uiGoto(page, '/secrets/add'),
+};
+
+export const secretsPom = {
+ ...locator,
+ ...assert,
+ ...goto,
+};
diff --git a/e2e/tests/secrets.crud-all-fields.spec.ts
b/e2e/tests/secrets.crud-all-fields.spec.ts
new file mode 100644
index 000000000..9ea07dc51
--- /dev/null
+++ b/e2e/tests/secrets.crud-all-fields.spec.ts
@@ -0,0 +1,159 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { secretsPom } from '@e2e/pom/secrets';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { expect } from '@playwright/test';
+
+import { API_SECRETS } from '@/config/constant';
+
+
+const createdSecretId = 'test-aws-secret-all-fields';
+const manager = 'aws';
+
+test.describe('CRUD secret with all fields (AWS)', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ test.afterAll(async () => {
+ // cleanup: delete the secret
+ if (createdSecretId) {
+ await
e2eReq.delete(`${API_SECRETS}/${manager}/${createdSecretId}`).catch((err) => {
+ // ignore 404 error if secret doesn't exist, rethrow others
+ if (err.response?.status !== 404 && !err.message.includes('404')) {
+ throw err;
+ }
+ });
+ }
+ });
+
+ test('should create a secret with all fields', async ({ page }) => {
+ await test.step('create secret via UI', async () => {
+ await secretsPom.toIndex(page);
+ await secretsPom.getAddSecretBtn(page).click();
+ await secretsPom.isAddPage(page);
+
+ await page.getByLabel('ID').fill(createdSecretId);
+
+ // Select Manager AWS
+ const managerSection = page.getByRole('group', { name: 'Secret Manager'
});
+ await managerSection.locator('input.mantine-Select-input').click();
+ await page.getByRole('option', { name: 'aws' }).click();
+
+ await page.getByLabel('Access Key ID').fill('AKIAIOSFODNN7EXAMPLE');
+ await page.getByLabel('Secret Access
Key').fill('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
+ await page.getByLabel('Session Token').fill('test-session-token-123');
+ await page.getByLabel('Region').fill('us-west-2');
+ await page.getByLabel('Endpoint
URL').fill('https://secretsmanager.us-west-2.amazonaws.com');
+
+ await secretsPom.getAddBtn(page).click();
+ });
+
+ await test.step('verify secret appears in UI', async () => {
+ await secretsPom.isIndexPage(page);
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await expect(row).toBeVisible();
+ });
+ });
+
+ test('should read/view the secret details', async ({ page }) => {
+ await test.step('navigate to secret details page and verify UI', async ()
=> {
+ await secretsPom.toIndex(page);
+ await secretsPom.isIndexPage(page);
+
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await row.getByRole('button', { name: 'View' }).click();
+ await secretsPom.isDetailPage(page);
+
+ const pageContent = await page.textContent('body');
+ expect(pageContent).toContain('Secret Manager');
+ await
expect(page.locator('input[name="id"]')).toHaveValue(createdSecretId);
+ // Verify AWS-specific fields are present (labels)
+ expect(pageContent).toContain('Access Key ID');
+ expect(pageContent).toContain('Secret Access Key');
+ expect(pageContent).toContain('Region');
+ });
+ });
+
+ test('should update the secret with new values', async ({ page }) => {
+ const updatedFields = {
+ 'Access Key ID': 'AKIAI44QH8DHBEXAMPLE',
+ 'Secret Access Key': 'je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY',
+ 'Session Token': 'updated-session-token-456',
+ 'Region': 'eu-west-1',
+ 'Endpoint URL': 'https://secretsmanager.eu-west-1.amazonaws.com',
+ };
+
+ await test.step('navigate to secret detail page', async () => {
+ await secretsPom.toIndex(page);
+ await secretsPom.isIndexPage(page);
+
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await row.getByRole('button', { name: 'View' }).click();
+ await secretsPom.isDetailPage(page);
+ });
+
+ await test.step('enter edit mode and update fields', async () => {
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Update AWS fields
+ for (const [label, value] of Object.entries(updatedFields)) {
+ await page.getByLabel(label).clear();
+ await page.getByLabel(label).fill(value);
+ }
+ });
+
+ await test.step('save the changes', async () => {
+ await page.getByRole('button', { name: 'Save' }).click();
+ await secretsPom.isDetailPage(page);
+ });
+
+ await test.step('verify secret was updated via UI', async () => {
+ // Check the actual field values in the detail page
+ for (const [label, value] of Object.entries(updatedFields)) {
+ await expect(page.getByLabel(label)).toHaveValue(value);
+ }
+ });
+ });
+
+ test('should delete the secret', async ({ page }) => {
+ await test.step('navigate to detail page and delete', async () => {
+ await secretsPom.toIndex(page);
+ await secretsPom.isIndexPage(page);
+
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await row.getByRole('button', { name: 'View' }).click();
+ await secretsPom.isDetailPage(page);
+
+ await page.getByRole('button', { name: 'Delete' }).click();
+
+ const deleteDialog = page.getByRole('dialog', { name: 'Delete Secret' });
+ await expect(deleteDialog).toBeVisible();
+ await deleteDialog.getByRole('button', { name: 'Delete' }).click();
+ });
+
+ await test.step('verify deletion and redirect', async () => {
+ await secretsPom.isIndexPage(page);
+ await expect(page.getByRole('cell', { name: createdSecretId
})).toBeHidden();
+ });
+
+ await test.step('verify secret was deleted via API', async () => {
+ await expect(async () => {
+ await e2eReq.get(`${API_SECRETS}/${manager}/${createdSecretId}`);
+ }).rejects.toThrow();
+ });
+ });
+});
diff --git a/e2e/tests/secrets.crud-required-fields.spec.ts
b/e2e/tests/secrets.crud-required-fields.spec.ts
new file mode 100644
index 000000000..fc2fa3b7d
--- /dev/null
+++ b/e2e/tests/secrets.crud-required-fields.spec.ts
@@ -0,0 +1,147 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { secretsPom } from '@e2e/pom/secrets';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { expect } from '@playwright/test';
+
+import { API_SECRETS } from '@/config/constant';
+
+const createdSecretId = 'test-vault-secret-required';
+const manager = 'vault';
+
+test.describe('CRUD secret with required fields only (Vault)', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ test.afterAll(async () => {
+ // cleanup: delete the secret
+ if (createdSecretId) {
+ await
e2eReq.delete(`${API_SECRETS}/${manager}/${createdSecretId}`).catch((err) => {
+ // ignore 404 error if secret doesn't exist, rethrow others
+ if (err.response?.status !== 404 && !err.message.includes('404')) {
+ throw err;
+ }
+ });
+ }
+ });
+
+ test('should create a secret with required fields', async ({ page }) => {
+ await test.step('create secret via UI', async () => {
+ await secretsPom.toIndex(page);
+ await secretsPom.getAddSecretBtn(page).click();
+ await secretsPom.isAddPage(page);
+
+ await page.getByLabel('ID').fill(createdSecretId);
+
+ // Vault is default
+ await page.getByLabel('URI').fill('http://vault.example.com:8200');
+ await page.getByLabel('Prefix').fill('/secret/test');
+ await page.getByLabel('Token').fill('test-vault-token-123');
+
+ await secretsPom.getAddBtn(page).click();
+ });
+
+ await test.step('verify secret appears in UI', async () => {
+ await secretsPom.isIndexPage(page);
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await expect(row).toBeVisible();
+ });
+ });
+
+ test('should read/view the secret details', async ({ page }) => {
+ await test.step('navigate to secret details page and verify UI', async ()
=> {
+ await secretsPom.toIndex(page);
+ await secretsPom.isIndexPage(page);
+
+ // Find and click the View button for the created secret
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await row.getByRole('button', { name: 'View' }).click();
+ await secretsPom.isDetailPage(page);
+
+ // Assert Vault field values using input selectors
+ await
expect(page.getByLabel('URI')).toHaveValue('http://vault.example.com:8200');
+ await expect(page.getByLabel('Prefix')).toHaveValue('/secret/test');
+ await
expect(page.getByLabel('Token')).toHaveValue('test-vault-token-123');
+ });
+ });
+
+ test('should update the secret', async ({ page }) => {
+ const updatedFields = {
+ URI: 'http://vault-updated.example.com:8200',
+ Prefix: '/secret/updated',
+ Token: 'updated-vault-token-456',
+ };
+
+ await test.step('navigate to secret detail page', async () => {
+ await secretsPom.toIndex(page);
+ await secretsPom.isIndexPage(page);
+
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await row.getByRole('button', { name: 'View' }).click();
+ await secretsPom.isDetailPage(page);
+ });
+
+ await test.step('enter edit mode and update fields', async () => {
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Update Vault fields
+ for (const [label, value] of Object.entries(updatedFields)) {
+ await page.getByLabel(label).clear();
+ await page.getByLabel(label).fill(value);
+ }
+ });
+
+ await test.step('save the changes', async () => {
+ await page.getByRole('button', { name: 'Save' }).click();
+ await secretsPom.isDetailPage(page);
+ });
+
+ await test.step('verify secret was updated via UI', async () => {
+ for (const [label, value] of Object.entries(updatedFields)) {
+ await expect(page.getByLabel(label)).toHaveValue(value);
+ }
+ });
+ });
+
+ test('should delete the secret', async ({ page }) => {
+ await test.step('navigate to detail page and delete', async () => {
+ await secretsPom.toIndex(page);
+ await secretsPom.isIndexPage(page);
+
+ const row = page.locator('tr').filter({ hasText: createdSecretId });
+ await row.getByRole('button', { name: 'View' }).click();
+ await secretsPom.isDetailPage(page);
+
+ await page.getByRole('button', { name: 'Delete' }).click();
+
+ const deleteDialog = page.getByRole('dialog', { name: 'Delete Secret' });
+ await expect(deleteDialog).toBeVisible();
+ await deleteDialog.getByRole('button', { name: 'Delete' }).click();
+ });
+
+ await test.step('verify deletion and redirect', async () => {
+ await secretsPom.isIndexPage(page);
+ await expect(page.getByRole('cell', { name: createdSecretId
})).toBeHidden();
+ });
+
+ await test.step('verify secret was deleted via API', async () => {
+ await expect(async () => {
+ await e2eReq.get(`${API_SECRETS}/${manager}/${createdSecretId}`);
+ }).rejects.toThrow();
+ });
+ });
+});
diff --git a/e2e/tests/secrets.list.spec.ts b/e2e/tests/secrets.list.spec.ts
new file mode 100644
index 000000000..0716ea8b7
--- /dev/null
+++ b/e2e/tests/secrets.list.spec.ts
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { secretsPom } from '@e2e/pom/secrets';
+import { setupPaginationTests } from '@e2e/utils/pagination-test-helper';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { expect, type Page } from '@playwright/test';
+
+import { API_SECRETS } from '@/config/constant';
+import type { APISIXType } from '@/types/schema/apisix';
+
+test('should navigate to secrets page', async ({ page }) => {
+ await test.step('navigate to secrets page', async () => {
+ await secretsPom.getSecretNavBtn(page).click();
+ await secretsPom.isIndexPage(page);
+ });
+
+ await test.step('verify secrets page components', async () => {
+ await expect(secretsPom.getAddSecretBtn(page)).toBeVisible();
+
+ // list table exists
+ const table = page.getByRole('table');
+ await expect(table).toBeVisible();
+ await expect(table.getByText('ID', { exact: true })).toBeVisible();
+ await expect(table.getByText('Secret Manager', { exact: true
})).toBeVisible();
+ await expect(table.getByText('Actions', { exact: true })).toBeVisible();
+ });
+});
+
+const secrets: APISIXType['Secret'][] = Array.from({ length: 11 }, (_, i) => ({
+ id: `secret_id_${i + 1}`,
+ manager: 'vault' as const,
+ uri: `http://vault-${i + 1}.example.com:8200`,
+ prefix: `/secret/path${i + 1}`,
+ token: `test-token-${i + 1}`,
+}));
+
+const putSecretReq = (req: typeof e2eReq, data: APISIXType['Secret']) => {
+ const { manager, id, ...rest } = data;
+ return req.put(`${API_SECRETS}/${manager}/${id}`, rest);
+};
+
+test.describe('page and page_size should work correctly', () => {
+ test.describe.configure({ mode: 'serial' });
+
+ test.beforeAll(async () => {
+ // Clean up existing secrets
+ const existingSecrets = await e2eReq
+ .get<unknown, APISIXType['RespSecretList']>(API_SECRETS)
+ .then((v) => v.data);
+ await Promise.all(
+ (existingSecrets.list || []).map((d) => {
+ const [manager, id] = d.value.id.split('/');
+ return e2eReq.delete(`${API_SECRETS}/${manager}/${id}`);
+ })
+ );
+
+ // Create test secrets
+ await Promise.all(secrets.map((d) => putSecretReq(e2eReq, d)));
+ });
+
+ test.afterAll(async () => {
+ await Promise.all(
+ secrets.map((d) => e2eReq.delete(`${API_SECRETS}/${d.manager}/${d.id}`))
+ );
+ });
+
+ // Setup pagination tests with secret-specific configurations
+ const filterItemsNotInPage = async (page: Page) => {
+ // filter the item which not in the current page
+ // it should be random, so we need get all items in the table
+ const itemsInPage = await page
+ .getByRole('cell', { name: /secret_id_/ })
+ .all();
+ const ids = await Promise.all(itemsInPage.map((v) => v.textContent()));
+ return secrets.filter((d) => !ids.includes(d.id));
+ };
+
+ setupPaginationTests(test, {
+ pom: secretsPom,
+ items: secrets,
+ filterItemsNotInPage,
+ getCell: (page, item) => page.getByRole('cell', { name: item.id }).first(),
+ });
+});