Skip to content

Conversation

FunamaYukina
Copy link
Member

@FunamaYukina FunamaYukina commented Sep 26, 2025

Summary

Enhance the Drizzle PostgreSQL parser to correctly handle complex method chaining patterns like .enableRLS().$comment(). This fixes GitHub folder URL parsing for Drizzle schemas that use multiple method chaining patterns.

Issues Fixed

  • Complex method chaining patterns like .enableRLS().$comment() were not being parsed correctly
  • GitHub folder URL feature was only showing some tables instead of all tables from Drizzle schemas
  • Tables using complex method chaining were being ignored during parsing

Changes Made

Core Parser Enhancements

  • Added extractPgTableFromChain() function: Recursively traverses method chains to find the base pgTable call, handling any depth of method chaining
  • Enhanced parsePgTableWithComment(): Modified to use extractPgTableFromChain() instead of expecting direct pgTable calls
  • Refactored mainParser: Better separation of concerns with dedicated handler functions for different call types
  • Made isPgTableCall private: No longer exported as it's only used internally

Test Coverage

  • Added comprehensive tests for .enableRLS() method chaining: Ensures simple method chaining works correctly
  • Added tests for .$comment() method chaining: Tests both simple and complex patterns
  • Added test for complex chaining: Specifically tests the .enableRLS().$comment() pattern that was failing

Technical Details

AST Structure Analysis

The complex chaining pattern .enableRLS().$comment() creates this AST structure:

  1. Root call: $comment(...)
  2. Its callee object: enableRLS() call
  3. enableRLS's callee object: pgTable(...) call (the base)

Solution Implementation

The fix involves recursively traversing the method chain to find the underlying pgTable call, regardless of how many intermediate method calls are chained.

Test Plan

  • All existing tests continue to pass
  • New tests for .enableRLS() method chaining pass
  • New tests for .$comment() method chaining pass
  • Complex chaining test (.enableRLS().$comment()) passes
  • Linting and type checking pass
  • Manual verification that GitHub folder URL parsing works with complex Drizzle schemas

Impact

This change ensures that ERD visualizations from GitHub folder URLs correctly display all tables from Drizzle schemas, regardless of the method chaining patterns used. Users can now use complex patterns like .enableRLS().$comment() without losing tables in their ERD diagrams.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Import ERD schemas directly from GitHub folder URLs (recursive).
    • Automatic schema format detection with sensible precedence.
    • Unified error view with clearer, contextual messages.
  • Improvements

    • More robust Drizzle Postgres parsing, including chained methods (e.g., RLS, comments), schema tables, and enums.
    • Consistent URL handling and content normalization across sources.
  • Refactor

    • Modularized ERD content fetching, format detection, and error rendering for a cleaner flow.
  • Tests

    • Comprehensive tests for GitHub URL handling.
    • Added tests covering Drizzle Postgres chained parsing scenarios.

Enhance the Drizzle PostgreSQL parser to correctly handle complex method
chaining patterns like `.enableRLS().$comment()`. Previously, only simple
chaining patterns were supported.

Key changes:
- Add extractPgTableFromChain() function to recursively traverse method chains
- Enhance parsePgTableWithComment() to handle nested method calls
- Refactor mainParser for better separation of concerns
- Add comprehensive tests for .enableRLS() and .$comment() method chaining
- Add test for complex chaining combining multiple methods
- Make isPgTableCall private as it's only used internally

This fixes GitHub folder URL parsing for Drizzle schemas that use
multiple method chaining patterns, ensuring all tables are correctly
parsed and displayed in ERD visualizations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

changeset-bot bot commented Sep 26, 2025

⚠️ No Changeset found

Latest commit: 5fb89ca

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

Refactors ERD page to delegate URL-based content fetching to new GitHub folder handler and a single-file path, consolidates error handling and format detection, and updates parsing flow. Adds a new GitHub URL handler module with tests. Enhances Drizzle Postgres parser to support chained pgTable patterns and comment/schema/enums handling with new AST utilities and tests.

Changes

Cohort / File(s) Summary of Changes
ERD URL handling and page flow
frontend/apps/app/app/erd/p/[...slug]/page.tsx, frontend/apps/app/app/erd/p/[...slug]/utils/githubUrlHandler.ts, frontend/apps/app/app/erd/p/[...slug]/utils/githubUrlHandler.test.ts
Page now uses helpers to fetch/normalize input from GitHub folders or single files; centralized error rendering; added determineSchemaFormat precedence. Introduced githubUrlHandler to parse GitHub folder URLs, recursively collect schema files, detect format, and return aggregated content; comprehensive unit tests added for parsing, recursion, filtering, and error cases.
Drizzle Postgres parser core
frontend/packages/schema/src/parser/drizzle/postgres/astUtils.ts, frontend/packages/schema/src/parser/drizzle/postgres/mainParser.ts, frontend/packages/schema/src/parser/drizzle/postgres/tableParser.ts
Added extractPgTableFromChain to unwrap chained calls; made isPgTableCall internal. Refactored main parser to dispatch for comment calls, schema-qualified tables, enums, and chained pgTable; tracks tables/enums and variable-to-table mapping. Updated table parser to support chained pgTable with comment via extractPgTableFromChain.
Drizzle Postgres parser tests
frontend/packages/schema/src/parser/drizzle/postgres/__tests__/index.test.ts
Added tests for enableRLS() chaining and .$comment() on pgTable ensuring table discovery, comments, and column types.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant P as ERD Page (page.tsx)
  participant H as URL Handlers
  participant F as Format Detector
  participant S as Schema Parser
  participant V as ERDViewer

  U->>P: Request /erd/p/[...slug]
  P->>H: Resolve content (GitHub folder or single file)
  alt Content fetched
    H-->>P: { input, detectedFormatFromFiles }
    P->>F: determineSchemaFormat(params, detectedFormatFromFiles, content)
    F-->>P: format
    P->>S: parse(input, format)
    S-->>P: schema
    P->>V: Render with schema/layout
  else Error or missing input
    H-->>P: { error }
    P->>V: renderErrorView(error or NetworkError)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Suggested reviewers

  • hoshinotsuyoshi
  • junkisai
  • NoritakaIkeda
  • MH4GF

Poem

A nibble of URLs, a hop through a tree,
I gather your schemas, as tidy as can be.
Drizzle chains untangled, enums in a row—
With whiskers aflutter, I render the flow.
Bounce, parse, display—let the ERD show! 🐰✨

Pre-merge checks and finishing touches and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description does not follow the repository’s required template because it lacks a “## Issue” section with a resolve field and a “## Why is this change needed?” section. Please restructure the description to include a “## Issue” section with a resolve entry referencing the related issue and a “## Why is this change needed?” section with a brief justification as specified by the template.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely and accurately describes the main change—adding support for complex Drizzle method chaining patterns—so it clearly reflects the PR’s focus on enhancing the schema parser’s handling of chains like .enableRLS().$comment().
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/github-drizzle-method-chaining-support
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/github-drizzle-method-chaining-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

vercel bot commented Sep 26, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
liam-app Ready Ready Preview Comment Sep 26, 2025 11:27am
liam-assets Ready Ready Preview Comment Sep 26, 2025 11:27am
liam-erd-sample Ready Ready Preview Comment Sep 26, 2025 11:27am
liam-storybook Ready Ready Preview Comment Sep 26, 2025 11:27am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
liam-docs Ignored Ignored Sep 26, 2025 11:27am

Copy link

supabase bot commented Sep 26, 2025

Updates to Preview Branch (feature/github-drizzle-method-chaining-support) ↗︎

Deployments Status Updated
Database Fri, 26 Sep 2025 11:23:50 UTC
Services Fri, 26 Sep 2025 11:23:50 UTC
APIs Fri, 26 Sep 2025 11:23:50 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Fri, 26 Sep 2025 11:24:08 UTC
Migrations Fri, 26 Sep 2025 11:24:14 UTC
Seeding Fri, 26 Sep 2025 11:24:14 UTC
Edge Functions Fri, 26 Sep 2025 11:24:14 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

Copy link
Contributor

🤖 Agent Deep Modeling Execution

Processing time: 7m 49s
Started at: 2025-09-26 11:24:14 UTC

Command Output

@liam-hq/agent@0.1.0 execute-deep-modeling /home/runner/work/liam/liam/frontend/internal-packages/agent
pnpm test:integration src/createGraph.integration.test.ts

@liam-hq/agent@0.1.0 test:integration /home/runner/work/liam/liam/frontend/internal-packages/agent
vitest --watch=false --passWithNoTests --config vitest.config.integration.ts src/createGraph.integration.test.ts

RUN v3.2.4 /home/runner/work/liam/liam/frontend/internal-packages/agent

(node:7523) ExperimentalWarning: WASI is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)

✅ [INFO] 2025-09-26T11:24:17.472Z
LangSmith Trace URL: https://smith.langchain.com/o/eed4d2d8-0bd8-4ca4-a452-4da88ef63fd6/projects/p/99d63e81-58f9-475a-8985-409b591e26b0?searchModel=%7B%22filter%22%3A%22and(eq(is_root%2C%20true)%2C%20and(eq(metadata_key%2C%20%5C%22thread_id%5C%22)%2C%20eq(metadata_value%2C%20%5C%223be183fb-abb2-42e7-8bc2-27a4768e56b8%5C%22)))%22%7D

⏺ (pm):

Purpose: Save finalized BRD for the simple user management system; Inputs: 1–2 sentence businessRequirement + categorized functionalRequirements object.

🔧 saveRequirementsToArtifactTool(businessRequirement: "Deliver a simple ...", functionalRequirements: {11 keys}) ⎿ unknown: Requirements saved successfully to artifact

🔧 schemaDesignTool(operations: [11 items])

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

⏺ (qa):

{
"testcaseWithDml": {
"requirementType": "functional",
"requirementCategory": "User Lifecycle Management",
"requirement": "Admins shall be able to create a user directly and require a password change on first login.",
"title": "Admin-created user directly with mandatory password change on first login",
"description": "This test case validates that an admin can create a new user record directly in the users table with require_password_change set to true, ensuring the password_hash remains NULL (indicating the user must set a password on first login). It also covers an edge-case by creating a second user with a richer set of optional fields to exercise additional nullable columns and JSONB profile data. The test concludes with a validation query to confirm the business rule is enforced and fields are populated as expected.",
"dmlOperation": {
"operation_type": "INSERT",
"sql": "\n-- Begin a test transaction (optional for clarity; remove if you prefer autocommit)\nBEGIN;\n\n-- 1) Admin creates a minimal user where a password must be set on first login\nINSERT INTO users (\n id,\n role,\n email,\n email_normalized,\n status,\n timezone,\n anonymized,\n created_at,\n updated_at,\n mfa_enabled,\n failed_login_attempts,\n require_password_change\n)\nSELECT\n gen_random_uuid(),\n (SELECT enumlabel FROM pg_type t JOIN pg_enum e ON e.enumtypid = t.oid WHERE t.typname = 'role_enum' LIMIT 1)::role_enum,\n 'newuser1@example.com',\n lower('newuser1@example.com'),\n (SELECT enumlabel FROM pg_type t JOIN pg_enum e ON e.enumtypid = t.oid WHERE t.typname = 'status_enum' LIMIT 1)::status_enum,\n 'UTC',\n false,\n now(),\n now(),\n false,\n 0,\n true;\n\n-- 2) Edge-case: admin creates a richer user with additional optional fields to exercise nullable columns and JSONB data\nINSERT INTO users (\n id,\n role,\n email,\n email_normalized,\n status,\n timezone,\n anonymized,\n created_at,\n updated_at,\n mfa_enabled,\n failed_login_attempts,\n require_password_change,\n first_name,\n last_name,\n phone,\n avatar_url,\n profile_json\n)\nSELECT\n gen_random_uuid(),\n (SELECT enumlabel FROM pg_type t JOIN pg_enum e ON e.enumtypid = t.oid WHERE t.typname = 'role_enum' LIMIT 1)::role_enum,\n 'edgecase@example.com',\n lower('edgecase@example.com'),\n (SELECT enumlabel FROM pg_type t JOIN pg_enum e ON e.enumtypid = t.oid WHERE t.typname = 'status_enum' LIMIT 1)::status_enum,\n 'UTC',\n false,\n now(),\n now(),\n true,\n 0,\n true,\n 'Edge'::text,\n 'Case'::text,\n '+1234567890'::text,\n 'https://example.com/avatar.png'::text,\n '{"preferences": {"theme": "dark"}}'::jsonb;\n\n-- 3) Validation: confirm inserted rows have expected business rules (password_hash remains NULL, require_password_change is true)\nSELECT\n id,\n email,\n email_normalized,\n require_password_change,\n password_hash,\n created_at,\n updated_at\nFROM users\nWHERE email IN ('newuser1@example.com', 'edgecase@example.com')\nORDER BY created_at ASC;\n\nCOMMIT;\n",
"description": "Inserts two test users: one minimal record requiring password change on first login; and a second richer record to test additional nullable fields and JSON data. It ends with a validation query to ensure require_password_change is true and password_hash remains NULL for both records."
}
}
}

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

⏺ (qa):

