283 lines
11 KiB
JavaScript
283 lines
11 KiB
JavaScript
|
|
jest.mock("../../utils/sm4-util", () => ({
|
||
|
|
encryptDataForSm3: jest.fn((value) => Buffer.from(value).toString("base64")),
|
||
|
|
decryptDataForSm3: jest.fn((value) => {
|
||
|
|
const patientValues = {
|
||
|
|
encryptedName: "张三",
|
||
|
|
encryptedCertNo: "11010519491231002X",
|
||
|
|
encryptedMobile: "13800138000",
|
||
|
|
};
|
||
|
|
return patientValues[value] || Buffer.from(value, "base64").toString();
|
||
|
|
}),
|
||
|
|
}));
|
||
|
|
jest.mock("../hn-his", () => jest.fn());
|
||
|
|
|
||
|
|
const { ObjectId } = require("mongodb");
|
||
|
|
const Sm4Util = require("../../utils/sm4-util");
|
||
|
|
const hnHis = require("../hn-his");
|
||
|
|
const selfAuth = require("./index");
|
||
|
|
|
||
|
|
const OWNER_USER_ID = "507f1f77bcf86cd799439011";
|
||
|
|
const OTHER_USER_ID = "507f191e810c19729de860ea";
|
||
|
|
const PATIENT_ID = "507f1f77bcf86cd799439012";
|
||
|
|
const REQUEST_ID = "507f1f77bcf86cd799439013";
|
||
|
|
const ACCOUNT_ID = "2088000000000000";
|
||
|
|
|
||
|
|
function sameValue(left, right) {
|
||
|
|
if (left instanceof ObjectId || right instanceof ObjectId) {
|
||
|
|
return String(left) === String(right);
|
||
|
|
}
|
||
|
|
return left === right;
|
||
|
|
}
|
||
|
|
|
||
|
|
function matches(record, query) {
|
||
|
|
return Object.entries(query).every(([key, expected]) => {
|
||
|
|
if (expected && expected.$in) return expected.$in.includes(record[key]);
|
||
|
|
return sameValue(record[key], expected);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function createDb({ requests = [], patient = null } = {}) {
|
||
|
|
const state = { requests, patient };
|
||
|
|
const authCollection = {
|
||
|
|
dropIndex: jest.fn().mockRejectedValue(Object.assign(new Error("index not found"), {
|
||
|
|
code: 27,
|
||
|
|
codeName: "IndexNotFound",
|
||
|
|
})),
|
||
|
|
createIndex: jest.fn().mockResolvedValue("index"),
|
||
|
|
insertOne: jest.fn(async (record) => {
|
||
|
|
if (!record._id) record._id = new ObjectId();
|
||
|
|
state.requests.push(record);
|
||
|
|
return { insertedId: record._id };
|
||
|
|
}),
|
||
|
|
findOne: jest.fn(async (query) => state.requests.find((record) => matches(record, query)) || null),
|
||
|
|
findOneAndUpdate: jest.fn(async (query, update) => {
|
||
|
|
const record = state.requests.find((entry) => matches(entry, query));
|
||
|
|
if (!record) return null;
|
||
|
|
Object.assign(record, update.$set || {});
|
||
|
|
if (update.$inc) {
|
||
|
|
Object.entries(update.$inc).forEach(([key, value]) => {
|
||
|
|
record[key] = (record[key] || 0) + value;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return record;
|
||
|
|
}),
|
||
|
|
updateOne: jest.fn(async (query, update) => {
|
||
|
|
const record = state.requests.find((entry) => matches(entry, query));
|
||
|
|
if (!record) return { modifiedCount: 0 };
|
||
|
|
Object.assign(record, update.$set || {});
|
||
|
|
if (update.$unset) Object.keys(update.$unset).forEach((key) => delete record[key]);
|
||
|
|
return { modifiedCount: 1 };
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
const patientCollection = {
|
||
|
|
findOne: jest.fn(async (query) => {
|
||
|
|
if (!state.patient || !matches(state.patient, query)) return null;
|
||
|
|
return state.patient;
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
const db = {
|
||
|
|
collection: jest.fn((name) => {
|
||
|
|
if (name === "hlw-patient-self-auth") return authCollection;
|
||
|
|
if (name === "hlw-patient") return patientCollection;
|
||
|
|
throw new Error(`Unexpected collection: ${name}`);
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
return { db, state, authCollection };
|
||
|
|
}
|
||
|
|
|
||
|
|
function makePatient() {
|
||
|
|
return {
|
||
|
|
_id: new ObjectId(PATIENT_ID),
|
||
|
|
accountId: ACCOUNT_ID,
|
||
|
|
anotherName: "encryptedName",
|
||
|
|
anotherIdNo: "encryptedCertNo",
|
||
|
|
anotherMobile: "encryptedMobile",
|
||
|
|
address: "测试地址",
|
||
|
|
disabled: false,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeRequest(status = "pending", timestamp = Date.now()) {
|
||
|
|
return {
|
||
|
|
_id: new ObjectId(REQUEST_ID),
|
||
|
|
patientId: new ObjectId(PATIENT_ID),
|
||
|
|
ownerAccountId: ACCOUNT_ID,
|
||
|
|
ownerUserId: OWNER_USER_ID,
|
||
|
|
requestTimestamp: timestamp,
|
||
|
|
status,
|
||
|
|
attempts: 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function requestId(request) {
|
||
|
|
return request._id.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
jest.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("rejects malformed authorization ids", async () => {
|
||
|
|
const { db } = createDb();
|
||
|
|
await expect(selfAuth({ type: "getSelfAuthPatient", id: "bad-id" }, db, {}))
|
||
|
|
.resolves.toEqual({ success: false, message: "授权链接无效" });
|
||
|
|
expect(db.collection).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("creates a pending request owned by the authenticated user", async () => {
|
||
|
|
const { db, state } = createDb({ patient: makePatient() });
|
||
|
|
const result = await selfAuth({
|
||
|
|
type: "createSelfAuthRequest",
|
||
|
|
patientId: PATIENT_ID,
|
||
|
|
accountId: ACCOUNT_ID,
|
||
|
|
}, db, { userId: OWNER_USER_ID });
|
||
|
|
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
expect(result.data.id).toMatch(/^[a-f\d]{24}$/i);
|
||
|
|
expect(result.data.id).toBe(state.requests[0]._id.toString());
|
||
|
|
expect(state.requests[0]).toEqual(expect.objectContaining({
|
||
|
|
ownerAccountId: ACCOUNT_ID,
|
||
|
|
ownerUserId: OWNER_USER_ID,
|
||
|
|
status: "pending",
|
||
|
|
}));
|
||
|
|
expect(state.requests[0]).not.toHaveProperty("authId");
|
||
|
|
expect(state.requests[0]).not.toHaveProperty("psnToken");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("returns masked patient details and rejects an expired request", async () => {
|
||
|
|
const active = makeRequest();
|
||
|
|
const activeDb = createDb({ requests: [active], patient: makePatient() }).db;
|
||
|
|
const activeResult = await selfAuth({ type: "getSelfAuthPatient", id: requestId(active) }, activeDb, {});
|
||
|
|
expect(activeResult).toEqual(expect.objectContaining({
|
||
|
|
success: true,
|
||
|
|
data: expect.objectContaining({
|
||
|
|
name: "张三",
|
||
|
|
certNo: "1101***********02X",
|
||
|
|
sex: "女",
|
||
|
|
}),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const expired = makeRequest("pending", Date.now() - 11 * 60 * 1000);
|
||
|
|
const expiredDb = createDb({ requests: [expired], patient: makePatient() }).db;
|
||
|
|
const expiredResult = await selfAuth({ type: "getSelfAuthPatient", id: requestId(expired) }, expiredDb, {});
|
||
|
|
expect(expiredResult).toEqual({ success: false, message: "授权链接已过期,请重新发起授权" });
|
||
|
|
});
|
||
|
|
|
||
|
|
test("rejects missing requests and patients", async () => {
|
||
|
|
const missingRequest = makeRequest();
|
||
|
|
const emptyDb = createDb({ patient: makePatient() }).db;
|
||
|
|
await expect(selfAuth({ type: "getSelfAuthPatient", id: requestId(missingRequest) }, emptyDb, {}))
|
||
|
|
.resolves.toEqual({ success: false, message: "授权链接不存在或已失效" });
|
||
|
|
|
||
|
|
const request = makeRequest();
|
||
|
|
const noPatientDb = createDb({ requests: [request] }).db;
|
||
|
|
await expect(selfAuth({ type: "getSelfAuthPatient", id: requestId(request) }, noPatientDb, {}))
|
||
|
|
.resolves.toEqual({ success: false, message: "就诊人不存在" });
|
||
|
|
});
|
||
|
|
|
||
|
|
test("reports zero years old for a valid infant id", () => {
|
||
|
|
const now = new Date();
|
||
|
|
const date = [
|
||
|
|
now.getFullYear(),
|
||
|
|
String(now.getMonth() + 1).padStart(2, "0"),
|
||
|
|
String(now.getDate()).padStart(2, "0"),
|
||
|
|
].join("");
|
||
|
|
const body = `110105${date}001`;
|
||
|
|
const factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||
|
|
const parity = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"];
|
||
|
|
const sum = body.split("").reduce((total, digit, index) => total + Number(digit) * factors[index], 0);
|
||
|
|
const certNo = `${body}${parity[sum % 11]}`;
|
||
|
|
expect(selfAuth.getPersonInfo(certNo, now).age).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("stores only the encrypted matching HIS archive after successful submission", async () => {
|
||
|
|
const request = makeRequest();
|
||
|
|
const { db, state } = createDb({ requests: [request], patient: makePatient() });
|
||
|
|
hnHis.mockResolvedValue({
|
||
|
|
success: true,
|
||
|
|
list: [{ socialno: "11010519491231002X", isyb: "1", patientId: "his-patient", blhno: "card-no" }],
|
||
|
|
tmbxx: [{ code: "11", name: "普通门诊" }],
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "secret-token" }, db, { userId: OTHER_USER_ID });
|
||
|
|
expect(result).toEqual({ success: true, message: "授权并建档成功", data: { status: "success" } });
|
||
|
|
expect(state.requests[0].status).toBe("success");
|
||
|
|
expect(state.requests[0].encryptedResult).toBeTruthy();
|
||
|
|
expect(state.requests[0]).not.toHaveProperty("psnToken");
|
||
|
|
expect(JSON.stringify(state.requests[0])).not.toContain("secret-token");
|
||
|
|
expect(Sm4Util.encryptDataForSm3).toHaveBeenCalledWith(expect.stringContaining("his-patient"));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("allows a failed request to be retried", async () => {
|
||
|
|
const request = makeRequest("failed");
|
||
|
|
request.errorMessage = "previous failure";
|
||
|
|
const { db, state } = createDb({ requests: [request], patient: makePatient() });
|
||
|
|
hnHis.mockResolvedValue({
|
||
|
|
success: true,
|
||
|
|
list: [{ socialno: "11010519491231002X", isyb: "1" }],
|
||
|
|
tmbxx: [],
|
||
|
|
});
|
||
|
|
const result = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "new-token" }, db, { userId: OTHER_USER_ID });
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
expect(state.requests[0].status).toBe("success");
|
||
|
|
expect(state.requests[0].attempts).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("only one concurrent submission can call HIS", async () => {
|
||
|
|
const request = makeRequest();
|
||
|
|
const { db } = createDb({ requests: [request], patient: makePatient() });
|
||
|
|
let releaseHis;
|
||
|
|
hnHis.mockImplementation(() => new Promise((resolve) => {
|
||
|
|
releaseHis = () => resolve({
|
||
|
|
success: true,
|
||
|
|
list: [{ socialno: "11010519491231002X", isyb: "1" }],
|
||
|
|
tmbxx: [],
|
||
|
|
});
|
||
|
|
}));
|
||
|
|
|
||
|
|
const first = selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token-1" }, db, { userId: OTHER_USER_ID });
|
||
|
|
await new Promise((resolve) => setImmediate(resolve));
|
||
|
|
const second = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token-2" }, db, { userId: OTHER_USER_ID });
|
||
|
|
expect(second).toEqual({
|
||
|
|
success: false,
|
||
|
|
message: "授权正在处理中,请勿重复提交",
|
||
|
|
data: { status: "processing" },
|
||
|
|
});
|
||
|
|
expect(hnHis).toHaveBeenCalledTimes(1);
|
||
|
|
releaseHis();
|
||
|
|
await expect(first).resolves.toEqual({ success: true, message: "授权并建档成功", data: { status: "success" } });
|
||
|
|
});
|
||
|
|
|
||
|
|
test.each([
|
||
|
|
[{ success: true, list: [{ socialno: "330100000000000000", isyb: "1" }] }, "医保建档失败,未找到匹配档案"],
|
||
|
|
[{ success: true, list: [{ socialno: "11010519491231002X", isyb: "0" }] }, "医保建档失败"],
|
||
|
|
])("marks invalid HIS results as failed", async (hisResult, message) => {
|
||
|
|
const request = makeRequest();
|
||
|
|
const { db, state } = createDb({ requests: [request], patient: makePatient() });
|
||
|
|
hnHis.mockResolvedValue(hisResult);
|
||
|
|
const result = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token" }, db, { userId: OTHER_USER_ID });
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
expect(result.message).toBe(message);
|
||
|
|
expect(state.requests[0].status).toBe("failed");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("is idempotent after success and restricts result access to the owner", async () => {
|
||
|
|
const archive = { socialno: "11010519491231002X", isyb: "1" };
|
||
|
|
const request = {
|
||
|
|
...makeRequest("success"),
|
||
|
|
encryptedResult: Buffer.from(JSON.stringify(archive)).toString("base64"),
|
||
|
|
encryption: "SM4",
|
||
|
|
};
|
||
|
|
const { db } = createDb({ requests: [request], patient: makePatient() });
|
||
|
|
|
||
|
|
const repeated = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token" }, db, { userId: OTHER_USER_ID });
|
||
|
|
expect(repeated.success).toBe(true);
|
||
|
|
expect(hnHis).not.toHaveBeenCalled();
|
||
|
|
|
||
|
|
const denied = await selfAuth({ type: "getSelfAuthResult", id: requestId(request) }, db, { userId: OTHER_USER_ID });
|
||
|
|
expect(denied).toEqual({ success: false, message: "授权记录不存在或无权访问" });
|
||
|
|
|
||
|
|
const allowed = await selfAuth({ type: "getSelfAuthResult", id: requestId(request) }, db, { userId: OWNER_USER_ID });
|
||
|
|
expect(allowed).toEqual({ success: true, message: "授权成功", data: { status: "success", hisArchive: archive } });
|
||
|
|
});
|