Fix CI integration test failures

- Add proxy-body-size annotation to allow unlimited uploads via nginx
- Add requires_direct_s3 marker for tests needing direct MinIO access
- Exclude requires_direct_s3 tests from CI (can't access MinIO from outside K8s)
- Add auto-migration for tag ref_count triggers to ensure they exist
This commit is contained in:
Mondo Diaz
2026-01-16 21:49:11 +00:00
parent 1f923ae71d
commit 423de8939e
9 changed files with 79 additions and 2 deletions

View File

@@ -52,13 +52,15 @@ kics:
- pip install --index-url "$PIP_INDEX_URL" pytest pytest-asyncio httpx - pip install --index-url "$PIP_INDEX_URL" pytest pytest-asyncio httpx
script: script:
- cd backend - cd backend
# Run full integration test suite, excluding large/slow tests # Run full integration test suite, excluding:
# - large/slow tests
# - requires_direct_s3 tests (can't access MinIO from outside K8s cluster)
# ORCHARD_TEST_URL tells the tests which server to connect to # ORCHARD_TEST_URL tells the tests which server to connect to
# Note: Auth tests work because dev/stage deployments have relaxed rate limits # Note: Auth tests work because dev/stage deployments have relaxed rate limits
- | - |
python -m pytest tests/integration/ -v \ python -m pytest tests/integration/ -v \
--junitxml=integration-report.xml \ --junitxml=integration-report.xml \
-m "not large and not slow" \ -m "not large and not slow and not requires_direct_s3" \
--tb=short --tb=short
artifacts: artifacts:
when: always when: always

View File

@@ -84,6 +84,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed CI integration test rate limiting: added configurable `ORCHARD_LOGIN_RATE_LIMIT` env var, relaxed to 1000/minute for dev/stage - Fixed CI integration test rate limiting: added configurable `ORCHARD_LOGIN_RATE_LIMIT` env var, relaxed to 1000/minute for dev/stage
- Fixed duplicate `TestSecurityEdgeCases` class definition in test_auth_api.py - Fixed duplicate `TestSecurityEdgeCases` class definition in test_auth_api.py
- Fixed integration tests auth: session-scoped client, configurable credentials via env vars, fail-fast on auth errors - Fixed integration tests auth: session-scoped client, configurable credentials via env vars, fail-fast on auth errors
- Fixed 413 Request Entity Too Large errors on uploads by adding `proxy-body-size: "0"` nginx annotation to Orchard ingress
- Fixed CI tests that require direct S3 access: added `@pytest.mark.requires_direct_s3` marker and excluded from CI
- Fixed ref_count triggers not being created: added auto-migration for tags ref_count trigger functions
- Fixed Content-Disposition header encoding for non-ASCII filenames using RFC 5987 (#38) - Fixed Content-Disposition header encoding for non-ASCII filenames using RFC 5987 (#38)
- Fixed deploy jobs running even when tests or security scans fail (changed rules from `when: always` to `when: on_success`) (#63) - Fixed deploy jobs running even when tests or security scans fail (changed rules from `when: always` to `when: on_success`) (#63)
- Fixed python_tests job not using internal PyPI proxy (#63) - Fixed python_tests job not using internal PyPI proxy (#63)

View File

@@ -170,6 +170,62 @@ def _run_migrations():
END IF; END IF;
END $$; END $$;
""", """,
# Create ref_count trigger functions for tags (ensures triggers exist even if initial migration wasn't run)
"""
CREATE OR REPLACE FUNCTION increment_artifact_ref_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE artifacts SET ref_count = ref_count + 1 WHERE id = NEW.artifact_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
""",
"""
CREATE OR REPLACE FUNCTION decrement_artifact_ref_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE artifacts SET ref_count = ref_count - 1 WHERE id = OLD.artifact_id;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
""",
"""
CREATE OR REPLACE FUNCTION update_artifact_ref_count()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.artifact_id != NEW.artifact_id THEN
UPDATE artifacts SET ref_count = ref_count - 1 WHERE id = OLD.artifact_id;
UPDATE artifacts SET ref_count = ref_count + 1 WHERE id = NEW.artifact_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
""",
# Create triggers for tags ref_count management
"""
DO $$
BEGIN
-- Drop and recreate triggers to ensure they're current
DROP TRIGGER IF EXISTS tags_ref_count_insert_trigger ON tags;
CREATE TRIGGER tags_ref_count_insert_trigger
AFTER INSERT ON tags
FOR EACH ROW
EXECUTE FUNCTION increment_artifact_ref_count();
DROP TRIGGER IF EXISTS tags_ref_count_delete_trigger ON tags;
CREATE TRIGGER tags_ref_count_delete_trigger
AFTER DELETE ON tags
FOR EACH ROW
EXECUTE FUNCTION decrement_artifact_ref_count();
DROP TRIGGER IF EXISTS tags_ref_count_update_trigger ON tags;
CREATE TRIGGER tags_ref_count_update_trigger
AFTER UPDATE ON tags
FOR EACH ROW
WHEN (OLD.artifact_id IS DISTINCT FROM NEW.artifact_id)
EXECUTE FUNCTION update_artifact_ref_count();
END $$;
""",
# Create ref_count trigger functions for package_versions # Create ref_count trigger functions for package_versions
""" """
CREATE OR REPLACE FUNCTION increment_version_ref_count() CREATE OR REPLACE FUNCTION increment_version_ref_count()

View File

@@ -34,6 +34,10 @@ def pytest_configure(config):
"markers", "markers",
"slow: marks tests as slow running", "slow: marks tests as slow running",
) )
config.addinivalue_line(
"markers",
"requires_direct_s3: marks tests that require direct S3/MinIO access (skipped in CI where S3 is not directly accessible)",
)
import io import io

View File

@@ -489,11 +489,15 @@ class TestArtifactIntegrityEndpoint:
assert len(response.content) == expected_size assert len(response.content) == expected_size
@pytest.mark.requires_direct_s3
class TestCorruptionDetection: class TestCorruptionDetection:
"""Tests for detecting corrupted S3 objects. """Tests for detecting corrupted S3 objects.
These tests directly manipulate S3 objects to simulate corruption These tests directly manipulate S3 objects to simulate corruption
and verify that the system can detect hash mismatches. and verify that the system can detect hash mismatches.
Note: These tests require direct S3/MinIO access and are skipped in CI
where S3 is not directly accessible from the test runner.
""" """
@pytest.mark.integration @pytest.mark.integration

View File

@@ -101,6 +101,7 @@ class TestLargeFileUploads:
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.slow @pytest.mark.slow
@pytest.mark.requires_direct_s3
def test_upload_100mb_file(self, integration_client, test_package, sized_content): def test_upload_100mb_file(self, integration_client, test_package, sized_content):
"""Test uploading a 100MB file (triggers multipart upload).""" """Test uploading a 100MB file (triggers multipart upload)."""
project, package = test_package project, package = test_package

View File

@@ -158,6 +158,7 @@ class TestUploadBasics:
assert artifact["size"] == len(content) assert artifact["size"] == len(content)
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.requires_direct_s3
def test_upload_creates_object_in_s3(self, integration_client, test_package): def test_upload_creates_object_in_s3(self, integration_client, test_package):
"""Test upload creates object in S3 storage.""" """Test upload creates object in S3 storage."""
project, package = test_package project, package = test_package
@@ -635,6 +636,7 @@ class TestUploadFailureCleanup:
"""Tests for cleanup when uploads fail.""" """Tests for cleanup when uploads fail."""
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.requires_direct_s3
def test_upload_failure_invalid_project_no_orphaned_s3( def test_upload_failure_invalid_project_no_orphaned_s3(
self, integration_client, unique_test_id self, integration_client, unique_test_id
): ):
@@ -657,6 +659,7 @@ class TestUploadFailureCleanup:
) )
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.requires_direct_s3
def test_upload_failure_invalid_package_no_orphaned_s3( def test_upload_failure_invalid_package_no_orphaned_s3(
self, integration_client, test_project, unique_test_id self, integration_client, test_project, unique_test_id
): ):
@@ -704,6 +707,7 @@ class TestS3StorageVerification:
"""Tests to verify S3 storage behavior.""" """Tests to verify S3 storage behavior."""
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.requires_direct_s3
def test_s3_single_object_after_duplicates( def test_s3_single_object_after_duplicates(
self, integration_client, test_package, unique_test_id self, integration_client, test_package, unique_test_id
): ):
@@ -759,6 +763,7 @@ class TestSecurityPathTraversal:
""" """
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.requires_direct_s3
def test_path_traversal_in_filename_stored_safely( def test_path_traversal_in_filename_stored_safely(
self, integration_client, test_package self, integration_client, test_package
): ):

View File

@@ -42,6 +42,7 @@ ingress:
className: "nginx" className: "nginx"
annotations: annotations:
cert-manager.io/cluster-issuer: "letsencrypt" cert-manager.io/cluster-issuer: "letsencrypt"
nginx.ingress.kubernetes.io/proxy-body-size: "0" # Disable body size limit for uploads
hosts: hosts:
- host: orchard-dev.common.global.bsf.tools # Overridden by CI - host: orchard-dev.common.global.bsf.tools # Overridden by CI
paths: paths:

View File

@@ -41,6 +41,7 @@ ingress:
className: "nginx" className: "nginx"
annotations: annotations:
cert-manager.io/cluster-issuer: "letsencrypt" cert-manager.io/cluster-issuer: "letsencrypt"
nginx.ingress.kubernetes.io/proxy-body-size: "0" # Disable body size limit for uploads
hosts: hosts:
- host: orchard-stage.common.global.bsf.tools - host: orchard-stage.common.global.bsf.tools
paths: paths: