Skip to content
37 changes: 18 additions & 19 deletions src/modules/escrow-payments/__tests__/approval.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
makeEscrowItem,
makeApproval,
makeBuyerUser,
makeQueryChain,
makeServiceTestingModule,
} from "./helpers";
import {
Expand Down Expand Up @@ -44,7 +43,7 @@ describe("EscrowPaymentsService › approvePayment", () => {
buyerApproved: true,
buyerApprovedAt: new Date(),
});
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findById.mockResolvedValue(payment);

await ctx.service.approvePayment(
PAYMENT_ID.toString(),
Expand All @@ -54,20 +53,20 @@ describe("EscrowPaymentsService › approvePayment", () => {
expect(payment.sellerApproved).toBe(true);
expect(payment.sellerApprovedAt).toBeInstanceOf(Date);
expect(payment.status).toBe("APPROVED");
expect(payment.save).toHaveBeenCalled();
expect(ctx.escrowPaymentRepo.save).toHaveBeenCalledWith(payment);
});

it("buyer 중복 승인 → AlreadyApprovedPaymentException, save 미호출", async () => {
const payment = makePayment({
status: "PENDING_APPROVAL",
buyerApproved: true,
});
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findById.mockResolvedValue(payment);

await expect(
ctx.service.approvePayment(PAYMENT_ID.toString(), BUYER_ID.toString()),
).rejects.toThrow(AlreadyApprovedPaymentException);
expect(payment.save).not.toHaveBeenCalled();
expect(ctx.escrowPaymentRepo.save).not.toHaveBeenCalled();
});
});

Expand All @@ -80,7 +79,7 @@ describe("EscrowPaymentsService › approvePayment", () => {
sellerApproved: true,
sellerApprovedAt: new Date(),
});
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findById.mockResolvedValue(payment);

await ctx.service.approvePayment(
PAYMENT_ID.toString(),
Expand All @@ -90,28 +89,28 @@ describe("EscrowPaymentsService › approvePayment", () => {
expect(payment.buyerApproved).toBe(true);
expect(payment.buyerApprovedAt).toBeInstanceOf(Date);
expect(payment.status).toBe("APPROVED");
expect(payment.save).toHaveBeenCalled();
expect(ctx.escrowPaymentRepo.save).toHaveBeenCalledWith(payment);
});

it("seller 중복 승인 → AlreadyApprovedPaymentException, save 미호출", async () => {
const payment = makePayment({
status: "PENDING_APPROVAL",
sellerApproved: true,
});
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findById.mockResolvedValue(payment);

await expect(
ctx.service.approvePayment(PAYMENT_ID.toString(), SELLER_ID.toString()),
).rejects.toThrow(AlreadyApprovedPaymentException);
expect(payment.save).not.toHaveBeenCalled();
expect(ctx.escrowPaymentRepo.save).not.toHaveBeenCalled();
});
});

// ── 공통 ────────────────────────────────────────────────────────────────────

it("ACTIVE 상태에서 승인 시도 → InvalidPaymentStatusException", async () => {
const payment = makePayment({ status: "ACTIVE" });
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findById.mockResolvedValue(payment);

await expect(
ctx.service.approvePayment(PAYMENT_ID.toString(), BUYER_ID.toString()),
Expand All @@ -120,7 +119,7 @@ describe("EscrowPaymentsService › approvePayment", () => {

it("buyer도 seller도 아닌 제3자 → UnauthorizedPaymentActionException", async () => {
const payment = makePayment({ status: "PENDING_APPROVAL" });
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findById.mockResolvedValue(payment);

await expect(
ctx.service.approvePayment(
Expand All @@ -131,7 +130,7 @@ describe("EscrowPaymentsService › approvePayment", () => {
});

it("존재하지 않는 결제 → EscrowPaymentNotFoundException", async () => {
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(null));
ctx.escrowPaymentRepo.findById.mockResolvedValue(null);

await expect(
ctx.service.approvePayment(PAYMENT_ID.toString(), BUYER_ID.toString()),
Expand All @@ -157,7 +156,7 @@ describe("EscrowPaymentsService › approveEvent", () => {
}),
],
});
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findByIdWithFulfillment.mockResolvedValue(payment);
return payment;
}

Expand All @@ -169,7 +168,7 @@ describe("EscrowPaymentsService › approveEvent", () => {
const payment = makePayment({
escrows: [makeEscrowItem({ status: "PENDING_ESCROW" })],
});
ctx.escrowPaymentModel.findById.mockReturnValue(makeQueryChain(payment));
ctx.escrowPaymentRepo.findByIdWithFulfillment.mockResolvedValue(payment);

await expect(
ctx.service.approveEvent(
Expand Down Expand Up @@ -247,15 +246,15 @@ describe("EscrowPaymentsService › approveEvent", () => {
expect(approval.buyerApproved).toBe(true);
expect(approval.buyerApprovedAt).toBeInstanceOf(Date);
expect(approval.completedAt).toBeUndefined();
expect(payment.save).toHaveBeenCalled();
expect(ctx.escrowPaymentRepo.save).toHaveBeenCalled();
});

it("양측 모두 승인 → completedAt 설정, EscrowFinish 자동 제출", async () => {
const payment = setupEscrowedPayment({
buyerApproved: true,
buyerApprovedAt: new Date(),
});
ctx.userModel.findById.mockReturnValue(makeQueryChain(makeBuyerUser()));
ctx.userFacade.findByIdWithSeed.mockResolvedValue(makeBuyerUser());

await ctx.service.approveEvent(
PAYMENT_ID.toString(),
Expand All @@ -280,7 +279,7 @@ describe("EscrowPaymentsService › approveEvent", () => {
buyerApproved: true,
buyerApprovedAt: new Date(),
});
ctx.userModel.findById.mockReturnValue(makeQueryChain(makeBuyerUser()));
ctx.userFacade.findByIdWithSeed.mockResolvedValue(makeBuyerUser());

await ctx.service.approveEvent(
PAYMENT_ID.toString(),
Expand All @@ -300,7 +299,7 @@ describe("EscrowPaymentsService › approveEvent", () => {
buyerApproved: true,
buyerApprovedAt: new Date(),
});
ctx.userModel.findById.mockReturnValue(makeQueryChain(makeBuyerUser()));
ctx.userFacade.findByIdWithSeed.mockResolvedValue(makeBuyerUser());

await ctx.service.approveEvent(
PAYMENT_ID.toString(),
Expand All @@ -317,7 +316,7 @@ describe("EscrowPaymentsService › approveEvent", () => {
buyerApproved: true,
buyerApprovedAt: new Date(),
});
ctx.userModel.findById.mockReturnValue(makeQueryChain(makeBuyerUser()));
ctx.userFacade.findByIdWithSeed.mockResolvedValue(makeBuyerUser());
ctx.xrplService.finishEscrow.mockRejectedValue(
new Error("XRPL network error"),
);
Expand Down
99 changes: 39 additions & 60 deletions src/modules/escrow-payments/__tests__/create.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
BUYER_ID,
SELLER_ID,
makeCrudServiceTestingModule,
makeQueryChain,
} from "./helpers";
import { BUYER_ID, SELLER_ID, makeCrudServiceTestingModule } from "./helpers";
import {
BuyerWalletNotFoundException,
SellerWalletNotFoundException,
Expand Down Expand Up @@ -42,26 +37,22 @@ describe("EscrowPaymentsCrudService › create", () => {

beforeEach(async () => {
ctx = await makeCrudServiceTestingModule();
ctx.userModel.findById.mockReturnValue(
makeQueryChain({
type: "buyer",
name: "Buyer Corp",
wallet: { address: BUYER_WALLET_ADDRESS },
}),
);
ctx.userModel.findOne.mockReturnValue(
makeQueryChain({
_id: SELLER_ID,
name: "Seller Corp",
wallet: { address: SELLER_WALLET_ADDRESS },
}),
);
ctx.userFacade.findByIdLean.mockResolvedValue({
type: "buyer",
name: "Buyer Corp",
wallet: { address: BUYER_WALLET_ADDRESS },
});
ctx.userFacade.findByWalletAddressAndType.mockResolvedValue({
_id: SELLER_ID,
name: "Seller Corp",
wallet: { address: SELLER_WALLET_ADDRESS },
});
});

it("totalAmountXrp를 escrow 항목 합산으로 계산", async () => {
await ctx.service.create(makeDto(), BUYER_ID.toString());

const constructorArg = ctx.escrowPaymentModel.mock.calls[0][0];
const constructorArg = ctx.escrowPaymentRepo.create.mock.calls[0][0];
expect(constructorArg.totalAmountXrp).toBe(1000);
expect(constructorArg.buyerWalletAddress).toBe(BUYER_WALLET_ADDRESS);
expect(constructorArg.sellerWalletAddress).toBe(SELLER_WALLET_ADDRESS);
Expand All @@ -70,7 +61,7 @@ describe("EscrowPaymentsCrudService › create", () => {
it("각 escrow 항목의 approvals를 requiredEventTypes로 초기화", async () => {
await ctx.service.create(makeDto(), BUYER_ID.toString());

const constructorArg = ctx.escrowPaymentModel.mock.calls[0][0];
const constructorArg = ctx.escrowPaymentRepo.create.mock.calls[0][0];
expect(constructorArg.escrows[0].approvals).toEqual([
expect.objectContaining({
eventType: "SHIPMENT_CONFIRMED",
Expand All @@ -84,7 +75,7 @@ describe("EscrowPaymentsCrudService › create", () => {
it("생성자(buyer)의 buyerApproved를 true로 자동 설정", async () => {
await ctx.service.create(makeDto(), BUYER_ID.toString());

const constructorArg = ctx.escrowPaymentModel.mock.calls[0][0];
const constructorArg = ctx.escrowPaymentRepo.create.mock.calls[0][0];
expect(constructorArg.buyerApproved).toBe(true);
expect(constructorArg.buyerApprovedAt).toBeInstanceOf(Date);
expect(constructorArg.sellerApproved).toBe(false);
Expand All @@ -93,38 +84,30 @@ describe("EscrowPaymentsCrudService › create", () => {
it("상태를 PENDING_APPROVAL로 초기화", async () => {
await ctx.service.create(makeDto(), BUYER_ID.toString());

const constructorArg = ctx.escrowPaymentModel.mock.calls[0][0];
const constructorArg = ctx.escrowPaymentRepo.create.mock.calls[0][0];
expect(constructorArg.status).toBe("PENDING_APPROVAL");
});

it("save() 호출", async () => {
const payment = { buyerId: BUYER_ID, sellerId: SELLER_ID };
const instance = {
save: jest.fn().mockReturnThis(),
toObject: jest.fn().mockReturnValue(payment),
};
instance.save.mockResolvedValue(instance);
ctx.escrowPaymentModel.mockReturnValue(instance);

it("create() 호출", async () => {
await ctx.service.create(makeDto(), BUYER_ID.toString());

expect(instance.save).toHaveBeenCalled();
expect(ctx.escrowPaymentRepo.create).toHaveBeenCalled();
});

it("counterpartyWalletAddress로 seller 조회 후 sellerId를 document에 주입", async () => {
await ctx.service.create(makeDto(), BUYER_ID.toString());

expect(ctx.userModel.findOne).toHaveBeenCalledWith(
{ "wallet.address": SELLER_WALLET_ADDRESS, type: "seller" },
{ _id: 1, name: 1, wallet: 1 },
expect(ctx.userFacade.findByWalletAddressAndType).toHaveBeenCalledWith(
SELLER_WALLET_ADDRESS,
"seller",
);

const constructorArg = ctx.escrowPaymentModel.mock.calls[0][0];
const constructorArg = ctx.escrowPaymentRepo.create.mock.calls[0][0];
expect(constructorArg.sellerId.toString()).toBe(SELLER_ID.toString());
});

it("존재하지 않는 seller 지갑 주소 → SellerWalletNotFoundException", async () => {
ctx.userModel.findOne.mockReturnValue(makeQueryChain(null));
ctx.userFacade.findByWalletAddressAndType.mockResolvedValue(null);

await expect(
ctx.service.create(makeDto(), BUYER_ID.toString()),
Expand All @@ -143,26 +126,22 @@ describe("EscrowPaymentsCrudService › create", () => {

beforeEach(async () => {
ctx = await makeCrudServiceTestingModule();
ctx.userModel.findById.mockReturnValue(
makeQueryChain({
type: "seller",
name: "Seller Corp",
wallet: { address: SELLER_WALLET_ADDRESS },
}),
);
ctx.userModel.findOne.mockReturnValue(
makeQueryChain({
_id: BUYER_ID,
name: "Buyer Corp",
wallet: { address: BUYER_WALLET_ADDRESS },
}),
);
ctx.userFacade.findByIdLean.mockResolvedValue({
type: "seller",
name: "Seller Corp",
wallet: { address: SELLER_WALLET_ADDRESS },
});
ctx.userFacade.findByWalletAddressAndType.mockResolvedValue({
_id: BUYER_ID,
name: "Buyer Corp",
wallet: { address: BUYER_WALLET_ADDRESS },
});
});

it("생성자(seller)의 sellerApproved를 true로 자동 설정", async () => {
await ctx.service.create(makeDto(), SELLER_ID.toString());

const constructorArg = ctx.escrowPaymentModel.mock.calls[0][0];
const constructorArg = ctx.escrowPaymentRepo.create.mock.calls[0][0];
expect(constructorArg.sellerApproved).toBe(true);
expect(constructorArg.sellerApprovedAt).toBeInstanceOf(Date);
expect(constructorArg.buyerApproved).toBe(false);
Expand All @@ -173,17 +152,17 @@ describe("EscrowPaymentsCrudService › create", () => {
it("counterpartyWalletAddress로 buyer 조회 후 buyerId를 document에 주입", async () => {
await ctx.service.create(makeDto(), SELLER_ID.toString());

expect(ctx.userModel.findOne).toHaveBeenCalledWith(
{ "wallet.address": BUYER_WALLET_ADDRESS, type: "buyer" },
{ _id: 1, name: 1, wallet: 1 },
expect(ctx.userFacade.findByWalletAddressAndType).toHaveBeenCalledWith(
BUYER_WALLET_ADDRESS,
"buyer",
);

const constructorArg = ctx.escrowPaymentModel.mock.calls[0][0];
const constructorArg = ctx.escrowPaymentRepo.create.mock.calls[0][0];
expect(constructorArg.buyerId.toString()).toBe(BUYER_ID.toString());
});

it("존재하지 않는 buyer 지갑 주소 → BuyerWalletNotFoundException", async () => {
ctx.userModel.findOne.mockReturnValue(makeQueryChain(null));
ctx.userFacade.findByWalletAddressAndType.mockResolvedValue(null);

await expect(
ctx.service.create(makeDto(), SELLER_ID.toString()),
Expand All @@ -195,7 +174,7 @@ describe("EscrowPaymentsCrudService › create", () => {

it("DB에 없는 userId → UnauthorizedPaymentActionException", async () => {
ctx = await makeCrudServiceTestingModule();
// findById 기본값: null (makeUserModelMock 참고)
// findByIdLean 기본값: null (makeUserFacadeMock 참고)

await expect(
ctx.service.create(
Expand Down
Loading
Loading