mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-29 03:57:05 +08:00
396 lines
16 KiB
TypeScript
396 lines
16 KiB
TypeScript
import { test, expect, type APIRequestContext } from '@playwright/test';
|
|
import {
|
|
WORKER_URL,
|
|
WORKER_URL_ENV_OFF,
|
|
createTestAddress,
|
|
deleteAddress,
|
|
hashPassword,
|
|
seedTestMail,
|
|
} from '../../fixtures/test-helpers';
|
|
|
|
const addressHeaders = (jwt: string) => ({ Authorization: `Bearer ${jwt}` });
|
|
|
|
async function listAddressMails(
|
|
request: APIRequestContext,
|
|
jwt: string,
|
|
state = 'all',
|
|
baseUrl = WORKER_URL,
|
|
) {
|
|
const response = await request.get(
|
|
`${baseUrl}/api/mails?limit=100&offset=0&mail_state=${state}`,
|
|
{ headers: addressHeaders(jwt) },
|
|
);
|
|
expect(response.ok()).toBe(true);
|
|
return await response.json();
|
|
}
|
|
|
|
async function updateAddressMailState(
|
|
request: APIRequestContext,
|
|
jwt: string,
|
|
ids: number[],
|
|
state: string,
|
|
) {
|
|
const response = await request.patch(`${WORKER_URL}/api/mails/state`, {
|
|
headers: addressHeaders(jwt),
|
|
data: { ids, state },
|
|
});
|
|
expect(response.ok()).toBe(true);
|
|
return await response.json();
|
|
}
|
|
|
|
async function getStoredFlags(request: APIRequestContext, mailId: number) {
|
|
const response = await request.get(`${WORKER_URL}/admin/test/mail_flags?mail_id=${mailId}`);
|
|
expect(response.ok()).toBe(true);
|
|
return (await response.json()).results as { mail_id: number; address_id: number; flag: number }[];
|
|
}
|
|
|
|
test.describe('Mail Read Status', () => {
|
|
test('new mail is unread and can be marked as read without changing other mailboxes', async ({ request }) => {
|
|
const first = await createTestAddress(request, 'mail-flags-first');
|
|
const second = await createTestAddress(request, 'mail-flags-second');
|
|
|
|
try {
|
|
const statesRes = await request.get(`${WORKER_URL}/api/mail-states`, {
|
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
|
});
|
|
expect(statesRes.ok()).toBe(true);
|
|
expect((await statesRes.json()).results.map((state: { value: string }) => state.value))
|
|
.toEqual(['all', 'unread', 'read']);
|
|
|
|
await seedTestMail(request, first.address, { subject: 'Unread mail' });
|
|
const listRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
|
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
|
});
|
|
expect(listRes.ok()).toBe(true);
|
|
const { results } = await listRes.json();
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].flags).toBeUndefined();
|
|
expect(results[0].unread).toBe(true);
|
|
expect(await getStoredFlags(request, results[0].id)).toEqual([{
|
|
mail_id: results[0].id,
|
|
address_id: first.address_id,
|
|
flag: 0,
|
|
}]);
|
|
|
|
const detailRes = await request.get(`${WORKER_URL}/api/mail/${results[0].id}`, {
|
|
headers: addressHeaders(first.jwt),
|
|
});
|
|
expect(detailRes.ok()).toBe(true);
|
|
expect((await detailRes.json()).unread).toBe(true);
|
|
|
|
const parsedDetailRes = await request.get(`${WORKER_URL}/api/parsed_mail/${results[0].id}`, {
|
|
headers: addressHeaders(first.jwt),
|
|
});
|
|
expect(parsedDetailRes.ok()).toBe(true);
|
|
expect((await parsedDetailRes.json()).unread).toBe(true);
|
|
|
|
const unreadRes = await request.get(
|
|
`${WORKER_URL}/api/mails?limit=10&offset=0&mail_state=unread`,
|
|
{ headers: { Authorization: `Bearer ${first.jwt}` } },
|
|
);
|
|
expect((await unreadRes.json()).results).toHaveLength(1);
|
|
|
|
const deniedRes = await request.patch(`${WORKER_URL}/api/mails/state`, {
|
|
headers: { Authorization: `Bearer ${second.jwt}` },
|
|
data: { ids: [results[0].id], state: 'read' },
|
|
});
|
|
expect(deniedRes.ok()).toBe(true);
|
|
expect((await deniedRes.json()).changes).toBe(0);
|
|
|
|
const updateRes = await request.patch(`${WORKER_URL}/api/mails/state`, {
|
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
|
data: { ids: [results[0].id], state: 'read' },
|
|
});
|
|
expect(updateRes.ok()).toBe(true);
|
|
const updateResult = await updateRes.json();
|
|
expect(updateResult.changes).toBe(1);
|
|
expect(updateResult.results[0].unread).toBe(false);
|
|
expect(await getStoredFlags(request, results[0].id)).toEqual([]);
|
|
|
|
const updatedListRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
|
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
|
});
|
|
expect((await updatedListRes.json()).results[0].unread).toBe(false);
|
|
|
|
const unreadAfterUpdateRes = await request.get(
|
|
`${WORKER_URL}/api/mails?limit=10&offset=0&mail_state=unread`,
|
|
{ headers: { Authorization: `Bearer ${first.jwt}` } },
|
|
);
|
|
expect((await unreadAfterUpdateRes.json()).results).toHaveLength(0);
|
|
|
|
const unreadStateRes = await request.patch(`${WORKER_URL}/api/mails/state`, {
|
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
|
data: { ids: [results[0].id], state: 'unread' },
|
|
});
|
|
expect(unreadStateRes.ok()).toBe(true);
|
|
expect((await unreadStateRes.json()).results[0].unread).toBe(true);
|
|
expect(await getStoredFlags(request, results[0].id)).toHaveLength(1);
|
|
} finally {
|
|
await deleteAddress(request, first.jwt);
|
|
await deleteAddress(request, second.jwt);
|
|
}
|
|
});
|
|
|
|
test('supports batch and idempotent updates with accurate filters and cleanup', async ({ request }) => {
|
|
const mailbox = await createTestAddress(request, 'mail-flags-batch');
|
|
try {
|
|
for (let index = 0; index < 3; index += 1) {
|
|
await seedTestMail(request, mailbox.address, { subject: `Flag batch ${index}` });
|
|
}
|
|
|
|
const initial = await listAddressMails(request, mailbox.jwt);
|
|
const ids = initial.results.map((mail: { id: number }) => mail.id);
|
|
expect(initial.count).toBe(3);
|
|
expect(initial.results.every((mail: { unread: boolean }) => mail.unread)).toBe(true);
|
|
|
|
const firstUpdate = await updateAddressMailState(request, mailbox.jwt, ids.slice(0, 2), 'read');
|
|
expect(firstUpdate.changes).toBe(2);
|
|
expect(firstUpdate.results).toHaveLength(2);
|
|
expect(firstUpdate.results.every((mail: { unread: boolean }) => !mail.unread)).toBe(true);
|
|
|
|
const duplicateUpdate = await updateAddressMailState(request, mailbox.jwt, ids.slice(0, 2), 'read');
|
|
expect(duplicateUpdate.changes).toBe(0);
|
|
|
|
const read = await listAddressMails(request, mailbox.jwt, 'read');
|
|
expect(read.count).toBe(2);
|
|
expect(read.results.map((mail: { id: number }) => mail.id).sort()).toEqual(ids.slice(0, 2).sort());
|
|
expect(read.results.every((mail: { unread: boolean }) => !mail.unread)).toBe(true);
|
|
|
|
const unread = await listAddressMails(request, mailbox.jwt, 'unread');
|
|
expect(unread.count).toBe(1);
|
|
expect(unread.results[0].id).toBe(ids[2]);
|
|
expect(unread.results[0].unread).toBe(true);
|
|
|
|
const duplicateUnread = await updateAddressMailState(request, mailbox.jwt, [ids[2]], 'unread');
|
|
expect(duplicateUnread.changes).toBe(0);
|
|
|
|
const deleteRes = await request.delete(`${WORKER_URL}/api/mails/${ids[2]}`, {
|
|
headers: addressHeaders(mailbox.jwt),
|
|
});
|
|
expect(deleteRes.ok()).toBe(true);
|
|
expect(await getStoredFlags(request, ids[2])).toEqual([]);
|
|
expect((await updateAddressMailState(request, mailbox.jwt, [ids[2]], 'unread')).changes).toBe(0);
|
|
} finally {
|
|
await deleteAddress(request, mailbox.jwt);
|
|
}
|
|
});
|
|
|
|
test('rejects unsupported mail states', async ({ request }) => {
|
|
const { jwt } = await createTestAddress(request, 'mail-flags-invalid');
|
|
try {
|
|
for (const data of [
|
|
{ ids: [1], state: 'invalid' },
|
|
{ ids: [1] },
|
|
{ ids: [], state: 'read' },
|
|
{ ids: [0], state: 'read' },
|
|
{ ids: [1.5], state: 'read' },
|
|
{ ids: ['1'], state: 'read' },
|
|
{ ids: Array.from({ length: 101 }, (_, index) => index + 1), state: 'read' },
|
|
]) {
|
|
const res = await request.patch(`${WORKER_URL}/api/mails/state`, {
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
data,
|
|
});
|
|
expect(res.status()).toBe(400);
|
|
}
|
|
} finally {
|
|
await deleteAddress(request, jwt);
|
|
}
|
|
});
|
|
|
|
test('mail without a flag relation is read by default', async ({ request }) => {
|
|
const mailbox = await createTestAddress(request, 'mail-flags-history');
|
|
try {
|
|
const seedRes = await request.post(`${WORKER_URL}/admin/test/seed_mail`, {
|
|
data: {
|
|
address: mailbox.address,
|
|
raw: [
|
|
`From: sender@example.com`,
|
|
`To: ${mailbox.address}`,
|
|
`Subject: Historical mail`,
|
|
`Message-ID: <historical-mail@test>`,
|
|
``,
|
|
`Historical body`,
|
|
].join('\r\n'),
|
|
},
|
|
});
|
|
expect(seedRes.ok()).toBe(true);
|
|
|
|
const list = async (state: string) => {
|
|
const response = await request.get(
|
|
`${WORKER_URL}/api/mails?limit=10&offset=0&mail_state=${state}`,
|
|
{ headers: { Authorization: `Bearer ${mailbox.jwt}` } },
|
|
);
|
|
expect(response.ok()).toBe(true);
|
|
return await response.json();
|
|
};
|
|
|
|
const historical = (await list('all')).results[0];
|
|
expect(historical.unread).toBe(false);
|
|
expect(await getStoredFlags(request, historical.id)).toEqual([]);
|
|
expect((await list('read')).results).toHaveLength(1);
|
|
expect((await list('unread')).results).toHaveLength(0);
|
|
} finally {
|
|
await deleteAddress(request, mailbox.jwt);
|
|
}
|
|
});
|
|
|
|
test('user APIs query and mutate flags only for bound addresses', async ({ request }) => {
|
|
const addresses: Awaited<ReturnType<typeof createTestAddress>>[] = [];
|
|
let outsider: Awaited<ReturnType<typeof createTestAddress>> | undefined;
|
|
let originalSettings: Record<string, unknown> | undefined;
|
|
let userId: number | undefined;
|
|
|
|
try {
|
|
const settingsRes = await request.get(`${WORKER_URL}/admin/user_settings`);
|
|
expect(settingsRes.ok()).toBe(true);
|
|
originalSettings = await settingsRes.json();
|
|
const enableRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
|
data: { ...originalSettings, enable: true, enableMailVerify: false, maxAddressCount: 0 },
|
|
});
|
|
expect(enableRes.ok()).toBe(true);
|
|
|
|
const email = `mail-flags-user-${Date.now()}@test.example.com`;
|
|
const password = hashPassword('mail-flags-password');
|
|
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: userJwt } = await loginRes.json();
|
|
const payload = JSON.parse(Buffer.from(userJwt.split('.')[1], 'base64url').toString('utf8'));
|
|
userId = payload.user_id;
|
|
|
|
addresses.push(
|
|
await createTestAddress(request, 'mf-user-a'),
|
|
await createTestAddress(request, 'mf-user-b'),
|
|
);
|
|
outsider = await createTestAddress(request, 'mf-outsider');
|
|
|
|
for (const mailbox of addresses) {
|
|
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
|
headers: {
|
|
...addressHeaders(mailbox.jwt),
|
|
'x-user-token': userJwt,
|
|
},
|
|
});
|
|
expect(bindRes.ok()).toBe(true);
|
|
await seedTestMail(request, mailbox.address, { subject: `Bound ${mailbox.address}` });
|
|
}
|
|
await seedTestMail(request, outsider.address, { subject: 'Outsider unread' });
|
|
|
|
const statesRes = await request.get(`${WORKER_URL}/user_api/mail-states`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
});
|
|
expect(statesRes.ok()).toBe(true);
|
|
expect((await statesRes.json()).results.map((state: { value: string }) => state.value))
|
|
.toEqual(['all', 'unread', 'read']);
|
|
|
|
const userList = async (state: string, address?: string) => {
|
|
const addressQuery = address ? `&address=${encodeURIComponent(address)}` : '';
|
|
const response = await request.get(
|
|
`${WORKER_URL}/user_api/mails?limit=20&offset=0&mail_state=${state}${addressQuery}`,
|
|
{ headers: { 'x-user-token': userJwt } },
|
|
);
|
|
expect(response.ok()).toBe(true);
|
|
return await response.json();
|
|
};
|
|
|
|
const unread = await userList('unread');
|
|
expect(unread.count).toBe(2);
|
|
expect(new Set(unread.results.map((mail: { address: string }) => mail.address)))
|
|
.toEqual(new Set(addresses.map(mailbox => mailbox.address)));
|
|
expect((await userList('unread', addresses[0].address)).results).toHaveLength(1);
|
|
expect((await userList('unread', outsider.address)).results).toHaveLength(0);
|
|
|
|
const firstMail = unread.results.find(
|
|
(mail: { address: string }) => mail.address === addresses[0].address,
|
|
);
|
|
const updateRes = await request.patch(`${WORKER_URL}/user_api/mails/state`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
data: { ids: [firstMail.id], state: 'read' },
|
|
});
|
|
expect(updateRes.ok()).toBe(true);
|
|
expect((await updateRes.json()).results[0].unread).toBe(false);
|
|
expect((await userList('read')).results.map((mail: { id: number }) => mail.id))
|
|
.toContain(firstMail.id);
|
|
|
|
const outsiderMail = (await listAddressMails(request, outsider.jwt)).results[0];
|
|
const deniedRes = await request.patch(`${WORKER_URL}/user_api/mails/state`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
data: { ids: [outsiderMail.id], state: 'read' },
|
|
});
|
|
expect(deniedRes.ok()).toBe(true);
|
|
expect((await deniedRes.json()).changes).toBe(0);
|
|
expect((await listAddressMails(request, outsider.jwt, 'unread')).results).toHaveLength(1);
|
|
} finally {
|
|
await Promise.allSettled(
|
|
[...addresses, outsider].filter((mailbox): mailbox is NonNullable<typeof mailbox> => mailbox !== undefined)
|
|
.map((mailbox) => deleteAddress(request, mailbox.jwt)),
|
|
);
|
|
if (userId !== undefined) {
|
|
await request.delete(`${WORKER_URL}/admin/users/${userId}`);
|
|
}
|
|
if (originalSettings) {
|
|
await request.post(`${WORKER_URL}/admin/user_settings`, { data: originalSettings });
|
|
}
|
|
}
|
|
});
|
|
|
|
test('disabled feature keeps legacy mail responses and rejects state APIs', async ({ request }) => {
|
|
test.skip(!WORKER_URL_ENV_OFF, 'WORKER_URL_ENV_OFF is not configured');
|
|
|
|
const createRes = await request.post(`${WORKER_URL_ENV_OFF}/api/new_address`, {
|
|
data: { name: `mail-flags-off-${Date.now()}`, domain: 'test.example.com' },
|
|
});
|
|
expect(createRes.ok()).toBe(true);
|
|
const mailbox = await createRes.json();
|
|
|
|
try {
|
|
const raw = [
|
|
`From: sender@example.com`,
|
|
`To: ${mailbox.address}`,
|
|
`Subject: Flags disabled`,
|
|
`Message-ID: <flags-disabled-${Date.now()}@test>`,
|
|
``,
|
|
`Disabled body`,
|
|
].join('\r\n');
|
|
const receiveRes = await request.post(`${WORKER_URL_ENV_OFF}/admin/test/receive_mail`, {
|
|
data: { from: 'sender@example.com', to: mailbox.address, raw },
|
|
});
|
|
expect(receiveRes.ok()).toBe(true);
|
|
|
|
const listRes = await request.get(`${WORKER_URL_ENV_OFF}/api/mails?limit=10&offset=0`, {
|
|
headers: addressHeaders(mailbox.jwt),
|
|
});
|
|
expect(listRes.ok()).toBe(true);
|
|
const list = await listRes.json();
|
|
expect(list.results).toHaveLength(1);
|
|
expect(list.results[0]).not.toHaveProperty('unread');
|
|
expect(list.results[0]).not.toHaveProperty('flags');
|
|
|
|
const statesRes = await request.get(`${WORKER_URL_ENV_OFF}/api/mail-states`, {
|
|
headers: addressHeaders(mailbox.jwt),
|
|
});
|
|
expect(statesRes.status()).toBe(403);
|
|
|
|
const filterRes = await request.get(
|
|
`${WORKER_URL_ENV_OFF}/api/mails?limit=10&offset=0&mail_state=unread`,
|
|
{ headers: addressHeaders(mailbox.jwt) },
|
|
);
|
|
expect(filterRes.status()).toBe(403);
|
|
|
|
const updateRes = await request.patch(`${WORKER_URL_ENV_OFF}/api/mails/state`, {
|
|
headers: addressHeaders(mailbox.jwt),
|
|
data: { ids: [list.results[0].id], state: 'read' },
|
|
});
|
|
expect(updateRes.status()).toBe(403);
|
|
} finally {
|
|
await request.delete(`${WORKER_URL_ENV_OFF}/admin/delete_address/${mailbox.address_id}`);
|
|
}
|
|
});
|
|
});
|