Skip to content
Merged
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
13 changes: 12 additions & 1 deletion packages/web-pkg/src/components/AppTemplates/AppWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import { DavPermission } from '@opencloud-eu/web-client/webdav'
import { HttpError } from '@opencloud-eu/web-client'
import { dirname } from 'path'
import { useFileActionsOpenWithApp } from '../../composables'
import { ResourceNotFoundError } from '../../errors'
import { UnsavedChangesModal } from '../Modals'
import {
decodeTextContent,
Expand Down Expand Up @@ -175,7 +176,10 @@ const loading = ref(!unref(noResourceLoading))
const loadingError: Ref<Error> = ref()
// apps that load their files themselves (e.g. the preview) report errors via the folder loading
const error = computed(() => unref(loadingError) || unref(folderLoadingError))
const isNotFoundError = computed(() => (unref(error) as HttpError)?.statusCode === 404)
const isNotFoundError = computed(() => {
const e = unref(error)
return e instanceof ResourceNotFoundError || (e as HttpError)?.statusCode === 404
})
const isReadOnly = ref(false)
/** The loaded text is not valid UTF-8, see `getFileContentsAsText`. */
const isLossyContent = ref(false)
Expand Down Expand Up @@ -420,9 +424,16 @@ const addMissingDriveAliasAndItem = async () => {
const loadResourceTask = useTask(function* (signal) {
try {
if (!unref(driveAliasAndItem)) {
if (!unref(fileId)) {
throw new ResourceNotFoundError('No file given')
}
yield addMissingDriveAliasAndItem()
}
space.value = unref(unref(currentFileContext).space)
if (!unref(space)) {
// the space is unknown or not accessible, like the server answers for such resources
throw new ResourceNotFoundError('Space not found')
}
const currentSpace = unref(space)
if (isPublicSpaceResource(currentSpace) && currentSpace.publicLinkPermission === undefined) {
// the link permission and file id are only known if the link got resolved beforehand,
Expand Down
7 changes: 7 additions & 0 deletions packages/web-pkg/src/errors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ export class RuntimeError extends Error {
export class ApiError extends RuntimeError {
name = 'ApiError'
}

/**
* error for resources that can't be found without the server telling so, e.g. an unknown space
*/
export class ResourceNotFoundError extends RuntimeError {
name = 'ResourceNotFoundError'
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { YjsSession, YjsStatus } from '../../../../src/composables/yjs'
import type { FileContentOptions, FileContext } from '../../../../src/composables/appDefaults'
import type { FileAction } from '../../../../src/composables/actions'
import { eventBus } from '../../../../src/services'
import { ResourceNotFoundError } from '../../../../src/errors'
import {
type CustomComponentExtension,
useExtensionRegistry,
Expand Down Expand Up @@ -126,6 +127,7 @@ function setup({
params: { driveAliasAndItem: 'personal/alan/a.md' }
}),
isGuestContext = false,
fileContext = mock<FileContext>({ space: mock<any>(), path: '/a.md' }),
fileContentOptions = undefined as FileContentOptions,
// Receives the errors Vue would otherwise report as unhandled.
errorHandler = undefined as (err: unknown) => void
Expand All @@ -144,7 +146,7 @@ function setup({
isConflicted.value = true
})
const statusRef = ref(status)
const currentFileContext = ref(mock<FileContext>({ space: mock<any>(), path: '/a.md' }))
const currentFileContext = ref(fileContext)
// Deferred so the test controls when each half of the load completes.
let resolveInfo: (r: Resource) => void
let rejectInfo: (e: Error) => void
Expand Down Expand Up @@ -286,6 +288,7 @@ function setup({
markConflicted,
putFileContents,
closeApp,
getFileInfo,
getFileContents,
unregisterAppBarExtension,
wasWrittenByRoom,
Expand Down Expand Up @@ -436,6 +439,8 @@ describe('AppWrapper — missing drive alias', () => {
const s = setup({
yjsEnabled: false,
currentRoute: route as unknown as RouteLocation,
// the drive resolver only finds the space once the route has the drive alias
fileContext: { space: undefined, path: '' } as FileContext,
errorHandler
})
// Like the router: the new route changes the file context, which restarts
Expand All @@ -456,6 +461,55 @@ describe('AppWrapper — missing drive alias', () => {
})
})

describe('AppWrapper — space that cannot be resolved', () => {
// e.g. the space of another user, whose alias the drive resolver doesn't know
const unresolvedFileContext = { space: undefined, path: '' } as FileContext

// no mock: it would hand out a file id for any query item
function routeWithoutFileId(driveAliasAndItem: string) {
return {
path: '/test-app',
query: {},
params: { driveAliasAndItem }
} as unknown as RouteLocation
}

function expectNotFoundMessage(s: ReturnType<typeof setup>) {
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(ResourceNotFoundError))
expect(s.wrapper.find('#app-wrapper-not-found-message').exists()).toBe(true)
expect(s.wrapper.findComponent({ name: 'ErrorScreen' }).exists()).toBe(false)
}

it('shows the not found message instead of loading the file without a space', async () => {
const s = setup({
yjsEnabled: false,
fileContext: unresolvedFileContext,
currentRoute: routeWithoutFileId('project/doesnotexist/x.md')
})
await flushPromises()

expect(s.getFileInfo).not.toHaveBeenCalled()
expect(getResourceContextSpy).not.toHaveBeenCalled()
expectNotFoundMessage(s)
})

it('shows the not found message for the app route without a file', async () => {
const errorHandler = vi.fn()
const s = setup({
yjsEnabled: false,
fileContext: unresolvedFileContext,
currentRoute: routeWithoutFileId(''),
errorHandler
})
await flushPromises()

expect(getResourceContextSpy).not.toHaveBeenCalled()
expect(s.getFileInfo).not.toHaveBeenCalled()
expect(errorHandler).not.toHaveBeenCalled()
expectNotFoundMessage(s)
})
})

describe('AppWrapper — closing the app while the file is loading', () => {
it('stops loading without an error', async () => {
const errorHandler = vi.fn()
Expand Down
Loading