Savvi Studio

Factories and Utilities Reference

Complete reference for all factory functions and utility helpers in the integration test infrastructure

Table of Contents

Overview

What are Factories?

Factories are functions that create database entities with sensible defaults. They handle:

  • ✅ Node type creation
  • ✅ Data initialization with defaults
  • ✅ Relationship setup (edges)
  • ✅ Permission grants
  • ✅ ID generation

Location: tests/integration/setup/factories/

What are Utilities?

Utilities are functions that query or clean up database state. They handle:

  • ✅ Common queries (access cache, permissions)
  • ✅ Permission checks
  • ✅ Cleanup operations
  • ✅ Data introspection

Location: tests/integration/setup/utilities/

Import All at Once

// Import everything you need
import {
  // Factories
  createUserWithOrgSetup,
  createResourceWithPermission,
  setupTestWorkspace,
  ensureNodeType,
  
  // Utilities
  queryUserAccessCache,
  refreshUserAccessCache,
  checkUserHasAccess,
  cleanupTestData
} from '../setup/factories/database-fixtures';

import {
  // Query helpers
  queryUserAccessCache,
  checkUserHasAccess,
  getCurrentUserId,
  
  // Cleanup helpers
  cleanupTestData,
  deleteNodesByType
} from '../setup/utilities';

// WorkOS factories
import {
  createUserEvent,
  createOrgEvent,
  createMembershipEvent
} from '../setup/factories/workos-events';

Factory Functions

Database Fixture Factories

All functions in tests/integration/setup/factories/database-fixtures.ts

ensureNodeType

Ensure a node type exists in the database (idempotent).

Signature:

function ensureNodeType(
  client: PoolClient,
  nodeTypeId: string,
  name?: string
): Promise<void>

Parameters:

  • client - Database client
  • nodeTypeId - ltree node type ID (e.g., 'test.resource')
  • name - Optional human-readable name

Example:

await ensureNodeType(client, 'test.resource', 'Resource');
await ensureNodeType(client, 'platform.user'); // name derived as 'user'

When to use:

  • Before creating nodes of a new type
  • In test setup to ensure types exist
  • Prefer createNodeWithType() which calls this automatically

ensureNodeTypes

Ensure multiple node types exist (batch operation).

Signature:

function ensureNodeTypes(
  client: PoolClient,
  types: Array<[string, string?]>
): Promise<void>

Parameters:

  • client - Database client
  • types - Array of [nodeTypeId, name?] tuples

Example:

await ensureNodeTypes(client, [
  ['test.resource', 'Resource'],
  ['test.project', 'Project'],
  ['platform.team']  // name will be 'team'
]);

When to use:

  • Setting up multiple node types at once
  • Test initialization
  • More efficient than multiple ensureNodeType() calls

createResourceWithPermission

Create a resource node and grant permission to a user.

Signature:

function createResourceWithPermission(
  client: PoolClient,
  options: {
    userNodeId: string;
    resourceType?: string;
    permissionLevel?: 'read' | 'write' | 'admin';
    resourceData?: Record<string, unknown>;
    externalId?: string;
  }
): Promise<{
  resourceId: string;
  resourceNodeId: string;
  permissionId: string;
}>

Parameters:

  • userNodeId - User node ID to grant permission to (required)
  • resourceType - Resource node type (default: 'test.resource')
  • permissionLevel - Permission level (default: 'read')
  • resourceData - Resource data object (default: {})
  • externalId - Optional external ID

Returns:

  • resourceId - External ID or node ID
  • resourceNodeId - Database node ID
  • permissionId - Permission edge ID

Example:

const { resourceNodeId, permissionId } = await createResourceWithPermission(
  client,
  {
    userNodeId: user.nodeId,
    resourceType: 'test.document',
    permissionLevel: 'write',
    resourceData: { 
      title: 'My Document',
      status: 'draft',
      created_at: new Date().toISOString()
    }
  }
);

When to use:

  • Testing permission grants
  • Creating user-owned resources
  • Permission verification tests
  • Access control scenarios

createUserWithOrgSetup

Create a user with organization and optional team.

Signature:

function createUserWithOrgSetup(
  client: PoolClient,
  options?: {
    userId?: string;
    orgId?: string;
    userData?: Record<string, unknown>;
    orgData?: Record<string, unknown>;
    includeTeam?: boolean;
    teamName?: string;
  }
): Promise<{
  user: { id: string; nodeId: string; data: Record<string, unknown> };
  org: { id: string; nodeId: string; data: Record<string, unknown> };
  team?: { id: string; nodeId: string; data: Record<string, unknown> };
}>

