feat: add email-server-worker with clean template architecture

Add new email-server-worker project implementing a self-scheduling background worker pattern with HTTP monitoring. Removed all business-specific code from copied source, creating a clean, reusable template.

Key features:
- Self-scheduling worker loop with configurable interval
- Graceful shutdown support (Docker-compatible)
- Prometheus metrics collection
- Health check endpoints (/healthcheck, /metrics, /ping)
- Example worker template for easy customization
- Comprehensive architecture documentation in CLAUDE.md

The worker is now ready for email server implementation with no external dependencies on Evolution/MSSQL/ElasticSearch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-30 09:03:58 +01:00
parent 7a526c5a85
commit 25c2f09eef
30 changed files with 1374 additions and 17 deletions

View File

@@ -0,0 +1,29 @@
import { LabelValues } from "prom-client";
export class Counter {
public inc() {
}
}
export class Histogram<T extends string> {
startTimer(labels?: LabelValues<T>): (labels?: LabelValues<T>) => void {
return((labels?: LabelValues<T>) => { });
}
}
class Register {
public setDefaultLabels(labels: Object) {
}
public metrics(): Promise<string> {
return(Promise.resolve(""));
}
public get contentType() {
return("");
}
}
export const register = new Register();

View File

@@ -0,0 +1,33 @@
import { Request, Response, NextFunction } from 'express';
import { NgitLocals } from "../../src/types/NgitLocals";
interface IMockHttpContext {
reqPath?:string
headersSent?:boolean
writableEnded?:boolean
method?:string
}
export const mockHttpContext = ({reqPath="/", headersSent=false, writableEnded=false, method="GET"}:IMockHttpContext|undefined = {}) => {
const req = {
path:reqPath,
method,
url:`https://localhost${reqPath}`,
params: {},
} as unknown as Request;
const res = {
end: jest.fn(),
status: jest.fn(),
setHeader: jest.fn(),
locals: {
stopPrometheusTimer: jest.fn(),
} as unknown as NgitLocals,
headersSent,
writableEnded,
} as unknown as Response;
const next:NextFunction = jest.fn();
return({req,res,next})
}

View File

@@ -0,0 +1,118 @@
import { errorRouter } from '../../src/routes/errorRouter';
import createError from "http-errors";
import { mockHttpContext } from "../helpers/mockHttpContext";
describe("errorRouter", () => {
beforeEach(() => {
mockWrite.mockClear();
});
test("u slučaju greške 404 mora vratiti string poruku 'page not found'", async () => {
const err = createError(404)
const {req,res,next} = mockHttpContext();
await errorRouter(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', "text/html");
expect(res.end).toHaveBeenCalledWith("page not found");
});
test("u slučaju greške 404 mora logirati request, response i tekst greške", async () => {
const err = createError(404)
const reqPath = "/neki-path/";
const {req,res,next} = mockHttpContext({ reqPath });
await errorRouter(err, req, res, next);
expect(res.locals.logger.info).toHaveBeenCalledWith("response", "page not found");
expect(res.locals.logger.error).toHaveBeenCalledWith(err.name, "page "+req.path+" not found");
});
test("ako su header-i već poslani, ne smiju biti poslani još jednom", async () => {
const err = createError(404)
const {req,res,next} = mockHttpContext({ headersSent:true, writableEnded:true });
await errorRouter(err, req, res, next);
expect(res.status).not.toHaveBeenCalled();
expect(res.setHeader).not.toHaveBeenCalled();
expect(res.end).not.toHaveBeenCalled();
});
test("ako NIJE već pozvana [end] metoda, treba je pozvati", async () => {
const err = createError(404)
const {req,res,next} = mockHttpContext({ headersSent:true, writableEnded:false });
await errorRouter(err, req, res, next);
expect(res.status).not.toHaveBeenCalled();
expect(res.setHeader).not.toHaveBeenCalled();
expect(res.end).toHaveBeenCalled();
});
test("mora zaustaviti Prometheus Timer", async () => {
const err = createError(404)
const {req,res,next} = mockHttpContext({ headersSent:true, writableEnded:false });
await errorRouter(err, req, res, next);
expect(res.locals.stopPrometheusTimer).toHaveBeenCalled();
});
test("u slučaju greške 500 mora vratiti string poruku 'internal server error'", async () => {
const err = createError(500)
const {req,res,next} = mockHttpContext();
await errorRouter(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', "text/html");
expect(res.end).toHaveBeenCalledWith("internal server error");
});
test("u slučaju greške 400 mora vratiti string poruku 'bad request' i logirati grešku", async () => {
const errorMessage = "mock error text 1";
const err = createError(400, errorMessage);
const {req,res,next} = mockHttpContext();
await errorRouter(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', "text/html");
expect(res.end).toHaveBeenCalledWith("bad request");
expect(res.locals.logger.errorwrite).toHaveBeenCalledWith(err.name, errorMessage);
});
test("u slučaju greške 401 mora vratiti string poruku 'unauthorized' i logirati grešku", async () => {
const errorMessage = "mock error text 2";
const err = createError(401, errorMessage)
const {req,res,next} = mockHttpContext();
await errorRouter(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', "text/html");
expect(res.end).toHaveBeenCalledWith("unauthorized");
expect(res.locals.logger.error).toHaveBeenCalledWith(err.name, errorMessage);
});
test("u slučaju greške 403 mora vratiti string poruku 'forbidden' i logirati grešku", async () => {
const errorMessage = "mock error text 3";
const err = createError(403, errorMessage);
const {req,res,next} = mockHttpContext();
await errorRouter(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', "text/html");
expect(res.end).toHaveBeenCalledWith("forbidden");
expect(res.locals.logger.error).toHaveBeenCalledWith(err.name, errorMessage);
});
});