chore: migrate to vitest

This commit is contained in:
Nicolas Meienberger 2026-04-01 19:36:31 +02:00
parent e265f7d478
commit 36e8c0de75
69 changed files with 402 additions and 229 deletions

View file

@ -24,7 +24,7 @@ bun run tsc
bun run test
# Run a specific test file
bunx dotenv-cli -e .env.test -- bun test --preload ./app/test/setup.ts path/to/test.ts
bunx dotenv-cli -e .env.test -- bunx --bun vitest run --project server path/to/test.ts
```
### Building

View file

@ -1,5 +1,5 @@
/** biome-ignore-all lint/style/noNonNullAssertion: Testing file - non-null assertions are acceptable here */
import { afterEach, expect, test, describe } from "bun:test";
import { afterEach, describe, expect, test } from "vitest";
import { cleanup, render, screen, fireEvent, within } from "@testing-library/react";
import { useState } from "react";
import { FileTree, type FileEntry } from "../file-tree";

View file

@ -1,11 +1,16 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { HttpResponse, http, server } from "~/test/msw/server";
import { cleanup, render, screen, userEvent, waitFor, within } from "~/test/test-utils";
import { fromAny } from "@total-typescript/shoehorn";
await mock.module("@tanstack/react-router", () => ({
useNavigate: () => mock(() => {}),
}));
vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
return {
...actual,
useNavigate: (() => vi.fn(async () => {})) as typeof actual.useNavigate,
};
});
import { RestoreForm } from "../restore-form";

View file

@ -1,5 +1,5 @@
import type { ComponentProps } from "react";
import { afterEach, describe, expect, test } from "bun:test";
import { afterEach, describe, expect, test } from "vitest";
import { HttpResponse, http, server } from "~/test/msw/server";
import { cleanup, fireEvent, render, screen, userEvent, waitFor, within } from "~/test/test-utils";

View file

@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { useEffect, useState } from "react";
import { cleanup, createTestQueryClient, render, screen } from "~/test/test-utils";
import { useServerEvents } from "../use-server-events";
@ -7,7 +7,7 @@ class MockEventSource {
static instances: MockEventSource[] = [];
public onerror: ((event: Event) => void) | null = null;
public close = mock(() => {});
public close = vi.fn(() => {});
private listeners = new Map<string, Set<(event: Event) => void>>();
constructor(public url: string) {
@ -71,8 +71,8 @@ describe("useServerEvents", () => {
beforeEach(() => {
MockEventSource.reset();
globalThis.EventSource = MockEventSource as unknown as typeof EventSource;
console.info = mock(() => {});
console.error = mock(() => {});
console.info = vi.fn(() => {});
console.error = vi.fn(() => {});
});
afterEach(() => {
@ -85,8 +85,8 @@ describe("useServerEvents", () => {
test("shares one EventSource across consumers and invalidates queries once on backup completion", async () => {
const queryClient = createTestQueryClient();
const invalidateQueries = mock(async () => undefined);
const refetchQueries = mock(async () => undefined);
const invalidateQueries = vi.fn(async () => undefined);
const refetchQueries = vi.fn(async () => undefined);
queryClient.invalidateQueries = invalidateQueries as typeof queryClient.invalidateQueries;
queryClient.refetchQueries = refetchQueries as typeof queryClient.refetchQueries;

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import {
DEFAULT_TIME_FORMAT,
formatDate,
@ -12,7 +12,7 @@ import {
} from "../datetime";
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
const sampleDate = new Date("2026-01-10T14:30:00.000Z");
@ -56,7 +56,7 @@ describe("datetime formatters", () => {
});
test("formats relative times without approximation prefixes", () => {
const nowSpy = spyOn(Date, "now").mockReturnValue(new Date("2026-01-10T14:35:00.000Z").getTime());
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(new Date("2026-01-10T14:35:00.000Z").getTime());
expect(formatTimeAgo(sampleDate)).toBe("5 minutes ago");

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { getVolumeMountPath } from "./volume-path";
import { fromAny } from "@total-typescript/shoehorn";

View file

@ -1,12 +1,17 @@
import { afterEach, describe, expect, mock, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import { cleanup, render, screen } from "~/test/test-utils";
await mock.module("@tanstack/react-router", () => ({
useNavigate: () => mock(() => {}),
}));
vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
await mock.module("~/client/modules/sso/components/sso-login-section", () => ({
SsoLoginSection: () => null,
return {
...actual,
useNavigate: (() => vi.fn(async () => {})) as typeof actual.useNavigate,
};
});
vi.mock("~/client/modules/sso/components/sso-login-section", () => ({
SsoLoginSection: () => <></>,
}));
import { LoginPage } from "../login";

View file

@ -1,18 +1,29 @@
import type { ReactNode } from "react";
import { afterEach, describe, expect, mock, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import { HttpResponse, http, server } from "~/test/msw/server";
import { cleanup, render, screen, waitFor } from "~/test/test-utils";
import { fromAny } from "@total-typescript/shoehorn";
await mock.module("@tanstack/react-router", () => ({
Link: ({ children }: { children?: ReactNode }) => <a href="/">{children}</a>,
}));
vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
await mock.module("~/client/lib/datetime", () => ({
useTimeFormat: () => ({
formatDateTime: () => "2026-03-26 00:00",
}),
}));
return {
...actual,
Link: (({ children }: { children?: ReactNode }) => <a href="/">{children}</a>) as typeof actual.Link,
};
});
vi.mock("~/client/lib/datetime", async (importOriginal) => {
const actual = await importOriginal<typeof import("~/client/lib/datetime")>();
return {
...actual,
useTimeFormat: () => ({
...actual.useTimeFormat(),
formatDateTime: () => "2026-03-26 00:00",
}),
};
});
import { SnapshotFileBrowser } from "../snapshot-file-browser";

View file

@ -1,40 +1,51 @@
import type { ReactNode } from "react";
import { afterEach, describe, expect, mock, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import { fromAny } from "@total-typescript/shoehorn";
import { HttpResponse, http, server } from "~/test/msw/server";
import { cleanup, render, screen } from "~/test/test-utils";
await mock.module("@tanstack/react-router", () => ({
Link: ({ children }: { children?: ReactNode }) => <a href="/">{children}</a>,
useNavigate: () => mock(() => {}),
useSearch: () => ({}),
vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
return {
...actual,
Link: (({ children }: { children?: ReactNode }) => <a href="/">{children}</a>) as typeof actual.Link,
useNavigate: (() => vi.fn(async () => {})) as typeof actual.useNavigate,
useSearch: (() => ({})) as typeof actual.useSearch,
};
});
vi.mock("~/client/components/backup-summary-card", () => ({
BackupSummaryCard: () => <></>,
}));
await mock.module("~/client/components/backup-summary-card", () => ({
BackupSummaryCard: () => null,
vi.mock("~/client/modules/backups/components/schedule-summary", () => ({
ScheduleSummary: () => <></>,
}));
await mock.module("~/client/modules/backups/components/schedule-summary", () => ({
ScheduleSummary: () => null,
vi.mock("~/client/modules/backups/components/snapshot-timeline", () => ({
SnapshotTimeline: () => <></>,
}));
await mock.module("~/client/modules/backups/components/snapshot-timeline", () => ({
SnapshotTimeline: () => null,
vi.mock("~/client/modules/backups/components/schedule-notifications-config", () => ({
ScheduleNotificationsConfig: () => <></>,
}));
await mock.module("~/client/modules/backups/components/schedule-notifications-config", () => ({
ScheduleNotificationsConfig: () => null,
vi.mock("~/client/modules/backups/components/schedule-mirrors-config", () => ({
ScheduleMirrorsConfig: () => <></>,
}));
await mock.module("~/client/modules/backups/components/schedule-mirrors-config", () => ({
ScheduleMirrorsConfig: () => null,
}));
vi.mock("~/client/lib/datetime", async (importOriginal) => {
const actual = await importOriginal<typeof import("~/client/lib/datetime")>();
await mock.module("~/client/lib/datetime", () => ({
useTimeFormat: () => ({
formatDateTime: () => "2026-03-26 00:00",
}),
}));
return {
...actual,
useTimeFormat: () => ({
...actual.useTimeFormat(),
formatDateTime: () => "2026-03-26 00:00",
}),
};
});
import { ScheduleDetailsPage } from "../backup-details";

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import { createTestSession } from "~/test/helpers/auth";
import { db } from "~/server/db/db";

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { eq } from "drizzle-orm";
import { db } from "~/server/db/db";
import { account, sessionsTable, usersTable } from "~/server/db/schema";

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { repoMutex } from "../repository-mutex";
describe("RepositoryMutex", () => {

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test, vi } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import { logger } from "@zerobyte/core/node";
import { Job, Scheduler } from "../scheduler";
@ -9,17 +9,17 @@ const flushMicrotasks = async () => {
const mockTimeZone = (timeZone: string) => {
// oxlint-disable-next-line typescript/unbound-method
const resolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
return spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockImplementation(
function (this: Intl.DateTimeFormat) {
return vi
.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions")
.mockImplementation(function (this: Intl.DateTimeFormat) {
return { ...resolvedOptions.call(this), timeZone };
},
);
});
};
describe("Scheduler", () => {
afterEach(async () => {
await Scheduler.clear();
mock.restore();
vi.restoreAllMocks();
vi.useRealTimers();
});
@ -28,7 +28,7 @@ describe("Scheduler", () => {
let runCount = 0;
let releaseRun: (() => void) | undefined;
const warn = spyOn(logger, "warn");
const warn = vi.spyOn(logger, "warn");
class TestJob extends Job {
async run() {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { isValidUsername, normalizeUsername } from "~/lib/username";
describe("username helpers", () => {

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { eq } from "drizzle-orm";
import { db, sqlite } from "~/server/db/db";
import { account, invitation, member, organization, usersTable } from "~/server/db/schema";

View file

@ -1,4 +1,4 @@
import { test, describe, beforeEach, expect } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { convertLegacyUserOnFirstLogin } from "../convert-legacy-user";
import { db } from "~/server/db/db";
import { usersTable, account, organization, member } from "~/server/db/schema";

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { db, sqlite } from "~/server/db/db";
import { account, member, organization, sessionsTable, usersTable } from "~/server/db/schema";
import { authService } from "../auth.service";

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { db, sqlite } from "~/server/db/db";
import { member, organization, sessionsTable, usersTable } from "~/server/db/schema";
import { authService } from "../auth.service";

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import {
createTestSession,

View file

@ -1,15 +1,34 @@
import { afterEach, describe, expect, test, vi } from "vitest";
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
access: vi.fn(actual.access),
};
});
vi.mock("../../../../utils/mountinfo", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../../utils/mountinfo")>();
return {
...actual,
getMountForPath: vi.fn(actual.getMountForPath),
};
});
import * as fs from "node:fs/promises";
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import * as mountinfo from "../../../../utils/mountinfo";
import { assertMounted } from "../backend-utils";
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("assertMountedFilesystem", () => {
test("throws when the path is not accessible", async () => {
spyOn(fs, "access").mockRejectedValueOnce(new Error("missing"));
vi.mocked(fs.access).mockRejectedValueOnce(new Error("missing"));
await expect(assertMounted("/tmp/volume", (fstype) => fstype.startsWith("nfs"))).rejects.toThrow(
"Volume is not mounted",
@ -17,8 +36,8 @@ describe("assertMountedFilesystem", () => {
});
test("throws when the mount filesystem does not match", async () => {
spyOn(fs, "access").mockResolvedValueOnce(undefined);
spyOn(mountinfo, "getMountForPath").mockResolvedValueOnce({
vi.mocked(fs.access).mockResolvedValueOnce(undefined);
vi.mocked(mountinfo.getMountForPath).mockResolvedValueOnce({
mountPoint: "/tmp/volume",
fstype: "cifs",
});
@ -29,8 +48,8 @@ describe("assertMountedFilesystem", () => {
});
test("accepts a matching mounted filesystem", async () => {
spyOn(fs, "access").mockResolvedValueOnce(undefined);
spyOn(mountinfo, "getMountForPath").mockResolvedValueOnce({
vi.mocked(fs.access).mockResolvedValueOnce(undefined);
vi.mocked(mountinfo.getMountForPath).mockResolvedValueOnce({
mountPoint: "/tmp/volume",
fstype: "nfs4",
});

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import { createTestSession, getAuthHeaders } from "~/test/helpers/auth";
import { createTestVolume } from "~/test/helpers/volume";

View file

@ -1,5 +1,5 @@
import waitForExpect from "wait-for-expect";
import { test, describe, mock, expect, afterEach, spyOn } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import { backupsService } from "../backups.service";
import { backupsExecutionService } from "../backups.execution";
import { createTestVolume } from "~/test/helpers/volume";
@ -19,12 +19,12 @@ import { repositoriesService } from "~/server/modules/repositories/repositories.
import { repoMutex } from "~/server/core/repository-mutex";
const setup = () => {
const resticBackupMock = mock((_: SafeSpawnParams) =>
const resticBackupMock = vi.fn((_: SafeSpawnParams) =>
Promise.resolve({ exitCode: 0, summary: generateBackupOutput(), error: "" }),
);
const resticForgetMock = mock(() => Promise.resolve({ success: true, data: null }));
const resticCopyMock = mock(() => Promise.resolve({ success: true, output: "" }));
const refreshStatsMock = mock(() =>
const resticForgetMock = vi.fn(() => Promise.resolve({ success: true, data: null }));
const resticCopyMock = vi.fn(() => Promise.resolve({ success: true, output: "" }));
const refreshStatsMock = vi.fn(() =>
Promise.resolve({
total_size: 0,
total_uncompressed_size: 0,
@ -35,11 +35,11 @@ const setup = () => {
}),
);
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
spyOn(restic, "forget").mockImplementation(resticForgetMock);
spyOn(restic, "copy").mockImplementation(resticCopyMock);
spyOn(repositoriesService, "refreshRepositoryStats").mockImplementation(refreshStatsMock);
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
vi.spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
vi.spyOn(restic, "forget").mockImplementation(resticForgetMock);
vi.spyOn(restic, "copy").mockImplementation(resticCopyMock);
vi.spyOn(repositoriesService, "refreshRepositoryStats").mockImplementation(refreshStatsMock);
vi.spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
return {
resticBackupMock,
@ -50,7 +50,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("backup execution - validation failures", () => {
@ -92,7 +92,7 @@ describe("backup execution - validation failures", () => {
...hydratedSchedule,
volume: null,
};
spyOn(scheduleQueries, "findById").mockResolvedValueOnce(fromAny(scheduleWithoutVolume));
vi.spyOn(scheduleQueries, "findById").mockResolvedValueOnce(fromAny(scheduleWithoutVolume));
// act
const result = await backupsExecutionService.validateBackupExecution(schedule.id);
@ -122,7 +122,7 @@ describe("backup execution - validation failures", () => {
...hydratedSchedule,
repository: null,
};
spyOn(scheduleQueries, "findById").mockResolvedValueOnce(fromAny(scheduleWithoutRepository));
vi.spyOn(scheduleQueries, "findById").mockResolvedValueOnce(fromAny(scheduleWithoutRepository));
// act
const result = await backupsExecutionService.validateBackupExecution(schedule.id);
@ -261,7 +261,7 @@ describe("stop backup", () => {
repositoryId: repository.id,
});
spyOn(repoMutex, "acquireShared").mockImplementation((_repositoryId, _operation, signal) => {
vi.spyOn(repoMutex, "acquireShared").mockImplementation((_repositoryId, _operation, signal) => {
return new Promise((_, reject) => {
if (signal?.aborted) {
reject(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"));

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import path from "node:path";
import { fromAny } from "@total-typescript/shoehorn";
import { createBackupOptions, processPattern } from "../backup.helpers";

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { scheduleQueries } from "../backups.queries";
import { createTestBackupSchedule } from "~/test/helpers/backup";
import { createTestVolume } from "~/test/helpers/volume";

View file

@ -1,5 +1,5 @@
import waitForExpect from "wait-for-expect";
import { test, describe, mock, expect, afterEach, spyOn } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import { eq } from "drizzle-orm";
import { backupsService } from "../backups.service";
import { createTestVolume } from "~/test/helpers/volume";
@ -16,8 +16,8 @@ import { backupsExecutionService } from "../backups.execution";
import { repositoriesService } from "~/server/modules/repositories/repositories.service";
const setup = () => {
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
const refreshStatsMock = mock(() =>
const resticBackupMock = vi.fn(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
const refreshStatsMock = vi.fn(() =>
Promise.resolve({
total_size: 0,
total_uncompressed_size: 0,
@ -27,9 +27,9 @@ const setup = () => {
snapshots_count: 0,
}),
);
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
spyOn(repositoriesService, "refreshRepositoryStats").mockImplementation(refreshStatsMock);
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
vi.spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
vi.spyOn(repositoriesService, "refreshRepositoryStats").mockImplementation(refreshStatsMock);
vi.spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
return {
resticBackupMock,
@ -38,7 +38,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("execute backup", () => {

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import { serverEvents } from "~/server/core/events";
import { createTestSession, getAuthHeaders } from "~/test/helpers/auth";

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { buildCustomShoutrrrUrl } from "../builders/custom";
import { buildDiscordShoutrrrUrl } from "../builders/discord";
import { buildEmailShoutrrrUrl } from "../builders/email";

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { mapNotificationConfigSecrets } from "../notification-config-secrets";
describe("mapNotificationConfigSecrets", () => {

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import { createTestSession, getAuthHeaders } from "~/test/helpers/auth";

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
@ -11,7 +11,7 @@ import { provisionedResourcesSchema, readProvisionedResourcesFile, syncProvision
describe("provisioning", () => {
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
test("rejects duplicate ids for the same organization", () => {
@ -347,9 +347,9 @@ describe("provisioning", () => {
const { organizationId } = await createTestSession();
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-provisioning-"));
const provisioningPath = path.join(tempDir, "provisioning.json");
const initMock = mock(() => Promise.resolve({ success: true, error: null }));
const initMock = vi.fn(() => Promise.resolve({ success: true, error: null }));
spyOn(restic, "init").mockImplementation(initMock);
vi.spyOn(restic, "init").mockImplementation(initMock);
await fs.writeFile(
provisioningPath,

View file

@ -1,4 +1,4 @@
import { test, describe, expect, spyOn } from "bun:test";
import { describe, expect, test, vi } from "vitest";
import crypto from "node:crypto";
import { PassThrough } from "node:stream";
import { createApp } from "~/server/app";
@ -329,7 +329,7 @@ describe("repositories updates", () => {
const { restic } = await import("~/server/core/restic");
const { ResticError } = await import("@zerobyte/core/restic");
const deleteSnapshotSpy = spyOn(restic, "deleteSnapshot").mockImplementation(async () => {
const deleteSnapshotSpy = vi.spyOn(restic, "deleteSnapshot").mockImplementation(async () => {
throw new ResticError(1, "Fatal: unexpected HTTP response (403): 403 Forbidden");
});
@ -358,7 +358,7 @@ describe("repositories updates", () => {
const stream = new PassThrough();
const expectedContent = "downloaded snapshot contents";
const dumpSnapshotSpy = spyOn(repositoriesService, "dumpSnapshot").mockResolvedValue({
const dumpSnapshotSpy = vi.spyOn(repositoriesService, "dumpSnapshot").mockResolvedValue({
stream,
completion: Promise.resolve(),
abort: () => {
@ -392,7 +392,7 @@ describe("repositories updates", () => {
const { repositoriesService } = await import("~/server/modules/repositories/repositories.service");
const stream = new PassThrough();
const dumpSnapshotSpy = spyOn(repositoriesService, "dumpSnapshot").mockResolvedValue({
const dumpSnapshotSpy = vi.spyOn(repositoriesService, "dumpSnapshot").mockResolvedValue({
stream,
completion: Promise.resolve(),
abort: () => {

View file

@ -3,7 +3,7 @@ import * as os from "node:os";
import nodePath from "node:path";
import { randomUUID } from "node:crypto";
import { Readable } from "node:stream";
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import type { RepositoryConfig } from "@zerobyte/core/restic";
import { REPOSITORY_BASE } from "~/server/core/constants";
import { serverEvents } from "~/server/core/events";
@ -38,15 +38,15 @@ const createTestRepository = async (organizationId: string) => {
};
describe("repositoriesService.createRepository", () => {
const initMock = mock(() => Promise.resolve({ success: true, error: null }));
const initMock = vi.fn(() => Promise.resolve({ success: true, error: null }));
beforeEach(() => {
initMock.mockClear();
spyOn(restic, "init").mockImplementation(initMock);
vi.spyOn(restic, "init").mockImplementation(initMock);
});
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
test("creates a shortId-scoped repository path when using the repository base directory", async () => {
@ -113,7 +113,7 @@ describe("repositoriesService.createRepository", () => {
const explicitPath = `${REPOSITORY_BASE}/custom-${randomUUID()}`;
const config: RepositoryConfig = { backend: "local", path: explicitPath, isExistingRepository: true };
spyOn(restic, "snapshots").mockImplementation(() => Promise.resolve([]));
vi.spyOn(restic, "snapshots").mockImplementation(() => Promise.resolve([]));
// act
const result = await withContext({ organizationId, userId: user.id }, () =>
@ -140,7 +140,7 @@ describe("repositoriesService.createRepository", () => {
describe("repositoriesService repository stats", () => {
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
test("returns empty stats when repository has not been populated yet", async () => {
@ -173,7 +173,7 @@ describe("repositoriesService repository stats", () => {
snapshots_count: 3,
};
const statsSpy = spyOn(restic, "stats").mockResolvedValue(expectedStats);
const statsSpy = vi.spyOn(restic, "stats").mockResolvedValue(expectedStats);
const refreshed = await withContext({ organizationId, userId: user.id }, () =>
repositoriesService.refreshRepositoryStats(repository.shortId),
@ -197,7 +197,7 @@ describe("repositoriesService repository stats", () => {
describe("repositoriesService.dumpSnapshot", () => {
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
const createDumpResult = () => ({
@ -232,7 +232,7 @@ describe("repositoriesService.dumpSnapshot", () => {
organizationId,
});
spyOn(restic, "snapshots").mockResolvedValue([
vi.spyOn(restic, "snapshots").mockResolvedValue([
{
id: snapshotId,
short_id: snapshotId,
@ -242,8 +242,8 @@ describe("repositoriesService.dumpSnapshot", () => {
},
]);
const dumpMock = mock(() => Promise.resolve(createDumpResult()));
spyOn(restic, "dump").mockImplementation(dumpMock);
const dumpMock = vi.fn(() => Promise.resolve(createDumpResult()));
vi.spyOn(restic, "dump").mockImplementation(dumpMock);
return {
organizationId,
@ -259,7 +259,7 @@ describe("repositoriesService.dumpSnapshot", () => {
snapshotId: "snapshot-123",
basePath: "/var/lib/zerobyte/volumes/vol123/_data",
});
const emitSpy = spyOn(serverEvents, "emit");
const emitSpy = vi.spyOn(serverEvents, "emit");
await withContext({ organizationId, userId }, () =>
repositoriesService.dumpSnapshot(shortId, "snapshot-123", `${basePath}/documents`, "dir"),
@ -330,7 +330,7 @@ describe("repositoriesService.dumpSnapshot", () => {
basePath: "/var/lib/zerobyte/volumes/vol123/_data",
});
expect(
await expect(
withContext({ organizationId, userId }, () =>
repositoriesService.dumpSnapshot(shortId, "snapshot-no-kind", `${basePath}/documents/report.txt`),
),
@ -354,14 +354,14 @@ describe("repositoriesService.dumpSnapshot", () => {
describe("repositoriesService.restoreSnapshot", () => {
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
const setupRestoreSnapshotScenario = async () => {
const { organizationId, user } = await createTestSession();
const repository = await createTestRepository(organizationId);
spyOn(restic, "snapshots").mockResolvedValue([
vi.spyOn(restic, "snapshots").mockResolvedValue([
{
id: "snapshot-restore",
short_id: "snapshot-restore",
@ -371,7 +371,7 @@ describe("repositoriesService.restoreSnapshot", () => {
},
]);
const restoreMock = mock(() =>
const restoreMock = vi.fn(() =>
Promise.resolve({
message_type: "summary" as const,
seconds_elapsed: 1,
@ -383,7 +383,7 @@ describe("repositoriesService.restoreSnapshot", () => {
bytes_restored: 1,
}),
);
spyOn(restic, "restore").mockImplementation(restoreMock);
vi.spyOn(restic, "restore").mockImplementation(restoreMock);
return {
organizationId,
@ -434,7 +434,7 @@ describe("repositoriesService.restoreSnapshot", () => {
describe("repositoriesService.getRetentionCategories", () => {
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
test("recomputes retention categories after repository cache invalidation", async () => {
@ -476,7 +476,7 @@ describe("repositoriesService.getRetentionCategories", () => {
],
});
const forgetSpy = spyOn(restic, "forget");
const forgetSpy = vi.spyOn(restic, "forget");
forgetSpy.mockResolvedValueOnce(buildForgetResponse(oldSnapshotId));
forgetSpy.mockResolvedValueOnce(buildForgetResponse(newSnapshotId));
@ -500,7 +500,7 @@ describe("repositoriesService.getRetentionCategories", () => {
describe("repositoriesService.deleteSnapshot", () => {
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
test("refreshes repository stats in background after successful deletion", async () => {
@ -515,8 +515,8 @@ describe("repositoriesService.deleteSnapshot", () => {
snapshots_count: 1,
};
spyOn(restic, "deleteSnapshot").mockResolvedValue({ success: true });
const statsSpy = spyOn(restic, "stats").mockResolvedValue(expectedStats);
vi.spyOn(restic, "deleteSnapshot").mockResolvedValue({ success: true });
const statsSpy = vi.spyOn(restic, "stats").mockResolvedValue(expectedStats);
await withContext({ organizationId, userId: user.id }, () =>
repositoriesService.deleteSnapshot(repository.shortId, "snap-1"),
@ -535,11 +535,11 @@ describe("repositoriesService.deleteSnapshot", () => {
const { organizationId, user } = await createTestSession();
const repository = await createTestRepository(organizationId);
spyOn(restic, "deleteSnapshot").mockImplementation(async () => {
vi.spyOn(restic, "deleteSnapshot").mockImplementation(async () => {
throw new ResticError(1, "Fatal: unexpected HTTP response (403): 403 Forbidden");
});
expect(
await expect(
withContext({ organizationId, userId: user.id }, () =>
repositoriesService.deleteSnapshot(repository.shortId, "snap123"),
),

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { mapAuthErrorToCode } from "../sso.errors";
describe("mapAuthErrorToCode", () => {

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import type { GenericEndpointContext } from "better-auth";
import { eq } from "drizzle-orm";
import { db } from "~/server/db/db";

View file

@ -1,12 +1,11 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import { config } from "~/server/core/config";
import { db } from "~/server/db/db";
import { account, invitation, member, organization, ssoProvider, usersTable, verification } from "~/server/db/schema";
import { createTestSession, createTestSessionWithOrgAdmin } from "~/test/helpers/auth";
const app = createApp();
const ssoRegisterUrl = new URL("/api/auth/sso/register", config.baseUrl).toString();
const ssoRegisterUrl = new URL("/api/auth/sso/register", "http://localhost:3000").toString();
function buildRegisterBody(organizationId: string, suffix: string) {
return {

View file

@ -1,11 +1,10 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import { config } from "~/server/core/config";
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
import { db } from "~/server/db/db";
const app = createApp();
const ssoSignInUrl = new URL("/api/auth/sign-in/sso", config.baseUrl).toString();
const ssoSignInUrl = new URL("/api/auth/sign-in/sso", "http://localhost:3000").toString();
function postSsoSignIn(body: Record<string, unknown>) {
return app.request(ssoSignInUrl, {

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { db, sqlite } from "~/server/db/db";
import { account, invitation, member, organization, sessionsTable, ssoProvider, usersTable } from "~/server/db/schema";
import { ssoService } from "../sso.service";

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import type { GenericEndpointContext } from "better-auth";
import { db } from "~/server/db/db";
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
@ -80,7 +80,7 @@ describe("requireSsoInvitation", () => {
await createSsoProvider("oidc-acme");
const ctx = createMockContext("/sign-up/email");
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("Missing providerId");
await expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("Missing providerId");
});
test("detects whether current request is an SSO callback", async () => {
@ -97,7 +97,7 @@ describe("requireSsoInvitation", () => {
await createSsoProvider("oidc-acme");
const ctx = createMockSsoCallbackContext("oidc-acme");
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited");
await expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited");
});
test("blocks SSO callback when invitation is expired", async () => {
@ -115,7 +115,7 @@ describe("requireSsoInvitation", () => {
});
const ctx = createMockSsoCallbackContext("oidc-acme");
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited");
await expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited");
});
test("allows SSO callback when a matching pending invitation exists", async () => {
@ -133,11 +133,11 @@ describe("requireSsoInvitation", () => {
});
const ctx = createMockSsoCallbackContext("oidc-acme");
expect(requireSsoInvitation(" USER@EXAMPLE.COM ", ctx)).resolves.toBeUndefined();
await expect(requireSsoInvitation(" USER@EXAMPLE.COM ", ctx)).resolves.toBeUndefined();
});
test("throws when provider is not registered", async () => {
const ctx = createMockSsoCallbackContext("missing-provider");
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("SSO provider not found");
await expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("SSO provider not found");
});
});

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { beforeEach, describe, expect, test } from "vitest";
import { eq } from "drizzle-orm";
import { db } from "~/server/db/db";
import { account, member, organization, ssoProvider, usersTable } from "~/server/db/schema";

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import type { AuthMiddlewareContext } from "~/server/lib/auth";
import { validateSsoCallbackUrls } from "../validate-callback-urls";

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import type { AuthMiddlewareContext } from "~/server/lib/auth";
import { validateSsoProviderId } from "../validate-provider-id";

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { createApp } from "~/server/app";
import { createTestSession, createTestSessionWithGlobalAdmin, getAuthHeaders } from "~/test/helpers/auth";

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { db } from "~/server/db/db";
import { volumesTable } from "~/server/db/schema";
import { createApp } from "~/server/app";

View file

@ -1,4 +1,4 @@
import { test, describe, expect } from "bun:test";
import { describe, expect, test } from "vitest";
import { volumeService } from "../volume.service";
import { db } from "~/server/db/db";
import { volumesTable } from "~/server/db/schema";

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { safeExec, safeSpawn } from "@zerobyte/core/node";
describe("safeExec", () => {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { findCommonAncestor } from "@zerobyte/core/utils";
describe("findCommonAncestor", () => {

View file

@ -1,10 +1,9 @@
import "./setup.ts";
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import { afterAll, afterEach, beforeAll, mock } from "bun:test";
import "./setup-shared";
import { afterAll, afterEach, beforeAll, vi } from "vitest";
import { client } from "~/client/api-client/client.gen";
import { server } from "~/test/msw/server";
void mock.module("~/client/hooks/use-root-loader-data", () => ({
vi.mock(import("~/client/hooks/use-root-loader-data"), () => ({
useRootLoaderData: () => ({
theme: "dark",
locale: "en-US",
@ -15,8 +14,6 @@ void mock.module("~/client/hooks/use-root-loader-data", () => ({
}),
}));
GlobalRegistrator.register({ url: "http://localhost:3000" });
client.setConfig({
baseUrl: "http://localhost:3000",
credentials: "include",

32
app/test/setup-shared.ts Normal file
View file

@ -0,0 +1,32 @@
import { vi } from "vitest";
process.env.BASE_URL = "http://localhost:3000";
process.env.TRUSTED_ORIGINS = "http://localhost:3000";
vi.mock(import("@zerobyte/core/node"), async () => {
const utils = await vi.importActual<typeof import("@zerobyte/core/node")>("@zerobyte/core/node");
return {
...utils,
logger: {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
},
};
});
vi.mock(import("~/server/utils/crypto"), async () => {
const cryptoModule = await vi.importActual<typeof import("~/server/utils/crypto")>("~/server/utils/crypto");
return {
cryptoUtils: {
...cryptoModule.cryptoUtils,
deriveSecret: async () => "test-secret",
sealSecret: async (v: string) => v,
resolveSecret: async (v: string) => v,
generateResticPassword: () => "test-restic-password",
},
};
});

View file

@ -1,32 +1,10 @@
import { beforeAll, mock } from "bun:test";
import "./setup-shared";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import path from "node:path";
import { cwd } from "node:process";
import { beforeAll } from "vitest";
import { db } from "~/server/db/db";
import * as utils from "@zerobyte/core/node";
void mock.module("@zerobyte/core/node", () => {
return {
...utils,
logger: {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
},
};
});
void mock.module("~/server/utils/crypto", () => ({
cryptoUtils: {
deriveSecret: async () => "test-secret",
sealSecret: async (v: string) => v,
resolveSecret: async (v: string) => v,
generateResticPassword: () => "test-restic-password",
},
}));
beforeAll(async () => {
const migrationsFolder = path.join(cwd(), "app", "drizzle");
migrate(db, { migrationsFolder });

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { isPathWithin, normalizeAbsolutePath } from "@zerobyte/core/utils";
describe("normalizeAbsolutePath", () => {

View file

@ -110,6 +110,7 @@
"typescript": "^5.9.3",
"vite": "^8.0.1",
"vite-bundle-analyzer": "^1.2.3",
"vitest": "^4.1.1",
"wait-for-expect": "^4.0.0",
},
},
@ -955,6 +956,8 @@
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@ -977,6 +980,8 @@
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@ -1021,6 +1026,20 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="],
"@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="],
"@vitest/runner": ["@vitest/runner@4.1.1", "", { "dependencies": { "@vitest/utils": "4.1.1", "pathe": "^2.0.3" } }, "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg=="],
"@vitest/spy": ["@vitest/spy@4.1.1", "", {}, "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA=="],
"@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="],
"@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
@ -1055,6 +1074,8 @@
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
@ -1101,6 +1122,8 @@
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
@ -1263,6 +1286,8 @@
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
@ -1277,12 +1302,16 @@
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@ -1661,6 +1690,8 @@
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
@ -1825,6 +1856,8 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"slugify": ["slugify@1.6.8", "", {}, "sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA=="],
@ -1845,9 +1878,11 @@
"srvx": ["srvx@0.10.1", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
"strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
@ -1883,12 +1918,16 @@
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
@ -1967,6 +2006,8 @@
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
"vitest": ["vitest@4.1.1", "", { "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", "@vitest/pretty-format": "4.1.1", "@vitest/runner": "4.1.1", "@vitest/snapshot": "4.1.1", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.1", "@vitest/browser-preview": "4.1.1", "@vitest/browser-webdriverio": "4.1.1", "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA=="],
"wait-for-expect": ["wait-for-expect@4.0.0", "", {}, "sha512-mcH2HYUUHhdFGHVJkgwkBxRihZO4VSuPyh6xhYHz7LEnYkcaLbTAEEsTpYiFw4UY45XdTZYYIaquuMucw9wWMw=="],
"web-haptics": ["web-haptics@0.0.6", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18", "svelte": ">=4", "vue": ">=3" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw=="],
@ -1985,6 +2026,8 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
@ -2087,6 +2130,8 @@
"@prisma/dev/hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"@prisma/dev/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="],
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="],

View file

@ -22,9 +22,9 @@
"gen:api-client": "dotenv -e .env.local -- openapi-ts",
"gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",
"studio": "drizzle-kit studio",
"test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts",
"test:client": "dotenv -e .env.test -- bun test app/client --preload ./app/test/setup-client.ts",
"test": "bun run test:server && bun run test:client && turbo run test",
"test:server": "dotenv -e .env.test -- bunx --bun vitest run --project server",
"test:client": "dotenv -e .env.test -- bunx --bun vitest run --project client",
"test": "dotenv -e .env.test -- bunx --bun vitest run && turbo run test",
"test:e2e": "NODE_ENV=test dotenv -e .env.local -- playwright test",
"test:e2e:ui": "NODE_ENV=test dotenv -e .env.local -- playwright test --ui",
"test:codegen": "playwright codegen localhost:4096"
@ -135,6 +135,7 @@
"typescript": "^5.9.3",
"vite": "^8.0.1",
"vite-bundle-analyzer": "^1.2.3",
"vitest": "^4.1.1",
"wait-for-expect": "^4.0.0"
},
"overrides": {

View file

@ -26,7 +26,7 @@
},
"scripts": {
"tsc": "tsc --noEmit",
"test": "bun test --preload ./test/setup.ts"
"test": "bunx vitest run --config ./vitest.config.ts"
},
"devDependencies": {
"@types/bun": "latest"

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as cleanupModule from "../../helpers/cleanup-temporary-keys";
import * as spawnModule from "../../../utils/spawn";
import { ResticError } from "../../error";
@ -61,8 +61,8 @@ type SetupOptions = {
const setup = ({ spawnResult = {}, onSpawnCall }: SetupOptions = {}) => {
let capturedArgs: string[] = [];
const cleanupSpy = spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
spyOn(spawnModule, "safeSpawn").mockImplementation((params) => {
const cleanupSpy = vi.spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
vi.spyOn(spawnModule, "safeSpawn").mockImplementation((params) => {
capturedArgs = params.args;
return Promise.resolve(onSpawnCall?.(params)).then(() => ({
exitCode: 0,
@ -89,7 +89,7 @@ const setup = ({ spawnResult = {}, onSpawnCall }: SetupOptions = {}) => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("backup command", () => {

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as cleanupModule from "../../helpers/cleanup-temporary-keys";
import * as nodeModule from "../../../node";
import { copy } from "../copy";
@ -29,8 +29,8 @@ const destConfig = {
const setup = () => {
let capturedArgs: string[] = [];
spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
spyOn(nodeModule, "safeExec").mockImplementation(async ({ args }) => {
vi.spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
vi.spyOn(nodeModule, "safeExec").mockImplementation(async ({ args }) => {
capturedArgs = args ?? [];
return { exitCode: 0, stdout: "copied", stderr: "", timedOut: false };
});
@ -41,7 +41,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("copy command", () => {

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as cleanupModule from "../../helpers/cleanup-temporary-keys";
import * as nodeModule from "../../../node";
import { deleteSnapshots } from "../delete-snapshots";
@ -22,8 +22,8 @@ const config = {
const setup = () => {
let capturedArgs: string[] = [];
spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
spyOn(nodeModule, "safeExec").mockImplementation(async ({ args }) => {
vi.spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
vi.spyOn(nodeModule, "safeExec").mockImplementation(async ({ args }) => {
capturedArgs = args ?? [];
return { exitCode: 0, stdout: "", stderr: "", timedOut: false };
});
@ -34,7 +34,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("deleteSnapshots command", () => {

View file

@ -1,6 +1,6 @@
import { PassThrough } from "node:stream";
import { spawn } from "node:child_process";
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as cleanupModule from "../../helpers/cleanup-temporary-keys";
import * as nodeModule from "../../../node";
import { dump } from "../dump";
@ -24,8 +24,8 @@ const config = {
const setup = () => {
let capturedArgs: string[] = [];
spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
spyOn(nodeModule, "safeSpawn").mockImplementation((params) => {
vi.spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
vi.spyOn(nodeModule, "safeSpawn").mockImplementation((params) => {
capturedArgs = params.args;
const child = { stdout: new PassThrough() } as unknown as ReturnType<typeof spawn>;
params.onSpawn?.(child);
@ -38,7 +38,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("dump command", () => {

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as cleanupModule from "../../helpers/cleanup-temporary-keys";
import * as spawnModule from "../../../utils/spawn";
import { ls } from "../ls";
@ -34,8 +34,8 @@ const snapshotLine = JSON.stringify({
const setup = () => {
let capturedArgs: string[] = [];
spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
spyOn(spawnModule, "safeSpawn").mockImplementation((params: SafeSpawnParams) => {
vi.spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
vi.spyOn(spawnModule, "safeSpawn").mockImplementation((params: SafeSpawnParams) => {
capturedArgs = params.args;
params.onStdout?.(snapshotLine);
return Promise.resolve({ exitCode: 0, summary: snapshotLine, error: "" });
@ -47,7 +47,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("ls command", () => {

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as spawnModule from "../../../utils/spawn";
import { restore } from "../restore";
import type { ResticDeps } from "../../types";
@ -33,7 +33,7 @@ const config = {
const setup = () => {
let capturedArgs: string[] = [];
spyOn(spawnModule, "safeSpawn").mockImplementation((params: SafeSpawnParams) => {
vi.spyOn(spawnModule, "safeSpawn").mockImplementation((params: SafeSpawnParams) => {
capturedArgs = params.args;
return Promise.resolve({ exitCode: 0, summary: successfulRestoreSummary, error: "" });
});
@ -64,7 +64,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("restore command", () => {

View file

@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
import { afterEach, describe, expect, test, vi } from "vitest";
import * as cleanupModule from "../../helpers/cleanup-temporary-keys";
import * as nodeModule from "../../../node";
import { tagSnapshots } from "../tag-snapshots";
@ -22,8 +22,8 @@ const config = {
const setup = () => {
let capturedArgs: string[] = [];
spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
spyOn(nodeModule, "safeExec").mockImplementation(async ({ args }) => {
vi.spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
vi.spyOn(nodeModule, "safeExec").mockImplementation(async ({ args }) => {
capturedArgs = args ?? [];
return { exitCode: 0, stdout: "", stderr: "", timedOut: false };
});
@ -34,7 +34,7 @@ const setup = () => {
};
afterEach(() => {
mock.restore();
vi.restoreAllMocks();
});
describe("tagSnapshots command", () => {

View file

@ -1,5 +1,5 @@
import fs from "node:fs/promises";
import { afterEach, describe, expect, test } from "bun:test";
import { afterEach, describe, expect, test } from "vitest";
import { buildEnv } from "../build-env";
import type { ResticDeps } from "../../types";

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { buildRepoUrl } from "../build-repo-url";
describe("buildRepoUrl", () => {

View file

@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from "vitest";
import { validateCustomResticParams } from "../validate-custom-params";
describe("validateCustomResticParams", () => {

View file

@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, test } from "bun:test";
import { afterEach, describe, expect, test } from "vitest";
import { FILE_MODES, writeFileWithMode } from "../fs";
const tempDirectories = new Set<string>();

View file

@ -1,6 +1,6 @@
import { mock } from "bun:test";
import { vi } from "vitest";
void mock.module("../src/utils/logger.ts", () => ({
vi.mock(import("../src/utils/logger.ts"), () => ({
logger: {
debug: () => {},
info: () => {},

View file

@ -0,0 +1,16 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
pool: "forks",
maxWorkers: 4,
server: {
deps: {
inline: ["zod"],
},
},
include: ["src/**/*.test.ts", "src/**/*.spec.ts"],
setupFiles: ["./test/setup.ts"],
},
});

View file

@ -2,7 +2,7 @@
"include": ["app/**/*"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"types": ["bun", "node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",

55
vitest.config.ts Normal file
View file

@ -0,0 +1,55 @@
import path from "node:path";
import { defineConfig } from "vitest/config";
export default defineConfig({
resolve: {
alias: {
"~": path.resolve(import.meta.dirname, "app"),
},
},
test: {
pool: "forks",
maxWorkers: 4,
server: {
deps: {
inline: ["zod"],
},
},
projects: [
{
extends: true,
test: {
name: "server",
environment: "node",
include: ["app/**/*.test.ts", "app/**/*.test.tsx", "app/**/*.spec.ts", "app/**/*.spec.tsx"],
exclude: [
"app/client/**/*.test.ts",
"app/client/**/*.test.tsx",
"app/client/**/*.spec.ts",
"app/client/**/*.spec.tsx",
],
setupFiles: ["./app/test/setup.ts"],
},
},
{
extends: true,
test: {
name: "client",
environment: "happy-dom",
environmentOptions: {
happyDOM: {
url: "http://localhost:3000",
},
},
include: [
"app/client/**/*.test.ts",
"app/client/**/*.test.tsx",
"app/client/**/*.spec.ts",
"app/client/**/*.spec.tsx",
],
setupFiles: ["./app/test/setup-client.ts"],
},
},
],
},
});