Parameters:

  • userId - User external ID (auto-generated if not provided)
  • orgId - Organization external ID (auto-generated if not provided)
  • userData - User data overrides
  • orgData - Organization data overrides
  • includeTeam - Whether to create a team (default: false)
  • teamName - Team name if includeTeam is true (default: 'Test Team')

Returns:

  • user - User info (id, nodeId, data)
  • org - Organization info (id, nodeId, data)
  • team - Team info if includeTeam was true

Example:

// Basic usage
const { user, org } = await createUserWithOrgSetup(client);

// With custom data
const { user, org } = await createUserWithOrgSetup(client, {
  userId: 'user_123',
  orgId: 'org_456',
  userData: { 
    email: 'john@example.com',
    first_name: 'John',
    last_name: 'Doe'
  },
  orgData: { 
    name: 'Acme Corp',
    domains: ['acme.com']
  }
});

// With team
const { user, org, team } = await createUserWithOrgSetup(client, {
  includeTeam: true,
  teamName: 'Engineering'
});

When to use:

  • Most common starting point for tests
  • User authentication tests
  • Permission hierarchy tests
  • Organization membership tests

setupTestWorkspace

Create a complete test workspace with user, org, optional team, and resources.

Signature:

function setupTestWorkspace(
  client: PoolClient,
  options?: {
    resourceCount?: number;
    resourceType?: string;
    resourcePermission?: 'read' | 'write' | 'admin';
    includeTeam?: boolean;
    userData?: Record<string, unknown>;
    orgData?: Record<string, unknown>;
  }
): Promise<{
  user: { id: string; nodeId: string; data: Record<string, unknown> };
  org: { id: string; nodeId: string; data: Record<string, unknown> };
  team?: { id: string; nodeId: string; data: Record<string, unknown> };
  resources: Array<{ id: string; nodeId: string; permissionId: string }>;
}>

Parameters:

  • resourceCount - Number of resources to create (default: 0)
  • resourceType - Type for created resources (default: 'test.resource')
  • resourcePermission - Permission level for resources (default: 'read')
  • includeTeam - Create a team (default: false)
  • userData - User data overrides
  • orgData - Organization data overrides

Returns:

  • user - User info
  • org - Organization info
  • team - Team info if includeTeam was true
  • resources - Array of created resources

Example:

const workspace = await setupTestWorkspace(client, {
  includeTeam: true,
  resourceCount: 5,
  resourceType: 'test.document',
  resourcePermission: 'write',
  userData: { role: 'developer' },
  orgData: { plan: 'enterprise' }
});

// Access created entities
console.log(workspace.user.id);
console.log(workspace.resources.length); // 5

When to use:

  • Complex test scenarios
  • Testing with multiple resources
  • Performance testing
  • End-to-end workflows

createNodeWithType

Create a node with automatic node type setup.

Signature:

function createNodeWithType(
  client: PoolClient,
  nodeType: string,
  data?: Record<string, unknown>,
  options?: {
    externalId?: string;
    typeName?: string;
  }
): Promise<{
  id: string;
  nodeId: string;
  data: Record<string, unknown>;
}>

Parameters:

  • client - Database client
  • nodeType - Node type ID
  • data - Node data (default: {})
  • options.externalId - Optional external ID
  • options.typeName - Optional type name

Returns:

  • id - External ID
  • nodeId - Database node ID
  • data - Node data

Example:

const node = await createNodeWithType(client, 'test.document', {
  title: 'My Document',
  status: 'draft',
  word_count: 1500
}, {
  externalId: 'doc_123'
});

When to use:

  • Creating custom node types
  • Generic node creation
  • When you need the node ID immediately

ensureEdgeType

Ensure an edge type exists in the database (idempotent).

Signature:

function ensureEdgeType(
  client: PoolClient,
  edgeTypeId: string,
  name?: string
): Promise<void>

Parameters:

  • client - Database client
  • edgeTypeId - ltree edge type ID (e.g., 'test.links')
  • name - Optional human-readable name

Example:

await ensureEdgeType(client, 'test.links', 'Links');
await ensureEdgeType(client, 'test.depends_on');

When to use:

  • Before creating edges of a new type
  • Prefer createConnectedNodes() which calls this automatically

createConnectedNodes

Create two nodes connected by an edge.

Signature:

