mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-10-04 04:04:14 +00:00
# What does this PR do? adds support for authorized users to the s3 files provider ## Test Plan existing and new unit tests
199 lines
8.4 KiB
Python
199 lines
8.4 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the terms described in the LICENSE file in
|
|
# the root directory of this source tree.
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from botocore.exceptions import ClientError
|
|
|
|
from llama_stack.apis.common.errors import ResourceNotFoundError
|
|
from llama_stack.apis.files import OpenAIFilePurpose
|
|
|
|
|
|
class TestS3FilesImpl:
|
|
"""Test suite for S3 Files implementation."""
|
|
|
|
async def test_upload_file(self, s3_provider, sample_text_file, s3_client, s3_config):
|
|
"""Test successful file upload."""
|
|
sample_text_file.filename = "test_upload_file"
|
|
result = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
assert result.filename == sample_text_file.filename
|
|
assert result.purpose == OpenAIFilePurpose.ASSISTANTS
|
|
assert result.bytes == len(sample_text_file.content)
|
|
assert result.id.startswith("file-")
|
|
|
|
# Verify file exists in S3 backend
|
|
response = s3_client.head_object(Bucket=s3_config.bucket_name, Key=result.id)
|
|
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
|
|
|
|
async def test_list_files_empty(self, s3_provider):
|
|
"""Test listing files when no files exist."""
|
|
result = await s3_provider.openai_list_files()
|
|
|
|
assert len(result.data) == 0
|
|
assert not result.has_more
|
|
assert result.first_id == ""
|
|
assert result.last_id == ""
|
|
|
|
async def test_retrieve_file(self, s3_provider, sample_text_file):
|
|
"""Test retrieving file metadata."""
|
|
sample_text_file.filename = "test_retrieve_file"
|
|
uploaded = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
retrieved = await s3_provider.openai_retrieve_file(uploaded.id)
|
|
|
|
assert retrieved.id == uploaded.id
|
|
assert retrieved.filename == uploaded.filename
|
|
assert retrieved.purpose == uploaded.purpose
|
|
assert retrieved.bytes == uploaded.bytes
|
|
|
|
async def test_retrieve_file_content(self, s3_provider, sample_text_file):
|
|
"""Test retrieving file content."""
|
|
sample_text_file.filename = "test_retrieve_file_content"
|
|
uploaded = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
response = await s3_provider.openai_retrieve_file_content(uploaded.id)
|
|
|
|
assert response.body == sample_text_file.content
|
|
assert response.headers["Content-Disposition"] == f'attachment; filename="{sample_text_file.filename}"'
|
|
|
|
async def test_delete_file(self, s3_provider, sample_text_file, s3_config, s3_client):
|
|
"""Test deleting a file."""
|
|
sample_text_file.filename = "test_delete_file"
|
|
uploaded = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
delete_response = await s3_provider.openai_delete_file(uploaded.id)
|
|
|
|
assert delete_response.id == uploaded.id
|
|
assert delete_response.deleted is True
|
|
|
|
with pytest.raises(ResourceNotFoundError, match="not found"):
|
|
await s3_provider.openai_retrieve_file(uploaded.id)
|
|
|
|
# Verify file is gone from S3 backend
|
|
with pytest.raises(ClientError) as exc_info:
|
|
s3_client.head_object(Bucket=s3_config.bucket_name, Key=uploaded.id)
|
|
assert exc_info.value.response["Error"]["Code"] == "404"
|
|
|
|
async def test_list_files(self, s3_provider, sample_text_file, sample_text_file2):
|
|
"""Test listing files after uploading some."""
|
|
sample_text_file.filename = "test_list_files_with_content_file1"
|
|
file1 = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
sample_text_file2.filename = "test_list_files_with_content_file2"
|
|
file2 = await s3_provider.openai_upload_file(
|
|
file=sample_text_file2,
|
|
purpose=OpenAIFilePurpose.BATCH,
|
|
)
|
|
|
|
result = await s3_provider.openai_list_files()
|
|
|
|
assert len(result.data) == 2
|
|
file_ids = {f.id for f in result.data}
|
|
assert file1.id in file_ids
|
|
assert file2.id in file_ids
|
|
|
|
async def test_list_files_with_purpose_filter(self, s3_provider, sample_text_file, sample_text_file2):
|
|
"""Test listing files with purpose filter."""
|
|
sample_text_file.filename = "test_list_files_with_purpose_filter_file1"
|
|
file1 = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
sample_text_file2.filename = "test_list_files_with_purpose_filter_file2"
|
|
await s3_provider.openai_upload_file(
|
|
file=sample_text_file2,
|
|
purpose=OpenAIFilePurpose.BATCH,
|
|
)
|
|
|
|
result = await s3_provider.openai_list_files(purpose=OpenAIFilePurpose.ASSISTANTS)
|
|
|
|
assert len(result.data) == 1
|
|
assert result.data[0].id == file1.id
|
|
assert result.data[0].purpose == OpenAIFilePurpose.ASSISTANTS
|
|
|
|
async def test_nonexistent_file_retrieval(self, s3_provider):
|
|
"""Test retrieving a non-existent file raises error."""
|
|
with pytest.raises(ResourceNotFoundError, match="not found"):
|
|
await s3_provider.openai_retrieve_file("file-nonexistent")
|
|
|
|
async def test_nonexistent_file_content_retrieval(self, s3_provider):
|
|
"""Test retrieving content of a non-existent file raises error."""
|
|
with pytest.raises(ResourceNotFoundError, match="not found"):
|
|
await s3_provider.openai_retrieve_file_content("file-nonexistent")
|
|
|
|
async def test_nonexistent_file_deletion(self, s3_provider):
|
|
"""Test deleting a non-existent file raises error."""
|
|
with pytest.raises(ResourceNotFoundError, match="not found"):
|
|
await s3_provider.openai_delete_file("file-nonexistent")
|
|
|
|
async def test_upload_file_without_filename(self, s3_provider, sample_text_file):
|
|
"""Test uploading a file without a filename uses the fallback."""
|
|
del sample_text_file.filename
|
|
result = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
assert result.purpose == OpenAIFilePurpose.ASSISTANTS
|
|
assert result.bytes == len(sample_text_file.content)
|
|
|
|
retrieved = await s3_provider.openai_retrieve_file(result.id)
|
|
assert retrieved.filename == result.filename
|
|
|
|
async def test_file_operations_when_s3_object_deleted(self, s3_provider, sample_text_file, s3_config, s3_client):
|
|
"""Test file operations when S3 object is deleted but metadata exists (negative test)."""
|
|
sample_text_file.filename = "test_orphaned_metadata"
|
|
uploaded = await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
# Directly delete the S3 object from the backend
|
|
s3_client.delete_object(Bucket=s3_config.bucket_name, Key=uploaded.id)
|
|
|
|
with pytest.raises(ResourceNotFoundError, match="not found") as exc_info:
|
|
await s3_provider.openai_retrieve_file_content(uploaded.id)
|
|
assert uploaded.id in str(exc_info).lower()
|
|
|
|
listed_files = await s3_provider.openai_list_files()
|
|
assert uploaded.id not in [file.id for file in listed_files.data]
|
|
|
|
async def test_upload_file_s3_put_object_failure(self, s3_provider, sample_text_file, s3_config, s3_client):
|
|
"""Test that put_object failure results in exception and no orphaned metadata."""
|
|
sample_text_file.filename = "test_s3_put_object_failure"
|
|
|
|
def failing_put_object(*args, **kwargs):
|
|
raise ClientError(
|
|
error_response={"Error": {"Code": "SolarRadiation", "Message": "Bloop"}}, operation_name="PutObject"
|
|
)
|
|
|
|
with patch.object(s3_provider.client, "put_object", side_effect=failing_put_object):
|
|
with pytest.raises(RuntimeError, match="Failed to upload file to S3"):
|
|
await s3_provider.openai_upload_file(
|
|
file=sample_text_file,
|
|
purpose=OpenAIFilePurpose.ASSISTANTS,
|
|
)
|
|
|
|
files_list = await s3_provider.openai_list_files()
|
|
assert len(files_list.data) == 0, "No file metadata should remain after failed upload"
|