109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
getConfigByProjectDomainLocation: vi.fn(),
|
|
getConfigsForProject: vi.fn(),
|
|
createConfig: vi.fn(),
|
|
updateConfig: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("cloudflare:workers", () => ({ env: {} }));
|
|
vi.mock("@/server/lib/dataforseo", () => ({ createDataforseoClient: vi.fn() }));
|
|
vi.mock(
|
|
"@/server/features/rank-tracking/repositories/RankTrackingRepository",
|
|
() => ({ RankTrackingRepository: mocks }),
|
|
);
|
|
|
|
const archivedConfig = {
|
|
id: "config_archived",
|
|
projectId: "project_1",
|
|
domain: "acme.com",
|
|
locationCode: 2840,
|
|
languageCode: "en",
|
|
devices: "both" as const,
|
|
serpDepth: 20,
|
|
scheduleInterval: "weekly" as const,
|
|
isActive: false,
|
|
lastSkipReason: "insufficient_credits",
|
|
};
|
|
|
|
const baseInput = {
|
|
projectId: "project_1",
|
|
domain: "acme.com",
|
|
locationCode: 2840,
|
|
languageCode: "es",
|
|
devices: "desktop" as const,
|
|
serpDepth: 40,
|
|
scheduleInterval: "daily" as const,
|
|
};
|
|
|
|
describe("RankTrackingService.createConfig", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
for (const mock of Object.values(mocks)) mock.mockReset();
|
|
});
|
|
|
|
it("reactivates an archived config instead of throwing, applying the new settings", async () => {
|
|
mocks.getConfigByProjectDomainLocation.mockResolvedValue(archivedConfig);
|
|
mocks.updateConfig.mockResolvedValue(undefined);
|
|
const { RankTrackingService } = await import("./RankTrackingService");
|
|
|
|
await expect(RankTrackingService.createConfig(baseInput)).resolves.toEqual({
|
|
configId: "config_archived",
|
|
});
|
|
|
|
expect(mocks.updateConfig).toHaveBeenCalledTimes(1);
|
|
expect(mocks.updateConfig).toHaveBeenCalledWith(
|
|
"config_archived",
|
|
"project_1",
|
|
expect.objectContaining({
|
|
isActive: true,
|
|
languageCode: "es",
|
|
devices: "desktop",
|
|
serpDepth: 40,
|
|
scheduleInterval: "daily",
|
|
lastSkipReason: null,
|
|
}),
|
|
);
|
|
// Reactivation must not insert a duplicate row.
|
|
expect(mocks.createConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("throws when an active config already tracks the same domain + location", async () => {
|
|
mocks.getConfigByProjectDomainLocation.mockResolvedValue({
|
|
...archivedConfig,
|
|
isActive: true,
|
|
});
|
|
const { RankTrackingService } = await import("./RankTrackingService");
|
|
|
|
await expect(
|
|
RankTrackingService.createConfig(baseInput),
|
|
).rejects.toMatchObject({ code: "VALIDATION_ERROR" });
|
|
expect(mocks.updateConfig).not.toHaveBeenCalled();
|
|
expect(mocks.createConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("creates a new config when none exists for the domain + location", async () => {
|
|
mocks.getConfigByProjectDomainLocation.mockResolvedValue(null);
|
|
mocks.getConfigsForProject.mockResolvedValue([]);
|
|
mocks.createConfig.mockResolvedValue(undefined);
|
|
const { RankTrackingService } = await import("./RankTrackingService");
|
|
|
|
const result = await RankTrackingService.createConfig(baseInput);
|
|
|
|
expect(result.configId).toBeTruthy();
|
|
expect(mocks.createConfig).toHaveBeenCalledTimes(1);
|
|
expect(mocks.updateConfig).not.toHaveBeenCalled();
|
|
expect(mocks.createConfig).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
id: result.configId,
|
|
projectId: "project_1",
|
|
domain: "acme.com",
|
|
devices: "desktop",
|
|
serpDepth: 40,
|
|
scheduleInterval: "daily",
|
|
}),
|
|
);
|
|
});
|
|
});
|