{
"testcaseWithDml": {
"requirementType": "functional",
"requirementCategory": "Authentication",
"requirement": "Users may optionally enable TOTP-based multi-factor authentication (MFA) with 10 one-time recovery codes; when enabled, MFA shall be required at login.",
"title": "MFA with 10 recovery codes: enablement, verification, and login enforcement",
"description": "This test scenario covers:\n- Creating a user with MFA enabled and generating exactly 10 one-time recovery codes (stored as hashed values).\n- Creating a second user with MFA initially disabled, then enabling MFA and generating a second set of 10 codes.\n- Simulating a one-code usage by marking a recovery code as used, and verifying the remaining unused count.\n- Soft-deleting the first user to validate retention metadata on lifecycle actions.\nAll IDs are generated via gen_random_uuid() and recovery codes are stored as md5 hashes to reflect hashed storage requirements.",
"dmlOperation": {
"operation_type": "INSERT",
"sql": "-- 1) Create Alice with MFA enabled and generate 10 recovery codes (hashed)\nWITH u1 AS (\n INSERT INTO users (\n id, role, email, email_normalized, timezone, status, created_at, updated_at,\n mfa_enabled, mfa_secret_enc, anonymized, failed_login_attempts, require_password_change\n ) VALUES (\n gen_random_uuid(),\n 'user'::role_enum,\n 'alice.mfa@example.com',\n 'alice.mfa@example.com',\n 'UTC',\n 'active'::status_enum,\n now(), now(),\n true,\n 'ENC_SECRET_ALICE',\n false,\n 0,\n false\n )\n RETURNING id\n)\nINSERT INTO mfa_recovery_codes (id, user_id, code_hash, created_at)\nSELECT gen_random_uuid(), u1.id, md5('CODE-' || to_char(n, 'FM0000')), now()\nFROM u1 CROSS JOIN generate_series(1, 10) AS n;\n\n-- 2) Create Bob with MFA disabled, then enable MFA and generate 10 recovery codes (hashed)\nWITH u2 AS (\n INSERT INTO users (\n id, role, email, email_normalized, timezone, status, created_at, updated_at,\n mfa_enabled, mfa_secret_enc, anonymized, failed_login_attempts, require_password_change\n ) VALUES (\n gen_random_uuid(),\n 'user'::role_enum,\n 'bob.mfa-off@example.com',\n 'bob.mfa-off@example.com',\n 'UTC',\n 'active'::status_enum,\n now(), now(),\n false,\n NULL,\n false,\n 0,\n false\n )\n RETURNING id\n),\nu2_enabled AS (\n UPDATE users\n SET mfa_enabled = true, mfa_secret_enc = 'ENC_SECRET_BOB', updated_at = now()\n WHERE id = (SELECT id FROM u2)\n RETURNING id\n)\nINSERT INTO mfa_recovery_codes (id, user_id, code_hash, created_at)\nSELECT gen_random_uuid(), u2_enabled.id, md5('CODE-' || to_char(n, 'FM0000')), now()\nFROM u2_enabled CROSS JOIN generate_series(1, 10) AS n;\n\n-- 3) Simulate usage of the first Alice recovery code (mark as used)\nUPDATE mfa_recovery_codes\nSET used_at = now()\nWHERE id = (\n SELECT code_hash FROM mfa_recovery_codes\n WHERE user_id = (SELECT id FROM users WHERE email_normalized = 'alice.mfa@example.com')\n ORDER BY created_at\n LIMIT 1\n)\nRETURNING ;\n\n-- 4) Verify remaining unused codes for Alice (should be 9 if all codes were unique)\nSELECT COUNT() AS remaining_unused_codes\nFROM mfa_recovery_codes\nWHERE user_id = (SELECT id FROM users WHERE email_normalized = 'alice.mfa@example.com')\n AND used_at IS NULL;\n\n-- 5) Soft-delete Alice (set deleted_at)\nUPDATE users\nSET deleted_at = now()\nWHERE email_normalized = 'alice.mfa@example.com'\nRETURNING id;\n",
"description": "This script creates two users to cover MFA enabled and MFA initially disabled paths, generates 10 recovery codes for each (stored as hashed values), simulates code usage, validates remaining available codes, and performs a soft-delete to validate lifecycle handling."
}
}
}

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

⏺ (qa):

{
"testcaseWithDml": {
"requirementType": "functional",
"requirementCategory": "Accessibility & Localization",
"requirement": "All UI text and transactional emails shall be available in English; dates/times shall be displayed in the signed-in user’s timezone.",
"title": "Localization and Timezone Rendering Validation for Admin Console",
"description": "This test validates localization and timezone behavior aligned with the requirement. It performs:\n- Insertion of two users in distinct timezones to exercise timezone-aware display logic.\n- Verification that email normalization lowercases emails (a prerequisite for consistent language/template routing).\n- Creation of audit_logs entries to signal English-language UI and email templates are used for actions.\n- Validation queries to ensure times are displayed in each user’s timezone and that English locale signals are present in audit logs.\nThe test uses gen_random_uuid() for all UUID columns and adheres to NOT NULL constraints where applicable.",
"dmlOperation": {
"operation_type": "INSERT",
"sql": "-- Insert two users with distinct timezones; ensure required NOT NULL fields are populated\nINSERT INTO users (\n id, role, email, email_normalized, status, timezone, last_name, anonymized, mfa_enabled,\n created_at, updated_at, display_name, first_name, profile_json, last_login_at,\n failed_login_attempts, require_password_change, email_verified_at, last_verification_sent_at,\n verification_resend_count_24h\n)\nVALUES\n (\n gen_random_uuid(),\n 'admin',\n 'ADMIN.Jones@example.org',\n LOWER('ADMIN.Jones@example.org'),\n 'active',\n 'Europe/London',\n 'Jones',\n FALSE,\n TRUE,\n now(),\n now(),\n 'Admin',\n 'Admin',\n NULL,\n '2025-06-01 12:00:00+00',\n 0,\n FALSE,\n NULL,\n NULL,\n 0\n ),\n (\n gen_random_uuid(),\n 'user',\n 'Alice.Smith@example.com',\n LOWER('Alice.Smith@example.com'),\n 'active',\n 'America/New_York',\n 'Smith',\n FALSE,\n TRUE,\n now(),\n now(),\n 'Alice',\n 'Alice',\n NULL,\n '2025-07-01 11:00:00+00',\n 0,\n FALSE,\n NULL,\n NULL,\n 0\n );\n\n-- Create audit log entries signaling English locale usage for UI and emails\nINSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)\nVALUES\n (\n 'ui.render',\n gen_random_uuid(),\n '{"locale":"en_US","view":"admin_dashboard"}'::jsonb,\n now(),\n '203.0.113.1',\n 'TestAgent/1.0',\n NULL,\n (SELECT id FROM users WHERE email = 'Alice.Smith@example.com' LIMIT 1)\n ),\n (\n 'email.sent',\n gen_random_uuid(),\n '{"template":"welcome_email","locale":"en_US"}'::jsonb,\n now(),\n '203.0.113.1',\n 'Mailer/2.0',\n NULL,\n (SELECT id FROM users WHERE email = 'Alice.Smith@example.com' LIMIT 1)\n ),\n (\n 'email.sent',\n gen_random_uuid(),\n '{"template":"invite_email","locale":"en_US"}'::jsonb,\n now(),\n '203.0.113.1',\n 'Mailer/2.0',\n NULL,\n (SELECT id FROM users WHERE email = 'ADMIN.Jones@example.org' LIMIT 1)\n );\n\n-- Validation queries (timezone display and locale signals)\nSELECT\n u.id AS user_id,\n u.email,\n u.timezone,\n u.created_at,\n (u.created_at AT TIME ZONE u.timezone) AS created_in_user_tz,\n u.last_login_at,\n (u.last_login_at AT TIME ZONE u.timezone) AS last_login_in_user_tz\nFROM users u\nWHERE u.email IN ('Alice.Smith@example.com','ADMIN.Jones@example.org')\nORDER BY u.email;\n\nSELECT a.action, a.metadata->>'locale' AS locale, a.metadata->>'template' AS template\nFROM audit_logs a\nWHERE a.action IN ('ui.render','email.sent')\nORDER BY a.created_at DESC\nLIMIT 6;",
"description": "Inserts two users with distinct timezones, creates English-language UI/email audit logs, and provides validation queries to confirm timezone-specific display and locale signaling."
}
}
}

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

⏺ (qa):

{
"testcaseWithDml": {
"requirementType": "functional",
"requirementCategory": "Authorization & Roles",
"requirement": "Only Admins shall be allowed to assign or change a user’s role.",
"title": "Admin-only role assignment enforcement: positive admin path and negative non-admin and invalid role paths",
"description": "This test validates that role assignments can only be performed by admins. It inserts three users (one admin and two non-admins), defines a helper function change_user_role(actor_id, target_user_id, new_role) that enforces the admin-only policy, then executes: (1) a positive path where the admin promotes a non-admin to admin, (2) a negative path where a non-admin attempts to change another user's role, and (3) a negative path where a valid admin attempts to set an invalid role value, ensuring proper error handling. All IDs are generated with gen_random_uuid() and email_normalized fields are used for lookups.",
"dmlOperation": {
"operation_type": "INSERT",
"sql": "-- 1) Create test users (admin and two non-admins)\nINSERT INTO users (\n id,\n role,\n email,\n email_normalized,\n status,\n timezone,\n anonymized,\n created_at,\n updated_at,\n mfa_enabled,\n failed_login_attempts,\n require_password_change\n) VALUES (\n gen_random_uuid(),\n 'admin',\n 'AdminOne@example.com',\n 'adminone@example.com',\n 'active',\n 'UTC',\n false,\n now(),\n now(),\n true,\n 0,\n false\n);\n\nINSERT INTO users (\n id,\n role,\n email,\n email_normalized,\n status,\n timezone,\n anonymized,\n created_at,\n updated_at,\n mfa_enabled,\n failed_login_attempts,\n require_password_change\n) VALUES (\n gen_random_uuid(),\n 'user',\n 'UserOne@example.com',\n 'userone@example.com',\n 'active',\n 'UTC',\n false,\n now(),\n now(),\n true,\n 0,\n false\n);\n\nINSERT INTO users (\n id,\n role,\n email,\n email_normalized,\n status,\n timezone,\n anonymized,\n created_at,\n updated_at,\n mfa_enabled,\n failed_login_attempts,\n require_password_change\n) VALUES (\n gen_random_uuid(),\n 'user',\n 'UserTwo@example.com',\n 'usertwo@example.com',\n 'active',\n 'UTC',\n false,\n now(),\n now(),\n true,\n 0,\n false\n);\n\n-- 2) Create a helper function that enforces admin-only role changes\nCREATE OR REPLACE FUNCTION public.change_user_role(actor_id uuid, target_user_id uuid, new_role text)\nRETURNS void AS $$\nBEGIN\n -- Only admins may change roles\n IF (SELECT role FROM users WHERE id = actor_id) <> 'admin' THEN\n RAISE EXCEPTION 'permission denied: only admins can change roles';\n END IF;\n -- Cast enforces valid enum values; invalid values will raise an error\n UPDATE users SET role = new_role::role_enum, updated_at = now() WHERE id = target_user_id;\nEND;\n$$ LANGUAGE plpgsql;\n\n-- 3) Perform positive and negative test scenarios\nDO $$\nDECLARE\n admin_id UUID;\n regular1_id UUID;\n regular2_id UUID;\nBEGIN\n -- Identify test users by normalized emails\n SELECT id INTO admin_id FROM users WHERE email_normalized = 'adminone@example.com';\n SELECT id INTO regular1_id FROM users WHERE email_normalized = 'userone@example.com';\n SELECT id INTO regular2_id FROM users WHERE email_normalized = 'usertwo@example.com';\n\n -- Positive: admin changes a non-admin to admin\n PERFORM public.change_user_role(admin_id, regular1_id, 'admin');\n IF (SELECT role FROM users WHERE id = regular1_id) <> 'admin' THEN\n RAISE EXCEPTION 'Positive test failed: role was not updated to admin';\n END IF;\n\n -- Negative: non-admin attempts to change someone else''s role\n BEGIN\n PERFORM public.change_user_role(regular2_id, admin_id, 'user');\n RAISE EXCEPTION 'Negative test failed: non-admin was able to change a role';\n EXCEPTION WHEN OTHERS THEN\n -- Expected: operation should fail\n END;\n\n -- Negative: admin attempts to set an invalid role value\n BEGIN\n PERFORM public.change_user_role(admin_id, regular1_id, 'superadmin');\n RAISE EXCEPTION 'Invalid-role test failed: invalid role value accepted';\n EXCEPTION WHEN OTHERS THEN\n -- Expected: invalid enum value rejected\n END;\nEND\n$$;",
"description": "Test setup of three users followed by a PL/pgSQL function enforcing admin-only role changes. Then a positive path where an admin changes another user to admin, and two negative paths: a non-admin attempting to change a role, and an admin attempting to assign an invalid role value. All IDs are generated with gen_random_uuid()."
}
}
}

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})

⏺ (qa):

{"testcaseWithDml":{"requirementType":"functional","requirementCategory":"Integration & API Access","requirement":"Programmatic responses shall include pagination metadata (total, page, and page_size) and shall enforce Admin-only access.","title":"Admin-only paginated user listing with total metadata; admin role gate enforced","description":"This test seeds a set of users (including admins, regular users, and a soft-deleted user) using UUIDs generated by gen_random_uuid(). It then exercises admin-only pagination with total metadata across two pages and validates that a non-admin context yields zero admin-visible rows while still returning the pagination metadata. Soft-deleted records are excluded from results. The test uses distinct created_at timestamps to ensure deterministic ordering for pagination.","dmlOperation":{"operation_type":"INSERT","sql":"-- Seed data: admins, regular users and a soft-deleted user (respect all not-null constraints)\nINSERT INTO users (\n id, role, email, phone, status, timezone, last_name, anonymized, avatar_url, created_at, deleted_at, deleted_by, first_name, note_admin, updated_at, mfa_enabled, display_name, profile_json, last_login_at, lockout_until, password_hash, mfa_secret_enc, email_normalized, email_verified_at, failed_login_attempts, require_password_change, last_verification_sent_at, verification_resend_count_24h\n) VALUES\n (gen_random_uuid(), 'admin'::role_enum, 'admin1@example.com', NULL, 'active'::status_enum, 'UTC', 'AdminLast1', false, NULL, now() - INTERVAL '60 seconds', NULL, NULL, 'Admin', NULL, now() - INTERVAL '60 seconds', true, 'Admin One', NULL, NULL, NULL, NULL, NULL, 'admin1@example.com', NULL, 0, false, NULL, 0),\n (gen_random_uuid(), 'admin'::role_enum, 'admin2@example.com', NULL, 'active'::status_enum, 'UTC', 'AdminLast2', false, NULL, now() - INTERVAL '50 seconds', NULL, NULL, 'Admin', NULL, now() - INTERVAL '50 seconds', true, 'Admin Two', NULL, NULL, NULL, NULL, NULL, 'admin2@example.com', NULL, 0, false, NULL, 0),\n (gen_random_uuid(), 'user'::role_enum, 'user1@example.com', NULL, 'active'::status_enum, 'UTC', 'UserLast1', false, NULL, now() - INTERVAL '40 seconds', NULL, NULL, 'User', NULL, now() - INTERVAL '40 seconds', false, 'User One', NULL, NULL, NULL, NULL, NULL, 'user1@example.com', NULL, 0, false, NULL, 0),\n (gen_random_uuid(), 'user'::role_enum, 'user2@example.com', NULL, 'active'::status_enum, 'UTC', 'UserLast2', false, NULL, now() - INTERVAL '30 seconds', NULL, NULL, 'User', NULL, now() - INTERVAL '30 seconds', false, 'User Two', NULL, NULL, NULL, NULL, NULL, 'user2@example.com', NULL, 0, false, NULL, 0),\n (gen_random_uuid(), 'user'::role_enum, 'user_deleted@example.com', NULL, 'active'::status_enum, 'UTC', 'DeletedUser', false, NULL, now() - INTERVAL '20 seconds', now() - INTERVAL '1 day', NULL, 'Deleted', NULL, now() - INTERVAL '20 seconds', false, 'Deleted User', NULL, NULL, NULL, NULL, NULL, 'user_deleted@example.com', NULL, 0, false, NULL, 0);\n\n-- 2) Admin context: page 1 (page=1, page_size=2) with pagination metadata\nWITH current_role AS (SELECT 'admin' AS role)\nSELECT 1 AS page, 2 AS page_size, total.total AS total, u.id, u.email, u.display_name, u.created_at\nFROM (\n SELECT count() AS total\n FROM users\n WHERE deleted_at IS NULL AND (SELECT role FROM current_role) = 'admin' AND role = 'admin'\n) AS total\nLEFT JOIN (\n SELECT id, email, display_name, created_at\n FROM users\n WHERE deleted_at IS NULL AND (SELECT role FROM current_role) = 'admin' AND role = 'admin'\n ORDER BY created_at\n LIMIT 2\n OFFSET 0\n) AS u ON TRUE;\n\n-- 3) Admin context: page 2 (page=2, page_size=2)\nWITH current_role AS (SELECT 'admin' AS role)\nSELECT 2 AS page, 2 AS page_size, total.total AS total, u.id, u.email, u.display_name, u.created_at\nFROM (\n SELECT count() AS total\n FROM users\n WHERE deleted_at IS NULL AND (SELECT role FROM current_role) = 'admin' AND role = 'admin'\n) AS total\nLEFT JOIN (\n SELECT id, email, display_name, created_at\n FROM users\n WHERE deleted_at IS NULL AND (SELECT role FROM current_role) = 'admin' AND role = 'admin'\n ORDER BY created_at\n LIMIT 2\n OFFSET 2\n) AS u ON TRUE;\n\n-- 4) Non-admin context: page 1 should yield total reflecting 0 admin-visible rows; data rows may be NULL for user fields\nWITH current_role AS (SELECT 'user' AS role)\nSELECT 1 AS page, 2 AS page_size, total.total AS total, u.id, u.email, u.display_name, u.created_at\nFROM (\n SELECT count(*) AS total\n FROM users\n WHERE deleted_at IS NULL AND (SELECT role FROM current_role) = 'admin' AND role = 'admin'\n) AS total\nLEFT JOIN (\n SELECT id, email, display_name, created_at\n FROM users\n WHERE deleted_at IS NULL AND (SELECT role FROM current_role) = 'admin' AND role = 'admin'\n ORDER BY created_at\n LIMIT 2\n OFFSET 0\n) AS u ON TRUE;","description":"Seed data includes two admin users and several regular users, plus a soft-deleted user. The admin context queries return pagination metadata (total, page, page_size) and up to 2 user rows for page 1 and page 2. The non-admin context demonstrates access enforcement by returning total=0 with no admin-visible rows, while still returning metadata."}}}

🔧 saveTestcase(testcaseWithDml: {6 keys})

🔧 saveTestcase(testcaseWithDml: {6 keys})stderr | src/createGraph.integration.test.ts > createGraph Integration > should execute complete workflow
fetch failed

Context: trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=f8e2f2d8-1664-43ff-9693-b8a089e96b1c; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=23428490-dea0-4b14-a2f4-512e4c15c578; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=0ab954fd-85cb-4cae-8825-7220c99a512d; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=31345c54-418a-4156-898f-dc427e5d2b7c; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=d05444c0-3917-4760-8a1c-f9f45ba2a448; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=62b0fb1b-987c-4e67-9234-153e9a738791; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=66e49701-d9ee-4eff-b41b-a4566c2dd7bd; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=44944a7e-2613-4daa-8a5c-e9e8aac04004; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=5fed826a-c2be-4083-bd14-23fd6d4c08f0; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=97e745e9-8b4d-4d17-b80c-c5139019766b; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=5ff369f1-bba5-4379-87ce-4cc180f3bf7e; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=d70824e1-f3b1-4dd0-aad0-ecb9306b72e6; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=283a7d48-fbe0-4ece-a1d7-dcd8f7cd926f; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=da81b46e-a86f-48ee-9e64-9b3beb96d3b8; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=54699032-299c-4eb8-94b1-55f1cf6de7f2; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=466275e5-f7cd-4e57-a530-533862d22440; trace=8f142a58-50ec-4089-8403-be670a2d7a80,id=ccda46db-d48e-4c47-beb2-45cf32eb0372

⎿ unknown: 1/31 test cases passed, 30 failed

Test Case: Email Uniqueness and Case-Insensitive Normalization on Save for Users

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: 032636f8-062d-4c63-91af-50ec5b750374
-- Email Uniqueness and Case-Insensitive Normalization on Save for Users
-- What this DML operation tests
INSERT INTO users (id, role, email, phone, status, timezone, last_name, anonymized, avatar_url, created_at, deleted_at, deleted_by, first_name, note_admin, updated_at, mfa_enabled, display_name, profile_json, last_login_at, lockout_until, password_hash, mfa_secret_enc, email_normalized, email_verified_at, failed_login_attempts, require_password_change, last_verification_sent_at, verification_resend_count_24h) VALUES (gen_random_uuid(), 'user'::role_enum, 'Alice@example.com', NULL, 'active'::status_enum, 'UTC', 'Anderson', false, NULL, now(), NULL, NULL, 'Alice', NULL, now(), false, 'Alice A.', NULL, NULL, NULL, NULL, NULL, 'alice@example.com', NULL, 0, false, NULL, 0)

Test Case: Users can view and edit their own profile fields

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: 3d1fd521-441c-45dc-9f4b-41456349da8f
-- Users can view and edit their own profile fields
-- Tests that a user can view and update their own profile fields: first_name, last_name, display_name, phone, timezone, avatar_url. Uses a single user identified by normalized email.
-- 1) Insert a new user with profile fields that can be edited
INSERT INTO users (
  id,
  role,
  email,
  phone,
  status,
  timezone,
  last_name,
  anonymized,
  avatar_url,
  created_at,
  deleted_at,
  deleted_by,
  first_name,
  note_admin,
  updated_at,
  mfa_enabled,
  display_name,
  profile_json,
  last_login_at,
  lockout_until,
  password_hash,
  mfa_secret_enc,
  email_normalized,
  email_verified_at,
  failed_login_attempts,
  require_password_change,
  last_verification_sent_at,
  verification_resend_count_24h
) VALUES (
  gen_random_uuid(),
  'user'::role_enum,
  'Test.User@example.com',
  NULL,
  'active'::status_enum,
  'UTC',
  'Doe',
  FALSE,
  NULL,
  now(),
  NULL,
  NULL,
  'John',
  NULL,
  now(),
  TRUE,
  'John D',
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,
  lower('Test.User@example.com'),
  NULL,
  0,
  FALSE,
  NULL,
  0
)

Test Case: Email Change Requires Re-Verification via Confirmation Link

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: ab5243bc-3b18-4baf-8149-c5091b3bfe40
-- Email Change Requires Re-Verification via Confirmation Link
-- What this DML operation tests
DO
$$
DECLARE
  user_id uuid;
  token_id uuid;
  new_email text := 'alice.new@example.com';
  expired_token_id uuid;
  second_user_id uuid;
  row_count int;
BEGIN
  -- 1) Create initial user
  INSERT INTO users (id, role, email, email_normalized, status, timezone, anonymized, created_at, updated_at, mfa_enabled, failed_login_attempts, require_password_change, first_name, last_name, password_hash)
  VALUES (gen_random_uuid(), 'user', 'Alice@example.com', lower('Alice@example.com'), 'active', 'UTC', false, now(), now(), false, 0, false, 'Alice', 'Doe', 'hash');
  SELECT id INTO user_id FROM users WHERE email = 'Alice@example.com';
  
  -- 2) Create an email change token for the new email
  INSERT INTO email_verification_tokens (id, token, user_id, created_at, expires_at, new_email, new_email_normalized)
  VALUES (gen_random_uuid(), gen_random_uuid()::text, user_id, now(), now() + interval '1 day', new_email, lower(new_email))
  RETURNING id INTO token_id;
  
  -- 3) Validate that the current email remains unchanged before applying token
  IF (SELECT email FROM users WHERE id = user_id) <> 'Alice@example.com' THEN
    RAISE EXCEPTION 'User email changed before token usage';
  END IF;
  
  -- 4) Use the token to perform the email change
  UPDATE email_verification_tokens SET used_at = now() WHERE id = token_id;
  UPDATE users
    SET email = new_email,
        email_normalized = lower(new_email),
        updated_at = now(),
        email_verified_at = now()
  WHERE id = user_id;
  
  -- 5) Verify that the email has updated
  IF (SELECT email FROM users WHERE id = user_id) <> new_email THEN
    RAISE EXCEPTION 'Email change did not take effect after token usage';
  END IF;
  
  -- 6) Create a second user for negative tests
  INSERT INTO users (id, role, email, email_normalized, status, timezone, anonymized, created_at, updated_at, mfa_enabled, failed_login_attempts, require_password_change, first_name, last_name, password_hash)
  VALUES (gen_random_uuid(), 'user', 'bob@example.com', lower('bob@example.com'), 'active', 'UTC', false, now(), now(), false, 0, false, 'Bob', 'Builder', 'hash');
  SELECT id INTO second_user_id FROM users WHERE email = 'bob@example.com';
  -- 7) Create an expired token for Bob
  INSERT INTO email_verification_tokens (id, token, user_id, created_at, expires_at, new_email, new_email_normalized)
  VALUES (gen_random_uuid(), gen_random_uuid()::text, second_user_id, now(), now() - interval '1 day', 'bob.new@example.com', lower('bob.new@example.com'))
  RETURNING id INTO expired_token_id;
  
  -- 8) Attempt to apply expired token (should not update Bob)
  UPDATE users u
    SET email = nt.new_email, email_normalized = nt.new_email_normalized, updated_at = now()
  FROM email_verification_tokens nt
  WHERE nt.id = expired_token_id
    AND nt.used_at IS NULL
    AND nt.expires_at > now()
    AND u.id = nt.user_id;
  GET DIAGNOSTICS row_count = ROW_COUNT;
  IF row_count > 0 THEN
    RAISE NOTICE 'Expired token unexpectedly updated a user email';
  ELSE
    RAISE NOTICE 'Expired token correctly prevented email update';
  END IF;
  
  -- 9) Attempt to reuse the same token (should not update)
  UPDATE email_verification_tokens SET used_at = now() WHERE id = token_id;
  UPDATE users u2
    SET email = (SELECT new_email FROM email_verification_tokens WHERE id = token_id),
        email_normalized = (SELECT lower(new_email) FROM email_verification_tokens WHERE id = token_id),
        updated_at = now()
  FROM email_verification_tokens nt
  WHERE nt.id = token_id
    AND nt.used_at IS NULL
    AND u2.id = nt.user_id;
  GET DIAGNOSTICS row_count = ROW_COUNT;
  IF row_count > 0 THEN
    RAISE NOTICE 'Reuse of an already-used token unexpectedly updated email';
  ELSE
    RAISE NOTICE 'Reuse of used token correctly prevented';
  END IF;
  
END
$$

Test Case: Self-registration with email/password creates user and generates email verification token

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: da4f47b4-ea18-4e09-9c9c-00655f5c125c
-- Self-registration with email/password creates user and generates email verification token
-- Test end-to-end self-registration flow: insert a new user with password, generate a verification token, and verify both user and token records exist with expected attributes.
-- Begin self-registration test: create a new user and its email verification token
WITH new_user AS (
  INSERT INTO users (
    id, role, email, email_normalized, password_hash, status, timezone, anonymized,
    created_at, updated_at, mfa_enabled, failed_login_attempts, verification_resend_count_24h, require_password_change
  ) VALUES (
    gen_random_uuid(),
    'user'::role_enum,
    'TestUser@example.com',
    'testuser@example.com',
    'pbkdf2-hash-example',
    'active'::status_enum,
    'UTC',
    false,
    NOW(),
    NOW(),
    false,
    0,
    0,
    false
  )
  RETURNING id
)
INSERT INTO email_verification_tokens (
  id, token, used_at, user_id, new_email, created_at, expires_at, new_email_normalized
)
VALUES (
  gen_random_uuid(),
  'token-' || gen_random_uuid(),
  NULL,
  (SELECT id FROM new_user),
  NULL,
  NOW(),
  NOW() + INTERVAL '15 minutes',
  NULL
)

Test Case: Access gating: active + verified users; invited flow requiring password setup

1. Error: invalid input value for enum role_enum: "admin"

-- 1: Create admin user (will issue invitation)
INSERT INTO users (
  id, role, email, phone, status, timezone, last_name, anonymized, avatar_url, created_at, deleted_at, deleted_by, first_name, note_admin, updated_at, mfa_enabled, display_name, profile_json, last_login_at, lockout_until, password_hash, mfa_secret_enc, email_normalized, email_verified_at, failed_login_attempts, require_password_change, last_verification_sent_at, verification_resend_count_24h
) VALUES (
  gen_random_uuid(),
  'admin',
  'admin@example.com',
  NULL,
  'active',
  'UTC',
  NULL,
  FALSE,
  NULL,
  now(),
  NULL,
  NULL,
  'Admin',
  NULL,
  now(),
  TRUE,
  'Admin',
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,
  lower('admin@example.com'),
  now(),
  0,
  FALSE,
  NULL,
  0
)

Test Case: Password reset request creates valid 60-minute expiry token and an expired token for the same user (positive and negative paths)

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: 079a56e5-4ee8-4404-a86b-4edbcd022578
-- Password reset request creates valid 60-minute expiry token and an expired token for the same user (positive and negative paths)
-- Creates a user and two tokens in a single operation: one valid token expiring in 60 minutes and one expired token. Then lists tokens for the user to verify both tokens exist and have correct expiry semantics.
WITH
  user_insert AS (
    INSERT INTO users (
      id, role, email, email_normalized, status, timezone, anonymized,
      created_at, updated_at, mfa_enabled, failed_login_attempts, require_password_change
    )
    VALUES (
      gen_random_uuid(),
      'user',
      'Alice.Smith@example.com',
      LOWER('Alice.Smith@example.com'),
      'active',
      'UTC',
      true,
      NOW(),
      NOW(),
      false,
      0,
      false
    )
    RETURNING id
  ),
  token_valid AS (
    INSERT INTO password_reset_tokens (
      id, token, user_id, created_at, expires_at, used_at, new_email, new_email_normalized
    )
    SELECT gen_random_uuid(), 'token-valid-12345', id, NOW(), NOW() + INTERVAL '60 minutes', NULL, NULL, NULL
    FROM user_insert
    RETURNING id
  ),
  token_expired AS (
    INSERT INTO password_reset_tokens (
      id, token, user_id, created_at, expires_at, used_at, new_email, new_email_normalized
    )
    SELECT gen_random_uuid(), 'token-expired-67890', id, NOW(), NOW() - INTERVAL '1 minute', NULL, NULL, NULL
    FROM user_insert
    RETURNING id
  )
SELECT
  (SELECT id FROM token_valid) AS valid_token_id,
  (SELECT id FROM token_expired) AS expired_token_id

Test Case: Lockout after 10 consecutive failed logins within 15 minutes with admin unlock and expiry-based auto-unlock

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: 60766a4f-087b-4d28-b027-081c03467c84
-- Lockout after 10 consecutive failed logins within 15 minutes with admin unlock and expiry-based auto-unlock
-- This DML script creates a user, simulates 9 failed login attempts within 15 minutes, adds a 10th failure to trigger lockout by setting lockout_until 15 minutes ahead, then demonstrates admin unlock and expiry-based automatic unlock path by setting the lockout window in the past.
DO $$
DECLARE
  v_user_id UUID;
  i integer;
BEGIN
  -- Create a new user
  INSERT INTO users (
    id, role, email, email_normalized, status, timezone, anonymized, created_at, updated_at, mfa_enabled,
    failed_login_attempts, verification_resend_count_24h, require_password_change
  ) VALUES (
    gen_random_uuid(), 'user', 'alice.lock@example.com', 'alice.lock@example.com', 'active', 'UTC', FALSE,
    now(), now(), FALSE, 0, 0, FALSE
  ) RETURNING id INTO v_user_id;

  -- 9 failed login attempts within 15 minutes
  FOR i IN 0..8 LOOP
    INSERT INTO login_attempts (id, success, user_id, created_at, ip_address, user_agent, email_attempted)
    VALUES (gen_random_uuid(), FALSE, v_user_id, now() - ((14 - i) * interval '1 minute'), '203.0.113.1', 'test-agent', 'alice.lock@example.com');
  END LOOP;

  -- 10th failed login attempt
  INSERT INTO login_attempts (id, success, user_id, created_at, ip_address, user_agent, email_attempted)
  VALUES (gen_random_uuid(), FALSE, v_user_id, now(), '203.0.113.1', 'test-agent', 'alice.lock@example.com');

  -- Trigger lockout: set lockout_until 15 minutes from now
  UPDATE users
  SET lockout_until = now() + interval '15 minutes',
      updated_at = now(),
      failed_login_attempts = 10
  WHERE id = v_user_id;
END
$$

Test Case: Test sessions expiry policy: inactivity vs absolute expiry

1. Error: insert or update on table "sessions" violates foreign key constraint "fk_sessions_user"

-- Test Case: 8c60472e-5ca5-4b2c-b621-5b1f76e663d1
-- Test sessions expiry policy: inactivity vs absolute expiry
-- Performs insertion of a test session and progressive expiry checks: inactivity expiry, absolute expiry, active state, and boundary condition at exactly 30 minutes of inactivity.
INSERT INTO sessions (id, revoked, user_id, created_at, ip_address, user_agent, session_token, last_activity_at, absolute_expires_at) VALUES (gen_random_uuid(), false, gen_random_uuid(), now(), NULL, NULL, 'sess_token_1', now(), now() + interval '24 hours')

Test Case: RBAC: Provision Admin and Member roles and validate role constraint

1. Error: invalid input value for enum status_enum: "active"

-- Test Case: 27a841b9-1992-48b3-8446-d889d3ffbfc9
-- RBAC: Provision Admin and Member roles and validate role constraint
-- Test that Admin and Member roles can be created and that a NULL role is rejected by the NOT NULL constraint (handled to avoid test failure).
-- Admin user
INSERT INTO users (
  id, role, email, phone, status, timezone, last_name, anonymized,
  created_at, updated_at, mfa_enabled, email_normalized,
  failed_login_attempts, require_password_change, verification_resend_count_24h
) VALUES (
  gen_random_uuid(),
  'Admin',
  'Admin@example.com',
  NULL,
  'active',
  'UTC',
  'AdminLast',
  FALSE,
  now(),
  now(),
  FALSE,
  'admin@example.com',
  0,
  FALSE,
  0
)

Test Case: Admin-driven comprehensive user management workflow coverage

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: c56b9313-54b1-4663-b138-4d1827ec16c8
-- Admin-driven comprehensive user management workflow coverage
-- Single DO block performing admin-driven operations: create admin and user, issue invitation, export, edit user profile and role, suspend/reactivate, and soft-delete with audit logging.
DO
$$
DECLARE
  admin_id uuid := gen_random_uuid();
  regular_id uuid := gen_random_uuid();
  invite_id uuid := gen_random_uuid();
  export_id uuid := gen_random_uuid();
  audit_id uuid := gen_random_uuid();
  now_ts timestamptz := now();
  metadata_json jsonb;
BEGIN
  -- Admin user
  INSERT INTO users (
    id, role, email, email_normalized, status, timezone,
    anonymized, created_at, updated_at, mfa_enabled,
    failed_login_attempts, require_password_change
  ) VALUES (
    admin_id, 'admin'::role_enum, 'admin@example.com', lower('admin@example.com'), 'active'::status_enum, 'UTC',
    true, now_ts, now_ts, false,
    0, false
  );

  -- Regular user
  INSERT INTO users (
    id, role, email, email_normalized, status, timezone,
    anonymized, created_at, updated_at, mfa_enabled,
    failed_login_attempts, require_password_change
  ) VALUES (
    regular_id, 'user'::role_enum, 'jane.doe@example.com', lower('jane.doe@example.com'), 'active'::status_enum, 'America/Los_Angeles',
    true, now_ts, now_ts, true,
    0, false
  );

  -- Invitation
  INSERT INTO invitations (
    id, email, token, revoked, created_at, expires_at, inviter_user_id, email_normalized
  ) VALUES (
    invite_id, 'new.user@example.com', 'invtoken123', false, now_ts, now_ts + interval '7 days', admin_id, lower('new.user@example.com')
  );

  -- Export job
  INSERT INTO exports (
    id, status, filters, file_url, created_at, actor_user_id
  ) VALUES (
    export_id, 'completed', '{"scope":"users","role":"user"}'::jsonb, '/exports/users.csv', now_ts, admin_id
  );

  -- Audit log: admin created user
  metadata_json := jsonb_build_object('action','admin_created_user','target_user_id', regular_id::text);
  INSERT INTO audit_logs(action, audit_id, metadata, created_at, actor_user_id, target_user_id)
  VALUES ('user.created', audit_id, metadata_json, now_ts, admin_id, NULL);

  -- Admin updates regular user profile
  UPDATE users SET
    email = 'jane.new@example.com',
    email_normalized = lower('jane.new@example.com'),
    first_name = 'Jane',
    last_name = 'Newman',
    timezone = 'America/Chicago',
    mfa_enabled = true,
    display_name = 'Jane New',
    updated_at = now_ts,
    profile_json = '{}'::jsonb
  WHERE id = regular_id;

  -- Change role of regular user to admin
  UPDATE users SET role = 'admin'::role_enum, updated_at = now_ts WHERE id = regular_id;

  -- Suspend and reactivate
  UPDATE users SET status = 'suspended'::status_enum, updated_at = now_ts WHERE id = regular_id;
  UPDATE users SET status = 'active'::status_enum, updated_at = now_ts WHERE id = regular_id;

  -- Soft-delete
  UPDATE users SET deleted_at = now_ts, deleted_by = admin_id, updated_at = now_ts WHERE id = regular_id;

END
$$

Test Case: Members can manage their own profile and credentials; admin data remains inaccessible and unaffected

1. Error: invalid input value for enum role_enum: "admin"

-- Insert an admin user; store the id for later reference
WITH admin_ins AS (
  INSERT INTO users (
    id,
    role,
    email,
    email_normalized,
    timezone,
    status,
    anonymized,
    created_at,
    updated_at,
    mfa_enabled,
    failed_login_attempts,
    require_password_change,
    verification_resend_count_24h
  ) VALUES (
    gen_random_uuid(),
    'admin'::role_enum,
    'admin1@example.com',
    'admin1@example.com',
    'UTC',
    'active'::status_enum,
    false,
    NOW(),
    NOW(),
    true,
    0,
    false,
    0
  )
  RETURNING id
)
INSERT INTO test_user_ids (role, id) SELECT 'admin', id FROM admin_ins

Test Case: Admin invitation by email with 7-day expiry and email normalization

1. Error: insert or update on table "invitations" violates foreign key constraint "fk_invitations_inviter"

-- Test Case: 287bcf92-8912-48e6-b7a4-eff710bc0fc6
-- Admin invitation by email with 7-day expiry and email normalization
-- Inserts two invitations: one with a mixed-case email to test normalization to lower-case in email_normalized, and a boundary-case invite with a fixed 7-day expiry. Validates that expires_at is created_at plus 7 days and that critical fields are set as expected.
-- Positive test: standard invite with mixed-case email and 7-day expiry
WITH t AS (
  SELECT now() AS created_at, gen_random_uuid() AS id
)
INSERT INTO invitations (
  id,
  email,
  token,
  revoked,
  created_at,
  expires_at,
  revoked_by,
  accepted_at,
  inviter_user_id,
  email_normalized
)
SELECT
  t.id,
  'Test.Invite@Example.COM',
  gen_random_uuid()::text,
  FALSE,
  t.created_at,
  t.created_at + INTERVAL '7 days',
  NULL, -- revoked_by
  NULL, -- accepted_at
  gen_random_uuid(),
  lower('Test.Invite@Example.COM')
FROM t

Test Case: Invited user completes setup by setting password and basic profile before activation to Active

1. Error: invalid input value for enum role_enum: "user"

-- 2) Create the invited user in Invited state with required non-null fields
INSERT INTO users (
  id, role, email, phone, status, timezone, last_name, anonymized, avatar_url, created_at, deleted_at, deleted_by, first_name, note_admin, updated_at, mfa_enabled, display_name, profile_json, last_login_at, lockout_until, password_hash, mfa_secret_enc, email_normalized, email_verified_at, failed_login_attempts, require_password_change, last_verification_sent_at, verification_resend_count_24h
)
VALUES (
  gen_random_uuid(),
  'user',
  'invitee@example.com',
  NULL,
  'Invited',
  'UTC',
  NULL,
  false,
  NULL,
  now(),
  NULL,
  NULL,
  NULL,
  NULL,
  now(),
  false,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,
  lower('invitee@example.com'),
  NULL,
  0,
  true,
  NULL,
  0
)

Test Case: Admin suspend/reactivate user with enforced lockout (status-based authentication control)

1. Error: invalid input value for enum role_enum: "admin"

-- 1) Create an admin user (to act as admin in lifecycle tests)
INSERT INTO users (
  id, role, email, email_normalized, status, timezone, anonymized, created_at, updated_at, mfa_enabled, failed_login_attempts, require_password_change
) VALUES (
  gen_random_uuid(),
  'admin'::role_enum,
  'admin@example.com',
  'admin@example.com',
  'active'::status_enum,
  'UTC',
  false,
  now(),
  now(),
  false,
  0,
  false
)

Test Case: Soft delete with 30-day restore window and purge anonymization

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: c0b34876-a1ad-44fd-8e3e-ba291e4c61cf
-- Soft delete with 30-day restore window and purge anonymization
-- This DML operation creates a user, performs soft delete, restores within the 30-day window, and finally simulates a purge after the retention window by anonymizing PII fields.
DO $$
DECLARE
  v_user_id UUID;
BEGIN
  -- Step 1: Create a new user (not anonymized yet)
  INSERT INTO users (
    id,
    role,
    email,
    phone,
    status,
    timezone,
    last_name,
    first_name,
    anonymized,
    avatar_url,
    created_at,
    updated_at,
    mfa_enabled,
    display_name,
    email_normalized,
    failed_login_attempts,
    require_password_change,
    verification_resend_count_24h
  ) VALUES (
    gen_random_uuid(),
    'user'::role_enum,
    'Alice@example.com',
    NULL,
    'active'::status_enum,
    'UTC',
    'Doe',
    'Alice',
    false,
    NULL,
    now(),
    now(),
    false,
    'Alice D',
    'alice@example.com',
    0,
    false,
    0
  )
  RETURNING id INTO v_user_id;

  -- Step 2: Soft delete the user
  UPDATE users
  SET deleted_at = now(),
      deleted_by = gen_random_uuid(),
      updated_at = now()
  WHERE id = v_user_id;

  -- Step 3: Restore within 30 days
  UPDATE users
  SET deleted_at = NULL,
      deleted_by = NULL,
      updated_at = now()
  WHERE id = v_user_id;

  -- Step 4: Soft delete again to simulate purge after 31 days
  UPDATE users
  SET deleted_at = now(), -- re-delete, to simulate passage of time
      updated_at = now(),
      deleted_by = gen_random_uuid()
  WHERE id = v_user_id;

  -- Step 5: Purge simulation after 31 days by anonymizing PII fields
  UPDATE users
  SET email = 'ANONYMIZED',
      email_normalized = 'anonymized',
      first_name = 'REDACTED',
      last_name = 'REDACTED',
      display_name = 'REDACTED',
      phone = NULL,
      avatar_url = NULL,
      note_admin = 'REDACTED',
      profile_json = NULL,
      updated_at = now()
  WHERE id = v_user_id;
END;
$$ LANGUAGE plpgsql

Test Case: Comprehensive admin console user-directory search and filter validation

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: 9da10faf-5d73-48ed-a71d-6ffaad8f7104
-- Comprehensive admin console user-directory search and filter validation
-- Inserts 4 users with varying roles, statuses, creation dates, and email verification states, then runs validation queries for searching by email, searching by name, filtering by role, filtering by status with a creation date range, and filtering by email verification state.
-- Inserting test users for Admin Console search and filter validation
INSERT INTO users (
  id,
  role,
  email,
  phone,
  status,
  timezone,
  last_name,
  anonymized,
  avatar_url,
  created_at,
  deleted_at,
  deleted_by,
  first_name,
  note_admin,
  updated_at,
  mfa_enabled,
  display_name,
  profile_json,
  last_login_at,
  lockout_until,
  password_hash,
  mfa_secret_enc,
  email_normalized,
  email_verified_at,
  failed_login_attempts,
  require_password_change,
  last_verification_sent_at,
  verification_resend_count_24h
) VALUES (
  gen_random_uuid(),
  'admin'::role_enum,
  'Admin.One@example.com',
  NULL,
  'active'::status_enum,
  'UTC',
  'One',
  false,
  NULL,
  TIMESTAMPTZ '2025-09-20 10:00:00+00',
  NULL,
  NULL,
  'Admin',
  NULL,
  TIMESTAMPTZ '2025-09-20 10:05:00+00',
  true,
  'Admin One',
  '{"prefs":{"theme":"dark"}}'::jsonb,
  TIMESTAMPTZ '2025-09-25 12:30:00+00',
  NULL,
  'pbkdf2$hash-admin$123',
  NULL,
  'admin.one@example.com',
  TIMESTAMPTZ '2025-09-20 10:00:00+00',
  0,
  false,
  NULL,
  0
)

Test Case: User Directory Sorting and Pagination Validation (created_at and last_login_at)

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: cfcdd305-5309-40b0-8d43-2f8f26b93d98
-- User Directory Sorting and Pagination Validation (created_at and last_login_at)
-- Test scripts to validate sorting and pagination in the user directory.
INSERT INTO users (
  id, role, email, phone, status, timezone, last_name, anonymized, avatar_url,
  created_at, deleted_at, deleted_by, first_name, note_admin, updated_at, mfa_enabled,
  display_name, profile_json, last_login_at, lockout_until, password_hash, mfa_secret_enc,
  email_normalized, email_verified_at, failed_login_attempts, require_password_change,
  last_verification_sent_at, verification_resend_count_24h
) VALUES
(gen_random_uuid(), 'admin', 'alpha1@example.com', NULL, 'active', 'UTC', 'Last1', TRUE, NULL, now() - interval '1 day', NULL, NULL, 'First1', NULL, now() - interval '1 day', TRUE, 'First1 Last1', NULL, now() - interval '20 hours', NULL, NULL, NULL, 'alpha1@example.com', NULL, 0, TRUE, NULL, 0),
(gen_random_uuid(), 'user', 'alpha2@example.com', NULL, 'active', 'UTC', 'Last2', TRUE, NULL, now() - interval '2 days', NULL, NULL, 'First2', NULL, now() - interval '2 days', FALSE, NULL, NULL, now() - interval '26 hours', NULL, NULL, NULL, 'alpha2@example.com', NULL, 1, FALSE, NULL, 0),
(gen_random_uuid(), 'user', 'alpha3@example.com', NULL, 'active', 'UTC', 'Last3', TRUE, NULL, now() - interval '3 days', NULL, NULL, 'First3', NULL, now() - interval '3 days', TRUE, NULL, NULL, now() - interval '24 hours', NULL, NULL, NULL, 'alpha3@example.com', NULL, 2, TRUE, NULL, 1),
(gen_random_uuid(), 'user', 'alpha4@example.com', NULL, 'active', 'UTC', 'Last4', TRUE, NULL, now() - interval '4 days', NULL, NULL, 'First4', NULL, now() - interval '4 days', FALSE, NULL, NULL, now() - interval '23 hours', NULL, NULL, NULL, 'alpha4@example.com', NULL, 3, FALSE, NULL, 1),
(gen_random_uuid(), 'user', 'alpha5@example.com', NULL, 'active', 'UTC', 'Last5', TRUE, NULL, now() - interval '5 days', NULL, NULL, 'First5', NULL, now() - interval '5 days', FALSE, NULL, NULL, now() - interval '22 hours', NULL, NULL, NULL, 'alpha5@example.com', NULL, 4, FALSE, NULL, 2),
(gen_random_uuid(), 'user', 'alpha6@example.com', NULL, 'active', 'UTC', 'Last6', TRUE, NULL, now() - interval '6 days', NULL, NULL, 'First6', NULL, now() - interval '6 days', TRUE, NULL, NULL, now() - interval '21 hours', NULL, NULL, NULL, 'alpha6@example.com', NULL, 5, TRUE, NULL, 2),
(gen_random_uuid(), 'user', 'alpha7@example.com', NULL, 'active', 'UTC', 'Last7', TRUE, NULL, now() - interval '7 days', NULL, NULL, 'First7', NULL, now() - interval '7 days', FALSE, NULL, NULL, now() - interval '20 hours', NULL, NULL, NULL, 'alpha7@example.com', NULL, 6, FALSE, NULL, 3),
(gen_random_uuid(), 'user', 'alpha8@example.com', NULL, 'active', 'UTC', 'Last8', TRUE, NULL, now() - interval '8 days', NULL, NULL, 'First8', NULL, now() - interval '8 days', FALSE, NULL, NULL, now() - interval '19 hours', NULL, NULL, NULL, 'alpha8@example.com', NULL, 7, FALSE, NULL, 3),
(gen_random_uuid(), 'user', 'alpha9@example.com', NULL, 'active', 'UTC', 'Last9', TRUE, NULL, now() - interval '9 days', NULL, NULL, 'First9', NULL, now() - interval '9 days', TRUE, NULL, NULL, now() - interval '18 hours', NULL, NULL, NULL, 'alpha9@example.com', NULL, 8, TRUE, NULL, 4),
(gen_random_uuid(), 'user', 'alpha10@example.com', NULL, 'active', 'UTC', 'Last10', TRUE, NULL, now() - interval '10 days', NULL, NULL, 'First10', NULL, now() - interval '10 days', FALSE, NULL, NULL, now() - interval '17 hours', NULL, NULL, NULL, 'alpha10@example.com', NULL, 9, FALSE, NULL, 4),
(gen_random_uuid(), 'user', 'alpha11@example.com', NULL, 'active', 'UTC', 'Last11', TRUE, NULL, now() - interval '11 days', NULL, NULL, 'First11', NULL, now() - interval '11 days', FALSE, NULL, NULL, now() - interval '16 hours', NULL, NULL, NULL, 'alpha11@example.com', NULL, 10, FALSE, NULL, 5),
(gen_random_uuid(), 'user', 'alpha12@example.com', NULL, 'active', 'UTC', 'Last12', TRUE, NULL, now() - interval '12 days', NULL, NULL, 'First12', NULL, now() - interval '12 days', TRUE, NULL, NULL, now() - interval '15 hours', NULL, NULL, NULL, 'alpha12@example.com', NULL, 11, TRUE, NULL, 5),
(gen_random_uuid(), 'user', 'alpha13@example.com', NULL, 'active', 'UTC', 'Last13', TRUE, NULL, now() - interval '13 days', NULL, NULL, 'First13', NULL, now() - interval '13 days', FALSE, NULL, NULL, now() - interval '14 hours', NULL, NULL, NULL, 'alpha13@example.com', NULL, 12, FALSE, NULL, 6),
(gen_random_uuid(), 'user', 'alpha14@example.com', NULL, 'active', 'UTC', 'Last14', TRUE, NULL, now() - interval '14 days', NULL, NULL, 'First14', NULL, now() - interval '14 days', FALSE, NULL, NULL, now() - interval '13 hours', NULL, NULL, NULL, 'alpha14@example.com', NULL, 13, FALSE, NULL, 6),
(gen_random_uuid(), 'user', 'alpha15@example.com', NULL, 'active', 'UTC', 'Last15', TRUE, NULL, now() - interval '15 days', NULL, NULL, 'First15', NULL, now() - interval '15 days', FALSE, NULL, NULL, now() - interval '12 hours', NULL, NULL, NULL, 'alpha15@example.com', NULL, 14, FALSE, NULL, 7),
(gen_random_uuid(), 'user', 'alpha16@example.com', NULL, 'active', 'UTC', 'Last16', TRUE, NULL, now() - interval '16 days', NULL, NULL, 'First16', NULL, now() - interval '16 days', TRUE, NULL, NULL, now() - interval '11 hours', NULL, NULL, NULL, 'alpha16@example.com', NULL, 15, TRUE, NULL, 7),
(gen_random_uuid(), 'user', 'alpha17@example.com', NULL, 'active', 'UTC', 'Last17', TRUE, NULL, now() - interval '17 days', NULL, NULL, 'First17', NULL, now() - interval '17 days', FALSE, NULL, NULL, now() - interval '10 hours', NULL, NULL, NULL, 'alpha17@example.com', NULL, 16, FALSE, NULL, 8),
(gen_random_uuid(), 'user', 'alpha18@example.com', NULL, 'active', 'UTC', 'Last18', TRUE, NULL, now() - interval '18 days', NULL, NULL, 'First18', NULL, now() - interval '18 days', FALSE, NULL, NULL, now() - interval '9 hours', NULL, NULL, NULL, 'alpha18@example.com', NULL, 17, FALSE, NULL, 8),
(gen_random_uuid(), 'user', 'alpha19@example.com', NULL, 'active', 'UTC', 'Last19', TRUE, NULL, now() - interval '19 days', NULL, NULL, 'First19', NULL, now() - interval '19 days', TRUE, NULL, NULL, now() - interval '8 hours', NULL, NULL, NULL, 'alpha19@example.com', NULL, 18, FALSE, NULL, 9),
(gen_random_uuid(), 'user', 'alpha20@example.com', NULL, 'active', 'UTC', 'Last20', TRUE, NULL, now() - interval '20 days', NULL, NULL, 'First20', NULL, now() - interval '20 days', FALSE, NULL, NULL, now() - interval '7 hours', NULL, NULL, NULL, 'alpha20@example.com', NULL, 19, FALSE, NULL, 9),
(gen_random_uuid(), 'user', 'alpha21@example.com', NULL, 'active', 'UTC', 'Last21', TRUE, NULL, now() - interval '21 days', NULL, NULL, 'First21', NULL, now() - interval '21 days', FALSE, NULL, NULL, now() - interval '6 hours', NULL, NULL, NULL, 'alpha21@example.com', NULL, 20, FALSE, NULL, 10),
(gen_random_uuid(), 'user', 'alpha22@example.com', NULL, 'active', 'UTC', 'Last22', TRUE, NULL, now() - interval '22 days', NULL, NULL, 'First22', NULL, now() - interval '22 days', TRUE, NULL, NULL, now() - interval '5 hours', NULL, NULL, NULL, 'alpha22@example.com', NULL, 21, FALSE, NULL, 10),
(gen_random_uuid(), 'user', 'alpha23@example.com', NULL, 'active', 'UTC', 'Last23', TRUE, NULL, now() - interval '23 days', NULL, NULL, 'First23', NULL, now() - interval '23 days', FALSE, NULL, NULL, now() - interval '4 hours', NULL, NULL, NULL, 'alpha23@example.com', NULL, 22, FALSE, NULL, 11),
(gen_random_uuid(), 'user', 'alpha24@example.com', NULL, 'active', 'UTC', 'Last24', TRUE, NULL, now() - interval '24 days', NULL, NULL, 'First24', NULL, now() - interval '24 days', TRUE, NULL, NULL, now() - interval '3 hours', NULL, NULL, NULL, 'alpha24@example.com', NULL, 23, FALSE, NULL, 11),
(gen_random_uuid(), 'user', 'alpha25@example.com', NULL, 'active', 'UTC', 'Last25', TRUE, NULL, now() - interval '25 days', NULL, NULL, 'First25', NULL, now() - interval '25 days', FALSE, NULL, NULL, now() - interval '2 hours', NULL, NULL, NULL, 'alpha25@example.com', NULL, 24, FALSE, NULL, 12),
(gen_random_uuid(), 'user', 'alpha26@example.com', NULL, 'active', 'UTC', 'Last26', TRUE, NULL, now() - interval '26 days', NULL, NULL, 'First26', NULL, now() - interval '26 days', TRUE, NULL, NULL, now() - interval '1 hour', NULL, NULL, NULL, 'alpha26@example.com', NULL, 25, FALSE, NULL, 12),
(gen_random_uuid(), 'user', 'alpha27@example.com', NULL, 'active', 'UTC', 'Last27', TRUE, NULL, now() - interval '27 days', NULL, NULL, 'First27', NULL, now() - interval '27 days', FALSE, NULL, NULL, now() - interval '30 minutes', NULL, NULL, NULL, 'alpha27@example.com', NULL, 26, FALSE, NULL, 13),
(gen_random_uuid(), 'user', 'alpha28@example.com', NULL, 'active', 'UTC', 'Last28', TRUE, NULL, now() - interval '28 days', NULL, NULL, 'First28', NULL, now() - interval '28 days', TRUE, NULL, NULL, now() - interval '25 minutes', NULL, NULL, NULL, 'alpha28@example.com', NULL, 27, FALSE, NULL, 13),
(gen_random_uuid(), 'user', 'alpha29@example.com', NULL, 'active', 'UTC', 'Last29', TRUE, NULL, now() - interval '29 days', NULL, NULL, 'First29', NULL, now() - interval '29 days', FALSE, NULL, NULL, now() - interval '20 minutes', NULL, NULL, NULL, 'alpha29@example.com', NULL, 28, FALSE, NULL, 14),
(gen_random_uuid(), 'user', 'alpha30@example.com', NULL, 'active', 'UTC', 'Last30', TRUE, NULL, now() - interval '30 days', NULL, NULL, 'First30', NULL, now() - interval '30 days', FALSE, NULL, NULL, now() - interval '15 minutes', NULL, NULL, NULL, 'alpha30@example.com', NULL, 29, FALSE, NULL, 14)

