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
10 changes: 9 additions & 1 deletion components/settings/panel/Delete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,15 @@ async function submitDelete() {
isDeleting.value = true;

try {
await workspacesClient.deleteWorkspace(workspace.id);
const { osmRequestSucceeded: osmCleanupSucceeded } = await workspacesClient.deleteWorkspace(workspace.id);

if (!osmCleanupSucceeded) {
toast.info(
'The workspace has been deleted, but the data may persist for some more time.',
{ clearOnUrlChange: false }
);
}

await navigateTo('/dashboard');
}
catch (error) {
Expand Down
12 changes: 10 additions & 2 deletions services/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export class WorkspacesClientError extends Error {
}
}

export interface DeleteWorkspaceResult {
osmRequestSucceeded: boolean;
}

export class WorkspacesClient extends BaseHttpClient implements ICancelableClient {
#tdeiClient: TdeiClient;
#osmClient: OsmApiClient;
Expand Down Expand Up @@ -241,11 +245,15 @@ export class WorkspacesClient extends BaseHttpClient implements ICancelableClien
);
}

async deleteWorkspace(id: WorkspaceId): Promise<void> {
await Promise.all([
async deleteWorkspace(id: WorkspaceId): Promise<DeleteWorkspaceResult> {
const [workspaceDelete, osmDelete] = await Promise.allSettled([
this._delete(`workspaces/${id}`),
this.#osmClient.deleteWorkspace(id),
]);
if (workspaceDelete.status === 'rejected') {
throw workspaceDelete.reason;
}
return { osmRequestSucceeded: osmDelete.status === 'fulfilled' };
}

async getLongFormQuestSettings(id: WorkspaceId): Promise<QuestSettings> {
Expand Down
31 changes: 31 additions & 0 deletions test/e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ function successToast(page: Page) {
function errorToast(page: Page) {
return page.locator('.Toastify__toast--error');
}
function infoToast(page: Page) {
return page.locator('.Toastify__toast--info');
}

test.describe('workspace settings', () => {
// @test e2e: the workspace can be renamed with the "Workspace Title" field, then clicking "Rename";
Expand Down Expand Up @@ -433,6 +436,34 @@ test.describe('workspace settings', () => {
await expect(page).toHaveURL(/\/dashboard$/);
});

test('shows an info toast when deletion succeeds but OSM cleanup fails', async ({ page }) => {
await seedAuthenticatedSession(page);
await stubSettings(page);

await page.route('**/workspaces/1', async (route) => {
if (route.request().method() === 'DELETE') {
return route.fulfill({ status: 204, body: '' });
}
return route.fallback();
});
await page.route('**/osm/**', (route) => {
return route.fulfill({ status: 403, body: 'Forbidden' });
});

await page.goto('/workspace/1/settings');

const del = page.locator('div.card', { hasText: 'Delete Workspace' });
await del.getByRole('button', { name: /^I understand/ }).click();
await del.getByRole('textbox').fill('delete');
await del.getByRole('button', { name: 'Delete this workspace', exact: true }).click();

await expect(page).toHaveURL(/\/dashboard$/);
await expect(infoToast(page)).toContainText(
'The workspace has been deleted, but the data may persist for some more time.'
);
await expect(errorToast(page)).toHaveCount(0);
});

// @test e2e: validate that all the API calls used on this page match the Swagger spec and that
// success and error states are handled properly with toasts (snapshot these).
test('all API calls conform to the OpenAPI spec', async ({ page }) => {
Expand Down
43 changes: 43 additions & 0 deletions test/unit/services/workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,49 @@ describe('WorkspacesClient.createBlankWorkspace', () => {
});
});

describe('WorkspacesClient.deleteWorkspace', () => {
it('reports failed OSM cleanup after the workspace record is deleted', async () => {
const deleteOsmWorkspace = vi.fn().mockRejectedValue(new Error('Forbidden'));
const client = new WorkspacesClient(
TEST_API_BASE,
TEST_API_BASE,
tdeiClient,
{ deleteWorkspace: deleteOsmWorkspace } as unknown as OsmApiClient
);

server.use(
http.delete(`${TEST_API_BASE}workspaces/1`, () => {
return new HttpResponse(null, { status: 204 });
})
);

await expect(client.deleteWorkspace(1)).resolves.toEqual({
osmRequestSucceeded: false
});
expect(deleteOsmWorkspace).toHaveBeenCalledWith(1);
});

it('still rejects when the workspace record cannot be deleted', async () => {
const deleteOsmWorkspace = vi.fn().mockResolvedValue(undefined);
const client = new WorkspacesClient(
TEST_API_BASE,
TEST_API_BASE,
tdeiClient,
{ deleteWorkspace: deleteOsmWorkspace } as unknown as OsmApiClient
);

server.use(
http.delete(`${TEST_API_BASE}workspaces/1`, () => {
return new HttpResponse(null, { status: 500, statusText: 'Server Error' });
})
);

await expect(client.deleteWorkspace(1)).rejects.toBeInstanceOf(
WorkspacesClientError
);
});
});

describe('WorkspacesClient.checkWorkspaceTitleAvailability', () => {
const LEGACY_API_BASE = 'http://legacy-api.test/';
const NEW_API_BASE = 'http://new-api.test/';
Expand Down
Loading