Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions fileglancer/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@
# Validate path exists and is accessible
absolute_path = os.path.join(expanded_mount_path, path.lstrip('/'))
try:
os.listdir(absolute_path)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a user-provided value.
except FileNotFoundError:
raise ValueError(f"Path {path} does not exist relative to {fsp_name}")
except PermissionError:
Expand Down Expand Up @@ -628,6 +628,10 @@
if username != proxied_path.username:
raise ValueError(f"Proxied path with sharing key {sharing_key} not found for user {username}")

# Merge into current session in case the object came from the cache
# (detached objects from a previous session won't persist changes on commit)
proxied_path = session.merge(proxied_path)

if new_sharing_name:
proxied_path.sharing_name = new_sharing_name

Expand All @@ -649,13 +653,14 @@
return proxied_path


def delete_proxied_path(session: Session, username: str, sharing_key: str):
"""Delete a proxied path"""
session.query(ProxiedPathDB).filter_by(username=username, sharing_key=sharing_key).delete()
def delete_proxied_path(session: Session, username: str, sharing_key: str) -> int:
"""Delete a proxied path. Returns the number of rows deleted."""
deleted = session.query(ProxiedPathDB).filter_by(username=username, sharing_key=sharing_key).delete()
session.commit()

# Remove from cache
_invalidate_sharing_key_cache(sharing_key)
return deleted


def _generate_unique_neuroglancer_key(session: Session) -> str:
Expand Down
7 changes: 7 additions & 0 deletions fileglancer/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ class ProxiedPath(BaseModel):
default=None
)

class UpdateProxiedPathPayload(BaseModel):
"""Payload for updating a proxied path"""
sharing_name: Optional[str] = None
fsp_name: Optional[str] = None
path: Optional[str] = None


