Add integration tests for deduplication and ref_count
- Add test_integration_uploads.py with 12 tests for duplicate upload scenarios - Add test_ref_count.py with 7 tests for ref_count management - Fix ArtifactDetailResponse to include sha256 and checksum fields - Fix health check SQL warning by wrapping in text() - Update tests to use unique content per test run for idempotency
This commit is contained in:
176
backend/tests/test_ref_count.py
Normal file
176
backend/tests/test_ref_count.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Unit and integration tests for reference counting behavior.
|
||||
|
||||
Tests cover:
|
||||
- ref_count is set correctly for new artifacts
|
||||
- ref_count increments on duplicate uploads
|
||||
- ref_count query correctly identifies existing artifacts
|
||||
- Artifact lookup by SHA256 hash works correctly
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import io
|
||||
from tests.conftest import (
|
||||
compute_sha256,
|
||||
upload_test_file,
|
||||
TEST_CONTENT_HELLO,
|
||||
TEST_HASH_HELLO,
|
||||
)
|
||||
|
||||
|
||||
class TestRefCountQuery:
|
||||
"""Tests for ref_count querying and artifact lookup."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_artifact_lookup_by_sha256(self, integration_client, test_package):
|
||||
"""Test artifact lookup by SHA256 hash (primary key) works correctly."""
|
||||
project, package = test_package
|
||||
content = b"unique content for lookup test"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# Upload a file
|
||||
upload_result = upload_test_file(
|
||||
integration_client, project, package, content, tag="v1"
|
||||
)
|
||||
assert upload_result["artifact_id"] == expected_hash
|
||||
|
||||
# Look up artifact by ID (SHA256)
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.status_code == 200
|
||||
|
||||
artifact = response.json()
|
||||
assert artifact["id"] == expected_hash
|
||||
assert artifact["sha256"] == expected_hash
|
||||
assert artifact["size"] == len(content)
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_ref_count_query_identifies_existing_artifact(
|
||||
self, integration_client, test_package
|
||||
):
|
||||
"""Test ref_count query correctly identifies existing artifacts by hash."""
|
||||
project, package = test_package
|
||||
content = b"content for ref count query test"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# Upload a file with a tag
|
||||
upload_result = upload_test_file(
|
||||
integration_client, project, package, content, tag="v1"
|
||||
)
|
||||
|
||||
# Query artifact and check ref_count
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.status_code == 200
|
||||
|
||||
artifact = response.json()
|
||||
assert artifact["ref_count"] >= 1 # At least 1 from the tag
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_ref_count_set_to_1_for_new_artifact_with_tag(
|
||||
self, integration_client, test_package, unique_test_id
|
||||
):
|
||||
"""Test ref_count is set to 1 for new artifacts when created with a tag."""
|
||||
project, package = test_package
|
||||
content = f"brand new content for ref count test {unique_test_id}".encode()
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# Upload a new file with a tag
|
||||
upload_result = upload_test_file(
|
||||
integration_client, project, package, content, tag="initial"
|
||||
)
|
||||
|
||||
assert upload_result["artifact_id"] == expected_hash
|
||||
assert upload_result["ref_count"] == 1
|
||||
assert upload_result["deduplicated"] is False
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_ref_count_increments_on_duplicate_upload_with_tag(
|
||||
self, integration_client, test_package, unique_test_id
|
||||
):
|
||||
"""Test ref_count is incremented when duplicate content is uploaded with a new tag."""
|
||||
project, package = test_package
|
||||
content = f"content that will be uploaded twice {unique_test_id}".encode()
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# First upload with tag
|
||||
result1 = upload_test_file(
|
||||
integration_client, project, package, content, tag="v1"
|
||||
)
|
||||
assert result1["ref_count"] == 1
|
||||
assert result1["deduplicated"] is False
|
||||
|
||||
# Second upload with different tag (same content)
|
||||
result2 = upload_test_file(
|
||||
integration_client, project, package, content, tag="v2"
|
||||
)
|
||||
assert result2["artifact_id"] == expected_hash
|
||||
assert result2["ref_count"] == 2
|
||||
assert result2["deduplicated"] is True
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_ref_count_after_multiple_tags(self, integration_client, test_package):
|
||||
"""Test ref_count correctly reflects number of tags pointing to artifact."""
|
||||
project, package = test_package
|
||||
content = b"content for multiple tag test"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# Upload with multiple tags
|
||||
tags = ["v1", "v2", "v3", "latest"]
|
||||
for i, tag in enumerate(tags):
|
||||
result = upload_test_file(
|
||||
integration_client, project, package, content, tag=tag
|
||||
)
|
||||
assert result["artifact_id"] == expected_hash
|
||||
assert result["ref_count"] == i + 1
|
||||
|
||||
# Verify final ref_count via artifact endpoint
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ref_count"] == len(tags)
|
||||
|
||||
|
||||
class TestRefCountWithDeletion:
|
||||
"""Tests for ref_count behavior when tags are deleted."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_ref_count_decrements_on_tag_delete(self, integration_client, test_package):
|
||||
"""Test ref_count decrements when a tag is deleted."""
|
||||
project, package = test_package
|
||||
content = b"content for delete test"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# Upload with two tags
|
||||
upload_test_file(integration_client, project, package, content, tag="v1")
|
||||
upload_test_file(integration_client, project, package, content, tag="v2")
|
||||
|
||||
# Verify ref_count is 2
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.json()["ref_count"] == 2
|
||||
|
||||
# Delete one tag
|
||||
delete_response = integration_client.delete(
|
||||
f"/api/v1/project/{project}/{package}/tags/v1"
|
||||
)
|
||||
assert delete_response.status_code == 204
|
||||
|
||||
# Verify ref_count is now 1
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.json()["ref_count"] == 1
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_ref_count_zero_after_all_tags_deleted(
|
||||
self, integration_client, test_package
|
||||
):
|
||||
"""Test ref_count goes to 0 when all tags are deleted."""
|
||||
project, package = test_package
|
||||
content = b"content that will be orphaned"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# Upload with one tag
|
||||
upload_test_file(integration_client, project, package, content, tag="only-tag")
|
||||
|
||||
# Delete the tag
|
||||
integration_client.delete(f"/api/v1/project/{project}/{package}/tags/only-tag")
|
||||
|
||||
# Verify ref_count is 0
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.json()["ref_count"] == 0
|
||||
Reference in New Issue
Block a user