function createConnectedNodes(
  client: PoolClient,
  options: {
    sourceType: string;
    targetType: string;
    edgeType: string;
    sourceData?: Record<string, unknown>;
    targetData?: Record<string, unknown>;
    edgeData?: Record<string, unknown>;
  }
): Promise<{
  source: { id: string; nodeId: string };
  target: { id: string; nodeId: string };
  edge: { id: string };
}>

Parameters:

  • sourceType - Source node type (required)
  • targetType - Target node type (required)
  • edgeType - Edge type connecting them (required)
  • sourceData - Source node data
  • targetData - Target node data
  • edgeData - Edge data

Returns:

  • source - Source node info
  • target - Target node info
  • edge - Edge info

Example:

const { source, target, edge } = await createConnectedNodes(client, {
  sourceType: 'test.person',
  targetType: 'test.company',
  edgeType: 'test.works_at',
  sourceData: { name: 'Alice', title: 'Engineer' },
  targetData: { name: 'Acme Corp', industry: 'Software' },
  edgeData: { start_date: '2024-01-01' }
});

When to use:

  • Testing graph relationships
  • Edge traversal tests
  • Relationship queries
  • Graph structure tests

WorkOS Event Factories

All functions in tests/integration/setup/factories/workos-events.ts

createUserEvent

Create a WorkOS user event (user.created, user.updated, user.deleted).

Signature:

function createUserEvent(
  type: 'user.created' | 'user.updated' | 'user.deleted',
  userId: string,
  overrides?: Partial<{
    email: string;
    firstName: string;
    lastName: string;
    emailVerified: boolean;
    profilePictureUrl: string | null;
    externalId: string | null;
    metadata: Record<string, string>;
    createdAt: string;
    updatedAt: string;
    lastSignInAt: string | null;
  }>,
  eventOverrides?: Partial<{
    id: string;
    createdAt: string;
  }>
): Event

Example:

const event = createUserEvent('user.created', 'user_01234', {
  email: 'john@example.com',
  firstName: 'John',
  lastName: 'Doe',
  emailVerified: true
});

// Process webhook
await processUserWebhook(event);

When to use:

  • Testing user webhook processing
  • User sync scenarios
  • User lifecycle events

createOrgEvent

Create a WorkOS organization event.

Signature:

function createOrgEvent(
  type: 'organization.created' | 'organization.updated' | 'organization.deleted',
  orgId: string,
  overrides?: Partial<{
    name: string;
    allowProfilesOutsideOrganization: boolean;
    domains: any[];
    externalId: string | null;
    metadata: Record<string, string>;
    createdAt: string;
    updatedAt: string;
  }>,
  eventOverrides?: Partial<{
    id: string;
    createdAt: string;
  }>
): Event

Example:

const event = createOrgEvent('organization.created', 'org_01234', {
  name: 'Acme Corporation',
  domains: [{ domain: 'acme.com' }],
  allowProfilesOutsideOrganization: false
});

When to use:

  • Testing organization webhook processing
  • Organization sync scenarios
  • Domain verification tests

createMembershipEvent

Create a WorkOS membership event.

Signature:

function createMembershipEvent(
  type: 'organization_membership.created' | 'organization_membership.updated' | 'organization_membership.deleted',
  membershipId: string,
  userId: string,
  organizationId: string,
  overrides?: Partial<{
    organizationName: string;
    status: 'active' | 'inactive';
    role: { slug: string };
    createdAt: string;
    updatedAt: string;
  }>,
  eventOverrides?: Partial<{
    id: string;
    createdAt: string;
  }>
): Event

Example:

const event = createMembershipEvent(
  'organization_membership.created',
  'membership_01234',
  'user_01234',
  'org_01234',
  {
    role: { slug: 'admin' },
    status: 'active'
  }
);

When to use:

  • Testing membership webhooks
  • Role assignment tests
  • Membership lifecycle events

createRoleEvent

Create a WorkOS role event.

Signature:

function createRoleEvent(
  type: 'role.created' | 'role.updated' | 'role.deleted',
  roleSlug: string,
  overrides?: Partial<{
    permissions: string[];
    createdAt: string;
    updatedAt: string;
  }>,
  eventOverrides?: Partial<{
    id: string;
    createdAt: string;
  }>
): Event

Example:

const event = createRoleEvent('role.created', 'engineer', {
  permissions: ['read', 'write', 'deploy']
});

When to use:

  • Testing role webhooks
  • Permission mapping tests
  • Role-based access control

Utility Functions

Query Helper Utilities