class ProxiedPathResponse(BaseModel):
paths: List[ProxiedPath] = Field(
description="A list of proxied paths"
Expand Down
52 changes: 30 additions & 22 deletions fileglancer/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _convert_external_bucket(db_bucket: db.ExternalBucketDB) -> ExternalBucket:
def _convert_proxied_path(db_path: db.ProxiedPathDB, external_proxy_url: Optional[HttpUrl]) -> ProxiedPath:
"""Convert a database ProxiedPathDB model to a Pydantic ProxiedPath model"""
if external_proxy_url:
url = f"{external_proxy_url}/{db_path.sharing_key}/{quote(db_path.sharing_name)}"
url = f"{external_proxy_url}/{db_path.sharing_key}/{quote(os.path.basename(db_path.path))}"
else:
logger.warning(f"No external proxy URL was provided, proxy links will not be available.")
url = None
Expand Down Expand Up @@ -199,29 +199,30 @@ def _get_user_context(username: str) -> UserContext:
return CurrentUserContext()


def _get_file_proxy_client(sharing_key: str, sharing_name: str) -> Tuple[FileProxyClient, UserContext] | Tuple[Response, None]:
def _get_file_proxy_client(sharing_key: str, path_basename: str) -> Tuple[FileProxyClient, UserContext] | Tuple[Response, None]:
with db.get_db_session(settings.db_url) as session:

proxied_path = db.get_proxied_path_by_sharing_key(session, sharing_key)
if not proxied_path:
return get_nosuchbucket_response(sharing_name), None
return get_nosuchbucket_response(path_basename), None

# Vol-E viewer sends URLs with literal % characters (not URL-encoded)
# FastAPI automatically decodes path parameters - % chars are treated as escapes, creating a garbled sharing_name if they're present
# FastAPI automatically decodes path parameters - % chars are treated as escapes, creating a garbled path_basename if they're present
# We therefore need to handle two cases:
# 1. Properly encoded requests (sharing_name matches DB value of proxied_path.sharing_name)
# 2. Vol-E's unencoded requests (unquote(proxied_path.sharing_name) matches the garbled request value)
if proxied_path.sharing_name != sharing_name and unquote(proxied_path.sharing_name) != sharing_name:
return get_error_response(404, "NoSuchKey", f"Sharing name mismatch for sharing key {sharing_key}", sharing_name), None
# 1. Properly encoded requests (path_basename matches os.path.basename of proxied_path.path)
# 2. Vol-E's unencoded requests (unquote of the expected basename matches the garbled request value)
expected_basename = os.path.basename(proxied_path.path)
if expected_basename != path_basename and unquote(expected_basename) != path_basename:
return get_error_response(404, "NoSuchKey", f"Path name mismatch for sharing key {sharing_key}", path_basename), None

fsp = db.get_file_share_path(session, proxied_path.fsp_name)
if not fsp:
return get_error_response(400, "InvalidArgument", f"File share path {proxied_path.fsp_name} not found", sharing_name), None
return get_error_response(400, "InvalidArgument", f"File share path {proxied_path.fsp_name} not found", path_basename), None
# Expand ~ to user's home directory before constructing the mount path
expanded_mount_path = os.path.expanduser(fsp.mount_path)
mount_path = f"{expanded_mount_path}/{proxied_path.path}"
# Use 256KB buffer for better performance on network filesystems
return FileProxyClient(proxy_kwargs={'target_name': sharing_name}, path=mount_path, buffer_size=256*1024), _get_user_context(proxied_path.username)
return FileProxyClient(proxy_kwargs={'target_name': os.path.basename(proxied_path.path)}, path=mount_path, buffer_size=256*1024), _get_user_context(proxied_path.username)


@asynccontextmanager
Expand Down Expand Up @@ -849,9 +850,13 @@ async def delete_neuroglancer_short_link(short_key: str = Path(..., description=
description="Create a new proxied path")
async def create_proxied_path(fsp_name: str = Query(..., description="The name of the file share path that this proxied path is associated with"),
path: str = Query(..., description="The path relative to the file share path mount point"),
sharing_name: Optional[str] = Query(default=None, description="Display name for the data link"),
username: str = Depends(get_current_user)):

sharing_name = os.path.basename(path)
if sharing_name:
sharing_name = sharing_name.strip()
if not sharing_name:
sharing_name = os.path.basename(path)
logger.info(f"Creating proxied path for {username} with sharing name {sharing_name} and fsp_name {fsp_name} and path {path}")
with db.get_db_session(settings.db_url) as session:
with _get_user_context(username): # Necessary to validate the user can access the proxied path
Expand Down Expand Up @@ -891,14 +896,17 @@ async def get_proxied_path(sharing_key: str = Path(..., description="The sharing

@app.put("/api/proxied-path/{sharing_key}", description="Update a proxied path by sharing key")
async def update_proxied_path(sharing_key: str = Path(..., description="The sharing key of the proxied path"),
fsp_name: Optional[str] = Query(default=None, description="The name of the file share path that this proxied path is associated with"),
path: Optional[str] = Query(default=None, description="The path relative to the file share path mount point"),
sharing_name: Optional[str] = Query(default=None, description="The sharing path of the proxied path"),
payload: UpdateProxiedPathPayload = Body(...),
username: str = Depends(get_current_user)):
# Strip sharing_name and reject whitespace-only values
if payload.sharing_name is not None:
payload.sharing_name = payload.sharing_name.strip()
if not payload.sharing_name:
raise HTTPException(status_code=400, detail="sharing_name cannot be empty")
with db.get_db_session(settings.db_url) as session:
with _get_user_context(username): # Necessary to validate the user can access the proxied path
try:
updated = db.update_proxied_path(session, username, sharing_key, new_path=path, new_sharing_name=sharing_name, new_fsp_name=fsp_name)
updated = db.update_proxied_path(session, username, sharing_key, new_path=payload.path, new_sharing_name=payload.sharing_name, new_fsp_name=payload.fsp_name)
return _convert_proxied_path(updated, settings.external_proxy_url)
except ValueError as e:
logger.error(f"Error updating proxied path: {e}")
Expand Down Expand Up @@ -970,11 +978,11 @@ async def get_neuroglancer_short_links(request: Request,
return NeuroglancerShortLinkResponse(links=links)


@app.get("/files/{sharing_key}/{sharing_name}")
@app.get("/files/{sharing_key}/{sharing_name}/{path:path}")
@app.get("/files/{sharing_key}/{path_basename}")
@app.get("/files/{sharing_key}/{path_basename}/{path:path}")
async def target_dispatcher(request: Request,
sharing_key: str,
sharing_name: str,
path_basename: str,
path: str | None = '',
list_type: Optional[int] = Query(None, alias="list-type"),
continuation_token: Optional[str] = Query(None, alias="continuation-token"),
Expand All @@ -988,7 +996,7 @@ async def target_dispatcher(request: Request,
if 'acl' in request.query_params:
return get_read_access_acl()

client, ctx = _get_file_proxy_client(sharing_key, sharing_name)
client, ctx = _get_file_proxy_client(sharing_key, path_basename)
if isinstance(client, Response):
return client

Expand All @@ -1015,10 +1023,10 @@ async def target_dispatcher(request: Request,
return handle


@app.head("/files/{sharing_key}/{sharing_name}/{path:path}")
async def head_object(sharing_key: str, sharing_name: str, path: str):
@app.head("/files/{sharing_key}/{path_basename}/{path:path}")
async def head_object(sharing_key: str, path_basename: str, path: str):
try:
client, ctx = _get_file_proxy_client(sharing_key, sharing_name)
client, ctx = _get_file_proxy_client(sharing_key, path_basename)
if isinstance(client, Response):
return client
with ctx:
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/__tests__/componentTests/DataLink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,45 @@
initialEntries: ['/browse/test_fsp/my_folder/my_zarr']
});

await waitFor(() => {

Check failure on line 48 in frontend/src/__tests__/componentTests/DataLink.test.tsx

View workflow job for this annotation

GitHub Actions / Build

src/__tests__/componentTests/DataLink.test.tsx > Data Link dialog > calls toast.error for a bad HTTP response

TestingLibraryElementError: Unable to find an element with the text: my_zarr. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="overflow: hidden; padding-right: 1024px;" > <div data-floating-ui-inert="" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <span aria-owns=":ri:" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" /> <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> </div> <div data-floating-ui-portal="" id=":ri:" > <div class="fixed inset-0 w-screen h-screen z-[9997] bg-black/50 data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in" data-open="true" style="position: fixed; overflow: auto; top: 0px; right: 0px; bottom: 0px; left: 0px;" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-floating-ui-inert="" data-type="inside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <div class="fixed z-[9998] top-1/2 left-1/2 -translate-x-1/2 max-h-[calc(100vh-32px)] overflow-y-auto -translate-y-1/2 rounded-xl shadow-2xl shadow-black/5 border border-surface data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in data-[open=true]:motion-safe:zoom-in-95 data-[open=true]:motion-safe:slide-in-from-left-1/2 data-[open=true]:motion-safe:slide-in-from-top-1/2 w-10/12 md:w-8/12 lg:w-6/12 h-max p-6 bg-surface-light" data-open="true" id=":rg:" role="dialog" tabindex="-1" > <button class="inline-grid place-items-center border align-middle select-none font-sans font-medium text-center transition-all duration-300 ease-in disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none data-[shape=circular]:rounded-full text-sm min-w-[34px] min-h-[34px] shadow-sm hover:shadow bg-transparent border-secondary hover:bg-secondary absolute right-4 top-4 text-secondary hover:text-background rounded-full" data-shape="default" > <svg aria-hidden="true" class="icon-default" fill="currentColor" height="1em" stroke="currentColor" stroke-width="0" viewBox="0 0 20 20" width="1em" xmlns="http://www.w3.org/2000/svg" > <path clip-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" fill-rule="evenodd" /> </svg> </button> <div class="flex flex-col gap-4 my-4" > <div class="flex flex-col gap-2" > <p class="font-sans antialiased text-base text-foreground font-semibold" > Are you sure you want to create a data link for this path? </p> <p class="antialiased text-foreground text-sm font-mono break-all" /> </div> <p class="font-sans antialiased text-base text-foreground" >

Check failure on line 48 in frontend/src/__tests__/componentTests/DataLink.test.tsx

View workflow job for this annotation

GitHub Actions / Build

src/__tests__/componentTests/DataLink.test.tsx > Data Link dialog > shows validation error when sharing name is empty

TestingLibraryElementError: Unable to find an element with the text: my_zarr. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="overflow: hidden; padding-right: 1024px;" > <div data-floating-ui-inert="" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <span aria-owns=":re:" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" /> <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> </div> <div data-floating-ui-portal="" id=":re:" > <div class="fixed inset-0 w-screen h-screen z-[9997] bg-black/50 data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in" data-open="true" style="position: fixed; overflow: auto; top: 0px; right: 0px; bottom: 0px; left: 0px;" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-floating-ui-inert="" data-type="inside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <div class="fixed z-[9998] top-1/2 left-1/2 -translate-x-1/2 max-h-[calc(100vh-32px)] overflow-y-auto -translate-y-1/2 rounded-xl shadow-2xl shadow-black/5 border border-surface data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in data-[open=true]:motion-safe:zoom-in-95 data-[open=true]:motion-safe:slide-in-from-left-1/2 data-[open=true]:motion-safe:slide-in-from-top-1/2 w-10/12 md:w-8/12 lg:w-6/12 h-max p-6 bg-surface-light" data-open="true" id=":rc:" role="dialog" tabindex="-1" > <button class="inline-grid place-items-center border align-middle select-none font-sans font-medium text-center transition-all duration-300 ease-in disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none data-[shape=circular]:rounded-full text-sm min-w-[34px] min-h-[34px] shadow-sm hover:shadow bg-transparent border-secondary hover:bg-secondary absolute right-4 top-4 text-secondary hover:text-background rounded-full" data-shape="default" > <svg aria-hidden="true" class="icon-default" fill="currentColor" height="1em" stroke="currentColor" stroke-width="0" viewBox="0 0 20 20" width="1em" xmlns="http://www.w3.org/2000/svg" > <path clip-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" fill-rule="evenodd" /> </svg> </button> <div class="flex flex-col gap-4 my-4" > <div class="flex flex-col gap-2" > <p class="font-sans antialiased text-base text-foreground font-semibold" > Are you sure you want to create a data link for this path? </p> <p class="antialiased text-foreground text-sm font-mono break-all" /> </div> <p class="font-sans antialiased text-base text-foreground" >

Check failure on line 48 in frontend/src/__tests__/componentTests/DataLink.test.tsx

View workflow job for this annotation

GitHub Actions / Build

src/__tests__/componentTests/DataLink.test.tsx > Data Link dialog > allows the user to set a custom sharing name

TestingLibraryElementError: Unable to find an element with the text: my_zarr. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="overflow: hidden; padding-right: 1024px;" > <div data-floating-ui-inert="" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <span aria-owns=":ra:" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" /> <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> </div> <div data-floating-ui-portal="" id=":ra:" > <div class="fixed inset-0 w-screen h-screen z-[9997] bg-black/50 data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in" data-open="true" style="position: fixed; overflow: auto; top: 0px; right: 0px; bottom: 0px; left: 0px;" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-floating-ui-inert="" data-type="inside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <div class="fixed z-[9998] top-1/2 left-1/2 -translate-x-1/2 max-h-[calc(100vh-32px)] overflow-y-auto -translate-y-1/2 rounded-xl shadow-2xl shadow-black/5 border border-surface data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in data-[open=true]:motion-safe:zoom-in-95 data-[open=true]:motion-safe:slide-in-from-left-1/2 data-[open=true]:motion-safe:slide-in-from-top-1/2 w-10/12 md:w-8/12 lg:w-6/12 h-max p-6 bg-surface-light" data-open="true" id=":r8:" role="dialog" tabindex="-1" > <button class="inline-grid place-items-center border align-middle select-none font-sans font-medium text-center transition-all duration-300 ease-in disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none data-[shape=circular]:rounded-full text-sm min-w-[34px] min-h-[34px] shadow-sm hover:shadow bg-transparent border-secondary hover:bg-secondary absolute right-4 top-4 text-secondary hover:text-background rounded-full" data-shape="default" > <svg aria-hidden="true" class="icon-default" fill="currentColor" height="1em" stroke="currentColor" stroke-width="0" viewBox="0 0 20 20" width="1em" xmlns="http://www.w3.org/2000/svg" > <path clip-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" fill-rule="evenodd" /> </svg> </button> <div class="flex flex-col gap-4 my-4" > <div class="flex flex-col gap-2" > <p class="font-sans antialiased text-base text-foreground font-semibold" > Are you sure you want to create a data link for this path? </p> <p class="antialiased text-foreground text-sm font-mono break-all" /> </div> <p class="font-sans antialiased text-base text-foreground" >

Check failure on line 48 in frontend/src/__tests__/componentTests/DataLink.test.tsx

View workflow job for this annotation

GitHub Actions / Build

src/__tests__/componentTests/DataLink.test.tsx > Data Link dialog > calls toast.success for an ok HTTP response

TestingLibraryElementError: Unable to find an element with the text: my_zarr. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="overflow: hidden; padding-right: 1024px;" > <div data-floating-ui-inert="" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <span aria-owns=":r6:" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" /> <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> </div> <div data-floating-ui-portal="" id=":r6:" > <div class="fixed inset-0 w-screen h-screen z-[9997] bg-black/50 data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in" data-open="true" style="position: fixed; overflow: auto; top: 0px; right: 0px; bottom: 0px; left: 0px;" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-floating-ui-inert="" data-type="inside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <div class="fixed z-[9998] top-1/2 left-1/2 -translate-x-1/2 max-h-[calc(100vh-32px)] overflow-y-auto -translate-y-1/2 rounded-xl shadow-2xl shadow-black/5 border border-surface data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in data-[open=true]:motion-safe:zoom-in-95 data-[open=true]:motion-safe:slide-in-from-left-1/2 data-[open=true]:motion-safe:slide-in-from-top-1/2 w-10/12 md:w-8/12 lg:w-6/12 h-max p-6 bg-surface-light" data-open="true" id=":r4:" role="dialog" tabindex="-1" > <button class="inline-grid place-items-center border align-middle select-none font-sans font-medium text-center transition-all duration-300 ease-in disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none data-[shape=circular]:rounded-full text-sm min-w-[34px] min-h-[34px] shadow-sm hover:shadow bg-transparent border-secondary hover:bg-secondary absolute right-4 top-4 text-secondary hover:text-background rounded-full" data-shape="default" > <svg aria-hidden="true" class="icon-default" fill="currentColor" height="1em" stroke="currentColor" stroke-width="0" viewBox="0 0 20 20" width="1em" xmlns="http://www.w3.org/2000/svg" > <path clip-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" fill-rule="evenodd" /> </svg> </button> <div class="flex flex-col gap-4 my-4" > <div class="flex flex-col gap-2" > <p class="font-sans antialiased text-base text-foreground font-semibold" > Are you sure you want to create a data link for this path? </p> <p class="antialiased text-foreground text-sm font-mono break-all" /> </div> <p class="font-sans antialiased text-base text-foreground" >

Check failure on line 48 in frontend/src/__tests__/componentTests/DataLink.test.tsx

View workflow job for this annotation

GitHub Actions / Build

src/__tests__/componentTests/DataLink.test.tsx > Data Link dialog > shows the display name input pre-populated with the path basename

TestingLibraryElementError: Unable to find an element with the text: my_zarr. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body style="overflow: hidden; padding-right: 1024px;" > <div data-floating-ui-inert="" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <span aria-owns=":r2:" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" /> <span aria-hidden="true" data-floating-ui-focus-guard="" data-type="outside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> </div> <div data-floating-ui-portal="" id=":r2:" > <div class="fixed inset-0 w-screen h-screen z-[9997] bg-black/50 data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in" data-open="true" style="position: fixed; overflow: auto; top: 0px; right: 0px; bottom: 0px; left: 0px;" > <span aria-hidden="true" data-floating-ui-focus-guard="" data-floating-ui-inert="" data-type="inside" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;" tabindex="0" /> <div class="fixed z-[9998] top-1/2 left-1/2 -translate-x-1/2 max-h-[calc(100vh-32px)] overflow-y-auto -translate-y-1/2 rounded-xl shadow-2xl shadow-black/5 border border-surface data-[open=true]:motion-safe:animate-in data-[open=true]:motion-safe:fade-in data-[open=true]:motion-safe:zoom-in-95 data-[open=true]:motion-safe:slide-in-from-left-1/2 data-[open=true]:motion-safe:slide-in-from-top-1/2 w-10/12 md:w-8/12 lg:w-6/12 h-max p-6 bg-surface-light" data-open="true" id=":r0:" role="dialog" tabindex="-1" > <button class="inline-grid place-items-center border align-middle select-none font-sans font-medium text-center transition-all duration-300 ease-in disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none data-[shape=circular]:rounded-full text-sm min-w-[34px] min-h-[34px] shadow-sm hover:shadow bg-transparent border-secondary hover:bg-secondary absolute right-4 top-4 text-secondary hover:text-background rounded-full" data-shape="default" > <svg aria-hidden="true" class="icon-default" fill="currentColor" height="1em" stroke="currentColor" stroke-width="0" viewBox="0 0 20 20" width="1em" xmlns="http://www.w3.org/2000/svg" > <path clip-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" fill-rule="evenodd" /> </svg> </button> <div class="flex flex-col gap-4 my-4" > <div class="flex flex-col gap-2" > <p class="font-sans antialiased text-base text-foreground font-semibold" > Are you sure you want to create a data link for this path? </p> <p class="antialiased text-foreground text-sm font-mono break-all" /> </div> <p class="font-sans antialiased text-base text-foreground" >
expect(screen.getByText('my_zarr', { exact: false })).toBeInTheDocument();
});
});

it('shows the display name input pre-populated with the path basename', async () => {
await waitFor(() => {
expect(screen.getByDisplayValue('my_zarr')).toBeInTheDocument();
});
const nameInput = screen.getByDisplayValue('my_zarr');
expect(nameInput).toHaveAttribute('type', 'text');
});

it('calls toast.success for an ok HTTP response', async () => {
const user = userEvent.setup();
// Wait for the sharing name input to be populated from async query
await waitFor(() => {
expect(screen.getByDisplayValue('my_zarr')).toBeInTheDocument();
});
await user.click(screen.getByText('Create Data Link'));
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith(
'Data link created successfully'
);
});
});

it('allows the user to set a custom sharing name', async () => {
const user = userEvent.setup();
await waitFor(() => {
expect(screen.getByDisplayValue('my_zarr')).toBeInTheDocument();
});
const nameInput = screen.getByDisplayValue('my_zarr');

await user.clear(nameInput);
await user.type(nameInput, 'My Custom Name');

expect(nameInput).toHaveValue('My Custom Name');

await user.click(screen.getByText('Create Data Link'));
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith(
Expand All @@ -60,6 +92,23 @@
});
});

it('shows validation error when sharing name is empty', async () => {
const user = userEvent.setup();
await waitFor(() => {
expect(screen.getByDisplayValue('my_zarr')).toBeInTheDocument();
});
const nameInput = screen.getByDisplayValue('my_zarr');

await user.clear(nameInput);

await user.click(screen.getByText('Create Data Link'));
await waitFor(() => {
expect(screen.getByText('Nickname cannot be empty')).toBeInTheDocument();
});
// toast.success should NOT have been called
expect(toast.success).not.toHaveBeenCalled();
});

it('calls toast.error for a bad HTTP response', async () => {
// Override the mock for this specific test to simulate an error
const { server } = await import('@/__tests__/mocks/node');
Expand All @@ -75,6 +124,10 @@
);

const user = userEvent.setup();
// Wait for the sharing name input to be populated from async query
await waitFor(() => {
expect(screen.getByDisplayValue('my_zarr')).toBeInTheDocument();
});
await user.click(screen.getByText('Create Data Link'));
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
Expand Down
26 changes: 22 additions & 4 deletions frontend/src/__tests__/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,31 @@ export const handlers = [
return HttpResponse.json({ paths: [] }, { status: 200 });
}),

http.post('/api/proxied-path', () => {
http.post('/api/proxied-path', ({ request }) => {
const url = new URL(request.url);
const sharingName = url.searchParams.get('sharing_name');
const path = url.searchParams.get('path') ?? '/test/path';
const pathBasename = path.split('/').pop() ?? path;
return HttpResponse.json({
username: 'testuser',
sharing_key: 'testkey',
sharing_name: 'testshare',
path: '/test/path',
fsp_name: 'test_fsp',
sharing_name: sharingName ?? pathBasename,
path: path,
fsp_name: url.searchParams.get('fsp_name') ?? 'test_fsp',
created_at: '2025-07-08T15:56:42.588942',
updated_at: '2025-07-08T15:56:42.588942',
url: `http://127.0.0.1:7878/files/testkey/${pathBasename}`
});
}),

http.put('/api/proxied-path/:sharingKey', async ({ params, request }) => {
const body = (await request.json()) as Record<string, string>;
return HttpResponse.json({
username: 'testuser',
sharing_key: params.sharingKey,
sharing_name: body.sharing_name ?? 'testshare',
path: body.path ?? '/test/path',
fsp_name: body.fsp_name ?? 'test_fsp',
created_at: '2025-07-08T15:56:42.588942',
updated_at: '2025-07-08T15:56:42.588942',
url: 'http://127.0.0.1:7878/files/testkey/test/path'
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ui/BrowsePage/N5Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default function N5Preview({
action="create"
onCancel={handleDialogCancel}
onConfirm={handleDialogConfirm}
path={path}
setPendingToolKey={setPendingToolKey}
setShowDataLinkDialog={setShowDataLinkDialog}
showDataLinkDialog={showDataLinkDialog}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ui/BrowsePage/ZarrPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export default function ZarrPreview({
action="create"
onCancel={handleDialogCancel}
onConfirm={handleDialogConfirm}
path={path}
setPendingToolKey={setPendingToolKey}
setShowDataLinkDialog={setShowDataLinkDialog}
showDataLinkDialog={showDataLinkDialog}
Expand Down
Loading
Loading