mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-06 20:32:55 +08:00
* fix: auto initialize default send balance * fix: tighten send access auto init flow * refactor: centralize send balance state * fix: separate legacy repair from admin control in send balance Add an `address_sender.source` column to distinguish legacy / auto / user / admin rows. `ensureDefaultSendBalance` now only repairs rows with `source IS NULL`, so admin-disabled and user-requested rows are never overwritten. Admin POST writes tag `source = 'admin'`; new auto-init inserts tag `'auto'`; `requestSendMailAccess` inserts tag `'user'`. Bumps DB_VERSION to v0.0.8 with the usual `PRAGMA table_info` guarded ALTER, plus a standalone SQL patch under db/. Adds E2E regressions: legacy repair path, admin-disabled rows stay disabled across settings and send, send after admin deletion auto-initializes a fresh row. * fix: drop runtime legacy repair; backfill source='legacy' on migrate Pre-v0.0.8 schema cannot distinguish legacy request-send-access remnants from admin-disabled rows — both share `balance = 0, enabled = 0`. Letting ensureDefaultSendBalance repair that shape on upgrade could silently re-enable an admin-disabled row. Remove the runtime repair path entirely: - `ensureDefaultSendBalance` now uses `ON CONFLICT(address) DO NOTHING`; existing rows are never touched. - The v0.0.8 migration (and the matching SQL patch) backfills every pre-existing row with `source = 'legacy'`, making pre-migration state explicitly off-limits to runtime auto-init. - E2E: flip the legacy test to the negative direction — a `source='legacy'` zero-balance row stays untouched by settings reads and send attempts. Harden `resetSenderToLegacy` to return 404 when `meta.changes < 1`. - Update changelog and docs: legacy/admin-disabled rows must be restored manually via the admin UI. * refactor: collapse send balance auto-init to missing-row insert Per review feedback: the runtime guarantee we actually need is "create an address_sender row when one is missing, leave existing rows alone". Once `ensureDefaultSendBalance` switched to `ON CONFLICT DO NOTHING`, the `source` column, the v0.0.8 migration, and the `resetSenderToLegacy` test endpoint became dead weight — the DO NOTHING path already protects admin-disabled and admin-edited rows without any provenance metadata. - Drop `address_sender.source` and the v0.0.8 migration; revert DB_VERSION to v0.0.7. No schema change ships with this PR. - Strip the `source` field from `ensureDefaultSendBalance`, `requestSendMailAccess`, and the admin-update path. - Remove the `/admin/test/reset_sender_to_legacy` test endpoint and its E2E helper; the negative legacy-repair test it served is no longer needed because the runtime no longer touches existing rows. - E2E coverage stays focused on the three guardrails: missing-row auto-init, admin-disabled rows stay disabled, admin deletion triggers a fresh re-insert. - Tighten changelog and docs to "auto-initialize missing rows". * docs: align common-issues with missing-row-only auto-init The FAQ entries for "DEFAULT_SEND_BALANCE set but still No balance" still described the old behaviour of repairing legacy `balance = 0 && enabled = 0` rows. Rewrite both zh and en rows to match the current runtime: only addresses with no existing `address_sender` row get auto-initialised; legacy, admin-disabled, and admin-edited rows must be restored manually through the admin console.
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import {
|
|
WORKER_URL,
|
|
createTestAddress,
|
|
requestSendAccess,
|
|
deleteAddress,
|
|
deleteAddressSender,
|
|
getAddressSender,
|
|
updateAddressSender,
|
|
} from '../../fixtures/test-helpers';
|
|
|
|
test.describe('Send Access', () => {
|
|
test('request send access stays idempotent when default balance is auto-initialized', async ({ request }) => {
|
|
const { jwt, address } = await createTestAddress(request, 'send-access');
|
|
|
|
try {
|
|
// First request — should succeed even if balance will also auto-init elsewhere.
|
|
await requestSendAccess(request, jwt);
|
|
|
|
// Verify balance is set via settings
|
|
const settingsRes = await request.get(`${WORKER_URL}/api/settings`, {
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
});
|
|
expect(settingsRes.ok()).toBe(true);
|
|
const settings = await settingsRes.json();
|
|
expect(settings.send_balance).toBe(10);
|
|
|
|
// Duplicate request should stay safe and idempotent.
|
|
const dupRes = await request.post(`${WORKER_URL}/api/request_send_mail_access`, {
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
});
|
|
expect(dupRes.ok()).toBe(true);
|
|
|
|
const sender = await getAddressSender(request, address);
|
|
expect(sender.balance).toBe(10);
|
|
expect(sender.enabled).toBe(1);
|
|
} finally {
|
|
await deleteAddress(request, jwt);
|
|
}
|
|
});
|
|
|
|
test('admin-disabled rows are not overwritten by settings or send', async ({ request }) => {
|
|
const { jwt, address } = await createTestAddress(request, 'sa-admin-blocked');
|
|
|
|
try {
|
|
await requestSendAccess(request, jwt);
|
|
|
|
const sender = await getAddressSender(request, address);
|
|
await updateAddressSender(request, {
|
|
address,
|
|
address_id: sender.id,
|
|
balance: 0,
|
|
enabled: false,
|
|
});
|
|
|
|
// Reading settings must not auto-repair an admin-disabled row.
|
|
const settingsRes = await request.get(`${WORKER_URL}/api/settings`, {
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
});
|
|
expect(settingsRes.ok()).toBe(true);
|
|
const settings = await settingsRes.json();
|
|
expect(settings.send_balance).toBe(0);
|
|
|
|
const stillDisabled = await getAddressSender(request, address);
|
|
expect(stillDisabled.balance).toBe(0);
|
|
expect(stillDisabled.enabled).toBe(0);
|
|
|
|
// Attempting to send must also fail and must not auto-repair the row.
|
|
const sendRes = await request.post(`${WORKER_URL}/api/send_mail`, {
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
data: {
|
|
from_name: 'E2E',
|
|
to_name: 'E2E',
|
|
to_mail: 'recipient@test.example.com',
|
|
subject: 'should not send',
|
|
content: 'body',
|
|
is_html: false,
|
|
},
|
|
});
|
|
expect(sendRes.ok()).toBe(false);
|
|
|
|
const afterSend = await getAddressSender(request, address);
|
|
expect(afterSend.balance).toBe(0);
|
|
expect(afterSend.enabled).toBe(0);
|
|
} finally {
|
|
await deleteAddress(request, jwt);
|
|
}
|
|
});
|
|
|
|
test('send after admin deletion auto-initializes a fresh sender row', async ({ request }) => {
|
|
const { jwt, address } = await createTestAddress(request, 'send-access-deleted');
|
|
|
|
try {
|
|
await requestSendAccess(request, jwt);
|
|
|
|
const sender = await getAddressSender(request, address);
|
|
await deleteAddressSender(request, sender.id);
|
|
|
|
const sendRes = await request.post(`${WORKER_URL}/api/send_mail`, {
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
data: {
|
|
from_name: 'E2E',
|
|
to_name: 'E2E',
|
|
to_mail: 'recipient@test.example.com',
|
|
subject: `E2E reinit ${Date.now()}`,
|
|
content: 'body',
|
|
is_html: false,
|
|
},
|
|
});
|
|
expect(sendRes.ok()).toBe(true);
|
|
|
|
// A fresh row should exist with the default balance decremented by 1.
|
|
const recreated = await getAddressSender(request, address);
|
|
expect(recreated.enabled).toBe(1);
|
|
expect(recreated.balance).toBe(9);
|
|
} finally {
|
|
await deleteAddress(request, jwt);
|
|
}
|
|
});
|
|
|
|
test('request send access does not falsely succeed when quota is already exhausted', async ({ request }) => {
|
|
const { jwt, address } = await createTestAddress(request, 'sendexh');
|
|
|
|
try {
|
|
await requestSendAccess(request, jwt);
|
|
|
|
const sender = await getAddressSender(request, address);
|
|
await updateAddressSender(request, {
|
|
address,
|
|
address_id: sender.id,
|
|
balance: 0,
|
|
enabled: true,
|
|
});
|
|
|
|
const retryRes = await request.post(`${WORKER_URL}/api/request_send_mail_access`, {
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
});
|
|
expect(retryRes.status()).toBe(400);
|
|
const retryBody = await retryRes.text();
|
|
expect(retryBody).toContain('Already');
|
|
} finally {
|
|
await deleteAddress(request, jwt);
|
|
}
|
|
});
|
|
});
|