Compare commits
3
Commits
7e59978f07
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6eef6c4bb | ||
|
|
ca4dc379ab | ||
|
|
11ef1715ab |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
@@ -22,7 +22,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_VERSION: v24.12.0
|
||||
NODE_VERSION: v24.22.2
|
||||
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}
|
||||
steps:
|
||||
- name: Check out the tag
|
||||
|
||||
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.2] - 2026-11-06
|
||||
|
||||
### Fixed
|
||||
- `is_allowed()` with a NULL user id failed with a NOT NULL violation on
|
||||
`auth_cache` instead of answering. `is_allowed()` and `is_allowed_nocache()`
|
||||
now return FALSE for a NULL user, the answer `rls_check()` already gave.
|
||||
Decisions for real users are unchanged.
|
||||
|
||||
## [1.0.1] - 2026-10-14
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "pgmorbac",
|
||||
"abstract": "Multi-OrBAC: Organization-Based Access Control with multi-organization support",
|
||||
"description": "A PostgreSQL extension implementing the Multi-OrBAC access control model \u2014 enabling organization-centric, context-aware, and hierarchical access control with delegation, separation of duty, and cross-organizational policies.",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"maintainer": [
|
||||
"Marc VILLAIN <marc.villain@epita.fr>"
|
||||
],
|
||||
@@ -10,8 +10,8 @@
|
||||
"provides": {
|
||||
"pgmorbac": {
|
||||
"abstract": "Multi-OrBAC access control extension",
|
||||
"file": "pgmorbac--1.0.1.sql",
|
||||
"version": "1.0.1"
|
||||
"file": "pgmorbac--1.0.2.sql",
|
||||
"version": "1.0.2"
|
||||
}
|
||||
},
|
||||
"prereqs": {
|
||||
|
||||
@@ -67,11 +67,11 @@ sudo ./tools/install.sh
|
||||
|
||||
```bash
|
||||
# Build versioned file from source
|
||||
make build # Concatenates src/ files into pgmorbac--1.0.1.sql
|
||||
make build # Concatenates src/ files into pgmorbac--1.0.2.sql
|
||||
|
||||
# Copy files to PostgreSQL extension directory
|
||||
sudo cp pgmorbac.control $(pg_config --sharedir)/extension/
|
||||
sudo cp pgmorbac--1.0.1.sql $(pg_config --sharedir)/extension/
|
||||
sudo cp pgmorbac--1.0.2.sql $(pg_config --sharedir)/extension/
|
||||
|
||||
# Enable in PostgreSQL
|
||||
psql -d mydb -c "CREATE EXTENSION pgmorbac;"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# pgmorbac extension
|
||||
# Multi-OrBAC access control model for PostgreSQL
|
||||
comment = 'Multi-OrBAC: Organization-Based Access Control with multi-organization support'
|
||||
default_version = '1.0.1'
|
||||
default_version = '1.0.2'
|
||||
relocatable = false
|
||||
schema = morbac
|
||||
|
||||
@@ -57,6 +57,11 @@ DECLARE
|
||||
v_cross_context UUID[];
|
||||
v_cross_prio INTEGER[];
|
||||
BEGIN
|
||||
-- No principal, no decision: the same answer rls_check() gives.
|
||||
IF p_user_id IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
v_is_system_principal := EXISTS (
|
||||
SELECT 1 FROM morbac.system_principals WHERE user_id = p_user_id
|
||||
);
|
||||
@@ -310,6 +315,10 @@ DECLARE
|
||||
v_computed_result BOOLEAN;
|
||||
v_expires_at TIMESTAMPTZ;
|
||||
BEGIN
|
||||
IF p_user_id IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
SELECT allowed, expires_at INTO v_cached_result, v_expires_at
|
||||
FROM morbac.auth_cache
|
||||
WHERE user_id = p_user_id
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
-- pgmorbac 1.0.1 -> 1.0.2
|
||||
--
|
||||
-- is_allowed() with a NULL user id failed with a NOT NULL violation on
|
||||
-- auth_cache instead of answering. Both is_allowed() and is_allowed_nocache()
|
||||
-- now return FALSE for a NULL user, as rls_check() already did. No decision
|
||||
-- for a real user changes.
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_activity TEXT,
|
||||
p_view TEXT
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_rule RECORD;
|
||||
v_max_prohibition_priority INTEGER := NULL;
|
||||
v_max_permission_priority INTEGER := NULL;
|
||||
v_is_system_principal BOOLEAN;
|
||||
v_activities TEXT[];
|
||||
v_views TEXT[];
|
||||
v_held_org UUID[];
|
||||
v_held_role UUID[];
|
||||
v_local_modality morbac.modality[];
|
||||
v_local_context UUID[];
|
||||
v_local_prio INTEGER[];
|
||||
v_cross_modality morbac.modality[];
|
||||
v_cross_context UUID[];
|
||||
v_cross_prio INTEGER[];
|
||||
BEGIN
|
||||
-- No principal, no decision: the same answer rls_check() gives.
|
||||
IF p_user_id IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
v_is_system_principal := EXISTS (
|
||||
SELECT 1 FROM morbac.system_principals WHERE user_id = p_user_id
|
||||
);
|
||||
|
||||
v_activities := ARRAY(SELECT activity FROM morbac.get_effective_activities(p_activity));
|
||||
v_views := ARRAY(SELECT view FROM morbac.get_effective_views(p_view));
|
||||
|
||||
-- Roles the user holds, per org. get_comprehensive_roles() returns nothing
|
||||
-- in an org without a direct assignment, delegation or derived role for the
|
||||
-- user, and its result only matters where a rule matches the request.
|
||||
WITH orgs AS MATERIALIZED (
|
||||
SELECT o.org_id
|
||||
FROM (
|
||||
SELECT ur.org_id FROM morbac.user_roles ur WHERE ur.user_id = p_user_id
|
||||
UNION
|
||||
SELECT d.org_id FROM morbac.delegations d WHERE d.delegatee_id = p_user_id
|
||||
UNION
|
||||
SELECT ro.org_id FROM morbac.derived_roles dr
|
||||
INNER JOIN morbac.roles ro ON ro.id = dr.role_id
|
||||
) o
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM morbac.rules r
|
||||
WHERE r.org_id = o.org_id
|
||||
AND r.activity = ANY (v_activities)
|
||||
AND r.view = ANY (v_views)
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM morbac.cross_org_rules cr
|
||||
WHERE cr.source_org_id = o.org_id
|
||||
AND cr.target_org_id = p_org_id
|
||||
AND cr.activity = ANY (v_activities)
|
||||
AND cr.view = ANY (v_views)
|
||||
)
|
||||
),
|
||||
held AS (
|
||||
SELECT DISTINCT c.org_id, g.role_id
|
||||
FROM orgs c
|
||||
CROSS JOIN LATERAL morbac.get_comprehensive_roles(p_user_id, c.org_id) g
|
||||
)
|
||||
SELECT array_agg(h.org_id ORDER BY h.org_id, h.role_id),
|
||||
array_agg(h.role_id ORDER BY h.org_id, h.role_id)
|
||||
INTO v_held_org, v_held_role
|
||||
FROM held h;
|
||||
|
||||
-- Local rules applicable at p_org_id (prohibitions are not collected for
|
||||
-- system principals, which skip them)
|
||||
WITH matched AS MATERIALIZED (
|
||||
SELECT r.org_id, r.scope, r.modality, r.context_id, COALESCE(r.priority, 0) AS prio
|
||||
FROM unnest(v_held_org, v_held_role) AS h(org_id, role_id)
|
||||
INNER JOIN morbac.rules r ON r.org_id = h.org_id AND r.role_id = h.role_id
|
||||
WHERE r.activity = ANY (v_activities)
|
||||
AND r.view = ANY (v_views)
|
||||
AND (r.modality = 'permission' OR (r.modality = 'prohibition' AND NOT v_is_system_principal))
|
||||
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||
),
|
||||
in_scope AS MATERIALIZED (
|
||||
SELECT s.org_id, s.scope
|
||||
FROM (SELECT DISTINCT m.org_id, m.scope FROM matched m) s
|
||||
WHERE morbac.org_in_scope(p_org_id, s.org_id, s.scope)
|
||||
)
|
||||
SELECT array_agg(m.modality ORDER BY m.prio DESC, m.modality, m.context_id),
|
||||
array_agg(m.context_id ORDER BY m.prio DESC, m.modality, m.context_id),
|
||||
array_agg(m.prio ORDER BY m.prio DESC, m.modality, m.context_id)
|
||||
INTO v_local_modality, v_local_context, v_local_prio
|
||||
FROM matched m
|
||||
INNER JOIN in_scope s ON s.org_id = m.org_id AND s.scope = m.scope;
|
||||
|
||||
-- Cross-org rules into p_org_id whose role the user holds in the source org
|
||||
SELECT array_agg(cr.modality ORDER BY COALESCE(cr.priority, 0) DESC, cr.modality, cr.context_id),
|
||||
array_agg(cr.context_id ORDER BY COALESCE(cr.priority, 0) DESC, cr.modality, cr.context_id),
|
||||
array_agg(COALESCE(cr.priority, 0) ORDER BY COALESCE(cr.priority, 0) DESC, cr.modality, cr.context_id)
|
||||
INTO v_cross_modality, v_cross_context, v_cross_prio
|
||||
FROM unnest(v_held_org, v_held_role) AS h(org_id, role_id)
|
||||
INNER JOIN morbac.cross_org_rules cr ON cr.source_org_id = h.org_id AND cr.role_id = h.role_id
|
||||
WHERE cr.target_org_id = p_org_id
|
||||
AND cr.activity = ANY (v_activities)
|
||||
AND cr.view = ANY (v_views)
|
||||
AND (cr.modality = 'permission' OR (cr.modality = 'prohibition' AND NOT v_is_system_principal))
|
||||
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until);
|
||||
|
||||
-- STEPS 1-3.5: Prohibitions - skipped entirely for system principals
|
||||
IF NOT v_is_system_principal THEN
|
||||
|
||||
-- STEP 1: Local prohibitions - find the highest-priority applicable one
|
||||
FOR v_rule IN
|
||||
SELECT u.context_id, u.prio
|
||||
FROM unnest(v_local_modality, v_local_context, v_local_prio) AS u(modality, context_id, prio)
|
||||
WHERE u.modality = 'prohibition'
|
||||
ORDER BY u.prio DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
v_max_prohibition_priority := v_rule.prio;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- STEP 2: Cross-org prohibitions - update max if a higher priority is found
|
||||
FOR v_rule IN
|
||||
SELECT u.context_id, u.prio
|
||||
FROM unnest(v_cross_modality, v_cross_context, v_cross_prio) AS u(modality, context_id, prio)
|
||||
WHERE u.modality = 'prohibition'
|
||||
ORDER BY u.prio DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
IF v_max_prohibition_priority IS NULL OR v_rule.prio > v_max_prohibition_priority THEN
|
||||
v_max_prohibition_priority := v_rule.prio;
|
||||
END IF;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- STEP 3: User-level prohibitions - direct user rules, update max if higher
|
||||
FOR v_rule IN
|
||||
SELECT ur.context_id, COALESCE(ur.priority, 0) AS prio
|
||||
FROM morbac.user_rules ur
|
||||
WHERE ur.user_id = p_user_id
|
||||
AND ur.org_id IS NOT DISTINCT FROM p_org_id
|
||||
AND ur.modality = 'prohibition'
|
||||
AND ur.activity = ANY (v_activities)
|
||||
AND ur.view = ANY (v_views)
|
||||
AND morbac.is_rule_valid(ur.valid_from, ur.valid_until)
|
||||
ORDER BY COALESCE(ur.priority, 0) DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
IF v_max_prohibition_priority IS NULL OR v_rule.prio > v_max_prohibition_priority THEN
|
||||
v_max_prohibition_priority := v_rule.prio;
|
||||
END IF;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- STEP 3.5: Global prohibitions
|
||||
FOR v_rule IN
|
||||
SELECT gr.context_id, COALESCE(gr.priority, 0) AS prio
|
||||
FROM morbac.global_rules gr
|
||||
WHERE (gr.user_id IS NULL OR gr.user_id = p_user_id)
|
||||
AND gr.modality = 'prohibition'
|
||||
AND (gr.activity IS NULL OR gr.activity = ANY (v_activities))
|
||||
AND (gr.view IS NULL OR gr.view = ANY (v_views))
|
||||
AND morbac.is_rule_valid(gr.valid_from, gr.valid_until)
|
||||
ORDER BY COALESCE(gr.priority, 0) DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
IF v_max_prohibition_priority IS NULL OR v_rule.prio > v_max_prohibition_priority THEN
|
||||
v_max_prohibition_priority := v_rule.prio;
|
||||
END IF;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
END IF; -- v_is_system_principal
|
||||
|
||||
-- STEP 4: Local permissions - find the highest-priority applicable one
|
||||
FOR v_rule IN
|
||||
SELECT u.context_id, u.prio
|
||||
FROM unnest(v_local_modality, v_local_context, v_local_prio) AS u(modality, context_id, prio)
|
||||
WHERE u.modality = 'permission'
|
||||
ORDER BY u.prio DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
v_max_permission_priority := v_rule.prio;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- STEP 5: Cross-org permissions - update max if higher found
|
||||
FOR v_rule IN
|
||||
SELECT u.context_id, u.prio
|
||||
FROM unnest(v_cross_modality, v_cross_context, v_cross_prio) AS u(modality, context_id, prio)
|
||||
WHERE u.modality = 'permission'
|
||||
ORDER BY u.prio DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
IF v_max_permission_priority IS NULL OR v_rule.prio > v_max_permission_priority THEN
|
||||
v_max_permission_priority := v_rule.prio;
|
||||
END IF;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- STEP 6: User-level permissions - direct user rules, update max if higher
|
||||
FOR v_rule IN
|
||||
SELECT ur.context_id, COALESCE(ur.priority, 0) AS prio
|
||||
FROM morbac.user_rules ur
|
||||
WHERE ur.user_id = p_user_id
|
||||
AND ur.org_id IS NOT DISTINCT FROM p_org_id
|
||||
AND ur.modality = 'permission'
|
||||
AND ur.activity = ANY (v_activities)
|
||||
AND ur.view = ANY (v_views)
|
||||
AND morbac.is_rule_valid(ur.valid_from, ur.valid_until)
|
||||
ORDER BY COALESCE(ur.priority, 0) DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
IF v_max_permission_priority IS NULL OR v_rule.prio > v_max_permission_priority THEN
|
||||
v_max_permission_priority := v_rule.prio;
|
||||
END IF;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- STEP 6.5: Global permissions
|
||||
FOR v_rule IN
|
||||
SELECT gr.context_id, COALESCE(gr.priority, 0) AS prio
|
||||
FROM morbac.global_rules gr
|
||||
WHERE (gr.user_id IS NULL OR gr.user_id = p_user_id)
|
||||
AND gr.modality = 'permission'
|
||||
AND (gr.activity IS NULL OR gr.activity = ANY (v_activities))
|
||||
AND (gr.view IS NULL OR gr.view = ANY (v_views))
|
||||
AND morbac.is_rule_valid(gr.valid_from, gr.valid_until)
|
||||
ORDER BY COALESCE(gr.priority, 0) DESC
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
IF v_max_permission_priority IS NULL OR v_rule.prio > v_max_permission_priority THEN
|
||||
v_max_permission_priority := v_rule.prio;
|
||||
END IF;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- STEP 7: Priority resolution
|
||||
IF v_max_prohibition_priority IS NULL THEN
|
||||
RETURN v_max_permission_priority IS NOT NULL;
|
||||
END IF;
|
||||
|
||||
IF v_max_permission_priority IS NOT NULL
|
||||
AND v_max_permission_priority > v_max_prohibition_priority THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.is_allowed_nocache(UUID, UUID, TEXT, TEXT) IS
|
||||
'Authorization without cache - use for debugging or when cache must be bypassed';
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.is_allowed(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_activity TEXT,
|
||||
p_view TEXT
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_cached_result BOOLEAN;
|
||||
v_computed_result BOOLEAN;
|
||||
v_expires_at TIMESTAMPTZ;
|
||||
BEGIN
|
||||
IF p_user_id IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
SELECT allowed, expires_at INTO v_cached_result, v_expires_at
|
||||
FROM morbac.auth_cache
|
||||
WHERE user_id = p_user_id
|
||||
AND org_id = p_org_id
|
||||
AND activity = p_activity
|
||||
AND view = p_view
|
||||
AND expires_at > CURRENT_TIMESTAMP;
|
||||
|
||||
IF FOUND THEN
|
||||
RETURN v_cached_result;
|
||||
END IF;
|
||||
|
||||
v_computed_result := morbac.is_allowed_nocache(p_user_id, p_org_id, p_activity, p_view);
|
||||
|
||||
-- NULL org_id (global-rules-only path) cannot be stored in auth_cache (org_id NOT NULL PK).
|
||||
-- Global rule changes flush the entire cache anyway, so skipping is safe.
|
||||
IF p_org_id IS NULL THEN
|
||||
RETURN v_computed_result;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
INSERT INTO morbac.auth_cache (user_id, org_id, activity, view, allowed, expires_at)
|
||||
VALUES (
|
||||
p_user_id,
|
||||
p_org_id,
|
||||
p_activity,
|
||||
p_view,
|
||||
v_computed_result,
|
||||
CURRENT_TIMESTAMP + make_interval(secs => morbac.get_config('cache_ttl_seconds')::integer)
|
||||
)
|
||||
ON CONFLICT (user_id, org_id, activity, view) DO UPDATE
|
||||
SET allowed = v_computed_result,
|
||||
computed_at = CURRENT_TIMESTAMP,
|
||||
expires_at = CURRENT_TIMESTAMP + make_interval(secs => morbac.get_config('cache_ttl_seconds')::integer);
|
||||
EXCEPTION WHEN read_only_sql_transaction THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
RETURN v_computed_result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.is_allowed(UUID, UUID, TEXT, TEXT) IS
|
||||
'Complete OrBAC authorization with caching (default) - use is_allowed_nocache() for debugging';
|
||||
@@ -0,0 +1,29 @@
|
||||
-- =============================================================================
|
||||
-- NULL user Tests
|
||||
-- =============================================================================
|
||||
-- is_allowed() and is_allowed_nocache() answer FALSE for a NULL user id, as
|
||||
-- rls_check() does, instead of failing on the auth_cache NOT NULL constraint.
|
||||
--
|
||||
-- Prerequisites: 00_setup.sql -> 17_is_allowed_equivalence.sql
|
||||
-- =============================================================================
|
||||
|
||||
\echo ''
|
||||
\echo '================================================================'
|
||||
\echo '18 -- NULL USER'
|
||||
\echo '================================================================'
|
||||
|
||||
SELECT morbac.t('is_allowed NULL user, org',
|
||||
morbac.is_allowed(NULL, '10000000-0000-0000-0000-000000000001'::uuid, 'read', 'documents'),
|
||||
FALSE);
|
||||
|
||||
SELECT morbac.t('is_allowed NULL user, no org',
|
||||
morbac.is_allowed(NULL, NULL, 'read', 'documents'),
|
||||
FALSE);
|
||||
|
||||
SELECT morbac.t('is_allowed_nocache NULL user, org',
|
||||
morbac.is_allowed_nocache(NULL, '10000000-0000-0000-0000-000000000001'::uuid, 'read', 'documents'),
|
||||
FALSE);
|
||||
|
||||
SELECT morbac.t('is_allowed NULL user writes nothing to auth_cache',
|
||||
NOT EXISTS (SELECT 1 FROM morbac.auth_cache WHERE user_id IS NULL),
|
||||
TRUE);
|
||||
+1
-1
@@ -51,7 +51,7 @@ for filename in "${BUILD_FILES[@]}"; do
|
||||
if [ -f "$filepath" ]; then
|
||||
cat "$filepath" >> "$OUTPUT"
|
||||
echo "" >> "$OUTPUT"
|
||||
((file_count++))
|
||||
file_count=$((file_count + 1))
|
||||
else
|
||||
echo "Warning: $filename not found, skipping" >&2
|
||||
fi
|
||||
|
||||
+3
-3
@@ -45,7 +45,7 @@ for test_file in "$TEST_DIR"/[0-9][0-9]_*.sql; do
|
||||
[ -f "$test_file" ] || continue
|
||||
|
||||
test_name=$(basename "$test_file" .sql)
|
||||
((total_files++))
|
||||
total_files=$((total_files + 1))
|
||||
|
||||
output=$(psql -U "$PG_USER" -d "$DB" -f "$test_file" 2>&1)
|
||||
exit_code=$?
|
||||
@@ -59,7 +59,7 @@ for test_file in "$TEST_DIR"/[0-9][0-9]_*.sql; do
|
||||
total_fail=$((total_fail + file_fail))
|
||||
|
||||
if [ $exit_code -ne 0 ] || [ "$file_fail" -gt 0 ] || [ "$file_errors" -gt 0 ]; then
|
||||
((failed_files++))
|
||||
failed_files=$((failed_files + 1))
|
||||
echo " [FAIL] $test_name — ${file_pass} passed, ${file_fail} failed, ${file_errors} SQL error(s)"
|
||||
# Print only failing lines for a focused view
|
||||
if [ "$file_errors" -gt 0 ]; then
|
||||
@@ -69,7 +69,7 @@ for test_file in "$TEST_DIR"/[0-9][0-9]_*.sql; do
|
||||
echo "$output" | grep "CHECK FAIL:" | sed 's/^/ /'
|
||||
fi
|
||||
else
|
||||
((passed_files++))
|
||||
passed_files=$((passed_files + 1))
|
||||
echo " [PASS] $test_name — ${file_pass} assertion(s) passed"
|
||||
fi
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user