All functions in tests/integration/setup/utilities/query-helpers.ts

queryUserAccessCache

Query the user access cache for a specific user.

Signature:

function queryUserAccessCache(
  client: PoolClient,
  userId: string,
  nodeId?: string
): Promise<CacheAccessRecord[]>

interface CacheAccessRecord {
  user_external_id: string;
  node_id: string;
  min_permission: string;
  depth: number;
  paths?: string[];
}

Example:

// Query all access for user
const access = await queryUserAccessCache(client, 'user_123');
expect(access.length).toBeGreaterThan(0);

// Query specific node
const nodeAccess = await queryUserAccessCache(client, 'user_123', nodeId);
expect(nodeAccess[0]?.min_permission).toBe('auth.read');

refreshUserAccessCache

Refresh the user access cache for a specific user.

Signature:

function refreshUserAccessCache(
  client: PoolClient,
  userId: string
): Promise<boolean>

Example:

// Grant permission
await grantPermission(client, userNodeId, resourceNodeId, 'write');

// Refresh cache to pick up new permission
await refreshUserAccessCache(client, userId);

// Verify
const access = await queryUserAccessCache(client, userId, resourceNodeId);
expect(access.length).toBeGreaterThan(0);

checkUserHasAccess

Check if user has access to a node.

Signature:

function checkUserHasAccess(
  client: PoolClient,
  nodeId: string
): Promise<boolean>

Example:

await asUser(userId, orgId, async (client) => {
  const hasAccess = await checkUserHasAccess(client, resourceId);
  expect(hasAccess).toBe(true);
});

checkUserCanManage

Check if user can manage a node (write permission).

Signature:

function checkUserCanManage(
  client: PoolClient,
  nodeId: string
): Promise<boolean>

Example:

const canManage = await checkUserCanManage(client, resourceId);
expect(canManage).toBe(true);

checkUserIsAdmin

Check if user is admin for a node.

Signature:

function checkUserIsAdmin(
  client: PoolClient,
  nodeId: string
): Promise<boolean>

Example:

const isAdmin = await checkUserIsAdmin(client, resourceId);
expect(isAdmin).toBe(false);

checkPermissionSatisfies

Check if one permission satisfies another (hierarchy check).

Signature:

function checkPermissionSatisfies(
  client: PoolClient,
  granted: string,
  required: string
): Promise<boolean>

Example:

// Check if admin satisfies read
const satisfies = await checkPermissionSatisfies(
  client,
  'auth.read.write.admin',
  'auth.read'
);
expect(satisfies).toBe(true);

getCurrentUserId

Get the current session user ID.

Signature:

function getCurrentUserId(
  client: PoolClient
): Promise<string | null>

Example:

await asUser(userId, orgId, async (client) => {
  const currentId = await getCurrentUserId(client);
  expect(currentId).toBe(userId);
});

getCurrentRole

Get the current database role.

Signature:

function getCurrentRole(
  client: PoolClient
): Promise<string>

Example:

const role = await getCurrentRole(client);
expect(role).toBe('studio_user');

countRecords

Count records in a table with optional filter.

Signature:

function countRecords(
  client: PoolClient,
  tableName: string,
  whereClause?: string,
  params?: unknown[]
): Promise<number>

Example:

const count = await countRecords(
  client,
  'auth.user_access_cache',
  'user_external_id = $1',
  ['user_123']
);

functionExists

Check if a database function exists.

Signature:

function functionExists(
  client: PoolClient,
  functionName: string,
  schema?: string
): Promise<boolean>

Example:

const exists = await functionExists(client, 'user_has_access', 'auth');
expect(exists).toBe(true);

roleExists

Check if a database role exists.

Signature:

function roleExists(
  client: PoolClient,
  roleName: string
): Promise<boolean>

Example:

const exists = await roleExists(client, 'studio_user');
expect(exists).toBe(true);

getNodesByType

Get all nodes of a specific type.

Signature:

function getNodesByType(
  client: PoolClient,
  nodeTypeId: string
): Promise<QueryResult['rows']>

Example:

const nodes = await getNodesByType(client, 'platform.user');
expect(nodes.length).toBeGreaterThan(0);

getEdgesByType

Get all edges of a specific type.

Signature:

function getEdgesByType(
  client: PoolClient,
  edgeTypeId: string
): Promise<QueryResult['rows']>

Example:

const edges = await getEdgesByType(client, 'auth.read');
expect(edges.length).toBeGreaterThan(0);

