From ebc3af52126171b89869ce6f09b07e351e7ce1f7 Mon Sep 17 00:00:00 2001 From: Sonia Sandler Date: Sun, 28 Jun 2026 22:03:53 -0400 Subject: [PATCH] chore: add error property to sandbox connection Assisted-by: Claude Sonnet 4.5 Signed-off-by: Sonia Sandler --- src/extension.spec.ts | 95 +++++++++++++++++++++++++++++++++++++++++++ src/extension.ts | 35 ++++++++++++---- 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/extension.spec.ts b/src/extension.spec.ts index 4e95e76..4415cd6 100644 --- a/src/extension.spec.ts +++ b/src/extension.spec.ts @@ -515,3 +515,98 @@ test('push image to sandbox does not change title after it is finished', async ( expect(report).not.toHaveBeenCalledWith(expect.objectContaining({ message: 'data-chunk-1' })); }); + +describe('connection error property', () => { + let config: KubeConfig; + let capturedConnection: podmanDesktopApi.KubernetesProviderConnection | undefined; + let providerMock: podmanDesktopApi.Provider; + let connectionFactory: podmanDesktopApi.KubernetesProviderConnectionFactory; + + beforeEach(async () => { + config = new KubeConfig(); + capturedConnection = undefined; + + vi.mocked(sandbox.getSignUpStatus).mockResolvedValue({ + apiEndpoint: 'https://sandbox-host-url', + username: 'username', + status: { + ready: true, + }, + } as unknown as sandbox.SBSignupResponse); + vi.spyOn(openshift, 'getPipelineServiceAccountToken').mockResolvedValue('token'); + vi.spyOn(kubeconfig, 'createOrLoadFromFile').mockReturnValue(config); + vi.spyOn(kubeconfig, 'exportToFile').mockImplementation(vi.fn()); + + providerMock = { + setKubernetesProviderConnectionFactory: vi.fn(), + registerKubernetesProviderConnection: (connection: podmanDesktopApi.KubernetesProviderConnection) => { + capturedConnection = connection; + return { + dispose: vi.fn(), + }; + }, + } as any as podmanDesktopApi.Provider; + vi.spyOn(podmanDesktopApi.provider, 'createProvider').mockReturnValue(providerMock); + vi.spyOn(podmanDesktopApi.authentication, 'getSession').mockResolvedValue({ + id: '1', + accessToken: 'accessTokenString', + idToken: 'idTokenString', + } as unknown as podmanDesktopApi.AuthenticationSession); + + await extension.activate(context); + connectionFactory = vi.mocked(providerMock.setKubernetesProviderConnectionFactory).mock.calls[0][0]; + }); + + test('connection error property is accessible via getter', async () => { + await connectionFactory.create?.({ + 'redhat.sandbox.context.name': 'test-context', + }); + + expect(capturedConnection).toBeDefined(); + expect(capturedConnection?.error).toBeUndefined(); + }); + + test('connection error is set when token validation fails', async () => { + // Mock got to fail token validation + vi.mocked(got).mockRejectedValue(new Error('Token has expired.')); + + await connectionFactory.create?.({ + 'redhat.sandbox.context.name': 'test-context', + }); + + // Wait for the connection status check to complete + await vi.waitFor(() => { + expect(capturedConnection?.error).toBeDefined(); + }, 3000); + + expect(capturedConnection?.error).toContain('Token has expired'); + }); + + test('connection error is cleared when connection succeeds', async () => { + // First fail, then succeed + vi.mocked(got) + .mockRejectedValueOnce(new Error('Connection failed')) + .mockResolvedValue({ + statusCode: 200, + body: JSON.stringify({ kind: 'User', metadata: { name: 'system:serviceaccount:username-dev:pipeline' } }), + } as any); + + await connectionFactory.create?.({ + 'redhat.sandbox.context.name': 'test-context', + }); + + // Wait for initial error + await vi.waitFor(() => { + expect(capturedConnection?.error).toBeDefined(); + }, 3000); + + expect(capturedConnection?.error).toContain('Connection failed'); + + // Wait for periodic update to clear the error + await vi.waitFor(() => { + expect(capturedConnection?.error).toBeUndefined(); + }, 5000); + + expect(capturedConnection?.status()).toEqual('started'); + }); +}); diff --git a/src/extension.ts b/src/extension.ts index 2c6b90c..ccb39e6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -35,6 +35,7 @@ interface ConnectionData { disposable?: extensionApi.Disposable; connection: extensionApi.KubernetesProviderConnection; status: extensionApi.ProviderConnectionStatus; + error?: string; } const StartedStatus: extensionApi.ProviderConnectionStatus = 'started'; @@ -148,11 +149,9 @@ async function deleteConnectionAndUpdateKubeconfig(contextName: string): Promise } async function registerConnection(contextName: string, apiURL: string, token: string): Promise { - // check if cluster is accessible - // const status = await getConnectionStatus(apiURL, token); - const connection = { + const connection: extensionApi.KubernetesProviderConnection = { name: contextName, - status: () => registeredConnections.get(contextName).status, + status: () => registeredConnections.get(contextName)?.status ?? 'unknown', endpoint: { apiURL, }, @@ -161,10 +160,24 @@ async function registerConnection(contextName: string, apiURL: string, token: st return deleteConnectionAndUpdateKubeconfig(contextName); }, }, + get error() { + return registeredConnections.get(contextName)?.error; + }, }; const connectionData: ConnectionData = { connection, status: UnknownStatus }; registeredConnections.set(contextName, connectionData); connectionData.disposable = provider.registerKubernetesProviderConnection(connection); + + // Check initial connection status + try { + const statusResult = await getConnectionStatus(apiURL, token); + connectionData.status = statusResult.status; + connectionData.error = statusResult.error; + } catch (error) { + console.error('Failed to get initial connection status:', error); + connectionData.error = `Failed to verify connection: ${String(error)}`; + } + return connectionData; } @@ -377,8 +390,9 @@ async function updateConnections(): Promise { // get current token from config file const token = config.getUser(config.getContextObject(contextName).user).token; const connectionData = registeredConnections.get(contextName); - return getConnectionStatus(connectionData.connection.endpoint.apiURL, token).then(status => { - connectionData.status = status; + return getConnectionStatus(connectionData.connection.endpoint.apiURL, token).then(result => { + connectionData.status = result.status; + connectionData.error = result.error; }); }); @@ -398,14 +412,17 @@ async function updateConnections(): Promise { ); } -async function getConnectionStatus(apiURL: string, token: string): Promise { +async function getConnectionStatus( + apiURL: string, + token: string, +): Promise<{ status: extensionApi.ProviderConnectionStatus; error?: string }> { return isTokenValid(apiURL, token) .then(() => { - return StartedStatus; + return { status: StartedStatus }; }) .catch(error => { console.error('Failed to connect to cluster:', error); - return UnknownStatus; + return { status: UnknownStatus, error: String(error) }; }); }