fix: Add authentication to various endpoints (#10977)

* fix: Add authentication to various endpoints

* [autofix.ci] apply automated fixes

* Couple more endpoints

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Update log_router.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Update mcp.py

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Fix ruff errors

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Update test_endpoints.py

* Fix tests

* Update Nvidia Remix.json

* Update test_registration.py

* [autofix.ci] apply automated fixes

* Update test_files.py

Co-Authored-By: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* Address review comments

Co-Authored-By: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* Review updates

Co-Authored-By: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Eric Hare
2025-12-11 19:36:29 -08:00
committed by GitHub
parent 9d57aa8f99
commit 3fed9fe1b5
16 changed files with 195 additions and 70 deletions

View File

@ -3,10 +3,12 @@ import json
from http import HTTPStatus
from typing import Annotated, Any
from fastapi import APIRouter, HTTPException, Query, Request
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
from lfx.log.logger import log_buffer
from langflow.services.auth.utils import get_current_active_user
log_router = APIRouter(tags=["Log"])
@ -50,12 +52,13 @@ async def event_generator(request: Request):
await asyncio.sleep(1)
@log_router.get("/logs-stream")
@log_router.get("/logs-stream", dependencies=[Depends(get_current_active_user)])
async def stream_logs(
request: Request,
):
"""HTTP/2 Server-Sent-Event (SSE) endpoint for streaming logs.
Requires authentication to prevent exposure of sensitive log data.
It establishes a long-lived connection to the server and receives log messages in real-time.
The client should use the header "Accept: text/event-stream".
"""
@ -69,12 +72,16 @@ async def stream_logs(
return StreamingResponse(event_generator(request), media_type="text/event-stream")
@log_router.get("/logs")
@log_router.get("/logs", dependencies=[Depends(get_current_active_user)])
async def logs(
lines_before: Annotated[int, Query(description="The number of logs before the timestamp or the last log")] = 0,
lines_after: Annotated[int, Query(description="The number of logs after the timestamp")] = 0,
timestamp: Annotated[int, Query(description="The timestamp to start getting logs from")] = 0,
):
"""Retrieve application logs with authentication required.
SECURITY: Logs may contain sensitive information and require authentication.
"""
global log_buffer # noqa: PLW0602
if log_buffer.enabled() is False:
raise HTTPException(

View File

@ -37,6 +37,7 @@ from langflow.api.v1.schemas import (
VerticesOrderResponse,
)
from langflow.exceptions.component import ComponentBuildError
from langflow.services.auth.utils import get_current_active_user
from langflow.services.chat.service import ChatService
from langflow.services.database.models.flow.model import Flow
from langflow.services.deps import (
@ -54,7 +55,7 @@ if TYPE_CHECKING:
router = APIRouter(tags=["Chat"])
@router.post("/build/{flow_id}/vertices", deprecated=True)
@router.post("/build/{flow_id}/vertices", deprecated=True, dependencies=[Depends(get_current_active_user)])
async def retrieve_vertices_order(
*,
flow_id: uuid.UUID,
@ -197,14 +198,17 @@ async def build_flow(
)
@router.get("/build/{job_id}/events")
@router.get("/build/{job_id}/events", dependencies=[Depends(get_current_active_user)])
async def get_build_events(
job_id: str,
queue_service: Annotated[JobQueueService, Depends(get_queue_service)],
*,
event_delivery: EventDeliveryType = EventDeliveryType.STREAMING,
):
"""Get events for a specific build job."""
"""Get events for a specific build job.
Requires authentication to prevent unauthorized access to build events.
"""
return await get_flow_events_response(
job_id=job_id,
queue_service=queue_service,
@ -212,12 +216,19 @@ async def get_build_events(
)
@router.post("/build/{job_id}/cancel", response_model=CancelFlowResponse)
@router.post(
"/build/{job_id}/cancel",
response_model=CancelFlowResponse,
dependencies=[Depends(get_current_active_user)],
)
async def cancel_build(
job_id: str,
queue_service: Annotated[JobQueueService, Depends(get_queue_service)],
):
"""Cancel a specific build job."""
"""Cancel a specific build job.
Requires authentication to prevent unauthorized build cancellation.
"""
try:
# Cancel the flow build and check if it was successful
cancellation_success = await cancel_flow_build(job_id=job_id, queue_service=queue_service)
@ -509,6 +520,7 @@ async def _stream_vertex(flow_id: str, vertex_id: str, chat_service: ChatService
"/build/{flow_id}/{vertex_id}/stream",
response_class=StreamingResponse,
deprecated=True,
dependencies=[Depends(get_current_active_user)],
)
async def build_vertex_stream(
flow_id: uuid.UUID,

View File

@ -961,10 +961,12 @@ async def custom_component_update(
raise SerializationError.from_exception(exc, data=component_node) from exc
@router.get("/config")
@router.get("/config", dependencies=[Depends(get_current_active_user)])
async def get_config() -> ConfigResponse:
"""Retrieve the current application configuration settings.
Requires authentication to prevent exposure of sensitive configuration details.
Returns:
ConfigResponse: The configuration settings of the application.

View File

@ -21,6 +21,41 @@ from langflow.services.storage.service import StorageService
router = APIRouter(tags=["Files"], prefix="/files")
def _get_allowed_profile_picture_folders(settings_service: SettingsService) -> set[str]:
"""Return the set of allowed profile picture folders.
This enumerates subdirectories under the profile_pictures directory in both
the user's config_dir and the package's bundled assets. This makes the API
flexible (users may add new folders under config_dir/profile_pictures) while
still safe because we only ever serve files contained within the resolved
base directory and validate path containment below.
If no directories can be found (unexpected), fall back to the curated
defaults {"People", "Space"} shipped with Langflow.
"""
allowed: set[str] = set()
try:
# Config-provided folders
config_dir = Path(settings_service.settings.config_dir)
cfg_base = config_dir / "profile_pictures"
if cfg_base.exists():
allowed.update({p.name for p in cfg_base.iterdir() if p.is_dir()})
# Package-provided folders
from langflow.initial_setup import setup
pkg_base = Path(setup.__file__).parent / "profile_pictures"
if pkg_base.exists():
allowed.update({p.name for p in pkg_base.iterdir() if p.is_dir()})
except Exception as _:
import logging
logger = logging.getLogger(__name__)
logger.exception("Exception occurred while getting allowed profile picture folders")
# Sensible defaults ensure tests and OOTB behavior
return allowed or {"People", "Space"}
# Create dep that gets the flow_id from the request
# then finds it in the database and returns it while
# using the current user as the owner
@ -31,10 +66,9 @@ async def get_flow(
):
# AttributeError: 'SelectOfScalar' object has no attribute 'first'
flow = await session.get(Flow, flow_id)
if not flow:
# Return 404 for both non-existent flows and unauthorized access to prevent information disclosure
if not flow or flow.user_id != current_user.id:
raise HTTPException(status_code=404, detail="Flow not found")
if flow.user_id != current_user.id:
raise HTTPException(status_code=403, detail="You don't have access to this flow")
return flow
@ -43,7 +77,6 @@ async def upload_file(
*,
file: UploadFile,
flow: Annotated[Flow, Depends(get_flow)],
current_user: CurrentActiveUser,
storage_service: Annotated[StorageService, Depends(get_storage_service)],
settings_service: Annotated[SettingsService, Depends(get_settings_service)],
) -> UploadFileResponse:
@ -57,9 +90,7 @@ async def upload_file(
status_code=413, detail=f"File size is larger than the maximum file size {max_file_size_upload}MB."
)
if flow.user_id != current_user.id:
raise HTTPException(status_code=403, detail="You don't have access to this flow")
# Authorization handled by get_flow dependency
try:
file_content = await file.read()
timestamp = datetime.now(tz=timezone.utc).astimezone().strftime("%Y-%m-%d_%H-%M-%S")
@ -74,9 +105,12 @@ async def upload_file(
@router.get("/download/{flow_id}/{file_name}")
async def download_file(
file_name: str, flow_id: UUID, storage_service: Annotated[StorageService, Depends(get_storage_service)]
file_name: str,
flow: Annotated[Flow, Depends(get_flow)],
storage_service: Annotated[StorageService, Depends(get_storage_service)],
):
flow_id_str = str(flow_id)
# Authorization handled by get_flow dependency
flow_id_str = str(flow.id)
extension = file_name.split(".")[-1]
if not extension:
@ -102,10 +136,14 @@ async def download_file(
@router.get("/images/{flow_id}/{file_name}")
async def download_image(file_name: str, flow_id: UUID):
async def download_image(
file_name: str,
flow: Annotated[Flow, Depends(get_flow)],
):
# Authorization handled by get_flow dependency
storage_service = get_storage_service()
extension = file_name.split(".")[-1]
flow_id_str = str(flow_id)
flow_id_str = str(flow.id)
if not extension:
raise HTTPException(status_code=500, detail=f"Extension not found for file {file_name}")
@ -138,16 +176,49 @@ async def download_profile_picture(
then fallback to the package's bundled profile_pictures directory.
"""
try:
# SECURITY: Validate inputs to prevent path traversal attacks
# Reject any path components that contain directory traversal sequences
if ".." in folder_name or ".." in file_name:
raise HTTPException(
status_code=400, detail="Path traversal patterns ('..') are not allowed in folder or file names"
)
# Only allow specific folder names (dynamic from config + package)
allowed_folders = _get_allowed_profile_picture_folders(settings_service)
if folder_name not in allowed_folders:
raise HTTPException(status_code=400, detail=f"Folder must be one of: {', '.join(sorted(allowed_folders))}")
# Validate file name contains no path separators
if "/" in file_name or "\\" in file_name:
raise HTTPException(status_code=400, detail="File name cannot contain path separators ('/' or '\\')")
extension = file_name.split(".")[-1]
config_dir = settings_service.settings.config_dir
config_path = Path(config_dir) # type: ignore[arg-type]
file_path = config_path / "profile_pictures" / folder_name / file_name
config_path = Path(config_dir).resolve() # type: ignore[arg-type]
# Construct the file path
file_path = (config_path / "profile_pictures" / folder_name / file_name).resolve()
# SECURITY: Verify the resolved path is still within the allowed directory
# This prevents path traversal even if symbolic links are involved
allowed_base = (config_path / "profile_pictures").resolve()
if not str(file_path).startswith(str(allowed_base)):
# Return 404 to prevent path traversal attempts from revealing system structure
raise HTTPException(status_code=404, detail="Profile picture not found")
# Fallback to package bundled profile pictures if not found in config_dir
if not file_path.exists():
from langflow.initial_setup import setup
package_path = Path(setup.__file__).parent / "profile_pictures" / folder_name / file_name
package_base = Path(setup.__file__).parent / "profile_pictures"
package_path = (package_base / folder_name / file_name).resolve()
# SECURITY: Verify package path is also within allowed directory
allowed_package_base = package_base.resolve()
if not str(package_path).startswith(str(allowed_package_base)):
# Return 404 to prevent path traversal attempts from revealing system structure
raise HTTPException(status_code=404, detail="Profile picture not found")
if package_path.exists():
file_path = package_path
else:
@ -177,29 +248,30 @@ async def list_profile_pictures(
config_dir = settings_service.settings.config_dir
config_path = Path(config_dir) # type: ignore[arg-type]
people_path = config_path / "profile_pictures" / "People"
space_path = config_path / "profile_pictures" / "Space"
# Build list for all allowed folders (dynamic)
allowed_folders = _get_allowed_profile_picture_folders(settings_service)
# List files directly from local filesystem
people = [f.name for f in people_path.iterdir() if f.is_file()] if people_path.exists() else []
space = [f.name for f in space_path.iterdir() if f.is_file()] if space_path.exists() else []
results: list[str] = []
cfg_base = config_path / "profile_pictures"
if cfg_base.exists():
for folder in sorted(allowed_folders):
p = cfg_base / folder
if p.exists():
results += [f"{folder}/{f.name}" for f in p.iterdir() if f.is_file()]
# Fallback to package bundled profile pictures if config_dir is empty
if not people and not space:
# Fallback to package if config_dir produced no results
if not results:
from langflow.initial_setup import setup
package_base = Path(setup.__file__).parent / "profile_pictures"
people_path = package_base / "People"
space_path = package_base / "Space"
people = [f.name for f in people_path.iterdir() if f.is_file()] if people_path.exists() else []
space = [f.name for f in space_path.iterdir() if f.is_file()] if space_path.exists() else []
for folder in sorted(allowed_folders):
p = package_base / folder
if p.exists():
results += [f"{folder}/{f.name}" for f in p.iterdir() if f.is_file()]
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
files = [f"People/{i}" for i in people]
files += [f"Space/{i}" for i in space]
return {"files": files}
return {"files": results}
@router.get("/list/{flow_id}")

View File

@ -4,7 +4,7 @@ import json
import logging
from typing import Annotated
from fastapi import APIRouter, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from lfx.base.models.unified_models import (
get_model_provider_variable_mapping,
get_model_providers,
@ -13,6 +13,7 @@ from lfx.base.models.unified_models import (
from pydantic import BaseModel, field_validator
from langflow.api.utils import CurrentActiveUser, DbSession
from langflow.services.auth.utils import get_current_active_user
from langflow.services.deps import get_variable_service
from langflow.services.variable.constants import CREDENTIAL_TYPE, GENERIC_TYPE
from langflow.services.variable.service import DatabaseVariableService
@ -93,7 +94,7 @@ class ModelStatusUpdate(BaseModel):
return v.strip()
@router.get("/providers", status_code=200)
@router.get("/providers", status_code=200, dependencies=[Depends(get_current_active_user)])
async def list_model_providers() -> list[str]:
"""Return available model providers."""
return get_model_providers()

View File

@ -22,7 +22,7 @@ from langflow.services.database.models.vertex_builds.model import VertexBuildMap
router = APIRouter(prefix="/monitor", tags=["Monitor"])
@router.get("/builds")
@router.get("/builds", dependencies=[Depends(get_current_active_user)])
async def get_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSession) -> VertexBuildMapModel:
try:
vertex_builds = await get_vertex_builds_by_flow_id(session, flow_id)
@ -31,7 +31,7 @@ async def get_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSessio
raise HTTPException(status_code=500, detail=str(e)) from e
@router.delete("/builds", status_code=204)
@router.delete("/builds", status_code=204, dependencies=[Depends(get_current_active_user)])
async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSession) -> None:
try:
await delete_vertex_builds_by_flow_id(session, flow_id)

View File

@ -26,8 +26,12 @@ router = APIRouter(tags=["Users"], prefix="/users")
async def add_user(
user: UserCreate,
session: DbSession,
current_user: Annotated[User, Depends(get_current_active_superuser)], # noqa: ARG001
) -> User:
"""Add a new user to the database."""
"""Add a new user to the database.
Requires superuser authentication to prevent unauthorized account creation.
"""
new_user = User.model_validate(user, from_attributes=True)
try:
new_user.password = get_password_hash(user.password)

View File

@ -1,17 +1,17 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, Depends, HTTPException
from lfx.base.prompts.api_utils import process_prompt_template
from lfx.custom.validate import validate_code
from lfx.log.logger import logger
from langflow.api.utils import CurrentActiveUser
from langflow.api.v1.base import Code, CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest
from langflow.services.auth.utils import get_current_active_user
# build router
router = APIRouter(prefix="/validate", tags=["Validate"])
@router.post("/code", status_code=200)
async def post_validate_code(code: Code, _current_user: CurrentActiveUser) -> CodeValidationResponse:
@router.post("/code", status_code=200, dependencies=[Depends(get_current_active_user)])
async def post_validate_code(code: Code) -> CodeValidationResponse:
try:
errors = validate_code(code.code)
return CodeValidationResponse(
@ -23,8 +23,10 @@ async def post_validate_code(code: Code, _current_user: CurrentActiveUser) -> Co
raise HTTPException(status_code=500, detail=str(e)) from e
@router.post("/prompt", status_code=200)
async def post_validate_prompt(prompt_request: ValidatePromptRequest) -> PromptValidationResponse:
@router.post("/prompt", status_code=200, dependencies=[Depends(get_current_active_user)])
async def post_validate_prompt(
prompt_request: ValidatePromptRequest,
) -> PromptValidationResponse:
try:
if not prompt_request.frontend_node:
return PromptValidationResponse(

View File

@ -98,7 +98,8 @@ async def fetch_file_object(file_id: uuid.UUID, current_user: CurrentActiveUser,
# Make sure the user has access to the file
if file.user_id != current_user.id:
raise HTTPException(status_code=403, detail="You don't have access to this file")
# Return 404 to prevent information disclosure about resource existence
raise HTTPException(status_code=404, detail="File not found")
return file
@ -225,8 +226,8 @@ async def upload_user_file(
# S3 bucket doesn't exist or file not found, or file was uploaded but can't be found
raise HTTPException(status_code=404, detail=str(e)) from e
except PermissionError as e:
# Access denied or invalid credentials
raise HTTPException(status_code=403, detail=str(e)) from e
# Access denied or invalid credentials - return 500 as this is a server config issue
raise HTTPException(status_code=500, detail="Error accessing storage") from e
except Exception as e:
# General error saving file or getting file size
raise HTTPException(status_code=500, detail=f"Error accessing file: {e}") from e

View File

@ -3,10 +3,11 @@ from asyncio import to_thread
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr
from langflow.logging import logger
from langflow.services.auth.utils import get_current_active_user
from langflow.services.deps import get_telemetry_service
from langflow.services.telemetry.schema import EmailPayload
@ -135,7 +136,7 @@ async def _send_email_telemetry(email: str) -> None:
logger.debug(f"Successfully sent email telemetry event: {payload.email}")
@router.get("/")
@router.get("/", dependencies=[Depends(get_current_active_user)])
async def get_registration():
"""Get the registered user (if any)."""
try:

View File

@ -21,8 +21,8 @@ async def test_get_version(client: AsyncClient):
assert "package" in result, "The dictionary must contain a key called 'package'"
async def test_get_config(client: AsyncClient):
response = await client.get("api/v1/config")
async def test_get_config(client: AsyncClient, logged_in_headers: dict):
response = await client.get("api/v1/config", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK

View File

@ -576,15 +576,17 @@ async def test_download_profile_picture_not_found_in_both_locations(empty_config
async def test_download_profile_picture_invalid_folder(empty_config_dir, files_client): # noqa: ARG001
"""Test 404 when using an invalid folder name.
"""Test 400 when using an invalid folder name.
Only 'People' and 'Space' folders should exist in the package.
Only 'People' and 'Space' folders are whitelisted for security.
Invalid folder names should be rejected with 400 Bad Request.
"""
response = await files_client.get("api/v1/files/profile_pictures/InvalidFolder/file.svg")
assert response.status_code == 404
assert response.status_code == 400
data = response.json()
assert "not found" in data["detail"].lower()
# Check for the new specific error message
assert "folder must be one of" in data["detail"].lower()
async def test_download_profile_picture_config_dir_takes_precedence(setup_profile_pictures, files_client):

View File

@ -2,9 +2,9 @@ from fastapi import status
from httpx import AsyncClient
async def test_add_user(client: AsyncClient):
async def test_add_user(client: AsyncClient, logged_in_headers_super_user):
basic_case = {"username": "string", "password": "string"}
response = await client.post("api/v1/users/", json=basic_case)
response = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@ -49,7 +49,7 @@ async def test_patch_user(client: AsyncClient, logged_in_headers_super_user):
name = "string"
updated_name = "string2"
basic_case = {"username": name, "password": "string"}
response_ = await client.post("api/v1/users/", json=basic_case)
response_ = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
id_ = response_.json()["id"]
basic_case["username"] = updated_name
response = await client.patch(f"api/v1/users/{id_}", json=basic_case, headers=logged_in_headers_super_user)
@ -88,7 +88,7 @@ async def test_reset_password(client: AsyncClient, logged_in_headers, active_use
async def test_delete_user(client: AsyncClient, logged_in_headers_super_user):
basic_case = {"username": "string", "password": "string"}
response_ = await client.post("api/v1/users/", json=basic_case)
response_ = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
id_ = response_.json()["id"]
response = await client.delete(f"api/v1/users/{id_}", headers=logged_in_headers_super_user)
result = response.json()

View File

@ -251,9 +251,14 @@ class TestAPIEndpoints:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from langflow.services.auth.utils import get_current_active_user
app = FastAPI()
app.include_router(router)
# Override auth dependency to bypass authentication
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
client = TestClient(app)
response = client.get("/registration/") # Fixed: Added /registration prefix
@ -271,9 +276,14 @@ class TestAPIEndpoints:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from langflow.services.auth.utils import get_current_active_user
app = FastAPI()
app.include_router(router)
# Override auth dependency to bypass authentication
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
client = TestClient(app)
response = client.get("/registration/") # Fixed: Added /registration prefix
@ -289,9 +299,14 @@ class TestAPIEndpoints:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from langflow.services.auth.utils import get_current_active_user
app = FastAPI()
app.include_router(router)
# Override auth dependency to bypass authentication
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
client = TestClient(app)
response = client.get("/registration/") # Fixed: Added /registration prefix
@ -328,6 +343,7 @@ class TestIntegration:
"""Test API endpoints with actual file operations."""
from fastapi import FastAPI
from fastapi.testclient import TestClient
from langflow.services.auth.utils import get_current_active_user
# Set up temporary file path
test_file = tmp_path / "registration.json"
@ -335,6 +351,10 @@ class TestIntegration:
app = FastAPI()
app.include_router(router)
# Override auth dependency to bypass authentication
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
client = TestClient(app)
# Test registration

View File

@ -231,18 +231,19 @@ What is a good name for a company that makes {product}?
INVALID_PROMPT = "This is an invalid prompt without any input variable."
async def test_valid_prompt(client: AsyncClient):
async def test_valid_prompt(client: AsyncClient, logged_in_headers):
PROMPT_REQUEST["template"] = VALID_PROMPT
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST)
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["input_variables"] == ["product"]
async def test_invalid_prompt(client: AsyncClient):
async def test_invalid_prompt(client: AsyncClient, logged_in_headers):
PROMPT_REQUEST["template"] = INVALID_PROMPT
response = await client.post(
"api/v1/validate/prompt",
json=PROMPT_REQUEST,
headers=logged_in_headers,
)
assert response.status_code == 200
assert response.json()["input_variables"] == []
@ -257,9 +258,9 @@ async def test_invalid_prompt(client: AsyncClient):
("{a}, {b}, and {c} are variables.", ["a", "b", "c"]),
],
)
async def test_various_prompts(client, prompt, expected_input_variables):
async def test_various_prompts(client, logged_in_headers, prompt, expected_input_variables):
PROMPT_REQUEST["template"] = prompt
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST)
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["input_variables"] == expected_input_variables

File diff suppressed because one or more lines are too long