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
84 changes: 70 additions & 14 deletions e2e/trino/connection-form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ function interceptSaveSuccess(page: import('@playwright/test').Page) {
valid: true,
posted: true,
errors: {},
data: { connectionUrl: 'http://localhost:8080', authType: 'none' },
data: {
connectionUrl: 'http://localhost:8080',
authType: 'none',
authUsername: '',
authPassword: ''
},
message: { type: 'success' }
};
await route.fulfill({
Expand Down Expand Up @@ -90,7 +95,7 @@ test.describe('Connection form (env-configured)', () => {
await page.goto('/trino');
await waitForHydration(page);

await expect(page.getByText('Connection', { exact: true })).not.toBeVisible();
await expect(page.getByText('Edit connection', { exact: true })).not.toBeVisible();
await expect(page.locator('input[type="url"]')).not.toBeVisible();
});
});
Expand All @@ -117,25 +122,27 @@ test.describe('Connection form (manual mode)', () => {
await page.goto('/trino');
await waitForHydration(page);

await expect(page.getByText('Connection', { exact: true })).toBeVisible();
await expect(page.getByText('Edit connection', { exact: true })).toBeVisible();
});

test('expanding the form reveals URL input', async ({ page }) => {
test('form is expanded initially when there is no connection', async ({ page }) => {
await page.goto('/trino');
await waitForHydration(page);

// Click the collapse toggle to open the form.
await page.getByLabel('Connection').check();

await expect(page.getByLabel('Edit connection')).toBeChecked();
await expect(page.getByLabel('URL')).toBeVisible();
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();

// The collapse toggle still hides the form.
await page.getByLabel('Edit connection').uncheck();
await expect(page.getByLabel('URL')).not.toBeVisible();
});

test('auth type toggle shows credential fields for basic auth', async ({ page }) => {
await page.goto('/trino');
await waitForHydration(page);

await page.getByLabel('Connection').check();
await page.getByLabel('Edit connection').check();

// Initially no credential fields (auth type defaults to "none").
await expect(page.getByLabel('Username')).not.toBeVisible();
Expand All @@ -152,7 +159,7 @@ test.describe('Connection form (manual mode)', () => {
await page.goto('/trino');
await waitForHydration(page);

await page.getByLabel('Connection').check();
await page.getByLabel('Edit connection').check();

// Switch to basic, then back to no auth.
await page.getByRole('radio', { name: 'Basic' }).click();
Expand All @@ -167,7 +174,7 @@ test.describe('Connection form (manual mode)', () => {
await page.goto('/trino');
await waitForHydration(page);

await page.getByLabel('Connection').check();
await page.getByLabel('Edit connection').check();

// Submit with empty URL.
await page.getByRole('button', { name: 'Save' }).click();
Expand All @@ -176,11 +183,25 @@ test.describe('Connection form (manual mode)', () => {
await expect(page.locator('.text-error')).toBeVisible();
});

test('invalid URL shows a translated validation message', async ({ page }) => {
await page.goto('/trino');
await waitForHydration(page);

await page.getByLabel('Edit connection').check();

await page.getByLabel('URL').fill('not a url');
await page.getByRole('button', { name: 'Save' }).click();

await expect(
page.getByText('Enter a valid URL, e.g. https://trino.example.com:8443.')
).toBeVisible();
});

test('basic auth requires username and password', async ({ page }) => {
await page.goto('/trino');
await waitForHydration(page);

await page.getByLabel('Connection').check();
await page.getByLabel('Edit connection').check();

// Fill URL but leave credentials empty with basic auth selected.
await page.getByLabel('URL').fill('http://localhost:8080');
Expand All @@ -197,7 +218,7 @@ test.describe('Connection form (manual mode)', () => {
await page.goto('/trino');
await waitForHydration(page);

await page.getByLabel('Connection').check();
await page.getByLabel('Edit connection').check();

const testUrl = 'http://localhost:8080';
await page.getByLabel('URL').fill(testUrl);
Expand All @@ -217,7 +238,7 @@ test.describe('Connection form (manual mode)', () => {
await page.goto('/trino');
await waitForHydration(page);

await page.getByLabel('Connection').check();
await page.getByLabel('Edit connection').check();

// The URL field should be pre-filled from localStorage.
await expect(page.getByLabel('URL')).toHaveValue('http://localhost:8080');
Expand All @@ -236,6 +257,41 @@ test.describe('Connection form (manual mode)', () => {
await expect(page.getByText('localhost:8080')).toBeVisible();
});

test('failed reconnect of a stored connection opens the form with the error', async ({
page
}) => {
await page.addInitScript(() => {
localStorage.setItem('trino_url', 'http://localhost:8080');
localStorage.setItem('trino_auth_type', 'none');
});

// The server is env-configured, so the automatic reconnect is rejected.
await page.goto('/trino');
await waitForHydration(page);

await expect(
page.getByText('The connection is managed via environment variables.')
).toBeVisible();
await expect(page.getByLabel('URL')).toHaveValue('http://localhost:8080');
});

test('network error during reconnect opens the form with the error', async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem('trino_url', 'http://localhost:8080');
localStorage.setItem('trino_auth_type', 'none');
});
await page.route(
(url) => url.pathname === '/trino' && url.search === '?/save',
(route) => route.abort()
);

await page.goto('/trino');
await waitForHydration(page);

await expect(page.getByText('Could not save the connection. Please try again.')).toBeVisible();
await expect(page.getByLabel('URL')).toHaveValue('http://localhost:8080');
});

test('saving a connection clears previous query results', async ({ page }) => {
await setTabSql(page, 'SELECT id, name FROM users');
interceptSaveSuccess(page);
Expand All @@ -249,7 +305,7 @@ test.describe('Connection form (manual mode)', () => {
await waitForQueryComplete(page);
await expect(page.getByRole('table', { name: 'Query results' })).toBeVisible();

await page.getByLabel('Connection').check();
await page.getByLabel('Edit connection').check();
await page.getByLabel('URL').fill('http://localhost:8080');
await page.getByRole('button', { name: 'Save' }).click();

Expand Down
9 changes: 8 additions & 1 deletion messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"trino_prev_page": "Vorherige Seite",
"trino_next_page": "Nächste Seite",
"trino_last_page": "Letzte Seite",
"trino_connection_label": "Verbindung",
"trino_connection_label": "Verbindung bearbeiten",
"trino_connection_error": "Bitte konfigurieren Sie eine gültige Verbindungs-URL.",
"trino_connection_url": "URL",
"trino_connection_auth": "Authentifizierung",
Expand All @@ -68,6 +68,13 @@
"trino_auth_password": "Passwort",
"trino_save_connection": "Speichern",
"trino_connection_saved": "Verbindung gespeichert.",
"trino_connection_url_invalid": "Bitte eine gültige URL eingeben, z. B. https://trino.example.com:8443.",
"trino_auth_username_required": "Für Basic-Authentifizierung ist ein Benutzername erforderlich",
"trino_auth_password_required": "Für Basic-Authentifizierung ist ein Passwort erforderlich",
"trino_connection_test_timeout": "Zeitüberschreitung beim Verbindungstest",
"trino_connection_test_failed": "Verbindung zu Trino fehlgeschlagen – bitte URL und Zugangsdaten prüfen.",
"trino_connection_save_failed": "Die Verbindung konnte nicht gespeichert werden. Bitte erneut versuchen.",
"trino_connection_env_managed": "Die Verbindung wird über Umgebungsvariablen verwaltet.",
"trino_cancel_query": "Abbrechen",
"trino_state_submitting": "Wird eingereicht",
"trino_state_queued": "Warteschlange",
Expand Down
9 changes: 8 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"trino_prev_page": "Previous page",
"trino_next_page": "Next page",
"trino_last_page": "Last page",
"trino_connection_label": "Connection",
"trino_connection_label": "Edit connection",
"trino_connection_error": "Please configure a valid connection URL.",
"trino_connection_url": "URL",
"trino_connection_auth": "Auth",
Expand All @@ -68,6 +68,13 @@
"trino_auth_password": "Password",
"trino_save_connection": "Save",
"trino_connection_saved": "Connection saved.",
"trino_connection_url_invalid": "Enter a valid URL, e.g. https://trino.example.com:8443.",
"trino_auth_username_required": "Username is required for basic authentication",
"trino_auth_password_required": "Password is required for basic authentication",
"trino_connection_test_timeout": "Connection test timed out",
"trino_connection_test_failed": "Could not connect to Trino — check the URL and credentials.",
"trino_connection_save_failed": "Could not save the connection. Please try again.",
"trino_connection_env_managed": "The connection is managed via environment variables.",
"trino_cancel_query": "Cancel",
"trino_state_submitting": "Submitting",
"trino_state_queued": "Queued",
Expand Down
16 changes: 11 additions & 5 deletions src/routes/(app)/trino/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '$lib/server/trino/user-clients.js';
import { ConnectionSchema, type ConnectionMessage } from './validation.js';
import type { Actions, PageServerLoad } from './$types';
import * as m from '$lib/paraglide/messages.js';

export const load: PageServerLoad = async ({ locals }) => {
locals.logger.debug('loading Trino page');
Expand All @@ -30,12 +31,17 @@ export const actions: Actions = {
save: async ({ request, locals }) => {
const log = locals.logger;

const form = await superValidate(request, zod(ConnectionSchema));

if (trinoConfigured) {
return fail(400, { error: 'Connection is managed via environment variables' });
log.debug('connection save rejected, Trino is configured via environment variables');
return message(
form,
{ type: 'error', message: m.trino_connection_env_managed() } satisfies ConnectionMessage,
{ status: 400 }
);
}

const form = await superValidate(request, zod(ConnectionSchema));

if (!form.valid) {
log.debug({ errors: form.errors }, 'connection form validation failed');
return fail(400, { form });
Expand Down Expand Up @@ -64,8 +70,8 @@ export const actions: Actions = {
const name = (err as { name?: string })?.name;
const reason =
name === 'TimeoutError' || name === 'AbortError'
? 'Connection test timed out'
: 'Could not connect to Trino — check the URL and credentials.';
? m.trino_connection_test_timeout()
: m.trino_connection_test_failed();
log.info({ err, trino_url: connectionUrl }, 'connection test failed');
return message(form, { type: 'error', message: reason } satisfies ConnectionMessage, {
status: 400
Expand Down
Loading
Loading