227 lines
8.5 KiB
TypeScript
227 lines
8.5 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
// ─── Mock environment variables (no hardcoded values) ───────────────────────
|
|
vi.mock("./secrets", () => ({
|
|
getSecrets: () => ({
|
|
nacMysqlUrl: "mysql://test:test@localhost:3306/nac_id",
|
|
nacMongoUrl: "mongodb://test:test@localhost:27017",
|
|
nacJwtSecret: "test-jwt-secret-at-least-32-chars-long",
|
|
nacAdminToken: "test-admin-token",
|
|
}),
|
|
}));
|
|
|
|
vi.mock("./nacAuth", () => ({
|
|
loginWithNacCredentials: vi.fn(),
|
|
verifyNacToken: vi.fn(),
|
|
listNacUsers: vi.fn(),
|
|
getNacUserCount: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./mongodb", () => ({
|
|
getMongoDb: vi.fn(),
|
|
}));
|
|
|
|
import { getSecrets } from "./secrets";
|
|
import { loginWithNacCredentials, verifyNacToken } from "./nacAuth";
|
|
import { getMongoDb } from "./mongodb";
|
|
|
|
// ─── Test: Secrets Module ────────────────────────────────────────────────────
|
|
describe("secrets module", () => {
|
|
it("returns all required secrets from environment variables", () => {
|
|
const secrets = getSecrets();
|
|
expect(secrets).toHaveProperty("nacMysqlUrl");
|
|
expect(secrets).toHaveProperty("nacMongoUrl");
|
|
expect(secrets).toHaveProperty("nacJwtSecret");
|
|
expect(secrets).toHaveProperty("nacAdminToken");
|
|
});
|
|
|
|
it("all secret values are non-empty strings", () => {
|
|
const secrets = getSecrets();
|
|
Object.values(secrets).forEach((v) => {
|
|
expect(typeof v).toBe("string");
|
|
expect(v.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it("secrets do not contain hardcoded production values", () => {
|
|
const secrets = getSecrets();
|
|
// Ensure no real IP addresses or production credentials appear in test
|
|
const secretStr = JSON.stringify(secrets);
|
|
expect(secretStr).not.toContain("103.96.148.7");
|
|
expect(secretStr).not.toContain("idP0ZaRGyLsTUA3a");
|
|
expect(secretStr).not.toContain("XKUigTFMJXhH");
|
|
});
|
|
});
|
|
|
|
// ─── Test: NAC Authentication ────────────────────────────────────────────────
|
|
describe("nacAuth module", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("loginWithNacCredentials returns null for invalid credentials", async () => {
|
|
(loginWithNacCredentials as any).mockResolvedValue(null);
|
|
const result = await loginWithNacCredentials("wrong@example.com", "wrongpassword");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("loginWithNacCredentials returns user and token for valid credentials", async () => {
|
|
const mockResult = {
|
|
token: "jwt.token.here",
|
|
user: {
|
|
id: 1,
|
|
email: "admin@newassetchain.io",
|
|
name: "Admin",
|
|
role: "admin",
|
|
kyc_level: 3,
|
|
},
|
|
};
|
|
(loginWithNacCredentials as any).mockResolvedValue(mockResult);
|
|
const result = await loginWithNacCredentials("admin@newassetchain.io", "password123");
|
|
expect(result).not.toBeNull();
|
|
expect(result?.token).toBeTruthy();
|
|
expect(result?.user.email).toBe("admin@newassetchain.io");
|
|
expect(result?.user.role).toBe("admin");
|
|
});
|
|
|
|
it("verifyNacToken returns null for invalid token", async () => {
|
|
(verifyNacToken as any).mockResolvedValue(null);
|
|
const result = await verifyNacToken("invalid.token");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("verifyNacToken returns user payload for valid token", async () => {
|
|
const mockUser = { id: 1, email: "admin@newassetchain.io", role: "admin" };
|
|
(verifyNacToken as any).mockResolvedValue(mockUser);
|
|
const result = await verifyNacToken("valid.jwt.token");
|
|
expect(result).toEqual(mockUser);
|
|
});
|
|
});
|
|
|
|
// ─── Test: MongoDB Connection ────────────────────────────────────────────────
|
|
describe("mongodb module", () => {
|
|
it("getMongoDb returns null when connection fails", async () => {
|
|
(getMongoDb as any).mockResolvedValue(null);
|
|
const db = await getMongoDb();
|
|
expect(db).toBeNull();
|
|
});
|
|
|
|
it("getMongoDb returns db instance when connected", async () => {
|
|
const mockDb = {
|
|
collection: vi.fn().mockReturnValue({
|
|
find: vi.fn().mockReturnValue({ toArray: vi.fn().mockResolvedValue([]) }),
|
|
countDocuments: vi.fn().mockResolvedValue(0),
|
|
insertOne: vi.fn().mockResolvedValue({ insertedId: "test-id" }),
|
|
}),
|
|
};
|
|
(getMongoDb as any).mockResolvedValue(mockDb);
|
|
const db = await getMongoDb();
|
|
expect(db).not.toBeNull();
|
|
expect(db).toHaveProperty("collection");
|
|
});
|
|
});
|
|
|
|
// ─── Test: RBAC Role Validation ──────────────────────────────────────────────
|
|
describe("RBAC role validation", () => {
|
|
const VALID_ROLES = ["admin", "reviewer", "legal", "viewer"];
|
|
|
|
it("all defined roles are valid", () => {
|
|
VALID_ROLES.forEach((role) => {
|
|
expect(["admin", "reviewer", "legal", "viewer"]).toContain(role);
|
|
});
|
|
});
|
|
|
|
it("admin role has highest privilege", () => {
|
|
const roleHierarchy = { admin: 4, reviewer: 3, legal: 2, viewer: 1 };
|
|
expect(roleHierarchy["admin"]).toBeGreaterThan(roleHierarchy["reviewer"]);
|
|
expect(roleHierarchy["reviewer"]).toBeGreaterThan(roleHierarchy["legal"]);
|
|
expect(roleHierarchy["legal"]).toBeGreaterThan(roleHierarchy["viewer"]);
|
|
});
|
|
|
|
it("invalid role is rejected", () => {
|
|
const isValidRole = (role: string) => VALID_ROLES.includes(role);
|
|
expect(isValidRole("superuser")).toBe(false);
|
|
expect(isValidRole("root")).toBe(false);
|
|
expect(isValidRole("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Test: Knowledge Base Data Validation ────────────────────────────────────
|
|
describe("knowledge base data validation", () => {
|
|
const VALID_JURISDICTIONS = ["CN", "HK", "US", "EU", "SG", "AE", "ALL"];
|
|
const VALID_ASSET_TYPES = ["RealEstate", "Securities", "DigitalToken", "Commodity", "IP", "ALL"];
|
|
|
|
it("all supported jurisdictions are valid", () => {
|
|
VALID_JURISDICTIONS.forEach((j) => {
|
|
expect(j).toBeTruthy();
|
|
expect(j.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it("compliance rule requires jurisdiction and asset type", () => {
|
|
const validateRule = (rule: any) => {
|
|
return (
|
|
VALID_JURISDICTIONS.includes(rule.jurisdiction) &&
|
|
VALID_ASSET_TYPES.includes(rule.assetType) &&
|
|
typeof rule.ruleName === "string" &&
|
|
rule.ruleName.length > 0
|
|
);
|
|
};
|
|
|
|
const validRule = { jurisdiction: "CN", assetType: "RealEstate", ruleName: "不动产登记证要求" };
|
|
const invalidRule = { jurisdiction: "XX", assetType: "Unknown", ruleName: "" };
|
|
|
|
expect(validateRule(validRule)).toBe(true);
|
|
expect(validateRule(invalidRule)).toBe(false);
|
|
});
|
|
|
|
it("tag sequence must be an array of non-empty strings", () => {
|
|
const validateTags = (tags: any) => {
|
|
return Array.isArray(tags) && tags.length > 0 && tags.every((t: any) => typeof t === "string" && t.length > 0);
|
|
};
|
|
|
|
expect(validateTags(["CN", "RealEstate", "Required"])).toBe(true);
|
|
expect(validateTags([])).toBe(false);
|
|
expect(validateTags(["CN", "", "Required"])).toBe(false);
|
|
expect(validateTags("not-an-array")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Test: Audit Log Structure ────────────────────────────────────────────────
|
|
describe("audit log structure", () => {
|
|
const VALID_ACTIONS = [
|
|
"LOGIN", "LOGOUT", "CREATE_RULE", "UPDATE_RULE", "DELETE_RULE",
|
|
"REVIEW_CASE", "CORRECT_TAG", "CREATE_TAG_RULE",
|
|
"REGISTER_PROTOCOL", "TOGGLE_PROTOCOL", "UPDATE_PROTOCOL_VERSION",
|
|
"TRIGGER_CRAWLER", "UPDATE_CRAWLER_CONFIG", "MANAGE_USER",
|
|
];
|
|
|
|
it("all audit actions are defined", () => {
|
|
expect(VALID_ACTIONS.length).toBeGreaterThan(0);
|
|
VALID_ACTIONS.forEach((action) => {
|
|
expect(typeof action).toBe("string");
|
|
expect(action.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it("audit log entry has required fields", () => {
|
|
const validateAuditEntry = (entry: any) => {
|
|
return (
|
|
VALID_ACTIONS.includes(entry.action) &&
|
|
typeof entry.userId === "number" &&
|
|
typeof entry.userEmail === "string" &&
|
|
entry.userEmail.includes("@") &&
|
|
entry.timestamp instanceof Date
|
|
);
|
|
};
|
|
|
|
const validEntry = {
|
|
action: "LOGIN",
|
|
userId: 1,
|
|
userEmail: "admin@newassetchain.io",
|
|
timestamp: new Date(),
|
|
};
|
|
expect(validateAuditEntry(validEntry)).toBe(true);
|
|
});
|
|
});
|