Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent activation of previous workspace when launching Connect via deep link to a different cluster #50063

Merged
merged 23 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
51c37e9
Refactor `setActiveWorkspace` to async/await
gzdunek Dec 10, 2024
f43b0ae
Keep all the logic that restores a single workspace state in `getWork…
gzdunek Dec 10, 2024
dc70bdf
Separate restoring state from setting active workspace
gzdunek Dec 10, 2024
a6bb953
Allow the dialog to reopen documents to be closed without any decision
gzdunek Dec 11, 2024
e941385
Add a test that verifies if the correct workspace is activated
gzdunek Dec 11, 2024
9fc2003
Docs improvements
gzdunek Dec 13, 2024
cc2644a
Return early if there's no restoredWorkspace
gzdunek Dec 13, 2024
3dde323
Fix logger name
gzdunek Dec 13, 2024
8c4dbec
Improve test name
gzdunek Dec 13, 2024
8776788
Make restored state immutable
gzdunek Dec 16, 2024
f5be332
Fix comment
gzdunek Dec 17, 2024
35194aa
Do not wait for functions to be called in tests
gzdunek Dec 17, 2024
706b0f0
Add tests for discarding documents reopen dialog
gzdunek Dec 17, 2024
4bb34e3
Move setting `isInitialized` to a separate method
gzdunek Dec 17, 2024
23f82b8
Remove restored workspace when logging out so that we won't try to re…
gzdunek Dec 17, 2024
26c1dac
Do not try to restore previous documents if the user already opened n…
gzdunek Dec 17, 2024
9e810c1
Do not close dialog in test
gzdunek Dec 20, 2024
0753223
Improve `isInitialized` comment
gzdunek Dec 20, 2024
e275bbb
Call `setActiveWorkspace` again when we fail to change the workspace
gzdunek Dec 20, 2024
e339d09
Fix the logic around restoring previous documents
gzdunek Dec 20, 2024
c20ec38
Try to reopen documents also after `cluster-connect` is canceled
gzdunek Dec 30, 2024
a9bec27
canRestoreDocuments -> hasDocumentsToReopen
gzdunek Dec 30, 2024
aef61d0
Disallow parallel `setActiveWorkspace` calls (#50384)
gzdunek Dec 31, 2024
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
183 changes: 183 additions & 0 deletions web/packages/teleterm/src/ui/AppInitializer/AppInitializer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'jest-canvas-mock';
import { render } from 'design/utils/testing';
import { screen, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
import { ConnectionsContextProvider } from 'teleterm/ui/TopBar/Connections/connectionsContext';
import { VnetContextProvider } from 'teleterm/ui/Vnet';
import Logger, { NullService } from 'teleterm/logger';
import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient';
import { ResourcesContextProvider } from 'teleterm/ui/DocumentCluster/resourcesContext';

import { AppInitializer } from './AppInitializer';

beforeAll(() => {
Logger.init(new NullService());
});

jest.mock('teleterm/ui/ClusterConnect', () => ({
ClusterConnect: props => (
<div
data-testid="mocked-dialog"
data-dialog-kind="cluster-connect"
data-dialog-is-hidden={props.hidden}
>
<button onClick={props.dialog.onSuccess}>Connect to cluster</button>
</div>
),
}));

test('activating a workspace via deep link overrides the previously active workspace', async () => {
// Before closing the app, both clusters were present in the state, with previouslyActiveCluster being active.
// However, the user clicked a deep link pointing to deepLinkCluster.
// The app should prioritize the user's intent by activating the workspace for the deep link,
// rather than reactivating the previously active cluster.
const previouslyActiveCluster = makeRootCluster({
uri: '/clusters/teleport-previously-active',
proxyHost: 'teleport-previously-active:3080',
name: 'teleport-previously-active',
connected: false,
});
const deepLinkCluster = makeRootCluster({
uri: '/clusters/teleport-deep-link',
proxyHost: 'teleport-deep-link:3080',
name: 'teleport-deep-link',
connected: false,
});
const appContext = new MockAppContext();
jest
.spyOn(appContext.statePersistenceService, 'getWorkspacesState')
.mockReturnValue({
rootClusterUri: previouslyActiveCluster.uri,
workspaces: {
[previouslyActiveCluster.uri]: {
localClusterUri: previouslyActiveCluster.uri,
documents: [],
location: undefined,
},
[deepLinkCluster.uri]: {
localClusterUri: deepLinkCluster.uri,
documents: [],
location: undefined,
},
},
});
appContext.mainProcessClient.configService.set(
'usageReporting.enabled',
false
);
jest.spyOn(appContext.tshd, 'listRootClusters').mockReturnValue(
new MockedUnaryCall({
clusters: [deepLinkCluster, previouslyActiveCluster],
})
);
jest.spyOn(appContext.modalsService, 'openRegularDialog');
const userInterfaceReady = withPromiseResolver();
jest
.spyOn(appContext.mainProcessClient, 'signalUserInterfaceReadiness')
.mockImplementation(() => userInterfaceReady.resolve());

render(
<MockAppContextProvider appContext={appContext}>
<ConnectionsContextProvider>
<VnetContextProvider>
<ResourcesContextProvider>
<AppInitializer />
</ResourcesContextProvider>
</VnetContextProvider>
</ConnectionsContextProvider>
</MockAppContextProvider>
);

// Wait for the app to finish initialization.
await act(() => userInterfaceReady.promise);
// Launch a deep link and do not wait for the result.
act(() => {
void appContext.deepLinksService.launchDeepLink({
status: 'success',
url: {
host: deepLinkCluster.proxyHost,
hostname: deepLinkCluster.name,
port: '1234',
pathname: '/authenticate_web_device',
username: deepLinkCluster.loggedInUser.name,
searchParams: {
id: '123',
redirect_uri: '',
token: 'abc',
},
},
});
});

// The cluster-connect dialog should be opened two times.
// The first one comes from restoring the previous session, but it is
// immediately canceled and replaced with a dialog to the cluster from
// the deep link.
await waitFor(
gzdunek marked this conversation as resolved.
Show resolved Hide resolved
() => {
expect(appContext.modalsService.openRegularDialog).toHaveBeenCalledTimes(
2
);
},
// A small timeout to prevent potential race conditions.
{ timeout: 10 }
);
expect(appContext.modalsService.openRegularDialog).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
kind: 'cluster-connect',
clusterUri: previouslyActiveCluster.uri,
})
);
expect(appContext.modalsService.openRegularDialog).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
kind: 'cluster-connect',
clusterUri: deepLinkCluster.uri,
})
);

// We blindly confirm the current cluster-connect dialog.
const dialogSuccessButton = await screen.findByRole('button', {
name: 'Connect to cluster',
});
await userEvent.click(dialogSuccessButton);

// Check if the first activated workspace is the one from the deep link.
expect(await screen.findByTitle(/Current cluster:/)).toBeVisible();
expect(
screen.queryByTitle(`Current cluster: ${deepLinkCluster.name}`)
).toBeVisible();
});

//TODO(gzdunek): Replace with Promise.withResolvers after upgrading to Node.js 22.
function withPromiseResolver() {
let resolver: () => void;
const promise = new Promise<void>(resolve => (resolver = resolve));
return {
resolve: resolver,
promise,
};
}
2 changes: 1 addition & 1 deletion web/packages/teleterm/src/ui/StatusBar/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function StatusBar() {
css={`
white-space: nowrap;
`}
title={clusterBreadcrumbs}
title={clusterBreadcrumbs && `Current cluster: ${clusterBreadcrumbs}`}
>
{clusterBreadcrumbs}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface WorkspacesState {
export interface Workspace {
localClusterUri: ClusterUri;
documents: Document[];
location: DocumentUri;
location: DocumentUri | undefined;
accessRequests: {
isBarCollapsed: boolean;
pending: PendingAccessRequest;
Expand Down
Loading