Skip to content

Commit 56600ca

Browse files
Unit and Integration tests for local repo (#3991)
1 parent 4d72b30 commit 56600ca

26 files changed

+3999
-82
lines changed

build_stream/container.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,6 @@ class ProdContainer(containers.DeclarativeContainer): # pylint: disable=R0903
295295
NfsPlaybookQueueResultRepository,
296296
)
297297

298-
299-
300-
301-
302-
303-
304298
# --- Local repo services ---
305299
input_file_service = providers.Factory(
306300
InputFileService,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Auth API integration fixtures using mock vault credentials."""
16+
17+
import base64
18+
from typing import Dict
19+
20+
import pytest
21+
22+
from tests.mocks.mock_vault_client import MockVaultClient
23+
24+
25+
@pytest.fixture
26+
def valid_auth_header() -> Dict[str, str]:
27+
"""Create valid Basic Auth header for registration endpoint."""
28+
credentials = base64.b64encode(
29+
f"{MockVaultClient.DEFAULT_TEST_USERNAME}:{MockVaultClient.DEFAULT_TEST_PASSWORD}".encode()
30+
).decode()
31+
return {"Authorization": f"Basic {credentials}"}
32+
33+
34+
@pytest.fixture
35+
def invalid_auth_header() -> Dict[str, str]:
36+
"""Create invalid Basic Auth header."""
37+
credentials = base64.b64encode(b"wrong_user:wrong_password").decode()
38+
return {"Authorization": f"Basic {credentials}"}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Shared fixtures for Local Repository API integration tests."""
16+
17+
import os
18+
from pathlib import Path
19+
from typing import Dict
20+
21+
import pytest
22+
from fastapi.testclient import TestClient
23+
from api.dependencies import verify_token
24+
25+
from main import app
26+
from infra.id_generator import UUIDv4Generator
27+
28+
29+
@pytest.fixture(scope="function")
30+
def client():
31+
"""Create test client with fresh container for each test."""
32+
os.environ["ENV"] = "dev"
33+
34+
def mock_verify_token():
35+
return {
36+
"sub": "test-client-123",
37+
"client_id": "test-client-123",
38+
"scopes": ["job:write", "job:read"]
39+
}
40+
41+
app.dependency_overrides[verify_token] = mock_verify_token
42+
43+
test_client = TestClient(app)
44+
45+
yield test_client
46+
47+
# Cleanup
48+
app.dependency_overrides.clear()
49+
50+
51+
@pytest.fixture(name="uuid_generator")
52+
def uuid_generator_fixture():
53+
"""UUID generator for test fixtures."""
54+
return UUIDv4Generator()
55+
56+
57+
@pytest.fixture(name="auth_headers")
58+
def auth_headers_fixture(uuid_generator) -> Dict[str, str]:
59+
"""Standard authentication headers for testing."""
60+
return {
61+
"Authorization": "Bearer test-client-123",
62+
"X-Correlation-Id": str(uuid_generator.generate()),
63+
"Idempotency-Key": f"test-key-{uuid_generator.generate()}",
64+
}
65+
66+
67+
@pytest.fixture
68+
def unique_correlation_id(uuid_generator) -> str:
69+
"""Generate unique correlation ID for each test."""
70+
return str(uuid_generator.generate())
71+
72+
73+
@pytest.fixture
74+
def created_job(client, auth_headers) -> str:
75+
"""Create a job and return its job_id."""
76+
payload = {"client_id": "test-client-123", "client_name": "test-client"}
77+
response = client.post("/api/v1/jobs", json=payload, headers=auth_headers)
78+
assert response.status_code == 201
79+
return response.json()["job_id"]
80+
81+
82+
@pytest.fixture
83+
def nfs_queue_dir(tmp_path):
84+
"""Create temporary NFS queue directory structure."""
85+
requests_dir = tmp_path / "requests"
86+
results_dir = tmp_path / "results"
87+
archive_dir = tmp_path / "archive" / "results"
88+
processing_dir = tmp_path / "processing"
89+
90+
requests_dir.mkdir(parents=True)
91+
results_dir.mkdir(parents=True)
92+
archive_dir.mkdir(parents=True)
93+
processing_dir.mkdir(parents=True)
94+
95+
return tmp_path
96+
97+
98+
@pytest.fixture
99+
def input_dir(tmp_path):
100+
"""Create temporary input directory with sample files."""
101+
base = tmp_path / "build_stream"
102+
return base
103+
104+
105+
def setup_input_files(input_dir_path: Path, job_id: str) -> Path:
106+
"""Create input files for a given job_id."""
107+
job_input = input_dir_path / job_id / "input"
108+
job_input.mkdir(parents=True, exist_ok=True)
109+
(job_input / "config.json").write_text('{"cluster_os": "rhel9.2"}')
110+
return job_input
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Integration tests for Local Repository create API."""
16+
17+
from unittest.mock import patch
18+
19+
from tests.integration.api.local_repo.conftest import setup_input_files
20+
21+
22+
class TestCreateLocalRepoSuccess:
23+
"""Happy-path create local repository tests."""
24+
25+
def test_returns_202_with_valid_request(
26+
self, client, auth_headers, created_job, nfs_queue_dir, input_dir
27+
):
28+
setup_input_files(input_dir, created_job)
29+
30+
with patch(
31+
"infra.repositories.nfs_input_repository"
32+
".NfsInputRepository.get_source_input_repository_path",
33+
return_value=input_dir / created_job / "input",
34+
), patch(
35+
"infra.repositories.nfs_input_repository"
36+
".NfsInputRepository.get_destination_input_repository_path",
37+
return_value=nfs_queue_dir / "dest_input",
38+
), patch(
39+
"infra.repositories.nfs_input_repository"
40+
".NfsInputRepository.validate_input_directory",
41+
return_value=True,
42+
), patch(
43+
"infra.repositories.nfs_playbook_queue_request_repository"
44+
".NfsPlaybookQueueRequestRepository.is_available",
45+
return_value=True,
46+
), patch(
47+
"infra.repositories.nfs_playbook_queue_request_repository"
48+
".NfsPlaybookQueueRequestRepository.write_request",
49+
return_value=nfs_queue_dir / "requests" / "test.json",
50+
):
51+
response = client.post(
52+
f"/api/v1/jobs/{created_job}/stages/create-local-repository",
53+
headers=auth_headers,
54+
)
55+
56+
assert response.status_code == 202
57+
data = response.json()
58+
assert data["job_id"] == created_job
59+
assert data["stage"] == "create-local-repository"
60+
assert data["status"] == "accepted"
61+
assert "submitted_at" in data
62+
assert "correlation_id" in data
63+
64+
def test_returns_correlation_id(
65+
self, client, created_job, unique_correlation_id,
66+
nfs_queue_dir, input_dir
67+
):
68+
setup_input_files(input_dir, created_job)
69+
headers = {
70+
"Authorization": "Bearer test-client-123",
71+
"X-Correlation-Id": unique_correlation_id,
72+
}
73+
74+
with patch(
75+
"infra.repositories.nfs_input_repository"
76+
".NfsInputRepository.get_source_input_repository_path",
77+
return_value=input_dir / created_job / "input",
78+
), patch(
79+
"infra.repositories.nfs_input_repository"
80+
".NfsInputRepository.get_destination_input_repository_path",
81+
return_value=nfs_queue_dir / "dest_input",
82+
), patch(
83+
"infra.repositories.nfs_input_repository"
84+
".NfsInputRepository.validate_input_directory",
85+
return_value=True,
86+
), patch(
87+
"infra.repositories.nfs_playbook_queue_request_repository"
88+
".NfsPlaybookQueueRequestRepository.is_available",
89+
return_value=True,
90+
), patch(
91+
"infra.repositories.nfs_playbook_queue_request_repository"
92+
".NfsPlaybookQueueRequestRepository.write_request",
93+
return_value=nfs_queue_dir / "requests" / "test.json",
94+
):
95+
response = client.post(
96+
f"/api/v1/jobs/{created_job}/stages/create-local-repository",
97+
headers=headers,
98+
)
99+
100+
assert response.status_code == 202
101+
assert response.json()["correlation_id"] == unique_correlation_id
102+
103+
104+
class TestCreateLocalRepoValidation:
105+
"""Validation scenarios for create local repository."""
106+
107+
def test_invalid_job_id_returns_400(self, client, auth_headers):
108+
response = client.post(
109+
"/api/v1/jobs/invalid-uuid/stages/create-local-repository",
110+
headers=auth_headers,
111+
)
112+
assert response.status_code == 400
113+
detail = response.json()["detail"]
114+
assert detail["error"] == "INVALID_JOB_ID"
115+
116+
def test_nonexistent_job_returns_404(self, client, auth_headers):
117+
fake_job_id = "018f3c4c-6a2e-7b2a-9c2a-3d8d2c4b9a11"
118+
response = client.post(
119+
f"/api/v1/jobs/{fake_job_id}/stages/create-local-repository",
120+
headers=auth_headers,
121+
)
122+
assert response.status_code == 404
123+
detail = response.json()["detail"]
124+
assert detail["error"] == "JOB_NOT_FOUND"
125+
126+
127+
class TestCreateLocalRepoAuthentication:
128+
"""Authentication header tests."""
129+
130+
def test_missing_authorization_returns_422(self, client, created_job):
131+
headers = {
132+
"X-Correlation-Id": "019bf590-1234-7890-abcd-ef1234567890",
133+
}
134+
response = client.post(
135+
f"/api/v1/jobs/{created_job}/stages/create-local-repository",
136+
headers=headers,
137+
)
138+
assert response.status_code == 422
139+
140+
def test_invalid_authorization_format_returns_401(self, client, created_job):
141+
headers = {
142+
"Authorization": "InvalidFormat test-token",
143+
"X-Correlation-Id": "019bf590-1234-7890-abcd-ef1234567890",
144+
}
145+
response = client.post(
146+
f"/api/v1/jobs/{created_job}/stages/create-local-repository",
147+
headers=headers,
148+
)
149+
assert response.status_code == 401
150+
151+
def test_empty_bearer_token_returns_401(self, client, created_job):
152+
headers = {
153+
"Authorization": "Bearer ",
154+
"X-Correlation-Id": "019bf590-1234-7890-abcd-ef1234567890",
155+
}
156+
response = client.post(
157+
f"/api/v1/jobs/{created_job}/stages/create-local-repository",
158+
headers=headers,
159+
)
160+
assert response.status_code == 401
161+
162+
163+
class TestCreateLocalRepoInputValidation:
164+
"""Input file validation tests."""
165+
166+
def test_missing_input_files_returns_400(self, client, auth_headers, created_job):
167+
with patch(
168+
"infra.repositories.nfs_input_repository"
169+
".NfsInputRepository.validate_input_directory",
170+
return_value=False,
171+
):
172+
response = client.post(
173+
f"/api/v1/jobs/{created_job}/stages/create-local-repository",
174+
headers=auth_headers,
175+
)
176+
177+
assert response.status_code == 400
178+
detail = response.json()["detail"]
179+
assert detail["error"] == "INPUT_FILES_MISSING"

0 commit comments

Comments
 (0)