Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions e2e/server/apisix_conf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,28 @@
#

apisix:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
apisix:
# 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.
apisix:

node_listen: 9080 # APISIX listening port
node_listen: 9080
enable_ipv6: false
proxy_mode: http&stream
stream_proxy:
tcp:
- 9100
udp:
- 9200

deployment:
admin:
allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow
- 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test.

allow_admin:
- 0.0.0.0/0
admin_key:
- name: "admin"
- name: admin
key: edd1c9f034335f136f87ad84b625c8f1
role: admin # admin: manage all configuration data

role: admin
etcd:
host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
- "http://etcd:2379" # multiple etcd address
prefix: "/apisix" # apisix configurations prefix
timeout: 30 # 30 seconds

host:
- http://etcd:2379
prefix: /apisix
timeout: 30
discovery:
nacos:
host:
- "http://nacos:8848"
- http://nacos:8848
76 changes: 76 additions & 0 deletions e2e/tests/bulk/routes.bulk-100.list-render.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* 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.
*/

// Bulk D-01: routes list page must render correctly with 100 rows.
// Data is seeded directly via the Admin API to keep the bulk-render
// assertion decoupled from the add-form behavior.

import { routesPom } from '@e2e/pom/routes';
import {
bulkCreateRoutes,
bulkDeleteRoutesByPrefix,
} from '@e2e/utils/bulk';
import { test } from '@e2e/utils/test';
import { expect } from '@playwright/test';

const PREFIX = 'bulk100';
const COUNT = 100;

test.describe.configure({ mode: 'serial', timeout: 120_000 });

test.beforeAll(async () => {
await bulkDeleteRoutesByPrefix(PREFIX);
await bulkCreateRoutes({ count: COUNT, prefix: PREFIX });
});

test.afterAll(async () => {
await bulkDeleteRoutesByPrefix(PREFIX);
});

test('routes list shows correct total and renders default page', async ({
page,
}) => {
const t0 = Date.now();
await routesPom.toIndex(page);
await routesPom.isIndexPage(page);

// Routes are listed in server-defined order (not insertion order), so
// bulk100-0 isn't guaranteed to be on page 1. Just require *some*
// seeded row to render.
const someSeededRow = page
.getByRole('row', { name: new RegExp(`${PREFIX}-\\d+`) })
.first();
await expect(someSeededRow).toBeVisible({ timeout: 5000 });
const renderMs = Date.now() - t0;
test
.info()
.annotations.push({ type: 'perf', description: `LCP-ish: ${renderMs}ms` });
expect(renderMs, 'list page should render in < 5s with 100 rows').toBeLessThan(
5000
);
});

test('pagination shows ≥ 10 pages at default page_size=10', async ({
page,
}) => {
await routesPom.toIndex(page);
await routesPom.isIndexPage(page);

// page=10 must be reachable (100 rows / 10 per page = 10 pages).
const page10 = page.getByRole('listitem', { name: '10' });
await expect(page10).toBeVisible();
});
95 changes: 95 additions & 0 deletions e2e/tests/bulk/routes.bulk-1000.list-search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* 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.
*/
/* eslint-disable playwright/no-wait-for-timeout -- regression test stabilization */

// Bulk D-03: with 1000 routes the list page must:
// - render the first page within a reasonable budget
// - allow URL-driven jump to a far page (e.g. ?page=50)
// - not flood the Admin API with per-row requests
//
// Marked as `bulk` — exclude from default CI runs.

import { routesPom } from '@e2e/pom/routes';
import {
bulkCreateRoutes,
bulkDeleteRoutesByPrefix,
} from '@e2e/utils/bulk';
import { test } from '@e2e/utils/test';
import { expect } from '@playwright/test';

const PREFIX = 'bulk1k';
const COUNT = 1000;

test.describe.configure({ mode: 'serial', timeout: 300_000 });

test.beforeAll(async () => {
await bulkDeleteRoutesByPrefix(PREFIX);
await bulkCreateRoutes({ count: COUNT, prefix: PREFIX });
});

test.afterAll(async () => {
await bulkDeleteRoutesByPrefix(PREFIX);
});

test('first page renders within 5s with 1000 rows in etcd', async ({
page,
}) => {
const t0 = Date.now();
await routesPom.toIndex(page);
await routesPom.isIndexPage(page);

await expect(
page
.getByRole('row', { name: new RegExp(`${PREFIX}-\\d+`) })
.first()
).toBeVisible({ timeout: 8000 });
const ms = Date.now() - t0;
test
.info()
.annotations.push({ type: 'perf', description: `first-page render: ${ms}ms` });
expect(ms).toBeLessThan(8000);
});

test('admin API call volume on list page load is bounded (≤ 5 GETs)', async ({
page,
}) => {
const adminCalls: string[] = [];
page.on('request', (req) => {
if (req.method() === 'GET' && req.url().includes('/apisix/admin/routes')) {
adminCalls.push(req.url());
}
});

await routesPom.toIndex(page);
await routesPom.isIndexPage(page);
await expect(
page
.getByRole('row', { name: new RegExp(`${PREFIX}-\\d+`) })
.first()
).toBeVisible({ timeout: 8000 });
// Settle for any deferred queries.
await page.waitForTimeout(800);

test.info().annotations.push({
type: 'perf',
description: `admin /routes GETs: ${adminCalls.length}`,
});
expect(
adminCalls.length,
'list page should not issue per-row GETs (≤ 5 total)'
).toBeLessThanOrEqual(5);
});
7 changes: 6 additions & 1 deletion e2e/tests/consumer_groups.crud-required-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,13 @@ test('should CRUD Consumer Group with required fields', async ({ page }) => {
const idField = page.getByRole('textbox', { name: 'ID', exact: true });
await expect(idField).toBeDisabled();

// Cancel without making changes
// Cancel without making changes. The Edit→Cancel guard now always
// confirms before discarding (see src/hooks/useEditCancelGuard.tsx).
await page.getByRole('button', { name: 'Cancel' }).click();
await page
.getByRole('dialog')
.getByRole('button', { name: 'Discard Changes' })
.click();

// Verify we're back in detail view
await consumerGroupsPom.isDetailPage(page);
Expand Down
80 changes: 80 additions & 0 deletions e2e/tests/edge/plugins.invalid-json-monaco.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* 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.
*/
/* eslint-disable playwright/no-wait-for-timeout -- regression test stabilization */

// Edge: typing syntactically-invalid JSON in the plugin config editor must
// either prevent submission or surface a visible error — never round-trip a
// broken config to the Admin API.

import { routesPom } from '@e2e/pom/routes';
import { e2eReq } from '@e2e/utils/req';
import { test } from '@e2e/utils/test';
import {
uiFillMonacoEditor,
uiGetMonacoEditor,
} from '@e2e/utils/ui';
import { expect } from '@playwright/test';

import { deleteAllRoutes } from '@/apis/routes';

const pluginName = 'cors';
const invalidJson = '{ "allow_origins": "*", '; // missing closing brace + trailing comma

test.beforeAll(async () => {
await deleteAllRoutes(e2eReq);
});

test.afterAll(async () => {
await deleteAllRoutes(e2eReq);
});

test('invalid JSON in plugin editor cannot be saved', async ({ page }) => {
await routesPom.toAdd(page);
await routesPom.isAddPage(page);

await page.getByRole('button', { name: 'Select Plugins' }).click();
const selectDialog = page.getByRole('dialog', { name: 'Select Plugins' });
await selectDialog.getByPlaceholder('Search').fill(pluginName);
await selectDialog
.getByTestId(`plugin-${pluginName}`)
.getByRole('button', { name: 'Add' })
.click();

const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' });
const editor = await uiGetMonacoEditor(page, addPluginDialog, false);
await uiFillMonacoEditor(page, editor, invalidJson);

await addPluginDialog.getByRole('button', { name: 'Add' }).click();

// The dialog must remain open OR an error must surface — what we forbid
// is silently accepting invalid JSON and closing the dialog as if it
// were valid.
await page.waitForTimeout(1000);
const dialogStillOpen = await addPluginDialog
.isVisible()
.catch(() => false);
const errorVisible = await page
.getByText(/(invalid|json|parse|syntax)/i)
.first()
.isVisible()
.catch(() => false);

expect(
dialogStillOpen || errorVisible,
'invalid JSON must NOT silently close the editor as if accepted'
).toBe(true);
});
Loading
Loading