Skip to content
Open
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
7 changes: 7 additions & 0 deletions web/packages/build/vite/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export function createViteConfig(
secure: false,
ws: true,
},
// /webapi/sites/:site/db/exec - database interactive sessions
[`^\\/v[0-9]+\\/webapi\\/sites\\/${siteName}\\/db\\/exec`]: {
target: `wss://${target}`,
changeOrigin: false,
secure: false,
ws: true,
},
'^\\/v[0-9]+\\/webapi\\/assistant\\/(.*?)': {
target: `https://${target}`,
changeOrigin: false,
Expand Down
173 changes: 125 additions & 48 deletions web/packages/teleport/src/Console/DocumentDb/DocumentDb.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,102 +17,179 @@
*/

import { screen } from '@testing-library/react';
import { MemoryRouter, useLocation } from 'react-router';

import '@testing-library/jest-dom';
import 'jest-canvas-mock';

import { act, render } from 'design/utils/testing';

import { ContextProvider } from 'teleport';
import { TestLayout } from 'teleport/Console/Console.story';
import ConsoleCtx from 'teleport/Console/consoleContext';
import Tty from 'teleport/lib/term/tty';
import ConsoleContextProvider from 'teleport/Console/consoleContextProvider';
import type { DocumentDb as DocumentDbType } from 'teleport/Console/stores';
import { TermEvent } from 'teleport/lib/term/enums';
import { createTeleportContext } from 'teleport/mocks/contexts';
import ResourceService from 'teleport/services/resources';
import type { Session } from 'teleport/services/session';

import { DocumentDb } from './DocumentDb';
import { Status, useDbSession } from './useDbSession';

jest.mock('./useDbSession');

const mockUseDbSession = useDbSession as jest.MockedFunction<
typeof useDbSession
>;

const setup = (status: Status) => {
mockUseDbSession.mockReturnValue({
tty: {
sendDbConnectData: jest.fn(),
on: jest.fn(),
removeListener: jest.fn(),
connect: jest.fn(),
disconnect: jest.fn(),
removeAllListeners: jest.fn(),
} as unknown as Tty,
status,
closeDocument: jest.fn(),
sendDbConnectData: jest.fn(),
session: baseSession,
});

const { ctx, consoleCtx } = getContexts();
// Mock Terminal component to avoid WebGL errors in jsdom
jest.mock('teleport/Console/DocumentSsh/Terminal', () => {
const React = require('react');
return {
Terminal: React.forwardRef((_props: any, ref: any) => {
React.useImperativeHandle(ref, () => ({
focus: jest.fn(),
}));
return <div data-testid="terminal">Terminal Mock</div>;
}),
};
});

const mockDatabase = {
kind: 'db' as const,
name: 'mydb',
protocol: 'postgres' as const,
names: ['test-db'],
users: ['test-user'],
roles: [],
description: '',
type: 'self-hosted',
labels: [],
hostname: 'localhost',
};

beforeEach(() => {
jest
.spyOn(ResourceService.prototype, 'fetchUnifiedResources')
.mockResolvedValue({
agents: [mockDatabase],
startKey: '',
totalCount: 1,
});
});

afterEach(() => {
jest.restoreAllMocks();
});

test('renders terminal window when session is established', async () => {
const { ctx, consoleCtx, tty } = getContexts();

render(
<ContextProvider ctx={ctx}>
<TestLayout ctx={consoleCtx}>
<ConsoleContextProvider value={consoleCtx}>
<DocumentDb doc={baseDoc} visible={true} />
</TestLayout>
</ConsoleContextProvider>
</ContextProvider>
);
};

test('renders loading indicator when status is loading', () => {
jest.useFakeTimers();
setup('loading');
await act(() =>
tty.emit(TermEvent.SESSION, JSON.stringify({ session: { kind: 'db' } }))
);

act(() => jest.runAllTimers());
expect(screen.getByTestId('indicator')).toBeInTheDocument();
expect(screen.getByTestId('terminal')).toBeInTheDocument();
});

test('renders terminal window when status is initialized', () => {
setup('initialized');
test('renders data dialog when status is waiting', async () => {
const { ctx, consoleCtx } = getContexts();

expect(screen.getByTestId('terminal')).toBeInTheDocument();
render(
<ContextProvider ctx={ctx}>
<ConsoleContextProvider value={consoleCtx}>
<DocumentDb doc={baseDoc} visible={true} />
</ConsoleContextProvider>
</ContextProvider>
);

expect(await screen.findByText('Connect To Database')).toBeInTheDocument();
});

test('renders data dialog when status is waiting', () => {
setup('waiting');
test('does not render data dialog when status is initialized', async () => {
const { ctx, consoleCtx, tty } = getContexts();

expect(screen.getByText('Connect To Database')).toBeInTheDocument();
tty.socket = { send: jest.fn() } satisfies Pick<WebSocket, 'send'>;

render(
<ContextProvider ctx={ctx}>
<ConsoleContextProvider value={consoleCtx}>
<DocumentDb doc={baseDoc} visible={true} />
</ConsoleContextProvider>
</ContextProvider>
);

const connectDialog = await screen.findByText('Connect To Database');
expect(connectDialog).toBeInTheDocument();

const connectButton = await screen.findByRole('button', { name: 'Connect' });
await act(async () => {
connectButton.click();
});

expect(screen.queryByText('Connect To Database')).not.toBeInTheDocument();
});

test('does not render data dialog when status is initialized', () => {
setup('initialized');
test('should keep the document at the connect URL after connecting', async () => {
const connectUrl = '/web/cluster/test-cluster/console/db/connect/test-db';
const connectDoc: DocumentDbType = {
kind: 'db' as const,
sid: 'test-session-id',
clusterId: 'test-cluster',
url: connectUrl,
created: new Date(),
name: 'test-db',
};

const { ctx, consoleCtx, tty } = getContexts();

expect(screen.queryByText('Connect to Database')).not.toBeInTheDocument();
render(
<MemoryRouter initialEntries={[connectUrl]}>
<ContextProvider ctx={ctx}>
<ConsoleContextProvider value={consoleCtx}>
<DocumentDb doc={connectDoc} visible={true} />
</ConsoleContextProvider>
</ContextProvider>
<LocationDisplay />
</MemoryRouter>
);

expect(screen.getByTestId('location-display')).toHaveTextContent(connectUrl);

await act(() =>
tty.emit(TermEvent.SESSION, JSON.stringify({ session: { kind: 'db' } }))
);

expect(screen.getByTestId('location-display')).toHaveTextContent(connectUrl);
});

const LocationDisplay = () => {
const location = useLocation();

return <div data-testid="location-display">{location.pathname}</div>;
};

function getContexts() {
const ctx = createTeleportContext();

const consoleCtx = new ConsoleCtx();
const tty = consoleCtx.createTty(baseSession);
tty.connect = () => null;
consoleCtx.createTty = () => tty;
consoleCtx.storeUser = ctx.storeUser;

return { ctx, consoleCtx };
return { ctx, consoleCtx, tty };
}

const baseDoc = {
const baseDoc: DocumentDbType = {
kind: 'db',
sid: 'sid-value',
clusterId: 'clusterId-value',
serverId: 'serverId-value',
login: 'login-value',
url: 'fd',
created: new Date(),
name: 'mydb',
} as const;
};

const baseSession: Session = {
kind: 'db',
Expand Down
5 changes: 0 additions & 5 deletions web/packages/teleport/src/Console/DocumentDb/useDbSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import { context, trace } from '@opentelemetry/api';
import { useEffect, useRef, useState } from 'react';

import cfg from 'teleport/config';
import ConsoleContext from 'teleport/Console/consoleContext';
import { useConsoleContext } from 'teleport/Console/consoleContextProvider';
import { DocumentDb } from 'teleport/Console/stores';
Expand Down Expand Up @@ -115,17 +114,13 @@ function handleTtyConnect(
sid = 'new';
}

const url = cfg.getDbSessionRoute({ clusterId, sid });
const createdDate = new Date(created);

ctx.updateDbDocument(docId, {
url,
created: createdDate,
sid,
clusterId,
});

ctx.gotoTab({ url });
}

export type Status = 'loading' | 'waiting' | 'initialized' | 'disconnected';
5 changes: 0 additions & 5 deletions web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ const cfg = {
kubeExec: '/web/cluster/:clusterId/console/kube/exec/:kubeId/',
kubeExecSession: '/web/cluster/:clusterId/console/kube/session/:sid',
dbConnect: '/web/cluster/:clusterId/console/db/connect/:serviceName',
dbSession: '/web/cluster/:clusterId/console/db/session/:sid',
player: '/web/cluster/:clusterId/session/:sid', // ?recordingType=ssh|desktop|k8s&durationMs=1234
login: '/web/login',
loginSuccess: '/web/msg/info/login_success',
Expand Down Expand Up @@ -829,10 +828,6 @@ const cfg = {
return generatePath(cfg.routes.dbConnect, { ...params });
},

getDbSessionRoute({ clusterId, sid }: UrlParams) {
return generatePath(cfg.routes.dbSession, { clusterId, sid });
},

getKubeExecSessionRoute(
{ clusterId, sid }: UrlParams,
mode?: ParticipantMode
Expand Down
Loading