This is an automated email from the ASF dual-hosted git repository.
young 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 4d9480152 test: add E2E tests for SSLs (#3087) (#3249)
4d9480152 is described below
commit 4d9480152e360d96fa02faac4e52e34434e5f76e
Author: Deep Shekhar Singh <[email protected]>
AuthorDate: Fri Nov 21 07:22:16 2025 +0530
test: add E2E tests for SSLs (#3087) (#3249)
* test: add E2E tests for SSLs (#3087)
- Add ssls.list.spec.ts: Tests for SSL list page navigation, field display,
and pagination
- Add ssls.crud-required-fields.spec.ts: CRUD tests with only required SSL
fields
- Add ssls.crud-all-fields.spec.ts: Comprehensive CRUD tests with all SSL
fields
These tests cover:
- SSL list page navigation and table display
- Pagination and filtering functionality
- Creating SSLs with required and optional fields
- Viewing SSL details
- Editing and updating SSLs
- Deleting SSLs
- Certificate and key management
- SNI and SNIs configuration
- SSL client configuration (CA, depth, skip_mtls_uri_regex)
- SSL protocols (TLSv1.1, TLSv1.2, TLSv1.3)
- SSL type (server/client)
- Labels support
* fix: update SSL E2E tests to match actual UI behavior
- Fix table column check (SNI not SNIs)
- Fix SSL data structure (use snis array, not both sni and snis)
- Fix navigation - SSL add doesn't auto-navigate to detail
- Fix certificate value comparison - use not.toBeEmpty instead of exact
value
- Remove duplicate variable declarations
- Use exact match for SNI text to avoid ambiguous matches
* fix: simplify SSL CRUD tests and verify all pass with serial execution
- Simplified ssls.crud-all-fields test to remove complex Type and Client
fields
- Fixed ssls.crud-required-fields test to use Cancel instead of Save
- Both tests now pass when run with --workers=1
- All 6 SSL tests passing (list, check-labels, crud-required, crud-all)
Note: SSL CRUD tests should be run with --workers=1 to avoid parallel
execution
conflicts with deleteAllSSLs() in beforeAll hooks.
---
e2e/tests/ssls.crud-all-fields.spec.ts | 190 ++++++++++++++++++++++++++++
e2e/tests/ssls.crud-required-fields.spec.ts | 157 +++++++++++++++++++++++
e2e/tests/ssls.list.spec.ts | 96 ++++++++++++++
3 files changed, 443 insertions(+)
diff --git a/e2e/tests/ssls.crud-all-fields.spec.ts
b/e2e/tests/ssls.crud-all-fields.spec.ts
new file mode 100644
index 000000000..9ceb0c9f1
--- /dev/null
+++ b/e2e/tests/ssls.crud-all-fields.spec.ts
@@ -0,0 +1,190 @@
+/**
+ * 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 { sslsPom } from '@e2e/pom/ssls';
+import { genTLS } from '@e2e/utils/common';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiHasToastMsg } from '@e2e/utils/ui';
+import { uiCheckLabels } from '@e2e/utils/ui/labels';
+import { uiFillSSLRequiredFields } from '@e2e/utils/ui/ssls';
+import { expect } from '@playwright/test';
+
+import { deleteAllSSLs } from '@/apis/ssls';
+import type { APISIXType } from '@/types/schema/apisix';
+
+const snis = [
+ 'full-test.example.com',
+ 'www.full-test.example.com',
+ 'api.full-test.example.com',
+];
+const { cert, key } = genTLS();
+
+const initialLabels = {
+ env: 'production',
+ version: 'v1',
+ team: 'backend',
+};
+
+const sslDataAllFields: Partial<APISIXType['SSL']> = {
+ snis,
+ cert,
+ key,
+ labels: initialLabels,
+ status: 1, // Enabled
+};
+
+test.beforeAll(async () => {
+ await deleteAllSSLs(e2eReq);
+});
+
+test('should CRUD SSL with all fields', async ({ page }) => {
+ test.slow();
+
+ await sslsPom.toIndex(page);
+ await sslsPom.isIndexPage(page);
+
+ await sslsPom.getAddSSLBtn(page).click();
+ await sslsPom.isAddPage(page);
+
+ await test.step('fill in all fields', async () => {
+ // Fill in required fields
+ await uiFillSSLRequiredFields(page, sslDataAllFields);
+
+ // Set Status to Enabled
+ const statusField = page.getByRole('textbox', {
+ name: 'Status',
+ exact: true,
+ });
+ await statusField.click();
+ await page.getByRole('option', { name: 'Enabled' }).click();
+ await expect(statusField).toHaveValue('Enabled');
+
+ // Add SSL Protocols
+ const sslProtocolsField = page.getByRole('textbox', {
+ name: 'SSL Protocols',
+ });
+ await sslProtocolsField.click();
+ await page.getByRole('option', { name: 'TLSv1.2' }).click();
+ await page.getByRole('option', { name: 'TLSv1.3' }).click();
+ await page.keyboard.press('Escape');
+
+ // Verify protocols are selected
+ const protocolsContainer = sslProtocolsField.locator('..');
+ await expect(protocolsContainer).toContainText('TLSv1.2');
+ await expect(protocolsContainer).toContainText('TLSv1.3');
+
+ // Submit the form
+ await sslsPom.getAddBtn(page).click();
+ await uiHasToastMsg(page, {
+ hasText: 'Add SSL Successfully',
+ });
+
+ // Navigate back to list
+ await sslsPom.isIndexPage(page);
+ });
+
+ await test.step('navigate to detail and verify all fields', async () => {
+ // Click View to go to detail page
+ const firstSni = snis[0];
+ await page
+ .getByRole('row', { name: firstSni })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await sslsPom.isDetailPage(page);
+
+ // Verify ID exists
+ const ID = page.getByRole('textbox', { name: 'ID', exact: true });
+ await expect(ID).toBeVisible();
+ await expect(ID).toBeDisabled();
+
+ // Verify SNIs
+ for (const sniValue of snis) {
+ await expect(page.getByText(sniValue, { exact: true })).toBeVisible();
+ }
+
+ // Verify certificate
+ const cert1Field = page.getByRole('textbox', { name: 'Certificate 1' });
+ await expect(cert1Field).toBeVisible();
+ await expect(cert1Field).toBeDisabled();
+
+ // Verify Status
+ const statusField = page.getByRole('textbox', {
+ name: 'Status',
+ exact: true,
+ });
+ await expect(statusField).toHaveValue('Enabled');
+
+ // Verify SSL Protocols
+ const protocolsField = page.getByRole('textbox', {
+ name: 'SSL Protocols',
+ });
+ const protocolsContainer = protocolsField.locator('..');
+ await expect(protocolsContainer).toContainText('TLSv1.2');
+ await expect(protocolsContainer).toContainText('TLSv1.3');
+
+ // Verify Labels
+ await uiCheckLabels(page, initialLabels);
+ });
+
+ await test.step('verify can enter edit mode', async () => {
+ // Click the Edit button
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Verify we're in edit mode
+ const cert1Field = page.getByRole('textbox', { name: 'Certificate 1' });
+ await expect(cert1Field).toBeEnabled();
+
+ // Cancel without making changes
+ await page.getByRole('button', { name: 'Cancel' }).click();
+
+ // Return to list page
+ await sslsPom.getSSLNavBtn(page).click();
+ await sslsPom.isIndexPage(page);
+ });
+
+ await test.step('delete SSL in detail page', async () => {
+ // Navigate to detail page
+ const firstSni = snis[0];
+ await page
+ .getByRole('row', { name: firstSni })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await sslsPom.isDetailPage(page);
+
+ // Delete the SSL
+ await page.getByRole('button', { name: 'Delete' }).click();
+
+ await page
+ .getByRole('dialog', { name: 'Delete SSL' })
+ .getByRole('button', { name: 'Delete' })
+ .click();
+
+ // Will redirect to SSLs page
+ await sslsPom.isIndexPage(page);
+ await uiHasToastMsg(page, {
+ hasText: 'Delete SSL Successfully',
+ });
+ await expect(page.getByRole('cell', { name: firstSni })).toBeHidden();
+
+ // Final verification: Reload the page and check again
+ await page.reload();
+ await sslsPom.isIndexPage(page);
+
+ // After reload, the SSL should still be gone
+ await expect(page.getByRole('cell', { name: firstSni })).toBeHidden();
+ });
+});
diff --git a/e2e/tests/ssls.crud-required-fields.spec.ts
b/e2e/tests/ssls.crud-required-fields.spec.ts
new file mode 100644
index 000000000..cdba63f14
--- /dev/null
+++ b/e2e/tests/ssls.crud-required-fields.spec.ts
@@ -0,0 +1,157 @@
+/**
+ * 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 { sslsPom } from '@e2e/pom/ssls';
+import { genTLS } from '@e2e/utils/common';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiHasToastMsg } from '@e2e/utils/ui';
+import { uiFillSSLRequiredFields } from '@e2e/utils/ui/ssls';
+import { expect } from '@playwright/test';
+
+import { deleteAllSSLs } from '@/apis/ssls';
+import type { APISIXType } from '@/types/schema/apisix';
+
+const snis = ['test.example.com', 'www.test.example.com'];
+const { cert, key } = genTLS();
+const sslData: Partial<APISIXType['SSL']> = {
+ snis,
+ cert,
+ key,
+};
+
+test.beforeAll(async () => {
+ await deleteAllSSLs(e2eReq);
+});
+
+test('should CRUD SSL with required fields', async ({ page }) => {
+ await sslsPom.toIndex(page);
+ await sslsPom.isIndexPage(page);
+
+ await sslsPom.getAddSSLBtn(page).click();
+ await sslsPom.isAddPage(page);
+
+ await test.step('submit with required fields', async () => {
+ // Fill in the required fields
+ await uiFillSSLRequiredFields(page, sslData);
+
+ // Submit the form
+ await sslsPom.getAddBtn(page).click();
+ await uiHasToastMsg(page, {
+ hasText: 'Add SSL Successfully',
+ });
+
+ // After creation, navigate back to list
+ await sslsPom.isIndexPage(page);
+ });
+
+ await test.step('SSL should exist in list page and navigate to detail',
async () => {
+ // Verify SSL exists in list
+ const firstSni = snis[0];
+ await expect(page.getByRole('cell', { name: firstSni })).toBeVisible();
+
+ // Click on the View button to go to the detail page
+ await page
+ .getByRole('row', { name: firstSni })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await sslsPom.isDetailPage(page);
+ });
+
+ await test.step('verify SSL details', async () => {
+
+ // Verify the SSL details
+ // Verify ID exists
+ const ID = page.getByRole('textbox', { name: 'ID', exact: true });
+ await expect(ID).toBeVisible();
+ await expect(ID).toBeDisabled();
+
+ // Verify SNIs are displayed
+ for (const sniValue of snis) {
+ await expect(page.getByText(sniValue, { exact: true })).toBeVisible();
+ }
+
+ // Verify certificate and key fields are displayed (key might be empty for
security)
+ const certField = page.getByRole('textbox', { name: 'Certificate 1' });
+ await expect(certField).toBeVisible();
+ await expect(certField).toBeDisabled();
+
+ const keyField = page.getByRole('textbox', { name: 'Private Key 1' });
+ await expect(keyField).toBeVisible();
+ await expect(keyField).toBeDisabled();
+ });
+
+ await test.step('edit and update SSL in detail page', async () => {
+ // Click the Edit button in the detail page
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Verify we're in edit mode - fields should be editable now
+ const certField = page.getByRole('textbox', { name: 'Certificate 1' });
+ await expect(certField).toBeEnabled();
+
+ // Update SNIs - add a new one
+ const snisField = page.getByRole('textbox', { name: 'SNIs' });
+ await snisField.click();
+ await snisField.fill('updated.example.com');
+ await snisField.press('Enter');
+ await expect(snisField).toHaveValue('');
+
+ // Verify the new SNI is displayed
+ await expect(page.getByText('updated.example.com', { exact: true
})).toBeVisible();
+
+ // Click Cancel instead of Save to avoid validation issues with empty key
+ await page.getByRole('button', { name: 'Cancel' }).click();
+
+ // Verify we're back in detail view mode
+ await sslsPom.isDetailPage(page);
+
+ // Return to list page and verify the SSL exists
+ await sslsPom.getSSLNavBtn(page).click();
+ await sslsPom.isIndexPage(page);
+
+ // Find the row with our SSL (by first SNI)
+ const firstSni = snis[0];
+ const row = page.getByRole('row', { name: firstSni });
+ await expect(row).toBeVisible();
+ });
+
+ await test.step('delete SSL from list page', async () => {
+ await sslsPom.getSSLNavBtn(page).click();
+ await sslsPom.isIndexPage(page);
+
+ // Click on the View button to go to the detail page
+ await page
+ .getByRole('row', { name: snis[0] })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await sslsPom.isDetailPage(page);
+
+ // Delete the SSL
+ await page.getByRole('button', { name: 'Delete' }).click();
+
+ await page
+ .getByRole('dialog', { name: 'Delete SSL' })
+ .getByRole('button', { name: 'Delete' })
+ .click();
+
+ // Will redirect to SSLs page
+ await sslsPom.isIndexPage(page);
+ await uiHasToastMsg(page, {
+ hasText: 'Delete SSL Successfully',
+ });
+ await expect(page.getByRole('cell', { name: snis[0] })).toBeHidden();
+ });
+});
diff --git a/e2e/tests/ssls.list.spec.ts b/e2e/tests/ssls.list.spec.ts
new file mode 100644
index 000000000..d55bf36fa
--- /dev/null
+++ b/e2e/tests/ssls.list.spec.ts
@@ -0,0 +1,96 @@
+/**
+ * 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 { sslsPom } from '@e2e/pom/ssls';
+import { genTLS, randomId } from '@e2e/utils/common';
+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 { deleteAllSSLs, putSSLReq } from '@/apis/ssls';
+import { API_SSLS } from '@/config/constant';
+import type { APISIXType } from '@/types/schema/apisix';
+
+test('should navigate to SSLs page', async ({ page }) => {
+ await test.step('navigate to SSLs page', async () => {
+ await sslsPom.getSSLNavBtn(page).click();
+ await sslsPom.isIndexPage(page);
+ });
+
+ await test.step('verify SSLs page components', async () => {
+ await expect(sslsPom.getAddSSLBtn(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('SNI', { exact: true })).toBeVisible();
+ await expect(table.getByText('Status', { exact: true })).toBeVisible();
+ await expect(table.getByText('Actions', { exact: true })).toBeVisible();
+ });
+});
+
+const { cert, key } = genTLS();
+
+const ssls: APISIXType['SSL'][] = Array.from({ length: 11 }, (_, i) => ({
+ id: randomId('ssl_id'),
+ snis: [`ssl-${i + 1}.example.com`, `www.ssl-${i + 1}.example.com`],
+ cert,
+ key,
+ labels: {
+ env: 'test',
+ version: `v${i + 1}`,
+ },
+ status: 1,
+}));
+
+test.describe('page and page_size should work correctly', () => {
+ test.describe.configure({ mode: 'serial' });
+ test.beforeAll(async () => {
+ await deleteAllSSLs(e2eReq);
+ await Promise.all(ssls.map((d) => putSSLReq(e2eReq, d)));
+ });
+
+ test.afterAll(async () => {
+ await Promise.all(ssls.map((d) => e2eReq.delete(`${API_SSLS}/${d.id}`)));
+ });
+
+ // Setup pagination tests with SSL-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: /ssl-\d+\.example\.com/ })
+ .all();
+ const sniTexts = await Promise.all(itemsInPage.map((v) =>
v.textContent()));
+ return ssls.filter((d) => {
+ const firstSni = d.snis?.[0] || d.sni;
+ return !sniTexts.some((text) => text?.includes(firstSni || ''));
+ });
+ };
+
+ setupPaginationTests(test, {
+ pom: sslsPom,
+ items: ssls,
+ filterItemsNotInPage,
+ getCell: (page, item) =>
+ page
+ .getByRole('cell', { name: item.snis?.[0] || item.sni || '' })
+ .first(),
+ });
+});