Ctrl K
ComunidadGuías…

schema/migrated-storage-invariants.integration.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/// <reference types="node" />

/**
 * Live storage invariants of the bootstrapped schema. Each test drives the
 * real interface, inserting a violating row against the installed database,
 * with an independently derived expectation, or reads the live catalog for a
 * shape the product depends on. Where an engine or integration suite already
 * proves a behaviour, no test is added here.
 *
 * Runs without a self-skip: the integration partition always provides a
 * database, so a missing one must fail loudly, never pass silently.
 */
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import test from "node:test";
import { pool } from "@openbooks/engine/platform/db";
import {
  createScratchOrg,
  createScratchUser,
  dropScratchOrg,
} from "@openbooks/engine/testing/fixtures";

function isConstraintViolation(error: unknown, pattern: RegExp): boolean {
  if (!(error instanceof Error)) return false;
  return pattern.test(error.message);
}

async function insertPayComponent(orgId: string): Promise<string> {
  const id = randomUUID();
  await pool.query(
    `insert into pay_components (id, org_id, code, name, kind)
     values ($1, $2, 'T8BASE', 'T8 base component', 'earning')`,
    [id, orgId],
  );
  return id;
}

async function insertPaySchedule(orgId: string): Promise<string> {
  const id = randomUUID();
  await pool.query(
    `insert into pay_schedules (id, org_id, name, frequency, periods_per_year, anchor_period_end)
     values ($1, $2, 'T8 schedule', 'biweekly', 26, '2026-01-02')`,
    [id, orgId],
  );
  return id;
}