getEdgesBetweenNodes

Get edges between two nodes.

Signature:

function getEdgesBetweenNodes(
  client: PoolClient,
  sourceNodeId: string,
  targetNodeId: string,
  edgeTypeId?: string
): Promise<QueryResult['rows']>

Example:

const edges = await getEdgesBetweenNodes(
  client,
  userNodeId,
  orgNodeId,
  'auth.read.member'
);
expect(edges.length).toBe(1);

Cleanup Helper Utilities

All functions in tests/integration/setup/utilities/cleanup-helpers.ts

deleteNodesByType

Delete all nodes of a specific type.

Signature:

function deleteNodesByType(
  client: PoolClient,
  nodeTypeId: string
): Promise<number>

Example:

const deleted = await deleteNodesByType(client, 'test.resource');
console.log(`Deleted ${deleted} nodes`);

deleteEdgesByType

Delete all edges of a specific type.

Signature:

function deleteEdgesByType(
  client: PoolClient,
  edgeTypeId: string
): Promise<number>

Example:

const deleted = await deleteEdgesByType(client, 'auth.read');

deleteEdge

Delete a specific edge by ID.

Signature:

function deleteEdge(
  client: PoolClient,
  edgeId: string
): Promise<boolean>

Example:

const deleted = await deleteEdge(client, edgeId);
expect(deleted).toBe(true);

deleteNode

Delete a specific node by ID.

Signature:

function deleteNode(
  client: PoolClient,
  nodeId: string
): Promise<boolean>

Example:

const deleted = await deleteNode(client, nodeId);
expect(deleted).toBe(true);

deleteEdgesBetweenNodes

Delete edges between two nodes.

Signature:

function deleteEdgesBetweenNodes(
  client: PoolClient,
  sourceNodeId: string,
  targetNodeId: string,
  edgeTypeId?: string
): Promise<number>

Example:

const deleted = await deleteEdgesBetweenNodes(
  client,
  userNodeId,
  orgNodeId,
  'auth.read.member'
);

clearUserAccessCache

Clear user access cache for a specific user.

Signature:

function clearUserAccessCache(
  client: PoolClient,
  userId: string
): Promise<number>

Example:

const deleted = await clearUserAccessCache(client, 'user_123');

clearAllUserAccessCache

Clear all user access cache entries (use with caution).

Signature:

function clearAllUserAccessCache(
  client: PoolClient
): Promise<number>

Example:

await clearAllUserAccessCache(client);

cleanupByExternalIdPattern

Cleanup test data by external ID pattern.

Signature:

function cleanupByExternalIdPattern(
  client: PoolClient,
  pattern: string
): Promise<number>

Example:

// Clean up all test IDs starting with 'test_'
const deleted = await cleanupByExternalIdPattern(client, 'test_%');

cleanupTestData

Delete test nodes and edges (composite cleanup).

Signature:

function cleanupTestData(
  client: PoolClient,
  options: {
    nodeTypes?: string[];
    edgeTypes?: string[];
    externalIdPattern?: string;
  }
): Promise<{ nodes: number; edges: number }>

Example:

const result = await cleanupTestData(client, {
  nodeTypes: ['test.resource', 'test.document'],
  edgeTypes: ['test.links'],
  externalIdPattern: 'test_%'
});
console.log(`Deleted ${result.nodes} nodes, ${result.edges} edges`);

truncateTable

Truncate a table (removes all rows).

Signature:

function truncateTable(
  client: PoolClient,
  tableName: string,
  cascade?: boolean
): Promise<boolean>

Example:

await truncateTable(client, 'test_schema.test_table', true);

Decision Guide

When to Use What?

Creating Data

Scenario Tool Example
Create user + org createUserWithOrgSetup() Most tests start here
Create resource with permission createResourceWithPermission() Permission tests
Create complete workspace setupTestWorkspace() Complex scenarios
Create custom node createNodeWithType() Generic nodes
Create connected nodes createConnectedNodes() Graph relationships

Querying Data

Scenario Tool Example
Check user access queryUserAccessCache() After granting permission
Verify permission level checkUserHasAccess() In user context
Check management rights checkUserCanManage() Before updates
Check admin rights checkUserIsAdmin() Before deletes
Get current user getCurrentUserId() Verify context
Find nodes by type getNodesByType() Search operations

Cleaning Up

