Example 07

SaaS multi-tenant isolation regression

Isolation bugs appear only when several tenants hold similar-looking records side by side.

Status
type-checked
Industry
SaaS
Language
TypeScript
Framework
Playwright
CI
any
Fulfilment
BUILD_DATASET
Profile
edge-cases-core

About this example

All data is synthetic. DataNivra provisions approved test data into a customer-controlled target; it does not run these tests. Credentials for the target are never returned by DataNivra — your runner holds its own.

Why test data matters

Isolation bugs appear only when several tenants hold similar-looking records side by side.

What is requested

edge-cases-core@1: 400 normal and 100 boundary records.

Policy gates

Within the profile; synthetic-only.

What is provisioned

A certified synthetic dataset.

Expected output

AUTO_APPROVED; the test runs once the request is READY.

Cleanup

withTestData releases the request even when the test fails.

Files

  • request.yaml
  • tenant-isolation.spec.ts

Run

  1. A tenant administrator defines and a second person approves the profile (datanivra test-data define ../../profiles/edge-cases-core.yaml --submit, then datanivra policies approve).
  2. datanivra test-data validate request.yaml && datanivra test-data plan request.yaml
  3. Run the example with DATANIVRA_BASE_URL and a session or workload token.

Files

Exactly as they are in the examples repository. Every value is a synthetic placeholder; nothing here is a credential.

request.yaml

# yaml-language-server: $schema=../../../../packages/contracts/schemas/v1/manifests/test-data-request.schema.json
# DataNivra test-data-request manifest (apiVersion datanivra.io/v1alpha1). Validate: datanivra test-data validate <file>
apiVersion: datanivra.io/v1alpha1
kind: TestDataRequest
metadata:
  name: tenant-isolation
spec:
  profile: { name: edge-cases-core, version: 1 }
  scenarios:
    - { scenario_code: CORE_NORMAL, count: 400 }
    - { scenario_code: CORE_BOUNDARY, count: 100 }

examples/test-data-api/scenarios/07-saas-tenant-isolation/request.yaml

tenant-isolation.spec.ts

/**
 * SaaS tenant isolation — Playwright test using approved test data. The browser exercises your application;
 * DataNivra only provisioned the data behind it (metadata flows through this file, never rows).
 */
import { readFileSync } from 'node:fs';
import { expect, test } from '@playwright/test';
import { createDataNivraClient } from '@datanivra/sdk';
import { TestDataClient } from '@datanivra/sdk/testdata';

const api = createDataNivraClient({
  baseUrl: process.env.DATANIVRA_BASE_URL ?? 'https://api.example.test',
  getAccessToken: () => process.env.DATANIVRA_TOKEN,
});

test('one tenant never sees records of another tenant', async ({ page }) => {
  const manifest = readFileSync(new URL('./request.yaml', import.meta.url), 'utf8');
  await new TestDataClient(api).withTestData(
    manifest,
    async (request) => {
      expect(request.state).toBe('READY');
      await page.goto(process.env.APP_URL ?? 'http://localhost:3000');
      // sign in as two synthetic tenants and assert neither list shows the other's ids
    },
    { idempotencyKey: process.env.CI_RUN_KEY },
  );
});

examples/test-data-api/scenarios/07-saas-tenant-isolation/tenant-isolation.spec.ts

The profile it uses: edge-cases-core

A tenant administrator defines this profile and a second person approves it; requests can only ask for what it allows. All profiles

edge-cases-core.yaml

# yaml-language-server: $schema=../../../packages/contracts/schemas/v1/manifests/test-data-profile.schema.json
# DataNivra test-data-profile manifest (apiVersion datanivra.io/v1alpha1). Validate: datanivra test-data validate <file>
# Placeholder ids (00000000-0000-4000-8000-0000000000NN): replace with your ids.
# 01 = approved target environment, 11 = certified dataset, 21 = customer agent,
# 99 = workload identity trust (datanivra workload-identities list).
apiVersion: datanivra.io/v1alpha1
kind: TestDataProfile
metadata:
  name: edge-cases-core
  description: Core synthetic edge cases (no industry pack)
spec:
  fulfilment: BUILD_DATASET
  build:
    business_domain: generic records
    dataset_kind: SYNTHETIC_ONLY
    target_environment_id: 00000000-0000-4000-8000-000000000001
    retention_days: 1
    max_total_records: 50000
    scenarios:
      - { scenario_code: CORE_NORMAL, kind: NORMAL, default_count: 200, max_count: 1000 }
      - { scenario_code: CORE_BOUNDARY, kind: BOUNDARY, default_count: 100, max_count: 500 }
      - {
          scenario_code: CORE_TEMPORAL_SEQUENCE,
          kind: TEMPORAL_SEQUENCE,
          default_count: 100,
          max_count: 500,
          included_by_default: false,
        }
      - {
          scenario_code: CORE_NULL_HEAVY,
          kind: NULL_HEAVY,
          default_count: 50,
          max_count: 200,
          included_by_default: false,
        }
      - {
          scenario_code: CORE_INVALID,
          kind: INVALID,
          default_count: 50,
          max_count: 200,
          included_by_default: false,
        }
      - {
          scenario_code: CORE_DUPLICATE,
          kind: DUPLICATE,
          default_count: 50,
          max_count: 200,
          included_by_default: false,
        }
      - {
          scenario_code: CORE_HIGH_VOLUME,
          kind: HIGH_VOLUME,
          default_count: 1000,
          max_count: 50000,
          included_by_default: false,
        }

examples/test-data-api/profiles/edge-cases-core.yaml