/** * PCD write tests: regex delete. */ import { assert, assertEquals, assertExists } from '@std/assert'; import { startServer, stopServer } from '$test-harness/server.ts'; import { run, setup, teardown, test } from '$test-harness/runner.ts'; import { PORTS } from '$test-harness/ports.ts'; import { base } from '../../harness/fixtures.ts'; import { normalizeSql, opCheckpoint, parseDesiredState, parseMetadata, setFailOnReferencedDelete, type JsonObject } from '../../harness/pcd.ts'; import { write } from '../../harness/write.ts'; import { assertActionFailed, assertSameGroup, createScenarioFactory, generatedCustomFormatOp, userOpsSince } from './helpers.ts'; const PORT = PORTS.pcd.writeRegexDelete; const ORIGIN = `http://localhost:${PORT}`; const { seededPcd } = createScenarioFactory(PORT, 'pcd-write-regex-delete'); setup(async () => { await startServer(PORT, { AUTH: 'off', ORIGIN }, 'preview'); }); teardown(async () => { await stopServer(PORT); }); /** * Context * Base layer seeded with one regex via base.regex(): * name='Delete Regex', pattern='\bdelete\b' * No tags, no referencing custom format conditions. Compiled. * * Submit * POST /regular-expressions/{ctx.dbId}/1?/delete with form fields: * layer = 'user' * * Expect * - userOpsSince(checkpoint).length === 1 * - op.metadata.operation === 'delete' * - op.metadata.changed_fields === ['deleted'] * - op.desired_state.deleted === true * - op.sql matches /delete from "?regular_expressions"?/i * - op.sql does NOT contain 'regular_expression_tags' */ test('unreferenced regex emits one delete op', async () => { const ctx = await seededPcd('simple', [ base.regex({ name: 'Delete Regex', pattern: '\\bdelete\\b' }) ]); const checkpoint = opCheckpoint(ctx); await write.regex.remove(ctx, 1); const ops = userOpsSince(ctx, checkpoint); assertEquals(ops.length, 1); const op = ops[0]; assertEquals(parseMetadata(op).operation, 'delete'); assertEquals(parseMetadata(op).changed_fields, ['deleted']); assertEquals(parseDesiredState(op).deleted, true); const sql = normalizeSql(op.sql); assert(/delete from "?regular_expressions"?/i.test(sql)); assert(!sql.includes('regular_expression_tags')); }); /** * Context * Base layer seeded with one regex via base.regex(): * name='Guarded Delete', pattern='\bguarded\b', description='Original' * Compiled. * * Submit * POST /regular-expressions/{ctx.dbId}/1?/delete with form fields: * layer = 'user' * * Expect * - userOpsSince(checkpoint).length === 1 * - the regex delete SQL guards by name only: * op.sql matches /delete from "?regular_expressions"? where "?name"? = /i * op.sql does NOT contain "pattern" in its WHERE clause * op.sql does NOT contain "description" in its WHERE clause * - desired_state still records the regex's identifying fields for diff display: * op.desired_state.pattern === '\bguarded\b' * op.desired_state.description === 'Original' */ test('regex delete SQL guards by name only', async () => { const ctx = await seededPcd('name-only-guard', [ base.regex({ name: 'Guarded Delete', pattern: '\\bguarded\\b', description: 'Original' }) ]); const checkpoint = opCheckpoint(ctx); await write.regex.remove(ctx, 1); const ops = userOpsSince(ctx, checkpoint); assertEquals(ops.length, 1); const op = ops[0]; const sql = normalizeSql(op.sql); const lowerSql = sql.toLowerCase(); const regexDeleteIndex = lowerSql.indexOf('delete from "regular_expressions"'); assert(regexDeleteIndex >= 0, 'expected regex delete in SQL'); const regexDeleteClause = sql.slice(regexDeleteIndex); assert(/where "?name"? = /i.test(regexDeleteClause)); assert( !/where[^;]*"?pattern"?\s*=/i.test(regexDeleteClause), `pattern should not appear in regex delete WHERE clause, got: ${regexDeleteClause}` ); assert( !/where[^;]*"?description"?\s*=/i.test(regexDeleteClause), `description should not appear in regex delete WHERE clause, got: ${regexDeleteClause}` ); const desired = parseDesiredState(op); assertEquals(desired.pattern, '\\bguarded\\b'); assertEquals(desired.description, 'Original'); }); /** * Context * Base layer seeded with one regex via base.regex(): * name='Tagged Delete Regex', pattern='\bdelete\b', tags=['A','B'] * Compiled. * * Submit * POST /regular-expressions/{ctx.dbId}/1?/delete with form fields: * layer = 'user' * * Expect * - userOpsSince(checkpoint).length === 1 * - op.desired_state.tags === ['A','B'] * - op.sql contains 'DELETE FROM regular_expression_tags' * - op.sql contains 'DELETE FROM "regular_expressions"' * - tag-link delete appears before the regex delete in the SQL */ test('regex with tags deletes tag links before the regex', async () => { const ctx = await seededPcd('with-tags', [ base.regex({ name: 'Tagged Delete Regex', pattern: '\\bdelete\\b', tags: ['A', 'B'] }) ]); const checkpoint = opCheckpoint(ctx); await write.regex.remove(ctx, 1); const ops = userOpsSince(ctx, checkpoint); assertEquals(ops.length, 1); const op = ops[0]; assertEquals(parseDesiredState(op).tags, ['A', 'B']); const sql = normalizeSql(op.sql); const lowerSql = sql.toLowerCase(); const tagDeleteIndex = lowerSql.indexOf('delete from regular_expression_tags'); const regexDeleteIndex = lowerSql.indexOf('delete from "regular_expressions"'); assert(tagDeleteIndex >= 0); assert(regexDeleteIndex >= 0); assert(tagDeleteIndex < regexDeleteIndex); }); /** * Context * Base layer seeded with: * - regex { name='Referenced Regex', pattern='\breferenced\b' } * - custom format 'Format One' with condition 'Release Title' * referencing 'Referenced Regex' via condition_patterns * general_settings.fail_on_referenced_delete = 1 (default). Compiled. * * Submit * POST /regular-expressions/{ctx.dbId}/1?/delete with form fields: * layer = 'user' * * Expect * - response.status >= 400 OR body contains '"type":"failure"' * - userOpsSince(checkpoint).length === 0 */ test('referenced regex is blocked when referenced deletes fail', async () => { const ctx = await seededPcd('referenced-blocked', [ base.regex({ name: 'Referenced Regex', pattern: '\\breferenced\\b' }), base.customFormatRegexCondition({ formatName: 'Format One', conditionName: 'Release Title', regexName: 'Referenced Regex' }) ]); const checkpoint = opCheckpoint(ctx); const response = await write.regex.submitRemove(ctx, 1); await assertActionFailed(response); assertEquals(userOpsSince(ctx, checkpoint).length, 0); }); /** * Context * Base layer seeded with: * - regex { name='Referenced Regex', pattern='\breferenced\b' } * - custom format 'Format One' with condition 'Release Title' * referencing 'Referenced Regex' via condition_patterns * setFailOnReferencedDelete(ctx, false) flips * general_settings.fail_on_referenced_delete to 0. Compiled. * * Submit * POST /regular-expressions/{ctx.dbId}/1?/delete with form fields: * layer = 'user' * * Expect * - userOpsSince(checkpoint).length === 2 * - one op with metadata.operation === 'delete' (the regex itself) * - one op with metadata.entity === 'custom_format' and * metadata.generated === true (the cascade) * - both ops share the same metadata.group_id * - cascade op.metadata.changed_fields === ['conditions'] * - cascade op.desired_state.conditions.removed is an Array */ test('referenced regex can remove dependent conditions when allowed', async () => { const ctx = await seededPcd('referenced-allowed', [ base.regex({ name: 'Referenced Regex', pattern: '\\breferenced\\b' }), base.customFormatRegexCondition({ formatName: 'Format One', conditionName: 'Release Title', regexName: 'Referenced Regex' }) ]); setFailOnReferencedDelete(ctx, false); const checkpoint = opCheckpoint(ctx); await write.regex.remove(ctx, 1); const ops = userOpsSince(ctx, checkpoint); assertEquals(ops.length, 2); const generatedOp = generatedCustomFormatOp(ops); const deleteOp = ops.find((op) => parseMetadata(op).operation === 'delete'); assertExists(deleteOp); assertSameGroup([generatedOp, deleteOp]); assertEquals(parseMetadata(generatedOp).changed_fields, ['conditions']); assertEquals( (parseDesiredState(generatedOp).conditions as JsonObject).removed instanceof Array, true ); }); await run();