Scenario Tool Example
Clean specific types deleteNodesByType() Targeted cleanup
Clean test pattern cleanupByExternalIdPattern() Pattern-based
Comprehensive cleanup cleanupTestData() Composite cleanup
Clear cache clearUserAccessCache() Cache invalidation
Delete specific entity deleteNode() / deleteEdge() Single entity

Migration Patterns

Before: Manual Node Creation

Old Pattern:

await client.query(`
  INSERT INTO graph.node_types (id, name)
  VALUES ('test.resource'::ltree, 'Resource')
  ON CONFLICT (id) DO NOTHING
`);

const result = await client.query(`
  INSERT INTO graph.nodes (node_type_id, external_id, data)
  VALUES ('test.resource'::ltree, $1, $2)
  RETURNING id
`, [resourceId, { name: 'Resource' }]);

const nodeId = result.rows[0].id;

New Pattern:

const { nodeId } = await createNodeWithType(client, 'test.resource', {
  name: 'Resource'
}, { externalId: resourceId });

Before: Manual Permission Grant

Old Pattern:

// Create node
const nodeResult = await client.query(`...`);
const nodeId = nodeResult.rows[0].id;

// Create permission edge
await client.query(`
  INSERT INTO graph.edge (edge_type_id, source_node_id, target_node_id, data)
  VALUES ('auth.read'::ltree, $1, $2, '{}')
`, [userNodeId, nodeId]);

// Refresh cache
await client.query('SELECT auth.refresh_user_access_cache($1)', [userId]);

New Pattern:

const { resourceNodeId } = await createResourceWithPermission(client, {
  userNodeId,
  permissionLevel: 'read'
});

await refreshUserAccessCache(client, userId);

Before: Manual User Setup

Old Pattern:

// Insert node types
await client.query(`...node types...`);

// Create user node
const userResult = await client.query(`...insert user...`);
const userNodeId = userResult.rows[0].id;

// Create org node
const orgResult = await client.query(`...insert org...`);
const orgNodeId = orgResult.rows[0].id;

// Create membership edge
await client.query(`...insert edge...`);

New Pattern:

const { user, org } = await createUserWithOrgSetup(client, {
  userId: 'user_123',
  orgId: 'org_456'
});

Before: Manual Cleanup

Old Pattern:

await client.query('DELETE FROM graph.edge WHERE edge_type_id = $1::ltree', ['test.links']);
await client.query('DELETE FROM graph.nodes WHERE node_type_id = $1::ltree', ['test.resource']);
await client.query('DELETE FROM auth.user_access_cache WHERE user_external_id LIKE $1', ['test_%']);

New Pattern:

const result = await cleanupTestData(client, {
  nodeTypes: ['test.resource'],
  edgeTypes: ['test.links'],
  externalIdPattern: 'test_%'
});

Before: Manual Cache Query

Old Pattern:

const result = await client.query(`
  SELECT * FROM auth.user_access_cache
  WHERE user_external_id = $1 AND node_id = $2
`, [userId, nodeId]);

const access = result.rows;

New Pattern:

const access = await queryUserAccessCache(client, userId, nodeId);

Common Workflows

Complete Test Workflow

Here's a complete example combining factories, utilities, and best practices:

import { describe, expect } from 'vitest';
import test from '../test';
import { generateTestId } from '@/test-utils-integration/utils/graph-fixtures';
import {
  createUserWithOrgSetup,
  createResourceWithPermission,
  setupTestWorkspace
} from '../setup/factories/database-fixtures';
import {
  queryUserAccessCache,
  refreshUserAccessCache,
  checkUserHasAccess,
  cleanupTestData
} from '../setup/utilities';

