fix(integration test): fix drive test used bytes with grace period
This commit is contained in:
@@ -1,12 +1,46 @@
|
||||
import { it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
||||
|
||||
const { session, ui } = vi.hoisted(() => ({
|
||||
const { session, ui, pageState } = vi.hoisted(() => ({
|
||||
session: { user: { id: '1', username: 'admin', role: 'admin' } },
|
||||
ui: { notify: vi.fn() }
|
||||
ui: { notify: vi.fn() },
|
||||
// Mock of SvelteKit's `$app/state` `page` — post-URL-routing
|
||||
// the admin page reads `page.params.tab` to derive which
|
||||
// section to render. Tests set the tab via `setTab(...)`
|
||||
// BEFORE `render(AdminPage)`; the derived picks it up on
|
||||
// initial mount. Previously the tab was chosen by clicking a
|
||||
// horizontal-tab button that no longer exists.
|
||||
pageState: {
|
||||
page: {
|
||||
url: new URL('http://localhost/admin'),
|
||||
params: {} as Record<string, string | undefined>,
|
||||
route: { id: '/admin/[[tab]]' },
|
||||
status: 200,
|
||||
error: null,
|
||||
data: {},
|
||||
form: null,
|
||||
state: {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
vi.mock('$lib/stores/session.svelte', () => ({ session }));
|
||||
vi.mock('$lib/stores/ui.svelte', () => ({ ui }));
|
||||
vi.mock('$app/state', () => pageState);
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
vi.mock('$app/paths', () => ({ base: '', resolve: (r: string) => r }));
|
||||
|
||||
/**
|
||||
* Set the current tab BEFORE calling `render(AdminPage)`. The
|
||||
* admin page reads the URL-derived tab in a `$derived`, which
|
||||
* captures the value at first render — mutating this mock later
|
||||
* doesn't retrigger. Tests that exercise multiple tabs render
|
||||
* once per tab (each in a fresh `render` call — @testing-library
|
||||
* unmounts between tests via its `beforeEach` cleanup).
|
||||
*/
|
||||
function setTab(tab: string | undefined) {
|
||||
pageState.page.params = tab ? { tab } : {};
|
||||
pageState.page.url = new URL(`http://localhost/admin${tab ? '/' + tab : ''}`);
|
||||
}
|
||||
vi.mock('$lib/api/endpoints/admin', () => ({
|
||||
clearPluginLogs: vi.fn(),
|
||||
createExternalMount: vi.fn(),
|
||||
@@ -87,6 +121,10 @@ const mount = {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset the tab mock so a test that sets `setTab('users')`
|
||||
// doesn't leak into the next test (default = /admin →
|
||||
// dashboard).
|
||||
setTab(undefined);
|
||||
m(admin.getDashboard).mockResolvedValue(dashboard);
|
||||
m(admin.listUsers).mockResolvedValue({ total: 1, users: [user] });
|
||||
m(admin.listPlugins).mockResolvedValue({ available: true, enabled: true, plugins: [] });
|
||||
@@ -136,8 +174,8 @@ it('toggles registration from the dashboard', async () => {
|
||||
|
||||
it('loads users when the users tab is opened and creates a user', async () => {
|
||||
m(admin.createUser).mockResolvedValue(undefined);
|
||||
setTab('users');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-users-tab'));
|
||||
await waitFor(() => expect(admin.listUsers).toHaveBeenCalled());
|
||||
await fireEvent.click(await screen.findByTestId('admin-users-create-btn'));
|
||||
await fireEvent.input(await screen.findByTestId('admin-create-user-username-input'), {
|
||||
@@ -151,33 +189,33 @@ it('loads users when the users tab is opened and creates a user', async () => {
|
||||
});
|
||||
|
||||
it('loads OIDC settings when the OIDC tab is opened', async () => {
|
||||
setTab('oidc');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-oidc-tab'));
|
||||
await waitFor(() => expect(admin.getOidcSettings).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('loads storage + migration when the storage tab is opened', async () => {
|
||||
setTab('storage');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-storage-tab'));
|
||||
await waitFor(() => expect(admin.getStorageSettings).toHaveBeenCalled());
|
||||
await waitFor(() => expect(admin.getMigration).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('loads SMTP info when the SMTP tab is opened', async () => {
|
||||
setTab('smtp');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-smtp-tab'));
|
||||
await waitFor(() => expect(admin.getSmtpInfo).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('loads plugins when the plugins tab is opened', async () => {
|
||||
setTab('plugins');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-plugins-tab'));
|
||||
await waitFor(() => expect(admin.listPlugins).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('loads external mounts when the mounts tab is opened and lists them', async () => {
|
||||
setTab('mounts');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-mounts-tab'));
|
||||
await waitFor(() => expect(admin.listExternalMounts).toHaveBeenCalled());
|
||||
// The configured mount is rendered in the table.
|
||||
expect(await screen.findByText('Media')).toBeTruthy();
|
||||
@@ -194,8 +232,8 @@ it('creates a mount from the mounts form', async () => {
|
||||
mount_path: 'Personal/Photos',
|
||||
config: { path: '/srv/photos', read_only: false }
|
||||
});
|
||||
setTab('mounts');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-mounts-tab'));
|
||||
await fireEvent.input(await screen.findByTestId('mount-name'), {
|
||||
target: { value: 'Photos' }
|
||||
});
|
||||
@@ -212,8 +250,8 @@ it('creates a mount from the mounts form', async () => {
|
||||
|
||||
it('deletes a mount through the confirm modal', async () => {
|
||||
m(admin.deleteExternalMount).mockResolvedValue(undefined);
|
||||
setTab('mounts');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-mounts-tab'));
|
||||
await fireEvent.click(await screen.findByTestId('mount-delete'));
|
||||
// deleteMount() gates on the styled confirm modal.
|
||||
await fireEvent.click(await screen.findByTestId('admin-confirm-ok-btn'));
|
||||
@@ -222,8 +260,8 @@ it('deletes a mount through the confirm modal', async () => {
|
||||
|
||||
it("toggles a user's role through the confirm modal", async () => {
|
||||
m(admin.setUserRole).mockResolvedValue(undefined);
|
||||
setTab('users');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-users-tab'));
|
||||
await fireEvent.click(await screen.findByTestId('admin-user-toggle-role-u1'));
|
||||
await fireEvent.click(await screen.findByTestId('admin-confirm-ok-btn'));
|
||||
await waitFor(() => expect(admin.setUserRole).toHaveBeenCalledWith('u1', 'admin'));
|
||||
@@ -231,8 +269,8 @@ it("toggles a user's role through the confirm modal", async () => {
|
||||
|
||||
it('deactivates a user through the confirm modal', async () => {
|
||||
m(admin.setUserActive).mockResolvedValue(undefined);
|
||||
setTab('users');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-users-tab'));
|
||||
await fireEvent.click(await screen.findByTestId('admin-user-toggle-active-u1'));
|
||||
await fireEvent.click(await screen.findByTestId('admin-confirm-ok-btn'));
|
||||
await waitFor(() => expect(admin.setUserActive).toHaveBeenCalledWith('u1', false));
|
||||
@@ -240,8 +278,8 @@ it('deactivates a user through the confirm modal', async () => {
|
||||
|
||||
it('saves OIDC settings from the OIDC form', async () => {
|
||||
m(admin.saveOidc).mockResolvedValue(undefined);
|
||||
setTab('oidc');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-oidc-tab'));
|
||||
await fireEvent.input(await screen.findByTestId('admin-oidc-issuer-input'), {
|
||||
target: { value: 'https://idp.test' }
|
||||
});
|
||||
@@ -251,8 +289,8 @@ it('saves OIDC settings from the OIDC form', async () => {
|
||||
|
||||
it('sends an SMTP test email', async () => {
|
||||
m(admin.sendSmtpTest).mockResolvedValue({ ok: true } as never);
|
||||
setTab('smtp');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-smtp-tab'));
|
||||
await fireEvent.input(await screen.findByTestId('admin-smtp-to-input'), {
|
||||
target: { value: 'to@x.test' }
|
||||
});
|
||||
@@ -263,8 +301,8 @@ it('sends an SMTP test email', async () => {
|
||||
it('saves storage settings and starts a migration', async () => {
|
||||
m(admin.saveStorage).mockResolvedValue(undefined);
|
||||
m(admin.migrationAction).mockResolvedValue(undefined);
|
||||
setTab('storage');
|
||||
render(AdminPage);
|
||||
await fireEvent.click(await screen.findByTestId('admin-storage-tab'));
|
||||
await fireEvent.submit(await screen.findByTestId('admin-storage-form'));
|
||||
await waitFor(() => expect(admin.saveStorage).toHaveBeenCalled());
|
||||
await fireEvent.click(await screen.findByTestId('admin-migration-start-btn'));
|
||||
|
||||
@@ -410,12 +410,24 @@ mod integration_tests {
|
||||
// 5. Set the artificially-wrong cached used_bytes. LAST, so
|
||||
// no INSERT-side trigger overwrites our fake (there is no
|
||||
// such trigger today, but ordering is cheap insurance).
|
||||
sqlx::query("UPDATE storage.drives SET used_bytes = $1 WHERE id = $2")
|
||||
.bind(cached)
|
||||
.bind(drive_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("set fake used_bytes");
|
||||
// Also backdate `created_at` past the tenant's 1-hour
|
||||
// grace window (`created_at < NOW() - INTERVAL '1 hour'`
|
||||
// in the SQL) — freshly-seeded fixtures are by definition
|
||||
// younger than that window and would otherwise be
|
||||
// silently skipped by the scan, leaving `scanned_count`
|
||||
// at 0 and the drift-detection assertions with nothing
|
||||
// to compare against.
|
||||
sqlx::query(
|
||||
"UPDATE storage.drives \
|
||||
SET used_bytes = $1, \
|
||||
created_at = NOW() - INTERVAL '2 hours' \
|
||||
WHERE id = $2",
|
||||
)
|
||||
.bind(cached)
|
||||
.bind(drive_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("set fake used_bytes + backdate");
|
||||
|
||||
drive_id
|
||||
}
|
||||
|
||||
+31
-39
@@ -15,44 +15,47 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test('walk every admin tab', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await expect(page.getByTestId('admin-dashboard-tab')).toBeVisible({ timeout: 15_000 });
|
||||
// Dashboard is the default tab.
|
||||
// Dashboard is the default section on `/admin` (bare path).
|
||||
// Assert on the sidebar entry (now the source of admin navigation)
|
||||
// and on content that only renders when Dashboard is active.
|
||||
await expect(page.getByTestId('appshell-nav-admin-dashboard-link')).toBeVisible({
|
||||
timeout: 15_000
|
||||
});
|
||||
await expect(page.getByTestId('admin-dashboard-registration-checkbox')).toBeVisible();
|
||||
|
||||
await page.getByTestId('admin-users-tab').click();
|
||||
await page.goto('/admin/users');
|
||||
await expect(page.getByTestId('admin-users-create-btn')).toBeVisible();
|
||||
|
||||
await page.getByTestId('admin-oidc-tab').click();
|
||||
await page.goto('/admin/oidc');
|
||||
await expect(page.getByTestId('admin-oidc-form')).toBeVisible();
|
||||
|
||||
await page.getByTestId('admin-storage-tab').click();
|
||||
await page.goto('/admin/storage');
|
||||
await expect(page.getByTestId('admin-storage-form')).toBeVisible();
|
||||
|
||||
await page.getByTestId('admin-smtp-tab').click();
|
||||
await page.goto('/admin/smtp');
|
||||
await expect(page.getByTestId('admin-smtp-send-btn')).toBeVisible();
|
||||
|
||||
await page.getByTestId('admin-plugins-tab').click();
|
||||
// Plugins panel content is conditional; assert the tab became active.
|
||||
await expect(page.getByTestId('admin-plugins-tab')).toHaveAttribute('aria-selected', 'true');
|
||||
await page.goto('/admin/plugins');
|
||||
// Plugins panel content is conditional; assert the URL landed
|
||||
// on the plugins section (proves routing worked; no content
|
||||
// guarantee).
|
||||
await expect(page).toHaveURL(/\/admin\/plugins$/);
|
||||
});
|
||||
|
||||
test('open the create-user form', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-users-tab').click();
|
||||
await page.goto('/admin/users');
|
||||
await page.getByTestId('admin-users-create-btn').click();
|
||||
await expect(page.getByTestId('admin-create-user-form')).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('storage tab: change backend select', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-storage-tab').click();
|
||||
await page.goto('/admin/storage');
|
||||
await expect(page.getByTestId('admin-storage-form')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId('admin-storage-backend-select').selectOption({ index: 1 }).catch(() => {});
|
||||
});
|
||||
|
||||
test('storage tab: save the local backend settings', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-storage-tab').click();
|
||||
await page.goto('/admin/storage');
|
||||
await expect(page.getByTestId('admin-storage-form')).toBeVisible({ timeout: 15_000 });
|
||||
// Keep the (safe) local backend and save — exercises the save handler without
|
||||
// reconfiguring storage to a remote backend.
|
||||
@@ -62,8 +65,7 @@ test('storage tab: save the local backend settings', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('oidc tab: toggle enabled and fill issuer', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-oidc-tab').click();
|
||||
await page.goto('/admin/oidc');
|
||||
await expect(page.getByTestId('admin-oidc-form')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId('admin-oidc-enabled-checkbox').check().catch(() => {});
|
||||
await page
|
||||
@@ -84,8 +86,7 @@ async function createUserRow(
|
||||
page: import('@playwright/test').Page,
|
||||
uname: string,
|
||||
): Promise<ReturnType<import('@playwright/test').Page['locator']>> {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-users-tab').click();
|
||||
await page.goto('/admin/users');
|
||||
await page.getByTestId('admin-users-create-btn').click();
|
||||
await expect(page.getByTestId('admin-create-user-form')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId('admin-create-user-username-input').fill(uname);
|
||||
@@ -165,8 +166,7 @@ test('save a user quota and deactivate the user', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('save oidc settings', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-oidc-tab').click();
|
||||
await page.goto('/admin/oidc');
|
||||
await expect(page.getByTestId('admin-oidc-form')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId('admin-oidc-issuer-input').fill('https://example.test/issuer').catch(() => {});
|
||||
await page.getByTestId('admin-oidc-client-id-input').fill('client-123').catch(() => {});
|
||||
@@ -175,9 +175,8 @@ test('save oidc settings', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('install, toggle, and delete a plugin', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-plugins-tab').click();
|
||||
await expect(page.getByTestId('admin-plugins-tab')).toHaveAttribute('aria-selected', 'true');
|
||||
await page.goto('/admin/plugins');
|
||||
await expect(page).toHaveURL(/\/admin\/plugins$/);
|
||||
|
||||
// Install the example hello plugin (plugins are enabled in the coverage env).
|
||||
await page.getByTestId('admin-plugins-install-input').setInputFiles(PLUGIN_ZIP);
|
||||
@@ -200,8 +199,7 @@ test('install, toggle, and delete a plugin', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('view plugin logs and details', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-plugins-tab').click();
|
||||
await page.goto('/admin/plugins');
|
||||
await page.getByTestId('admin-plugins-install-input').setInputFiles(PLUGIN_ZIP);
|
||||
await expect(page.locator('[data-testid^="admin-plugin-details-"]').first()).toBeVisible({
|
||||
timeout: 20_000,
|
||||
@@ -223,9 +221,8 @@ test('view plugin logs and details', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('plugins tab: save retention settings', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-plugins-tab').click();
|
||||
await expect(page.getByTestId('admin-plugins-tab')).toHaveAttribute('aria-selected', 'true');
|
||||
await page.goto('/admin/plugins');
|
||||
await expect(page).toHaveURL(/\/admin\/plugins$/);
|
||||
// The retention form is conditional; fill + save it when present.
|
||||
const retention = page.getByTestId('admin-plugin-retention-form');
|
||||
if (await retention.isVisible().catch(() => false)) {
|
||||
@@ -243,8 +240,7 @@ test('toggle the dashboard registration setting', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('send a test email from the smtp tab', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-smtp-tab').click();
|
||||
await page.goto('/admin/smtp');
|
||||
await expect(page.getByTestId('admin-smtp-to-input')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId('admin-smtp-to-input').fill('test@example.test');
|
||||
await page.getByTestId('admin-smtp-send-btn').click();
|
||||
@@ -252,8 +248,7 @@ test('send a test email from the smtp tab', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('storage tab: fill the S3 backend fields', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-storage-tab').click();
|
||||
await page.goto('/admin/storage');
|
||||
await expect(page.getByTestId('admin-storage-form')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Switch to S3 to reveal + fill the conditional fields (no save — that would
|
||||
@@ -270,8 +265,7 @@ test('storage tab: fill the S3 backend fields', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('oidc tab: run discovery against a bogus issuer', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-oidc-tab').click();
|
||||
await page.goto('/admin/oidc');
|
||||
await expect(page.getByTestId('admin-oidc-form')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId('admin-oidc-issuer-input').fill('https://idp.example.test').catch(() => {});
|
||||
// Discovery fails (no real IdP) — exercises the discover + error path.
|
||||
@@ -286,8 +280,7 @@ test('users tab: paginate the user list', async ({ page }) => {
|
||||
for (let i = 0; i < 26; i++) {
|
||||
await apiAdminCreateUser(page, `pageu${Date.now()}${i}`);
|
||||
}
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-users-tab').click();
|
||||
await page.goto('/admin/users');
|
||||
await expect(page.getByTestId('admin-users-pager-next-btn')).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId('admin-users-pager-next-btn').click();
|
||||
await page.waitForTimeout(400);
|
||||
@@ -295,8 +288,7 @@ test('users tab: paginate the user list', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('plugins tab: install then save retention settings', async ({ page }) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByTestId('admin-plugins-tab').click();
|
||||
await page.goto('/admin/plugins');
|
||||
await page.getByTestId('admin-plugins-install-input').setInputFiles(PLUGIN_ZIP);
|
||||
await expect(page.locator('[data-testid^="admin-plugin-delete-"]').first()).toBeVisible({
|
||||
timeout: 20_000,
|
||||
|
||||
Reference in New Issue
Block a user