Test Case: Admin Console: view user detail, edit profile, resend invitation, reset password, change status and role

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: 111ed730-f57a-430b-8350-84ee7dd7db56
-- Admin Console: view user detail, edit profile, resend invitation, reset password, change status and role
-- This DO block creates an admin and a target user, issues an invitation, creates a password reset token for the target, then performs profile edits, status and role changes for the target, and finally resends an invitation. It ends with queries to verify resulting states in users, invitations, and password_reset_tokens.
DO $$
DECLARE
  admin_id uuid;
  target_id uuid;
  invite_id uuid;
  resend_invite_id uuid;
  reset_token_id uuid;
BEGIN
  -- 1) Create an admin user
  INSERT INTO users (
    id,
    role,
    email,
    email_normalized,
    status,
    timezone,
    anonymized,
    created_at,
    updated_at,
    mfa_enabled,
    failed_login_attempts,
    require_password_change
  )
  VALUES (
    gen_random_uuid(),
    'admin'::role_enum,
    'Admin.Jones@example.com',
    lower('Admin.Jones@example.com'),
    'active'::status_enum,
    'UTC',
    false,
    now(),
    now(),
    true,
    0,
    false
  )
  RETURNING id INTO admin_id;

  -- 2) Create a target user to be managed by the admin
  INSERT INTO users (
    id,
    role,
    email,
    email_normalized,
    status,
    timezone,
    anonymized,
    created_at,
    updated_at,
    mfa_enabled,
    failed_login_attempts,
    require_password_change
  )
  VALUES (
    gen_random_uuid(),
    'user'::role_enum,
    'Target.User@example.com',
    lower('Target.User@example.com'),
    'active'::status_enum,
    'UTC',
    false,
    now(),
    now(),
    true,
    0,
    false
  )
  RETURNING id INTO target_id;

  -- 3) Issue an invitation for the target user, linked to the admin
  INSERT INTO invitations (
    id,
    email,
    token,
    revoked,
    created_at,
    expires_at,
    email_normalized,
    inviter_user_id
  )
  VALUES (
    gen_random_uuid(),
    'Target.User@example.com',
    'INVITE-' || gen_random_uuid(),
    false,
    now(),
    now() + interval '7 days',
    lower('Target.User@example.com'),
    admin_id
  )
  RETURNING id INTO invite_id;

  -- 4) Create a password reset token for the target (admin-initiated)
  INSERT INTO password_reset_tokens (
    id,
    token,
    used_at,
    user_id,
    created_at,
    expires_at
  )
  VALUES (
    gen_random_uuid(),
    'RESET-' || gen_random_uuid(),
    NULL,
    target_id,
    now(),
    now() + interval '60 minutes'
  )
  RETURNING id INTO reset_token_id;

  -- 5) Admin edits profile fields for the target user
  UPDATE users
  SET first_name = 'Target',
      last_name = 'User',
      display_name = 'Target User',
      timezone = 'America/Los_Angeles',
      updated_at = now(),
      email = 'Target.User@example.com',
      email_normalized = lower('Target.User@example.com'),
      profile_json = '{"theme":"dark"}'
  WHERE id = target_id;

  -- 6) Admin changes status (e.g., deactivate/suspend)
  UPDATE users
  SET status = 'inactive'::status_enum,
      updated_at = now()
  WHERE id = target_id;

  -- 7) Admin changes role for the target (e.g., promote to admin)
  UPDATE users
  SET role = 'admin'::role_enum,
      updated_at = now()
  WHERE id = target_id;

  -- 8) Admin resends invitation by creating a new invitation token for the same target email
  INSERT INTO invitations (
    id,
    email,
    token,
    revoked,
    created_at,
    expires_at,
    email_normalized,
    inviter_user_id
  )
  VALUES (
    gen_random_uuid(),
    'Target.User@example.com',
    'RESEND-' || gen_random_uuid(),
    false,
    now(),
    now() + interval '7 days',
    lower('Target.User@example.com'),
    admin_id
  )
  RETURNING id INTO resend_invite_id;

END
$$

Test Case: Admin export of current user list with role=user and status=active

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: 818bee10-7670-4303-947b-1af63ab8c0d1
-- Admin export of current user list with role=user and status=active
-- What this DML operation tests
-- Insert admin user
INSERT INTO users (id, role, email, phone, status, timezone, last_name, anonymized, avatar_url, created_at, deleted_at, deleted_by, first_name, note_admin, updated_at, mfa_enabled, display_name, profile_json, last_login_at, lockout_until, password_hash, mfa_secret_enc, email_normalized, email_verified_at, failed_login_attempts, require_password_change, last_verification_sent_at, verification_resend_count_24h)
VALUES (
  gen_random_uuid(),
  'admin',
  'admin@example.com',
  NULL,
  'active',
  'UTC',
  NULL,
  false,
  NULL,
  NOW(),
  NULL,
  NULL,
  'Admin',
  NULL,
  NOW(),
  TRUE,
  'Admin Admin',
  '{"theme":"dark"}'::jsonb,
  NOW(),
  NULL,
  'hash_admin',
  NULL,
  lower('admin@example.com'),
  NOW(),
  0,
  TRUE,
  NULL,
  0
)

Test Case: Transactional email flows: invitation, verification, password reset, suspension/reactivation, and role changes

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: 60b18393-34ab-4041-95bc-5d737c99abf9
-- Transactional email flows: invitation, verification, password reset, suspension/reactivation, and role changes
-- The SQL script validates each transactional email trigger path through a sequence of inserts and state transitions. It creates an admin and invitee user, issues an invitation, generates verification and password reset tokens, exercises verification and password reset flows, performs role changes and suspensions with corresponding audit logs, revokes the invitation as a negative path, and demonstrates token expiry handling. All IDs are generated with gen_random_uuid().
-- 1) Create an admin user (the approver/triggerer of notifications)
INSERT INTO users (
  id,
  role,
  email,
  phone,
  status,
  timezone,
  last_name,
  anonymized,
  avatar_url,
  created_at,
  deleted_at,
  deleted_by,
  first_name,
  note_admin,
  updated_at,
  mfa_enabled,
  display_name,
  profile_json,
  last_login_at,
  lockout_until,
  password_hash,
  mfa_secret_enc,
  email_normalized,
  email_verified_at,
  failed_login_attempts,
  require_password_change,
  last_verification_sent_at,
  verification_resend_count_24h
) VALUES (
  gen_random_uuid(),            -- id
  'admin',                       -- role
  'Admin@example.COM',           -- email (preserve casing)
  NULL,                          -- phone
  'active',                      -- status
  'UTC',                         -- timezone
  NULL,                          -- last_name
  FALSE,                         -- anonymized
  NULL,                          -- avatar_url
  now(),                         -- created_at
  NULL,                          -- deleted_at
  NULL,                          -- deleted_by
  'System Admin',                -- first_name
  NULL,                          -- note_admin
  now(),                         -- updated_at
  TRUE,                          -- mfa_enabled
  'System Admin',                -- display_name
  NULL,                          -- profile_json
  NULL,                          -- last_login_at
  NULL,                          -- lockout_until
  NULL,                          -- password_hash
  NULL,                          -- mfa_secret_enc
  'admin@example.com',           -- email_normalized
  NULL,                          -- email_verified_at
  0,                             -- failed_login_attempts
  FALSE,                         -- require_password_change
  NULL,                          -- last_verification_sent_at
  0                              -- verification_resend_count_24h
)

Test Case: Resend email verification: enforce max 3 per 24h with boundary aging

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: bb10b0c3-1317-4917-b943-912e98a3d585
-- Resend email verification: enforce max 3 per 24h with boundary aging
-- This script creates a test user, then performs three resends (t1-t3) within 24h. It then attempts a fourth resend (t4) which should be blocked by the 24h limit. It ages the three tokens to move them outside the 24h window (age_tokens) and then issues a fifth resend (t5) which should now be allowed. It returns the count of tokens in the last 24 hours and the id of the fifth token if created.
WITH
  u AS MATERIALIZED (
    INSERT INTO users (
      id, role, email, phone, status, timezone, last_name, anonymized, avatar_url,
      created_at, deleted_at, deleted_by, first_name, note_admin, updated_at, mfa_enabled,
      display_name, profile_json, last_login_at, lockout_until, password_hash, mfa_secret_enc,
      email_normalized, email_verified_at, failed_login_attempts, require_password_change,
      last_verification_sent_at, verification_resend_count_24h
    ) VALUES (
      gen_random_uuid(),
      'user'::role_enum,
      'TestUser@example.com',
      NULL,
      'active'::status_enum,
      'UTC',
      NULL,
      FALSE,
      NULL,
      NOW(),
      NULL,
      NULL,
      NULL,
      NULL,
      NOW(),
      FALSE,
      NULL,
      NULL,
      NULL,
      NULL,
      NULL,
      NULL,
      LOWER('TestUser@example.com'), -- email_normalized
      NULL,
      0,
      FALSE,
      NULL,
      0
    )
    RETURNING id
  ),
  t1 AS MATERIALIZED (
    INSERT INTO email_verification_tokens (
      id, token, used_at, user_id, new_email, created_at, expires_at, new_email_normalized
    ) VALUES (
      gen_random_uuid(),
      gen_random_uuid()::text,
      NULL,
      (SELECT id FROM u),
      NULL,
      NOW(),
      NOW() + interval '1 hour',
      NULL
    ) RETURNING id
  ),
  t2 AS MATERIALIZED (
    INSERT INTO email_verification_tokens (
      id, token, used_at, user_id, new_email, created_at, expires_at, new_email_normalized
    ) VALUES (
      gen_random_uuid(),
      gen_random_uuid()::text,
      NULL,
      (SELECT id FROM u),
      NULL,
      NOW(),
      NOW() + interval '1 hour',
      NULL
    ) RETURNING id
  ),
  t3 AS MATERIALIZED (
    INSERT INTO email_verification_tokens (
      id, token, used_at, user_id, new_email, created_at, expires_at, new_email_normalized
    ) VALUES (
      gen_random_uuid(),
      gen_random_uuid()::text,
      NULL,
      (SELECT id FROM u),
      NULL,
      NOW(),
      NOW() + interval '1 hour',
      NULL
    ) RETURNING id
  ),
  t4 AS MATERIALIZED (
    INSERT INTO email_verification_tokens (
      id, token, used_at, user_id, new_email, created_at, expires_at, new_email_normalized
    ) SELECT gen_random_uuid(), gen_random_uuid()::text, NULL, (SELECT id FROM u), NULL, NOW(), NOW() + interval '1 hour', NULL
    FROM u
    WHERE (SELECT COUNT(*) FROM email_verification_tokens et WHERE et.user_id = (SELECT id FROM u) AND et.created_at >= NOW() - interval '24 hours') < 3
    RETURNING id
  ),
  -- Age existing tokens to move them outside the 24h window to simulate boundary behavior
  age_tokens AS MATERIALIZED (
    UPDATE email_verification_tokens
    SET created_at = created_at - interval '25 hours'
    WHERE user_id = (SELECT id FROM u) AND id IN ((SELECT id FROM t1), (SELECT id FROM t2), (SELECT id FROM t3))
    RETURNING id
  ),
  t5 AS MATERIALIZED (
    INSERT INTO email_verification_tokens (
      id, token, used_at, user_id, new_email, created_at, expires_at, new_email_normalized
    ) SELECT gen_random_uuid(), gen_random_uuid()::text, NULL, (SELECT id FROM u), NULL, NOW(), NOW() + interval '1 hour', NULL
    FROM u
    WHERE (SELECT COUNT(*) FROM email_verification_tokens et WHERE et.user_id = (SELECT id FROM u) AND et.created_at >= NOW() - interval '24 hours') < 3
    RETURNING id
  )
SELECT 'completed' AS status,
       (SELECT COUNT(*) FROM email_verification_tokens et WHERE et.user_id = (SELECT id FROM u) AND et.created_at >= NOW() - interval '24 hours') AS tokens_in_24h,
       (SELECT id FROM t5) AS fifth_token_id

Test Case: Audit_logs: Immutable audit entries for system/admin actions plus uniqueness negative test

1. Error: insert or update on table "audit_logs" violates foreign key constraint "fk_audit_actor"

-- Positive: admin action with non-null actor and target (both generated UUIDs)
INSERT INTO audit_logs (audit_id, action, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
VALUES (gen_random_uuid(), 'admin.user.lockout', '{"reason":"policy_violation","duration":"24h"}'::jsonb, NOW(), '203.0.113.5', 'AuditTest/1.0', gen_random_uuid(), gen_random_uuid())

Test Case: End-to-end audit scenario: admin creates user, invites, accepts, updates, role changes, suspensions, logins, password resets, email verification, and export

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: 9a05b7ae-8663-40c9-bd1d-60b9e054a926
-- End-to-end audit scenario: admin creates user, invites, accepts, updates, role changes, suspensions, logins, password resets, email verification, and export
-- What this DML operation tests
DO $$
DECLARE
  admin_id UUID;
  invite_id UUID;
  bob_id UUID;
  reset_token_id UUID;
  export_id UUID;
BEGIN
  -- 1) Create admin user
  admin_id := gen_random_uuid();
  INSERT INTO users (
    id, role, email, email_normalized, status, timezone,
    last_name, anonymized, avatar_url, created_at, updated_at,
    mfa_enabled, failed_login_attempts, require_password_change,
    first_name, last_login_at, lockout_until, password_hash,
    mfa_secret_enc, email_verified_at, last_verification_sent_at,
    verification_resend_count_24h, display_name, profile_json, phone,
    note_admin, deleted_at, deleted_by
  ) VALUES (
    admin_id, 'admin'::role_enum, 'Admin@Acme.COM', 'admin@acme.com', 'active'::status_enum, 'UTC',
    NULL, false, NULL, now(), now(),
    false, 0, false,
    'Admin', NULL, NULL, NULL,
    NULL, NULL, NULL,
    0, 'Admin', NULL, NULL,
    NULL, NULL, NULL
  );

  -- 2) Log user.created for admin
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.created', gen_random_uuid(), jsonb_build_object('target_user_id', admin_id, 'email', 'Admin@Acme.COM', 'role', 'admin'), now(), NULL, NULL, NULL, admin_id);

  -- 3) Admin invites Bob
  invite_id := gen_random_uuid();
  INSERT INTO invitations (id, email, email_normalized, token, revoked, created_at, expires_at, inviter_user_id, accepted_at, revoked_by)
  VALUES (invite_id, 'Bob@example.com', 'bob@example.com', 'token-' || gen_random_uuid()::text, false, now(), now() + interval '7 days', admin_id, NULL, NULL);

  -- 4) Log user.invited
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.invited', gen_random_uuid(), jsonb_build_object('invitation_id', invite_id, 'email', 'Bob@example.com'), now(), NULL, NULL, admin_id, NULL);

  -- 5) Bob accepts invite: create Bob user
  bob_id := gen_random_uuid();
  INSERT INTO users (
    id, role, email, email_normalized, status, timezone,
    last_name, anonymized, avatar_url, created_at, updated_at,
    mfa_enabled, failed_login_attempts, require_password_change,
    first_name, last_login_at, lockout_until, password_hash,
    mfa_secret_enc, email_verified_at, last_verification_sent_at,
    verification_resend_count_24h, display_name, profile_json, phone,
    note_admin, deleted_at, deleted_by
  ) VALUES (
    bob_id, 'user'::role_enum, 'Bob@example.com', 'bob@example.com', 'active'::status_enum, 'UTC',
    NULL, false, NULL, now(), now(),
    false, 0, true,
    'Bob', NULL, NULL, NULL,
    NULL, NULL, NULL,
    0, NULL, NULL, NULL,
    NULL, NULL, NULL
  );

  -- 6) Mark invite accepted
  UPDATE invitations SET accepted_at = now() WHERE id = invite_id;
  -- 7) Log accepted_invite
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.accepted_invite', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'invitation_id', invite_id), now(), NULL, NULL, admin_id, bob_id);
  -- 8) Log user.created for Bob
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.created', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'email', 'Bob@example.com', 'role', 'user'), now(), NULL, NULL, admin_id, bob_id);

  -- 9) Update Bob: display name
  UPDATE users SET display_name = 'Bob The Builder', updated_at = now() WHERE id = bob_id;
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.updated', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'field', 'display_name', 'new_value', 'Bob The Builder'), now(), NULL, NULL, admin_id, bob_id);

  -- 10) Change Bob's role to admin
  UPDATE users SET role = 'admin'::role_enum, updated_at = now() WHERE id = bob_id;
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.role_changed', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'new_value', 'admin'), now(), NULL, NULL, admin_id, bob_id);

  -- 11) Suspend Bob
  UPDATE users SET status = 'suspended'::status_enum, lockout_until = now() + interval '2 hours', updated_at = now() WHERE id = bob_id;
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.suspended', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'new_value', 'suspended'), now(), NULL, NULL, admin_id, bob_id);

  -- 12) Reactivate Bob
  UPDATE users SET status = 'active'::status_enum, lockout_until = NULL, updated_at = now() WHERE id = bob_id;
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.reactivated', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'new_value', 'active'), now(), NULL, NULL, admin_id, bob_id);

  -- 13) Delete Bob
  UPDATE users SET deleted_at = now(), updated_at = now(), status = 'deleted'::status_enum WHERE id = bob_id;
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('user.deleted', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'deleted_at', now()), now(), NULL, NULL, admin_id, bob_id);

  -- 14) Login success for Bob
  INSERT INTO login_attempts (id, success, user_id, created_at, ip_address, user_agent, email_attempted)
  VALUES (gen_random_uuid(), true, bob_id, now(), '203.0.113.10', 'UnitTest/1.0', 'Bob@example.com');
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('login.success', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id), now(), '203.0.113.10', 'UnitTest/1.0', NULL, bob_id);

  -- 15) Login failure
  INSERT INTO login_attempts (id, success, user_id, created_at, ip_address, user_agent, email_attempted)
  VALUES (gen_random_uuid(), false, NULL, now(), '198.51.100.42', 'UnitTest/1.0', 'Unknown@example.com');
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('login.failure', gen_random_uuid(), jsonb_build_object('email_attempted', 'Unknown@example.com'), now(), '198.51.100.42', 'UnitTest/1.0', NULL, NULL);

  -- 16) Password reset requested for Bob
  reset_token_id := gen_random_uuid();
  INSERT INTO password_reset_tokens (id, token, used_at, user_id, created_at, expires_at)
  VALUES (reset_token_id, 'reset-' || gen_random_uuid()::text, NULL, bob_id, now(), now() + interval '60 minutes');
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('password.reset_requested', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'token', 'reset-' || gen_random_uuid()::text), now(), NULL, NULL, NULL, bob_id);

  -- 17) Password reset completed for Bob
  UPDATE users SET password_hash = 'hashed-bob-password', updated_at = now() WHERE id = bob_id;
  UPDATE password_reset_tokens SET used_at = now() WHERE id = reset_token_id;
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('password.reset_completed', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id), now(), NULL, NULL, NULL, bob_id);

  -- 18) Email verified for Bob
  UPDATE users SET email_verified_at = now(), updated_at = now() WHERE id = bob_id;
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('email.verified', gen_random_uuid(), jsonb_build_object('target_user_id', bob_id, 'email', 'Bob@example.com'), now(), NULL, NULL, NULL, bob_id);

  -- 19) Export performed
  export_id := gen_random_uuid();
  INSERT INTO exports (id, status, filters, file_url, created_at, actor_user_id)
  VALUES (export_id, 'completed', '{"entity":"users","status":["active"]}'::jsonb, '/exports/users_active.csv', now(), admin_id);
  INSERT INTO audit_logs (action, audit_id, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
  VALUES ('export.performed', gen_random_uuid(), jsonb_build_object('export_id', export_id, 'entity', 'users'), now(), NULL, NULL, admin_id, NULL);
END;
$$

Test Case: Audit logs filtering and CSV export: filter by action/actor/target/date, 90-day window, and export

1. Error: insert or update on table "audit_logs" violates foreign key constraint "fk_audit_actor"

-- Test Case: 4eed5b9f-83cd-4c9d-9a67-fabff8c085b8
-- Audit logs filtering and CSV export: filter by action/actor/target/date, 90-day window, and export
-- Tests the export path by copying filtered audit_logs rows to CSV output for actions admin.export and user.created within the last 90 days.
WITH
  actor AS (SELECT gen_random_uuid() AS actor_id),
  target AS (SELECT gen_random_uuid() AS target_id),
  a1 AS (SELECT gen_random_uuid() AS audit_id),
  a2 AS (SELECT gen_random_uuid() AS audit_id),
  a3 AS (SELECT gen_random_uuid() AS audit_id),
  a4 AS (SELECT gen_random_uuid() AS audit_id),
  a5 AS (SELECT gen_random_uuid() AS audit_id),
  a6 AS (SELECT gen_random_uuid() AS audit_id),
  a7 AS (SELECT gen_random_uuid() AS audit_id)
INSERT INTO audit_logs (audit_id, action, metadata, created_at, ip_address, user_agent, actor_user_id, target_user_id)
SELECT (SELECT audit_id FROM a1), 'admin.export', '{}'::jsonb, now() - interval '2 days', '203.0.113.1', 'Postgres', (SELECT actor_id FROM actor), NULL
UNION ALL
SELECT (SELECT audit_id FROM a2), 'user.created', '{}'::jsonb, now() - interval '10 days', '198.51.100.2', 'Postgres', (SELECT actor_id FROM actor), (SELECT target_id FROM target)
UNION ALL
SELECT (SELECT audit_id FROM a3), 'user.password_changed', '{}'::jsonb, now() - interval '95 days', '198.51.100.3', 'Postgres', (SELECT actor_id FROM actor), (SELECT target_id FROM target)
UNION ALL
SELECT (SELECT audit_id FROM a4), 'admin.login', '{}'::jsonb, now() - interval '5 days', NULL, NULL, (SELECT actor_id FROM actor), NULL
UNION ALL
SELECT (SELECT audit_id FROM a5), 'user.deleted', '{}'::jsonb, now() - interval '85 days', '203.0.113.4', 'Postgres', (SELECT actor_id FROM actor), (SELECT target_id FROM target)
UNION ALL
SELECT (SELECT audit_id FROM a6), 'admin.export', '{}'::jsonb, now() - interval '90 days', '203.0.113.5', 'Postgres', (SELECT actor_id FROM actor), NULL
UNION ALL
SELECT (SELECT audit_id FROM a7), 'user.updated', '{}'::jsonb, now() - interval '0 days', '203.0.113.6', 'Postgres', NULL, (SELECT target_id FROM target)

Test Case: Self-service profile data export as JSON

1. Error: invalid input value for enum role_enum: "user"

-- Insert a normal user with a complete profile (includes a sample profile_json payload)
INSERT INTO users (
  id, role, email, email_normalized, status, timezone, anonymized, created_at, updated_at, mfa_enabled,
  failed_login_attempts, require_password_change, verification_resend_count_24h, display_name, first_name, last_name, phone, avatar_url, profile_json, last_login_at, lockout_until, email_verified_at, last_verification_sent_at, password_hash, mfa_secret_enc
) VALUES (
  gen_random_uuid(), 'user', 'Alice@example.com', 'alice@example.com', 'active', 'UTC', false, now(), now(), true,
  0, false, 0, NULL, NULL, NULL, NULL, NULL, '{"preferences": {"theme": "dark"}, "bio": "Software engineer"}'::jsonb, NULL, NULL, NULL, NULL, NULL, NULL
)

Test Case: Admin-driven immediate anonymization of softly-deleted user bypassing 30-day retention

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: f6e8c994-f816-4868-a000-82a85c2fa81c
-- Admin-driven immediate anonymization of softly-deleted user bypassing 30-day retention
-- Set up a soft-deleted user (within 30 days retention) and perform admin-driven anonymization; verify fields are sanitized and anonymized flag is set.
WITH inserted AS (
  INSERT INTO users (
    id,
    role,
    email,
    email_normalized,
    status,
    timezone,
    anonymized,
    created_at,
    updated_at,
    mfa_enabled,
    failed_login_attempts,
    require_password_change,
    verification_resend_count_24h,
    display_name,
    deleted_at
  ) VALUES (
    gen_random_uuid(),
    CAST('admin' AS role_enum),
    'test.user.anonymize@example.com',
    LOWER('test.user.anonymize@example.com'),
    CAST('active' AS status_enum),
    'UTC',
    FALSE,
    NOW(),
    NOW(),
    TRUE,
    0,
    FALSE,
    0,
    'Test User Anonymize',
    NOW() - INTERVAL '3 days'
  )
  RETURNING id
),
anonymize AS (
  UPDATE users
  SET anonymized = TRUE,
      email = 'REDACTED',
      email_normalized = 'redacted',
      phone = NULL,
      last_name = NULL,
      first_name = NULL,
      display_name = NULL,
      avatar_url = NULL,
      profile_json = '{}'::jsonb,
      last_login_at = NULL,
      password_hash = NULL,
      mfa_secret_enc = NULL,
      note_admin = NULL
  WHERE id IN (SELECT id FROM inserted)
  RETURNING id
)
SELECT
  u.id,
  u.anonymized,
  u.email,
  u.email_normalized,
  u.first_name,
  u.last_name,
  u.display_name,
  u.phone,
  u.avatar_url,
  u.profile_json
FROM users u
WHERE u.id IN (SELECT id FROM anonymize)

Test Case: Audit logs retention and soft-delete anonymization lifecycle

1. Error: invalid input value for enum role_enum: "admin"

-- Test Case: b83ab108-5ab6-49e7-bb7e-a635e2ab9056
-- Audit logs retention and soft-delete anonymization lifecycle
-- Test data setup for retention verification, including an audit_log older than 1 year and a soft-deleted user older than 30 days; then perform anonymization and run verification queries.
-- 1) Seed data: an admin user and a soft-deleted user (older than 30 days)
INSERT INTO users (
  id, role, email, email_normalized, timezone, status, anonymized, created_at, updated_at, mfa_enabled, failed_login_attempts, require_password_change
) VALUES (
  gen_random_uuid(), 'admin', 'admin.user@example.com', 'admin.user@example.com', 'UTC', 'active', false, now(), now(), true, 0, false
)

Test Case: Admin: Validate user lifecycle counts including soft-deleted within last 30 days

1. Error: cannot truncate a table referenced in a foreign key constraint

-- Test Case: 5b0492a4-396a-442e-89da-31cdda65ac1b
-- Admin: Validate user lifecycle counts including soft-deleted within last 30 days
-- Inserts seven users with a mix of active, invited, suspended, and soft-deleted states, including soft-deletes within and outside a 30-day window. Validates total and categorized counts accordingly.
TRUNCATE TABLE users RESTART IDENTITY

Test Case: Weekly new registrations (last 8 weeks) summary and CSV export

1. Error: invalid input value for enum role_enum: "user"

-- Test Case: bc53b3de-9fae-4c2f-8e62-6b544db14b64
-- Weekly new registrations (last 8 weeks) summary and CSV export
-- This DML inserts test users across the last 8 weeks (2 per week) plus one older record outside the 8-week window to test boundary logic. It then runs a weekly summary for the last 8 weeks and exports the underlying dataset to CSV to validate admin export capability.
-- Older record outside the 8-week window (to test boundary exclusion)
INSERT INTO users (id, role, email, email_normalized, status, timezone, anonymized, created_at, updated_at, mfa_enabled, failed_login_attempts, verification_resend_count_24h)
VALUES
  (gen_random_uuid(), 'user'::role_enum, 'older.user@example.com', lower('older.user@example.com'), 'active'::status_enum, 'UTC', false, now() - interval '9 weeks', now() - interval '9 weeks', true, 0, 0)

Test Case: Admin API Access: List, Retrieve, Create/Invite, Update Profile, Change Role/Status, and Export Users

1. Error: invalid input value for enum role_enum: "admin"

WITH admin_row AS (
  INSERT INTO users (
    id, role, email, email_normalized, status, timezone, last_name, anonymized, mfa_enabled, created_at, updated_at, failed_login_attempts, require_password_change, verification_resend_count_24h
  ) VALUES (
    gen_random_uuid(), 'admin'::role_enum, 'admin.user@example.com', lower('admin.user@example.com'), 'active'::status_enum, 'UTC', 'Admin', false, true, now(), now(), 0, false, 0
  ) RETURNING id
),
invite_row AS (
  INSERT INTO invitations (
    id, email, token, revoked, created_at, expires_at, inviter_user_id, email_normalized
  ) VALUES (
    gen_random_uuid(), 'new.user@example.com', 'invite-token-abc', false, now(), now() + interval '7 days', (SELECT id FROM admin_row), lower('new.user@example.com')
  ) RETURNING id
),
new_user AS (
  INSERT INTO users (
    id, role, email, email_normalized, status, timezone, last_name, anonymized, mfa_enabled, created_at, updated_at, failed_login_attempts, require_password_change, verification_resend_count_24h
  ) VALUES (
    gen_random_uuid(), 'user'::role_enum, 'new.user@example.com', lower('new.user@example.com'), 'active'::status_enum, 'UTC', NULL, false, false, now(), now(), 0, false, 0
  ) RETURNING id
),
list_users AS (
  SELECT id, email, email_normalized, status, role, created_at FROM users ORDER BY created_at LIMIT 10
),
retrieve_admin AS (
  SELECT id, email FROM users WHERE id IN (SELECT id FROM admin_row)
),
update_profile AS (
  UPDATE users
  SET first_name = 'New', last_name = 'Admin', display_name = 'Admin User', updated_at = now()
  WHERE id = (SELECT id FROM admin_row)
  RETURNING *
),
change_role_status AS (
  UPDATE users
  SET role = 'admin'::role_enum, status = 'inactive'::status_enum, updated_at = now()
  WHERE id = (SELECT id FROM new_user)
  RETURNING *
),
export_row AS (
  INSERT INTO exports (id, status, filters, file_url, created_at, actor_user_id)
  VALUES (gen_random_uuid(), 'completed', '{"export":"users","scope":"all"}'::jsonb, 'https://example.com/users.csv', now(), (SELECT id FROM admin_row))
  RETURNING id
)
SELECT
  (SELECT count(*) FROM list_users) AS listed_users,
  (SELECT email FROM retrieve_admin) AS admin_email,
  (SELECT id FROM export_row) AS export_id

⏺ (lead):

Concise recap of what was accomplished

  • Main user requirements analyzed

    • User lifecycle: registration, self-service profile management, invitations, activation, suspended/active states, soft delete with eventual purge.
    • Authentication and security: password hashing, password reset, email verification, MFA (optional) with recovery codes, lockout policies, session expiry.
    • Authorization: two roles (Admin, Member) with Admins able to manage users and Members limited to their own profile.
    • Admin console capabilities: search, filter, export, audit logs, and programmatic API access for user management.
    • Data governance and privacy: data retention, anonymization on purge, user data export, and audit trail retention.
    • Reporting and metrics: counts (total, Active, Invited, Suspended, soft-deleted), weekly registrations, and exports.
    • Accessibility and localization: English UI and email templates; timezone-aware date rendering.
  • Key database design decisions

    • Enumerations:
      • role_enum with values Admin, Member
      • status_enum with values Invited, Active, Suspended, Deleted
    • Core user model:
      • users table with UUID id, email and email_normalized (case-insensitive lookups), password_hash, profile_json, MFA fields, status, role, timestamps, soft-delete fields (deleted_at, anonymized), and retention controls (email_verified_at, last_login_at, failed_login_attempts, lockout_until, require_password_change, etc.).
    • Email handling:
      • email stored with original casing (email) and a normalized lowercase (email_normalized) for lookups and uniqueness.
      • Unique constraint on email_normalized; index on email_normalized; normalized comparisons ensure case-insensitive uniqueness.
    • Invitations and verification:
      • invitations table for onboarding with token, expires, invited_by reference (inviter_user_id), and email_normalized for lookups.
      • email_verification_tokens table to handle email confirmations and email-change flows.
      • password_reset_tokens table for password resets.
    • Security and MFA:
      • mfa_enabled flag and mfa_secret_enc for TOTP secret storage; mfa_recovery_codes (hashes) per user.
    • Sessions and access:
      • sessions table with session_token, user_id, created_at, last_activity_at, absolute_expires_at, and inactivity-based expiry logic.
    • Audit and observability:
      • audit_logs table with immutable audit entries (audit_id, actor_user_id, target_user_id, action, metadata, ip_address, user_agent, created_at).
    • Admin actions and exports:
      • exports table to track admin-initiated CSV exports with filters, status, and file_url.
    • Data governance schemas:
      • profile_json for extensible user attributes.
      • anonymization flag (anonymized) and fields to purge/redact during anonymization.
    • Indexing strategy:
      • primary indexes on pk and unique indexes where appropriate (e.g., idx_users_email_normalized, idx_users_created_at, idx_users_last_login_at).
    • Testing scaffolding:
      • Extensive DML/test cases created to exercise registration flows, updates, invites, email changes with verification, lockout, session expiry, MFA flows, auditing, privacy/purge, and admin-driven operations.
  • Schemas, tables, or data structures created or modified

    • Enums added:
      • /enums/role_enum (Admin, Member)
      • /enums/status_enum (Invited, Active, Suspended, Deleted)
    • Tables added:
      • users
      • invitations
      • email_verification_tokens
      • password_reset_tokens
      • sessions
      • mfa_recovery_codes
      • login_attempts
      • audit_logs
      • exports
    • Additional supporting structures in tests:
      • JSONB profile_json fields for extensibility
      • Various foreign key relationships (e.g., invitations.inviter_user_id -> users.id, audit_logs actor/target references)
    • Indexes created for lookup performance:
      • idx_users_email_normalized, idx_users_created_at, idx_users_last_login_at
      • others on foreign keys and tokens where applicable
  • Important outcomes or results

    • Database schema updated and validated; 147/147 DDL statements executed; new version created.
    • A large set of test cases were saved to validate the design, including:
      • Self-registration, email verification, password resets, MFA, invitations, email change flows, and admin-assisted onboarding.
      • Admin console workflows: user search, detail view, edit, resend invitations, export, and audit visibility.
      • RBAC enforcement tests (Admin vs Member actions, admin-only role changes).
      • Data governance tests: soft-delete, anonymization, retention windows, and privacy controls.
      • Auditing and reporting tests: filterable audit logs with CSV export, retention rules, and retention-based anonymization.
      • API/integration-oriented tests: listing, retrieving, updating, and exporting users; pagination and metadata handling.
    • Some test scripts encountered issues during execution:
      • Several failures due to mismatches between test data and defined enums (e.g., using role 'user' when only Admin/Member exist in role_enum).
      • Case sensitivity and normalization mismatches surfaced (e.g., inconsistency between input role values and enum constraints).
    • Overall outcome: a robust, RBAC-driven, auditable, and governance-aware user management schema with comprehensive admin tooling and test coverage, plus identified gaps to align test data with the defined enums and constraints.

Next steps (suggested)

  • Align test scripts with the defined enums (use Admin/Member for role values; ensure uppercase/lowercase consistency with enum definitions).
  • Validate all test DML against the final schema to avoid enum/type mismatches.
  • Consider adding explicit NOT NULL constraints where needed for critical fields and ensure test data respects all FK relationships.
  • Iterate on test coverage to close any gaps where tests fail due to data-model mismatches rather than design issues.
  • Implement a focused regression suite to validate critical flows (registration, invitation, activation, email verification, password reset, MFA, audit logging, and anonymization) against the final schema.·

Test Files 1 passed (1)
Tests 1 passed (1)
Start at 11:24:15
Duration 467.05s (transform 440ms, setup 0ms, collect 1.31s, tests 465.43s, environment 0ms, prepare 103ms)

Status: Completed successfully

Finished at: 2025-09-26 11:32:03 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant