Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
-- ================================================================
-- SMOKE TESTS MENSAIS — v2 (SECURITY INVOKER porque fn_run_smoke_tests
-- interna usa RESET role que não funciona em SECURITY DEFINER)
-- ================================================================

CREATE TABLE IF NOT EXISTS public.smoke_tests_runs (
id bigserial PRIMARY KEY,
ran_at timestamptz NOT NULL DEFAULT now(),
test_name text NOT NULL,
test_category text,
result text NOT NULL,
details text,
duration_ms numeric
);

CREATE INDEX IF NOT EXISTS idx_smoke_tests_runs_ran_at
ON public.smoke_tests_runs (ran_at DESC);

CREATE INDEX IF NOT EXISTS idx_smoke_tests_runs_test_result
ON public.smoke_tests_runs (test_name, result, ran_at DESC);

ALTER TABLE public.smoke_tests_runs ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS smoke_tests_runs_read_admin ON public.smoke_tests_runs;
CREATE POLICY smoke_tests_runs_read_admin
ON public.smoke_tests_runs FOR SELECT
TO authenticated
USING (is_admin_or_above((SELECT auth.uid())));
Comment on lines +24 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add INSERT policy for smoke_tests_runs before INVOKER writes

RLS is enabled on public.smoke_tests_runs, but this migration only creates a FOR SELECT policy for authenticated. The same migration defines fn_run_and_persist_smoke_tests() as SECURITY INVOKER and grants it to authenticated, and that function performs INSERT into smoke_tests_runs; without a matching FOR INSERT policy, authenticated callers (including admin dashboard users) will hit an RLS violation and the RPC cannot persist results.

Useful? React with 👍 / 👎.


GRANT SELECT ON public.smoke_tests_runs TO authenticated;
Comment on lines +22 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Grant INSERT on smoke_tests_runs for invoker RPC

The migration grants only SELECT on public.smoke_tests_runs to authenticated, but fn_run_and_persist_smoke_tests() is SECURITY INVOKER and executes INSERT into that table. Even if RLS policies are corrected, authenticated users invoking this RPC will still fail with table-permission errors until INSERT privilege is granted.

Useful? React with 👍 / 👎.

REVOKE ALL ON public.smoke_tests_runs FROM anon;
GRANT USAGE, SELECT ON SEQUENCE public.smoke_tests_runs_id_seq TO authenticated;

-- Wrapper SECURITY INVOKER (default)
CREATE OR REPLACE FUNCTION public.fn_run_and_persist_smoke_tests()
RETURNS TABLE(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drop function before redefining return type

This migration introduces public.fn_run_and_persist_smoke_tests() as RETURNS TABLE (...), but a later migration already in the repo (supabase/migrations/20260525063000_restore_smoke_test_observability_contract.sql) redefines the same zero-argument function as RETURNS void using CREATE OR REPLACE FUNCTION. PostgreSQL does not allow CREATE OR REPLACE to change an existing function’s return type, so a clean migration replay will now fail when it reaches that later file with cannot change return type of existing function.

Useful? React with 👍 / 👎.

ran_at timestamptz,
total_tests int,
passed int,
failed int,
warned int,
failed_tests text[]
)
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = public, pg_catalog
AS $$
DECLARE
v_ran_at timestamptz := now();
v_total int := 0;
v_passed int := 0;
v_failed int := 0;
v_warned int := 0;
v_failed_tests text[] := ARRAY[]::text[];
r RECORD;
BEGIN
FOR r IN SELECT * FROM public.fn_run_smoke_tests() LOOP
v_total := v_total + 1;

INSERT INTO public.smoke_tests_runs (
ran_at, test_name, test_category, result, details, duration_ms
) VALUES (
v_ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms
);
Comment on lines +60 to +64

IF upper(r.result) = 'PASS' THEN v_passed := v_passed + 1;
ELSIF upper(r.result) = 'FAIL' THEN
v_failed := v_failed + 1;
v_failed_tests := array_append(v_failed_tests, r.test_name);
ELSIF upper(r.result) IN ('WARN','WARNING') THEN v_warned := v_warned + 1;
END IF;
Comment on lines +66 to +71
END LOOP;

RETURN QUERY SELECT v_ran_at, v_total, v_passed, v_failed, v_warned, v_failed_tests;
END;
$$;

COMMENT ON FUNCTION public.fn_run_and_persist_smoke_tests() IS
'Wrapper SECURITY INVOKER. Roda fn_run_smoke_tests(), persiste em smoke_tests_runs e retorna sumário.
Cron mensal + execução manual via dashboard admin.';

-- Grant para admin/postgres executar
GRANT EXECUTE ON FUNCTION public.fn_run_and_persist_smoke_tests() TO postgres;
GRANT EXECUTE ON FUNCTION public.fn_run_and_persist_smoke_tests() TO authenticated;

-- Views
CREATE OR REPLACE VIEW public.v_smoke_tests_latest_run
WITH (security_invoker = on) AS
WITH latest AS (
SELECT max(ran_at) AS ran_at FROM public.smoke_tests_runs
)
SELECT
r.ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms
FROM public.smoke_tests_runs r
JOIN latest l ON r.ran_at = l.ran_at
ORDER BY
CASE upper(r.result)
WHEN 'FAIL' THEN 1 WHEN 'WARN' THEN 2 WHEN 'PASS' THEN 3 ELSE 4
END, r.test_name;
Comment on lines +97 to +99

GRANT SELECT ON public.v_smoke_tests_latest_run TO authenticated;
REVOKE SELECT ON public.v_smoke_tests_latest_run FROM anon;

CREATE OR REPLACE VIEW public.v_smoke_tests_trend
WITH (security_invoker = on) AS
SELECT
ran_at,
count(*) AS total,
count(*) FILTER (WHERE upper(result) = 'PASS') AS passed,
count(*) FILTER (WHERE upper(result) = 'FAIL') AS failed,
count(*) FILTER (WHERE upper(result) IN ('WARN','WARNING')) AS warned,
Comment on lines +109 to +111
round(avg(duration_ms)::numeric, 1) AS avg_duration_ms
FROM public.smoke_tests_runs
GROUP BY ran_at
ORDER BY ran_at DESC
LIMIT 12;

GRANT SELECT ON public.v_smoke_tests_trend TO authenticated;
REVOKE SELECT ON public.v_smoke_tests_trend FROM anon;

-- Cron mensal (dia 1, 03h UTC — domingo madrugada se cair, sem impacto)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'smoke_tests_monthly') THEN
PERFORM cron.schedule(
'smoke_tests_monthly',
'0 3 1 * *',
$cron$ SELECT public.fn_run_and_persist_smoke_tests(); $cron$
);
END IF;
END $$;

-- Rotação: manter últimas 24 execuções (~2 anos)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'smoke_tests_runs_purge') THEN
PERFORM cron.schedule(
'smoke_tests_runs_purge',
'0 4 1 * *',
$cron$
DELETE FROM public.smoke_tests_runs
WHERE ran_at < (
SELECT min(ran_at) FROM (
SELECT DISTINCT ran_at FROM public.smoke_tests_runs
ORDER BY ran_at DESC LIMIT 24
) keep
);
$cron$
);
END IF;
END $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
-- ================================================================
-- FIX: fn_run_smoke_tests retorna result com emoji prefix
-- ("✅ PASS", "❌ FAIL", "⚠️ WARN"). Ajustar parser para usar LIKE.
-- ================================================================

CREATE OR REPLACE FUNCTION public.fn_run_and_persist_smoke_tests()
RETURNS TABLE(
ran_at timestamptz,
total_tests int,
passed int,
failed int,
warned int,
failed_tests text[]
)
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = public, pg_catalog
AS $$
DECLARE
v_ran_at timestamptz := now();
v_total int := 0;
v_passed int := 0;
v_failed int := 0;
v_warned int := 0;
v_failed_tests text[] := ARRAY[]::text[];
r RECORD;
BEGIN
FOR r IN SELECT * FROM public.fn_run_smoke_tests() LOOP
v_total := v_total + 1;

INSERT INTO public.smoke_tests_runs (
ran_at, test_name, test_category, result, details, duration_ms
) VALUES (
v_ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms
);

-- Match com emoji prefix (✅ PASS, ❌ FAIL, ⚠️ WARN)
IF r.result ILIKE '%PASS%' THEN
v_passed := v_passed + 1;
ELSIF r.result ILIKE '%FAIL%' THEN
v_failed := v_failed + 1;
v_failed_tests := array_append(v_failed_tests, r.test_name);
ELSIF r.result ILIKE '%WARN%' THEN
v_warned := v_warned + 1;
END IF;
END LOOP;

RETURN QUERY SELECT v_ran_at, v_total, v_passed, v_failed, v_warned, v_failed_tests;
END;
$$;

-- View latest_run ajustar ordenação para emoji prefix
CREATE OR REPLACE VIEW public.v_smoke_tests_latest_run
WITH (security_invoker = on) AS
WITH latest AS (
SELECT max(ran_at) AS ran_at FROM public.smoke_tests_runs
)
SELECT
r.ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms
FROM public.smoke_tests_runs r
JOIN latest l ON r.ran_at = l.ran_at
ORDER BY
CASE
WHEN r.result ILIKE '%FAIL%' THEN 1
WHEN r.result ILIKE '%WARN%' THEN 2
WHEN r.result ILIKE '%PASS%' THEN 3
ELSE 4
END,
r.test_name;

-- View trend ajustar
CREATE OR REPLACE VIEW public.v_smoke_tests_trend
WITH (security_invoker = on) AS
SELECT
ran_at,
count(*) AS total,
count(*) FILTER (WHERE result ILIKE '%PASS%') AS passed,
count(*) FILTER (WHERE result ILIKE '%FAIL%') AS failed,
count(*) FILTER (WHERE result ILIKE '%WARN%') AS warned,
round(avg(duration_ms)::numeric, 1) AS avg_duration_ms
FROM public.smoke_tests_runs
GROUP BY ran_at
ORDER BY ran_at DESC
LIMIT 12;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
-- ================================================================
-- ROLLOUT GRADUAL DO KILL-SWITCH (A/B controlado)
--
-- Permite desligar o switch progressivamente: 5% → 25% → 50% → 100%
-- usando hash determinístico do user_id/session_id como bucket.
--
-- Estratégia:
-- 1. Coluna rollout_percentage em system_kill_switches (0-100)
-- 2. Função fn_should_apply_kill_switch(switch_name, bucket_key) → boolean
-- Retorna TRUE se a chave cai dentro do bucket de rollout (kill aplicado)
-- 3. View v_kill_switch_with_rollout para o front consultar
-- ===============================================================

-- 1) Adicionar coluna de rollout
ALTER TABLE public.system_kill_switches
ADD COLUMN IF NOT EXISTS rollout_percentage smallint NOT NULL DEFAULT 100
CHECK (rollout_percentage BETWEEN 0 AND 100);

COMMENT ON COLUMN public.system_kill_switches.rollout_percentage IS
'Porcentagem de tráfego (0-100) que recebe o kill-switch quando enabled=false.
100 = todos os clientes (default). 5 = apenas 5% (canary).
Útil para A/B testing antes de desligar definitivamente.';

-- 2) Função pura de roteamento — determinística por hash do bucket_key
CREATE OR REPLACE FUNCTION public.fn_should_apply_kill_switch(
p_switch_name text,
p_bucket_key text
) RETURNS boolean
LANGUAGE plpgsql
STABLE
SECURITY INVOKER
SET search_path = public, pg_catalog
AS $$
DECLARE
v_enabled boolean;
v_rollout smallint;
v_bucket int;
BEGIN
-- Lê estado do switch (RLS aplicável: anon tem SELECT)
SELECT enabled, rollout_percentage
INTO v_enabled, v_rollout
FROM public.system_kill_switches
WHERE switch_name = p_switch_name;

-- Switch não cadastrado = não aplicar (fail-open)
IF NOT FOUND THEN RETURN false; END IF;

-- Switch ATIVO (enabled=true) = nunca aplicar kill, independente de rollout
IF v_enabled THEN RETURN false; END IF;

-- Switch OFF (enabled=false): aplicar kill conforme rollout %
IF v_rollout >= 100 THEN RETURN true; END IF;
IF v_rollout <= 0 THEN RETURN false; END IF;

-- Hash determinístico → bucket 0-99
-- Mesmo bucket_key sempre cai no mesmo bucket (estabilidade entre reloads)
v_bucket := abs(hashtext(coalesce(p_bucket_key, 'anonymous')) % 100);
RETURN v_bucket < v_rollout;
END;
$$;

COMMENT ON FUNCTION public.fn_should_apply_kill_switch IS
'Decide se o kill-switch deve ser APLICADO (= bloquear chamada) para um bucket.
Determinístico por bucket_key (mesma chave sempre cai no mesmo grupo).
Lógica:
enabled=true → false (nunca bloqueia)
enabled=false, rollout=100 → true (sempre bloqueia)
enabled=false, rollout=0 → false (nunca bloqueia — pré-rollout)
enabled=false, rollout=X% → true se hashtext(bucket_key) % 100 < X
Bucket_key sugerido: auth.uid() para logged-in; localStorage uuid para anon.';

GRANT EXECUTE ON FUNCTION public.fn_should_apply_kill_switch TO anon;
GRANT EXECUTE ON FUNCTION public.fn_should_apply_kill_switch TO authenticated;

-- 3) Validar: switch atual está enabled=true, então sempre retorna false
SELECT
public.fn_should_apply_kill_switch('edge_external_db_bridge', 'test-user-1') AS rollout_test_1,
public.fn_should_apply_kill_switch('edge_external_db_bridge', 'test-user-2') AS rollout_test_2,
public.fn_should_apply_kill_switch('non_existent_switch', 'anyone') AS unknown_switch;
Comment on lines +75 to +79
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Restore the BEFORE INSERT trigger that auto-generates public.quotes.quote_number.
-- The trigger was lost during the migration-replay drift cleanup while
-- public.generate_quote_number() survived. Without it, quote_number (NOT NULL,
-- no default) is never populated and every quote INSERT from the app fails.
DROP TRIGGER IF EXISTS set_quote_number ON public.quotes;

CREATE TRIGGER set_quote_number
BEFORE INSERT ON public.quotes
FOR EACH ROW
WHEN (NEW.quote_number IS NULL OR NEW.quote_number = '')
EXECUTE FUNCTION public.generate_quote_number();
Loading