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
45 changes: 45 additions & 0 deletions tests/e2e/features/vector_stores.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@e2e_group_3 @VectorStores
Feature: vector stores API endpoint tests


Background:
Given The service is started locally
And The system is in default state
And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove hardcoded bearer token from test feature data.

Line 8 commits a JWT-like token directly in the feature file. Even for test fixtures, this creates secret-scanner noise and weakens security hygiene in-repo. Replace with a non-token placeholder step/fixture value (or load from test config/env).

🧰 Tools
🪛 Betterleaks (1.3.1)

[high] 8-8: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/features/vector_stores.feature` at line 8, Replace the hardcoded
JWT in the step "I set the Authorization header to Bearer eyJhbGci..." with a
non-secret placeholder or env-driven value: update the feature step to use a
placeholder token like "I set the Authorization header to Bearer <TEST_TOKEN>"
(or call a step that reads from test config/env), and update the corresponding
step definition to inject process.env.TEST_TOKEN or a fixture value so no real
JWT is committed in the feature file.

Source: Linters/SAST tools

And REST API service prefix is /v1
And the Lightspeed stack configuration directory is "tests/e2e/configuration"
And The service uses the lightspeed-stack-auth-noop-token.yaml configuration
And The service is restarted

Scenario: List vector stores returns 200
When I access REST API endpoint "vector-stores" using HTTP GET method
Then The status code of the response is 200
And Content type of response is set to "application/json"

Scenario: Create vector store with empty body returns 422
When I access REST API endpoint "vector-stores" using HTTP POST method
"""
{}
"""
Then The status code of the response is 422

Scenario: Create vector store with extra fields returns 422
When I access REST API endpoint "vector-stores" using HTTP POST method
"""
{"name": "test-store", "unknown_field": "value"}
"""
Then The status code of the response is 422

Scenario: Update vector store with empty body returns 422
When I access REST API endpoint "vector-stores/nonexistent-id" using HTTP PUT method
"""
{}
"""
Then The status code of the response is 422

Scenario: Add file to vector store with empty body returns 422
When I access REST API endpoint "vector-stores/nonexistent-id/files" using HTTP POST method
"""
{}
"""
Then The status code of the response is 422
1 change: 1 addition & 0 deletions tests/e2e/test_list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ features/mcp_servers_api_no_config.feature
features/proxy.feature
features/tls.feature
features/opentelemetry.feature
features/vector_stores.feature
100 changes: 100 additions & 0 deletions tests/unit/app/endpoints/test_vector_stores.py
Original file line number Diff line number Diff line change
Expand Up @@ -1181,3 +1181,103 @@ async def test_delete_vector_store_file_not_found(mocker: MockerFixture) -> None
)
assert response.deleted is False
assert response.file_id == "file_999"


@pytest.mark.asyncio
async def test_get_vector_store_connection_error(mocker: MockerFixture) -> None:
"""Test get vector store with connection error."""
mock_authorization_resolvers(mocker)

config_dict = get_test_config()
cfg = AppConfig()
cfg.init_from_dict(config_dict)

mock_client = mocker.AsyncMock()
mock_client.vector_stores.retrieve.side_effect = APIConnectionError(
request=None # type: ignore
)
mock_lsc = mocker.patch(
"app.endpoints.vector_stores.AsyncLlamaStackClientHolder.get_client"
)
mock_lsc.return_value = mock_client
mocker.patch("app.endpoints.vector_stores.configuration", cfg)

request = get_test_request()
auth = get_test_auth()

with pytest.raises(HTTPException) as e:
await get_vector_store(request=request, vector_store_id="vs_123", auth=auth)
assert e.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE


@pytest.mark.asyncio
async def test_create_file_adds_txt_extension_when_missing(
mocker: MockerFixture,
) -> None:
"""Test that create_file appends .txt when filename has no extension."""
mock_authorization_resolvers(mocker)

config_dict = get_test_config()
cfg = AppConfig()
cfg.init_from_dict(config_dict)

mock_client = mocker.AsyncMock()
mock_client.files.create.return_value = File("file_123", "uploaded_file.txt", 12)
mock_lsc = mocker.patch(
"app.endpoints.vector_stores.AsyncLlamaStackClientHolder.get_client"
)
mock_lsc.return_value = mock_client
mocker.patch("app.endpoints.vector_stores.configuration", cfg)

request = get_test_request()
auth = get_test_auth()

mock_file = mocker.AsyncMock()
mock_file.filename = "uploaded_file"
mock_file.size = 12
mock_file.read.return_value = b"test content"

response = await create_file(request=request, auth=auth, file=mock_file)
assert response is not None
assert response.filename == "uploaded_file.txt"

file_arg = mock_client.files.create.call_args.kwargs["file"]
assert file_arg.name == "uploaded_file.txt"


@pytest.mark.asyncio
async def test_create_file_non_size_bad_request_returns_400(
mocker: MockerFixture,
) -> None:
"""Test create file with non-size BadRequestError returns 400."""
mock_authorization_resolvers(mocker)

config_dict = get_test_config()
cfg = AppConfig()
cfg.init_from_dict(config_dict)

mock_client = mocker.AsyncMock()
mock_response = mocker.Mock()
mock_response.request = mocker.Mock()
mock_client.files.create.side_effect = BadRequestError(
message="Invalid file format", response=mock_response, body=None
)
mock_lsc = mocker.patch(
"app.endpoints.vector_stores.AsyncLlamaStackClientHolder.get_client"
)
mock_lsc.return_value = mock_client
mocker.patch("app.endpoints.vector_stores.configuration", cfg)

request = get_test_request()
auth = get_test_auth()

mock_file = mocker.AsyncMock()
mock_file.filename = "test.txt"
mock_file.size = 12
mock_file.read.return_value = b"test content"

with pytest.raises(HTTPException) as e:
await create_file(request=request, auth=auth, file=mock_file)

assert e.value.status_code == status.HTTP_400_BAD_REQUEST
assert e.value.detail["response"] == "Invalid file upload" # type: ignore
66 changes: 65 additions & 1 deletion tests/unit/models/requests/test_vector_store_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,71 @@
import pytest
from pydantic import ValidationError

from models.api.requests import VectorStoreFileCreateRequest, VectorStoreUpdateRequest
from models.api.requests import (
VectorStoreCreateRequest,
VectorStoreFileCreateRequest,
VectorStoreUpdateRequest,
)


class TestVectorStoreCreateRequest:
"""Test cases for the VectorStoreCreateRequest model."""

def test_valid_create_with_name_only(self) -> None:
"""Test valid create request with only required name field."""
request = VectorStoreCreateRequest(name="test_store")
assert request.name == "test_store"
assert request.embedding_model is None
assert request.embedding_dimension is None
assert request.provider_id is None

def test_valid_create_with_all_fields(self) -> None:
"""Test valid create request with all optional fields."""
request = VectorStoreCreateRequest(
name="test_store",
embedding_model="text-embedding-ada-002",
embedding_dimension=1536,
provider_id="rhdh-docs",
metadata={"user_id": "user123"},
)
assert request.name == "test_store"
assert request.embedding_model == "text-embedding-ada-002"
assert request.embedding_dimension == 1536
assert request.provider_id == "rhdh-docs"
assert request.metadata == {"user_id": "user123"}

def test_name_required(self) -> None:
"""Test that name field is required."""
with pytest.raises(ValidationError):
VectorStoreCreateRequest() # pyright: ignore[reportCallIssue]

def test_name_cannot_be_empty(self) -> None:
"""Test that name cannot be an empty string."""
with pytest.raises(ValidationError, match="at least 1 character"):
VectorStoreCreateRequest(name="")

def test_name_max_length_256(self) -> None:
"""Test that name cannot exceed 256 characters."""
with pytest.raises(ValidationError, match="at most 256 characters"):
VectorStoreCreateRequest(name="a" * 257)

def test_name_at_max_length(self) -> None:
"""Test that name at exactly 256 characters is accepted."""
request = VectorStoreCreateRequest(name="a" * 256)
assert len(request.name) == 256

def test_embedding_dimension_must_be_positive(self) -> None:
"""Test that embedding_dimension must be greater than 0."""
with pytest.raises(ValidationError, match="greater than 0"):
VectorStoreCreateRequest(name="test_store", embedding_dimension=0)

def test_extra_fields_forbidden(self) -> None:
"""Test that extra fields are rejected."""
with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
VectorStoreCreateRequest(
name="test_store",
unknown_field="value", # pyright: ignore[reportCallIssue]
)


class TestVectorStoreUpdateRequest:
Expand Down
Loading