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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,14 @@ All endpoints are under `/api/v1/`:
|----------------------------|--------|------------------------------------------|
| `/health/` | GET | API health check |
| `/list/?filter=...` | GET | List available core packages |
| `/<package_name>/get/` | GET | Download a `.core` file |
| `/get/?core=...` | GET | Download a `.core` file by VLNV name |
| `/validate/` | POST | Validate a core file (`multipart/form`) |
| `/publish/` | POST | Publish a core file to GitHub |

- **Download (`/get/`)**: Provide the `core` query parameter with the full VLNV (e.g., `acme:lib1:foo:1.0.0`).
- **Validation and publishing**: Upload core files (and optional signatures) via `multipart/form-data`.
- **OpenAPI/Swagger docs**: Interactive documentation is available at `/api/v1/docs/swagger/` and `/api/v1/docs/redoc/`.

---

## Web UI
Expand Down
13 changes: 13 additions & 0 deletions core_directory/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ def is_signed(self):
"""
return bool(self.sig_url)

@property
def sanitized_vlnv(self):
"""
Returns a filesystem- and URL-safe version of the core's VLNV (Vendor:Library:Name:Version),
with colons and other problematic characters replaced by underscores.
"""
return (
f'{self.project.vendor.sanitized_name}_'
f'{self.project.library.sanitized_name}_'
f'{self.project.sanitized_name}_'
f'{self.sanitized_name}'
)

class Meta:
"""Ensure version is unique per project."""
unique_together = ('project', 'version')
Expand Down
130 changes: 81 additions & 49 deletions core_directory/tests/api/test_cores.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,106 @@
from unittest import mock
import pytest
from django.urls import reverse
from core_directory.models import Vendor, Library, Project, CorePackage

@pytest.mark.django_db
@mock.patch("core_directory.views.api_views.Github")
def test_cores_success(mock_github, client, mocker):
def test_cores_success(client, mocker):
url = reverse('core_directory:core_list')
# Mock GitHub repo and contents
mock_repo = mock.Mock()
mock_content = mock.Mock()
mock_content.type = "file"
mock_content.path = "foo.core"
mock_repo.get_contents.return_value = [mock_content]
mock_github.return_value.get_repo.return_value = mock_repo
mocker.patch("os.getenv", side_effect=lambda key, default=None: "dummy_token" if key == "GITHUB_ACCESS_TOKEN" else default)
# Mock the queryset to return a list of vlnv_names
mock_qs = mocker.Mock()
mock_qs.order_by.return_value = mock_qs
mock_qs.values_list.return_value = ["acme:lib1:core1:1.0.0"]
mock_filter = mocker.patch("core_directory.models.CorePackage.objects.filter", return_value=mock_qs)

response = client.get(url)
assert response.status_code == 200
assert response.json() == ["foo"]
assert response.json() == ["acme:lib1:core1:1.0.0"]
mock_filter.assert_called_once_with(vlnv_name__icontains="")

@pytest.mark.django_db
def test_cores_github_exception(client, mocker):
url = reverse('core_directory:core_list')
mock_repo = mocker.Mock()
# Define a mock exception with a .data attribute
class GithubException(Exception):
def __init__(self):
self.data = "fail"
mock_repo.get_contents.side_effect = GithubException()
mock_github = mocker.patch("core_directory.views.api_views.Github")
mock_github.return_value.get_repo.return_value = mock_repo
mocker.patch("core_directory.views.api_views.GithubException", GithubException)
mocker.patch("os.getenv", side_effect=lambda key, default=None: "dummy_token" if key == "GITHUB_ACCESS_TOKEN" else default)
def test_multiple_cores_success(client):
# Set up test data: two cores in the database
vendor = Vendor.objects.create(name="Acme")
library = Library.objects.create(vendor=vendor, name="Lib1")
project1 = Project.objects.create(vendor=vendor, library=library, name="Core1", description="desc")
project2 = Project.objects.create(vendor=vendor, library=library, name="Core2", description="desc")
CorePackage.objects.create(
project=project1,
vlnv_name="acme:lib1:core1:1.0.0",
version="1.0.0",
version_major=1,
version_minor=0,
version_patch=0,
core_url="https://example.com/core1",
description="desc"
)
CorePackage.objects.create(
project=project2,
vlnv_name="acme:lib1:core2:1.0.0",
version="1.0.0",
version_major=1,
version_minor=0,
version_patch=0,
core_url="https://example.com/core2_v1.0.0",
description="desc"
)
CorePackage.objects.create(
project=project2,
vlnv_name="acme:lib1:core2:0.1.0",
version="0.1.0",
version_major=0,
version_minor=1,
version_patch=0,
core_url="https://example.com/core2_v0.1.0",
description="desc"
)

url = reverse('core_directory:core_list')
response = client.get(url)
assert response.status_code == 500
assert "GitHub error" in str(response.content)
assert "fail" in str(response.content)

import pytest
from django.urls import reverse
assert response.status_code == 200
# The API returns a list of vlnv_names
assert set(response.json()) == {"acme:lib1:core1:1.0.0", "acme:lib1:core2:0.1.0", "acme:lib1:core2:1.0.0"}

@pytest.mark.django_db
def test_cores_with_filter(client, mocker):
def test_cores_with_filter(client):
# Set up test data: two cores in the database
vendor = Vendor.objects.create(name="Acme")
library = Library.objects.create(vendor=vendor, name="Lib1")
project1 = Project.objects.create(vendor=vendor, library=library, name="foo_core", description="desc")
project2 = Project.objects.create(vendor=vendor, library=library, name="bar_core", description="desc")
cp1 = CorePackage.objects.create(
project=project1,
vlnv_name="acme:lib1:foo_core:1.0.0",
version="1.0.0",
version_major=1,
version_minor=0,
version_patch=0,
core_url="https://example.com/foo_core",
description="desc"
)
cp2 = CorePackage.objects.create(
project=project2,
vlnv_name="acme:lib1:bar_core:1.0.0",
version="1.0.0",
version_major=1,
version_minor=0,
version_patch=0,
core_url="https://example.com/bar_core",
description="desc"
)

url = reverse('core_directory:core_list')
mock_repo = mocker.Mock()
# Simulate two core files, only one matches the filter
mock_content1 = mocker.Mock()
mock_content1.type = "file"
mock_content1.path = "foo.core"
mock_content2 = mocker.Mock()
mock_content2.type = "file"
mock_content2.path = "bar.core"
mock_repo.get_contents.return_value = [mock_content1, mock_content2]
mock_github = mocker.patch("core_directory.views.api_views.Github")
mock_github.return_value.get_repo.return_value = mock_repo
mocker.patch("os.getenv", side_effect=lambda key, default=None: "dummy_token" if key == "GITHUB_ACCESS_TOKEN" else default)

# Apply filter 'foo'
response = client.get(url, {"filter": "foo"})
assert response.status_code == 200
data = response.json()
assert data == ["foo"]
assert response.json() == ["acme:lib1:foo_core:1.0.0"]

# Apply filter 'bar'
response = client.get(url, {"filter": "bar"})
assert response.status_code == 200
data = response.json()
assert data == ["bar"]
assert response.json() == ["acme:lib1:bar_core:1.0.0"]

# Apply filter that matches nothing
response = client.get(url, {"filter": "baz"})
assert response.status_code == 200
data = response.json()
assert data == []
assert response.json() == []
105 changes: 61 additions & 44 deletions core_directory/tests/api/test_get_core.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,78 @@
import pytest
from django.urls import reverse
from core_directory.models import Vendor, Library, Project, CorePackage

@pytest.mark.django_db
def test_getcore_success(client, mocker):
url = reverse('core_directory:core_get', kwargs={"package_name": "foo"})
# Mock the repo and content
mock_repo = mocker.Mock()
mock_content = mocker.Mock()
mock_content.decoded_content = b"core content"
mock_repo.get_contents.return_value = mock_content
mock_github = mocker.patch("core_directory.views.api_views.Github")
mock_github.return_value.get_repo.return_value = mock_repo
mocker.patch("os.getenv", side_effect=lambda key, default=None: "dummy_token" if key == "GITHUB_ACCESS_TOKEN" else default)
# Set up test data
vendor = Vendor.objects.create(name="Acme")
library = Library.objects.create(vendor=vendor, name="Lib1")
project = Project.objects.create(vendor=vendor, library=library, name="foo", description="desc")
core_package = CorePackage.objects.create(
project=project,
vlnv_name="acme:lib1:foo:1.0.0",
version="1.0.0",
version_major=1,
version_minor=0,
version_patch=0,
core_url="https://example.com/foo.core",
description="desc"
)

response = client.get(url)
# Mock requests.get to return a fake file
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.content = b"core file content"
mocker.patch("requests.get", return_value=mock_response)

url = reverse('core_directory:core_get')
response = client.get(url, {"core": "acme:lib1:foo:1.0.0"})
assert response.status_code == 200
assert b"core content" in response.content
assert response["Content-Disposition"].endswith("foo.core")
assert b'core file content' in response.content
assert response["Content-Disposition"].endswith("acme_lib1_foo_1.0.0.core")

@pytest.mark.django_db
def test_getcore_not_found(client, mocker):
url = reverse('core_directory:core_get', kwargs={"package_name": "foo"})
mock_repo = mocker.Mock()
class NotFound(Exception):
status = 404
data = "not found"
mock_repo.get_contents.side_effect = NotFound()
mock_github = mocker.patch("core_directory.views.api_views.Github")
mock_github.return_value.get_repo.return_value = mock_repo
mocker.patch("core_directory.views.api_views.GithubException", NotFound)
mocker.patch("os.getenv", side_effect=lambda key, default=None: "dummy_token" if key == "GITHUB_ACCESS_TOKEN" else default)

response = client.get(url)
def test_getcore_not_found(client):
url = reverse('core_directory:core_get')
response = client.get(url, {"core": "acme:lib1:doesnotexist:1.0.0"})
assert response.status_code == 404
assert "not found" in str(response.content)
assert b"not available" in response.content or b"not available" in response.json().get("error", "").lower()

import pytest
from django.urls import reverse
from core_directory.models import Vendor, Library, Project, CorePackage

@pytest.mark.django_db
def test_getcore_github_exception(client, mocker):
url = reverse('core_directory:core_get', kwargs={"package_name": "foo"})

# Define a mock GithubException with .status and .data attributes
class GithubException(Exception):
def __init__(self, status=500, data="fail"):
self.status = status
self.data = data

mock_repo = mocker.Mock()
mock_repo.get_contents.side_effect = GithubException(500, "fail")
mock_github = mocker.patch("core_directory.views.api_views.Github")
mock_github.return_value.get_repo.return_value = mock_repo
mocker.patch("core_directory.views.api_views.GithubException", GithubException)
mocker.patch("os.getenv", side_effect=lambda key, default=None: "dummy_token" if key == "GITHUB_ACCESS_TOKEN" else default)
def test_getcore_file_not_found(client, mocker):
# Set up test data: core exists in DB
vendor = Vendor.objects.create(name="Acme")
library = Library.objects.create(vendor=vendor, name="Lib1")
project = Project.objects.create(vendor=vendor, library=library, name="foo", description="desc")
core_package = CorePackage.objects.create(
project=project,
vlnv_name="acme:lib1:foo:1.0.0",
version="1.0.0",
version_major=1,
version_minor=0,
version_patch=0,
core_url="https://example.com/foo.core",
description="desc"
)

# Mock requests.get to simulate file not found (404)
mock_response = mocker.Mock()
mock_response.status_code = 404
mock_response.content = b""
mocker.patch("requests.get", return_value=mock_response)

url = reverse('core_directory:core_get')
response = client.get(url, {"core": "acme:lib1:foo:1.0.0"})
assert response.status_code == 404
assert b"not found" in response.content or b"not found" in response.json().get("error", "").lower()

@pytest.mark.django_db
def test_getcore_missing_param(client):
url = reverse("core_directory:core_get")
response = client.get(url)
assert response.status_code == 500
assert b"GitHub error" in response.content
assert b"fail" in response.content
assert response.status_code == 400
assert b"missing" in response.content or b"required" in response.content
42 changes: 40 additions & 2 deletions core_directory/tests/api/test_publish.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import pytest

from io import BytesIO

from django.urls import reverse
from django.core.files.uploadedfile import SimpleUploadedFile

from core_directory.models import Vendor, Library, Project, CorePackage

@pytest.mark.django_db
def test_publish_success(client, mocker):
url = reverse('core_directory:publish')
Expand All @@ -28,11 +33,44 @@ class GithubException(Exception):
mocker.patch("os.getenv", side_effect=lambda key, default=None: "dummy_token" if key == "GITHUB_ACCESS_TOKEN" else default)

response = client.post(url, data={"core_file": SimpleUploadedFile("test.core", b"dummy")})
assert response.status_code in (200, 201)
assert response.status_code is 201
assert b"published" in response.content or b"valid" in response.content

@pytest.mark.django_db
def test_publish_already_exists(client, mocker):
def test_publish_core_already_exists_in_db(client, mocker):
# Set up test data: create a core with the same VLNV in the database
vendor = Vendor.objects.create(name="Acme")
library = Library.objects.create(vendor=vendor, name="Lib1")
project = Project.objects.create(vendor=vendor, library=library, name="foo", description="desc")
CorePackage.objects.create(
project=project,
vlnv_name="acme:lib1:foo:1.0.0",
version="1.0.0",
version_major=1,
version_minor=0,
version_patch=0,
core_url="https://example.com/foo.core",
description="desc"
)

url = reverse('core_directory:publish')
# Mock serializer
mock_serializer = mocker.patch("core_directory.views.api_views.CoreSerializer")
instance = mock_serializer.return_value
instance.is_valid.return_value = True
instance.validated_data = {
"vlnv_name": "acme:lib1:foo:1.0.0",
"core_file": SimpleUploadedFile("test.core", b"dummy"),
"sanitized_name": "core",
"signature_file": None,
}

response = client.post(url, data={"core_file": SimpleUploadedFile("test.core", b"dummy")})
assert response.status_code == 409
assert b"already exists" in response.content

@pytest.mark.django_db
def test_publish_already_exists_on_github(client, mocker):
url = reverse('core_directory:publish')
mock_serializer = mocker.patch("core_directory.views.api_views.CoreSerializer")
instance = mock_serializer.return_value
Expand Down
2 changes: 1 addition & 1 deletion core_directory/tests/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
("redirect_to_docs", {}, 301, False, "get"),
("health_check", {}, 200, False, "get"),
("core_list", {}, 200, False, "get"),
("core_get", {"package_name": "example"}, 200, False, "get"),
("core_get", {}, 400, False, "get"),
("validate", {}, 400, False, "post"),
("publish", {}, 400, False, "post"),
("api_docs_landing", {}, 200, False, "get"),
Expand Down
2 changes: 1 addition & 1 deletion core_directory/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
path('', RedirectView.as_view(url='docs/', permanent=True), name='redirect_to_docs'),
path('health/', HealthCheckView.as_view(), name='health_check'),
path('list/', Cores.as_view(), name='core_list'),
path('<str:package_name>/get/', GetCore.as_view(), name='core_get'),
path('get/', GetCore.as_view(), name='core_get'),
path('validate/', Validate.as_view(), name='validate'),
path('publish/', Publish.as_view(), name='publish'),

Expand Down
Loading