describe('Document Sharing', () => {
  test('user can share document with team member', async ({ asSystem, asUser }) => {
    let owner: any;
    let member: any;
    let documentId: string;
    
    // Setup: Create test environment
    await asSystem(async (client) => {
      // Create owner with organization
      owner = await createUserWithOrgSetup(client, {
        userId: generateTestId('owner'),
        orgId: generateTestId('org'),
        userData: { role: 'owner' }
      });
      
      // Create team member in same org
      member = await createUserWithOrgSetup(client, {
        userId: generateTestId('member'),
        orgId: owner.org.id, // Same org
        userData: { role: 'member' }
      });
      
      // Owner creates a document
      const { resourceNodeId } = await createResourceWithPermission(client, {
        userNodeId: owner.user.nodeId,
        resourceType: 'test.document',
        permissionLevel: 'admin',
        resourceData: {
          title: 'Shared Document',
          content: 'Secret content'
        }
      });
      documentId = resourceNodeId;
      
      // Refresh caches
      await refreshUserAccessCache(client, owner.user.id);
      await refreshUserAccessCache(client, member.user.id);
    });
    
    // Test: Owner can access document
    await asUser(owner.user.id, owner.org.id, async (client) => {
      const hasAccess = await checkUserHasAccess(client, documentId);
      expect(hasAccess).toBe(true);
    });
    
    // Test: Member initially cannot access
    await asUser(member.user.id, member.org.id, async (client) => {
      const hasAccess = await checkUserHasAccess(client, documentId);
      expect(hasAccess).toBe(false);
    });
    
    // Action: Owner shares with member
    await asSystem(async (client) => {
      await createResourceWithPermission(client, {
        userNodeId: member.user.nodeId,
        resourceType: 'test.document',
        permissionLevel: 'read',
        resourceData: {}, // Existing document
        externalId: documentId
      });
      
      await refreshUserAccessCache(client, member.user.id);
    });
    
    // Verify: Member can now access
    await asUser(member.user.id, member.org.id, async (client) => {
      const hasAccess = await checkUserHasAccess(client, documentId);
      expect(hasAccess).toBe(true);
      
      // Verify permission level
      const access = await queryUserAccessCache(client, member.user.id, documentId);
      expect(access[0]?.min_permission).toBe('auth.read');
    });
    
    // Cleanup
    await asSystem(async (client) => {
      await cleanupTestData(client, {
        nodeTypes: ['platform.user', 'platform.organization', 'test.document'],
        edgeTypes: ['auth.read', 'auth.read.member'],
        externalIdPattern: 'test_%'
      });
    });
  });
});

Permission Hierarchy Workflow

Testing transitive permissions through organizational hierarchy:

test('permissions flow through org hierarchy', async ({ asSystem }) => {
  await asSystem(async (client) => {
    // Create complete workspace
    const workspace = await setupTestWorkspace(client, {
      includeTeam: true,
      resourceCount: 0 // We'll create resources manually
    });
    
    // Create resource owned by org
    const { resourceNodeId } = await createResourceWithPermission(client, {
      userNodeId: workspace.org.nodeId, // Grant to org
      resourceType: 'test.project',
      permissionLevel: 'write',
      resourceData: { name: 'Company Project' }
    });
    
    // Refresh user cache
    await refreshUserAccessCache(client, workspace.user.id);
    
    // User inherits permission through org membership
    const access = await queryUserAccessCache(
      client,
      workspace.user.id,
      resourceNodeId
    );
    
    expect(access.length).toBeGreaterThan(0);
    expect(access[0].depth).toBeGreaterThan(0); // Transitive
    expect(access[0].min_permission).toMatch(/auth\.read\.write/);
    
    // Cleanup
    await cleanupTestData(client, {
      nodeTypes: ['platform.user', 'platform.organization', 'platform.team', 'test.project'],
      edgeTypes: ['auth.read.member', 'auth.read.write']
    });
  });
});

WorkOS Webhook Workflow

Testing complete webhook processing flow:

import { createUserEvent, createOrgEvent, createMembershipEvent } from '../setup/factories/workos-events';

test('process complete user onboarding', async ({ asSystem }) => {
  const userId = 'user_01234';
  const orgId = 'org_01234';
  const membershipId = 'membership_01234';
  
  // Step 1: Organization created
  const orgEvent = createOrgEvent('organization.created', orgId, {
    name: 'Acme Corp',
    domains: [{ domain: 'acme.com' }]
  });
  await processWebhook(orgEvent);
  
  // Step 2: User created
  const userEvent = createUserEvent('user.created', userId, {
    email: 'john@acme.com',
    firstName: 'John',
    lastName: 'Doe'
  });
  await processWebhook(userEvent);
  
  // Step 3: Membership created
  const membershipEvent = createMembershipEvent(
    'organization_membership.created',
    membershipId,
    userId,
    orgId,
    { role: { slug: 'admin' } }
  );
  await processWebhook(membershipEvent);
  
  // Verify complete state
  await asSystem(async (client) => {
    // Verify user exists
    const users = await getNodesByType(client, 'platform.user');
    const user = users.find(u => u.external_id === userId);
    expect(user).toBeDefined();
    expect(user.data.email).toBe('john@acme.com');
    
    // Verify org exists
    const orgs = await getNodesByType(client, 'platform.organization');
    const org = orgs.find(o => o.external_id === orgId);
    expect(org).toBeDefined();
    expect(org.data.name).toBe('Acme Corp');
    
    // Verify membership edge
    const edges = await getEdgesBetweenNodes(
      client,
      user.id,
      org.id,
      'auth.read.member'
    );
    expect(edges.length).toBe(1);
    
    // Verify user has access to org
    await refreshUserAccessCache(client, userId);
    const access = await queryUserAccessCache(client, userId, org.id);
    expect(access.length).toBeGreaterThan(0);
  });
});

Bulk Operations Workflow

Testing scenarios with many resources:

test('user manages multiple projects', async ({ asSystem }) => {
  await asSystem(async (client) => {
    // Create workspace with many resources
    const workspace = await setupTestWorkspace(client, {
      resourceCount: 10,
      resourceType: 'test.project',
      resourcePermission: 'admin'
    });
    
    expect(workspace.resources).toHaveLength(10);
    
    // Refresh cache
    await refreshUserAccessCache(client, workspace.user.id);
    
    // Verify all resources accessible
    const access = await queryUserAccessCache(client, workspace.user.id);
    expect(access.length).toBeGreaterThanOrEqual(10);
    
    // Verify admin permission on all
    const projectAccess = access.filter(a => 
      workspace.resources.some(r => r.nodeId === a.node_id)
    );
    
    projectAccess.forEach(a => {
      expect(a.min_permission).toBe('auth.read.write.admin');
    });
    
    // Cleanup all at once
    await cleanupTestData(client, {
      nodeTypes: ['test.project', 'platform.user', 'platform.organization'],
      externalIdPattern: 'test_%'
    });
  });
});

Complex Graph Relationship Workflow

Testing interconnected nodes:

test('complex graph relationships', async ({ asSystem }) => {
  await asSystem(async (client) => {
    // Create person-company-project relationships
    const { source: person, target: company, edge: employment } = 
      await createConnectedNodes(client, {
        sourceType: 'test.person',
        targetType: 'test.company',
        edgeType: 'test.works_at',
        sourceData: { name: 'Alice', title: 'Engineer' },
        targetData: { name: 'Acme Corp', industry: 'Software' }
      });
    
    const { source: companyNode, target: project } = 
      await createConnectedNodes(client, {
        sourceType: 'test.company',
        targetType: 'test.project',
        edgeType: 'test.owns',
        sourceData: { id: company.id }, // Link to existing company
        targetData: { name: 'Project X', status: 'active' }
      });
    
    // Verify relationships
    const employmentEdges = await getEdgesBetweenNodes(
      client,
      person.nodeId,
      company.nodeId,
      'test.works_at'
    );
    expect(employmentEdges).toHaveLength(1);
    
    const ownershipEdges = await getEdgesBetweenNodes(
      client,
      company.nodeId,
      project.nodeId,
      'test.owns'
    );
    expect(ownershipEdges).toHaveLength(1);
    
    // Cleanup
    await cleanupTestData(client, {
      nodeTypes: ['test.person', 'test.company', 'test.project'],
      edgeTypes: ['test.works_at', 'test.owns']
    });
  });
});

Quick Reference Card

Most Used Factories

// User + Org (most common start)
const { user, org } = await createUserWithOrgSetup(client);

// Resource with permission
const { resourceNodeId } = await createResourceWithPermission(client, {
  userNodeId: user.nodeId,
  permissionLevel: 'write'
});

// Complete workspace
const workspace = await setupTestWorkspace(client, {
  includeTeam: true,
  resourceCount: 5
});

Most Used Utilities

// Query access cache
const access = await queryUserAccessCache(client, userId);
const nodeAccess = await queryUserAccessCache(client, userId, nodeId);

// Refresh cache
await refreshUserAccessCache(client, userId);

// Check permissions
const hasAccess = await checkUserHasAccess(client, nodeId);
const canManage = await checkUserCanManage(client, nodeId);
const isAdmin = await checkUserIsAdmin(client, nodeId);

// Cleanup
await cleanupTestData(client, {
  nodeTypes: ['test.resource'],
  externalIdPattern: 'test_%'
});

Common Imports

import test from '../test';
import { generateTestId } from '@/test-utils-integration/utils/graph-fixtures';
import {
  createUserWithOrgSetup,
  createResourceWithPermission,
  setupTestWorkspace
} from '../setup/factories/database-fixtures';
import {
  queryUserAccessCache,
  refreshUserAccessCache,
  checkUserHasAccess,
  cleanupTestData
} from '../setup/utilities';

Next Steps: