perf: paginate user addresses and optimize ownership queries (#1105)

* perf: paginate user addresses and optimize mail ownership queries

* docs: document user address pagination

* fix: address pagination review feedback

* fix: cover user address pagination flows

* test: fix user mailbox tab selector

* test: stabilize remote address search flow

* test: stabilize user address browser flow

* fix: preserve address pagination compatibility

* refactor: simplify bound address query types

* refactor: reuse list query for bound addresses

* fix: preserve paginated address totals

* fix: preserve bound address helper contracts

* fix: require pagination for user addresses

* refactor: keep shared pagination behavior unchanged

* fix: align pagination docs and tests

* fix: clear stale address selections

* refactor: simplify user address pagination

* refactor: limit user address changes to pagination

* test: select a visible mailbox address

* fix: preserve bound address response fields
This commit is contained in:
Dream Hunter
2026-08-09 19:23:41 +08:00
committed by GitHub
parent f9281818e9
commit a09ede8944
12 changed files with 450 additions and 37 deletions
@@ -0,0 +1,170 @@
import { test, expect, type APIRequestContext } from '@playwright/test';
import {
WORKER_URL,
createTestAddress,
deleteAddress,
hashPassword,
seedTestMail,
} from '../../fixtures/test-helpers';
async function createUser(request: APIRequestContext) {
const email = `address-page-${Date.now()}@test.example.com`;
const password = hashPassword('test-password-123');
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
data: { email, password },
});
expect(registerRes.ok()).toBe(true);
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
data: { email, password },
});
expect(loginRes.ok()).toBe(true);
const { jwt } = await loginRes.json();
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8'));
return { jwt, userId: payload.user_id as number };
}
test.describe('User address pagination', () => {
test('paginates addresses and enforces mail ownership', async ({ request }) => {
const addresses: Awaited<ReturnType<typeof createTestAddress>>[] = [];
let outsider: Awaited<ReturnType<typeof createTestAddress>> | undefined;
let originalUserSettings: Record<string, unknown> | undefined;
let userId: number | undefined;
try {
const settingsRes = await request.get(`${WORKER_URL}/admin/user_settings`);
expect(settingsRes.ok()).toBe(true);
originalUserSettings = await settingsRes.json();
const enableUserRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
data: {
...originalUserSettings,
enable: true,
enableMailVerify: false,
maxAddressCount: 0,
},
});
expect(enableUserRes.ok()).toBe(true);
const user = await createUser(request);
const userJwt = user.jwt;
userId = user.userId;
addresses.push(...await Promise.all([
createTestAddress(request, 'user-page-a'),
createTestAddress(request, 'user-page-b'),
createTestAddress(request, 'user-page-c'),
]));
outsider = await createTestAddress(request, 'user-page-outsider');
for (const item of addresses) {
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
headers: {
Authorization: `Bearer ${item.jwt}`,
'x-user-token': userJwt,
},
});
expect(bindRes.ok()).toBe(true);
}
const defaultPageRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
headers: { 'x-user-token': userJwt },
});
expect(defaultPageRes.ok()).toBe(true);
const defaultPage = await defaultPageRes.json();
expect(defaultPage.count).toBe(3);
expect(defaultPage.results).toHaveLength(3);
const firstPageRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=2&offset=0`,
{ headers: { 'x-user-token': userJwt } },
);
expect(firstPageRes.ok()).toBe(true);
const firstPage = await firstPageRes.json();
expect(firstPage.count).toBe(3);
expect(firstPage.results).toHaveLength(2);
expect(firstPage.results[0].mail_count).toBe(0);
expect(firstPage.results[0].send_count).toBe(0);
expect(firstPage.results[0]).toHaveProperty('source_meta');
expect(firstPage.results[0]).not.toHaveProperty('password');
const secondPageRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=2&offset=2`,
{ headers: { 'x-user-token': userJwt } },
);
expect(secondPageRes.ok()).toBe(true);
const secondPage = await secondPageRes.json();
expect(secondPage.count).toBe(0);
expect(secondPage.results).toHaveLength(1);
const invalidLimitRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=101&offset=0`,
{ headers: { 'x-user-token': userJwt } },
);
expect(invalidLimitRes.status()).toBe(400);
const invalidOffsetRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=20&offset=-1`,
{ headers: { 'x-user-token': userJwt } },
);
expect(invalidOffsetRes.status()).toBe(400);
await seedTestMail(request, addresses[0].address, { subject: 'Bound mail' });
await seedTestMail(request, outsider.address, { subject: 'Outsider mail' });
const userMailsRes = await request.get(`${WORKER_URL}/user_api/mails?limit=20&offset=0`, {
headers: { 'x-user-token': userJwt },
});
expect(userMailsRes.ok()).toBe(true);
const userMails = await userMailsRes.json();
expect(userMails.count).toBe(1);
expect(userMails.results[0].address).toBe(addresses[0].address);
const filteredOutsiderMailsRes = await request.get(
`${WORKER_URL}/user_api/mails?limit=20&offset=0&address=${encodeURIComponent(outsider.address)}`,
{ headers: { 'x-user-token': userJwt } },
);
expect(filteredOutsiderMailsRes.ok()).toBe(true);
const filteredOutsiderMails = await filteredOutsiderMailsRes.json();
expect(filteredOutsiderMails.count).toBe(0);
expect(filteredOutsiderMails.results).toHaveLength(0);
const outsiderMailsRes = await request.get(`${WORKER_URL}/api/mails?limit=20&offset=0`, {
headers: { Authorization: `Bearer ${outsider.jwt}` },
});
expect(outsiderMailsRes.ok()).toBe(true);
const outsiderMails = await outsiderMailsRes.json();
expect(outsiderMails.results).toHaveLength(1);
const forbiddenDeleteRes = await request.delete(
`${WORKER_URL}/user_api/mails/${outsiderMails.results[0].id}`,
{ headers: { 'x-user-token': userJwt } },
);
expect(forbiddenDeleteRes.ok()).toBe(true);
const outsiderAfterRes = await request.get(`${WORKER_URL}/api/mails?limit=20&offset=0`, {
headers: { Authorization: `Bearer ${outsider.jwt}` },
});
expect(outsiderAfterRes.ok()).toBe(true);
const outsiderAfter = await outsiderAfterRes.json();
expect(outsiderAfter.results).toHaveLength(1);
} finally {
try {
await Promise.allSettled(
[...addresses, outsider].filter((item) => item !== undefined)
.map((item) => deleteAddress(request, item.jwt)),
);
if (userId !== undefined) {
const deleteUserRes = await request.delete(`${WORKER_URL}/admin/users/${userId}`);
expect(deleteUserRes.ok()).toBe(true);
}
} finally {
if (originalUserSettings) {
const restoreSettingsRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
data: originalUserSettings,
});
expect(restoreSettingsRes.ok()).toBe(true);
}
}
}
});
});
@@ -0,0 +1,135 @@
import { expect, request as apiRequest, test } from '@playwright/test';
import type { APIRequestContext } from '@playwright/test';
import {
FRONTEND_URL,
WORKER_URL,
createTestAddress,
deleteAddress,
hashPassword,
} from '../../fixtures/test-helpers';
async function saveUserSettings(request: APIRequestContext, settings: Record<string, unknown>) {
const response = await request.post(`${WORKER_URL}/admin/user_settings`, { data: settings });
expect(response.ok()).toBe(true);
}
async function createUser(request: APIRequestContext) {
const email = `address-browser-${Date.now()}@test.example.com`;
const password = hashPassword('test-password-123');
const registerResponse = await request.post(`${WORKER_URL}/user_api/register`, {
data: { email, password },
});
expect(registerResponse.ok()).toBe(true);
const loginResponse = await request.post(`${WORKER_URL}/user_api/login`, {
data: { email, password },
});
expect(loginResponse.ok()).toBe(true);
const { jwt } = await loginResponse.json();
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8'));
return { email, jwt, userId: payload.user_id as number };
}
async function bindAddress(request: APIRequestContext, userJwt: string, addressJwt: string) {
const response = await request.post(`${WORKER_URL}/user_api/bind_address`, {
headers: {
Authorization: `Bearer ${addressJwt}`,
'x-user-token': userJwt,
},
});
expect(response.ok()).toBe(true);
}
test.describe('User address pagination browser flow', () => {
test('paginates addresses and filters mail', async ({ page }) => {
test.setTimeout(120_000);
const request = await apiRequest.newContext();
const createdAddresses: Awaited<ReturnType<typeof createTestAddress>>[] = [];
let originalUserSettings: Record<string, unknown> | undefined;
let userId: number | undefined;
try {
const settingsResponse = await request.get(`${WORKER_URL}/admin/user_settings`);
expect(settingsResponse.ok()).toBe(true);
originalUserSettings = await settingsResponse.json();
await saveUserSettings(request, {
...originalUserSettings,
enable: true,
enableMailVerify: false,
maxAddressCount: 0,
});
const user = await createUser(request);
userId = user.userId;
for (let index = 0; index < 21; index += 1) {
const address = await createTestAddress(request, `browser-page-${index}-`);
createdAddresses.push(address);
await bindAddress(request, user.jwt, address.jwt);
}
const defaultAddressPageResponse = await request.get(
`${WORKER_URL}/user_api/bind_address`,
{ headers: { 'x-user-token': user.jwt } },
);
expect(defaultAddressPageResponse.ok()).toBe(true);
const defaultAddressPage = await defaultAddressPageResponse.json();
expect(defaultAddressPage.count).toBe(21);
expect(defaultAddressPage.results).toHaveLength(20);
await page.goto(`${FRONTEND_URL}/en/`);
await page.evaluate((userJwt) => {
localStorage.setItem('userJwt', userJwt);
}, user.jwt);
await page.goto(`${FRONTEND_URL}/en/user`);
await expect(page.getByText(user.email)).toBeVisible({ timeout: 15_000 });
const pagination = page.locator('.n-pagination').first();
const addressRows = page.locator('.n-data-table-tbody .n-data-table-tr');
await expect(pagination).toContainText(/Total:\s*21/);
await expect(addressRows).toHaveCount(20);
await pagination.locator('.n-pagination-item').filter({ hasText: /^2$/ }).click();
await expect(addressRows).toHaveCount(1);
const selectedAddress = createdAddresses[20];
const initialMailboxAddressesResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return url.pathname === '/user_api/bind_address'
&& url.searchParams.get('limit') === '100';
});
await page.getByText('Mail Box', { exact: true }).click();
const initialMailboxResponse = await initialMailboxAddressesResponse;
expect(initialMailboxResponse.ok()).toBe(true);
const mailboxAddressSelect = page.locator('.n-input-group .n-select').first();
await mailboxAddressSelect.click();
const mailboxOptions = page.locator('.n-base-select-menu:visible');
await expect(mailboxOptions).toContainText(selectedAddress.address);
const filteredMailResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return url.pathname === '/user_api/mails'
&& url.searchParams.get('address') === selectedAddress.address;
});
await mailboxOptions.getByText(selectedAddress.address, { exact: true }).click();
expect((await filteredMailResponse).ok()).toBe(true);
} finally {
try {
try {
await Promise.allSettled(createdAddresses.map((address) => deleteAddress(request, address.jwt)));
if (userId !== undefined) {
await request.delete(`${WORKER_URL}/admin/users/${userId}`);
}
} finally {
if (originalUserSettings) {
await saveUserSettings(request, originalUserSettings);
}
}
} finally {
await request.dispose();
}
}
});
});