Преглед на файлове

test: clean up test artifacts

Y7000\张扬阳 преди 2 седмици
родител
ревизия
3c519b8471

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "test:user-admin": "tsx tests/test-user-admin.ts",
     "test:permission-admin": "tsx tests/test-permission-admin.ts",
     "test:media-pipeline": "tsx tests/integration/media-pipeline.test.ts",
+    "test:cleanup": "tsx tests/cleanup-test-artifacts.ts",
     "worker:media": "tsx src/workers/media-processor.ts"
   },
   "repository": {

+ 29 - 26
src/actions/media.ts

@@ -32,6 +32,7 @@ export type MediaListItem = MediaStatusResult & {
 
 type UploadMediaOptions = {
   ownerId?: string | null;
+  enqueue?: boolean;
 };
 
 type GetMediaStatusOptions = {
@@ -140,34 +141,36 @@ export async function uploadMedia(
     throw error;
   }
 
-  try {
-    const { mediaQueue } = await import('@/lib/queue/index');
-    await mediaQueue.add(
-      'process-media',
-      {
-        mediaId: mediaRecord.id,
-        storageKey: storageKey,
-      },
-      {
-        attempts: 3,
-        backoff: {
-          type: 'exponential',
-          delay: 3000,
+  if (options.enqueue !== false) {
+    try {
+      const { mediaQueue } = await import('@/lib/queue/index');
+      await mediaQueue.add(
+        'process-media',
+        {
+          mediaId: mediaRecord.id,
+          storageKey: storageKey,
         },
-        removeOnComplete: 100,
-        removeOnFail: 100,
-      }
-    );
-  } catch (error) {
-    await db.update(media)
-      .set({
-        status: 'failed',
-        errorMessage: error instanceof Error ? error.message : 'Queue submission failed',
-        updatedAt: new Date(),
-      })
-      .where(eq(media.id, mediaRecord.id));
+        {
+          attempts: 3,
+          backoff: {
+            type: 'exponential',
+            delay: 3000,
+          },
+          removeOnComplete: 100,
+          removeOnFail: 100,
+        }
+      );
+    } catch (error) {
+      await db.update(media)
+        .set({
+          status: 'failed',
+          errorMessage: error instanceof Error ? error.message : 'Queue submission failed',
+          updatedAt: new Date(),
+        })
+        .where(eq(media.id, mediaRecord.id));
 
-    throw error;
+      throw error;
+    }
   }
 
   return {

+ 64 - 0
tests/cleanup-test-artifacts.ts

@@ -0,0 +1,64 @@
+import { ilike, inArray, or } from 'drizzle-orm';
+import { db } from '../src/db';
+import { permissions, rolePermissions, users } from '../src/db/schema/auth';
+import { media } from '../src/db/schema/media';
+import { aclRules, resources } from '../src/db/schema/resource';
+import { cleanupMediaRecord } from './helpers/media-cleanup';
+
+async function cleanupTestArtifacts() {
+  const mediaRows = await db.select({ id: media.id }).from(media).where(or(
+    ilike(media.filename, 'e2e-%'),
+    ilike(media.filename, 'media-status-%'),
+    ilike(media.filename, 'media-list-%'),
+    ilike(media.filename, 'permission-test%'),
+  ));
+
+  for (const row of mediaRows) {
+    await cleanupMediaRecord(row.id);
+  }
+
+  const resourceRows = await db.select({ id: resources.id }).from(resources).where(or(
+    ilike(resources.path, '/permission-admin/%'),
+    ilike(resources.path, '/media-status/%'),
+    ilike(resources.path, '/media-list/%'),
+  ));
+
+  const resourceIds = resourceRows.map((row) => row.id);
+  if (resourceIds.length > 0) {
+    await db.delete(aclRules).where(inArray(aclRules.resourceId, resourceIds));
+    await db.delete(resources).where(inArray(resources.id, resourceIds));
+  }
+
+  const permissionRows = await db
+    .select({ id: permissions.id })
+    .from(permissions)
+    .where(ilike(permissions.action, 'read-1%'));
+
+  const permissionIds = permissionRows.map((row) => row.id);
+  if (permissionIds.length > 0) {
+    await db.delete(rolePermissions).where(inArray(rolePermissions.permissionId, permissionIds));
+    await db.delete(permissions).where(inArray(permissions.id, permissionIds));
+  }
+
+  const userRows = await db
+    .select({ id: users.id })
+    .from(users)
+    .where(ilike(users.email, 'admin-flow-%@ekb.com'));
+
+  const userIds = userRows.map((row) => row.id);
+  if (userIds.length > 0) {
+    await db.delete(users).where(inArray(users.id, userIds));
+  }
+
+  console.log('Cleaned test artifacts:', {
+    media: mediaRows.length,
+    resources: resourceRows.length,
+    permissions: permissionRows.length,
+    users: userRows.length,
+  });
+}
+
+cleanupTestArtifacts().catch((error) => {
+  console.error('Failed to clean test artifacts:', error);
+  process.exit(1);
+});

+ 61 - 0
tests/helpers/media-cleanup.ts

@@ -0,0 +1,61 @@
+import { eq } from 'drizzle-orm';
+import { db } from '../../src/db';
+import { media } from '../../src/db/schema/media';
+import { resources } from '../../src/db/schema/resource';
+import { mediaBucketName } from '../../src/lib/config';
+import minioClient from '../../src/lib/minio';
+
+type MediaMetadata = {
+  hlsPath?: string;
+};
+
+async function removeObjectIfExists(objectName: string) {
+  try {
+    await minioClient.removeObject(mediaBucketName, objectName);
+  } catch (error: any) {
+    if (error?.code !== 'NoSuchKey' && error?.code !== 'NotFound') {
+      throw error;
+    }
+  }
+}
+
+async function removePrefix(prefix: string) {
+  const objectNames: string[] = [];
+  const stream = minioClient.listObjectsV2(mediaBucketName, prefix, true);
+
+  await new Promise<void>((resolve, reject) => {
+    stream.on('data', (object) => {
+      if (object.name) objectNames.push(object.name);
+    });
+    stream.on('error', reject);
+    stream.on('end', resolve);
+  });
+
+  if (objectNames.length > 0) {
+    await minioClient.removeObjects(mediaBucketName, objectNames);
+  }
+}
+
+export async function cleanupMediaRecord(mediaId: string) {
+  const [mediaRecord] = await db.select().from(media).where(eq(media.id, mediaId)).limit(1);
+
+  if (!mediaRecord) {
+    return;
+  }
+
+  const metadata = mediaRecord.metadata as MediaMetadata | null;
+  const hlsPath = metadata?.hlsPath;
+  const hlsPrefix = hlsPath?.includes('/') ? hlsPath.slice(0, hlsPath.lastIndexOf('/') + 1) : null;
+
+  await removeObjectIfExists(mediaRecord.storageKey);
+  if (hlsPrefix) {
+    await removePrefix(hlsPrefix);
+  }
+
+  await db.transaction(async (tx) => {
+    await tx.delete(media).where(eq(media.id, mediaId));
+    if (mediaRecord.resourceId) {
+      await tx.delete(resources).where(eq(resources.id, mediaRecord.resourceId));
+    }
+  });
+}

+ 7 - 1
tests/integration/media-pipeline.test.ts

@@ -4,10 +4,12 @@ import { startMediaWorker } from '../../src/workers/media-processor';
 import * as fs from 'fs';
 import * as path from 'path';
 import * as os from 'os';
+import { cleanupMediaRecord } from '../helpers/media-cleanup';
 
 async function runTest() {
   console.log('🧪 Starting Media Pipeline Integration Test...');
   let exitCode = 1;
+  let mediaId: string | null = null;
 
   // 1. Setup: Create a dummy video file (mp4)
   const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-media-'));
@@ -34,7 +36,7 @@ async function runTest() {
     // 3. Simulate Upload (Producer)
     console.log('📤 Simulating upload...');
     const fileMock = {
-      name: 'test_sample.mp4',
+      name: 'e2e-pipeline-test_sample.mp4',
       type: 'video/mp4',
       size: fs.statSync(testFilePath).size,
       arrayBuffer: async () => {
@@ -47,6 +49,7 @@ async function runTest() {
     };
 
     const record = await uploadMedia(fileMock as any);
+    mediaId = record.media.id;
     console.log(`✅ Uploaded. Media ID: ${record.media.id}, Status: ${record.media.status}`);
 
     // 4. Wait for Worker to process (Polling)
@@ -87,6 +90,9 @@ async function runTest() {
   } finally {
     // Cleanup
     fs.rmSync(testDir, { recursive: true, force: true });
+    if (mediaId) {
+      await cleanupMediaRecord(mediaId);
+    }
     await mediaQueue.close();
     process.exit(exitCode);
   }

+ 37 - 29
tests/test-media-list.ts

@@ -4,6 +4,7 @@ import { db } from '../src/db';
 import { users } from '../src/db/schema/auth';
 import { media } from '../src/db/schema/media';
 import { resources } from '../src/db/schema/resource';
+import { cleanupMediaRecord } from './helpers/media-cleanup';
 
 async function upsertUser(email: string, name: string) {
   const [existing] = await db.select().from(users).where(eq(users.email, email)).limit(1);
@@ -23,6 +24,7 @@ async function upsertUser(email: string, name: string) {
 
 async function runTest() {
   console.log('Testing media list visibility and filters...');
+  const createdMediaIds: string[] = [];
 
   const owner = await upsertUser('media-list-owner@ekb.com', 'Media List Owner');
   const outsider = await upsertUser('media-list-outsider@ekb.com', 'Media List Outsider');
@@ -43,6 +45,7 @@ async function runTest() {
     size: BigInt(1),
     status: 'pending',
   }).returning();
+  createdMediaIds.push(createdMedia.id);
 
   const [completedResource] = await db.insert(resources).values({
     name: `media-list-filter-${suffix}.mp4`,
@@ -59,35 +62,40 @@ async function runTest() {
     size: BigInt(1),
     status: 'completed',
   }).returning();
-
-  const ownerList = await listMedia({ auth: { userId: owner.id, groupIds: [] } });
-  const outsiderList = await listMedia({ auth: { userId: outsider.id, groupIds: [] } });
-
-  const ownerCanSee = ownerList.some((item) => item.id === createdMedia.id);
-  const outsiderCanSee = outsiderList.some((item) => item.id === createdMedia.id);
-
-  if (!ownerCanSee || outsiderCanSee) {
-    console.error('Media list visibility did not respect ownership permissions.');
-    process.exit(1);
-  }
-
-  const queryList = await listMedia({
-    auth: { userId: owner.id, groupIds: [] },
-    query: `filter-${suffix}`,
-  });
-  const completedList = await listMedia({
-    auth: { userId: owner.id, groupIds: [] },
-    status: 'completed',
-  });
-
-  const queryMatched = queryList.some((item) => item.id === completedMedia.id)
-    && !queryList.some((item) => item.id === createdMedia.id);
-  const statusMatched = completedList.some((item) => item.id === completedMedia.id)
-    && !completedList.some((item) => item.id === createdMedia.id);
-
-  if (!queryMatched || !statusMatched) {
-    console.error('Media list filters did not match expected results.');
-    process.exit(1);
+  createdMediaIds.push(completedMedia.id);
+
+  try {
+    const ownerList = await listMedia({ auth: { userId: owner.id, groupIds: [] } });
+    const outsiderList = await listMedia({ auth: { userId: outsider.id, groupIds: [] } });
+
+    const ownerCanSee = ownerList.some((item) => item.id === createdMedia.id);
+    const outsiderCanSee = outsiderList.some((item) => item.id === createdMedia.id);
+
+    if (!ownerCanSee || outsiderCanSee) {
+      throw new Error('Media list visibility did not respect ownership permissions.');
+    }
+
+    const queryList = await listMedia({
+      auth: { userId: owner.id, groupIds: [] },
+      query: `filter-${suffix}`,
+    });
+    const completedList = await listMedia({
+      auth: { userId: owner.id, groupIds: [] },
+      status: 'completed',
+    });
+
+    const queryMatched = queryList.some((item) => item.id === completedMedia.id)
+      && !queryList.some((item) => item.id === createdMedia.id);
+    const statusMatched = completedList.some((item) => item.id === completedMedia.id)
+      && !completedList.some((item) => item.id === createdMedia.id);
+
+    if (!queryMatched || !statusMatched) {
+      throw new Error('Media list filters did not match expected results.');
+    }
+  } finally {
+    for (const mediaId of createdMediaIds) {
+      await cleanupMediaRecord(mediaId);
+    }
   }
 
   console.log('Media list visibility and filters passed.');

+ 28 - 29
tests/test-media-permission.ts

@@ -4,6 +4,7 @@ import { db } from '../src/db';
 import { users } from '../src/db/schema/auth';
 import { media } from '../src/db/schema/media';
 import { resources } from '../src/db/schema/resource';
+import { cleanupMediaRecord } from './helpers/media-cleanup';
 
 async function upsertUser(email: string, name: string) {
   const [existing] = await db.select().from(users).where(eq(users.email, email)).limit(1);
@@ -23,21 +24,11 @@ async function upsertUser(email: string, name: string) {
 
 async function runTest() {
   console.log('🔐 Testing media permission checks...');
+  let ownedMediaId: string | null = null;
 
   const owner = await upsertUser('media-owner@ekb.com', 'Media Owner');
   const outsider = await upsertUser('media-outsider@ekb.com', 'Media Outsider');
 
-  const [latestCompletedMedia] = await db
-    .select()
-    .from(media)
-    .where(eq(media.status, 'completed'))
-    .limit(1);
-
-  if (!latestCompletedMedia?.metadata) {
-    console.error('❌ No completed media found. Run npm run test:media-pipeline first.');
-    process.exit(1);
-  }
-
   const pathSuffix = `${Date.now()}-permission-test.mp4`;
   const [resource] = await db.insert(resources).values({
     name: 'permission-test.mp4',
@@ -53,28 +44,36 @@ async function runTest() {
     mimeType: 'video/mp4',
     size: BigInt(1),
     status: 'completed',
-    metadata: latestCompletedMedia.metadata,
+    metadata: {
+      hlsPath: `hls/permission-test-${pathSuffix}/index.m3u8`,
+      processedAt: new Date().toISOString(),
+    },
   }).returning();
-
-  const ownerStatus = await getMediaStatus(ownedMedia.id, {
-    auth: { userId: owner.id, groupIds: [] },
-  });
-
-  if (!ownerStatus?.hlsUrl) {
-    console.error('❌ Owner did not receive playback URL.');
-    process.exit(1);
-  }
+  ownedMediaId = ownedMedia.id;
 
   try {
-    await getMediaStatus(ownedMedia.id, {
-      auth: { userId: outsider.id, groupIds: [] },
+    const ownerStatus = await getMediaStatus(ownedMedia.id, {
+      auth: { userId: owner.id, groupIds: [] },
     });
-    console.error('❌ Outsider unexpectedly received media access.');
-    process.exit(1);
-  } catch (error) {
-    const message = error instanceof Error ? error.message : String(error);
-    if (!message.includes('No matching permission')) {
-      throw error;
+
+    if (!ownerStatus?.hlsUrl) {
+      throw new Error('Owner did not receive playback URL.');
+    }
+
+    try {
+      await getMediaStatus(ownedMedia.id, {
+        auth: { userId: outsider.id, groupIds: [] },
+      });
+      throw new Error('Outsider unexpectedly received media access.');
+    } catch (error) {
+      const message = error instanceof Error ? error.message : String(error);
+      if (!message.includes('No matching permission')) {
+        throw error;
+      }
+    }
+  } finally {
+    if (ownedMediaId) {
+      await cleanupMediaRecord(ownedMediaId);
     }
   }
 

+ 39 - 28
tests/test-media-status.ts

@@ -1,38 +1,49 @@
-import { and, desc, eq, sql } from 'drizzle-orm';
 import { getMediaStatus } from '../src/actions/media';
 import { db } from '../src/db';
 import { media } from '../src/db/schema/media';
+import { resources } from '../src/db/schema/resource';
+import { cleanupMediaRecord } from './helpers/media-cleanup';
 
 async function runTest() {
   console.log('Testing media status lookup...');
 
-  const [latestCompletedMedia] = await db
-    .select()
-    .from(media)
-    .where(and(eq(media.status, 'completed'), sql`${media.metadata}->>'hlsPath' is not null`))
-    .orderBy(desc(media.updatedAt))
-    .limit(1);
-
-  if (!latestCompletedMedia) {
-    console.error('No completed media with HLS output found. Run npm run test:media-pipeline first.');
-    process.exit(1);
-  }
-
-  const status = await getMediaStatus(latestCompletedMedia.id);
-
-  if (!status) {
-    console.error('getMediaStatus returned null.');
-    process.exit(1);
-  }
-
-  console.log('Media ID:', status.id);
-  console.log('Status:', status.status);
-  console.log('HLS Path:', status.hlsPath);
-  console.log('HLS URL generated:', Boolean(status.hlsUrl));
-
-  if (status.status !== 'completed' || !status.hlsPath || !status.hlsUrl) {
-    console.error('Media status response is missing completed playback data.');
-    process.exit(1);
+  const suffix = Date.now();
+  const [resource] = await db.insert(resources).values({
+    name: `media-status-${suffix}.mp4`,
+    path: `/media-status/${suffix}.mp4`,
+    type: 'file',
+  }).returning();
+
+  const [createdMedia] = await db.insert(media).values({
+    resourceId: resource.id,
+    filename: `media-status-${suffix}.mp4`,
+    storageKey: `uploads/media-status-${suffix}.mp4`,
+    mimeType: 'video/mp4',
+    size: BigInt(1),
+    status: 'completed',
+    metadata: {
+      hlsPath: `hls/media-status-${suffix}/index.m3u8`,
+      processedAt: new Date().toISOString(),
+    },
+  }).returning();
+
+  try {
+    const status = await getMediaStatus(createdMedia.id);
+
+    if (!status) {
+      throw new Error('getMediaStatus returned null.');
+    }
+
+    console.log('Media ID:', status.id);
+    console.log('Status:', status.status);
+    console.log('HLS Path:', status.hlsPath);
+    console.log('HLS URL generated:', Boolean(status.hlsUrl));
+
+    if (status.status !== 'completed' || !status.hlsPath || !status.hlsUrl) {
+      throw new Error('Media status response is missing completed playback data.');
+    }
+  } finally {
+    await cleanupMediaRecord(createdMedia.id);
   }
 
   console.log('Media status lookup passed.');

+ 40 - 31
tests/test-permission-admin.ts

@@ -12,44 +12,53 @@ async function runTest() {
     resourceType: 'video',
   }).returning();
 
-  const [viewerRole] = await db.select().from(roles).where(eq(roles.name, 'viewer')).limit(1);
-  const [tester] = await db.select().from(users).where(eq(users.email, 'tester@ekb.com')).limit(1);
+  let resourceId: string | null = null;
 
-  if (!viewerRole || !tester) {
-    console.error('❌ Required seed data missing. Run npx tsx src/db/seed.ts first.');
-    process.exit(1);
-  }
+  try {
+    const [viewerRole] = await db.select().from(roles).where(eq(roles.name, 'viewer')).limit(1);
+    const [tester] = await db.select().from(users).where(eq(users.email, 'tester@ekb.com')).limit(1);
 
-  await db.insert(rolePermissions).values({
-    roleId: viewerRole.id,
-    permissionId: permission.id,
-  }).onConflictDoNothing();
+    if (!viewerRole || !tester) {
+      throw new Error('Required seed data missing. Run npx tsx src/db/seed.ts first.');
+    }
 
-  const [rolePermission] = await db.select().from(rolePermissions).where(and(
-    eq(rolePermissions.roleId, viewerRole.id),
-    eq(rolePermissions.permissionId, permission.id),
-  )).limit(1);
+    await db.insert(rolePermissions).values({
+      roleId: viewerRole.id,
+      permissionId: permission.id,
+    }).onConflictDoNothing();
 
-  const [resource] = await db.insert(resources).values({
-    name: `permission-admin-${suffix}.mp4`,
-    path: `/permission-admin/${suffix}.mp4`,
-    type: 'file',
-  }).returning();
+    const [rolePermission] = await db.select().from(rolePermissions).where(and(
+      eq(rolePermissions.roleId, viewerRole.id),
+      eq(rolePermissions.permissionId, permission.id),
+    )).limit(1);
 
-  const [aclRule] = await db.insert(aclRules).values({
-    resourceId: resource.id,
-    subjectType: 'user',
-    subjectId: tester.id,
-    permissionType: 'deny',
-    action: 'read',
-  }).returning();
+    const [resource] = await db.insert(resources).values({
+      name: `permission-admin-${suffix}.mp4`,
+      path: `/permission-admin/${suffix}.mp4`,
+      type: 'file',
+    }).returning();
+    resourceId = resource.id;
+
+    const [aclRule] = await db.insert(aclRules).values({
+      resourceId: resource.id,
+      subjectType: 'user',
+      subjectId: tester.id,
+      permissionType: 'deny',
+      action: 'read',
+    }).returning();
 
-  await db.delete(aclRules).where(eq(aclRules.id, aclRule.id));
-  const [deletedAcl] = await db.select().from(aclRules).where(eq(aclRules.id, aclRule.id)).limit(1);
+    await db.delete(aclRules).where(eq(aclRules.id, aclRule.id));
+    const [deletedAcl] = await db.select().from(aclRules).where(eq(aclRules.id, aclRule.id)).limit(1);
 
-  if (!rolePermission || deletedAcl) {
-    console.error('❌ Permission admin data flow failed.');
-    process.exit(1);
+    if (!rolePermission || deletedAcl) {
+      throw new Error('Permission admin data flow failed.');
+    }
+  } finally {
+    await db.delete(rolePermissions).where(eq(rolePermissions.permissionId, permission.id));
+    await db.delete(permissions).where(eq(permissions.id, permission.id));
+    if (resourceId) {
+      await db.delete(resources).where(eq(resources.id, resourceId));
+    }
   }
 
   console.log('✅ Permission administration data flow passed.');

+ 14 - 3
tests/test-upload.ts

@@ -4,6 +4,7 @@ import path from 'path';
 import { uploadMedia } from '../src/actions/media';
 import { mediaBucketName } from '../src/lib/config';
 import minioClient from '../src/lib/minio';
+import { cleanupMediaRecord } from './helpers/media-cleanup';
 
 dotenv.config({ path: path.resolve(process.cwd(), '.env') });
 
@@ -20,17 +21,23 @@ async function runTest() {
   console.log('Starting end-to-end upload test...');
   console.log('1. Calling uploadMedia action...');
 
+  let mediaId: string | null = null;
+  let failed = false;
+
   try {
     const fileBuffer = fs.readFileSync('test_sample.mp4');
     const result = await uploadMedia({
-      name: 'test_sample.mp4',
+      name: 'e2e-upload-test_sample.mp4',
       type: 'video/mp4',
       size: fileBuffer.byteLength,
       arrayBuffer: async () => fileBuffer.buffer.slice(
         fileBuffer.byteOffset,
         fileBuffer.byteOffset + fileBuffer.byteLength
       ),
+    }, {
+      enqueue: false,
     });
+    mediaId = result.media.id;
 
     console.log('Action returned successfully.');
     console.log('Media ID:', result.media.id);
@@ -50,10 +57,14 @@ async function runTest() {
     console.error('\nTest failed.');
     console.error('Error Message:', error.message);
     if (error.code) console.error('Error Code:', error.code);
-    process.exit(1);
+    failed = true;
+  } finally {
+    if (mediaId) {
+      await cleanupMediaRecord(mediaId);
+    }
   }
 
-  process.exit(0);
+  process.exit(failed ? 1 : 0);
 }
 
 runTest();

+ 31 - 30
tests/test-user-admin.ts

@@ -13,44 +13,45 @@ async function runTest() {
     passwordHash: await hashPassword('initial-password'),
   }).returning();
 
-  const [viewerRole] = await db.select().from(roles).where(eq(roles.name, 'viewer')).limit(1);
-  const [engGroup] = await db.select().from(groups).where(eq(groups.name, 'Engineering Department')).limit(1);
+  try {
+    const [viewerRole] = await db.select().from(roles).where(eq(roles.name, 'viewer')).limit(1);
+    const [engGroup] = await db.select().from(groups).where(eq(groups.name, 'Engineering Department')).limit(1);
 
-  if (!viewerRole || !engGroup) {
-    console.error('❌ Required seed role/group missing. Run npx tsx src/db/seed.ts first.');
-    process.exit(1);
-  }
+    if (!viewerRole || !engGroup) {
+      throw new Error('Required seed role/group missing. Run npx tsx src/db/seed.ts first.');
+    }
 
-  await db.insert(userRoles).values({
-    userId: createdUser.id,
-    roleId: viewerRole.id,
-  }).onConflictDoNothing();
+    await db.insert(userRoles).values({
+      userId: createdUser.id,
+      roleId: viewerRole.id,
+    }).onConflictDoNothing();
 
-  await db.insert(userGroups).values({
-    userId: createdUser.id,
-    groupId: engGroup.id,
-  }).onConflictDoNothing();
+    await db.insert(userGroups).values({
+      userId: createdUser.id,
+      groupId: engGroup.id,
+    }).onConflictDoNothing();
 
-  const newPasswordHash = await hashPassword('reset-password');
-  await db.update(users)
-    .set({ passwordHash: newPasswordHash, updatedAt: new Date() })
-    .where(eq(users.id, createdUser.id));
+    const newPasswordHash = await hashPassword('reset-password');
+    await db.update(users)
+      .set({ passwordHash: newPasswordHash, updatedAt: new Date() })
+      .where(eq(users.id, createdUser.id));
 
-  const [updatedUser] = await db.select().from(users).where(eq(users.id, createdUser.id)).limit(1);
-  const [assignedRole] = await db.select().from(userRoles).where(eq(userRoles.userId, createdUser.id)).limit(1);
-  const [assignedGroup] = await db.select().from(userGroups).where(eq(userGroups.userId, createdUser.id)).limit(1);
+    const [updatedUser] = await db.select().from(users).where(eq(users.id, createdUser.id)).limit(1);
+    const [assignedRole] = await db.select().from(userRoles).where(eq(userRoles.userId, createdUser.id)).limit(1);
+    const [assignedGroup] = await db.select().from(userGroups).where(eq(userGroups.userId, createdUser.id)).limit(1);
 
-  if (!updatedUser || !assignedRole || !assignedGroup) {
-    console.error('❌ User admin write flow did not persist expected rows.');
-    process.exit(1);
-  }
+    if (!updatedUser || !assignedRole || !assignedGroup) {
+      throw new Error('User admin write flow did not persist expected rows.');
+    }
 
-  const resetPasswordWorks = await verifyPassword('reset-password', updatedUser.passwordHash);
-  const oldPasswordWorks = await verifyPassword('initial-password', updatedUser.passwordHash);
+    const resetPasswordWorks = await verifyPassword('reset-password', updatedUser.passwordHash);
+    const oldPasswordWorks = await verifyPassword('initial-password', updatedUser.passwordHash);
 
-  if (!resetPasswordWorks || oldPasswordWorks) {
-    console.error('❌ Password reset verification failed.');
-    process.exit(1);
+    if (!resetPasswordWorks || oldPasswordWorks) {
+      throw new Error('Password reset verification failed.');
+    }
+  } finally {
+    await db.delete(users).where(eq(users.id, createdUser.id));
   }
 
   console.log('✅ User administration data flow passed.');