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(),
+  });
+});

Reply via email to