From 4646d0362502d44eb9c3c0715e3e62103ad1504d Mon Sep 17 00:00:00 2001 From: Diogo Castro Date: Mon, 27 Jul 2026 10:22:44 +0200 Subject: [PATCH 1/3] fix(client): do not hardcode the base path of dav requests We might want to have a space that has a base path different than the normal ones, so hardcoding the value to extract from the dav response was not correct. --- .../src/helpers/resource/functions.ts | 40 +++++++---- .../web-client/src/webdav/listFileVersions.ts | 8 +-- packages/web-client/src/webdav/listFiles.ts | 8 +-- .../unit/helpers/resource/functions.spec.ts | 72 +++++++++++++++++++ 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/packages/web-client/src/helpers/resource/functions.ts b/packages/web-client/src/helpers/resource/functions.ts index 707a09c9952..3dcc01944e1 100644 --- a/packages/web-client/src/helpers/resource/functions.ts +++ b/packages/web-client/src/helpers/resource/functions.ts @@ -86,23 +86,35 @@ const convertObjectToCamelCaseKeys = (data: Record) => { return converted } -export function buildResource(resource: WebDavResponseResource): Resource { - const name = - resource.basename || resource.props[DavProperty.Name]?.toString() || basename(resource.filename) - const id = resource.props[DavProperty.FileId] - - const isFolder = resource.type === 'directory' - let resourcePath: string - - if (resource.filename.startsWith('/files') || resource.filename.startsWith('/space')) { - resourcePath = resource.filename.split('/').slice(3).join('/') - } else { - resourcePath = resource.filename +function deriveResourcePath(filename: string, webDavBasePath?: string): string { + if (webDavBasePath) { + const base = urlJoin(webDavBasePath, { leadingSlash: true, trailingSlash: false }) + if (filename === base) { + return '/' + } + if (filename.startsWith(`${base}/`)) { + return filename.slice(base.length) + } + // supplied base didn't match this resource -> fall through to the legacy heuristic below } + let resourcePath = filename + if (filename.startsWith('/files') || filename.startsWith('/space')) { + resourcePath = filename.split('/').slice(3).join('/') + } if (!resourcePath.startsWith('/')) { resourcePath = `/${resourcePath}` } + return resourcePath +} + +export function buildResource(resource: WebDavResponseResource, webDavBasePath?: string): Resource { + const name = + resource.basename || resource.props[DavProperty.Name]?.toString() || basename(resource.filename) + const id = resource.props[DavProperty.FileId] + + const isFolder = resource.type === 'directory' + const resourcePath = deriveResourcePath(resource.filename, webDavBasePath) const extension = extractExtensionFromFile({ ...resource, id, name, path: resourcePath }) @@ -204,9 +216,7 @@ export function buildResource(resource: WebDavResponseResource): Resource { return this.permissions.indexOf(DavPermission.Shared) >= 0 }, isShareRoot(): boolean { - return resource.props[DavProperty.ShareRoot] - ? resource.filename.split('/').length === 3 - : false + return resource.props[DavProperty.ShareRoot] ? resourcePath === '/' : false }, canDeny: function () { return this.permissions.indexOf(DavPermission.Deny) >= 0 diff --git a/packages/web-client/src/webdav/listFileVersions.ts b/packages/web-client/src/webdav/listFileVersions.ts index ace6f22bd41..bb34240b6a0 100644 --- a/packages/web-client/src/webdav/listFileVersions.ts +++ b/packages/web-client/src/webdav/listFileVersions.ts @@ -6,12 +6,10 @@ import { buildResource } from '../helpers' export const ListFileVersionsFactory = (dav: DAV, options: WebDavOptions) => { return { async listFileVersions(id: string, opts: DAVRequestOptions = {}) { + const webDavPath = urlJoin('meta', id, 'v', { leadingSlash: true }) // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [currentFolder, ...versions] = await dav.propfind( - urlJoin('meta', id, 'v', { leadingSlash: true }), - opts - ) - return versions.map(buildResource) + const [currentFolder, ...versions] = await dav.propfind(webDavPath, opts) + return versions.map((v) => buildResource(v, webDavPath)) } } } diff --git a/packages/web-client/src/webdav/listFiles.ts b/packages/web-client/src/webdav/listFiles.ts index c2a6d26012f..0fc1a6c4922 100644 --- a/packages/web-client/src/webdav/listFiles.ts +++ b/packages/web-client/src/webdav/listFiles.ts @@ -89,10 +89,10 @@ export const ListFilesFactory = ( driveAlias: space.driveAlias, webDavPath: space.webDavPath }), - children: children.map(buildResource) + children: children.map((r) => buildResource(r, space.webDavPath)) } as ListFilesResult } - const resources = webDavResources.map(buildResource) + const resources = webDavResources.map((r) => buildResource(r, space.webDavPath)) return { resource: resources[0], children: resources.slice(1) } as ListFilesResult } @@ -116,11 +116,11 @@ export const ListFilesFactory = ( }) if (isTrash) { return { - resource: buildResource(webDavResources[0]), + resource: buildResource(webDavResources[0], webDavPath), children: webDavResources.slice(1).map(buildDeletedResource) } as ListFilesResult } - const resources = webDavResources.map(buildResource) + const resources = webDavResources.map((r) => buildResource(r, space.webDavPath)) const resourceIsSpace = fileId === space.id if (fileId && !resourceIsSpace && fileId !== resources[0].fileId) { return listFilesCorrectedPath() diff --git a/packages/web-client/tests/unit/helpers/resource/functions.spec.ts b/packages/web-client/tests/unit/helpers/resource/functions.spec.ts index a08bb8d7a76..13d6d8021c9 100644 --- a/packages/web-client/tests/unit/helpers/resource/functions.spec.ts +++ b/packages/web-client/tests/unit/helpers/resource/functions.spec.ts @@ -151,4 +151,76 @@ describe('buildResource', () => { expect(resource.canEditTags()).toBeFalsy() } ) + + describe('path', () => { + it('resolves to "/" when the filename is an exact match of webDavBasePath', () => { + const webDavResponse = mockDeep({ + filename: '/spaces/abc', + basename: 'abc', + props: {} + }) + const resource = buildResource(webDavResponse, '/spaces/abc') + expect(resource.path).toBe('/') + }) + + it('strips webDavBasePath as a prefix when the filename is nested underneath it', () => { + const webDavResponse = mockDeep({ + filename: '/spaces/abc/sub/dir/file.txt', + basename: 'file.txt', + props: {} + }) + const resource = buildResource(webDavResponse, '/spaces/abc') + expect(resource.path).toBe('/sub/dir/file.txt') + }) + + it('falls back to the legacy heuristic when webDavBasePath only shares a string prefix, not a real path segment', () => { + const webDavResponse = mockDeep({ + filename: '/spaces/abcdef/foo', + basename: 'foo', + props: {} + }) + const resource = buildResource(webDavResponse, '/spaces/abc') + // '/spaces/abc' doesn't actually match '/spaces/abcdef/foo' (different space id), so this + // falls through to the legacy heuristic rather than being falsely treated as a prefix match + expect(resource.path).toBe('/foo') + }) + + it('falls back to the legacy /files-or/space heuristic when no webDavBasePath is given', () => { + const webDavResponse = buildMockWebDavResponse({}) + const resource = buildResource(webDavResponse) + expect(resource.path).toBe('/file.txt') + }) + }) + + describe('isShareRoot', () => { + it('is false when the ShareRoot prop is absent', () => { + const webDavResponse = mockDeep({ + filename: '/spaces/abc', + basename: 'abc', + props: { [DavProperty.ShareRoot]: undefined } + }) + const resource = buildResource(webDavResponse, '/spaces/abc') + expect(resource.isShareRoot()).toBeFalsy() + }) + + it('is true when the ShareRoot prop is present and the resource is the root', () => { + const webDavResponse = mockDeep({ + filename: '/spaces/abc', + basename: 'abc', + props: { [DavProperty.ShareRoot]: '/some/remote/path' } + }) + const resource = buildResource(webDavResponse, '/spaces/abc') + expect(resource.isShareRoot()).toBeTruthy() + }) + + it('is false when the ShareRoot prop is present but the resource is a descendant of the root', () => { + const webDavResponse = mockDeep({ + filename: '/spaces/abc/sub', + basename: 'sub', + props: { [DavProperty.ShareRoot]: '/some/remote/path' } + }) + const resource = buildResource(webDavResponse, '/spaces/abc') + expect(resource.isShareRoot()).toBeFalsy() + }) + }) }) From e3b0539ab74f5d6590e23108174ff3cc552f4062 Mon Sep 17 00:00:00 2001 From: Diogo Castro Date: Mon, 27 Jul 2026 11:14:15 +0200 Subject: [PATCH 2/3] feat(files): explorer catch all space If running on EOS, keep a catch all space. Useful for public folders in spaces that are not shared or accessible by the logged in user. This allows access to any path a user has access to, given by the storage acls. Ensure we do not match the EOS space when user lands in personal area. --- packages/web-app-files/src/HandleUpload.ts | 1 + packages/web-app-files/src/index.ts | 12 ++++++ .../src/views/spaces/Projects.vue | 8 ++-- .../tests/unit/HandleUpload.spec.ts | 16 +++++++ .../web-app-files/tests/unit/index.spec.ts | 16 +++++++ .../web-client/src/helpers/space/functions.ts | 27 ++++++++++++ .../web-client/src/helpers/space/types.ts | 1 + .../unit/helpers/space/functions.spec.ts | 31 ++++++++++++- .../src/composables/piniaStores/spaces.ts | 12 +++++- .../composables/piniaStores/spaces.spec.ts | 43 ++++++++++++++++++- 10 files changed, 160 insertions(+), 7 deletions(-) diff --git a/packages/web-app-files/src/HandleUpload.ts b/packages/web-app-files/src/HandleUpload.ts index 50159f83285..abbaf46fef0 100644 --- a/packages/web-app-files/src/HandleUpload.ts +++ b/packages/web-app-files/src/HandleUpload.ts @@ -213,6 +213,7 @@ export class HandleUpload extends BasePlugin { if ( !targetUploadSpace || isShareSpaceResource(targetUploadSpace) || + targetUploadSpace.driveType === 'explorer' || (isPersonalSpaceResource(targetUploadSpace) && !targetUploadSpace.isOwner(this.userStore.user)) ) { diff --git a/packages/web-app-files/src/index.ts b/packages/web-app-files/src/index.ts index 63686978ebc..01d96c6a2bd 100644 --- a/packages/web-app-files/src/index.ts +++ b/packages/web-app-files/src/index.ts @@ -145,6 +145,18 @@ export const navItems = (context: ComponentCustomProperties): AppNavigationItem[ route: { path: `/${appInfo.id}/spaces/projects` }, + isActive: () => { + // the eos explorer space's driveAlias ('eos') is a url-prefix of real eos-backed + // personal/project driveAliases (`eos/user/...`, `eos/project/...`), so activeFor's + // href-prefix match below would otherwise also fire while browsing those unrelated + // spaces. Gate on the actually resolved current space first. + const currentSpace = spacesStores.currentSpace + return ( + !currentSpace || + isProjectSpaceResource(currentSpace) || + currentSpace.driveType === 'explorer' + ) + }, activeFor: () => { const projects = [{ path: `/${appInfo.id}/spaces/project` }] spacesStores.spaces.forEach((drive) => { diff --git a/packages/web-app-files/src/views/spaces/Projects.vue b/packages/web-app-files/src/views/spaces/Projects.vue index 4d26da32526..67bd964581a 100644 --- a/packages/web-app-files/src/views/spaces/Projects.vue +++ b/packages/web-app-files/src/views/spaces/Projects.vue @@ -471,20 +471,20 @@ export default defineComponent({ } const getTotalQuota = (space: SpaceResource) => { - if (space.spaceQuota.total === 0) { + if (space.spaceQuota?.total === 0) { return $gettext('Unrestricted') } - return formatFileSize(space.spaceQuota.total, language.current) + return formatFileSize(space.spaceQuota?.total, language.current) } const getUsedQuota = (space: SpaceResource) => { - if (space.spaceQuota.used === undefined) { + if (space.spaceQuota?.used === undefined) { return '-' } return formatFileSize(space.spaceQuota.used, language.current) } const getRemainingQuota = (space: SpaceResource) => { - if (space.spaceQuota.remaining === undefined) { + if (space.spaceQuota?.remaining === undefined) { return '-' } return formatFileSize(space.spaceQuota.remaining, language.current) diff --git a/packages/web-app-files/tests/unit/HandleUpload.spec.ts b/packages/web-app-files/tests/unit/HandleUpload.spec.ts index c994a914143..735c191efcf 100644 --- a/packages/web-app-files/tests/unit/HandleUpload.spec.ts +++ b/packages/web-app-files/tests/unit/HandleUpload.spec.ts @@ -187,6 +187,22 @@ describe('HandleUpload', () => { ]) expect(result).toBeFalsy() }) + it('does not check quota for explorer spaces', async () => { + const size = 100 + const space = mock({ + driveType: 'explorer', + id: '1' + }) + const { instance } = getWrapper({ spaces: [space] }) + const result = await instance.checkQuotaExceeded([ + mock({ + name: 'name', + meta: { spaceId: '1', routeName: locationSpacesGeneric.name as string }, + data: { size } as Blob + }) + ]) + expect(result).toBeFalsy() + }) it("does not check quota for other's personal spaces", async () => { const size = 100 const remaining = 90 diff --git a/packages/web-app-files/tests/unit/index.spec.ts b/packages/web-app-files/tests/unit/index.spec.ts index ce430f962a2..52590231c4e 100644 --- a/packages/web-app-files/tests/unit/index.spec.ts +++ b/packages/web-app-files/tests/unit/index.spec.ts @@ -30,5 +30,21 @@ describe('Web app files', () => { expect(items[0].isVisible()).toBeFalsy() }) }) + describe('Spaces', () => { + it.each([ + { currentSpace: undefined, expectedResult: true }, + { currentSpace: mock({ driveType: 'project' }), expectedResult: true }, + { currentSpace: mock({ driveType: 'explorer' }), expectedResult: true }, + { currentSpace: mock({ driveType: 'personal' }), expectedResult: false } + ])( + 'is active only for project/explorer spaces, not for other eos-backed spaces like personal', + ({ currentSpace, expectedResult }) => { + const spacesStore = useSpacesStore() + spacesStore.currentSpace = currentSpace + const items = navItems(undefined) + expect(items[4].isActive()).toBe(expectedResult) + } + ) + }) }) }) diff --git a/packages/web-client/src/helpers/space/functions.ts b/packages/web-client/src/helpers/space/functions.ts index 08e7096ed06..35d931c8e44 100644 --- a/packages/web-client/src/helpers/space/functions.ts +++ b/packages/web-client/src/helpers/space/functions.ts @@ -8,6 +8,7 @@ import { ResourceIndicator } from '../resource' import { + EOS_EXPLORER_SPACE_ID, isPersonalSpaceResource, isPublicSpaceResource, PublicSpaceResource, @@ -130,6 +131,32 @@ export function buildShareSpaceResource({ return space } +export function buildWebDavEosExplorerPath(userName: string) { + return urlJoin('files', userName, 'eos', { leadingSlash: true }) +} + +export function buildEosExplorerSpace({ + userName, + serverUrl +}: { + userName: string + serverUrl: string +}): SpaceResource { + const space = buildSpace( + { + id: EOS_EXPLORER_SPACE_ID, + name: 'EOS', + driveType: 'explorer', + driveAlias: 'eos', + webDavPath: buildWebDavEosExplorerPath(userName), + serverUrl + }, + {} + ) + space.mimeType = 'eos' + return space +} + export function buildSpace( data: Drive & { path?: string diff --git a/packages/web-client/src/helpers/space/types.ts b/packages/web-client/src/helpers/space/types.ts index 64a819ecb32..9a067dacfb7 100644 --- a/packages/web-client/src/helpers/space/types.ts +++ b/packages/web-client/src/helpers/space/types.ts @@ -16,6 +16,7 @@ import { Ability, Resource } from '../resource' export const SHARE_JAIL_ID = 'a0ca6a90-a365-4782-871e-d44447bbc668' export const OCM_PROVIDER_ID = '89f37a33-858b-45fa-8890-a1f2b27d90e1' +export const EOS_EXPLORER_SPACE_ID = 'f6b1a2c3-7d4e-4b8a-9c2f-3e5d6a7b8c9d' export type SpaceMember = { grantedTo: SharePointIdentitySet diff --git a/packages/web-client/tests/unit/helpers/space/functions.spec.ts b/packages/web-client/tests/unit/helpers/space/functions.spec.ts index 0a2853a7b70..f21995c698b 100644 --- a/packages/web-client/tests/unit/helpers/space/functions.spec.ts +++ b/packages/web-client/tests/unit/helpers/space/functions.spec.ts @@ -1,4 +1,4 @@ -import { buildSpace } from '../../../../src/helpers/space' +import { buildEosExplorerSpace, buildSpace, EOS_EXPLORER_SPACE_ID } from '../../../../src/helpers/space' import { mock } from 'vitest-mock-extended' import { Ability, GraphSharePermission, ShareRole } from '@ownclouders/web-client' import { Drive, User } from '@ownclouders/web-client/graph/generated' @@ -396,3 +396,32 @@ describe('buildSpace', () => { ) }) }) + +describe('buildEosExplorerSpace', () => { + const userName = 'jdoe' + const serverUrl = 'https://example.com' + + it('builds a synthetic space with the expected identity and webdav path', () => { + const space = buildEosExplorerSpace({ userName, serverUrl }) + expect(space.id).toBe(EOS_EXPLORER_SPACE_ID) + expect(space.driveType).toBe('explorer') + expect(space.driveAlias).toBe('eos') + expect(space.webDavPath).toBe('/files/jdoe/eos') + expect(space.mimeType).toBe('eos') + }) + + it('resolves the webdav url through the legacy per-user files endpoint', () => { + const space = buildEosExplorerSpace({ userName, serverUrl }) + expect(space.getWebDavUrl({ path: '' })).toBe( + 'https://example.com/remote.php/dav/files/jdoe/eos' + ) + }) + + it.each(['canRename', 'canBeDeleted', 'canShare'] as const)( + '%s is false given no user/membership', + (method) => { + const space = buildEosExplorerSpace({ userName, serverUrl }) + expect(space[method]()).toBeFalsy() + } + ) +}) diff --git a/packages/web-pkg/src/composables/piniaStores/spaces.ts b/packages/web-pkg/src/composables/piniaStores/spaces.ts index b444cf865a2..0b9db91f00f 100644 --- a/packages/web-pkg/src/composables/piniaStores/spaces.ts +++ b/packages/web-pkg/src/composables/piniaStores/spaces.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' import { computed, ref, unref } from 'vue' import { + buildEosExplorerSpace, buildShareSpaceResource, isMountPointSpaceResource, SpaceResource @@ -214,7 +215,16 @@ export const useSpacesStore = defineStore('spaces', () => { }) ]) - addSpaces([...personalSpaces, ...projectSpaces]) + const eosExplorerSpaces = configStore.options.runningOnEos + ? [ + buildEosExplorerSpace({ + userName: userStore.user.onPremisesSamAccountName, + serverUrl: configStore.serverUrl + }) + ] + : [] + + addSpaces([...personalSpaces, ...projectSpaces, ...eosExplorerSpaces]) spacesInitialized.value = true } finally { spacesLoading.value = false diff --git a/packages/web-pkg/tests/unit/composables/piniaStores/spaces.spec.ts b/packages/web-pkg/tests/unit/composables/piniaStores/spaces.spec.ts index fa658f5b1f5..16a71135408 100644 --- a/packages/web-pkg/tests/unit/composables/piniaStores/spaces.spec.ts +++ b/packages/web-pkg/tests/unit/composables/piniaStores/spaces.spec.ts @@ -2,12 +2,15 @@ import { getComposableWrapper } from '@ownclouders/web-test-helpers' import { useSpacesStore, sortSpaceMembers, - useSharesStore + useSharesStore, + useConfigStore, + useUserStore } from '../../../../src/composables/piniaStores' import { createPinia, setActivePinia } from 'pinia' import { mock, mockDeep } from 'vitest-mock-extended' import { CollaboratorShare, GraphSharePermission, SpaceResource } from '@ownclouders/web-client' import { Graph } from '@ownclouders/web-client/graph' +import { User } from '@ownclouders/web-client/graph/generated' describe('spaces', () => { beforeEach(() => { @@ -203,6 +206,44 @@ describe('spaces', () => { expect(instance.spaces.length).toBe(2) expect(instance.spacesLoading).toBeFalsy() expect(instance.spacesInitialized).toBeTruthy() + expect(instance.spaces.some((s) => s.driveType === 'explorer')).toBeFalsy() + } + }) + }) + it('does not add an eos explorer space when runningOnEos is disabled', () => { + getWrapper({ + setup: async (instance) => { + const spaces = [mock({ id: '1' })] + const graphClient = mockDeep() + graphClient.drives.listMyDrives.mockResolvedValue(spaces) + const configStore = useConfigStore() + configStore.options.runningOnEos = false + + await instance.loadSpaces({ graphClient }) + + expect(instance.spaces.length).toBe(2) + expect(instance.spaces.some((s) => s.driveType === 'explorer')).toBeFalsy() + } + }) + }) + it('adds an eos explorer space when runningOnEos is enabled', () => { + getWrapper({ + setup: async (instance) => { + const spaces = [mock({ id: '1' })] + const graphClient = mockDeep() + graphClient.drives.listMyDrives.mockResolvedValue(spaces) + const configStore = useConfigStore() + configStore.options.runningOnEos = true + const userStore = useUserStore() + userStore.setUser(mock({ onPremisesSamAccountName: 'jdoe' })) + + await instance.loadSpaces({ graphClient }) + + expect(instance.spaces.length).toBe(3) + const explorerSpace = instance.spaces.find((s) => s.driveType === 'explorer') + expect(explorerSpace).toBeDefined() + expect(explorerSpace.driveAlias).toBe('eos') + expect(explorerSpace.webDavPath).toBe('/files/jdoe/eos') } }) }) From 69d23a73520c544ab49982dde960f6b0efa94c09 Mon Sep 17 00:00:00 2001 From: Diogo Castro Date: Mon, 27 Jul 2026 21:12:02 +0200 Subject: [PATCH 3/3] fix(runtime): fix issues when using browser history Ensure spaces are registered to prevent failures while using the browser history. --- .../src/pages/resolvePublicLink.vue | 6 ++++-- .../tests/unit/pages/resolvePublicLink.spec.ts | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/web-runtime/src/pages/resolvePublicLink.vue b/packages/web-runtime/src/pages/resolvePublicLink.vue index 068167a2172..46a37601a92 100644 --- a/packages/web-runtime/src/pages/resolvePublicLink.vue +++ b/packages/web-runtime/src/pages/resolvePublicLink.vue @@ -216,6 +216,10 @@ export default defineComponent({ } } + // must happen before any early return below (e.g. redirectUrl), otherwise the space + // never gets registered and resolving it again later fails with "resource not found" + spacesStore.upsertSpace(unref(loadedSpace)) + const url = queryItemAsString(unref(redirectUrl)) if (url) { router.push({ path: url }) @@ -244,8 +248,6 @@ export default defineComponent({ path = dirname(unref(item)) } - spacesStore.upsertSpace(unref(loadedSpace)) - const driveAliasAndItem = urlJoin(unref(isOcmLink) ? `ocm/` : `public/`, unref(token), path) const targetLocation: RouteLocationNamedRaw = { name: 'files-public-link', diff --git a/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts b/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts index 31f813125cc..01ecd8f83d4 100644 --- a/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts +++ b/packages/web-runtime/tests/unit/pages/resolvePublicLink.spec.ts @@ -1,7 +1,13 @@ import ResolvePublicLink from '../../../src/pages/resolvePublicLink.vue' import { defaultPlugins, defaultComponentMocks, shallowMount } from '@ownclouders/web-test-helpers' import { mockDeep } from 'vitest-mock-extended' -import { CapabilityStore, ClientService, useRouteParam, useRouteQuery } from '@ownclouders/web-pkg' +import { + CapabilityStore, + ClientService, + useRouteParam, + useRouteQuery, + useSpacesStore +} from '@ownclouders/web-pkg' import { DavHttpError, SpaceResource } from '@ownclouders/web-client' import { authService } from '../../../src/services/auth' import { ref } from 'vue' @@ -95,6 +101,16 @@ describe('resolvePublicLink', () => { ) }) }) + describe('redirect url', () => { + it('registers the resolved space before navigating away, so re-resolving it later still finds it', async () => { + const { wrapper } = getWrapper() + await wrapper.vm.loadPublicSpaceTask.last + await wrapper.vm.resolvePublicLinkTask.last + + const spacesStore = useSpacesStore() + expect(spacesStore.upsertSpace).toHaveBeenCalled() + }) + }) describe('internal link', () => { it('redirects the user to the login page', async () => { const { wrapper, mocks } = getWrapper({ isInternalLink: true })