feat: implementar pruebas para la inicialización de usuarios y estructuras en la base de datos
This commit is contained in:
parent
e4d355d00f
commit
8fcbf422f4
206
server/src/infrastructure/http/server.test.ts
Normal file
206
server/src/infrastructure/http/server.test.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
const sync = jest.fn();
|
||||||
|
const closeDatabase = jest.fn();
|
||||||
|
const createAdapter = jest.fn();
|
||||||
|
const updateDealer = jest.fn();
|
||||||
|
const updateQuote = jest.fn();
|
||||||
|
const prepareStructure = jest.fn();
|
||||||
|
const prepareUsers = jest.fn();
|
||||||
|
const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||||
|
const server = Object.assign(new EventEmitter(), {
|
||||||
|
listening: false,
|
||||||
|
listen: jest.fn(),
|
||||||
|
close: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("http", () => ({ createServer: () => server }));
|
||||||
|
jest.mock("@/config", () => ({ config: { environment: "test", server: { port: 3001 } } }));
|
||||||
|
jest.mock("../express/app", () => ({ app: {} }));
|
||||||
|
jest.mock("../logger", () => ({ initLogger: () => logger }));
|
||||||
|
jest.mock("@/contexts/common/infrastructure/sequelize", () => ({ createSequelizeAdapter: () => createAdapter() }));
|
||||||
|
jest.mock("@/contexts/sales/infrastructure/sequelize/dealer.model", () => ({
|
||||||
|
DEFAULT_TAX_LABEL: "% IVA", Dealer_Model: { update: (...args: unknown[]) => updateDealer(...args) },
|
||||||
|
}));
|
||||||
|
jest.mock("@/contexts/sales/infrastructure/sequelize/quote.model", () => ({
|
||||||
|
DEFAULT_QUOTE_TAX_LABEL: "% IVA", Quote_Model: { update: (...args: unknown[]) => updateQuote(...args) },
|
||||||
|
}));
|
||||||
|
jest.mock("../sequelize", () => ({
|
||||||
|
initStructure: (...args: unknown[]) => prepareStructure(...args),
|
||||||
|
insertUsers: () => prepareUsers(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const originalExitCode = process.exitCode;
|
||||||
|
let handlers: Record<string, (...args: unknown[]) => void>;
|
||||||
|
let exit: jest.SpyInstance;
|
||||||
|
const flush = () => new Promise<void>((resolve) => setImmediate(resolve));
|
||||||
|
const loadServer = () => { jest.isolateModules(() => { require("./server"); }); };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.exitCode = undefined;
|
||||||
|
server.removeAllListeners();
|
||||||
|
server.listening = false;
|
||||||
|
handlers = {};
|
||||||
|
jest.spyOn(process, "on").mockImplementation(((event: string, handler: (...args: unknown[]) => void) => {
|
||||||
|
handlers[event] = handler;
|
||||||
|
return process;
|
||||||
|
}) as typeof process.on);
|
||||||
|
jest.spyOn(process, "once").mockImplementation(((event: string, handler: (...args: unknown[]) => void) => {
|
||||||
|
handlers[event] = handler;
|
||||||
|
return process;
|
||||||
|
}) as typeof process.once);
|
||||||
|
exit = jest.spyOn(process, "exit").mockImplementation((() => undefined) as never);
|
||||||
|
sync.mockResolvedValue(undefined);
|
||||||
|
closeDatabase.mockResolvedValue(undefined);
|
||||||
|
createAdapter.mockReturnValue({ sync, connection: { close: closeDatabase } });
|
||||||
|
updateDealer.mockResolvedValue(undefined);
|
||||||
|
updateQuote.mockResolvedValue(undefined);
|
||||||
|
prepareStructure.mockResolvedValue(undefined);
|
||||||
|
prepareUsers.mockResolvedValue(undefined);
|
||||||
|
server.listen.mockImplementation(() => {
|
||||||
|
server.listening = true;
|
||||||
|
server.emit("listening");
|
||||||
|
});
|
||||||
|
server.close.mockImplementation((callback: (error?: Error) => void) => {
|
||||||
|
server.listening = false;
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
jest.useRealTimers();
|
||||||
|
process.exitCode = originalExitCode;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits for each initialization step before opening HTTP", async () => {
|
||||||
|
const order: string[] = [];
|
||||||
|
for (const [name, step] of [
|
||||||
|
["sync", sync], ["dealer", updateDealer], ["quote", updateQuote],
|
||||||
|
["structure", prepareStructure], ["users", prepareUsers],
|
||||||
|
] as const) step.mockImplementation(async () => { order.push(name); });
|
||||||
|
server.listen.mockImplementation(() => { order.push("listen"); server.emit("listening"); });
|
||||||
|
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(order).toEqual(["sync", "dealer", "quote", "structure", "users", "listen"]);
|
||||||
|
expect(sync).toHaveBeenCalledWith({ force: false, alter: true });
|
||||||
|
expect(exit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["structure", "users"])("does not listen while %s initialization is pending", async (stage) => {
|
||||||
|
let finish!: () => void;
|
||||||
|
const step = stage === "structure" ? prepareStructure : prepareUsers;
|
||||||
|
step.mockReturnValue(new Promise<void>((resolve) => { finish = resolve; }));
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
expect(server.listen).not.toHaveBeenCalled();
|
||||||
|
finish();
|
||||||
|
await flush();
|
||||||
|
expect(server.listen).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["sync", "dealer", "quote", "structure", "users"])("closes the database and exits on %s failure without opening HTTP", async (stage) => {
|
||||||
|
const steps = { sync, dealer: updateDealer, quote: updateQuote, structure: prepareStructure, users: prepareUsers };
|
||||||
|
const failure = new Error("Initialization failed");
|
||||||
|
steps[stage as keyof typeof steps].mockRejectedValue(failure);
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
expect(server.listen).not.toHaveBeenCalled();
|
||||||
|
expect(server.close).not.toHaveBeenCalled();
|
||||||
|
expect(closeDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(logger.error).toHaveBeenCalledWith("Server startup or execution failed", failure);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles synchronous adapter creation failure", async () => {
|
||||||
|
createAdapter.mockImplementationOnce(() => { throw new Error("Adapter failed"); });
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
expect(server.listen).not.toHaveBeenCalled();
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles an occupied port and closes the database without closing an unstarted HTTP server", async () => {
|
||||||
|
server.listen.mockImplementation(() => { server.emit("error", Object.assign(new Error("Port occupied"), { code: "EADDRINUSE" })); });
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
expect(server.close).not.toHaveBeenCalled();
|
||||||
|
expect(closeDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["SIGINT", "SIGTERM"])("closes HTTP then the database on %s, only once", async (signal) => {
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
handlers[signal]();
|
||||||
|
handlers[signal]();
|
||||||
|
await flush();
|
||||||
|
expect(server.close).toHaveBeenCalledTimes(1);
|
||||||
|
expect(closeDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(server.close.mock.invocationCallOrder[0]).toBeLessThan(closeDatabase.mock.invocationCallOrder[0]);
|
||||||
|
expect(exit).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["uncaughtException", "unhandledRejection"])("terminates on %s instead of continuing to serve", async (event) => {
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
handlers[event](new Error("Fatal failure"));
|
||||||
|
await flush();
|
||||||
|
expect(server.close).toHaveBeenCalledTimes(1);
|
||||||
|
expect(closeDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles HTTP errors after startup", async () => {
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
server.emit("error", new Error("HTTP failure"));
|
||||||
|
await flush();
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(closeDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still closes the database if HTTP close fails", async () => {
|
||||||
|
server.close.mockImplementation((callback: (error?: Error) => void) => callback(new Error("Close failed")));
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
handlers.SIGTERM();
|
||||||
|
await flush();
|
||||||
|
expect(closeDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exits with an error if database close fails", async () => {
|
||||||
|
closeDatabase.mockRejectedValue(new Error("Close failed"));
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
handlers.SIGTERM();
|
||||||
|
await flush();
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not open HTTP if interrupted during initialization", async () => {
|
||||||
|
let finish!: () => void;
|
||||||
|
prepareStructure.mockReturnValue(new Promise<void>((resolve) => { finish = resolve; }));
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
handlers.SIGTERM();
|
||||||
|
finish();
|
||||||
|
await flush();
|
||||||
|
expect(server.listen).not.toHaveBeenCalled();
|
||||||
|
expect(prepareUsers).not.toHaveBeenCalled();
|
||||||
|
expect(exit).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forces an error exit when HTTP shutdown exceeds the deadline", async () => {
|
||||||
|
loadServer();
|
||||||
|
await flush();
|
||||||
|
jest.useFakeTimers();
|
||||||
|
server.close.mockImplementation(() => undefined);
|
||||||
|
handlers.SIGTERM();
|
||||||
|
jest.advanceTimersByTime(30000);
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-use-before-define */
|
|
||||||
import rTracer from "cls-rtracer";
|
import rTracer from "cls-rtracer";
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import { assign } from "lodash";
|
import { assign } from "lodash";
|
||||||
@ -13,7 +12,6 @@ import {
|
|||||||
DEFAULT_QUOTE_TAX_LABEL,
|
DEFAULT_QUOTE_TAX_LABEL,
|
||||||
Quote_Model,
|
Quote_Model,
|
||||||
} from "@/contexts/sales/infrastructure/sequelize/quote.model";
|
} from "@/contexts/sales/infrastructure/sequelize/quote.model";
|
||||||
import { trace } from "console";
|
|
||||||
import { config } from "../../config";
|
import { config } from "../../config";
|
||||||
import { app } from "../express/app";
|
import { app } from "../express/app";
|
||||||
import { initLogger } from "../logger";
|
import { initLogger } from "../logger";
|
||||||
@ -37,55 +35,6 @@ export const currentState = assign(
|
|||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
|
||||||
const serverStop = (server: http.Server) => {
|
|
||||||
const forceTimeout = 30000;
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
logger.warn("⚡️ Shutting down server");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
logger.error("Could not close connections in time, forcefully shutting down");
|
|
||||||
resolve();
|
|
||||||
}, forceTimeout).unref();
|
|
||||||
|
|
||||||
server.close((err) => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Closed out remaining connections.");
|
|
||||||
logger.info("Bye!");
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*const now = DateTime.now();
|
|
||||||
// Destroy server and available connections.
|
|
||||||
logger.info(`Time: ${now.toLocaleString()}`);
|
|
||||||
logger.info('Shutting down at: ' + new Date());
|
|
||||||
if (server) {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Bye!');
|
|
||||||
process.exit(1);*/
|
|
||||||
};
|
|
||||||
|
|
||||||
const serverError = (error: any) => {
|
|
||||||
if (error.code === "EADDRINUSE") {
|
|
||||||
logger.debug(`⛔️ Server wasn't able to start properly.`);
|
|
||||||
logger.error(`The port ${error.port} is already used by another application.`);
|
|
||||||
} else {
|
|
||||||
logger.debug(`⛔️ Server wasn't able to start properly.`);
|
|
||||||
logger.error(error);
|
|
||||||
trace(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
serverStop(server);
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const serverConnection = (conn: any) => {
|
const serverConnection = (conn: any) => {
|
||||||
const key = `${conn.remoteAddress}:${conn.remotePort}`;
|
const key = `${conn.remoteAddress}:${conn.remotePort}`;
|
||||||
currentState.connections[key] = conn;
|
currentState.connections[key] = conn;
|
||||||
@ -95,57 +44,108 @@ const serverConnection = (conn: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sequelizeConn = createSequelizeAdapter();
|
let sequelizeConn: ReturnType<typeof createSequelizeAdapter> | undefined;
|
||||||
//const firebirdConn = createFirebirdAdapter();
|
let stopping = false;
|
||||||
|
|
||||||
const server: http.Server = http
|
const server = http
|
||||||
.createServer(app)
|
.createServer(app)
|
||||||
.once("listening", () =>
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
//firebirdConn.disconnect();
|
|
||||||
serverStop(server);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.on("close", () =>
|
.on("close", () =>
|
||||||
logger.info(`Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`)
|
logger.info(`Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`)
|
||||||
)
|
)
|
||||||
.on("connection", serverConnection)
|
.on("connection", serverConnection);
|
||||||
.on("error", serverError);
|
|
||||||
|
const stopServer = async (exitCode: number) => {
|
||||||
|
if (exitCode !== 0) process.exitCode = exitCode;
|
||||||
|
if (stopping) return;
|
||||||
|
stopping = true;
|
||||||
|
logger.warn("⚡️ Shutting down server");
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
logger.error("Could not close connections in time, forcefully shutting down");
|
||||||
|
process.exit(1);
|
||||||
|
}, 30000);
|
||||||
|
timeout.unref();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (server.listening) {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => (error ? reject(error) : resolve()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
logger.error("Could not close HTTP server", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sequelizeConn?.connection.close();
|
||||||
|
} catch (error) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
logger.error("Could not close database connection", error);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
process.exit(process.exitCode || exitCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const failServer = (error: unknown) => {
|
||||||
|
logger.error("Server startup or execution failed", error);
|
||||||
|
void stopServer(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.once("SIGINT", () => { void stopServer(0); });
|
||||||
|
process.once("SIGTERM", () => { void stopServer(0); });
|
||||||
|
process.on("uncaughtException", failServer);
|
||||||
|
process.on("unhandledRejection", failServer);
|
||||||
|
|
||||||
|
const startServer = async () => {
|
||||||
|
try {
|
||||||
|
sequelizeConn = createSequelizeAdapter();
|
||||||
|
await sequelizeConn.sync({ force: false, alter: true });
|
||||||
|
if (stopping) return;
|
||||||
|
|
||||||
try {
|
|
||||||
//firebirdConn.sync().then(() => {
|
|
||||||
sequelizeConn.sync({ force: false, alter: true }).then(async () => {
|
|
||||||
await Dealer_Model.update(
|
await Dealer_Model.update(
|
||||||
{ default_tax_label: DEFAULT_TAX_LABEL },
|
{ default_tax_label: DEFAULT_TAX_LABEL },
|
||||||
{ where: { default_tax_label: null } }
|
{ where: { default_tax_label: null } }
|
||||||
);
|
);
|
||||||
|
if (stopping) return;
|
||||||
await Quote_Model.update(
|
await Quote_Model.update(
|
||||||
{ tax_label: DEFAULT_QUOTE_TAX_LABEL },
|
{ tax_label: DEFAULT_QUOTE_TAX_LABEL },
|
||||||
{ where: { tax_label: null } }
|
{ where: { tax_label: null } }
|
||||||
);
|
);
|
||||||
|
if (stopping) return;
|
||||||
|
|
||||||
initStructure(sequelizeConn.connection);
|
await initStructure(sequelizeConn.connection);
|
||||||
insertUsers();
|
if (stopping) return;
|
||||||
|
await insertUsers();
|
||||||
|
if (stopping) return;
|
||||||
|
|
||||||
// Launch server
|
await new Promise<void>((resolve, reject) => {
|
||||||
server.listen(currentState.server.port, () => {
|
const onError = (error: Error) => {
|
||||||
const now = DateTime.now();
|
server.removeListener("listening", onListening);
|
||||||
logger.info(`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`);
|
reject(error);
|
||||||
logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`);
|
};
|
||||||
logger.info(`Environment: ${currentState.environment}`);
|
const onListening = () => {
|
||||||
logger.info(`Process PID: ${process.pid}`);
|
server.removeListener("error", onError);
|
||||||
logger.info("To shut down your server, press <CTRL> + C at any time");
|
server.on("error", failServer);
|
||||||
logger.info(`⚡️ Server: http://${currentState.server.hostname}:${currentState.server.port}`);
|
resolve();
|
||||||
|
};
|
||||||
|
server.once("error", onError);
|
||||||
|
server.once("listening", onListening);
|
||||||
|
server.listen(currentState.server.port);
|
||||||
});
|
});
|
||||||
});
|
if (stopping) return;
|
||||||
//});
|
|
||||||
} catch (error) {
|
|
||||||
serverError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("uncaughtException", (error: any) => {
|
const now = DateTime.now();
|
||||||
//firebirdConn.disconnect();
|
logger.info(`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`);
|
||||||
logger.error(`${new Date().toUTCString()} uncaughtException:`, error.message);
|
logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`);
|
||||||
logger.error(error.stack);
|
logger.info(`Environment: ${currentState.environment}`);
|
||||||
//process.exit(1);
|
logger.info(`Process PID: ${process.pid}`);
|
||||||
});
|
logger.info("To shut down your server, press <CTRL> + C at any time");
|
||||||
|
logger.info(`⚡️ Server: http://${currentState.server.hostname}:${currentState.server.port}`);
|
||||||
|
} catch (error) {
|
||||||
|
failServer(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void startServer();
|
||||||
|
|||||||
56
server/src/infrastructure/sequelize/initData.test.ts
Normal file
56
server/src/infrastructure/sequelize/initData.test.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { insertUsers } from "./initData";
|
||||||
|
import { initializeAdmin, initializeSampleDealer, initializeSampleUser } from "@/contexts/users/application/userServices";
|
||||||
|
|
||||||
|
jest.mock("@/config", () => ({ config: { defaults: {} } }));
|
||||||
|
jest.mock("@/contexts/common/domain", () => ({ RepositoryManager: { getInstance: () => ({ getRepository: jest.fn() }) } }));
|
||||||
|
jest.mock("@/contexts/common/infrastructure/sequelize", () => ({ createSequelizeAdapter: jest.fn() }));
|
||||||
|
jest.mock("@/contexts/sales/infrastructure/Dealer.repository", () => ({ registerDealerRepository: jest.fn() }));
|
||||||
|
jest.mock("@/contexts/users/infrastructure/User.repository", () => ({ registerUserRepository: jest.fn() }));
|
||||||
|
jest.mock("@/contexts/users/application/userServices", () => ({
|
||||||
|
initializeAdmin: jest.fn(), initializeSampleUser: jest.fn(), initializeSampleDealer: jest.fn(),
|
||||||
|
}));
|
||||||
|
const admin = jest.mocked(initializeAdmin);
|
||||||
|
const user = jest.mocked(initializeSampleUser);
|
||||||
|
const dealer = jest.mocked(initializeSampleDealer);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
admin.mockResolvedValue(undefined);
|
||||||
|
user.mockResolvedValue(false);
|
||||||
|
dealer.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("awaits admin initialization before initializing the sample user", async () => {
|
||||||
|
let finish!: () => void;
|
||||||
|
admin.mockReturnValue(new Promise<void>((resolve) => { finish = resolve; }));
|
||||||
|
const pending = insertUsers();
|
||||||
|
expect(user).not.toHaveBeenCalled();
|
||||||
|
finish();
|
||||||
|
await pending;
|
||||||
|
expect(user).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dealer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["admin", "user", "dealer"])("propagates %s initialization errors", async (stage) => {
|
||||||
|
user.mockResolvedValue({} as Exclude<Awaited<ReturnType<typeof initializeSampleUser>>, boolean>);
|
||||||
|
const failure = new Error("Initialization failed");
|
||||||
|
({ admin, user, dealer })[stage as "admin" | "user" | "dealer"].mockRejectedValue(failure);
|
||||||
|
await expect(insertUsers()).rejects.toBe(failure);
|
||||||
|
expect(user).toHaveBeenCalledTimes(stage === "admin" ? 0 : 1);
|
||||||
|
expect(dealer).toHaveBeenCalledTimes(stage === "dealer" ? 1 : 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not complete until the sample dealer is initialized", async () => {
|
||||||
|
user.mockResolvedValue({} as Exclude<Awaited<ReturnType<typeof initializeSampleUser>>, boolean>);
|
||||||
|
let finish!: () => void;
|
||||||
|
dealer.mockReturnValue(new Promise<void>((resolve) => { finish = resolve; }));
|
||||||
|
let completed = false;
|
||||||
|
const pending = insertUsers().then(() => { completed = true; });
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(dealer).toHaveBeenCalledTimes(1);
|
||||||
|
expect(completed).toBe(false);
|
||||||
|
finish();
|
||||||
|
await pending;
|
||||||
|
expect(completed).toBe(true);
|
||||||
|
});
|
||||||
@ -20,14 +20,14 @@ export const insertUsers = async () => {
|
|||||||
registerUserRepository(context);
|
registerUserRepository(context);
|
||||||
registerDealerRepository(context);
|
registerDealerRepository(context);
|
||||||
|
|
||||||
initializeAdmin(context.adapter, context.repositoryManager.getRepository("User"));
|
await initializeAdmin(context.adapter, context.repositoryManager.getRepository("User"));
|
||||||
const user = await initializeSampleUser(
|
const user = await initializeSampleUser(
|
||||||
context.adapter,
|
context.adapter,
|
||||||
context.repositoryManager.getRepository("User")
|
context.repositoryManager.getRepository("User")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
initializeSampleDealer(
|
await initializeSampleDealer(
|
||||||
user,
|
user,
|
||||||
context.adapter,
|
context.adapter,
|
||||||
context.repositoryManager.getRepository("Dealer")
|
context.repositoryManager.getRepository("Dealer")
|
||||||
|
|||||||
32
server/src/infrastructure/sequelize/initStructure.test.ts
Normal file
32
server/src/infrastructure/sequelize/initStructure.test.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Sequelize } from "sequelize";
|
||||||
|
import { initStructure } from "./initStructure";
|
||||||
|
|
||||||
|
jest.mock("@/infrastructure/logger", () => ({ initLogger: () => ({ debug: jest.fn(), error: jest.fn() }) }));
|
||||||
|
|
||||||
|
it.each(["catalog", "catalog_translations", "v_catalog"])("propagates %s creation errors and stops initialization", async (object) => {
|
||||||
|
const failure = new Error("DDL failed");
|
||||||
|
const query = jest.fn(async (sql: string) => {
|
||||||
|
if (sql.includes("SELECT COUNT(*)")) return [[{ count: 0 }]];
|
||||||
|
if (sql.includes(`CREATE TABLE ${object} (`) || sql.includes(`CREATE VIEW ${object} AS`)) throw failure;
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
await expect(initStructure({ query } as unknown as Sequelize)).rejects.toBe(failure);
|
||||||
|
const lastSQL = query.mock.calls[query.mock.calls.length - 1][0];
|
||||||
|
expect(lastSQL).toContain(object === "v_catalog" ? `CREATE VIEW ${object}` : `CREATE TABLE ${object}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates metadata query errors", async () => {
|
||||||
|
const failure = new Error("Metadata failed");
|
||||||
|
const query = jest.fn().mockRejectedValue(failure);
|
||||||
|
await expect(initStructure({ query } as unknown as Sequelize)).rejects.toBe(failure);
|
||||||
|
expect(query).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("awaits catalog tables and view creation in order", async () => {
|
||||||
|
const query = jest.fn(async (sql: string) => sql.includes("SELECT COUNT(*)") ? [[{ count: 0 }]] : []);
|
||||||
|
await initStructure({ query } as unknown as Sequelize);
|
||||||
|
expect(query).toHaveBeenCalledTimes(6);
|
||||||
|
expect(query.mock.calls[1][0]).toContain("CREATE TABLE catalog (");
|
||||||
|
expect(query.mock.calls[3][0]).toContain("CREATE TABLE catalog_translations (");
|
||||||
|
expect(query.mock.calls[5][0]).toContain("CREATE VIEW v_catalog AS");
|
||||||
|
});
|
||||||
@ -49,6 +49,7 @@ const createCatalogTableIfNotExists = async (sequelize: Sequelize) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error al crear la tabla "catalog":', error);
|
logger.error('Error al crear la tabla "catalog":', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,6 +98,7 @@ const createCatalogTranslationsTableIfNotExists = async (sequelize: Sequelize) =
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error al crear la tabla "catalog_translations":', error);
|
logger.error('Error al crear la tabla "catalog_translations":', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -138,10 +140,11 @@ const createVCatalogViewIfNotExists = async (sequelize: Sequelize) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error al crear la vista "v_catalog":', error);
|
logger.error('Error al crear la vista "v_catalog":', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initStructure = async (sequelize) => {
|
export const initStructure = async (sequelize: Sequelize) => {
|
||||||
await createCatalogTableIfNotExists(sequelize);
|
await createCatalogTableIfNotExists(sequelize);
|
||||||
await createCatalogTranslationsTableIfNotExists(sequelize);
|
await createCatalogTranslationsTableIfNotExists(sequelize);
|
||||||
await createVCatalogViewIfNotExists(sequelize);
|
await createVCatalogViewIfNotExists(sequelize);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user