test("overlapping active pay-derived windows for one rule identity are refused", async () => {
  // Concurrent effective windows for the same (org, code) must not
  // both be active — payroll would price the same earnings twice.
  const org = await createScratchOrg();
  try {
    const componentId = await insertPayComponent(org.orgId);
    const first = randomUUID();
    await pool.query(
      `insert into pay_derived_rules
         (id, org_id, code, name, component_id, trigger, rate_value, effective_from, effective_to, is_active)
       values ($1, $2, 'T8RULE', 'T8 rule', $3, 'time_entry', 10, '2026-01-01', '2026-12-31', true)`,
      [first, org.orgId, componentId],
    );
    const overlapping = randomUUID();
    const refused = await pool
      .query(
        `insert into pay_derived_rules
           (id, org_id, code, name, component_id, trigger, rate_value, effective_from, effective_to, is_active)
         values ($1, $2, 'T8RULE', 'T8 rule', $3, 'time_entry', 10, '2026-06-01', null, true)`,
        [overlapping, org.orgId, componentId],
      )
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(refused, /pay_derived_rules_no_active_overlap|exclusion/i),
      `expected the overlap exclusion to refuse, got: ${String(refused)}`,
    );
    // An inactive twin is not a second live rule and must commit.
    const inactive = randomUUID();
    await pool.query(
      `insert into pay_derived_rules
         (id, org_id, code, name, component_id, trigger, rate_value, effective_from, effective_to, is_active)
       values ($1, $2, 'T8RULE', 'T8 rule', $3, 'time_entry', 10, '2026-06-01', null, false)`,
      [inactive, org.orgId, componentId],
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("tax rates refuse negative percentages and setup codes refuse duplicates", async () => {
  // A negative rate would post inverted tax; a duplicate setup code
  // would make pack resolution ambiguous.
  const org = await createScratchOrg();
  try {
    const codeId = randomUUID();
    await pool.query(
      `insert into tax_codes (id, org_id, code, name) values ($1, $2, 'T8GST', 'T8 tax')`,
      [codeId, org.orgId],
    );
    const negative = await pool
      .query(
        `insert into tax_rates (id, org_id, tax_code_id, rate_percent, effective_from)
         values ($1, $2, $3, -5, '2026-01-01')`,
        [randomUUID(), org.orgId, codeId],
      )
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(negative, /tax_rates_rate_percent_domain|check/i),
      `expected the rate domain check to refuse, got: ${String(negative)}`,
    );
    const duplicate = await pool
      .query(`insert into tax_codes (id, org_id, code, name) values ($1, $2, 'T8GST', 'T8 copy')`, [
        randomUUID(),
        org.orgId,
      ])
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(duplicate, /tax_codes_org_code_unique|unique/i),
      `expected the setup-code unique to refuse, got: ${String(duplicate)}`,
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("subscription plans refuse negative amounts", async () => {
  // A negative plan amount would bill customers to receive money.
  const org = await createScratchOrg();
  try {
    const refused = await pool
      .query(
        `insert into subscription_plans (id, org_id, name, amount) values ($1, $2, 'T8 plan', -10)`,
        [randomUUID(), org.orgId],
      )
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(refused, /subscription_plans_amount_nonnegative|check/i),
      `expected the amount check to refuse, got: ${String(refused)}`,
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("forecast snapshots take at most one target and commit org-wide", async () => {
  // A snapshot forecasting for both an owner and a team double-counts
  // the pipeline; an org-wide snapshot is the honest default.
  const org = await createScratchOrg();
  const userId = randomUUID();
  const teamId = randomUUID();
  try {
    // An active user must carry a role assignment; createScratchUser
    // inserts user and assignment atomically.
    await createScratchUser(org.orgId, "T8 owner", "t8_owner", userId);
    await pool.query(
      `insert into crm_sales_teams (id, org_id, key, name) values ($1, $2, 'T8', 'T8 team')`,
      [teamId, org.orgId],
    );
    const snapshot = (id: string, owner: string | null, team: string | null) =>
      pool.query(
        `insert into crm_forecast_snapshots
           (id, org_id, owner_user_id, sales_team_id, period_start, period_end,
            snapshot_kind, currency, pipeline_amount, weighted_amount, worst_case_amount,
            most_likely_amount, upside_amount, closed_amount)
         values ($1, $2, $3, $4, '2026-01-01', '2026-03-31', 'commit', 'CAD', 0, 0, 0, 0, 0, 0)`,
        [id, org.orgId, owner, team],
      );
    const both = await snapshot(randomUUID(), userId, teamId).then(
      () => null,
      (error: unknown) => error,
    );
    assert.ok(
      isConstraintViolation(both, /crm_forecast_snapshot_target|check/i),
      `expected the single-target check to refuse, got: ${String(both)}`,
    );
    await snapshot(randomUUID(), null, null);
    await snapshot(randomUUID(), userId, null);
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("payroll profiles refuse an omitted country instead of defaulting one", async () => {
  // No country built-in exists, so an unset country must fail NOT NULL
  // rather than silently becoming Canada.
  const org = await createScratchOrg();
  try {
    const scheduleId = await insertPaySchedule(org.orgId);
    const refused = await pool
      .query(
        `insert into employee_payroll_profiles (id, org_id, employee_party_id, pay_schedule_id, province)
         values ($1, $2, $3, $4, 'Ontario')`,
        [randomUUID(), org.orgId, org.customerId, scheduleId],
      )
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(refused, /null value in column "country"|not-null/i),
      `expected NOT NULL on country to refuse, got: ${String(refused)}`,
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("payroll pack facts refuse out-of-range values and accept unknown", async () => {
  // Pack-declared facts are nullable (null = unknown, never guessed)
  // but a present value outside the pack's bounds is refused.
  const org = await createScratchOrg();
  try {
    const scheduleId = await insertPaySchedule(org.orgId);
    const base = (id: string, country: string) =>
      pool.query(
        `insert into employee_payroll_profiles
           (id, org_id, employee_party_id, pay_schedule_id, province, country)
         values ($1, $2, $3, $4, 'Ontario', $5)`,
        [id, org.orgId, org.customerId, scheduleId, country],
      );
    const profileId = randomUUID();
    await base(profileId, "ES");
    const outOfRange = await pool
      .query(`update employee_payroll_profiles set es_ano_nacimiento = 1800 where id = $1`, [
        profileId,
      ])
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(outOfRange, /employee_payroll_profiles_es_ano|check/i),
      `expected the birth-year check to refuse, got: ${String(outOfRange)}`,
    );
    await pool.query(
      `update employee_payroll_profiles set es_ano_nacimiento = null where id = $1`,
      [profileId],
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("sync runs detach from a deleted connection instead of vanishing", async () => {
  // Run history is evidence; deleting a connection must null the link
  // (ON DELETE SET NULL over a nullable column), not cascade the runs.
  const org = await createScratchOrg();
  try {
    const connectionId = randomUUID();
    await pool.query(
      `insert into connections (id, org_id, source, display_name) values ($1, $2, 't8', 'T8 connection')`,
      [connectionId, org.orgId],
    );
    const runId = randomUUID();
    await pool.query(
      `insert into sync_runs (id, org_id, source, connection_id) values ($1, $2, 't8', $3)`,
      [runId, org.orgId, connectionId],
    );
    await pool.query(`delete from connections where id = $1`, [connectionId]);
    const stored = await pool.query(`select connection_id, status from sync_runs where id = $1`, [
      runId,
    ]);
    assert.equal(stored.rows[0]?.connection_id, null);
    assert.equal(stored.rows[0]?.status, "running");
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("sftp prefixes refuse escape shapes and accept tenant folders", async () => {
  // A tenant names folders, never physical locations — dot segments,
  // absolute paths, and backslashes must not reach storage.
  const org = await createScratchOrg();
  try {
    // Usernames are globally unique: scope the probe name to this run.
    const username = `t8user-${randomUUID().slice(0, 8)}`;
    const attempt = (prefix: string) =>
      pool.query(
        `insert into sftp_servers (id, org_id, name, username, root_prefix)
         values ($1, $2, 'T8 sftp', $4, $3)`,
        [randomUUID(), org.orgId, prefix, username],
      );
    const escape = await attempt("../escape").then(
      () => null,
      (error: unknown) => error,
    );
    assert.ok(
      isConstraintViolation(escape, /sftp_servers_root_prefix_safe|check/i),
      `expected the prefix guard to refuse, got: ${String(escape)}`,
    );
    await attempt("t8-ok/sub");
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("recognition rules refuse a second live version of one code", async () => {
  // Editing a rule in use must create a successor, never mutate the
  // version obligations already pinned — so only one live row per code.
  const org = await createScratchOrg();
  try {
    await pool.query(
      `insert into recognition_rules (id, org_id, code, name, method)
       values ($1, $2, 'T8RULE', 'T8 rule', 'straight_line_even')`,
      [randomUUID(), org.orgId],
    );
    const duplicate = await pool
      .query(
        `insert into recognition_rules (id, org_id, code, name, method)
         values ($1, $2, 'T8RULE', 'T8 twin', 'straight_line_even')`,
        [randomUUID(), org.orgId],
      )
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(duplicate, /recognition_rules_org_code|unique/i),
      `expected the live-version unique to refuse, got: ${String(duplicate)}`,
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("delivery keys refuse malformed values and double-claimed identities", async () => {
  // The key is a fixed engine-minted shape, and one logical
  // delivery owns exactly one canonical row.
  const org = await createScratchOrg();
  try {
    const malformed = await pool
      .query(
        `insert into email_log (id, org_id, subject, delivery_key) values ($1, $2, 'T8', 'not-a-key')`,
        [randomUUID(), org.orgId],
      )
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(malformed, /email_log_delivery_key_format|check/i),
      `expected the key format check to refuse, got: ${String(malformed)}`,
    );
    const key = `obem_${"a".repeat(40)}`;
    await pool.query(
      `insert into email_log (id, org_id, subject, delivery_key) values ($1, $2, 'T8', $3)`,
      [randomUUID(), org.orgId, key],
    );
    const twin = await pool
      .query(
        `insert into email_log (id, org_id, subject, delivery_key) values ($1, $2, 'T8 twin', $3)`,
        [randomUUID(), org.orgId, key],
      )
      .then(
        () => null,
        (error: unknown) => error,
      );
    assert.ok(
      isConstraintViolation(twin, /email_log_delivery_key|unique/i),
      `expected the delivery identity unique to refuse, got: ${String(twin)}`,
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("document revisions advance on every committed update", async () => {
  // Two committed revisions must never share a token. The
  // collapse shape — an update storing updated_at byte-identical to the
  // row — is rewritten strictly forward by documents_revision_monotonic,
  // and openbooks_bump_revision_seq counts every UPDATE independently of
  // the editable display timestamp. (The web optimistic-concurrency test
  // proving the same lives in a skip-guarded plain .test.ts, so it never
  // runs; this is the live cover.)
  const org = await createScratchOrg();
  try {
    const id = randomUUID();
    // Microsecond-exact display timestamps: node-pg truncates to
    // milliseconds, which would never reproduce the collapse shape.
    const stamp = "2026-08-24T12:00:00.400001Z";
    await pool.query(
      `insert into documents (id, org_id, kind, document_number, subsidiary_id, document_date, currency, status,
         created_at, updated_at)
       values ($1, $2, 'transfer', $3, $4, '2026-01-02', 'CAD', 'draft', $5::timestamptz, $5::timestamptz)`,
      [id, org.orgId, `T8REV-${id.slice(0, 8)}`, org.subsidiaryId, stamp],
    );
    const read = async (): Promise<{ updated_at: Date; revision_seq: string }> =>
      (await pool.query(`select updated_at, revision_seq from documents where id = $1`, [id]))
        .rows[0] as {
        updated_at: Date;
        revision_seq: string;
      };
    const first = await read();
    // Collapse-shaped write: store updated_at byte-identical to the row.
    await pool.query(
      `update documents set memo = 'T8 collapse', updated_at = $2::timestamptz where id = $1`,
      [id, stamp],
    );
    const second = await read();
    assert.ok(
      second.updated_at > first.updated_at,
      `a collapse write must move updated_at strictly forward, got ${String(second.updated_at)}`,
    );
    assert.equal(
      BigInt(second.revision_seq),
      BigInt(first.revision_seq) + 1n,
      "every committed UPDATE must bump the revision counter exactly once",
    );
    await pool.query(`update documents set memo = 'T8 second' where id = $1`, [id]);
    const third = await read();
    assert.ok(
      third.updated_at > second.updated_at,
      `a second update must advance updated_at again, got ${String(third.updated_at)}`,
    );
    assert.equal(
      BigInt(third.revision_seq),
      BigInt(second.revision_seq) + 1n,
      "the revision counter must keep counting past the first bump",
    );
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("bank-feed bookkeeping separates the attempt cursor from the success cursor", async () => {
  // Last_attempt_at records every finished attempt; last_sync_at is
  // the success-only pull cursor the runner reads. The two must move
  // independently — recording an attempt (even a failed one) must not
  // advance the success cursor. (Runner-side attempt/success sequencing
  // lives in engine banking tests; this is the storage half.)
  const org = await createScratchOrg();
  try {
    const id = randomUUID();
    await pool.query(
      `insert into bank_feed_connections (id, org_id, name, provider, account_id)
       values ($1, $2, 'T8 feed', 'manual', $3)`,
      [id, org.orgId, randomUUID()],
    );
    await pool.query(
      `update bank_feed_connections set last_attempt_at = now(), last_error = 'T8 probe failure', status = 'error'
        where id = $1`,
      [id],
    );
    const attempted = (
      await pool.query(
        `select last_attempt_at, last_sync_at from bank_feed_connections where id = $1`,
        [id],
      )
    ).rows[0] as { last_attempt_at: Date | null; last_sync_at: Date | null };
    assert.ok(
      attempted.last_attempt_at !== null,
      "a finished attempt must stamp the attempt cursor",
    );
    assert.equal(attempted.last_sync_at, null, "a failed attempt must not move the success cursor");
    await pool.query(
      `update bank_feed_connections set last_attempt_at = now(), last_sync_at = now(), last_error = null,
         status = 'connected' where id = $1`,
      [id],
    );
    const succeeded = (
      await pool.query(
        `select last_attempt_at, last_sync_at from bank_feed_connections where id = $1`,
        [id],
      )
    ).rows[0] as { last_attempt_at: Date | null; last_sync_at: Date | null };
    assert.ok(succeeded.last_sync_at !== null, "a success must stamp the success cursor");
    assert.ok(succeeded.last_attempt_at !== null, "a success is also a finished attempt");
  } finally {
    await dropScratchOrg(org.orgId);
  }
});

test("the installed catalog carries the canonical schema shape", async () => {
  // Table/function/trigger/index presence, retired-model absence,
  // adjustment RLS wiring and source-identity indexes, read from the live
  // catalog: no scratch org needed, nothing written.
  const tables = await pool.query(
    `select tablename from pg_tables where schemaname = 'public' and tablename = any ($1)`,
    [
      [
        "lease_agreements",
        "lease_agreement_schedule_lines",
        "inventory_writedowns",
        "pay_schedules",
        "pay_components",
        "pay_run_adjustments",
        "employee_payroll_profiles",
        "employee_pay_components",
        "pay_runs",
        "pay_stubs",
        "pay_stub_lines",
        "payroll_opening_balances",
        "union_agreements",
        "union_classifications",
        "union_fringes",
        "auth_sessions",
        "auth_login_events",
        "auth_mfa_factors",
        "auth_oidc_identities",
      ],
    ],
  );
  assert.equal(tables.rows.length, 19, "every canonical table must exist in the installed catalog");
  const routines = await pool.query(
    `select proname from pg_proc where pronamespace = 'public'::regnamespace and proname = any ($1)`,
    [["posted_document_financial_guard"]],
  );
  assert.equal(routines.rows.length, 1, "the posted-document financial guard must exist");
  const triggers = await pool.query(
    `select tgname from pg_trigger where tgname = any ($1) and not tgisinternal`,
    [["documents_posted_financial_guard"]],
  );
  assert.equal(triggers.rows.length, 1, "the posted-document guard trigger must exist");
  const indexes = await pool.query(
    `select indexname from pg_indexes where schemaname = 'public' and indexname = any ($1)`,
    [
      [
        "backup_runs_one_inflight_per_org",
        "parties_org_source_identity",
        "projects_org_source_identity",
      ],
    ],
  );
  assert.equal(
    indexes.rows.length,
    3,
    "one-inflight backup, tenant source-identity indexes must exist",
  );
  const retired = await pool.query(
    `select tablename from pg_tables where schemaname = 'public' and tablename = any ($1)`,
    [
      [
        "orphaned_tax_component_evidence",
        "_migration_control_exceptions",
        "_migration_schema_convergence",
        "selection_source",
        "legacy_json_migration",
        "validation_replay",
      ],
    ],
  );
  assert.deepEqual(
    retired.rows.map((row) => row.tablename),
    [],
    "no upgrade-only evidence model may survive in the catalog",
  );
  const policies = await pool.query(
    `select policyname, permissive, roles::text, cmd, qual is not null as scoped, with_check is not null as checked
       from pg_policies where schemaname = 'public' and tablename = 'pay_run_adjustments' and policyname = 'org_isolation'`,
  );
  assert.equal(policies.rows.length, 1, "pay_run_adjustments must carry the org_isolation policy");
  assert.equal(policies.rows[0]?.scoped, true, "org_isolation must filter reads");
  assert.equal(policies.rows[0]?.checked, true, "org_isolation must filter writes");
  const relkind = await pool.query(
    `select relforcerowsecurity, relrowsecurity from pg_class where relname = 'pay_run_adjustments'`,
  );
  assert.equal(
    relkind.rows[0]?.relrowsecurity,
    true,
    "pay_run_adjustments must enforce row security",
  );
  assert.equal(
    relkind.rows[0]?.relforcerowsecurity,
    true,
    "pay_run_adjustments must force row security",
  );
});

test("the payroll opening-balances view exposes the statutory columns", async () => {
  // The statutory columns of the governed payroll opening-balances view are
  // its interface to payroll reporting.
  const columns = await pool.query(
    `select column_name from information_schema.columns
     where table_schema = 'openbooks_query' and table_name = 'payroll_opening_balances'`,
  );
  const names = new Set(columns.rows.map((row) => row.column_name));
  for (const column of ["cpp2_bonus_ytd", "qc_csb_ytd", "fica_withheld_ytd"]) {
    assert.ok(names.has(column), `the view must expose ${column}`);
  }
});