718 lines
19 KiB
TypeScript
718 lines
19 KiB
TypeScript
/**
|
|
* Edge-case tests for HTTP error handling, legacy compatibility, and background cleanup paths.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createExecutionContext, env, waitOnExecutionContext } from 'cloudflare:test';
|
|
import worker from '../src/index';
|
|
import { buildPasteBlobKey, createPaste, getEnvelope } from '../src/lib/repository';
|
|
import { resetState, sampleEnvelope, IncomingRequest } from './support';
|
|
|
|
// Direct row insertion keeps failure-path tests precise without going through the full API.
|
|
async function insertExpiredPaste(id: string, expireAt: number): Promise<void> {
|
|
await env.DB.prepare(
|
|
`INSERT INTO pastes (
|
|
id, schema_version, blob_key, created_at, expire_at, status,
|
|
burn_after_reading, discussion_open, formatter, has_attachment,
|
|
delete_token_hash, comment_count, size_bytes, metadata_json
|
|
) VALUES (?1, 2, ?2, ?3, ?4, 'active', 0, 0, 'plaintext', 0, 'hash', 0, 10, '{}')`,
|
|
)
|
|
.bind(id, buildPasteBlobKey(id), 1_700_000_000_000, expireAt)
|
|
.run();
|
|
await env.PASTES_BUCKET.put(buildPasteBlobKey(id), JSON.stringify(sampleEnvelope()));
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await resetState();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('http and worker edge cases', () => {
|
|
it('returns config and handles API preflight and unknown routes', async () => {
|
|
const configResponse = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/config'),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
TURNSTILE_SITE_KEY: 'site-key',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
const config = await configResponse.json();
|
|
expect(configResponse.status).toBe(200);
|
|
expect(config.appVersion).toBe('1.0.0');
|
|
expect(config.requireTurnstile).toBe(true);
|
|
expect(config.turnstileSiteKey).toBe('site-key');
|
|
expect(config.uiLanguageLabel).toBe('English');
|
|
expect(config.uiLanguageCode).toBe('en');
|
|
expect(config.uiLanguageName).toBe('English');
|
|
expect(config.uiLanguageRtl).toBe(false);
|
|
expect(config.uiLanguages).toHaveLength(37);
|
|
expect(config.uiThemeLabel).toBe('bootstrap5');
|
|
expect(config.uiThemes).toContain('bootstrap-dark-page');
|
|
|
|
const optionsResponse = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes/abcdef1234567890', {
|
|
method: 'OPTIONS',
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(optionsResponse.status).toBe(204);
|
|
expect(optionsResponse.headers.get('access-control-allow-methods')).toContain('GET');
|
|
|
|
const missingRoute = await worker.fetch(new IncomingRequest('https://example.com/api/v1/unknown'), env, createExecutionContext());
|
|
expect(missingRoute.status).toBe(404);
|
|
});
|
|
|
|
it('rejects malformed payloads and invalid identifiers', async () => {
|
|
const invalidJson = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: '{',
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(invalidJson.status).toBe(400);
|
|
expect((await invalidJson.json()).error).toContain('valid JSON');
|
|
|
|
const emptyBody = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(emptyBody.status).toBe(400);
|
|
|
|
const oversized = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
ct: 'x'.repeat(256),
|
|
}),
|
|
),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_MAX_PASTE_BYTES: '64',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(oversized.status).toBe(413);
|
|
|
|
const invalidId = await worker.fetch(new IncomingRequest('https://example.com/api/v1/pastes/not-an-id'), env, createExecutionContext());
|
|
expect(invalidId.status).toBe(404);
|
|
|
|
const missingClaimToken = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes/abcdef1234567890/consume', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(missingClaimToken.status).toBe(400);
|
|
});
|
|
|
|
it('enforces turnstile and import authorization failures', async () => {
|
|
const missingSecret = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(sampleEnvelope()),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(missingSecret.status).toBe(500);
|
|
|
|
const missingToken = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(sampleEnvelope()),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
TURNSTILE_SECRET_KEY: 'secret',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(missingToken.status).toBe(400);
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(
|
|
async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
'error-codes': ['bad-token'],
|
|
}),
|
|
{
|
|
headers: { 'content-type': 'application/json' },
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
const failedTurnstile = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'CF-Connecting-IP': '203.0.113.5',
|
|
},
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
turnstileToken: 'bad-token',
|
|
}),
|
|
),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
TURNSTILE_SECRET_KEY: 'secret',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(failedTurnstile.status).toBe(400);
|
|
expect((await failedTurnstile.json()).error).toContain('bad-token');
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ success: false }), {
|
|
headers: { 'content-type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
|
|
const failedWithoutCodes = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
turnstileToken: 'bad-token',
|
|
}),
|
|
),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
TURNSTILE_SECRET_KEY: 'secret',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(failedWithoutCodes.status).toBe(400);
|
|
expect((await failedWithoutCodes.json()).error).toContain('Turnstile verification failed.');
|
|
|
|
const importDisabled = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/admin/import/privatebin', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(importDisabled.status).toBe(404);
|
|
|
|
const importUnauthorized = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/admin/import/privatebin', {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
authorization: 'Bearer wrong-token',
|
|
},
|
|
body: JSON.stringify({ pasteId: 'abcdef1234567890', paste: sampleEnvelope() }),
|
|
}),
|
|
{
|
|
...env,
|
|
IMPORT_TOKEN: 'expected-token',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(importUnauthorized.status).toBe(401);
|
|
});
|
|
|
|
it('covers alternate asset, import, and unexpected error branches', async () => {
|
|
const noNavigationHints = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes?abcdef1234567890'),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(noNavigationHints.status).toBe(404);
|
|
|
|
const htmlShareNavigation = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes?abcdef1234567890', {
|
|
headers: { accept: 'text/html' },
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(htmlShareNavigation.status).toBe(200);
|
|
expect(htmlShareNavigation.headers.get('content-type')).toContain('text/html');
|
|
|
|
const queryRoute = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes?pasteid=abcdef1234567890', {
|
|
headers: { accept: 'text/html' },
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(queryRoute.status).toBe(404);
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ success: true }), {
|
|
headers: { 'content-type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
|
|
const turnstileAccepted = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
turnstileToken: 'good-token',
|
|
}),
|
|
),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
TURNSTILE_SECRET_KEY: 'secret',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(turnstileAccepted.status).toBe(201);
|
|
|
|
const skippedImport = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/admin/import/privatebin', {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-import-token': 'expected-token',
|
|
},
|
|
body: JSON.stringify({
|
|
pasteId: '90909090abcdefab',
|
|
paste: sampleEnvelope({
|
|
meta: { created: 1_700_000_000, expire_date: 1 },
|
|
}),
|
|
}),
|
|
}),
|
|
{
|
|
...env,
|
|
IMPORT_TOKEN: 'expected-token',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(skippedImport.status).toBe(200);
|
|
expect((await skippedImport.json()).skipped).toBe(true);
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () => {
|
|
throw new Error('turnstile exploded');
|
|
}),
|
|
);
|
|
|
|
const apiInternal = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
turnstileToken: 'broken-token',
|
|
}),
|
|
),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
TURNSTILE_SECRET_KEY: 'secret',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(apiInternal.status).toBe(500);
|
|
expect((await apiInternal.json()).error).toContain('Internal server error');
|
|
|
|
const legacyInternal = await worker.fetch(
|
|
new IncomingRequest('https://example.com/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'X-Requested-With': 'JSONHttpRequest',
|
|
},
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
turnstileToken: 'broken-token',
|
|
}),
|
|
),
|
|
}),
|
|
{
|
|
...env,
|
|
XBIN_REQUIRE_TURNSTILE: 'true',
|
|
TURNSTILE_SECRET_KEY: 'secret',
|
|
},
|
|
createExecutionContext(),
|
|
);
|
|
expect(legacyInternal.status).toBe(500);
|
|
expect((await legacyInternal.json()).message).toContain('Internal server error');
|
|
});
|
|
|
|
it('supports legacy JSON API success and failure flows', async () => {
|
|
const legacyHeaders = {
|
|
'content-type': 'application/json',
|
|
'X-Requested-With': 'JSONHttpRequest',
|
|
};
|
|
|
|
const createResponse = await worker.fetch(
|
|
new IncomingRequest('https://example.com/', {
|
|
method: 'POST',
|
|
headers: legacyHeaders,
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
adata: [['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'], 'plaintext', 1, 0],
|
|
}),
|
|
),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
const created = await createResponse.json();
|
|
expect(createResponse.status).toBe(201);
|
|
expect(created.status).toBe(0);
|
|
expect(created.url).toContain(`?${created.id}`);
|
|
|
|
const getResponse = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/?${created.id}`, {
|
|
headers: { 'X-Requested-With': 'JSONHttpRequest' },
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(getResponse.status).toBe(200);
|
|
expect((await getResponse.json()).status).toBe(0);
|
|
|
|
const commentResponse = await worker.fetch(
|
|
new IncomingRequest('https://example.com/', {
|
|
method: 'POST',
|
|
headers: legacyHeaders,
|
|
body: JSON.stringify({
|
|
v: 2,
|
|
adata: ['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'],
|
|
ct: 'legacy-comment',
|
|
pasteid: created.id,
|
|
parentid: created.id,
|
|
}),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(commentResponse.status).toBe(201);
|
|
expect((await commentResponse.json()).status).toBe(0);
|
|
|
|
const wrongDelete = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/?pasteid=${created.id}`, {
|
|
method: 'DELETE',
|
|
headers: legacyHeaders,
|
|
body: JSON.stringify({ deletetoken: 'wrong-token' }),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(wrongDelete.status).toBe(403);
|
|
expect((await wrongDelete.json()).status).toBe(1);
|
|
|
|
const deleteCtx = createExecutionContext();
|
|
const deleteResponse = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/?pasteid=${created.id}`, {
|
|
method: 'DELETE',
|
|
headers: legacyHeaders,
|
|
body: JSON.stringify({ deletetoken: created.deletetoken }),
|
|
}),
|
|
env,
|
|
deleteCtx,
|
|
);
|
|
expect(deleteResponse.status).toBe(200);
|
|
await waitOnExecutionContext(deleteCtx);
|
|
|
|
const missingLegacyId = await worker.fetch(
|
|
new IncomingRequest('https://example.com/', {
|
|
headers: { 'X-Requested-With': 'JSONHttpRequest' },
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(missingLegacyId.status).toBe(400);
|
|
expect((await missingLegacyId.json()).status).toBe(1);
|
|
|
|
const wrongMethod = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/?pasteid=${created.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'X-Requested-With': 'JSONHttpRequest' },
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(wrongMethod.status).toBe(405);
|
|
expect((await wrongMethod.json()).status).toBe(1);
|
|
});
|
|
|
|
it('covers modern and legacy API edge failures', async () => {
|
|
const created = await createPaste(
|
|
env,
|
|
{
|
|
envelope: sampleEnvelope(),
|
|
expireKey: '1day',
|
|
turnstileToken: null,
|
|
},
|
|
Date.now(),
|
|
);
|
|
|
|
const missingDeleteToken = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/api/v1/pastes/${created.id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(missingDeleteToken.status).toBe(400);
|
|
|
|
const wrongDeleteToken = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/api/v1/pastes/${created.id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ deleteToken: 'wrong-token' }),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(wrongDeleteToken.status).toBe(403);
|
|
|
|
const discussionDisabled = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/api/v1/pastes/${created.id}/comments`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(sampleEnvelope({ parentId: created.id })),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(discussionDisabled.status).toBe(400);
|
|
|
|
await env.PASTES_BUCKET.delete(buildPasteBlobKey(created.id));
|
|
const missingBlob = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/api/v1/pastes/${created.id}`),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(missingBlob.status).toBe(404);
|
|
|
|
const burnResponse = await worker.fetch(
|
|
new IncomingRequest('https://example.com/api/v1/pastes', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
adata: [['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'], 'plaintext', 0, 1],
|
|
}),
|
|
),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
const burned = await burnResponse.json();
|
|
|
|
const firstRead = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/api/v1/pastes/${burned.id}`),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(firstRead.status).toBe(200);
|
|
|
|
const secondRead = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/api/v1/pastes/${burned.id}`),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(secondRead.status).toBe(409);
|
|
|
|
const wrongConsume = await worker.fetch(
|
|
new IncomingRequest(`https://example.com/api/v1/pastes/${burned.id}/consume`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ claimToken: 'wrong-token' }),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(wrongConsume.status).toBe(400);
|
|
|
|
const legacyHeaders = {
|
|
'content-type': 'application/json',
|
|
'X-Requested-With': 'JSONHttpRequest',
|
|
};
|
|
const legacyPaste = await worker.fetch(
|
|
new IncomingRequest('https://example.com/', {
|
|
method: 'POST',
|
|
headers: legacyHeaders,
|
|
body: JSON.stringify(
|
|
sampleEnvelope({
|
|
adata: [['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'], 'plaintext', 1, 0],
|
|
}),
|
|
),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
const legacyCreated = await legacyPaste.json();
|
|
|
|
const legacyComment = await worker.fetch(
|
|
new IncomingRequest('https://example.com/', {
|
|
method: 'POST',
|
|
headers: legacyHeaders,
|
|
body: JSON.stringify({
|
|
v: 2,
|
|
adata: ['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'],
|
|
ct: 'legacy-comment-without-parent',
|
|
pasteid: legacyCreated.id,
|
|
}),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(legacyComment.status).toBe(201);
|
|
|
|
const legacyMissingDeleteId = await worker.fetch(
|
|
new IncomingRequest('https://example.com/?pasteid=', {
|
|
method: 'DELETE',
|
|
headers: legacyHeaders,
|
|
body: JSON.stringify({ deletetoken: 'wrong-token' }),
|
|
}),
|
|
env,
|
|
createExecutionContext(),
|
|
);
|
|
expect(legacyMissingDeleteId.status).toBe(400);
|
|
});
|
|
|
|
it('runs scheduled cleanup and queue processing', async () => {
|
|
const emptyQueued: unknown[] = [];
|
|
const emptyCtx = createExecutionContext();
|
|
await worker.scheduled?.(
|
|
{} as ScheduledController,
|
|
{
|
|
...env,
|
|
GC_QUEUE: {
|
|
sendBatch: async (messages: unknown[]) => {
|
|
emptyQueued.push(...messages);
|
|
},
|
|
} as Queue,
|
|
},
|
|
emptyCtx,
|
|
);
|
|
await waitOnExecutionContext(emptyCtx);
|
|
expect(emptyQueued).toHaveLength(0);
|
|
|
|
await insertExpiredPaste('abababababababab', Date.now() - 10_000);
|
|
|
|
const queued: unknown[] = [];
|
|
const ctx = createExecutionContext();
|
|
await worker.scheduled?.(
|
|
{} as ScheduledController,
|
|
{
|
|
...env,
|
|
GC_QUEUE: {
|
|
sendBatch: async (messages: unknown[]) => {
|
|
queued.push(...messages);
|
|
},
|
|
} as Queue,
|
|
},
|
|
ctx,
|
|
);
|
|
await waitOnExecutionContext(ctx);
|
|
expect(queued).toHaveLength(1);
|
|
|
|
const created = await createPaste(
|
|
env,
|
|
{
|
|
envelope: sampleEnvelope(),
|
|
expireKey: '1day',
|
|
turnstileToken: null,
|
|
},
|
|
Date.now(),
|
|
);
|
|
|
|
let acked = false;
|
|
let retried = false;
|
|
await worker.queue?.(
|
|
{
|
|
messages: [
|
|
{
|
|
body: { pasteId: created.id, finalStatus: 'expired' },
|
|
ack() {
|
|
acked = true;
|
|
},
|
|
retry() {
|
|
retried = true;
|
|
},
|
|
},
|
|
],
|
|
} as never,
|
|
env,
|
|
);
|
|
expect(acked).toBe(true);
|
|
expect(retried).toBe(false);
|
|
expect(await getEnvelope(env.PASTES_BUCKET, buildPasteBlobKey(created.id))).toBeNull();
|
|
|
|
let retriedOnFailure = false;
|
|
await worker.queue?.(
|
|
{
|
|
messages: [
|
|
{
|
|
body: { pasteId: 'ffffffffffffffff', finalStatus: 'expired' },
|
|
ack() {
|
|
throw new Error('ack should not run');
|
|
},
|
|
retry() {
|
|
retriedOnFailure = true;
|
|
},
|
|
},
|
|
],
|
|
} as never,
|
|
{
|
|
...env,
|
|
PASTES_BUCKET: {
|
|
delete: async () => {
|
|
throw new Error('boom');
|
|
},
|
|
} as R2Bucket,
|
|
},
|
|
);
|
|
expect(retriedOnFailure).toBe(true);
|
|
});
|
|
});
|