Remove unused service registry
This commit is contained in:
1 parent
837c4598ff
commit
4b1e8fb002
6 files changed
+333
-77
No files matched your search
@@ -19,7 +19,6 @@ import {
|
||||
AuthService,
|
||||
MonitorService,
|
||||
QueueService,
|
||||
ServiceRegistry,
|
||||
TeamService,
|
||||
RoleService,
|
||||
TeamMemberService,
|
||||
@@ -109,9 +108,5 @@ export const initServices = async () => {
|
||||
stripeService,
|
||||
};
|
||||
|
||||
Object.values(services).forEach((service) => {
|
||||
ServiceRegistry.register(service.SERVICE_NAME, service);
|
||||
});
|
||||
|
||||
return services;
|
||||
};
|
||||
@@ -22,7 +22,6 @@ export { default as StatusService } from "@/services/infrastructure/StatusServic
|
||||
export { default as JobQueue } from "@/services/infrastructure/JobQueue.js";
|
||||
export { default as JobGenerator } from "@/services/infrastructure/JobGenerator.js";
|
||||
|
||||
export { default as ServiceRegistry } from "@/services/system/ServiceRegistry.js";
|
||||
export { default as SettingsService } from "@/services/system/SettingsService.js";
|
||||
export { default as BillingService } from "@/services/system/BillingService.js";
|
||||
export { default as StripeService } from "@/services/system/StripeService.js";
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
const SERVICE_NAME = "ServiceRegistry";
|
||||
|
||||
export interface IServiceRegistry {
|
||||
register: (name: string, service: any) => void;
|
||||
get: (name: string) => any;
|
||||
listServices: () => string[];
|
||||
}
|
||||
|
||||
class ServiceRegistry implements IServiceRegistry {
|
||||
public SERVICE_NAME: string;
|
||||
private static _instance: ServiceRegistry;
|
||||
|
||||
private services: { [key: string]: any };
|
||||
|
||||
private constructor() {
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.services = {};
|
||||
}
|
||||
|
||||
static get instance(): ServiceRegistry {
|
||||
if (!this._instance) {
|
||||
this._instance = new ServiceRegistry();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return this.SERVICE_NAME;
|
||||
}
|
||||
|
||||
// Instance methods
|
||||
register(name: string, service: any) {
|
||||
this.services[name] = service;
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
if (!this.services[name]) {
|
||||
throw new Error(`Service ${name} is not registered`);
|
||||
}
|
||||
return this.services[name];
|
||||
}
|
||||
|
||||
listServices() {
|
||||
return Object.keys(this.services);
|
||||
}
|
||||
|
||||
static get(name: string) {
|
||||
if (!ServiceRegistry.instance) {
|
||||
throw new Error("ServiceRegistry not initialized");
|
||||
}
|
||||
return ServiceRegistry.instance.get(name);
|
||||
}
|
||||
|
||||
static register(name: string, service: any) {
|
||||
if (!ServiceRegistry.instance) {
|
||||
throw new Error("ServiceRegistry not initialized");
|
||||
}
|
||||
return ServiceRegistry.instance.register(name, service);
|
||||
}
|
||||
|
||||
static listServices() {
|
||||
if (!ServiceRegistry.instance) {
|
||||
throw new Error("ServiceRegistry not initialized");
|
||||
}
|
||||
return ServiceRegistry.instance.listServices();
|
||||
}
|
||||
}
|
||||
|
||||
export default ServiceRegistry;
|
||||
@@ -257,6 +257,106 @@ describe("StatusService.updateMonitorStats", () => {
|
||||
expect(stats.avgResponseTime).toBeCloseTo((200 * 10 + 400) / 11);
|
||||
expect(stats.uptimePercentage).toBeCloseTo(7 / 11);
|
||||
expect(stats.lastResponseTime).toBe(400);
|
||||
expect(stats.maxResponseTime).toBe(400);
|
||||
});
|
||||
expect(stats.maxResponseTime).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
// Additional coverage: evaluateThresholds branches
|
||||
describe("StatusService.evaluateThresholds", () => {
|
||||
const service = new StatusService();
|
||||
|
||||
const buildCheck = (systemOverrides = {}) => ({
|
||||
system: {
|
||||
cpu: { usage_percent: 0.2, temperature: [40, 45] },
|
||||
memory: { usage_percent: 0.3 },
|
||||
disk: [{ usage_percent: 0.4 }],
|
||||
...systemOverrides,
|
||||
},
|
||||
});
|
||||
|
||||
it("returns early when monitor is not infrastructure or not up", async () => {
|
||||
const { monitor } = buildMonitor({ type: "http", status: "up", thresholds: { cpu: 50 } });
|
||||
const check = buildCheck();
|
||||
const res1 = await service.evaluateThresholds(monitor, check);
|
||||
expect(res1).toEqual({ hasBreach: false, notes: [], metrics: expect.any(Object) });
|
||||
|
||||
const { monitor: m2 } = buildMonitor({ type: "infrastructure", status: "down", thresholds: { cpu: 50 } });
|
||||
const res2 = await service.evaluateThresholds(m2, check);
|
||||
expect(res2).toEqual({ hasBreach: false, notes: [], metrics: expect.any(Object) });
|
||||
});
|
||||
|
||||
it("returns early when thresholds or system missing", async () => {
|
||||
const { monitor } = buildMonitor({ type: "infrastructure", status: "up" });
|
||||
const check = buildCheck();
|
||||
const res1 = await service.evaluateThresholds(monitor, check);
|
||||
expect(res1.hasBreach).toBe(false);
|
||||
|
||||
const { monitor: m2 } = buildMonitor({ type: "infrastructure", status: "up", thresholds: { cpu: 50 } });
|
||||
const res2 = await service.evaluateThresholds(m2, { system: undefined });
|
||||
expect(res2.hasBreach).toBe(false);
|
||||
});
|
||||
|
||||
it("detects cpu/memory/disk/temperature breaches and fills notes", async () => {
|
||||
const { monitor } = buildMonitor({
|
||||
type: "infrastructure",
|
||||
status: "up",
|
||||
thresholds: { cpu: 50, memory: 60, disk: 80, temperature: 70 },
|
||||
});
|
||||
const check = buildCheck({
|
||||
cpu: { usage_percent: 0.8, temperature: [60, 75] }, // 80% & 75C
|
||||
memory: { usage_percent: 0.7 }, // 70%
|
||||
disk: [{ usage_percent: 0.5 }, { usage_percent: 0.95 }], // max 95%
|
||||
});
|
||||
const res = await service.evaluateThresholds(monitor, check);
|
||||
expect(res.hasBreach).toBe(true);
|
||||
expect(res.notes).toEqual(
|
||||
expect.arrayContaining([
|
||||
"cpu 80% > 50%",
|
||||
"memory 70% > 60%",
|
||||
"disk 95.00% > 80%",
|
||||
"temperature 75C > 70C",
|
||||
])
|
||||
);
|
||||
expect(res.metrics.cpu).toMatchObject({ value: 80, threshold: 50, breached: true });
|
||||
expect(res.metrics.memory).toMatchObject({ value: 70, threshold: 60, breached: true });
|
||||
expect(res.metrics.disk).toMatchObject({ value: 95, threshold: 80, breached: true });
|
||||
});
|
||||
|
||||
it("handles non-breach values and undefined fields safely", async () => {
|
||||
const { monitor } = buildMonitor({
|
||||
type: "infrastructure",
|
||||
status: "up",
|
||||
thresholds: { cpu: 95, memory: 95, disk: 99, temperature: 100 },
|
||||
});
|
||||
const check = buildCheck({
|
||||
cpu: { usage_percent: undefined, temperature: [] },
|
||||
memory: {},
|
||||
disk: [{}, { usage_percent: 0.1 }], // max 10%
|
||||
});
|
||||
const res = await service.evaluateThresholds(monitor, check);
|
||||
expect(res.hasBreach).toBe(false);
|
||||
expect(res.notes).toEqual([]);
|
||||
expect(res.metrics.cpu.breached).toBe(false);
|
||||
expect(res.metrics.memory.breached).toBe(false);
|
||||
expect(res.metrics.disk.breached).toBe(false);
|
||||
});
|
||||
|
||||
it("returns safe default when unexpected error occurs", async () => {
|
||||
const { monitor } = buildMonitor({
|
||||
type: "infrastructure",
|
||||
status: "up",
|
||||
thresholds: { cpu: 50 },
|
||||
});
|
||||
// Create a system object that throws when accessed to exercise catch {}
|
||||
const throwingSystem = new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
throw new Error("boom");
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await service.evaluateThresholds(monitor, { system: throwingSystem });
|
||||
expect(res).toEqual({ hasBreach: false, notes: [], metrics: expect.any(Object) });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import BillingService from "@/services/system/BillingService.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
|
||||
// Mock Org model
|
||||
jest.mock("@/db/models/index.js", () => ({
|
||||
Org: { findById: jest.fn(), findOne: jest.fn() },
|
||||
}));
|
||||
import { Org } from "@/db/models/index.js";
|
||||
|
||||
// Capture a shared mock Stripe client instance
|
||||
jest.mock("stripe", () => {
|
||||
const mockClient = {
|
||||
prices: { list: jest.fn() },
|
||||
customers: { create: jest.fn() },
|
||||
checkout: { sessions: { create: jest.fn() } },
|
||||
billingPortal: { sessions: { create: jest.fn() } },
|
||||
subscriptions: { retrieve: jest.fn() },
|
||||
};
|
||||
const MockStripe = jest.fn(() => mockClient);
|
||||
MockStripe.__mockClient = mockClient;
|
||||
return { __esModule: true, default: MockStripe };
|
||||
});
|
||||
import Stripe from "stripe";
|
||||
|
||||
describe("BillingService", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("listPlans", () => {
|
||||
it("lists plans in order and maps prices by lookup_key", async () => {
|
||||
(Stripe.__mockClient).prices.list.mockResolvedValue({
|
||||
data: [
|
||||
{ lookup_key: "free", unit_amount: 0 },
|
||||
{ lookup_key: "pro", unit_amount: 1000 },
|
||||
{ lookup_key: "business", unit_amount: 2500 },
|
||||
{ lookup_key: "enterprise", unit_amount: 9999 },
|
||||
],
|
||||
});
|
||||
const svc = new BillingService();
|
||||
const plans = await svc.listPlans();
|
||||
expect(plans.map((p) => p.plan)).toEqual([
|
||||
"free",
|
||||
"pro",
|
||||
"business",
|
||||
"enterprise",
|
||||
]);
|
||||
expect(plans.find((p) => p.plan === "pro")?.price).toBe(1000);
|
||||
expect(plans.find((p) => p.plan === "enterprise")?.price).toBe(9999);
|
||||
});
|
||||
});
|
||||
|
||||
describe("subscribePlan", () => {
|
||||
it("throws when planKey missing", async () => {
|
||||
const svc = new BillingService();
|
||||
await expect(svc.subscribePlan("e@x.com", "org1", undefined)).rejects.toMatchObject({
|
||||
message: "planKey is required",
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws when planKey invalid", async () => {
|
||||
const svc = new BillingService();
|
||||
await expect(svc.subscribePlan("e@x.com", "org1", "bogus"))
|
||||
.rejects.toMatchObject({ message: "Invalid planKey", status: 400 });
|
||||
});
|
||||
|
||||
it("throws when org not found", async () => {
|
||||
Org.findById.mockResolvedValue(null);
|
||||
const svc = new BillingService();
|
||||
await expect(svc.subscribePlan("e@x.com", "org1", "pro"))
|
||||
.rejects.toMatchObject({ message: "Organization not found", status: 404 });
|
||||
});
|
||||
|
||||
it("creates customer and new subscription checkout when none exists", async () => {
|
||||
const org = { _id: "org1", save: jest.fn(), billingCustomerId: null, subscriptionId: null };
|
||||
Org.findById.mockResolvedValue(org);
|
||||
(Stripe.__mockClient).customers.create.mockResolvedValue({ id: "cus_123" });
|
||||
(Stripe.__mockClient).prices.list.mockResolvedValue({ data: [{ id: "price_123", lookup_key: "pro" }] });
|
||||
(Stripe.__mockClient).checkout.sessions.create.mockResolvedValue({ url: "https://checkout" });
|
||||
|
||||
const svc = new BillingService();
|
||||
const url = await svc.subscribePlan("e@x.com", "org1", "pro");
|
||||
expect(org.billingCustomerId).toBe("cus_123");
|
||||
expect(org.save).toHaveBeenCalled();
|
||||
expect(Stripe.__mockClient.checkout.sessions.create).toHaveBeenCalled();
|
||||
expect(url).toBe("https://checkout");
|
||||
});
|
||||
|
||||
it("updates existing subscription via billing portal when subscriptionId exists", async () => {
|
||||
const org = { _id: "org1", save: jest.fn(), billingCustomerId: "cus_123", subscriptionId: "sub_1" };
|
||||
Org.findById.mockResolvedValue(org);
|
||||
(Stripe.__mockClient).prices.list.mockResolvedValue({ data: [{ id: "price_123", lookup_key: "pro" }] });
|
||||
(Stripe.__mockClient).billingPortal.sessions.create.mockResolvedValue({ url: "https://portal" });
|
||||
|
||||
const svc = new BillingService();
|
||||
const url = await svc.subscribePlan("e@x.com", "org1", "pro");
|
||||
expect(Stripe.__mockClient.billingPortal.sessions.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ customer: "cus_123" })
|
||||
);
|
||||
expect(url).toBe("https://portal");
|
||||
});
|
||||
|
||||
it("throws when no price found for plan", async () => {
|
||||
const org = { _id: "org1", save: jest.fn(), billingCustomerId: "cus_123", subscriptionId: null };
|
||||
Org.findById.mockResolvedValue(org);
|
||||
(Stripe.__mockClient).prices.list.mockResolvedValue({ data: [] });
|
||||
const svc = new BillingService();
|
||||
await expect(svc.subscribePlan("e@x.com", "org1", "pro"))
|
||||
.rejects.toMatchObject({ message: "Price not found for the selected plan", status: 404 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelSubscription", () => {
|
||||
it("throws when org not found", async () => {
|
||||
Org.findById.mockResolvedValue(null);
|
||||
const svc = new BillingService();
|
||||
await expect(svc.cancelSubscription("org1"))
|
||||
.rejects.toMatchObject({ message: "Organization not found", status: 404 });
|
||||
});
|
||||
|
||||
it("throws when no active subscription found", async () => {
|
||||
Org.findById.mockResolvedValue({ _id: "org1", subscriptionId: null });
|
||||
const svc = new BillingService();
|
||||
await expect(svc.cancelSubscription("org1"))
|
||||
.rejects.toMatchObject({ message: "No active subscription found", status: 400 });
|
||||
});
|
||||
|
||||
it("returns billing portal url on success", async () => {
|
||||
Org.findById.mockResolvedValue({ _id: "org1", billingCustomerId: "cus_1", subscriptionId: "sub_1" });
|
||||
(Stripe.__mockClient).billingPortal.sessions.create.mockResolvedValue({ url: "https://cancel" });
|
||||
const svc = new BillingService();
|
||||
const url = await svc.cancelSubscription("org1");
|
||||
expect(url).toBe("https://cancel");
|
||||
});
|
||||
|
||||
it("throws when billing portal session lacks url", async () => {
|
||||
Org.findById.mockResolvedValue({ _id: "org1", billingCustomerId: "cus_1", subscriptionId: "sub_1" });
|
||||
(Stripe.__mockClient).billingPortal.sessions.create.mockResolvedValue({ url: null });
|
||||
const svc = new BillingService();
|
||||
await expect(svc.cancelSubscription("org1"))
|
||||
.rejects.toMatchObject({ message: "Failed to create billing portal session", status: 500 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("confirmPlan", () => {
|
||||
it("throws when org not found", async () => {
|
||||
Org.findOne.mockResolvedValue(null);
|
||||
const svc = new BillingService();
|
||||
await expect(svc.confirmPlan("org1", "pro"))
|
||||
.rejects.toMatchObject({ message: "Organization not found", status: 404 });
|
||||
});
|
||||
|
||||
it("returns true only when expected is free if no subscriptionId", async () => {
|
||||
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: null });
|
||||
const svc = new BillingService();
|
||||
await expect(svc.confirmPlan("org1", "free")).resolves.toBe(true);
|
||||
await expect(svc.confirmPlan("org1", "pro")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when active plan equals org.planKey", async () => {
|
||||
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: "sub1", planKey: "business" });
|
||||
(Stripe.__mockClient).subscriptions.retrieve.mockResolvedValue({ items: { data: [{ price: { lookup_key: "business" } }] } });
|
||||
const svc = new BillingService();
|
||||
await expect(svc.confirmPlan("org1", "pro")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when active plan equals expectedPlanKey", async () => {
|
||||
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: "sub1", planKey: "pro" });
|
||||
(Stripe.__mockClient).subscriptions.retrieve.mockResolvedValue({ items: { data: [{ price: { lookup_key: "enterprise" } }] } });
|
||||
const svc = new BillingService();
|
||||
await expect(svc.confirmPlan("org1", "enterprise")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when active plan differs from expected", async () => {
|
||||
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: "sub1", planKey: "pro" });
|
||||
(Stripe.__mockClient).subscriptions.retrieve.mockResolvedValue({ items: { data: [{ price: { lookup_key: "business" } }] } });
|
||||
const svc = new BillingService();
|
||||
await expect(svc.confirmPlan("org1", "enterprise")).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { EntitlementsFactory, SaaSEntitlementsProvider, SelfHostedEntitlementsProvider } from "@/services/system/EntitlementsService.js";
|
||||
import { Org } from "@/db/models/index.js";
|
||||
|
||||
jest.mock("@/db/models/index.js", () => ({
|
||||
Org: { findById: jest.fn() },
|
||||
}));
|
||||
|
||||
describe("EntitlementsService", () => {
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
test("SaaS provider maps plan keys to Plans", async () => {
|
||||
const cases = [
|
||||
{ planKey: "free", expected: "free" },
|
||||
{ planKey: "pro", expected: "pro" },
|
||||
{ planKey: "business", expected: "business" },
|
||||
{ planKey: "enterprise", expected: "enterprise" },
|
||||
{ planKey: undefined, expected: "free" },
|
||||
];
|
||||
|
||||
for (const c of cases) {
|
||||
Org.findById.mockReturnValueOnce({ lean: () => Promise.resolve({ planKey: c.planKey }) });
|
||||
const provider = new SaaSEntitlementsProvider();
|
||||
const ent = await provider.getForOrg("org1");
|
||||
expect(ent.plan).toBe(c.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("Self-hosted provider returns unlimited plan", async () => {
|
||||
const provider = new SelfHostedEntitlementsProvider();
|
||||
const ent = await provider.getForOrg("org1");
|
||||
expect(ent.plan).toBe("unlimited");
|
||||
});
|
||||
|
||||
test("Factory returns correct provider based on DEPLOYMENT_MODE", () => {
|
||||
jest.resetModules();
|
||||
jest.isolateModules(() => {
|
||||
jest.doMock("@/config/index.js", () => ({ config: { DEPLOYMENT_MODE: "self_hosted" } }));
|
||||
const mod = require("@/services/system/EntitlementsService.js");
|
||||
expect(mod.EntitlementsFactory.create()).toBeInstanceOf(mod.SelfHostedEntitlementsProvider);
|
||||
});
|
||||
|
||||
jest.resetModules();
|
||||
jest.isolateModules(() => {
|
||||
jest.doMock("@/config/index.js", () => ({ config: { DEPLOYMENT_MODE: "saas" } }));
|
||||
const mod = require("@/services/system/EntitlementsService.js");
|
||||
expect(mod.EntitlementsFactory.create()).toBeInstanceOf(mod.SaaSEntitlementsProvider);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user