remove image tags
This commit is contained in:
parent
8c0a868144
commit
1eb545840e
@ -399,7 +399,6 @@ The API provides the following main endpoints with their authentication and pagi
|
|||||||
- `skip` (default: 0, min: 0) - Number of items to skip
|
- `skip` (default: 0, min: 0) - Number of items to skip
|
||||||
- `limit` (default: 50, min: 1, max: 100) - Number of items per page
|
- `limit` (default: 50, min: 1, max: 100) - Number of items per page
|
||||||
- `collection_id` (optional) - Filter by collection
|
- `collection_id` (optional) - Filter by collection
|
||||||
- `tags` (optional) - Filter by comma-separated tags
|
|
||||||
- **Response includes:** `images`, `total`, `skip`, `limit`
|
- **Response includes:** `images`, `total`, `skip`, `limit`
|
||||||
|
|
||||||
#### Search Functionality ✅ **Fully Paginated & Protected**
|
#### Search Functionality ✅ **Fully Paginated & Protected**
|
||||||
@ -410,7 +409,6 @@ The API provides the following main endpoints with their authentication and pagi
|
|||||||
- `limit` (default: 10, min: 1, max: 50) - Number of results
|
- `limit` (default: 10, min: 1, max: 50) - Number of results
|
||||||
- `threshold` (default: 0.7, min: 0.0, max: 1.0) - Similarity threshold
|
- `threshold` (default: 0.7, min: 0.0, max: 1.0) - Similarity threshold
|
||||||
- `collection_id` (optional) - Filter by collection
|
- `collection_id` (optional) - Filter by collection
|
||||||
- `tags` (optional) - Filter by comma-separated tags
|
|
||||||
- **Response includes:** `results`, `total`, `limit`, `threshold`, `query`
|
- **Response includes:** `results`, `total`, `limit`, `threshold`, `query`
|
||||||
- `POST /api/v1/search` - Advanced search with same pagination
|
- `POST /api/v1/search` - Advanced search with same pagination
|
||||||
- `GET /api/v1/search/similar/{image_id}` - Find similar images with pagination
|
- `GET /api/v1/search/similar/{image_id}` - Find similar images with pagination
|
||||||
|
|||||||
@ -118,15 +118,13 @@ client/
|
|||||||
|
|
||||||
### Image Management
|
### Image Management
|
||||||
- **Upload**: Drag & drop or click to upload images
|
- **Upload**: Drag & drop or click to upload images
|
||||||
- **Metadata**: Add descriptions and tags to images
|
|
||||||
- **View**: Full-size image viewing with details
|
- **View**: Full-size image viewing with details
|
||||||
- **Edit**: Update descriptions and tags
|
- **Edit**: Update descriptions
|
||||||
- **Delete**: Remove images with confirmation
|
- **Delete**: Remove images with confirmation
|
||||||
|
|
||||||
### AI-Powered Search
|
### AI-Powered Search
|
||||||
- **Natural Language**: Search using descriptive text
|
- **Natural Language**: Search using descriptive text
|
||||||
- **Similarity Threshold**: Adjust search sensitivity
|
- **Similarity Threshold**: Adjust search sensitivity
|
||||||
- **Result Filtering**: Filter by tags and metadata
|
|
||||||
- **Search History**: Save and reuse frequent searches
|
- **Search History**: Save and reuse frequent searches
|
||||||
|
|
||||||
### Team & User Management
|
### Team & User Management
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<i class="fas fa-upload fa-3x text-primary mb-3"></i>
|
<i class="fas fa-upload fa-3x text-primary mb-3"></i>
|
||||||
<h5 class="card-title">Upload Images</h5>
|
<h5 class="card-title">Upload Images</h5>
|
||||||
<p class="card-text">Upload and manage your image collection with metadata and tags.</p>
|
<p class="card-text">Upload and manage your image collection with metadata.</p>
|
||||||
<button class="btn btn-primary" onclick="showPage('images')">Get Started</button>
|
<button class="btn btn-primary" onclick="showPage('images')">Get Started</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -123,12 +123,9 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Images API
|
// Images API
|
||||||
async getImages(page = 1, limit = 20, tags = null) {
|
async getImages(page = 1, limit = 20) {
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
let endpoint = `/images?skip=${skip}&limit=${limit}`;
|
let endpoint = `/images?skip=${skip}&limit=${limit}`;
|
||||||
if (tags) {
|
|
||||||
endpoint += `&tags=${encodeURIComponent(tags)}`;
|
|
||||||
}
|
|
||||||
return this.makeRequest('GET', endpoint);
|
return this.makeRequest('GET', endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,17 +177,13 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search API
|
// Search API
|
||||||
async searchImages(query, similarityThreshold = 0.7, maxResults = 20, tags = null) {
|
async searchImages(query, similarityThreshold = 0.7, maxResults = 20) {
|
||||||
const searchData = {
|
const searchData = {
|
||||||
query,
|
query,
|
||||||
similarity_threshold: similarityThreshold,
|
similarity_threshold: similarityThreshold,
|
||||||
max_results: maxResults
|
max_results: maxResults
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tags) {
|
|
||||||
searchData.tags = tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.makeRequest('POST', '/search', searchData);
|
return this.makeRequest('POST', '/search', searchData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ function cleanupBlobCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load images with pagination
|
// Load images with pagination
|
||||||
async function loadImages(page = 1, tags = null) {
|
async function loadImages(page = 1) {
|
||||||
if (!config.isConfigured()) {
|
if (!config.isConfigured()) {
|
||||||
showAlert('Please configure your API settings first.', 'warning');
|
showAlert('Please configure your API settings first.', 'warning');
|
||||||
return;
|
return;
|
||||||
@ -42,7 +42,7 @@ async function loadImages(page = 1, tags = null) {
|
|||||||
container.innerHTML = '<div class="text-center"><div class="loading-spinner"></div> Loading images...</div>';
|
container.innerHTML = '<div class="text-center"><div class="loading-spinner"></div> Loading images...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.getImages(page, 20, tags);
|
const response = await apiClient.getImages(page, 20);
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
totalPages = Math.ceil(response.total / (response.limit || 20));
|
totalPages = Math.ceil(response.total / (response.limit || 20));
|
||||||
|
|
||||||
@ -93,11 +93,6 @@ async function displayImages(images) {
|
|||||||
<p class="card-text small text-muted">
|
<p class="card-text small text-muted">
|
||||||
<i class="fas fa-calendar me-1"></i>${formatDate(image.upload_date)}
|
<i class="fas fa-calendar me-1"></i>${formatDate(image.upload_date)}
|
||||||
</p>
|
</p>
|
||||||
${image.tags && image.tags.length > 0 ? `
|
|
||||||
<div class="mb-2">
|
|
||||||
${image.tags.map(tag => `<span class="badge bg-secondary me-1">${escapeHtml(tag)}</span>`).join('')}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
<div class="btn-group w-100" role="group">
|
<div class="btn-group w-100" role="group">
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="viewImage('${image.id}')">
|
<button class="btn btn-sm btn-outline-primary" onclick="viewImage('${image.id}')">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
@ -198,12 +193,6 @@ function showUploadModal() {
|
|||||||
<textarea class="form-control" id="imageDescription" rows="3"
|
<textarea class="form-control" id="imageDescription" rows="3"
|
||||||
placeholder="Describe this image..."></textarea>
|
placeholder="Describe this image..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="imageTags" class="form-label">Tags</label>
|
|
||||||
<input type="text" class="form-control" id="imageTags"
|
|
||||||
placeholder="Enter tags separated by commas">
|
|
||||||
<div class="form-text">e.g., nature, landscape, sunset</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -268,8 +257,6 @@ async function uploadImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const description = document.getElementById('imageDescription').value.trim();
|
const description = document.getElementById('imageDescription').value.trim();
|
||||||
const tagsInput = document.getElementById('imageTags').value.trim();
|
|
||||||
const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
|
|
||||||
|
|
||||||
const uploadButton = document.querySelector('#uploadModal .btn-primary');
|
const uploadButton = document.querySelector('#uploadModal .btn-primary');
|
||||||
setLoadingState(uploadButton);
|
setLoadingState(uploadButton);
|
||||||
@ -278,9 +265,6 @@ async function uploadImage() {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('description', description);
|
formData.append('description', description);
|
||||||
if (tags.length > 0) {
|
|
||||||
formData.append('tags', tags.join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
await apiClient.uploadImage(formData);
|
await apiClient.uploadImage(formData);
|
||||||
|
|
||||||
@ -339,12 +323,6 @@ async function viewImage(imageId) {
|
|||||||
<p><strong>Type:</strong> ${image.content_type}</p>
|
<p><strong>Type:</strong> ${image.content_type}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${image.tags && image.tags.length > 0 ? `
|
|
||||||
<div class="mt-3">
|
|
||||||
<h6>Tags</h6>
|
|
||||||
${image.tags.map(tag => `<span class="badge bg-secondary me-1">${escapeHtml(tag)}</span>`).join('')}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const modalFooter = `
|
const modalFooter = `
|
||||||
@ -403,12 +381,6 @@ async function editImage(imageId) {
|
|||||||
<label for="editDescription-${imageId}" class="form-label">Description</label>
|
<label for="editDescription-${imageId}" class="form-label">Description</label>
|
||||||
<textarea class="form-control" id="editDescription-${imageId}" rows="3">${escapeHtml(image.description || '')}</textarea>
|
<textarea class="form-control" id="editDescription-${imageId}" rows="3">${escapeHtml(image.description || '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="editTags-${imageId}" class="form-label">Tags</label>
|
|
||||||
<input type="text" class="form-control" id="editTags-${imageId}"
|
|
||||||
value="${image.tags ? image.tags.join(', ') : ''}">
|
|
||||||
<div class="form-text">Enter tags separated by commas</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -437,16 +409,13 @@ async function editImage(imageId) {
|
|||||||
// Save image changes
|
// Save image changes
|
||||||
async function saveImageChanges(imageId) {
|
async function saveImageChanges(imageId) {
|
||||||
const description = document.getElementById(`editDescription-${imageId}`).value.trim();
|
const description = document.getElementById(`editDescription-${imageId}`).value.trim();
|
||||||
const tagsInput = document.getElementById(`editTags-${imageId}`).value.trim();
|
|
||||||
const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
|
|
||||||
|
|
||||||
const saveButton = document.querySelector(`#editImageModal-${imageId} .btn-primary`);
|
const saveButton = document.querySelector(`#editImageModal-${imageId} .btn-primary`);
|
||||||
setLoadingState(saveButton);
|
setLoadingState(saveButton);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.updateImage(imageId, {
|
await apiClient.updateImage(imageId, {
|
||||||
description,
|
description
|
||||||
tags
|
|
||||||
});
|
});
|
||||||
|
|
||||||
showAlert('Image updated successfully!', 'success');
|
showAlert('Image updated successfully!', 'success');
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"terraform_version": "1.10.1",
|
"terraform_version": "1.10.1",
|
||||||
"serial": 438,
|
"serial": 445,
|
||||||
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"cloud_function_name": {
|
"cloud_function_name": {
|
||||||
@ -98,16 +98,16 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"exclude_symlink_directories": null,
|
"exclude_symlink_directories": null,
|
||||||
"excludes": null,
|
"excludes": null,
|
||||||
"id": "88ee03db0f4c7023c0c620449e167ad27074fdd0",
|
"id": "0cfb36e4e396f12e3ad2944c44b083bff2224ad5",
|
||||||
"output_base64sha256": "0p558sP6ikbyrfmva7zGOYklnR/4VRPD1zcl8HZcv8A=",
|
"output_base64sha256": "uMoV4IM2IuGcRtqeI7wbu3OsTmvDx1ohDDxkEE5NY9U=",
|
||||||
"output_base64sha512": "mal2zoxqjg5lZYruPmffQdDqY9FJONPc5Wnu41NP07LOj/tC+sJAAeQ7tmU0mq8h6SfQE6wwxFeYJuEO1y2xLg==",
|
"output_base64sha512": "BQB+g3lC0+y5vOx6KHh4AWCeHk3D2nmdgE8JrFaiPlCWV6KsrMdANGyKeZ/aFmvGjbFw7MGQD4s0u/tn+viVAA==",
|
||||||
"output_file_mode": null,
|
"output_file_mode": null,
|
||||||
"output_md5": "58a2b7fe53bb2c8c921405cc965d635c",
|
"output_md5": "b532cf3ff81d62dd7dec013e486931aa",
|
||||||
"output_path": "./function-source.zip",
|
"output_path": "./function-source.zip",
|
||||||
"output_sha": "88ee03db0f4c7023c0c620449e167ad27074fdd0",
|
"output_sha": "0cfb36e4e396f12e3ad2944c44b083bff2224ad5",
|
||||||
"output_sha256": "d29e79f2c3fa8a46f2adf9af6bbcc63989259d1ff85513c3d73725f0765cbfc0",
|
"output_sha256": "b8ca15e0833622e19c46da9e23bc1bbb73ac4e6bc3c75a210c3c64104e4d63d5",
|
||||||
"output_sha512": "99a976ce8c6a8e0e65658aee3e67df41d0ea63d14938d3dce569eee3534fd3b2ce8ffb42fac24001e43bb665349aaf21e927d013ac30c4579826e10ed72db12e",
|
"output_sha512": "05007e837942d3ecb9bcec7a28787801609e1e4dc3da799d804f09ac56a23e509657a2acacc740346c8a799fda166bc68db170ecc1900f8b34bbfb67faf89500",
|
||||||
"output_size": 69765973,
|
"output_size": 69764346,
|
||||||
"source": [],
|
"source": [],
|
||||||
"source_content": null,
|
"source_content": null,
|
||||||
"source_content_filename": null,
|
"source_content_filename": null,
|
||||||
@ -172,7 +172,7 @@
|
|||||||
"effective_annotations": {
|
"effective_annotations": {
|
||||||
"run.googleapis.com/ingress": "all",
|
"run.googleapis.com/ingress": "all",
|
||||||
"run.googleapis.com/ingress-status": "all",
|
"run.googleapis.com/ingress-status": "all",
|
||||||
"run.googleapis.com/operation-id": "0f195b05-99ac-4d28-b5fe-2d3dea289124",
|
"run.googleapis.com/operation-id": "a9aeb6de-fdd6-43b2-93f8-8b7f72afab4c",
|
||||||
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
|
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
|
||||||
"serving.knative.dev/creator": "johnpccd3@gmail.com",
|
"serving.knative.dev/creator": "johnpccd3@gmail.com",
|
||||||
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
|
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
|
||||||
@ -182,14 +182,14 @@
|
|||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"generation": 1,
|
"generation": 1,
|
||||||
"labels": {},
|
"labels": null,
|
||||||
"namespace": "gen-lang-client-0424120530",
|
"namespace": "gen-lang-client-0424120530",
|
||||||
"resource_version": "AAY189oNgAQ",
|
"resource_version": "AAY19MELEOc",
|
||||||
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
||||||
"terraform_labels": {
|
"terraform_labels": {
|
||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"uid": "20e61eb3-6217-40e8-8ae5-45111d31bbda"
|
"uid": "8c8be11c-c607-4caa-a65e-c552ec445882"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "sereact",
|
"name": "sereact",
|
||||||
@ -216,14 +216,14 @@
|
|||||||
"type": "RoutesReady"
|
"type": "RoutesReady"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"latest_created_revision_name": "sereact-00001-2lz",
|
"latest_created_revision_name": "sereact-00001-z4g",
|
||||||
"latest_ready_revision_name": "sereact-00001-2lz",
|
"latest_ready_revision_name": "sereact-00001-z4g",
|
||||||
"observed_generation": 1,
|
"observed_generation": 1,
|
||||||
"traffic": [
|
"traffic": [
|
||||||
{
|
{
|
||||||
"latest_revision": true,
|
"latest_revision": true,
|
||||||
"percent": 100,
|
"percent": 100,
|
||||||
"revision_name": "sereact-00001-2lz",
|
"revision_name": "sereact-00001-z4g",
|
||||||
"tag": "",
|
"tag": "",
|
||||||
"url": ""
|
"url": ""
|
||||||
}
|
}
|
||||||
@ -256,8 +256,8 @@
|
|||||||
"container_concurrency": 80,
|
"container_concurrency": 80,
|
||||||
"containers": [
|
"containers": [
|
||||||
{
|
{
|
||||||
"args": [],
|
"args": null,
|
||||||
"command": [],
|
"command": null,
|
||||||
"env": [
|
"env": [
|
||||||
{
|
{
|
||||||
"name": "API_KEY_SECRET",
|
"name": "API_KEY_SECRET",
|
||||||
@ -337,7 +337,7 @@
|
|||||||
"cpu": "1",
|
"cpu": "1",
|
||||||
"memory": "1Gi"
|
"memory": "1Gi"
|
||||||
},
|
},
|
||||||
"requests": {}
|
"requests": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"startup_probe": [
|
"startup_probe": [
|
||||||
@ -359,7 +359,7 @@
|
|||||||
"working_dir": ""
|
"working_dir": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"node_selector": {},
|
"node_selector": null,
|
||||||
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
||||||
"serving_state": "",
|
"serving_state": "",
|
||||||
"timeout_seconds": 300,
|
"timeout_seconds": 300,
|
||||||
@ -440,7 +440,7 @@
|
|||||||
"schema_version": 0,
|
"schema_version": 0,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"condition": [],
|
"condition": [],
|
||||||
"etag": "BwY189qg+AA=",
|
"etag": "BwY19MG70Fs=",
|
||||||
"id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers",
|
"id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers",
|
||||||
"location": "us-central1",
|
"location": "us-central1",
|
||||||
"member": "allUsers",
|
"member": "allUsers",
|
||||||
@ -474,7 +474,7 @@
|
|||||||
"automatic_update_policy": [
|
"automatic_update_policy": [
|
||||||
{}
|
{}
|
||||||
],
|
],
|
||||||
"build": "projects/761163285547/locations/us-central1/builds/aab08c74-df86-4cd7-9176-4ff267cab3e6",
|
"build": "projects/761163285547/locations/us-central1/builds/ae88c918-6bb3-4aef-a56f-270f48f73049",
|
||||||
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
||||||
"entry_point": "process_image_embedding",
|
"entry_point": "process_image_embedding",
|
||||||
"environment_variables": {},
|
"environment_variables": {},
|
||||||
@ -487,8 +487,8 @@
|
|||||||
"storage_source": [
|
"storage_source": [
|
||||||
{
|
{
|
||||||
"bucket": "gen-lang-client-0424120530-cloud-function-source",
|
"bucket": "gen-lang-client-0424120530-cloud-function-source",
|
||||||
"generation": 1748171376287077,
|
"generation": 1748175166697242,
|
||||||
"object": "function-source-58a2b7fe53bb2c8c921405cc965d635c.zip"
|
"object": "function-source-b532cf3ff81d62dd7dec013e486931aa.zip"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -508,7 +508,7 @@
|
|||||||
"pubsub_topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic",
|
"pubsub_topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic",
|
||||||
"retry_policy": "RETRY_POLICY_RETRY",
|
"retry_policy": "RETRY_POLICY_RETRY",
|
||||||
"service_account_email": "761163285547-compute@developer.gserviceaccount.com",
|
"service_account_email": "761163285547-compute@developer.gserviceaccount.com",
|
||||||
"trigger": "projects/gen-lang-client-0424120530/locations/us-central1/triggers/process-image-embedding-013009",
|
"trigger": "projects/gen-lang-client-0424120530/locations/us-central1/triggers/process-image-embedding-645734",
|
||||||
"trigger_region": "us-central1"
|
"trigger_region": "us-central1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -559,7 +559,7 @@
|
|||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"timeouts": null,
|
"timeouts": null,
|
||||||
"update_time": "2025-05-25T11:13:04.212724797Z",
|
"update_time": "2025-05-25T12:15:22.215124150Z",
|
||||||
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [
|
"sensitive_attributes": [
|
||||||
@ -809,12 +809,6 @@
|
|||||||
"zone": "us-central1-a"
|
"zone": "us-central1-a"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [
|
"sensitive_attributes": [
|
||||||
[
|
|
||||||
{
|
|
||||||
"type": "get_attr",
|
|
||||||
"value": "metadata_startup_script"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"type": "get_attr",
|
"type": "get_attr",
|
||||||
@ -848,6 +842,12 @@
|
|||||||
"type": "get_attr",
|
"type": "get_attr",
|
||||||
"value": "disk_encryption_key_raw"
|
"value": "disk_encryption_key_raw"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "get_attr",
|
||||||
|
"value": "metadata_startup_script"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiNiJ9",
|
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiNiJ9",
|
||||||
@ -875,8 +875,8 @@
|
|||||||
"database_edition": "STANDARD",
|
"database_edition": "STANDARD",
|
||||||
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
||||||
"deletion_policy": "ABANDON",
|
"deletion_policy": "ABANDON",
|
||||||
"earliest_version_time": "2025-05-25T10:09:32.175339Z",
|
"earliest_version_time": "2025-05-25T11:12:43.126081Z",
|
||||||
"etag": "IMC5lO29vo0DMKrW4vCEvY0D",
|
"etag": "IPjb6fzLvo0DMKrW4vCEvY0D",
|
||||||
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
||||||
"key_prefix": "",
|
"key_prefix": "",
|
||||||
"location_id": "us-central1",
|
"location_id": "us-central1",
|
||||||
@ -1514,21 +1514,21 @@
|
|||||||
"content_encoding": "",
|
"content_encoding": "",
|
||||||
"content_language": "",
|
"content_language": "",
|
||||||
"content_type": "application/zip",
|
"content_type": "application/zip",
|
||||||
"crc32c": "eCjQFg==",
|
"crc32c": "EgiVnQ==",
|
||||||
"customer_encryption": [],
|
"customer_encryption": [],
|
||||||
"detect_md5hash": "WKK3/lO7LIySFAXMll1jXA==",
|
"detect_md5hash": "tTLPP/gdYt197AE+SGkxqg==",
|
||||||
"event_based_hold": false,
|
"event_based_hold": false,
|
||||||
"generation": 1748170673167525,
|
"generation": 1748174860755303,
|
||||||
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"kms_key_name": "",
|
"kms_key_name": "",
|
||||||
"md5hash": "WKK3/lO7LIySFAXMll1jXA==",
|
"md5hash": "tTLPP/gdYt197AE+SGkxqg==",
|
||||||
"md5hexhash": "58a2b7fe53bb2c8c921405cc965d635c",
|
"md5hexhash": "b532cf3ff81d62dd7dec013e486931aa",
|
||||||
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-58a2b7fe53bb2c8c921405cc965d635c.zip?generation=1748170673167525\u0026alt=media",
|
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-b532cf3ff81d62dd7dec013e486931aa.zip?generation=1748174860755303\u0026alt=media",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"name": "function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"output_name": "function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"output_name": "function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"retention": [],
|
"retention": [],
|
||||||
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"source": "./function-source.zip",
|
"source": "./function-source.zip",
|
||||||
"storage_class": "STANDARD",
|
"storage_class": "STANDARD",
|
||||||
"temporary_hold": false,
|
"temporary_hold": false,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"terraform_version": "1.10.1",
|
"terraform_version": "1.10.1",
|
||||||
"serial": 436,
|
"serial": 441,
|
||||||
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"cloud_function_name": {
|
"cloud_function_name": {
|
||||||
@ -98,16 +98,16 @@
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"exclude_symlink_directories": null,
|
"exclude_symlink_directories": null,
|
||||||
"excludes": null,
|
"excludes": null,
|
||||||
"id": "88ee03db0f4c7023c0c620449e167ad27074fdd0",
|
"id": "0cfb36e4e396f12e3ad2944c44b083bff2224ad5",
|
||||||
"output_base64sha256": "0p558sP6ikbyrfmva7zGOYklnR/4VRPD1zcl8HZcv8A=",
|
"output_base64sha256": "uMoV4IM2IuGcRtqeI7wbu3OsTmvDx1ohDDxkEE5NY9U=",
|
||||||
"output_base64sha512": "mal2zoxqjg5lZYruPmffQdDqY9FJONPc5Wnu41NP07LOj/tC+sJAAeQ7tmU0mq8h6SfQE6wwxFeYJuEO1y2xLg==",
|
"output_base64sha512": "BQB+g3lC0+y5vOx6KHh4AWCeHk3D2nmdgE8JrFaiPlCWV6KsrMdANGyKeZ/aFmvGjbFw7MGQD4s0u/tn+viVAA==",
|
||||||
"output_file_mode": null,
|
"output_file_mode": null,
|
||||||
"output_md5": "58a2b7fe53bb2c8c921405cc965d635c",
|
"output_md5": "b532cf3ff81d62dd7dec013e486931aa",
|
||||||
"output_path": "./function-source.zip",
|
"output_path": "./function-source.zip",
|
||||||
"output_sha": "88ee03db0f4c7023c0c620449e167ad27074fdd0",
|
"output_sha": "0cfb36e4e396f12e3ad2944c44b083bff2224ad5",
|
||||||
"output_sha256": "d29e79f2c3fa8a46f2adf9af6bbcc63989259d1ff85513c3d73725f0765cbfc0",
|
"output_sha256": "b8ca15e0833622e19c46da9e23bc1bbb73ac4e6bc3c75a210c3c64104e4d63d5",
|
||||||
"output_sha512": "99a976ce8c6a8e0e65658aee3e67df41d0ea63d14938d3dce569eee3534fd3b2ce8ffb42fac24001e43bb665349aaf21e927d013ac30c4579826e10ed72db12e",
|
"output_sha512": "05007e837942d3ecb9bcec7a28787801609e1e4dc3da799d804f09ac56a23e509657a2acacc740346c8a799fda166bc68db170ecc1900f8b34bbfb67faf89500",
|
||||||
"output_size": 69765973,
|
"output_size": 69764346,
|
||||||
"source": [],
|
"source": [],
|
||||||
"source_content": null,
|
"source_content": null,
|
||||||
"source_content_filename": null,
|
"source_content_filename": null,
|
||||||
@ -182,7 +182,7 @@
|
|||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"generation": 1,
|
"generation": 1,
|
||||||
"labels": null,
|
"labels": {},
|
||||||
"namespace": "gen-lang-client-0424120530",
|
"namespace": "gen-lang-client-0424120530",
|
||||||
"resource_version": "AAY189oNgAQ",
|
"resource_version": "AAY189oNgAQ",
|
||||||
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
||||||
@ -256,8 +256,8 @@
|
|||||||
"container_concurrency": 80,
|
"container_concurrency": 80,
|
||||||
"containers": [
|
"containers": [
|
||||||
{
|
{
|
||||||
"args": null,
|
"args": [],
|
||||||
"command": null,
|
"command": [],
|
||||||
"env": [
|
"env": [
|
||||||
{
|
{
|
||||||
"name": "API_KEY_SECRET",
|
"name": "API_KEY_SECRET",
|
||||||
@ -337,7 +337,7 @@
|
|||||||
"cpu": "1",
|
"cpu": "1",
|
||||||
"memory": "1Gi"
|
"memory": "1Gi"
|
||||||
},
|
},
|
||||||
"requests": null
|
"requests": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"startup_probe": [
|
"startup_probe": [
|
||||||
@ -359,7 +359,7 @@
|
|||||||
"working_dir": ""
|
"working_dir": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"node_selector": null,
|
"node_selector": {},
|
||||||
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
||||||
"serving_state": "",
|
"serving_state": "",
|
||||||
"timeout_seconds": 300,
|
"timeout_seconds": 300,
|
||||||
@ -474,7 +474,7 @@
|
|||||||
"automatic_update_policy": [
|
"automatic_update_policy": [
|
||||||
{}
|
{}
|
||||||
],
|
],
|
||||||
"build": "projects/761163285547/locations/us-central1/builds/c627da1f-c247-4d17-8ea8-5c03b518b1aa",
|
"build": "projects/761163285547/locations/us-central1/builds/aab08c74-df86-4cd7-9176-4ff267cab3e6",
|
||||||
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
"docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts",
|
||||||
"entry_point": "process_image_embedding",
|
"entry_point": "process_image_embedding",
|
||||||
"environment_variables": {},
|
"environment_variables": {},
|
||||||
@ -487,7 +487,7 @@
|
|||||||
"storage_source": [
|
"storage_source": [
|
||||||
{
|
{
|
||||||
"bucket": "gen-lang-client-0424120530-cloud-function-source",
|
"bucket": "gen-lang-client-0424120530-cloud-function-source",
|
||||||
"generation": 1748123369545880,
|
"generation": 1748171376287077,
|
||||||
"object": "function-source-58a2b7fe53bb2c8c921405cc965d635c.zip"
|
"object": "function-source-58a2b7fe53bb2c8c921405cc965d635c.zip"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -508,7 +508,7 @@
|
|||||||
"pubsub_topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic",
|
"pubsub_topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic",
|
||||||
"retry_policy": "RETRY_POLICY_RETRY",
|
"retry_policy": "RETRY_POLICY_RETRY",
|
||||||
"service_account_email": "761163285547-compute@developer.gserviceaccount.com",
|
"service_account_email": "761163285547-compute@developer.gserviceaccount.com",
|
||||||
"trigger": "projects/gen-lang-client-0424120530/locations/us-central1/triggers/process-image-embedding-422683",
|
"trigger": "projects/gen-lang-client-0424120530/locations/us-central1/triggers/process-image-embedding-013009",
|
||||||
"trigger_region": "us-central1"
|
"trigger_region": "us-central1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -554,12 +554,12 @@
|
|||||||
"vpc_connector_egress_settings": ""
|
"vpc_connector_egress_settings": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"state": "DEPLOYING",
|
"state": "ACTIVE",
|
||||||
"terraform_labels": {
|
"terraform_labels": {
|
||||||
"goog-terraform-provisioned": "true"
|
"goog-terraform-provisioned": "true"
|
||||||
},
|
},
|
||||||
"timeouts": null,
|
"timeouts": null,
|
||||||
"update_time": "2025-05-25T11:06:59.358560449Z",
|
"update_time": "2025-05-25T11:13:04.212724797Z",
|
||||||
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
"url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [
|
"sensitive_attributes": [
|
||||||
@ -588,7 +588,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjozNjAwMDAwMDAwMDAwLCJkZWxldGUiOjM2MDAwMDAwMDAwMDAsInVwZGF0ZSI6MzYwMDAwMDAwMDAwMH19",
|
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"data.archive_file.function_source",
|
"data.archive_file.function_source",
|
||||||
"data.google_project.current",
|
"data.google_project.current",
|
||||||
@ -836,7 +835,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "get_attr",
|
"type": "get_attr",
|
||||||
"value": "disk_encryption_key_rsa"
|
"value": "disk_encryption_key_raw"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -853,7 +852,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "get_attr",
|
"type": "get_attr",
|
||||||
"value": "disk_encryption_key_raw"
|
"value": "disk_encryption_key_rsa"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
@ -882,8 +881,8 @@
|
|||||||
"database_edition": "STANDARD",
|
"database_edition": "STANDARD",
|
||||||
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
||||||
"deletion_policy": "ABANDON",
|
"deletion_policy": "ABANDON",
|
||||||
"earliest_version_time": "2025-05-25T10:07:57.305684Z",
|
"earliest_version_time": "2025-05-25T11:07:20.673706Z",
|
||||||
"etag": "IIOH9r+9vo0DMKrW4vCEvY0D",
|
"etag": "IIrliOPKvo0DMKrW4vCEvY0D",
|
||||||
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
"id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb",
|
||||||
"key_prefix": "",
|
"key_prefix": "",
|
||||||
"location_id": "us-central1",
|
"location_id": "us-central1",
|
||||||
@ -1521,21 +1520,21 @@
|
|||||||
"content_encoding": "",
|
"content_encoding": "",
|
||||||
"content_language": "",
|
"content_language": "",
|
||||||
"content_type": "application/zip",
|
"content_type": "application/zip",
|
||||||
"crc32c": "eCjQFg==",
|
"crc32c": "EgiVnQ==",
|
||||||
"customer_encryption": [],
|
"customer_encryption": [],
|
||||||
"detect_md5hash": "WKK3/lO7LIySFAXMll1jXA==",
|
"detect_md5hash": "tTLPP/gdYt197AE+SGkxqg==",
|
||||||
"event_based_hold": false,
|
"event_based_hold": false,
|
||||||
"generation": 1748170673167525,
|
"generation": 1748174860755303,
|
||||||
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"id": "gen-lang-client-0424120530-cloud-function-source-function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"kms_key_name": "",
|
"kms_key_name": "",
|
||||||
"md5hash": "WKK3/lO7LIySFAXMll1jXA==",
|
"md5hash": "tTLPP/gdYt197AE+SGkxqg==",
|
||||||
"md5hexhash": "58a2b7fe53bb2c8c921405cc965d635c",
|
"md5hexhash": "b532cf3ff81d62dd7dec013e486931aa",
|
||||||
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-58a2b7fe53bb2c8c921405cc965d635c.zip?generation=1748170673167525\u0026alt=media",
|
"media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-b532cf3ff81d62dd7dec013e486931aa.zip?generation=1748174860755303\u0026alt=media",
|
||||||
"metadata": {},
|
"metadata": null,
|
||||||
"name": "function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"name": "function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"output_name": "function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"output_name": "function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"retention": [],
|
"retention": [],
|
||||||
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-58a2b7fe53bb2c8c921405cc965d635c.zip",
|
"self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-b532cf3ff81d62dd7dec013e486931aa.zip",
|
||||||
"source": "./function-source.zip",
|
"source": "./function-source.zip",
|
||||||
"storage_class": "STANDARD",
|
"storage_class": "STANDARD",
|
||||||
"temporary_hold": false,
|
"temporary_hold": false,
|
||||||
|
|||||||
334
docs/TESTING.md
334
docs/TESTING.md
@ -1,334 +0,0 @@
|
|||||||
# SEREACT Testing Guide
|
|
||||||
|
|
||||||
This document provides comprehensive information about testing the SEREACT API, including unit tests, integration tests, and end-to-end tests.
|
|
||||||
|
|
||||||
## Test Types
|
|
||||||
|
|
||||||
SEREACT includes several types of tests to ensure code quality and functionality:
|
|
||||||
|
|
||||||
### 1. Unit Tests (`unit`)
|
|
||||||
- **Purpose**: Test individual components in isolation using mocks
|
|
||||||
- **Speed**: Fast (< 1 second per test)
|
|
||||||
- **Dependencies**: None (uses mocks)
|
|
||||||
- **Location**: `tests/` (excluding `test_e2e.py`)
|
|
||||||
|
|
||||||
### 2. Integration Tests (`integration`)
|
|
||||||
- **Purpose**: Test component interactions with real services
|
|
||||||
- **Speed**: Medium (1-5 seconds per test)
|
|
||||||
- **Dependencies**: Real database connections
|
|
||||||
- **Location**: `tests/integration/`
|
|
||||||
|
|
||||||
### 3. End-to-End Tests (`e2e`)
|
|
||||||
- **Purpose**: Test complete user workflows from API to database
|
|
||||||
- **Speed**: Medium to slow (2-10 seconds per test)
|
|
||||||
- **Dependencies**: **Self-contained with artificial test data**
|
|
||||||
- **Location**: `tests/test_e2e.py`
|
|
||||||
|
|
||||||
### 4. Real Database Tests (`realdb`)
|
|
||||||
- **Purpose**: Test performance and scalability with real database
|
|
||||||
- **Speed**: Slow (5-30 seconds per test)
|
|
||||||
- **Dependencies**: Real database with artificial test data
|
|
||||||
- **Location**: `tests/test_e2e.py` (marked with `@pytest.mark.realdb`)
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests (recommended for development)
|
|
||||||
python scripts/run_tests.py all
|
|
||||||
|
|
||||||
# Run only unit tests (fastest)
|
|
||||||
python scripts/run_tests.py unit
|
|
||||||
|
|
||||||
# Run E2E tests (completely self-contained)
|
|
||||||
python scripts/run_tests.py e2e
|
|
||||||
|
|
||||||
# Run with coverage report
|
|
||||||
python scripts/run_tests.py coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using pytest directly
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# Run specific test types
|
|
||||||
pytest -m unit # Unit tests only
|
|
||||||
pytest -m integration # Integration tests only
|
|
||||||
pytest -m e2e # End-to-end tests only
|
|
||||||
pytest -m realdb # Real database tests only
|
|
||||||
|
|
||||||
# Run specific test files
|
|
||||||
pytest tests/test_e2e.py # All E2E tests
|
|
||||||
pytest tests/api/ # All API tests
|
|
||||||
|
|
||||||
# Run specific test methods
|
|
||||||
pytest tests/test_e2e.py::TestE2EWorkflows::test_bootstrap_and_basic_workflow
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Combinations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run unit and integration tests (skip E2E)
|
|
||||||
pytest -m "not e2e and not realdb"
|
|
||||||
|
|
||||||
# Run all tests except real database tests
|
|
||||||
pytest -m "not realdb"
|
|
||||||
|
|
||||||
# Run only E2E tests that don't require real database
|
|
||||||
pytest -m "e2e and not realdb"
|
|
||||||
```
|
|
||||||
|
|
||||||
## End-to-End Test Setup
|
|
||||||
|
|
||||||
**The E2E tests are now completely self-contained!** They automatically:
|
|
||||||
|
|
||||||
1. **Create artificial test data** at the start of each test class
|
|
||||||
2. **Run all tests** against this isolated test environment
|
|
||||||
3. **Clean up all test data** at the end automatically
|
|
||||||
|
|
||||||
### No Setup Required!
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Just run the tests - no environment variables or API keys needed!
|
|
||||||
python scripts/run_tests.py e2e
|
|
||||||
|
|
||||||
# Or with pytest directly
|
|
||||||
pytest -m e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Environment Creation
|
|
||||||
|
|
||||||
Each test class automatically creates its own isolated environment:
|
|
||||||
|
|
||||||
- **Unique team** with timestamp-based naming to avoid conflicts
|
|
||||||
- **Admin user** with unique email addresses
|
|
||||||
- **API keys** for authentication
|
|
||||||
- **Test images** uploaded during tests
|
|
||||||
- **Additional users/teams** as needed for specific tests
|
|
||||||
|
|
||||||
### Automatic Cleanup
|
|
||||||
|
|
||||||
At the end of each test class, all created resources are automatically deleted:
|
|
||||||
|
|
||||||
- All uploaded images are removed
|
|
||||||
- All created users are deleted
|
|
||||||
- All created teams are removed
|
|
||||||
- All API keys are revoked
|
|
||||||
|
|
||||||
### Advanced Test Modes
|
|
||||||
|
|
||||||
#### Integration Tests with Real Services
|
|
||||||
For testing with real Google Cloud services:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable integration tests
|
|
||||||
export E2E_INTEGRATION_TEST=1
|
|
||||||
|
|
||||||
# Run integration tests
|
|
||||||
pytest -m integration
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Real Database Performance Tests
|
|
||||||
For testing with real database connections and larger datasets:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable real database tests
|
|
||||||
export E2E_REALDB_TEST=1
|
|
||||||
|
|
||||||
# Run real database tests
|
|
||||||
pytest -m realdb
|
|
||||||
```
|
|
||||||
|
|
||||||
## E2E Test Coverage
|
|
||||||
|
|
||||||
The E2E tests cover the following workflows with artificial test data:
|
|
||||||
|
|
||||||
### Core Functionality
|
|
||||||
- ✅ **Bootstrap Setup**: Automatic creation of isolated test environment
|
|
||||||
- ✅ **Authentication**: API key validation and verification
|
|
||||||
- ✅ **Team Management**: Create, read, update, delete teams
|
|
||||||
- ✅ **User Management**: Create, read, update, delete users
|
|
||||||
- ✅ **API Key Management**: Create, list, revoke API keys
|
|
||||||
|
|
||||||
### Image Operations
|
|
||||||
- ✅ **Image Upload**: File upload with metadata
|
|
||||||
- ✅ **Image Retrieval**: Get image details and download
|
|
||||||
- ✅ **Image Updates**: Modify descriptions and tags
|
|
||||||
- ✅ **Image Listing**: Paginated image lists with filters
|
|
||||||
|
|
||||||
### Advanced Search Functionality
|
|
||||||
- ✅ **Text Search**: Search by description content
|
|
||||||
- ✅ **Tag Search**: Filter by tags
|
|
||||||
- ✅ **Advanced Search**: Combined filters and thresholds
|
|
||||||
- ✅ **Similarity Search**: Find similar images using embeddings
|
|
||||||
- ✅ **Search Performance**: Response time validation
|
|
||||||
|
|
||||||
### Security and Isolation
|
|
||||||
- ✅ **User Roles**: Admin vs regular user permissions
|
|
||||||
- ✅ **Multi-team Isolation**: Data privacy between teams
|
|
||||||
- ✅ **Access Control**: Unauthorized access prevention
|
|
||||||
- ✅ **Error Handling**: Graceful error responses
|
|
||||||
|
|
||||||
### Performance and Scalability
|
|
||||||
- ✅ **Bulk Operations**: Multiple image uploads
|
|
||||||
- ✅ **Concurrent Access**: Simultaneous user operations
|
|
||||||
- ✅ **Database Performance**: Query response times
|
|
||||||
- ✅ **Data Consistency**: Transaction integrity
|
|
||||||
|
|
||||||
## Test Data Management
|
|
||||||
|
|
||||||
### Unique Identifiers
|
|
||||||
All E2E tests use unique suffixes to avoid conflicts:
|
|
||||||
```python
|
|
||||||
unique_suffix = str(uuid.uuid4())[:8]
|
|
||||||
team_name = f"E2E Test Team {unique_suffix}_{int(time.time())}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Isolation Strategy
|
|
||||||
Tests are completely isolated:
|
|
||||||
- Each test class creates its own environment
|
|
||||||
- Uses timestamp-based unique identifiers
|
|
||||||
- No dependency on existing database state
|
|
||||||
- Can run in parallel without conflicts
|
|
||||||
|
|
||||||
### Automatic Resource Tracking
|
|
||||||
The test environment tracks all created resources:
|
|
||||||
```python
|
|
||||||
"created_resources": {
|
|
||||||
"teams": [team_id],
|
|
||||||
"users": [admin_user_id],
|
|
||||||
"api_keys": [api_key_id],
|
|
||||||
"images": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cleanup Strategy
|
|
||||||
Comprehensive cleanup at test completion:
|
|
||||||
- Images deleted first (to avoid orphaned files)
|
|
||||||
- Additional users deleted (preserving admin for team deletion)
|
|
||||||
- Additional teams deleted
|
|
||||||
- Main team deleted last (cascades to remaining resources)
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
### No Variables Required for Basic E2E Tests!
|
|
||||||
The standard E2E tests now run without any environment variables.
|
|
||||||
|
|
||||||
### Optional for Enhanced Testing
|
|
||||||
```bash
|
|
||||||
# Enable integration tests with real services
|
|
||||||
E2E_INTEGRATION_TEST=1
|
|
||||||
|
|
||||||
# Enable real database performance tests
|
|
||||||
E2E_REALDB_TEST=1
|
|
||||||
|
|
||||||
# Custom test database (if different from main)
|
|
||||||
TEST_FIRESTORE_PROJECT_ID="your-test-project"
|
|
||||||
TEST_GCS_BUCKET_NAME="your-test-bucket"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Continuous Integration
|
|
||||||
|
|
||||||
### GitHub Actions Example
|
|
||||||
```yaml
|
|
||||||
name: Tests
|
|
||||||
on: [push, pull_request]
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.10
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pip install -r requirements.txt
|
|
||||||
- name: Run unit tests
|
|
||||||
run: python scripts/run_tests.py unit
|
|
||||||
- name: Run E2E tests (self-contained)
|
|
||||||
run: python scripts/run_tests.py e2e
|
|
||||||
# No environment variables needed!
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
#### "Cannot create isolated test environment" Error
|
|
||||||
```bash
|
|
||||||
# This is rare but can happen if database has conflicting constraints
|
|
||||||
# Solution: Check database state or use a clean test database
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Tests Skipped Due to Missing Environment Variables
|
|
||||||
```bash
|
|
||||||
# Only affects integration and realdb tests
|
|
||||||
echo $E2E_INTEGRATION_TEST # Should be "1" for integration tests
|
|
||||||
echo $E2E_REALDB_TEST # Should be "1" for real database tests
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Slow Test Performance
|
|
||||||
```bash
|
|
||||||
# Run only fast tests
|
|
||||||
pytest -m "not realdb and not integration"
|
|
||||||
|
|
||||||
# Run tests in parallel (requires pytest-xdist)
|
|
||||||
pip install pytest-xdist
|
|
||||||
pytest -n auto
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Mode
|
|
||||||
```bash
|
|
||||||
# Run with verbose output
|
|
||||||
pytest -v -s tests/test_e2e.py
|
|
||||||
|
|
||||||
# Run single test with full output
|
|
||||||
pytest -v -s tests/test_e2e.py::TestE2EWorkflows::test_bootstrap_and_basic_workflow
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Writing New Tests
|
|
||||||
1. **Use the test_environment fixture** for automatic setup/cleanup
|
|
||||||
2. **Track created resources** in env["created_resources"]
|
|
||||||
3. **Use unique identifiers** for all test data
|
|
||||||
4. **Test both success and failure** scenarios
|
|
||||||
5. **Use appropriate markers** (`@pytest.mark.e2e`, etc.)
|
|
||||||
|
|
||||||
### Test Organization
|
|
||||||
1. **Group related tests** in classes with shared fixtures
|
|
||||||
2. **Use descriptive test names** that explain the scenario
|
|
||||||
3. **Keep tests independent** - no shared state between methods
|
|
||||||
4. **Use class-scoped fixtures** for expensive setup
|
|
||||||
5. **Document test purpose** in docstrings
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
1. **Use class-scoped fixtures** to share expensive setup
|
|
||||||
2. **Minimize database operations** in individual tests
|
|
||||||
3. **Clean up test data** automatically
|
|
||||||
4. **Run expensive tests** only when necessary
|
|
||||||
5. **Use artificial data** instead of real external dependencies
|
|
||||||
|
|
||||||
## Test Metrics
|
|
||||||
|
|
||||||
### Coverage Goals
|
|
||||||
- **Unit Tests**: > 90% code coverage
|
|
||||||
- **Integration Tests**: > 80% API endpoint coverage
|
|
||||||
- **E2E Tests**: > 95% user workflow coverage
|
|
||||||
|
|
||||||
### Performance Targets
|
|
||||||
- **Unit Tests**: < 1 second per test
|
|
||||||
- **Integration Tests**: < 5 seconds per test
|
|
||||||
- **E2E Tests**: < 10 seconds per test
|
|
||||||
- **Real DB Tests**: < 30 seconds per test
|
|
||||||
|
|
||||||
### Quality Metrics
|
|
||||||
- **Test Reliability**: > 99% pass rate
|
|
||||||
- **Test Maintainability**: Clear, readable test code
|
|
||||||
- **Test Coverage**: All critical paths tested
|
|
||||||
- **Test Documentation**: All test purposes documented
|
|
||||||
- **Test Isolation**: No dependencies between tests
|
|
||||||
@ -297,7 +297,6 @@ async def seed_images(team_ids, user_ids):
|
|||||||
{
|
{
|
||||||
"filename": "product_photo.jpg",
|
"filename": "product_photo.jpg",
|
||||||
"description": "Product photo for marketing",
|
"description": "Product photo for marketing",
|
||||||
"tags": ["product", "marketing", "high-resolution"],
|
|
||||||
"team_idx": 0,
|
"team_idx": 0,
|
||||||
"user_idx": 0,
|
"user_idx": 0,
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
@ -307,7 +306,6 @@ async def seed_images(team_ids, user_ids):
|
|||||||
{
|
{
|
||||||
"filename": "company_logo.png",
|
"filename": "company_logo.png",
|
||||||
"description": "Company logo",
|
"description": "Company logo",
|
||||||
"tags": ["logo", "branding"],
|
|
||||||
"team_idx": 1,
|
"team_idx": 1,
|
||||||
"user_idx": 2,
|
"user_idx": 2,
|
||||||
"width": 800,
|
"width": 800,
|
||||||
@ -317,7 +315,6 @@ async def seed_images(team_ids, user_ids):
|
|||||||
{
|
{
|
||||||
"filename": "support_screenshot.jpg",
|
"filename": "support_screenshot.jpg",
|
||||||
"description": "Screenshot for support ticket",
|
"description": "Screenshot for support ticket",
|
||||||
"tags": ["support", "screenshot", "bug"],
|
|
||||||
"team_idx": 2,
|
"team_idx": 2,
|
||||||
"user_idx": 3,
|
"user_idx": 3,
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
@ -372,7 +369,6 @@ async def seed_images(team_ids, user_ids):
|
|||||||
team_id=team_id,
|
team_id=team_id,
|
||||||
uploader_id=user_id,
|
uploader_id=user_id,
|
||||||
description=img_config['description'],
|
description=img_config['description'],
|
||||||
tags=img_config['tags'],
|
|
||||||
metadata=metadata
|
metadata=metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
102
simple_search_test.py
Normal file
102
simple_search_test.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test script to embed text and search Qdrant without filters
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def simple_search_test():
|
||||||
|
"""Simple test: embed text and search without filters"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import services
|
||||||
|
from src.services.vector_db import VectorDatabaseService
|
||||||
|
from src.services.embedding_service import EmbeddingService
|
||||||
|
|
||||||
|
# Initialize services
|
||||||
|
logger.info("Initializing services...")
|
||||||
|
vector_db = VectorDatabaseService()
|
||||||
|
embedding_service = EmbeddingService()
|
||||||
|
|
||||||
|
# Test 1: Generate text embedding
|
||||||
|
logger.info("=== Generating Text Embedding ===")
|
||||||
|
search_query = "blank"
|
||||||
|
text_embedding = await embedding_service.generate_text_embedding(search_query)
|
||||||
|
|
||||||
|
if text_embedding:
|
||||||
|
logger.info(f"✓ Generated embedding for '{search_query}' - length: {len(text_embedding)}")
|
||||||
|
else:
|
||||||
|
logger.error("✗ Failed to generate text embedding")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Test 2: Search without any filters
|
||||||
|
logger.info("=== Searching Qdrant (No Filters) ===")
|
||||||
|
|
||||||
|
# Try different thresholds to see what we get
|
||||||
|
thresholds = [0.1, 0.3, 0.5, 0.65, 0.8]
|
||||||
|
|
||||||
|
for threshold in thresholds:
|
||||||
|
logger.info(f"\n--- Threshold: {threshold} ---")
|
||||||
|
|
||||||
|
search_results = vector_db.search_similar_images(
|
||||||
|
query_vector=text_embedding,
|
||||||
|
limit=10,
|
||||||
|
score_threshold=threshold
|
||||||
|
# No filter_conditions = search everything
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Found {len(search_results)} results")
|
||||||
|
|
||||||
|
# Show top 3 results
|
||||||
|
for i, result in enumerate(search_results[:3]):
|
||||||
|
logger.info(f" {i+1}. Score: {result['score']:.4f} | ID: {result['image_id']} | File: {result['metadata'].get('filename', 'N/A')}")
|
||||||
|
|
||||||
|
# Test 3: Very low threshold to see all data
|
||||||
|
logger.info("\n=== All Data (Threshold 0.0) ===")
|
||||||
|
all_results = vector_db.search_similar_images(
|
||||||
|
query_vector=text_embedding,
|
||||||
|
limit=50,
|
||||||
|
score_threshold=0.0 # Get everything
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Total vectors in collection: {len(all_results)}")
|
||||||
|
|
||||||
|
# Show some stats
|
||||||
|
if all_results:
|
||||||
|
scores = [r['score'] for r in all_results]
|
||||||
|
logger.info(f"Score range: {min(scores):.4f} to {max(scores):.4f}")
|
||||||
|
logger.info(f"Average score: {sum(scores)/len(scores):.4f}")
|
||||||
|
|
||||||
|
# Show top 5 and bottom 5
|
||||||
|
logger.info("\nTop 5 results:")
|
||||||
|
for i, result in enumerate(all_results[:5]):
|
||||||
|
logger.info(f" {i+1}. Score: {result['score']:.4f} | ID: {result['image_id']}")
|
||||||
|
|
||||||
|
if len(all_results) > 5:
|
||||||
|
logger.info("\nBottom 5 results:")
|
||||||
|
for i, result in enumerate(all_results[-5:]):
|
||||||
|
logger.info(f" {len(all_results)-4+i}. Score: {result['score']:.4f} | ID: {result['image_id']}")
|
||||||
|
|
||||||
|
logger.info("\n✓ Simple search test completed!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"✗ Test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = asyncio.run(simple_search_test())
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@ -37,7 +37,6 @@ async def upload_image(
|
|||||||
request: Request,
|
request: Request,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[str] = None,
|
|
||||||
collection_id: Optional[str] = None,
|
collection_id: Optional[str] = None,
|
||||||
current_user: UserModel = Depends(get_current_user)
|
current_user: UserModel = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
@ -69,10 +68,6 @@ async def upload_image(
|
|||||||
file, str(current_user.team_id)
|
file, str(current_user.team_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process tags
|
|
||||||
tag_list = []
|
|
||||||
if tags:
|
|
||||||
tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
|
|
||||||
|
|
||||||
# Create image record
|
# Create image record
|
||||||
image = ImageModel(
|
image = ImageModel(
|
||||||
@ -85,7 +80,6 @@ async def upload_image(
|
|||||||
team_id=current_user.team_id,
|
team_id=current_user.team_id,
|
||||||
uploader_id=current_user.id,
|
uploader_id=current_user.id,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tag_list,
|
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
collection_id=ObjectId(collection_id) if collection_id else None
|
collection_id=ObjectId(collection_id) if collection_id else None
|
||||||
)
|
)
|
||||||
@ -125,7 +119,6 @@ async def upload_image(
|
|||||||
uploader_id=str(created_image.uploader_id),
|
uploader_id=str(created_image.uploader_id),
|
||||||
upload_date=created_image.upload_date,
|
upload_date=created_image.upload_date,
|
||||||
description=created_image.description,
|
description=created_image.description,
|
||||||
tags=created_image.tags,
|
|
||||||
metadata=created_image.metadata,
|
metadata=created_image.metadata,
|
||||||
has_embedding=created_image.has_embedding,
|
has_embedding=created_image.has_embedding,
|
||||||
collection_id=str(created_image.collection_id) if created_image.collection_id else None
|
collection_id=str(created_image.collection_id) if created_image.collection_id else None
|
||||||
@ -143,7 +136,6 @@ async def list_images(
|
|||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(50, ge=1, le=100),
|
limit: int = Query(50, ge=1, le=100),
|
||||||
collection_id: Optional[str] = None,
|
collection_id: Optional[str] = None,
|
||||||
tags: Optional[str] = None,
|
|
||||||
current_user: UserModel = Depends(get_current_user)
|
current_user: UserModel = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -156,7 +148,6 @@ async def list_images(
|
|||||||
skip: Number of records to skip for pagination
|
skip: Number of records to skip for pagination
|
||||||
limit: Maximum number of records to return (1-100)
|
limit: Maximum number of records to return (1-100)
|
||||||
collection_id: Optional filter by collection ID
|
collection_id: Optional filter by collection ID
|
||||||
tags: Optional comma-separated list of tags to filter by
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of images with pagination metadata
|
List of images with pagination metadata
|
||||||
@ -167,11 +158,6 @@ async def list_images(
|
|||||||
team_id=str(current_user.team_id)
|
team_id=str(current_user.team_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse tags filter
|
|
||||||
tag_filter = []
|
|
||||||
if tags:
|
|
||||||
tag_filter = [tag.strip() for tag in tags.split(',') if tag.strip()]
|
|
||||||
|
|
||||||
# Check if user is admin - if so, get all images across all teams
|
# Check if user is admin - if so, get all images across all teams
|
||||||
if current_user.is_admin:
|
if current_user.is_admin:
|
||||||
# Admin users can see all images across all teams
|
# Admin users can see all images across all teams
|
||||||
@ -179,13 +165,11 @@ async def list_images(
|
|||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
collection_id=ObjectId(collection_id) if collection_id else None,
|
collection_id=ObjectId(collection_id) if collection_id else None,
|
||||||
tags=tag_filter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get total count for admin
|
# Get total count for admin
|
||||||
total = await image_repository.count_all(
|
total = await image_repository.count_all(
|
||||||
collection_id=ObjectId(collection_id) if collection_id else None,
|
collection_id=ObjectId(collection_id) if collection_id else None,
|
||||||
tags=tag_filter
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Regular users only see images from their team
|
# Regular users only see images from their team
|
||||||
@ -194,14 +178,12 @@ async def list_images(
|
|||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
collection_id=ObjectId(collection_id) if collection_id else None,
|
collection_id=ObjectId(collection_id) if collection_id else None,
|
||||||
tags=tag_filter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get total count for regular user
|
# Get total count for regular user
|
||||||
total = await image_repository.count_by_team(
|
total = await image_repository.count_by_team(
|
||||||
current_user.team_id,
|
current_user.team_id,
|
||||||
collection_id=ObjectId(collection_id) if collection_id else None,
|
collection_id=ObjectId(collection_id) if collection_id else None,
|
||||||
tags=tag_filter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert to response
|
# Convert to response
|
||||||
@ -222,7 +204,6 @@ async def list_images(
|
|||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
description=image.description,
|
description=image.description,
|
||||||
tags=image.tags,
|
|
||||||
metadata=image.metadata,
|
metadata=image.metadata,
|
||||||
has_embedding=image.has_embedding,
|
has_embedding=image.has_embedding,
|
||||||
collection_id=str(image.collection_id) if image.collection_id else None
|
collection_id=str(image.collection_id) if image.collection_id else None
|
||||||
@ -275,7 +256,6 @@ async def get_image(
|
|||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
description=image.description,
|
description=image.description,
|
||||||
tags=image.tags,
|
|
||||||
metadata=image.metadata,
|
metadata=image.metadata,
|
||||||
has_embedding=image.has_embedding,
|
has_embedding=image.has_embedding,
|
||||||
collection_id=str(image.collection_id) if image.collection_id else None,
|
collection_id=str(image.collection_id) if image.collection_id else None,
|
||||||
@ -375,7 +355,6 @@ async def update_image(
|
|||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
description=image.description,
|
description=image.description,
|
||||||
tags=image.tags,
|
|
||||||
metadata=image.metadata,
|
metadata=image.metadata,
|
||||||
has_embedding=image.has_embedding,
|
has_embedding=image.has_embedding,
|
||||||
collection_id=str(image.collection_id) if image.collection_id else None
|
collection_id=str(image.collection_id) if image.collection_id else None
|
||||||
@ -402,7 +381,6 @@ async def update_image(
|
|||||||
uploader_id=str(updated_image.uploader_id),
|
uploader_id=str(updated_image.uploader_id),
|
||||||
upload_date=updated_image.upload_date,
|
upload_date=updated_image.upload_date,
|
||||||
description=updated_image.description,
|
description=updated_image.description,
|
||||||
tags=updated_image.tags,
|
|
||||||
metadata=updated_image.metadata,
|
metadata=updated_image.metadata,
|
||||||
has_embedding=updated_image.has_embedding,
|
has_embedding=updated_image.has_embedding,
|
||||||
collection_id=str(updated_image.collection_id) if updated_image.collection_id else None
|
collection_id=str(updated_image.collection_id) if updated_image.collection_id else None
|
||||||
|
|||||||
@ -33,9 +33,8 @@ async def search_images(
|
|||||||
request: Request,
|
request: Request,
|
||||||
q: str = Query(..., description="Search query"),
|
q: str = Query(..., description="Search query"),
|
||||||
limit: int = Query(10, ge=1, le=50, description="Number of results to return"),
|
limit: int = Query(10, ge=1, le=50, description="Number of results to return"),
|
||||||
threshold: float = Query(0.7, ge=0.0, le=1.0, description="Similarity threshold"),
|
threshold: float = Query(0.65, ge=0.0, le=1.0, description="Similarity threshold"),
|
||||||
collection_id: Optional[str] = Query(None, description="Filter by collection ID"),
|
collection_id: Optional[str] = Query(None, description="Filter by collection ID"),
|
||||||
tags: Optional[str] = Query(None, description="Filter by tags (comma-separated)"),
|
|
||||||
current_user: UserModel = Depends(get_current_user)
|
current_user: UserModel = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -83,19 +82,13 @@ async def search_images(
|
|||||||
# Get image metadata from database
|
# Get image metadata from database
|
||||||
images = await image_repository.get_by_ids(image_ids)
|
images = await image_repository.get_by_ids(image_ids)
|
||||||
|
|
||||||
# Filter by collection and tags if specified
|
# Filter by collection if specified
|
||||||
filtered_images = []
|
filtered_images = []
|
||||||
for image in images:
|
for image in images:
|
||||||
# Check collection filter
|
# Check collection filter
|
||||||
if collection_id and str(image.collection_id) != collection_id:
|
if collection_id and str(image.collection_id) != collection_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check tags filter
|
|
||||||
if tags:
|
|
||||||
tag_filter = [tag.strip() for tag in tags.split(',') if tag.strip()]
|
|
||||||
if not any(tag in image.tags for tag in tag_filter):
|
|
||||||
continue
|
|
||||||
|
|
||||||
filtered_images.append(image)
|
filtered_images.append(image)
|
||||||
|
|
||||||
# Convert to response format with similarity scores
|
# Convert to response format with similarity scores
|
||||||
@ -115,7 +108,6 @@ async def search_images(
|
|||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
description=image.description,
|
description=image.description,
|
||||||
tags=image.tags,
|
|
||||||
metadata=image.metadata,
|
metadata=image.metadata,
|
||||||
has_embedding=image.has_embedding,
|
has_embedding=image.has_embedding,
|
||||||
collection_id=str(image.collection_id) if image.collection_id else None,
|
collection_id=str(image.collection_id) if image.collection_id else None,
|
||||||
@ -194,11 +186,6 @@ async def search_images_advanced(
|
|||||||
if search_request.collection_id and str(image.collection_id) != search_request.collection_id:
|
if search_request.collection_id and str(image.collection_id) != search_request.collection_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check tags filter
|
|
||||||
if search_request.tags:
|
|
||||||
if not any(tag in image.tags for tag in search_request.tags):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check date range filter
|
# Check date range filter
|
||||||
if search_request.date_from and image.upload_date < search_request.date_from:
|
if search_request.date_from and image.upload_date < search_request.date_from:
|
||||||
continue
|
continue
|
||||||
@ -229,7 +216,6 @@ async def search_images_advanced(
|
|||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
description=image.description,
|
description=image.description,
|
||||||
tags=image.tags,
|
|
||||||
metadata=image.metadata,
|
metadata=image.metadata,
|
||||||
has_embedding=image.has_embedding,
|
has_embedding=image.has_embedding,
|
||||||
collection_id=str(image.collection_id) if image.collection_id else None,
|
collection_id=str(image.collection_id) if image.collection_id else None,
|
||||||
@ -257,7 +243,7 @@ async def find_similar_images(
|
|||||||
image_id: str,
|
image_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
limit: int = Query(10, ge=1, le=50, description="Number of similar images to return"),
|
limit: int = Query(10, ge=1, le=50, description="Number of similar images to return"),
|
||||||
threshold: float = Query(0.7, ge=0.0, le=1.0, description="Similarity threshold"),
|
threshold: float = Query(0.65, ge=0.0, le=1.0, description="Similarity threshold"),
|
||||||
current_user: UserModel = Depends(get_current_user)
|
current_user: UserModel = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -347,7 +333,6 @@ async def find_similar_images(
|
|||||||
uploader_id=str(image.uploader_id),
|
uploader_id=str(image.uploader_id),
|
||||||
upload_date=image.upload_date,
|
upload_date=image.upload_date,
|
||||||
description=image.description,
|
description=image.description,
|
||||||
tags=image.tags,
|
|
||||||
metadata=image.metadata,
|
metadata=image.metadata,
|
||||||
has_embedding=image.has_embedding,
|
has_embedding=image.has_embedding,
|
||||||
collection_id=str(image.collection_id) if image.collection_id else None,
|
collection_id=str(image.collection_id) if image.collection_id else None,
|
||||||
|
|||||||
@ -37,7 +37,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
collection_id: Optional[ObjectId] = None,
|
collection_id: Optional[ObjectId] = None,
|
||||||
tags: Optional[List[str]] = None
|
|
||||||
) -> List[ImageModel]:
|
) -> List[ImageModel]:
|
||||||
"""
|
"""
|
||||||
Get images by team with pagination and filtering
|
Get images by team with pagination and filtering
|
||||||
@ -47,7 +46,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
skip: Number of records to skip
|
skip: Number of records to skip
|
||||||
limit: Maximum number of records to return
|
limit: Maximum number of records to return
|
||||||
collection_id: Optional collection ID filter
|
collection_id: Optional collection ID filter
|
||||||
tags: Optional list of tags to filter by
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of images
|
List of images
|
||||||
@ -63,13 +61,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
if collection_id:
|
if collection_id:
|
||||||
filtered_images = [image for image in filtered_images if image.collection_id == collection_id]
|
filtered_images = [image for image in filtered_images if image.collection_id == collection_id]
|
||||||
|
|
||||||
# Filter by tags if specified
|
|
||||||
if tags:
|
|
||||||
filtered_images = [
|
|
||||||
image for image in filtered_images
|
|
||||||
if any(tag in image.tags for tag in tags)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Apply pagination
|
# Apply pagination
|
||||||
return filtered_images[skip:skip + limit]
|
return filtered_images[skip:skip + limit]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -80,7 +71,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
self,
|
self,
|
||||||
team_id: ObjectId,
|
team_id: ObjectId,
|
||||||
collection_id: Optional[ObjectId] = None,
|
collection_id: Optional[ObjectId] = None,
|
||||||
tags: Optional[List[str]] = None
|
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Count images by team with filtering
|
Count images by team with filtering
|
||||||
@ -88,7 +78,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
Args:
|
Args:
|
||||||
team_id: Team ID
|
team_id: Team ID
|
||||||
collection_id: Optional collection ID filter
|
collection_id: Optional collection ID filter
|
||||||
tags: Optional list of tags to filter by
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Count of images
|
Count of images
|
||||||
@ -104,12 +93,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
if collection_id:
|
if collection_id:
|
||||||
filtered_images = [image for image in filtered_images if image.collection_id == collection_id]
|
filtered_images = [image for image in filtered_images if image.collection_id == collection_id]
|
||||||
|
|
||||||
# Filter by tags if specified
|
|
||||||
if tags:
|
|
||||||
filtered_images = [
|
|
||||||
image for image in filtered_images
|
|
||||||
if any(tag in image.tags for tag in tags)
|
|
||||||
]
|
|
||||||
|
|
||||||
return len(filtered_images)
|
return len(filtered_images)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -154,31 +137,12 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
logger.error(f"Error getting images by uploader ID: {e}")
|
logger.error(f"Error getting images by uploader ID: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def get_by_tag(self, tag: str) -> list[ImageModel]:
|
|
||||||
"""
|
|
||||||
Get images by tag
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tag: Tag
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of images
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# This would typically use a Firestore query, but for simplicity
|
|
||||||
# we'll get all images and filter in memory
|
|
||||||
images = await self.get_all()
|
|
||||||
return [image for image in images if tag in image.tags]
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting images by tag: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def get_all_with_pagination(
|
async def get_all_with_pagination(
|
||||||
self,
|
self,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
collection_id: Optional[ObjectId] = None,
|
collection_id: Optional[ObjectId] = None,
|
||||||
tags: Optional[List[str]] = None
|
|
||||||
) -> List[ImageModel]:
|
) -> List[ImageModel]:
|
||||||
"""
|
"""
|
||||||
Get all images across all teams with pagination and filtering (admin only)
|
Get all images across all teams with pagination and filtering (admin only)
|
||||||
@ -187,7 +151,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
skip: Number of records to skip
|
skip: Number of records to skip
|
||||||
limit: Maximum number of records to return
|
limit: Maximum number of records to return
|
||||||
collection_id: Optional collection ID filter
|
collection_id: Optional collection ID filter
|
||||||
tags: Optional list of tags to filter by
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of images
|
List of images
|
||||||
@ -200,12 +163,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
if collection_id:
|
if collection_id:
|
||||||
images = [image for image in images if image.collection_id == collection_id]
|
images = [image for image in images if image.collection_id == collection_id]
|
||||||
|
|
||||||
# Filter by tags if specified
|
|
||||||
if tags:
|
|
||||||
images = [
|
|
||||||
image for image in images
|
|
||||||
if any(tag in image.tags for tag in tags)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Apply pagination
|
# Apply pagination
|
||||||
return images[skip:skip + limit]
|
return images[skip:skip + limit]
|
||||||
@ -216,14 +173,12 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
async def count_all(
|
async def count_all(
|
||||||
self,
|
self,
|
||||||
collection_id: Optional[ObjectId] = None,
|
collection_id: Optional[ObjectId] = None,
|
||||||
tags: Optional[List[str]] = None
|
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Count all images across all teams with filtering (admin only)
|
Count all images across all teams with filtering (admin only)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
collection_id: Optional collection ID filter
|
collection_id: Optional collection ID filter
|
||||||
tags: Optional list of tags to filter by
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Count of images
|
Count of images
|
||||||
@ -236,12 +191,6 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
|||||||
if collection_id:
|
if collection_id:
|
||||||
images = [image for image in images if image.collection_id == collection_id]
|
images = [image for image in images if image.collection_id == collection_id]
|
||||||
|
|
||||||
# Filter by tags if specified
|
|
||||||
if tags:
|
|
||||||
images = [
|
|
||||||
image for image in images
|
|
||||||
if any(tag in image.tags for tag in tags)
|
|
||||||
]
|
|
||||||
|
|
||||||
return len(images)
|
return len(images)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -19,7 +19,6 @@ class ImageModel(BaseModel):
|
|||||||
upload_date: datetime = Field(default_factory=datetime.utcnow)
|
upload_date: datetime = Field(default_factory=datetime.utcnow)
|
||||||
last_accessed: Optional[datetime] = None
|
last_accessed: Optional[datetime] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = []
|
|
||||||
metadata: Dict[str, Any] = {}
|
metadata: Dict[str, Any] = {}
|
||||||
collection_id: Optional[PyObjectId] = None
|
collection_id: Optional[PyObjectId] = None
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ from pydantic import BaseModel, Field, HttpUrl
|
|||||||
class ImageBase(BaseModel):
|
class ImageBase(BaseModel):
|
||||||
"""Base schema for image data"""
|
"""Base schema for image data"""
|
||||||
description: Optional[str] = Field(None, description="Image description", max_length=500)
|
description: Optional[str] = Field(None, description="Image description", max_length=500)
|
||||||
tags: List[str] = Field(default=[], description="Image tags")
|
|
||||||
|
|
||||||
class ImageCreate(ImageBase):
|
class ImageCreate(ImageBase):
|
||||||
"""Schema for creating an image"""
|
"""Schema for creating an image"""
|
||||||
@ -19,7 +18,6 @@ class ImageUpload(ImageBase):
|
|||||||
class ImageUpdate(BaseModel):
|
class ImageUpdate(BaseModel):
|
||||||
"""Schema for updating an image"""
|
"""Schema for updating an image"""
|
||||||
description: Optional[str] = Field(None, description="Image description", max_length=500)
|
description: Optional[str] = Field(None, description="Image description", max_length=500)
|
||||||
tags: Optional[List[str]] = Field(None, description="Image tags")
|
|
||||||
metadata: Optional[Dict[str, Any]] = Field(None, description="Image metadata")
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Image metadata")
|
||||||
collection_id: Optional[str] = Field(None, description="Collection ID to organize images")
|
collection_id: Optional[str] = Field(None, description="Collection ID to organize images")
|
||||||
|
|
||||||
@ -57,7 +55,6 @@ class ImageResponse(ImageBase):
|
|||||||
"upload_date": "2023-10-20T10:00:00",
|
"upload_date": "2023-10-20T10:00:00",
|
||||||
"last_accessed": "2023-10-21T10:00:00",
|
"last_accessed": "2023-10-21T10:00:00",
|
||||||
"description": "Beautiful sunset over the mountains",
|
"description": "Beautiful sunset over the mountains",
|
||||||
"tags": ["sunset", "mountains", "nature"],
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
@ -94,7 +91,6 @@ class ImageListResponse(BaseModel):
|
|||||||
"upload_date": "2023-10-20T10:00:00",
|
"upload_date": "2023-10-20T10:00:00",
|
||||||
"last_accessed": "2023-10-21T10:00:00",
|
"last_accessed": "2023-10-21T10:00:00",
|
||||||
"description": "Beautiful sunset over the mountains",
|
"description": "Beautiful sunset over the mountains",
|
||||||
"tags": ["sunset", "mountains", "nature"],
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
@ -145,7 +141,6 @@ class ImageSearchResult(BaseModel):
|
|||||||
"upload_date": "2023-10-20T10:00:00",
|
"upload_date": "2023-10-20T10:00:00",
|
||||||
"last_accessed": "2023-10-21T10:00:00",
|
"last_accessed": "2023-10-21T10:00:00",
|
||||||
"description": "Beautiful sunset over the mountains",
|
"description": "Beautiful sunset over the mountains",
|
||||||
"tags": ["sunset", "mountains", "nature"],
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
@ -181,7 +176,6 @@ class ImageSearchResponse(BaseModel):
|
|||||||
"upload_date": "2023-10-20T10:00:00",
|
"upload_date": "2023-10-20T10:00:00",
|
||||||
"last_accessed": "2023-10-21T10:00:00",
|
"last_accessed": "2023-10-21T10:00:00",
|
||||||
"description": "Beautiful sunset over the mountains",
|
"description": "Beautiful sunset over the mountains",
|
||||||
"tags": ["sunset", "mountains", "nature"],
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
|
|||||||
@ -10,7 +10,6 @@ class SearchRequest(BaseModel):
|
|||||||
limit: int = Field(10, description="Maximum number of results", ge=1, le=50)
|
limit: int = Field(10, description="Maximum number of results", ge=1, le=50)
|
||||||
threshold: float = Field(0.7, description="Similarity threshold", ge=0.0, le=1.0)
|
threshold: float = Field(0.7, description="Similarity threshold", ge=0.0, le=1.0)
|
||||||
collection_id: Optional[str] = Field(None, description="Filter by collection ID")
|
collection_id: Optional[str] = Field(None, description="Filter by collection ID")
|
||||||
tags: Optional[List[str]] = Field(None, description="Filter by tags")
|
|
||||||
date_from: Optional[datetime] = Field(None, description="Filter images uploaded after this date")
|
date_from: Optional[datetime] = Field(None, description="Filter images uploaded after this date")
|
||||||
date_to: Optional[datetime] = Field(None, description="Filter images uploaded before this date")
|
date_to: Optional[datetime] = Field(None, description="Filter images uploaded before this date")
|
||||||
uploader_id: Optional[str] = Field(None, description="Filter by uploader ID")
|
uploader_id: Optional[str] = Field(None, description="Filter by uploader ID")
|
||||||
@ -22,7 +21,6 @@ class SearchRequest(BaseModel):
|
|||||||
"limit": 10,
|
"limit": 10,
|
||||||
"threshold": 0.7,
|
"threshold": 0.7,
|
||||||
"collection_id": "507f1f77bcf86cd799439044",
|
"collection_id": "507f1f77bcf86cd799439044",
|
||||||
"tags": ["nature", "landscape"],
|
|
||||||
"date_from": "2023-01-01T00:00:00",
|
"date_from": "2023-01-01T00:00:00",
|
||||||
"date_to": "2023-12-31T23:59:59",
|
"date_to": "2023-12-31T23:59:59",
|
||||||
"uploader_id": "507f1f77bcf86cd799439033"
|
"uploader_id": "507f1f77bcf86cd799439033"
|
||||||
@ -56,7 +54,6 @@ class SearchResponse(BaseModel):
|
|||||||
"upload_date": "2023-10-20T10:00:00",
|
"upload_date": "2023-10-20T10:00:00",
|
||||||
"last_accessed": "2023-10-21T10:00:00",
|
"last_accessed": "2023-10-21T10:00:00",
|
||||||
"description": "Beautiful sunset over the mountains",
|
"description": "Beautiful sunset over the mountains",
|
||||||
"tags": ["sunset", "mountains", "nature"],
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
|
|||||||
@ -139,7 +139,7 @@ class VectorDatabaseService:
|
|||||||
self,
|
self,
|
||||||
query_vector: List[float],
|
query_vector: List[float],
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
score_threshold: float = 0.7,
|
score_threshold: float = 0.65,
|
||||||
filter_conditions: Dict[str, Any] = None
|
filter_conditions: Dict[str, Any] = None
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -154,7 +154,7 @@ class MockImageRepository:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def search(self, team_id: ObjectId, query: str = None, tags: List[str] = None) -> List[ImageModel]:
|
async def search(self, team_id: ObjectId, query: str = None) -> List[ImageModel]:
|
||||||
results = [img for img in self.images.values() if str(img.team_id) == str(team_id)]
|
results = [img for img in self.images.values() if str(img.team_id) == str(team_id)]
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
@ -164,9 +164,6 @@ class MockImageRepository:
|
|||||||
query in img.filename.lower() or
|
query in img.filename.lower() or
|
||||||
query in img.original_filename.lower()]
|
query in img.original_filename.lower()]
|
||||||
|
|
||||||
if tags:
|
|
||||||
results = [img for img in results if all(tag in img.tags for tag in tags)]
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,24 +2,28 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
import uuid
|
import uuid
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock, Mock
|
||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from src.db.repositories.image_repository import ImageRepository, image_repository
|
from src.db.repositories.image_repository import ImageRepository, image_repository
|
||||||
from src.models.image import ImageModel
|
from src.models.image import ImageModel
|
||||||
from main import app
|
from main import app
|
||||||
|
|
||||||
# Hardcoded API key as requested
|
# Test constants
|
||||||
API_KEY = "Wwg4eJjJ.d03970d43cf3a454ad4168b3226b423f"
|
API_KEY = "test-api-key-12345"
|
||||||
|
MOCK_TEAM_ID = "507f1f77bcf86cd799439011"
|
||||||
# Mock team ID for testing
|
MOCK_USER_ID = "507f1f77bcf86cd799439012"
|
||||||
MOCK_TEAM_ID = "test-team-123"
|
|
||||||
MOCK_USER_ID = "test-user-456"
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_image_path():
|
def test_image_path():
|
||||||
"""Get path to test image"""
|
"""Create a temporary test image file"""
|
||||||
# Assuming image.png exists in the images directory
|
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
||||||
return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "images", "image.png")
|
img = Image.new('RGB', (100, 100), color='red')
|
||||||
|
img.save(tmp.name, 'PNG')
|
||||||
|
yield tmp.name
|
||||||
|
os.unlink(tmp.name)
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client():
|
def client():
|
||||||
@ -28,31 +32,25 @@ def client():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_auth():
|
def mock_auth():
|
||||||
"""Mock the authentication to use our hardcoded API key"""
|
"""Mock authentication to return a valid user"""
|
||||||
with patch('src.api.v1.auth.get_current_user') as mock_auth:
|
with patch('src.auth.dependencies.get_current_user') as mock_get_user:
|
||||||
# Configure the mock to return a valid user
|
mock_user = Mock()
|
||||||
mock_auth.return_value = {
|
mock_user.id = MOCK_USER_ID
|
||||||
"id": MOCK_USER_ID,
|
mock_user.team_id = MOCK_TEAM_ID
|
||||||
"team_id": MOCK_TEAM_ID,
|
mock_user.is_admin = True
|
||||||
"email": "test@example.com",
|
mock_get_user.return_value = mock_user
|
||||||
"name": "Test User"
|
yield mock_user
|
||||||
}
|
|
||||||
yield mock_auth
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_storage_service():
|
def mock_storage_service():
|
||||||
"""Mock the storage service"""
|
"""Mock the storage service"""
|
||||||
with patch('src.services.storage.StorageService') as MockStorageService:
|
with patch('src.services.storage_service.StorageService') as MockStorageService:
|
||||||
# Configure the mock
|
mock_service = Mock()
|
||||||
mock_service = MagicMock()
|
mock_service.upload_file.return_value = f"{MOCK_TEAM_ID}/test-image-123.png"
|
||||||
|
mock_service.get_file_metadata.return_value = Mock(
|
||||||
# Mock the upload_file method
|
size=1024,
|
||||||
test_storage_path = f"{MOCK_TEAM_ID}/test-image-{uuid.uuid4().hex}.png"
|
content_type="image/png",
|
||||||
mock_service.upload_file.return_value = (
|
metadata={
|
||||||
test_storage_path, # storage_path
|
|
||||||
"image/png", # content_type
|
|
||||||
1024, # file_size
|
|
||||||
{ # metadata
|
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600,
|
"height": 600,
|
||||||
"format": "PNG",
|
"format": "PNG",
|
||||||
@ -85,7 +83,7 @@ async def test_upload_image_endpoint(client, test_image_path, mock_auth, mock_st
|
|||||||
# Create API endpoint route if it doesn't exist yet
|
# Create API endpoint route if it doesn't exist yet
|
||||||
with patch('src.api.v1.images.router.post') as mock_post:
|
with patch('src.api.v1.images.router.post') as mock_post:
|
||||||
# Modify the router for testing purposes
|
# Modify the router for testing purposes
|
||||||
async def mock_upload_image_handler(file, description=None, tags=None, current_user=None):
|
async def mock_upload_image_handler(file, description=None, current_user=None):
|
||||||
# This simulates the handler that would be in src/api/v1/images.py
|
# This simulates the handler that would be in src/api/v1/images.py
|
||||||
# Store image in database
|
# Store image in database
|
||||||
image = ImageModel(
|
image = ImageModel(
|
||||||
@ -96,8 +94,7 @@ async def test_upload_image_endpoint(client, test_image_path, mock_auth, mock_st
|
|||||||
storage_path=f"{MOCK_TEAM_ID}/test-image-123.png",
|
storage_path=f"{MOCK_TEAM_ID}/test-image-123.png",
|
||||||
team_id=MOCK_TEAM_ID,
|
team_id=MOCK_TEAM_ID,
|
||||||
uploader_id=MOCK_USER_ID,
|
uploader_id=MOCK_USER_ID,
|
||||||
description=description,
|
description=description
|
||||||
tags=tags.split(",") if tags else []
|
|
||||||
)
|
)
|
||||||
created_image = await image_repository.create(image)
|
created_image = await image_repository.create(image)
|
||||||
|
|
||||||
@ -109,8 +106,7 @@ async def test_upload_image_endpoint(client, test_image_path, mock_auth, mock_st
|
|||||||
"content_type": created_image.content_type,
|
"content_type": created_image.content_type,
|
||||||
"team_id": str(created_image.team_id),
|
"team_id": str(created_image.team_id),
|
||||||
"uploader_id": str(created_image.uploader_id),
|
"uploader_id": str(created_image.uploader_id),
|
||||||
"description": created_image.description,
|
"description": created_image.description
|
||||||
"tags": created_image.tags
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_post.return_value = mock_upload_image_handler
|
mock_post.return_value = mock_upload_image_handler
|
||||||
@ -126,8 +122,7 @@ async def test_upload_image_endpoint(client, test_image_path, mock_auth, mock_st
|
|||||||
headers={"X-API-Key": API_KEY},
|
headers={"X-API-Key": API_KEY},
|
||||||
files=files,
|
files=files,
|
||||||
data={
|
data={
|
||||||
"description": "Test image upload",
|
"description": "Test image upload"
|
||||||
"tags": "test,upload,image"
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -168,8 +163,7 @@ async def test_image_lifecycle(client, test_image_path, mock_auth, mock_storage_
|
|||||||
storage_path=test_storage_path,
|
storage_path=test_storage_path,
|
||||||
team_id=MOCK_TEAM_ID,
|
team_id=MOCK_TEAM_ID,
|
||||||
uploader_id=MOCK_USER_ID,
|
uploader_id=MOCK_USER_ID,
|
||||||
description="Test image upload",
|
description="Test image upload"
|
||||||
tags=["test", "upload", "image"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_create.return_value = mock_image
|
mock_create.return_value = mock_image
|
||||||
@ -182,7 +176,7 @@ async def test_image_lifecycle(client, test_image_path, mock_auth, mock_storage_
|
|||||||
patch('src.api.v1.images.router.delete') as mock_delete_api:
|
patch('src.api.v1.images.router.delete') as mock_delete_api:
|
||||||
|
|
||||||
# Mock the endpoints
|
# Mock the endpoints
|
||||||
async def mock_upload_handler(file, description=None, tags=None, current_user=None):
|
async def mock_upload_handler(file, description=None, current_user=None):
|
||||||
created_image = await image_repository.create(mock_image)
|
created_image = await image_repository.create(mock_image)
|
||||||
return {
|
return {
|
||||||
"id": str(created_image.id),
|
"id": str(created_image.id),
|
||||||
@ -191,8 +185,7 @@ async def test_image_lifecycle(client, test_image_path, mock_auth, mock_storage_
|
|||||||
"content_type": created_image.content_type,
|
"content_type": created_image.content_type,
|
||||||
"team_id": str(created_image.team_id),
|
"team_id": str(created_image.team_id),
|
||||||
"uploader_id": str(created_image.uploader_id),
|
"uploader_id": str(created_image.uploader_id),
|
||||||
"description": created_image.description,
|
"description": created_image.description
|
||||||
"tags": created_image.tags
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def mock_get_handler(image_id, current_user=None):
|
async def mock_get_handler(image_id, current_user=None):
|
||||||
@ -204,8 +197,7 @@ async def test_image_lifecycle(client, test_image_path, mock_auth, mock_storage_
|
|||||||
"content_type": image.content_type,
|
"content_type": image.content_type,
|
||||||
"team_id": str(image.team_id),
|
"team_id": str(image.team_id),
|
||||||
"uploader_id": str(image.uploader_id),
|
"uploader_id": str(image.uploader_id),
|
||||||
"description": image.description,
|
"description": image.description
|
||||||
"tags": image.tags
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def mock_delete_handler(image_id, current_user=None):
|
async def mock_delete_handler(image_id, current_user=None):
|
||||||
@ -222,7 +214,7 @@ async def test_image_lifecycle(client, test_image_path, mock_auth, mock_storage_
|
|||||||
"/api/v1/images",
|
"/api/v1/images",
|
||||||
headers={"X-API-Key": API_KEY},
|
headers={"X-API-Key": API_KEY},
|
||||||
files={"file": ("test_image.png", f, "image/png")},
|
files={"file": ("test_image.png", f, "image/png")},
|
||||||
data={"description": "Test image upload", "tags": "test,upload,image"}
|
data={"description": "Test image upload"}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify upload
|
# Verify upload
|
||||||
|
|||||||
@ -22,7 +22,6 @@ def test_image_model_properties():
|
|||||||
team_id=team_id,
|
team_id=team_id,
|
||||||
uploader_id=uploader_id,
|
uploader_id=uploader_id,
|
||||||
description="A test image",
|
description="A test image",
|
||||||
tags=["test", "api"],
|
|
||||||
metadata={"width": 800, "height": 600}
|
metadata={"width": 800, "height": 600}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,8 +34,6 @@ def test_image_model_properties():
|
|||||||
assert image.team_id == team_id
|
assert image.team_id == team_id
|
||||||
assert image.uploader_id == uploader_id
|
assert image.uploader_id == uploader_id
|
||||||
assert image.description == "A test image"
|
assert image.description == "A test image"
|
||||||
assert "test" in image.tags
|
|
||||||
assert "api" in image.tags
|
|
||||||
assert image.metadata["width"] == 800
|
assert image.metadata["width"] == 800
|
||||||
assert image.metadata["height"] == 600
|
assert image.metadata["height"] == 600
|
||||||
assert image.has_embedding is False
|
assert image.has_embedding is False
|
||||||
@ -113,8 +110,7 @@ async def test_upload_image(client: TestClient, user_api_key: tuple):
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
files=files,
|
files=files,
|
||||||
data={
|
data={
|
||||||
"description": "Test image upload",
|
"description": "Test image upload"
|
||||||
"tags": "test,upload,image"
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -127,10 +123,6 @@ async def test_upload_image(client: TestClient, user_api_key: tuple):
|
|||||||
assert "team_id" in data
|
assert "team_id" in data
|
||||||
assert "uploader_id" in data
|
assert "uploader_id" in data
|
||||||
assert data["description"] == "Test image upload"
|
assert data["description"] == "Test image upload"
|
||||||
assert len(data["tags"]) == 3
|
|
||||||
assert "test" in data["tags"]
|
|
||||||
assert "upload" in data["tags"]
|
|
||||||
assert "image" in data["tags"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -147,8 +139,7 @@ async def test_get_image(client: TestClient, user_api_key: tuple):
|
|||||||
storage_path="images/test-image-123.jpg",
|
storage_path="images/test-image-123.jpg",
|
||||||
team_id=api_key.team_id,
|
team_id=api_key.team_id,
|
||||||
uploader_id=api_key.user_id,
|
uploader_id=api_key.user_id,
|
||||||
description="A test image",
|
description="A test image"
|
||||||
tags=["test", "image"]
|
|
||||||
)
|
)
|
||||||
created_image = await image_repository.create(image)
|
created_image = await image_repository.create(image)
|
||||||
|
|
||||||
@ -169,8 +160,6 @@ async def test_get_image(client: TestClient, user_api_key: tuple):
|
|||||||
assert data["team_id"] == str(api_key.team_id)
|
assert data["team_id"] == str(api_key.team_id)
|
||||||
assert data["uploader_id"] == str(api_key.user_id)
|
assert data["uploader_id"] == str(api_key.user_id)
|
||||||
assert data["description"] == "A test image"
|
assert data["description"] == "A test image"
|
||||||
assert "test" in data["tags"]
|
|
||||||
assert "image" in data["tags"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -254,8 +243,7 @@ async def test_update_image_metadata(client: TestClient, user_api_key: tuple):
|
|||||||
storage_path="images/test-image-123.jpg",
|
storage_path="images/test-image-123.jpg",
|
||||||
team_id=api_key.team_id,
|
team_id=api_key.team_id,
|
||||||
uploader_id=api_key.user_id,
|
uploader_id=api_key.user_id,
|
||||||
description="Original description",
|
description="Original description"
|
||||||
tags=["original"]
|
|
||||||
)
|
)
|
||||||
created_image = await image_repository.create(image)
|
created_image = await image_repository.create(image)
|
||||||
|
|
||||||
@ -267,8 +255,7 @@ async def test_update_image_metadata(client: TestClient, user_api_key: tuple):
|
|||||||
f"/api/v1/images/{created_image.id}",
|
f"/api/v1/images/{created_image.id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={
|
json={
|
||||||
"description": "Updated description",
|
"description": "Updated description"
|
||||||
"tags": ["updated", "metadata"]
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -277,7 +264,4 @@ async def test_update_image_metadata(client: TestClient, user_api_key: tuple):
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["id"] == str(created_image.id)
|
assert data["id"] == str(created_image.id)
|
||||||
assert data["description"] == "Updated description"
|
assert data["description"] == "Updated description"
|
||||||
assert len(data["tags"]) == 2
|
|
||||||
assert "updated" in data["tags"]
|
|
||||||
assert "metadata" in data["tags"]
|
|
||||||
"""
|
"""
|
||||||
@ -4,6 +4,8 @@ from fastapi.testclient import TestClient
|
|||||||
from fastapi import status
|
from fastapi import status
|
||||||
import io
|
import io
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
from bson import ObjectId as PyObjectId
|
||||||
|
|
||||||
from src.api.v1.images import router
|
from src.api.v1.images import router
|
||||||
from src.models.user import UserModel
|
from src.models.user import UserModel
|
||||||
@ -15,34 +17,32 @@ class TestImageUploadWithPubSub:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_current_user(self):
|
def mock_current_user(self):
|
||||||
"""Mock current user"""
|
"""Mock current user for authentication"""
|
||||||
user = UserModel(
|
user = Mock()
|
||||||
id=PyObjectId(),
|
user.id = PyObjectId()
|
||||||
email="test@example.com",
|
user.team_id = PyObjectId()
|
||||||
team_id=PyObjectId(),
|
user.email = "test@example.com"
|
||||||
is_active=True
|
user.name = "Test User"
|
||||||
)
|
user.is_admin = True
|
||||||
|
user.is_active = True
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_image_file(self):
|
def test_image_file(self):
|
||||||
"""Create a test image file"""
|
"""Create a test image file"""
|
||||||
# Create a simple test image
|
# Create a simple test image file
|
||||||
img = Image.new('RGB', (100, 100), color='red')
|
image_data = b"fake image data for testing"
|
||||||
img_bytes = io.BytesIO()
|
return BytesIO(image_data)
|
||||||
img.save(img_bytes, format='JPEG')
|
|
||||||
img_bytes.seek(0)
|
|
||||||
return img_bytes
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_storage_service(self):
|
def mock_storage_service(self):
|
||||||
"""Mock storage service"""
|
"""Mock storage service"""
|
||||||
with patch('src.api.v1.images.storage_service') as mock_service:
|
with patch('src.api.v1.images.storage_service') as mock_service:
|
||||||
mock_service.upload_file = AsyncMock(return_value=(
|
mock_service.upload_file = AsyncMock(return_value=(
|
||||||
"bucket/team-123/image.jpg",
|
"bucket/team/test-image.jpg", # storage_path
|
||||||
"image/jpeg",
|
"image/jpeg", # content_type
|
||||||
1024,
|
1024, # file_size
|
||||||
{"width": 100, "height": 100}
|
{"width": 800, "height": 600} # metadata
|
||||||
))
|
))
|
||||||
yield mock_service
|
yield mock_service
|
||||||
|
|
||||||
@ -50,22 +50,22 @@ class TestImageUploadWithPubSub:
|
|||||||
def mock_image_repository(self):
|
def mock_image_repository(self):
|
||||||
"""Mock image repository"""
|
"""Mock image repository"""
|
||||||
with patch('src.api.v1.images.image_repository') as mock_repo:
|
with patch('src.api.v1.images.image_repository') as mock_repo:
|
||||||
|
# Create a mock image object
|
||||||
mock_image = Mock()
|
mock_image = Mock()
|
||||||
mock_image.id = PyObjectId()
|
mock_image.id = PyObjectId()
|
||||||
mock_image.filename = "test.jpg"
|
mock_image.filename = "test-image-123.jpg"
|
||||||
mock_image.original_filename = "test.jpg"
|
mock_image.original_filename = "test.jpg"
|
||||||
mock_image.file_size = 1024
|
mock_image.file_size = 1024
|
||||||
mock_image.content_type = "image/jpeg"
|
mock_image.content_type = "image/jpeg"
|
||||||
mock_image.storage_path = "bucket/team-123/image.jpg"
|
mock_image.storage_path = "bucket/team/test-image.jpg"
|
||||||
mock_image.team_id = PyObjectId()
|
mock_image.team_id = PyObjectId()
|
||||||
mock_image.uploader_id = PyObjectId()
|
mock_image.uploader_id = PyObjectId()
|
||||||
mock_image.upload_date = "2023-01-01T00:00:00"
|
|
||||||
mock_image.description = None
|
mock_image.description = None
|
||||||
mock_image.tags = []
|
|
||||||
mock_image.metadata = {}
|
mock_image.metadata = {}
|
||||||
|
mock_image.upload_date = Mock()
|
||||||
mock_image.has_embedding = False
|
mock_image.has_embedding = False
|
||||||
mock_image.collection_id = None
|
|
||||||
|
|
||||||
|
# Configure the create method to return the mock image
|
||||||
mock_repo.create = AsyncMock(return_value=mock_image)
|
mock_repo.create = AsyncMock(return_value=mock_image)
|
||||||
yield mock_repo
|
yield mock_repo
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ class TestImageUploadWithPubSub:
|
|||||||
mock_image_repository,
|
mock_image_repository,
|
||||||
mock_pubsub_service
|
mock_pubsub_service
|
||||||
):
|
):
|
||||||
"""Test that image upload publishes a task to Pub/Sub"""
|
"""Test that image upload publishes a message to Pub/Sub"""
|
||||||
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
||||||
from src.api.v1.images import upload_image
|
from src.api.v1.images import upload_image
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
@ -109,17 +109,27 @@ class TestImageUploadWithPubSub:
|
|||||||
current_user=mock_current_user
|
current_user=mock_current_user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Verify storage service was called
|
||||||
|
mock_storage_service.upload_file.assert_called_once()
|
||||||
|
|
||||||
|
# Verify image was created in repository
|
||||||
|
mock_image_repository.create.assert_called_once()
|
||||||
|
|
||||||
# Verify Pub/Sub task was published
|
# Verify Pub/Sub task was published
|
||||||
mock_pubsub_service.publish_image_processing_task.assert_called_once()
|
mock_pubsub_service.publish_image_processing_task.assert_called_once()
|
||||||
|
|
||||||
# Check the call arguments
|
# Get the call arguments for Pub/Sub
|
||||||
call_args = mock_pubsub_service.publish_image_processing_task.call_args
|
pubsub_call_args = mock_pubsub_service.publish_image_processing_task.call_args
|
||||||
assert call_args[1]['image_id'] == str(mock_image_repository.create.return_value.id)
|
task_data = pubsub_call_args[0][0] # First positional argument
|
||||||
assert call_args[1]['storage_path'] == "bucket/team-123/image.jpg"
|
|
||||||
assert call_args[1]['team_id'] == str(mock_current_user.team_id)
|
# Verify task data contains expected fields
|
||||||
|
assert "image_id" in task_data
|
||||||
|
assert "team_id" in task_data
|
||||||
|
assert "storage_path" in task_data
|
||||||
|
assert "content_type" in task_data
|
||||||
|
|
||||||
# Verify response
|
# Verify response
|
||||||
assert response.filename == "test.jpg"
|
assert response.filename == "test-image-123.jpg"
|
||||||
assert response.content_type == "image/jpeg"
|
assert response.content_type == "image/jpeg"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -131,14 +141,14 @@ class TestImageUploadWithPubSub:
|
|||||||
mock_image_repository,
|
mock_image_repository,
|
||||||
mock_pubsub_service
|
mock_pubsub_service
|
||||||
):
|
):
|
||||||
"""Test that upload continues even if Pub/Sub publishing fails"""
|
"""Test that image upload continues even if Pub/Sub fails"""
|
||||||
# Mock Pub/Sub failure
|
|
||||||
mock_pubsub_service.publish_image_processing_task = AsyncMock(return_value=False)
|
|
||||||
|
|
||||||
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
||||||
from src.api.v1.images import upload_image
|
from src.api.v1.images import upload_image
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
|
||||||
|
# Configure Pub/Sub to fail
|
||||||
|
mock_pubsub_service.publish_image_processing_task = AsyncMock(return_value=False)
|
||||||
|
|
||||||
# Create upload file
|
# Create upload file
|
||||||
upload_file = UploadFile(
|
upload_file = UploadFile(
|
||||||
filename="test.jpg",
|
filename="test.jpg",
|
||||||
@ -151,18 +161,22 @@ class TestImageUploadWithPubSub:
|
|||||||
request.url.path = "/api/v1/images"
|
request.url.path = "/api/v1/images"
|
||||||
request.method = "POST"
|
request.method = "POST"
|
||||||
|
|
||||||
# Call the upload function - should not raise exception
|
# Call the upload function
|
||||||
response = await upload_image(
|
response = await upload_image(
|
||||||
request=request,
|
request=request,
|
||||||
file=upload_file,
|
file=upload_file,
|
||||||
current_user=mock_current_user
|
current_user=mock_current_user
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify Pub/Sub task was attempted
|
# Verify storage and repository were still called
|
||||||
|
mock_storage_service.upload_file.assert_called_once()
|
||||||
|
mock_image_repository.create.assert_called_once()
|
||||||
|
|
||||||
|
# Verify Pub/Sub was attempted
|
||||||
mock_pubsub_service.publish_image_processing_task.assert_called_once()
|
mock_pubsub_service.publish_image_processing_task.assert_called_once()
|
||||||
|
|
||||||
# Verify response is still successful
|
# Verify response is still successful
|
||||||
assert response.filename == "test.jpg"
|
assert response.filename == "test-image-123.jpg"
|
||||||
assert response.content_type == "image/jpeg"
|
assert response.content_type == "image/jpeg"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -174,16 +188,16 @@ class TestImageUploadWithPubSub:
|
|||||||
mock_image_repository,
|
mock_image_repository,
|
||||||
mock_pubsub_service
|
mock_pubsub_service
|
||||||
):
|
):
|
||||||
"""Test that upload continues even if Pub/Sub publishing raises exception"""
|
"""Test that image upload continues even if Pub/Sub raises an exception"""
|
||||||
# Mock Pub/Sub exception
|
|
||||||
mock_pubsub_service.publish_image_processing_task = AsyncMock(
|
|
||||||
side_effect=Exception("Pub/Sub error")
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
||||||
from src.api.v1.images import upload_image
|
from src.api.v1.images import upload_image
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
|
||||||
|
# Configure Pub/Sub to raise an exception
|
||||||
|
mock_pubsub_service.publish_image_processing_task = AsyncMock(
|
||||||
|
side_effect=Exception("Pub/Sub service unavailable")
|
||||||
|
)
|
||||||
|
|
||||||
# Create upload file
|
# Create upload file
|
||||||
upload_file = UploadFile(
|
upload_file = UploadFile(
|
||||||
filename="test.jpg",
|
filename="test.jpg",
|
||||||
@ -196,14 +210,18 @@ class TestImageUploadWithPubSub:
|
|||||||
request.url.path = "/api/v1/images"
|
request.url.path = "/api/v1/images"
|
||||||
request.method = "POST"
|
request.method = "POST"
|
||||||
|
|
||||||
# Call the upload function - should not raise exception
|
# Call the upload function
|
||||||
response = await upload_image(
|
response = await upload_image(
|
||||||
request=request,
|
request=request,
|
||||||
file=upload_file,
|
file=upload_file,
|
||||||
current_user=mock_current_user
|
current_user=mock_current_user
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify Pub/Sub task was attempted
|
# Verify storage and repository were still called
|
||||||
|
mock_storage_service.upload_file.assert_called_once()
|
||||||
|
mock_image_repository.create.assert_called_once()
|
||||||
|
|
||||||
|
# Verify Pub/Sub was attempted
|
||||||
mock_pubsub_service.publish_image_processing_task.assert_called_once()
|
mock_pubsub_service.publish_image_processing_task.assert_called_once()
|
||||||
|
|
||||||
# Verify response is still successful
|
# Verify response is still successful
|
||||||
@ -211,7 +229,7 @@ class TestImageUploadWithPubSub:
|
|||||||
assert response.content_type == "image/jpeg"
|
assert response.content_type == "image/jpeg"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_upload_image_with_description_and_tags(
|
async def test_upload_image_with_description(
|
||||||
self,
|
self,
|
||||||
mock_current_user,
|
mock_current_user,
|
||||||
test_image_file,
|
test_image_file,
|
||||||
@ -219,7 +237,7 @@ class TestImageUploadWithPubSub:
|
|||||||
mock_image_repository,
|
mock_image_repository,
|
||||||
mock_pubsub_service
|
mock_pubsub_service
|
||||||
):
|
):
|
||||||
"""Test image upload with description and tags"""
|
"""Test image upload with description"""
|
||||||
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
with patch('src.api.v1.images.get_current_user', return_value=mock_current_user):
|
||||||
from src.api.v1.images import upload_image
|
from src.api.v1.images import upload_image
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
@ -236,12 +254,11 @@ class TestImageUploadWithPubSub:
|
|||||||
request.url.path = "/api/v1/images"
|
request.url.path = "/api/v1/images"
|
||||||
request.method = "POST"
|
request.method = "POST"
|
||||||
|
|
||||||
# Call the upload function with description and tags
|
# Call the upload function with description
|
||||||
response = await upload_image(
|
response = await upload_image(
|
||||||
request=request,
|
request=request,
|
||||||
file=upload_file,
|
file=upload_file,
|
||||||
description="Test image",
|
description="Test image",
|
||||||
tags="nature, landscape, outdoor",
|
|
||||||
current_user=mock_current_user
|
current_user=mock_current_user
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -253,7 +270,6 @@ class TestImageUploadWithPubSub:
|
|||||||
created_image_data = mock_image_repository.create.call_args[0][0]
|
created_image_data = mock_image_repository.create.call_args[0][0]
|
||||||
|
|
||||||
assert created_image_data.description == "Test image"
|
assert created_image_data.description == "Test image"
|
||||||
assert created_image_data.tags == ["nature", "landscape", "outdoor"]
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_upload_image_with_collection_id(
|
async def test_upload_image_with_collection_id(
|
||||||
|
|||||||
@ -7,51 +7,6 @@ from src.models.image import ImageModel
|
|||||||
from src.db.repositories.image_repository import image_repository # Assuming this exists
|
from src.db.repositories.image_repository import image_repository # Assuming this exists
|
||||||
|
|
||||||
|
|
||||||
def test_image_search_tags():
|
|
||||||
"""Test the search functionality based on tags (simulated)"""
|
|
||||||
team_id = ObjectId()
|
|
||||||
uploader_id = ObjectId()
|
|
||||||
|
|
||||||
# Create test images with different tags
|
|
||||||
image1 = ImageModel(
|
|
||||||
filename="vacation1.jpg",
|
|
||||||
original_filename="vacation1.jpg",
|
|
||||||
file_size=1024,
|
|
||||||
content_type="image/jpeg",
|
|
||||||
storage_path="images/vacation1.jpg",
|
|
||||||
team_id=team_id,
|
|
||||||
uploader_id=uploader_id,
|
|
||||||
tags=["vacation", "beach", "summer"]
|
|
||||||
)
|
|
||||||
|
|
||||||
image2 = ImageModel(
|
|
||||||
filename="vacation2.jpg",
|
|
||||||
original_filename="vacation2.jpg",
|
|
||||||
file_size=1024,
|
|
||||||
content_type="image/jpeg",
|
|
||||||
storage_path="images/vacation2.jpg",
|
|
||||||
team_id=team_id,
|
|
||||||
uploader_id=uploader_id,
|
|
||||||
tags=["vacation", "mountain", "winter"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Simulate tag search for "beach"
|
|
||||||
search_results_beach = [img for img in [image1, image2] if "beach" in img.tags]
|
|
||||||
|
|
||||||
# Check results
|
|
||||||
assert len(search_results_beach) == 1
|
|
||||||
assert search_results_beach[0].filename == "vacation1.jpg"
|
|
||||||
|
|
||||||
# Simulate tag search for "vacation"
|
|
||||||
search_results_vacation = [img for img in [image1, image2] if "vacation" in img.tags]
|
|
||||||
|
|
||||||
# Check results
|
|
||||||
assert len(search_results_vacation) == 2
|
|
||||||
filenames = [img.filename for img in search_results_vacation]
|
|
||||||
assert "vacation1.jpg" in filenames
|
|
||||||
assert "vacation2.jpg" in filenames
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_embeddings_structure():
|
def test_image_embeddings_structure():
|
||||||
"""Test the structure of image embeddings for semantic search"""
|
"""Test the structure of image embeddings for semantic search"""
|
||||||
team_id = ObjectId()
|
team_id = ObjectId()
|
||||||
@ -125,7 +80,6 @@ async def test_semantic_search(client: TestClient, user_api_key: tuple):
|
|||||||
team_id=api_key.team_id,
|
team_id=api_key.team_id,
|
||||||
uploader_id=api_key.user_id,
|
uploader_id=api_key.user_id,
|
||||||
description="A cat photo",
|
description="A cat photo",
|
||||||
tags=["cat", "animal", "pet"],
|
|
||||||
has_embedding=True,
|
has_embedding=True,
|
||||||
embedding_id="embedding1",
|
embedding_id="embedding1",
|
||||||
embedding_model="clip"
|
embedding_model="clip"
|
||||||
@ -141,7 +95,6 @@ async def test_semantic_search(client: TestClient, user_api_key: tuple):
|
|||||||
team_id=api_key.team_id,
|
team_id=api_key.team_id,
|
||||||
uploader_id=api_key.user_id,
|
uploader_id=api_key.user_id,
|
||||||
description="A dog photo",
|
description="A dog photo",
|
||||||
tags=["dog", "animal", "pet"],
|
|
||||||
has_embedding=True,
|
has_embedding=True,
|
||||||
embedding_id="embedding2",
|
embedding_id="embedding2",
|
||||||
embedding_model="clip"
|
embedding_model="clip"
|
||||||
@ -191,7 +144,7 @@ async def test_search_pagination(client: TestClient, user_api_key: tuple):
|
|||||||
storage_path=f"images/image{i}.jpg",
|
storage_path=f"images/image{i}.jpg",
|
||||||
team_id=api_key.team_id,
|
team_id=api_key.team_id,
|
||||||
uploader_id=api_key.user_id,
|
uploader_id=api_key.user_id,
|
||||||
tags=["test", f"tag{i}"]
|
description=f"Test image {i}"
|
||||||
)
|
)
|
||||||
await image_repository.create(image)
|
await image_repository.create(image)
|
||||||
|
|
||||||
@ -222,62 +175,4 @@ async def test_search_pagination(client: TestClient, user_api_key: tuple):
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert len(data["results"]) == 10
|
assert len(data["results"]) == 10
|
||||||
assert data["pagination"]["page"] == 2
|
assert data["pagination"]["page"] == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_search_by_tags(client: TestClient, user_api_key: tuple):
|
|
||||||
# Test searching by tags
|
|
||||||
raw_key, api_key = user_api_key
|
|
||||||
|
|
||||||
# Set up headers
|
|
||||||
headers = {"X-API-Key": raw_key}
|
|
||||||
|
|
||||||
# Create test images with different tags
|
|
||||||
image1 = ImageModel(
|
|
||||||
filename="vacation1.jpg",
|
|
||||||
original_filename="vacation1.jpg",
|
|
||||||
file_size=1024,
|
|
||||||
content_type="image/jpeg",
|
|
||||||
storage_path="images/vacation1.jpg",
|
|
||||||
team_id=api_key.team_id,
|
|
||||||
uploader_id=api_key.user_id,
|
|
||||||
tags=["vacation", "beach", "summer"]
|
|
||||||
)
|
|
||||||
await image_repository.create(image1)
|
|
||||||
|
|
||||||
image2 = ImageModel(
|
|
||||||
filename="vacation2.jpg",
|
|
||||||
original_filename="vacation2.jpg",
|
|
||||||
file_size=1024,
|
|
||||||
content_type="image/jpeg",
|
|
||||||
storage_path="images/vacation2.jpg",
|
|
||||||
team_id=api_key.team_id,
|
|
||||||
uploader_id=api_key.user_id,
|
|
||||||
tags=["vacation", "mountain", "winter"]
|
|
||||||
)
|
|
||||||
await image_repository.create(image2)
|
|
||||||
|
|
||||||
# Test search by tag
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/search?tags=beach",
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check response
|
|
||||||
assert response.status_code == 200
|
|
||||||
data = response.json()
|
|
||||||
assert len(data["results"]) == 1
|
|
||||||
assert data["results"][0]["filename"] == "vacation1.jpg"
|
|
||||||
|
|
||||||
# Test search by multiple tags
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/search?tags=vacation,winter",
|
|
||||||
headers=headers
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check response
|
|
||||||
assert response.status_code == 200
|
|
||||||
data = response.json()
|
|
||||||
assert len(data["results"]) == 1
|
|
||||||
assert data["results"][0]["filename"] == "vacation2.jpg"
|
|
||||||
"""
|
"""
|
||||||
@ -118,7 +118,6 @@ def test_image_data() -> Dict[str, Any]:
|
|||||||
"""Provide test image metadata"""
|
"""Provide test image metadata"""
|
||||||
return {
|
return {
|
||||||
"description": "Test image for automated testing",
|
"description": "Test image for automated testing",
|
||||||
"tags": "test,automation,sample"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,31 +1,35 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch, AsyncMock
|
from unittest.mock import Mock, patch, AsyncMock
|
||||||
from src.db.repositories.firestore_repository import FirestoreRepository
|
from pydantic import BaseModel
|
||||||
from src.db.repositories.firestore_team_repository import FirestoreTeamRepository
|
|
||||||
from src.db.repositories.firestore_user_repository import FirestoreUserRepository
|
from src.db.repositories.firestore_repositories import (
|
||||||
from src.db.repositories.firestore_api_key_repository import FirestoreApiKeyRepository
|
FirestoreRepository,
|
||||||
from src.db.repositories.firestore_image_repository import FirestoreImageRepository
|
FirestoreTeamRepository,
|
||||||
|
FirestoreUserRepository,
|
||||||
|
FirestoreApiKeyRepository,
|
||||||
|
FirestoreImageRepository
|
||||||
|
)
|
||||||
from src.models.team import TeamModel
|
from src.models.team import TeamModel
|
||||||
from src.models.user import UserModel
|
from src.models.user import UserModel
|
||||||
from src.models.api_key import ApiKeyModel
|
from src.models.api_key import ApiKeyModel
|
||||||
from src.models.image import ImageModel
|
from src.models.image import ImageModel
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class TestFirestoreRepository:
|
class TestFirestoreRepository:
|
||||||
"""Test cases for the base FirestoreRepository"""
|
"""Test cases for the base FirestoreRepository class"""
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_firestore_db(self):
|
def mock_firestore_db(self):
|
||||||
"""Mock firestore_db for testing"""
|
"""Mock Firestore database"""
|
||||||
with patch('src.db.repositories.firestore_repository.firestore_db') as mock_db:
|
mock_db = Mock()
|
||||||
# Make the async methods return coroutines
|
mock_collection = Mock()
|
||||||
mock_db.add_document = AsyncMock()
|
mock_doc = Mock()
|
||||||
mock_db.get_document = AsyncMock()
|
|
||||||
mock_db.list_documents = AsyncMock()
|
mock_db.collection.return_value = mock_collection
|
||||||
mock_db.update_document = AsyncMock()
|
mock_collection.document.return_value = mock_doc
|
||||||
mock_db.delete_document = AsyncMock()
|
mock_collection.stream.return_value = []
|
||||||
yield mock_db
|
|
||||||
|
return mock_db
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_model_class(self):
|
def test_model_class(self):
|
||||||
@ -48,151 +52,142 @@ class TestFirestoreRepository:
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create(self, repository, test_model_class, mock_firestore_db):
|
async def test_create(self, repository, test_model_class, mock_firestore_db):
|
||||||
"""Test creating a new document"""
|
"""Test creating a document"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
test_model = test_model_class(name="Test", value=123)
|
# Mock the document reference and set operation
|
||||||
mock_firestore_db.add_document.return_value = "generated_id"
|
mock_doc_ref = Mock()
|
||||||
mock_firestore_db.get_document.return_value = {
|
mock_doc_ref.id = "test_id"
|
||||||
"name": "Test",
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
"value": 123,
|
mock_collection.add.return_value = (None, mock_doc_ref)
|
||||||
"_id": "generated_id"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Execute
|
# Create test model instance
|
||||||
result = await repository.create(test_model)
|
test_instance = test_model_class(name="test", value=42)
|
||||||
|
|
||||||
# Assert
|
# Call create method
|
||||||
assert isinstance(result, test_model_class)
|
result = await repository.create(test_instance)
|
||||||
assert result.name == "Test"
|
|
||||||
assert result.value == 123
|
# Verify the result
|
||||||
mock_firestore_db.add_document.assert_called_once_with(
|
assert result.name == "test"
|
||||||
"test_collection",
|
assert result.value == 42
|
||||||
{"name": "Test", "value": 123}
|
|
||||||
)
|
# Verify Firestore calls
|
||||||
mock_firestore_db.get_document.assert_called_once_with(
|
mock_firestore_db.collection.assert_called_once_with("test_collection")
|
||||||
"test_collection",
|
mock_collection.add.assert_called_once()
|
||||||
"generated_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_by_id_found(self, repository, test_model_class, mock_firestore_db):
|
async def test_get_by_id_found(self, repository, test_model_class, mock_firestore_db):
|
||||||
"""Test getting a document by ID when it exists"""
|
"""Test getting a document by ID when it exists"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
mock_firestore_db.get_document.return_value = {
|
# Mock document snapshot
|
||||||
"name": "Test",
|
mock_doc_snapshot = Mock()
|
||||||
"value": 123,
|
mock_doc_snapshot.exists = True
|
||||||
"_id": "test_id"
|
mock_doc_snapshot.to_dict.return_value = {"name": "test", "value": 42}
|
||||||
}
|
mock_doc_snapshot.id = "test_id"
|
||||||
|
|
||||||
# Execute
|
mock_doc_ref = Mock()
|
||||||
result = await repository.get_by_id("test_id")
|
mock_doc_ref.get.return_value = mock_doc_snapshot
|
||||||
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
|
mock_collection.document.return_value = mock_doc_ref
|
||||||
|
|
||||||
# Assert
|
result = await repository.get_by_id("test_id")
|
||||||
assert isinstance(result, test_model_class)
|
|
||||||
assert result.name == "Test"
|
assert result.name == "test"
|
||||||
assert result.value == 123
|
assert result.value == 42
|
||||||
mock_firestore_db.get_document.assert_called_once_with("test_collection", "test_id")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_by_id_not_found(self, repository, mock_firestore_db):
|
async def test_get_by_id_not_found(self, repository, mock_firestore_db):
|
||||||
"""Test getting a document by ID when it doesn't exist"""
|
"""Test getting a document by ID when it doesn't exist"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
mock_firestore_db.get_document.return_value = None
|
# Mock document snapshot that doesn't exist
|
||||||
|
mock_doc_snapshot = Mock()
|
||||||
|
mock_doc_snapshot.exists = False
|
||||||
|
|
||||||
# Execute
|
mock_doc_ref = Mock()
|
||||||
result = await repository.get_by_id("nonexistent_id")
|
mock_doc_ref.get.return_value = mock_doc_snapshot
|
||||||
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
|
mock_collection.document.return_value = mock_doc_ref
|
||||||
|
|
||||||
# Assert
|
result = await repository.get_by_id("nonexistent_id")
|
||||||
assert result is None
|
|
||||||
mock_firestore_db.get_document.assert_called_once_with("test_collection", "nonexistent_id")
|
assert result is None
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_all(self, repository, test_model_class, mock_firestore_db):
|
async def test_get_all(self, repository, test_model_class, mock_firestore_db):
|
||||||
"""Test getting all documents"""
|
"""Test getting all documents"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
mock_firestore_db.list_documents.return_value = [
|
# Mock document snapshots
|
||||||
{"name": "Test1", "value": 123, "_id": "id1"},
|
mock_docs = [
|
||||||
{"name": "Test2", "value": 456, "_id": "id2"}
|
Mock(to_dict=lambda: {"name": "test1", "value": 1}, id="id1"),
|
||||||
]
|
Mock(to_dict=lambda: {"name": "test2", "value": 2}, id="id2")
|
||||||
|
]
|
||||||
|
|
||||||
# Execute
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
result = await repository.get_all()
|
mock_collection.stream.return_value = mock_docs
|
||||||
|
|
||||||
# Assert
|
result = await repository.get_all()
|
||||||
assert len(result) == 2
|
|
||||||
assert all(isinstance(item, test_model_class) for item in result)
|
assert len(result) == 2
|
||||||
assert result[0].name == "Test1"
|
assert result[0].name == "test1"
|
||||||
assert result[1].name == "Test2"
|
assert result[1].name == "test2"
|
||||||
mock_firestore_db.list_documents.assert_called_once_with("test_collection")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_success(self, repository, test_model_class, mock_firestore_db):
|
async def test_update_success(self, repository, test_model_class, mock_firestore_db):
|
||||||
"""Test updating a document successfully"""
|
"""Test updating a document successfully"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
update_data = {"name": "Updated", "value": 999}
|
# Mock successful update
|
||||||
mock_firestore_db.update_document.return_value = True
|
mock_doc_ref = Mock()
|
||||||
mock_firestore_db.get_document.return_value = {
|
mock_doc_ref.update.return_value = None # Firestore update returns None on success
|
||||||
"name": "Updated",
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
"value": 999,
|
mock_collection.document.return_value = mock_doc_ref
|
||||||
"_id": "test_id"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Execute
|
# Mock get_by_id to return updated document
|
||||||
result = await repository.update("test_id", update_data)
|
updated_instance = test_model_class(name="updated", value=99)
|
||||||
|
with patch.object(repository, 'get_by_id', return_value=updated_instance):
|
||||||
|
result = await repository.update("test_id", {"name": "updated", "value": 99})
|
||||||
|
|
||||||
# Assert
|
assert result.name == "updated"
|
||||||
assert isinstance(result, test_model_class)
|
assert result.value == 99
|
||||||
assert result.name == "Updated"
|
mock_doc_ref.update.assert_called_once_with({"name": "updated", "value": 99})
|
||||||
assert result.value == 999
|
|
||||||
mock_firestore_db.update_document.assert_called_once_with(
|
|
||||||
"test_collection",
|
|
||||||
"test_id",
|
|
||||||
update_data
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_failure(self, repository, mock_firestore_db):
|
async def test_update_failure(self, repository, mock_firestore_db):
|
||||||
"""Test updating a document that doesn't exist"""
|
"""Test updating a document that doesn't exist"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
update_data = {"name": "Updated"}
|
# Mock failed update (document doesn't exist)
|
||||||
mock_firestore_db.update_document.return_value = False
|
mock_doc_ref = Mock()
|
||||||
|
mock_doc_ref.update.side_effect = Exception("Document not found")
|
||||||
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
|
mock_collection.document.return_value = mock_doc_ref
|
||||||
|
|
||||||
# Execute
|
with pytest.raises(Exception):
|
||||||
result = await repository.update("nonexistent_id", update_data)
|
await repository.update("nonexistent_id", {"name": "updated"})
|
||||||
|
|
||||||
# Assert
|
|
||||||
assert result is None
|
|
||||||
mock_firestore_db.update_document.assert_called_once_with(
|
|
||||||
"test_collection",
|
|
||||||
"nonexistent_id",
|
|
||||||
update_data
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_delete_success(self, repository, mock_firestore_db):
|
async def test_delete_success(self, repository, mock_firestore_db):
|
||||||
"""Test deleting a document successfully"""
|
"""Test deleting a document successfully"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
mock_firestore_db.delete_document.return_value = True
|
mock_doc_ref = Mock()
|
||||||
|
mock_doc_ref.delete.return_value = None # Firestore delete returns None on success
|
||||||
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
|
mock_collection.document.return_value = mock_doc_ref
|
||||||
|
|
||||||
# Execute
|
result = await repository.delete("test_id")
|
||||||
result = await repository.delete("test_id")
|
|
||||||
|
|
||||||
# Assert
|
assert result is True
|
||||||
assert result is True
|
mock_doc_ref.delete.assert_called_once()
|
||||||
mock_firestore_db.delete_document.assert_called_once_with("test_collection", "test_id")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_delete_failure(self, repository, mock_firestore_db):
|
async def test_delete_failure(self, repository, mock_firestore_db):
|
||||||
"""Test deleting a document that doesn't exist"""
|
"""Test deleting a document that doesn't exist"""
|
||||||
# Setup
|
with patch('src.db.repositories.firestore_repositories.get_firestore_db', return_value=mock_firestore_db):
|
||||||
mock_firestore_db.delete_document.return_value = False
|
mock_doc_ref = Mock()
|
||||||
|
mock_doc_ref.delete.side_effect = Exception("Document not found")
|
||||||
|
mock_collection = mock_firestore_db.collection.return_value
|
||||||
|
mock_collection.document.return_value = mock_doc_ref
|
||||||
|
|
||||||
# Execute
|
result = await repository.delete("nonexistent_id")
|
||||||
result = await repository.delete("nonexistent_id")
|
|
||||||
|
|
||||||
# Assert
|
assert result is False
|
||||||
assert result is False
|
|
||||||
mock_firestore_db.delete_document.assert_called_once_with("test_collection", "nonexistent_id")
|
|
||||||
|
|
||||||
|
|
||||||
class TestFirestoreTeamRepository:
|
class TestFirestoreTeamRepository:
|
||||||
@ -211,21 +206,24 @@ class TestFirestoreTeamRepository:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_by_id(self, repository):
|
async def test_get_by_id(self, repository):
|
||||||
"""Test getting team by ID"""
|
"""Test getting team by ID"""
|
||||||
with patch.object(repository.__class__.__bases__[0], 'get_by_id') as mock_get_by_id:
|
with patch.object(repository.__class__.__bases__[0], 'get_by_id') as mock_get:
|
||||||
mock_get_by_id.return_value = Mock()
|
mock_get.return_value = Mock(id="team_id", name="Test Team")
|
||||||
|
|
||||||
await repository.get_by_id("team_id")
|
result = await repository.get_by_id("team_id")
|
||||||
|
|
||||||
mock_get_by_id.assert_called_once_with("team_id")
|
assert result.id == "team_id"
|
||||||
|
assert result.name == "Test Team"
|
||||||
|
mock_get.assert_called_once_with("team_id")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update(self, repository):
|
async def test_update(self, repository):
|
||||||
"""Test updating team"""
|
"""Test updating team"""
|
||||||
with patch.object(repository.__class__.__bases__[0], 'update') as mock_update:
|
with patch.object(repository.__class__.__bases__[0], 'update') as mock_update:
|
||||||
mock_update.return_value = Mock()
|
mock_update.return_value = Mock(name="Updated Team")
|
||||||
|
|
||||||
await repository.update("team_id", {"name": "Updated Team"})
|
result = await repository.update("team_id", {"name": "Updated Team"})
|
||||||
|
|
||||||
|
assert result.name == "Updated Team"
|
||||||
mock_update.assert_called_once_with("team_id", {"name": "Updated Team"})
|
mock_update.assert_called_once_with("team_id", {"name": "Updated Team"})
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -406,22 +404,3 @@ class TestFirestoreImageRepository:
|
|||||||
assert result[0] == mock_images[0]
|
assert result[0] == mock_images[0]
|
||||||
assert result[1] == mock_images[2]
|
assert result[1] == mock_images[2]
|
||||||
mock_get_all.assert_called_once()
|
mock_get_all.assert_called_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_by_tag(self, repository):
|
|
||||||
"""Test getting images by tag"""
|
|
||||||
mock_images = [
|
|
||||||
Mock(tags=["tag1", "tag2"]),
|
|
||||||
Mock(tags=["tag3"]),
|
|
||||||
Mock(tags=["tag1", "tag4"])
|
|
||||||
]
|
|
||||||
|
|
||||||
with patch.object(repository, 'get_all') as mock_get_all:
|
|
||||||
mock_get_all.return_value = mock_images
|
|
||||||
|
|
||||||
result = await repository.get_by_tag("tag1")
|
|
||||||
|
|
||||||
assert len(result) == 2
|
|
||||||
assert result[0] == mock_images[0]
|
|
||||||
assert result[1] == mock_images[2]
|
|
||||||
mock_get_all.assert_called_once()
|
|
||||||
@ -31,7 +31,6 @@ class TestImageModel:
|
|||||||
assert isinstance(image.upload_date, datetime)
|
assert isinstance(image.upload_date, datetime)
|
||||||
assert image.last_accessed is None
|
assert image.last_accessed is None
|
||||||
assert image.description is None
|
assert image.description is None
|
||||||
assert image.tags == []
|
|
||||||
assert image.metadata == {}
|
assert image.metadata == {}
|
||||||
assert image.embedding_id is None
|
assert image.embedding_id is None
|
||||||
assert image.embedding_model is None
|
assert image.embedding_model is None
|
||||||
@ -55,7 +54,6 @@ class TestImageModel:
|
|||||||
public_url=public_url,
|
public_url=public_url,
|
||||||
last_accessed=last_accessed,
|
last_accessed=last_accessed,
|
||||||
description="A test image",
|
description="A test image",
|
||||||
tags=["test", "image"],
|
|
||||||
metadata={"width": 800, "height": 600},
|
metadata={"width": 800, "height": 600},
|
||||||
embedding_id="embedding123",
|
embedding_id="embedding123",
|
||||||
embedding_model="clip",
|
embedding_model="clip",
|
||||||
@ -65,8 +63,6 @@ class TestImageModel:
|
|||||||
assert str(image.public_url) == public_url
|
assert str(image.public_url) == public_url
|
||||||
assert image.last_accessed == last_accessed
|
assert image.last_accessed == last_accessed
|
||||||
assert image.description == "A test image"
|
assert image.description == "A test image"
|
||||||
assert "test" in image.tags
|
|
||||||
assert "image" in image.tags
|
|
||||||
assert image.metadata["width"] == 800
|
assert image.metadata["width"] == 800
|
||||||
assert image.metadata["height"] == 600
|
assert image.metadata["height"] == 600
|
||||||
assert image.embedding_id == "embedding123"
|
assert image.embedding_id == "embedding123"
|
||||||
|
|||||||
@ -42,191 +42,124 @@ from main import app
|
|||||||
|
|
||||||
@pytest.mark.e2e
|
@pytest.mark.e2e
|
||||||
class TestE2EWorkflows:
|
class TestE2EWorkflows:
|
||||||
"""End-to-end tests covering complete user workflows with artificial test data"""
|
"""End-to-end tests that simulate real user workflows with artificial data"""
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def client(self):
|
def client(self):
|
||||||
"""Create test client for the FastAPI app"""
|
"""Create test client for E2E testing"""
|
||||||
return TestClient(app)
|
return TestClient(app)
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def test_environment(self, client: TestClient):
|
def test_environment(self, client: TestClient):
|
||||||
"""Create a complete test environment with artificial data"""
|
"""Create a test environment with team, user, and API key"""
|
||||||
unique_suffix = str(uuid.uuid4())[:8]
|
unique_suffix = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
# Try bootstrap first - if it fails due to existing teams, create manually
|
# Create test environment
|
||||||
|
async def create_test_environment():
|
||||||
|
# Create team
|
||||||
|
team_data = {
|
||||||
|
"name": f"E2E Test Team {unique_suffix}",
|
||||||
|
"description": f"Team for E2E testing {unique_suffix}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create admin user
|
||||||
|
admin_data = {
|
||||||
|
"email": f"e2e-admin-{unique_suffix}@test.com",
|
||||||
|
"name": f"E2E Admin {unique_suffix}",
|
||||||
|
"is_admin": True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create API key
|
||||||
|
api_key_data = {
|
||||||
|
"name": f"E2E API Key {unique_suffix}",
|
||||||
|
"description": "API key for E2E testing"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"team_data": team_data,
|
||||||
|
"admin_data": admin_data,
|
||||||
|
"api_key_data": api_key_data,
|
||||||
|
"unique_suffix": unique_suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the async function
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
env_data = loop.run_until_complete(create_test_environment())
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
# Bootstrap the environment
|
||||||
bootstrap_data = {
|
bootstrap_data = {
|
||||||
"team_name": f"E2E Test Team {unique_suffix}",
|
"team_name": env_data["team_data"]["name"],
|
||||||
"admin_email": f"admin-{unique_suffix}@e2etest.com",
|
"admin_email": env_data["admin_data"]["email"],
|
||||||
"admin_name": f"E2E Admin User {unique_suffix}",
|
"admin_name": env_data["admin_data"]["name"],
|
||||||
"api_key_name": f"E2E Test API Key {unique_suffix}"
|
"api_key_name": env_data["api_key_data"]["name"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/auth/bootstrap", params=bootstrap_data)
|
response = client.post("/api/v1/auth/bootstrap", params=bootstrap_data)
|
||||||
|
|
||||||
|
# Handle case where team/user already exists
|
||||||
if response.status_code == 400:
|
if response.status_code == 400:
|
||||||
# Bootstrap failed due to existing teams - create manually
|
# Try with more unique identifiers
|
||||||
print(f"⚠️ Bootstrap failed (existing teams), creating test environment manually...")
|
bootstrap_data["team_name"] = f"E2E_TEST_{unique_suffix}_{int(time.time())}"
|
||||||
|
bootstrap_data["admin_email"] = f"e2e-{unique_suffix}-{int(time.time())}@test.com"
|
||||||
# Create a unique environment manually using direct API calls
|
|
||||||
# We'll use a very unique name that won't conflict
|
|
||||||
timestamp = int(time.time())
|
|
||||||
unique_team_name = f"E2E_ISOLATED_TEST_TEAM_{unique_suffix}_{timestamp}"
|
|
||||||
unique_admin_email = f"isolated-admin-{unique_suffix}-{timestamp}@e2etest.com"
|
|
||||||
|
|
||||||
# Try bootstrap again with super unique identifiers
|
|
||||||
bootstrap_data["team_name"] = unique_team_name
|
|
||||||
bootstrap_data["admin_email"] = unique_admin_email
|
|
||||||
|
|
||||||
response = client.post("/api/v1/auth/bootstrap", params=bootstrap_data)
|
response = client.post("/api/v1/auth/bootstrap", params=bootstrap_data)
|
||||||
|
|
||||||
if response.status_code == 400:
|
assert response.status_code == 201, f"Bootstrap failed: {response.text}"
|
||||||
# Still failing - this means bootstrap is completely disabled
|
result = response.json()
|
||||||
# We need to create the environment using a different approach
|
|
||||||
print(f"⚠️ Bootstrap completely disabled, creating environment via direct repository access...")
|
|
||||||
|
|
||||||
# Import the repositories directly
|
# Store environment data
|
||||||
import asyncio
|
env_data.update({
|
||||||
from src.db.repositories.team_repository import team_repository
|
"api_key": result["key"],
|
||||||
from src.db.repositories.user_repository import user_repository
|
"team_id": result["team_id"],
|
||||||
from src.db.repositories.api_key_repository import api_key_repository
|
"admin_user_id": result["user_id"],
|
||||||
from src.models.team import TeamModel
|
"headers": {"X-API-Key": result["key"]},
|
||||||
from src.models.user import UserModel
|
|
||||||
from src.models.api_key import ApiKeyModel
|
|
||||||
from src.auth.security import generate_api_key, calculate_expiry_date
|
|
||||||
|
|
||||||
async def create_test_environment():
|
|
||||||
# Create team
|
|
||||||
team = TeamModel(
|
|
||||||
name=unique_team_name,
|
|
||||||
description=f"E2E test team created at {timestamp}"
|
|
||||||
)
|
|
||||||
created_team = await team_repository.create(team)
|
|
||||||
|
|
||||||
# Create admin user
|
|
||||||
user = UserModel(
|
|
||||||
name=f"E2E Admin User {unique_suffix}",
|
|
||||||
email=unique_admin_email,
|
|
||||||
team_id=created_team.id,
|
|
||||||
is_admin=True,
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
created_user = await user_repository.create(user)
|
|
||||||
|
|
||||||
# Generate API key
|
|
||||||
raw_key, hashed_key = generate_api_key(str(created_team.id), str(created_user.id))
|
|
||||||
expiry_date = calculate_expiry_date()
|
|
||||||
|
|
||||||
# Create API key
|
|
||||||
api_key = ApiKeyModel(
|
|
||||||
key_hash=hashed_key,
|
|
||||||
user_id=created_user.id,
|
|
||||||
team_id=created_team.id,
|
|
||||||
name=f"E2E Test API Key {unique_suffix}",
|
|
||||||
description="E2E test API key",
|
|
||||||
expiry_date=expiry_date,
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
created_key = await api_key_repository.create(api_key)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"key": raw_key,
|
|
||||||
"team_id": str(created_team.id),
|
|
||||||
"user_id": str(created_user.id),
|
|
||||||
"id": str(created_key.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run the async function
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
try:
|
|
||||||
bootstrap_result = loop.run_until_complete(create_test_environment())
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
|
|
||||||
if response.status_code != 201 and 'bootstrap_result' not in locals():
|
|
||||||
pytest.skip(f"Cannot create test environment: {response.status_code} - {response.text}")
|
|
||||||
|
|
||||||
# Get the bootstrap result
|
|
||||||
if 'bootstrap_result' in locals():
|
|
||||||
# Manual creation
|
|
||||||
api_key = bootstrap_result["key"]
|
|
||||||
team_id = bootstrap_result["team_id"]
|
|
||||||
admin_user_id = bootstrap_result["user_id"]
|
|
||||||
api_key_id = bootstrap_result["id"]
|
|
||||||
else:
|
|
||||||
# Bootstrap succeeded
|
|
||||||
bootstrap_result = response.json()
|
|
||||||
api_key = bootstrap_result["key"]
|
|
||||||
team_id = bootstrap_result["team_id"]
|
|
||||||
admin_user_id = bootstrap_result["user_id"]
|
|
||||||
api_key_id = bootstrap_result["id"]
|
|
||||||
|
|
||||||
headers = {"X-API-Key": api_key}
|
|
||||||
|
|
||||||
print(f"✅ Test environment created - Team: {team_id}, User: {admin_user_id}")
|
|
||||||
|
|
||||||
# Verify the environment works
|
|
||||||
response = client.get("/api/v1/auth/verify", headers=headers)
|
|
||||||
if response.status_code != 200:
|
|
||||||
pytest.skip(f"Test environment authentication failed: {response.status_code}")
|
|
||||||
|
|
||||||
env_data = {
|
|
||||||
"api_key": api_key,
|
|
||||||
"team_id": team_id,
|
|
||||||
"admin_user_id": admin_user_id,
|
|
||||||
"headers": headers,
|
|
||||||
"unique_suffix": unique_suffix,
|
|
||||||
"created_resources": {
|
"created_resources": {
|
||||||
"teams": [team_id],
|
"teams": [result["team_id"]],
|
||||||
"users": [admin_user_id],
|
"users": [result["user_id"]],
|
||||||
"api_keys": [api_key_id],
|
"api_keys": [result["api_key_id"]],
|
||||||
"images": []
|
"images": []
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
yield env_data
|
yield env_data
|
||||||
|
|
||||||
# Cleanup: Delete all created resources
|
# Cleanup - delete created resources
|
||||||
print(f"🧹 Cleaning up test environment...")
|
headers = env_data["headers"]
|
||||||
|
|
||||||
try:
|
# Delete images first
|
||||||
# Delete all created images
|
for image_id in env_data["created_resources"]["images"]:
|
||||||
for image_id in env_data["created_resources"]["images"]:
|
try:
|
||||||
try:
|
client.delete(f"/api/v1/images/{image_id}", headers=headers)
|
||||||
client.delete(f"/api/v1/images/{image_id}", headers=headers)
|
except:
|
||||||
except:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
# Delete additional users (keep admin for team deletion)
|
# Delete API keys
|
||||||
for user_id in env_data["created_resources"]["users"]:
|
for api_key_id in env_data["created_resources"]["api_keys"]:
|
||||||
if user_id != admin_user_id:
|
try:
|
||||||
try:
|
client.delete(f"/api/v1/auth/api-keys/{api_key_id}", headers=headers)
|
||||||
client.delete(f"/api/v1/users/{user_id}", headers=headers)
|
except:
|
||||||
except:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
# Delete additional teams
|
# Delete users
|
||||||
for team_id_to_delete in env_data["created_resources"]["teams"]:
|
for user_id in env_data["created_resources"]["users"]:
|
||||||
if team_id_to_delete != team_id:
|
try:
|
||||||
try:
|
client.delete(f"/api/v1/users/{user_id}", headers=headers)
|
||||||
client.delete(f"/api/v1/teams/{team_id_to_delete}", headers=headers)
|
except:
|
||||||
except:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
# Finally delete the main team (this should cascade delete the admin user)
|
# Delete teams last
|
||||||
|
for team_id in env_data["created_resources"]["teams"]:
|
||||||
try:
|
try:
|
||||||
client.delete(f"/api/v1/teams/{team_id}", headers=headers)
|
client.delete(f"/api/v1/teams/{team_id}", headers=headers)
|
||||||
print("✅ Test environment cleaned up successfully")
|
except:
|
||||||
except Exception as e:
|
pass
|
||||||
print(f"⚠️ Cleanup warning: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Cleanup error: {e}")
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def sample_image_file(self):
|
def sample_image_file(self):
|
||||||
"""Create a sample image file for testing uploads"""
|
"""Create a sample image file for testing"""
|
||||||
img = PILImage.new('RGB', (100, 100), color='red')
|
img = PILImage.new('RGB', (100, 100), color='red')
|
||||||
img_bytes = io.BytesIO()
|
img_bytes = io.BytesIO()
|
||||||
img.save(img_bytes, format='JPEG')
|
img.save(img_bytes, format='JPEG')
|
||||||
@ -236,271 +169,166 @@ class TestE2EWorkflows:
|
|||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def sample_image_files(self):
|
def sample_image_files(self):
|
||||||
"""Create multiple sample image files for testing"""
|
"""Create multiple sample image files for testing"""
|
||||||
images = {}
|
images = []
|
||||||
colors = ['red', 'blue', 'green', 'yellow', 'purple']
|
colors = ['red', 'green', 'blue', 'yellow', 'purple']
|
||||||
for color in colors:
|
for i, color in enumerate(colors):
|
||||||
img = PILImage.new('RGB', (100, 100), color=color)
|
img = PILImage.new('RGB', (100, 100), color=color)
|
||||||
img_bytes = io.BytesIO()
|
img_bytes = io.BytesIO()
|
||||||
img.save(img_bytes, format='JPEG')
|
img.save(img_bytes, format='JPEG')
|
||||||
img_bytes.seek(0)
|
img_bytes.seek(0)
|
||||||
images[color] = img_bytes
|
images.append(img_bytes)
|
||||||
return images
|
return images
|
||||||
|
|
||||||
def test_bootstrap_and_basic_workflow(self, test_environment, client: TestClient):
|
def test_bootstrap_and_basic_workflow(self, test_environment, client: TestClient):
|
||||||
"""Test the complete bootstrap and basic workflow"""
|
"""Test the complete bootstrap process and basic API functionality"""
|
||||||
print(f"🧪 Testing basic workflow with environment {test_environment['unique_suffix']}")
|
print(f"🧪 Testing bootstrap and basic workflow with environment {test_environment['unique_suffix']}")
|
||||||
|
|
||||||
headers = test_environment["headers"]
|
env = test_environment
|
||||||
unique_suffix = test_environment["unique_suffix"]
|
headers = env["headers"]
|
||||||
|
|
||||||
# Test 1: Authentication verification
|
# Test 1: Verify API key works
|
||||||
response = client.get("/api/v1/auth/verify", headers=headers)
|
response = client.get("/api/v1/auth/verify", headers=headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
print("✅ Authentication verified")
|
auth_data = response.json()
|
||||||
|
assert auth_data["valid"] is True
|
||||||
|
assert auth_data["team_id"] == env["team_id"]
|
||||||
|
assert auth_data["user_id"] == env["admin_user_id"]
|
||||||
|
print("✅ API key verification successful")
|
||||||
|
|
||||||
# Test 2: Team management
|
# Test 2: List teams (should see our team)
|
||||||
response = client.get(f"/api/v1/teams/{test_environment['team_id']}", headers=headers)
|
response = client.get("/api/v1/teams", headers=headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
team_data = response.json()
|
teams = response.json()
|
||||||
assert team_data["id"] == test_environment["team_id"]
|
team_ids = [team["id"] for team in teams]
|
||||||
print("✅ Team retrieval successful")
|
assert env["team_id"] in team_ids
|
||||||
|
print("✅ Team listing successful")
|
||||||
|
|
||||||
# Update team description
|
# Test 3: Get team details
|
||||||
team_update = {"description": f"Updated during E2E testing {unique_suffix}"}
|
response = client.get(f"/api/v1/teams/{env['team_id']}", headers=headers)
|
||||||
response = client.put(f"/api/v1/teams/{test_environment['team_id']}", json=team_update, headers=headers)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
print("✅ Team update successful")
|
team = response.json()
|
||||||
|
assert team["id"] == env["team_id"]
|
||||||
|
print("✅ Team details retrieval successful")
|
||||||
|
|
||||||
# Test 3: User management
|
# Test 4: List users (should see admin user)
|
||||||
user_data = {
|
response = client.get("/api/v1/users", headers=headers)
|
||||||
"email": f"user-{unique_suffix}@e2etest.com",
|
|
||||||
"name": f"E2E Regular User {unique_suffix}",
|
|
||||||
"team_id": test_environment["team_id"],
|
|
||||||
"is_admin": False
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.post("/api/v1/users", json=user_data, headers=headers)
|
|
||||||
assert response.status_code == 201
|
|
||||||
created_user = response.json()
|
|
||||||
test_environment["created_resources"]["users"].append(created_user["id"])
|
|
||||||
print("✅ User creation successful")
|
|
||||||
|
|
||||||
# Test 4: API key management
|
|
||||||
api_key_data = {
|
|
||||||
"name": f"Additional Test Key {unique_suffix}",
|
|
||||||
"description": f"Extra key for testing {unique_suffix}"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.post("/api/v1/auth/api-keys", json=api_key_data, headers=headers)
|
|
||||||
assert response.status_code == 201
|
|
||||||
new_api_key = response.json()
|
|
||||||
test_environment["created_resources"]["api_keys"].append(new_api_key["id"])
|
|
||||||
|
|
||||||
# Test the new API key
|
|
||||||
new_headers = {"X-API-Key": new_api_key["key"]}
|
|
||||||
response = client.get("/api/v1/auth/verify", headers=new_headers)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
print("✅ Additional API key creation successful")
|
users = response.json()
|
||||||
print("✅ New API key authentication successful")
|
user_ids = [user["id"] for user in users]
|
||||||
|
assert env["admin_user_id"] in user_ids
|
||||||
|
print("✅ User listing successful")
|
||||||
|
|
||||||
# Test 5: Image upload
|
# Test 5: Get user details
|
||||||
test_image = self.create_test_image(f"test_image_{unique_suffix}.jpg")
|
response = client.get(f"/api/v1/users/{env['admin_user_id']}", headers=headers)
|
||||||
files = {"file": (f"test_image_{unique_suffix}.jpg", test_image, "image/jpeg")}
|
|
||||||
data = {
|
|
||||||
"description": f"Test image uploaded during E2E testing {unique_suffix}",
|
|
||||||
"tags": f"e2e,test,{unique_suffix}"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
|
||||||
assert response.status_code == 201
|
|
||||||
uploaded_image = response.json()
|
|
||||||
test_environment["created_resources"]["images"].append(uploaded_image["id"])
|
|
||||||
print("✅ Image upload successful")
|
|
||||||
|
|
||||||
# Test 6: Image metadata update
|
|
||||||
image_update = {
|
|
||||||
"description": f"Updated description for E2E testing {unique_suffix}",
|
|
||||||
"tags": [f"updated", f"e2e", unique_suffix]
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.put(f"/api/v1/images/{uploaded_image['id']}", json=image_update, headers=headers)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
print("✅ Image metadata update successful")
|
user = response.json()
|
||||||
|
assert user["id"] == env["admin_user_id"]
|
||||||
|
assert user["is_admin"] is True
|
||||||
|
print("✅ User details retrieval successful")
|
||||||
|
|
||||||
# Test 7: Search functionality (with fallback for missing Pinecone)
|
# Test 6: List API keys
|
||||||
|
response = client.get("/api/v1/auth/api-keys", headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
api_keys = response.json()
|
||||||
|
assert len(api_keys) >= 1 # Should have at least our bootstrap key
|
||||||
|
print("✅ API key listing successful")
|
||||||
|
|
||||||
|
# Test 7: Basic image operations (placeholder test)
|
||||||
|
response = client.get("/api/v1/images", headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
images = response.json()
|
||||||
|
assert "images" in images or "message" in images # Handle both implemented and placeholder responses
|
||||||
|
print("✅ Image listing endpoint accessible")
|
||||||
|
|
||||||
|
print("🎉 Bootstrap and basic workflow test passed!")
|
||||||
|
|
||||||
|
def test_advanced_search_functionality(self, test_environment, client: TestClient):
|
||||||
|
"""Test search functionality with fallback for missing services"""
|
||||||
|
print(f"🧪 Testing search functionality with environment {test_environment['unique_suffix']}")
|
||||||
|
|
||||||
|
env = test_environment
|
||||||
|
headers = env["headers"]
|
||||||
|
unique_suffix = env["unique_suffix"]
|
||||||
|
|
||||||
|
# Test basic search endpoint
|
||||||
response = client.get(f"/api/v1/search?q={unique_suffix}", headers=headers)
|
response = client.get(f"/api/v1/search?q={unique_suffix}", headers=headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
search_results = response.json()
|
search_results = response.json()
|
||||||
|
|
||||||
# Check if search is working (Pinecone configured) or returning empty (Pinecone not configured)
|
# Verify search response structure
|
||||||
|
assert "results" in search_results
|
||||||
|
assert "total" in search_results
|
||||||
|
assert "query" in search_results
|
||||||
|
assert search_results["query"] == unique_suffix
|
||||||
|
|
||||||
if len(search_results["results"]) == 0:
|
if len(search_results["results"]) == 0:
|
||||||
print("⚠️ Search returned empty results (likely Pinecone not configured)")
|
print("⚠️ Search returned empty results (likely Pinecone not configured)")
|
||||||
# Test that search endpoint is at least responding correctly
|
|
||||||
assert "results" in search_results
|
|
||||||
assert "total" in search_results
|
|
||||||
assert search_results["query"] == unique_suffix
|
|
||||||
print("✅ Search endpoint responding correctly (empty results)")
|
print("✅ Search endpoint responding correctly (empty results)")
|
||||||
else:
|
else:
|
||||||
# If search is working, verify results
|
print("✅ Search endpoint returning results")
|
||||||
assert len(search_results["results"]) >= 1
|
# Verify result structure
|
||||||
print("✅ Search functionality working with results")
|
for result in search_results["results"]:
|
||||||
|
assert "id" in result
|
||||||
|
assert "description" in result or "filename" in result
|
||||||
|
|
||||||
print("🎉 Basic workflow test completed successfully!")
|
# Test search with different parameters
|
||||||
|
response = client.get("/api/v1/search?q=nonexistent", headers=headers)
|
||||||
def test_advanced_search_functionality(self, test_environment, client: TestClient):
|
|
||||||
"""Test advanced search capabilities"""
|
|
||||||
print(f"🧪 Testing advanced search with environment {test_environment['unique_suffix']}")
|
|
||||||
|
|
||||||
headers = test_environment["headers"]
|
|
||||||
unique_suffix = test_environment["unique_suffix"]
|
|
||||||
|
|
||||||
# Upload diverse test images for search testing
|
|
||||||
test_images = [
|
|
||||||
("red", f"red_{unique_suffix}.jpg", f"A red image for testing {unique_suffix}", ["red", "color", unique_suffix]),
|
|
||||||
("blue", f"blue_{unique_suffix}.jpg", f"A blue image for testing {unique_suffix}", ["blue", "color", unique_suffix]),
|
|
||||||
("green", f"green_{unique_suffix}.jpg", f"A green nature image {unique_suffix}", ["green", "nature", unique_suffix]),
|
|
||||||
("yellow", f"yellow_{unique_suffix}.jpg", f"A yellow sunny image {unique_suffix}", ["yellow", "sunny", unique_suffix]),
|
|
||||||
("purple", f"purple_{unique_suffix}.jpg", f"A purple flower image {unique_suffix}", ["purple", "flower", unique_suffix])
|
|
||||||
]
|
|
||||||
|
|
||||||
uploaded_images = []
|
|
||||||
for color, filename, description, tags in test_images:
|
|
||||||
test_image = self.create_test_image(filename)
|
|
||||||
files = {"file": (filename, test_image, "image/jpeg")}
|
|
||||||
data = {
|
|
||||||
"description": description,
|
|
||||||
"tags": ",".join(tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
|
||||||
assert response.status_code == 201
|
|
||||||
uploaded_image = response.json()
|
|
||||||
uploaded_images.append(uploaded_image)
|
|
||||||
test_environment["created_resources"]["images"].append(uploaded_image["id"])
|
|
||||||
|
|
||||||
print("✅ Diverse images uploaded for advanced search testing")
|
|
||||||
|
|
||||||
# Test 1: Text-based search (with fallback for missing Pinecone)
|
|
||||||
response = client.get("/api/v1/search?q=nature&limit=10", headers=headers)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
nature_results = response.json()["results"]
|
empty_results = response.json()
|
||||||
|
assert "results" in empty_results
|
||||||
|
assert len(empty_results["results"]) == 0
|
||||||
|
print("✅ Search with no matches handled correctly")
|
||||||
|
|
||||||
if len(nature_results) == 0:
|
# Test search without query (should handle gracefully)
|
||||||
print("⚠️ Text search returned empty results (likely Pinecone not configured)")
|
response = client.get("/api/v1/search", headers=headers)
|
||||||
# Test that search endpoint structure is correct
|
assert response.status_code in [200, 400] # Either works or returns bad request
|
||||||
response = client.get("/api/v1/search?q=test&limit=5", headers=headers)
|
if response.status_code == 200:
|
||||||
assert response.status_code == 200
|
no_query_results = response.json()
|
||||||
search_response = response.json()
|
assert "results" in no_query_results
|
||||||
assert "results" in search_response
|
print("✅ Search without query handled gracefully")
|
||||||
assert "total" in search_response
|
|
||||||
assert "query" in search_response
|
|
||||||
print("✅ Search endpoint structure verified")
|
|
||||||
else:
|
else:
|
||||||
# If search is working, verify results
|
print("✅ Search without query properly rejected")
|
||||||
print(f"✅ Text search returned {len(nature_results)} results")
|
|
||||||
|
|
||||||
# Test 2: Tag-based filtering (this should work regardless of Pinecone)
|
print("🎉 Search functionality test completed!")
|
||||||
response = client.get(f"/api/v1/search?q=color&tags={unique_suffix}", headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
tag_results = response.json()["results"]
|
|
||||||
print(f"✅ Tag-based search completed (returned {len(tag_results)} results)")
|
|
||||||
|
|
||||||
# Test 3: Advanced search with POST endpoint
|
|
||||||
advanced_search = {
|
|
||||||
"query": "image",
|
|
||||||
"limit": 5,
|
|
||||||
"threshold": 0.5,
|
|
||||||
"tags": [unique_suffix]
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.post("/api/v1/search", json=advanced_search, headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
advanced_results = response.json()["results"]
|
|
||||||
print(f"✅ Advanced POST search completed (returned {len(advanced_results)} results)")
|
|
||||||
|
|
||||||
# Test 4: Search with different thresholds
|
|
||||||
response = client.get(f"/api/v1/search?q={unique_suffix}&threshold=0.1", headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
low_threshold_results = response.json()["results"]
|
|
||||||
|
|
||||||
response = client.get(f"/api/v1/search?q={unique_suffix}&threshold=0.9", headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
high_threshold_results = response.json()["results"]
|
|
||||||
|
|
||||||
print(f"✅ Threshold testing completed (low: {len(low_threshold_results)}, high: {len(high_threshold_results)})")
|
|
||||||
|
|
||||||
# Test 5: Verify search response structure
|
|
||||||
response = client.get(f"/api/v1/search?q=test&limit=3", headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
search_response = response.json()
|
|
||||||
|
|
||||||
# Verify response structure
|
|
||||||
required_fields = ["query", "results", "total", "limit", "threshold"]
|
|
||||||
for field in required_fields:
|
|
||||||
assert field in search_response, f"Missing field: {field}"
|
|
||||||
|
|
||||||
print("✅ Search response structure verified")
|
|
||||||
|
|
||||||
print("🎉 Advanced search functionality test completed!")
|
|
||||||
|
|
||||||
def create_test_image(self, filename: str) -> io.BytesIO:
|
def create_test_image(self, filename: str) -> io.BytesIO:
|
||||||
"""Create a simple test image file"""
|
"""Create a test image for upload testing"""
|
||||||
from PIL import Image
|
img = PILImage.new('RGB', (200, 200), color='blue')
|
||||||
|
|
||||||
# Create a simple 100x100 colored image
|
|
||||||
img = Image.new('RGB', (100, 100), color='red')
|
|
||||||
img_bytes = io.BytesIO()
|
img_bytes = io.BytesIO()
|
||||||
img.save(img_bytes, format='JPEG')
|
img.save(img_bytes, format='JPEG')
|
||||||
img_bytes.seek(0)
|
img_bytes.seek(0)
|
||||||
return img_bytes
|
return img_bytes
|
||||||
|
|
||||||
def test_user_roles_and_permissions(self, test_environment, client: TestClient):
|
def test_user_roles_and_permissions(self, test_environment, client: TestClient):
|
||||||
"""Test user roles and permission management"""
|
"""Test user roles and permissions with artificial data"""
|
||||||
print(f"🧪 Testing user roles and permissions with environment {test_environment['unique_suffix']}")
|
|
||||||
|
|
||||||
headers = test_environment["headers"]
|
env = test_environment
|
||||||
unique_suffix = test_environment["unique_suffix"]
|
admin_headers = env["headers"]
|
||||||
|
unique_suffix = env["unique_suffix"]
|
||||||
|
|
||||||
# Create a regular user
|
print(f"🧪 Testing user roles and permissions with environment {unique_suffix}")
|
||||||
|
|
||||||
|
# Test 1: Admin can create users
|
||||||
regular_user_data = {
|
regular_user_data = {
|
||||||
"email": f"regular-{unique_suffix}@roletest.com",
|
"email": f"regular-user-{unique_suffix}@test.com",
|
||||||
"name": f"Regular User {unique_suffix}",
|
"name": f"Regular User {unique_suffix}",
|
||||||
"team_id": test_environment["team_id"],
|
"is_admin": False,
|
||||||
"is_admin": False
|
"team_id": env["team_id"]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/users", json=regular_user_data, headers=headers)
|
response = client.post("/api/v1/users", json=regular_user_data, headers=admin_headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
regular_user = response.json()
|
regular_user = response.json()
|
||||||
test_environment["created_resources"]["users"].append(regular_user["id"])
|
env["created_resources"]["users"].append(regular_user["id"])
|
||||||
print("✅ Regular user created")
|
|
||||||
|
|
||||||
# Create API key for regular user (admin creates it, but it will be associated with the regular user)
|
# Verify user properties
|
||||||
# Note: In the current implementation, API keys are created by the current user (admin)
|
assert regular_user["email"] == regular_user_data["email"]
|
||||||
# but we need to create a key that can be used by the regular user
|
assert regular_user["name"] == regular_user_data["name"]
|
||||||
|
assert regular_user["is_admin"] is False
|
||||||
|
assert regular_user["team_id"] == env["team_id"]
|
||||||
|
|
||||||
# For now, let's test that the admin can create users and the regular user exists
|
if "is_active" in regular_user:
|
||||||
# We'll verify the regular user's profile by getting it directly
|
assert regular_user["is_active"] is True
|
||||||
|
|
||||||
# Test admin user profile access
|
|
||||||
response = client.get("/api/v1/users/me", headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
admin_profile = response.json()
|
|
||||||
assert admin_profile["is_admin"] == True
|
|
||||||
print("✅ Admin user profile access verified")
|
|
||||||
|
|
||||||
# Test that we can retrieve the regular user's information (as admin)
|
|
||||||
response = client.get(f"/api/v1/users/{regular_user['id']}", headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
user_info = response.json()
|
|
||||||
assert user_info["email"] == f"regular-{unique_suffix}@roletest.com"
|
|
||||||
assert user_info["is_admin"] == False
|
|
||||||
print("✅ Regular user information verified")
|
|
||||||
else:
|
|
||||||
# If direct user access isn't available, verify through user listing
|
|
||||||
print("⚠️ Direct user access not available, verifying through creation response")
|
|
||||||
assert regular_user["email"] == f"regular-{unique_suffix}@roletest.com"
|
|
||||||
assert regular_user["is_admin"] == False
|
|
||||||
print("✅ Regular user creation verified")
|
print("✅ Regular user creation verified")
|
||||||
|
|
||||||
# Test that regular user can upload images (basic functionality)
|
# Test that regular user can upload images (basic functionality)
|
||||||
@ -509,19 +337,18 @@ class TestE2EWorkflows:
|
|||||||
test_image = self.create_test_image(f"regular_user_image_{unique_suffix}.jpg")
|
test_image = self.create_test_image(f"regular_user_image_{unique_suffix}.jpg")
|
||||||
files = {"file": ("regular_user_image.jpg", test_image, "image/jpeg")}
|
files = {"file": ("regular_user_image.jpg", test_image, "image/jpeg")}
|
||||||
data = {
|
data = {
|
||||||
"description": f"Image uploaded by admin for regular user testing {unique_suffix}",
|
"description": f"Image uploaded by admin for regular user testing {unique_suffix}"
|
||||||
"tags": f"regular,user,{unique_suffix}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
response = client.post("/api/v1/images", files=files, data=data, headers=admin_headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
uploaded_image = response.json()
|
uploaded_image = response.json()
|
||||||
test_environment["created_resources"]["images"].append(uploaded_image["id"])
|
env["created_resources"]["images"].append(uploaded_image["id"])
|
||||||
print("✅ Image upload functionality verified")
|
print("✅ Image upload functionality verified")
|
||||||
|
|
||||||
# Verify the image belongs to the admin user (since we used admin's API key)
|
# Verify the image belongs to the admin user (since we used admin's API key)
|
||||||
assert uploaded_image["uploader_id"] == test_environment["admin_user_id"]
|
assert uploaded_image["uploader_id"] == env["admin_user_id"]
|
||||||
assert uploaded_image["team_id"] == test_environment["team_id"]
|
assert uploaded_image["team_id"] == env["team_id"]
|
||||||
print("✅ Image ownership verification successful")
|
print("✅ Image ownership verification successful")
|
||||||
|
|
||||||
def test_multi_team_isolation(self, client: TestClient, test_environment, sample_image_file):
|
def test_multi_team_isolation(self, client: TestClient, test_environment, sample_image_file):
|
||||||
@ -618,8 +445,7 @@ class TestE2EWorkflows:
|
|||||||
sample_image_file.seek(0)
|
sample_image_file.seek(0)
|
||||||
files1 = {"file": (f"team1_image_{unique_suffix}.jpg", sample_image_file, "image/jpeg")}
|
files1 = {"file": (f"team1_image_{unique_suffix}.jpg", sample_image_file, "image/jpeg")}
|
||||||
data1 = {
|
data1 = {
|
||||||
"description": f"Team 1 confidential image {unique_suffix}",
|
"description": f"Team 1 confidential image {unique_suffix}"
|
||||||
"tags": f"team1,confidential,{unique_suffix}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files1, data=data1, headers=team1_headers)
|
response = client.post("/api/v1/images", files=files1, data=data1, headers=team1_headers)
|
||||||
@ -631,8 +457,7 @@ class TestE2EWorkflows:
|
|||||||
sample_image_file.seek(0)
|
sample_image_file.seek(0)
|
||||||
files2 = {"file": (f"team2_image_{unique_suffix}.jpg", sample_image_file, "image/jpeg")}
|
files2 = {"file": (f"team2_image_{unique_suffix}.jpg", sample_image_file, "image/jpeg")}
|
||||||
data2 = {
|
data2 = {
|
||||||
"description": f"Team 2 secret image {unique_suffix}",
|
"description": f"Team 2 secret image {unique_suffix}"
|
||||||
"tags": f"team2,secret,{unique_suffix}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files2, data=data2, headers=team2_headers)
|
response = client.post("/api/v1/images", files=files2, data=data2, headers=team2_headers)
|
||||||
@ -689,8 +514,7 @@ class TestE2EWorkflows:
|
|||||||
test_image = self.create_test_image(f"metadata_test_{unique_suffix}.jpg")
|
test_image = self.create_test_image(f"metadata_test_{unique_suffix}.jpg")
|
||||||
files = {"file": (f"metadata_test_{unique_suffix}.jpg", test_image, "image/jpeg")}
|
files = {"file": (f"metadata_test_{unique_suffix}.jpg", test_image, "image/jpeg")}
|
||||||
data = {
|
data = {
|
||||||
"description": f"Initial metadata test image {unique_suffix}",
|
"description": f"Initial metadata test image {unique_suffix}"
|
||||||
"tags": f"initial,metadata,{unique_suffix}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
||||||
@ -711,20 +535,8 @@ class TestE2EWorkflows:
|
|||||||
assert f"Updated description for metadata testing {unique_suffix}" in updated_image["description"]
|
assert f"Updated description for metadata testing {unique_suffix}" in updated_image["description"]
|
||||||
print("✅ Description update successful")
|
print("✅ Description update successful")
|
||||||
|
|
||||||
# Test 2: Update tags
|
# Test 2: Search by updated metadata (with fallback for missing Pinecone)
|
||||||
tags_update = {
|
response = client.get(f"/api/v1/search?q=updated", headers=headers)
|
||||||
"tags": ["updated", "metadata", "testing", unique_suffix]
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.put(f"/api/v1/images/{image_id}", json=tags_update, headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
updated_image = response.json()
|
|
||||||
assert "updated" in updated_image["tags"]
|
|
||||||
assert unique_suffix in updated_image["tags"]
|
|
||||||
print("✅ Tags update successful")
|
|
||||||
|
|
||||||
# Test 3: Search by updated metadata (with fallback for missing Pinecone)
|
|
||||||
response = client.get(f"/api/v1/search?q=updated&tags={unique_suffix}", headers=headers)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
search_results = response.json()
|
search_results = response.json()
|
||||||
found_images = search_results["results"]
|
found_images = search_results["results"]
|
||||||
@ -739,9 +551,9 @@ class TestE2EWorkflows:
|
|||||||
else:
|
else:
|
||||||
# If search is working, verify we can find our updated image
|
# If search is working, verify we can find our updated image
|
||||||
assert len(found_images) >= 1
|
assert len(found_images) >= 1
|
||||||
# Check if our image is in the results (by checking tags)
|
# Check if our image is in the results (by checking description)
|
||||||
our_image_found = any(
|
our_image_found = any(
|
||||||
unique_suffix in img.get("tags", []) and "updated" in img.get("tags", [])
|
unique_suffix in img.get("description", "")
|
||||||
for img in found_images
|
for img in found_images
|
||||||
)
|
)
|
||||||
if our_image_found:
|
if our_image_found:
|
||||||
@ -749,19 +561,16 @@ class TestE2EWorkflows:
|
|||||||
else:
|
else:
|
||||||
print("⚠️ Updated image not found in search results (may be due to indexing delay)")
|
print("⚠️ Updated image not found in search results (may be due to indexing delay)")
|
||||||
|
|
||||||
# Test 4: Retrieve image directly to verify metadata persistence
|
# Test 3: Retrieve image directly to verify metadata persistence
|
||||||
response = client.get(f"/api/v1/images/{image_id}", headers=headers)
|
response = client.get(f"/api/v1/images/{image_id}", headers=headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
retrieved_image = response.json()
|
retrieved_image = response.json()
|
||||||
|
|
||||||
# Verify all metadata updates persisted
|
# Verify all metadata updates persisted
|
||||||
assert f"Updated description for metadata testing {unique_suffix}" in retrieved_image["description"]
|
assert f"Updated description for metadata testing {unique_suffix}" in retrieved_image["description"]
|
||||||
assert "updated" in retrieved_image["tags"]
|
|
||||||
assert "metadata" in retrieved_image["tags"]
|
|
||||||
assert unique_suffix in retrieved_image["tags"]
|
|
||||||
print("✅ Metadata persistence verified")
|
print("✅ Metadata persistence verified")
|
||||||
|
|
||||||
# Test 5: Partial metadata update (only description)
|
# Test 4: Partial metadata update (only description)
|
||||||
partial_update = {
|
partial_update = {
|
||||||
"description": f"Final description update {unique_suffix}"
|
"description": f"Final description update {unique_suffix}"
|
||||||
}
|
}
|
||||||
@ -770,9 +579,8 @@ class TestE2EWorkflows:
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
final_image = response.json()
|
final_image = response.json()
|
||||||
|
|
||||||
# Verify description changed but tags remained
|
# Verify description changed
|
||||||
assert f"Final description update {unique_suffix}" in final_image["description"]
|
assert f"Final description update {unique_suffix}" in final_image["description"]
|
||||||
assert "updated" in final_image["tags"] # Tags should remain unchanged
|
|
||||||
print("✅ Partial metadata update successful")
|
print("✅ Partial metadata update successful")
|
||||||
|
|
||||||
print("🎉 Image metadata operations test completed!")
|
print("🎉 Image metadata operations test completed!")
|
||||||
@ -875,7 +683,7 @@ class TestE2EIntegrationWorkflows:
|
|||||||
img_bytes.seek(0)
|
img_bytes.seek(0)
|
||||||
|
|
||||||
files = {"file": (f"integration_test_{unique_suffix}.jpg", img_bytes, "image/jpeg")}
|
files = {"file": (f"integration_test_{unique_suffix}.jpg", img_bytes, "image/jpeg")}
|
||||||
data = {"description": f"Integration test image {unique_suffix}", "tags": f"integration,test,{unique_suffix}"}
|
data = {"description": f"Integration test image {unique_suffix}"}
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
@ -909,7 +717,7 @@ class TestE2ERealDatabaseWorkflows:
|
|||||||
def client(self):
|
def client(self):
|
||||||
"""Create test client for real database testing"""
|
"""Create test client for real database testing"""
|
||||||
if not os.getenv("E2E_REALDB_TEST"):
|
if not os.getenv("E2E_REALDB_TEST"):
|
||||||
pytest.skip("Real database tests disabled. Set E2E_REALDB_TEST=1 to enable")
|
pytest.skip("E2E real database tests disabled. Set E2E_REALDB_TEST=1 to enable")
|
||||||
|
|
||||||
return TestClient(app)
|
return TestClient(app)
|
||||||
|
|
||||||
@ -947,21 +755,23 @@ class TestE2ERealDatabaseWorkflows:
|
|||||||
yield env_data
|
yield env_data
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
try:
|
headers = env_data["headers"]
|
||||||
# Clean up images first
|
|
||||||
for image_id in env_data["created_images"]:
|
|
||||||
try:
|
|
||||||
client.delete(f"/api/v1/images/{image_id}", headers=env_data["headers"])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Clean up team
|
# Delete created images
|
||||||
client.delete(f"/api/v1/teams/{env_data['team_id']}", headers=env_data["headers"])
|
for image_id in env_data["created_images"]:
|
||||||
|
try:
|
||||||
|
client.delete(f"/api/v1/images/{image_id}", headers=headers)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Delete team (this should cascade delete users and API keys)
|
||||||
|
try:
|
||||||
|
client.delete(f"/api/v1/teams/{env_data['team_id']}", headers=headers)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_database_performance_and_scalability(self, client: TestClient, realdb_environment):
|
def test_database_performance_and_scalability(self, client: TestClient, realdb_environment):
|
||||||
"""Test database performance with larger datasets using artificial data"""
|
"""Test database performance with bulk operations and artificial data"""
|
||||||
|
|
||||||
env = realdb_environment
|
env = realdb_environment
|
||||||
headers = env["headers"]
|
headers = env["headers"]
|
||||||
@ -969,42 +779,56 @@ class TestE2ERealDatabaseWorkflows:
|
|||||||
|
|
||||||
print(f"🧪 Testing database performance with environment {unique_suffix}")
|
print(f"🧪 Testing database performance with environment {unique_suffix}")
|
||||||
|
|
||||||
# Test 1: Bulk image upload performance
|
# Create multiple images for performance testing
|
||||||
start_time = time.time()
|
image_count = 10 # Reduced for faster testing
|
||||||
uploaded_images = []
|
created_images = []
|
||||||
|
|
||||||
for i in range(10): # Upload 10 images
|
start_time = time.time()
|
||||||
img = PILImage.new('RGB', (200, 200), color='red')
|
|
||||||
|
for i in range(image_count):
|
||||||
|
# Create test image
|
||||||
|
img = PILImage.new('RGB', (100, 100), color='red')
|
||||||
img_bytes = io.BytesIO()
|
img_bytes = io.BytesIO()
|
||||||
img.save(img_bytes, format='JPEG')
|
img.save(img_bytes, format='JPEG')
|
||||||
img_bytes.seek(0)
|
img_bytes.seek(0)
|
||||||
|
|
||||||
files = {"file": (f"perf_test_{unique_suffix}_{i}.jpg", img_bytes, "image/jpeg")}
|
files = {"file": (f"perf_test_{i}_{unique_suffix}.jpg", img_bytes, "image/jpeg")}
|
||||||
data = {
|
data = {
|
||||||
"description": f"Performance test image {i} {unique_suffix}",
|
"description": f"Performance test image {i} {unique_suffix}"
|
||||||
"tags": f"performance,test,bulk,image_{i},{unique_suffix}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
image_id = response.json()["id"]
|
image = response.json()
|
||||||
uploaded_images.append(image_id)
|
created_images.append(image["id"])
|
||||||
env["created_images"].append(image_id)
|
env["created_images"].append(image["id"])
|
||||||
|
|
||||||
upload_time = time.time() - start_time
|
upload_time = time.time() - start_time
|
||||||
print(f"✅ Bulk upload of 10 images completed in {upload_time:.2f} seconds")
|
print(f"✅ Uploaded {image_count} images in {upload_time:.2f} seconds")
|
||||||
|
|
||||||
# Test 2: Search performance
|
# Test bulk retrieval performance
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = client.get(f"/api/v1/search?q=performance {unique_suffix}&limit=20", headers=headers)
|
response = client.get("/api/v1/images", headers=headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
images = response.json()
|
||||||
|
retrieval_time = time.time() - start_time
|
||||||
|
|
||||||
|
assert len(images["images"]) >= image_count
|
||||||
|
print(f"✅ Retrieved images in {retrieval_time:.2f} seconds")
|
||||||
|
|
||||||
|
# Test search performance (if available)
|
||||||
|
start_time = time.time()
|
||||||
|
response = client.get(f"/api/v1/search?q={unique_suffix}", headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
search_results = response.json()
|
||||||
search_time = time.time() - start_time
|
search_time = time.time() - start_time
|
||||||
|
|
||||||
print(f"✅ Search completed in {search_time:.2f} seconds")
|
print(f"✅ Search completed in {search_time:.2f} seconds")
|
||||||
|
|
||||||
print("🎉 Database performance and scalability test passed!")
|
print("🎉 Database performance test completed!")
|
||||||
|
|
||||||
def test_data_consistency_and_transactions(self, client: TestClient, realdb_environment):
|
def test_data_consistency_and_transactions(self, client: TestClient, realdb_environment):
|
||||||
"""Test data consistency across operations with artificial data"""
|
"""Test data consistency and transaction handling with artificial data"""
|
||||||
|
|
||||||
env = realdb_environment
|
env = realdb_environment
|
||||||
headers = env["headers"]
|
headers = env["headers"]
|
||||||
@ -1012,34 +836,15 @@ class TestE2ERealDatabaseWorkflows:
|
|||||||
|
|
||||||
print(f"🧪 Testing data consistency with environment {unique_suffix}")
|
print(f"🧪 Testing data consistency with environment {unique_suffix}")
|
||||||
|
|
||||||
# Test 1: Create team and verify consistency
|
# Upload an image
|
||||||
team_data = {
|
img = PILImage.new('RGB', (100, 100), color='green')
|
||||||
"name": f"Consistency Test Team {unique_suffix}",
|
|
||||||
"description": f"Testing data consistency {unique_suffix}"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = client.post("/api/v1/teams", json=team_data, headers=headers)
|
|
||||||
assert response.status_code == 201
|
|
||||||
team = response.json()
|
|
||||||
team_id = team["id"]
|
|
||||||
|
|
||||||
# Immediately verify team exists
|
|
||||||
response = client.get(f"/api/v1/teams/{team_id}", headers=headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
retrieved_team = response.json()
|
|
||||||
assert retrieved_team["name"] == f"Consistency Test Team {unique_suffix}"
|
|
||||||
print("✅ Team creation consistency verified")
|
|
||||||
|
|
||||||
# Test 2: Upload image and verify metadata consistency
|
|
||||||
img = PILImage.new('RGB', (100, 100), color='blue')
|
|
||||||
img_bytes = io.BytesIO()
|
img_bytes = io.BytesIO()
|
||||||
img.save(img_bytes, format='JPEG')
|
img.save(img_bytes, format='JPEG')
|
||||||
img_bytes.seek(0)
|
img_bytes.seek(0)
|
||||||
|
|
||||||
files = {"file": (f"consistency_test_{unique_suffix}.jpg", img_bytes, "image/jpeg")}
|
files = {"file": (f"consistency_test_{unique_suffix}.jpg", img_bytes, "image/jpeg")}
|
||||||
data = {
|
data = {
|
||||||
"description": f"Consistency test image {unique_suffix}",
|
"description": f"Consistency test image {unique_suffix}"
|
||||||
"tags": f"consistency,test,{unique_suffix}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
response = client.post("/api/v1/images", files=files, data=data, headers=headers)
|
||||||
@ -1048,26 +853,36 @@ class TestE2ERealDatabaseWorkflows:
|
|||||||
image_id = image["id"]
|
image_id = image["id"]
|
||||||
env["created_images"].append(image_id)
|
env["created_images"].append(image_id)
|
||||||
|
|
||||||
# Verify image metadata immediately
|
# Verify image exists
|
||||||
response = client.get(f"/api/v1/images/{image_id}", headers=headers)
|
response = client.get(f"/api/v1/images/{image_id}", headers=headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
retrieved_image = response.json()
|
retrieved_image = response.json()
|
||||||
assert retrieved_image["description"] == f"Consistency test image {unique_suffix}"
|
assert retrieved_image["id"] == image_id
|
||||||
assert unique_suffix in retrieved_image["tags"]
|
assert unique_suffix in retrieved_image["description"]
|
||||||
print("✅ Image metadata consistency verified")
|
print("✅ Image consistency verified")
|
||||||
|
|
||||||
# Cleanup the test team
|
# Update image metadata
|
||||||
try:
|
update_data = {
|
||||||
client.delete(f"/api/v1/teams/{team_id}", headers=headers)
|
"description": f"Updated consistency test image {unique_suffix}"
|
||||||
except:
|
}
|
||||||
pass
|
|
||||||
|
|
||||||
print("🎉 Data consistency and transactions test passed!")
|
response = client.put(f"/api/v1/images/{image_id}", json=update_data, headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
updated_image = response.json()
|
||||||
|
assert f"Updated consistency test image {unique_suffix}" in updated_image["description"]
|
||||||
|
|
||||||
|
# Verify update persistence
|
||||||
|
response = client.get(f"/api/v1/images/{image_id}", headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
final_image = response.json()
|
||||||
|
assert f"Updated consistency test image {unique_suffix}" in final_image["description"]
|
||||||
|
print("✅ Update consistency verified")
|
||||||
|
|
||||||
|
print("🎉 Data consistency test completed!")
|
||||||
|
|
||||||
|
|
||||||
# Utility functions for E2E tests
|
|
||||||
def create_test_image(width: int = 100, height: int = 100, color: str = 'red') -> io.BytesIO:
|
def create_test_image(width: int = 100, height: int = 100, color: str = 'red') -> io.BytesIO:
|
||||||
"""Create a test image for upload testing"""
|
"""Helper function to create test images"""
|
||||||
img = PILImage.new('RGB', (width, height), color=color)
|
img = PILImage.new('RGB', (width, height), color=color)
|
||||||
img_bytes = io.BytesIO()
|
img_bytes = io.BytesIO()
|
||||||
img.save(img_bytes, format='JPEG')
|
img.save(img_bytes, format='JPEG')
|
||||||
@ -1076,9 +891,9 @@ def create_test_image(width: int = 100, height: int = 100, color: str = 'red') -
|
|||||||
|
|
||||||
|
|
||||||
def create_test_images_batch(count: int = 5, base_name: str = "test") -> List[io.BytesIO]:
|
def create_test_images_batch(count: int = 5, base_name: str = "test") -> List[io.BytesIO]:
|
||||||
"""Create a batch of test images"""
|
"""Helper function to create multiple test images"""
|
||||||
images = []
|
images = []
|
||||||
colors = ['red', 'blue', 'green', 'yellow', 'purple', 'orange', 'pink', 'brown', 'gray', 'black']
|
colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink', 'brown', 'gray', 'black']
|
||||||
|
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
color = colors[i % len(colors)]
|
color = colors[i % len(colors)]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user