Files
collective-lists/supabase/tests/007_search_tsvector.sql
Oier Bravo Urtasun 2806e06db2 feat(fase-4): global search + RLS isolation audit (211 tests green)
4.0 Tests primero
  Vitest: search.test.ts (10 — S-01..S-04), rls-audit.test.ts (42 —
    3 intruders × 14 cross-collective ops proving zero leakage)
  pgTAP: 007_search_tsvector.sql (9 — columns, GIN indexes, function
    signature, generated-vector invariant)
  Playwright: search.test.ts (2 — SR-01 happy-path, SR-02 filter toggle)

4.1 Búsqueda global
  Migration 010_search_vectors.sql: STORED GENERATED tsvector columns
    on shopping_items/tasks/notes + GIN indexes + search_result_type
    enum + search_in_collective() SECURITY INVOKER function (RLS-aware,
    trash-excluded per RN-08) + GRANT EXECUTE to anon/authenticated.
    Uses `simple` config (no stemmer — bilingual en/es).
  Store apps/web/src/lib/stores/search.ts — RPC wrapper
  /search: debounced input (300ms, ≥2 chars), type filter chips with
    multi-toggle, results grouped per type with data-testid hooks for
    Playwright, deep-link per type (shopping_item → /lists/[id], task →
    /tasks/[id], note → /notes/[id])

4.4 Security
  rls-audit.test.ts replaces the curl-based manual audit with a
    parametrised matrix — zero cross-collective reads/writes under
    three different attacker roles

Realtime flake hardening
  Add 250ms settle after SUBSCRIBED in realtime-helpers.ts — the Phoenix
    socket reports SUBSCRIBED slightly before the backend finishes
    propagating the filter to the WAL subscription, so mutations fired
    immediately after subscribe could miss the filter under load.

4.Z Verification
  just test-all → 211 verdes, 2 skipped
    34 pgTAP (was 25, +9 search)
    140 Vitest integration (was 88, +10 search +42 rls-audit; 2 skipped)
    6 Vitest unit (unchanged)
    31 Playwright (was 29, +2 search)
  Deferred (prod-deploy scope):
    PWA install/push (needs real iOS/Android hardware)
    Kong rate-limit plugin config
    JWT_SECRET / ANON_KEY rotation
    Lighthouse PWA ≥ 90 manual validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:47:38 +02:00

79 lines
2.5 KiB
PL/PgSQL

-- pgTAP: search_vectors — generated tsvector columns + GIN indexes + function — Fase 4
-- Run with: psql -U postgres -d postgres -f supabase/tests/007_search_tsvector.sql
CREATE EXTENSION IF NOT EXISTS pgtap;
BEGIN;
SELECT plan(9);
-- Column existence — stored generated tsvector
SELECT has_column('public', 'shopping_items', 'search',
'SV-01: shopping_items.search column exists');
SELECT has_column('public', 'tasks', 'search',
'SV-02: tasks.search column exists');
SELECT has_column('public', 'notes', 'search',
'SV-03: notes.search column exists');
-- GIN indexes present
SELECT ok(
EXISTS (
SELECT 1 FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'shopping_items' AND indexname = 'shopping_items_search_idx'
),
'SV-04: shopping_items_search_idx GIN index exists'
);
SELECT ok(
EXISTS (
SELECT 1 FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'tasks' AND indexname = 'tasks_search_idx'
),
'SV-05: tasks_search_idx GIN index exists'
);
SELECT ok(
EXISTS (
SELECT 1 FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'notes' AND indexname = 'notes_search_idx'
),
'SV-06: notes_search_idx GIN index exists'
);
-- Function exists and is callable
SELECT has_function('public', 'search_in_collective',
ARRAY['uuid', 'text', 'public.search_result_type[]', 'uuid', 'timestamptz', 'timestamptz'],
'SV-07: search_in_collective(uuid, text, types[], creator, from, to) exists');
-- Functional invariants on the generated vector: insert an item, vector reflects the name
DO $$
DECLARE
v_item_id uuid;
v_vector tsvector;
BEGIN
INSERT INTO public.shopping_items (list_id, name, sort_order, created_by)
VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'pgtap unique xyzzy', 9998,
'11111111-1111-1111-1111-111111111111')
RETURNING id INTO v_item_id;
SELECT search INTO v_vector FROM public.shopping_items WHERE id = v_item_id;
PERFORM set_config('pgtap.vector_text', v_vector::text, false);
PERFORM set_config('pgtap.vector_match',
CASE WHEN v_vector @@ to_tsquery('simple', 'xyzzy') THEN 'true' ELSE 'false' END,
false);
END $$;
SELECT matches(
current_setting('pgtap.vector_text'),
'xyzzy',
'SV-08: generated shopping_items.search vector contains the inserted lexeme'
);
SELECT is(
current_setting('pgtap.vector_match'),
'true',
'SV-09: generated shopping_items.search vector matches its tsquery'
);
SELECT * FROM finish();
ROLLBACK;