All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m10s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 11s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
190 lines
5.8 KiB
JavaScript
190 lines
5.8 KiB
JavaScript
jest.mock("../models/group-invites.model", () => ({
|
|
addGroupMember: jest.fn(),
|
|
createGroupAuditLog: jest.fn(),
|
|
createInviteLink: jest.fn(),
|
|
createOrTouchPendingJoinRequest: jest.fn(),
|
|
consumeSingleUseInvite: jest.fn(),
|
|
deleteInviteLink: jest.fn(),
|
|
getGroupById: jest.fn(),
|
|
getGroupSettings: jest.fn(),
|
|
getInviteLinkById: jest.fn(),
|
|
getInviteLinkSummaryByToken: jest.fn(),
|
|
getManageableGroupsForUser: jest.fn(),
|
|
getPendingJoinRequest: jest.fn(),
|
|
getUserGroupRole: jest.fn(),
|
|
isGroupMember: jest.fn(),
|
|
listInviteLinks: jest.fn(),
|
|
revokeInviteLink: jest.fn(),
|
|
reviveInviteLink: jest.fn(),
|
|
upsertGroupSettings: jest.fn(),
|
|
withTransaction: jest.fn(),
|
|
}));
|
|
|
|
const invitesModel = require("../models/group-invites.model");
|
|
const invitesService = require("../services/group-invites.service");
|
|
|
|
function inviteSummary(overrides = {}) {
|
|
return {
|
|
id: 30,
|
|
group_id: 10,
|
|
group_name: "Test Group",
|
|
token: "1234567890abcdef1234567890fedcba",
|
|
policy: "AUTO_ACCEPT",
|
|
current_join_policy: "AUTO_ACCEPT",
|
|
single_use: false,
|
|
expires_at: "2030-01-01T00:00:00.000Z",
|
|
used_at: null,
|
|
revoked_at: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("group invites service", () => {
|
|
beforeEach(() => {
|
|
invitesModel.withTransaction.mockImplementation(async (handler) => handler({}));
|
|
});
|
|
|
|
test("create link success writes audit with request_id and token last4 only", async () => {
|
|
invitesModel.getGroupById.mockResolvedValue({ id: 1, name: "G1" });
|
|
invitesModel.getUserGroupRole.mockResolvedValue("admin");
|
|
invitesModel.createInviteLink.mockResolvedValue({
|
|
id: 55,
|
|
group_id: 1,
|
|
token: "1234567890abcdef1234567890fedcba",
|
|
policy: "AUTO_ACCEPT",
|
|
single_use: true,
|
|
expires_at: "2030-01-01T00:00:00.000Z",
|
|
created_at: "2026-01-01T00:00:00.000Z",
|
|
});
|
|
|
|
const link = await invitesService.createInviteLink(
|
|
7,
|
|
1,
|
|
"AUTO_ACCEPT",
|
|
true,
|
|
"2030-01-01T00:00:00.000Z",
|
|
"req-123",
|
|
"127.0.0.1",
|
|
"ua"
|
|
);
|
|
|
|
expect(link.id).toBe(55);
|
|
expect(invitesModel.createGroupAuditLog).toHaveBeenCalledTimes(1);
|
|
const auditPayload = invitesModel.createGroupAuditLog.mock.calls[0][0];
|
|
expect(auditPayload.requestId).toBe("req-123");
|
|
expect(auditPayload.metadata).toEqual({ inviteCodeLast4: "dcba" });
|
|
});
|
|
|
|
test("accept auto-accept adds membership", async () => {
|
|
invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(inviteSummary());
|
|
invitesModel.isGroupMember.mockResolvedValue(false);
|
|
invitesModel.getPendingJoinRequest.mockResolvedValue(null);
|
|
invitesModel.getUserGroupRole.mockResolvedValue(null);
|
|
invitesModel.addGroupMember.mockResolvedValue(true);
|
|
|
|
const result = await invitesService.acceptInviteLink(
|
|
99,
|
|
"token-1",
|
|
"req-1",
|
|
"127.0.0.1",
|
|
"ua"
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
status: "JOINED",
|
|
group: { id: 10, name: "Test Group" },
|
|
});
|
|
expect(invitesModel.addGroupMember).toHaveBeenCalled();
|
|
expect(invitesModel.createGroupAuditLog.mock.calls[0][0].eventType).toBe(
|
|
"GROUP_INVITE_USED"
|
|
);
|
|
});
|
|
|
|
test("accept manual policy creates pending request", async () => {
|
|
invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(
|
|
inviteSummary({
|
|
current_join_policy: "APPROVAL_REQUIRED",
|
|
single_use: true,
|
|
})
|
|
);
|
|
invitesModel.isGroupMember.mockResolvedValue(false);
|
|
invitesModel.getPendingJoinRequest.mockResolvedValue(null);
|
|
invitesModel.getUserGroupRole.mockResolvedValue(null);
|
|
invitesModel.createOrTouchPendingJoinRequest.mockResolvedValue({
|
|
id: 1,
|
|
status: "PENDING",
|
|
});
|
|
|
|
const result = await invitesService.acceptInviteLink(
|
|
99,
|
|
"token-2",
|
|
"req-2",
|
|
"127.0.0.1",
|
|
"ua"
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
status: "PENDING",
|
|
group: { id: 10, name: "Test Group" },
|
|
});
|
|
expect(invitesModel.createOrTouchPendingJoinRequest).toHaveBeenCalled();
|
|
expect(invitesModel.consumeSingleUseInvite).toHaveBeenCalledWith(30, {});
|
|
expect(invitesModel.createGroupAuditLog.mock.calls[0][0].eventType).toBe(
|
|
"GROUP_INVITE_REQUESTED"
|
|
);
|
|
});
|
|
|
|
test.each([
|
|
["INVITE_EXPIRED", inviteSummary({ expires_at: "2020-01-01T00:00:00.000Z" })],
|
|
["INVITE_REVOKED", inviteSummary({ revoked_at: "2026-01-01T00:00:00.000Z" })],
|
|
[
|
|
"INVITE_USED",
|
|
inviteSummary({ single_use: true, used_at: "2026-01-01T00:00:00.000Z" }),
|
|
],
|
|
])("rejects %s links", async (expectedCode, summary) => {
|
|
invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(summary);
|
|
invitesModel.isGroupMember.mockResolvedValue(false);
|
|
invitesModel.getPendingJoinRequest.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
invitesService.acceptInviteLink(99, "token-3", "req-3", "127.0.0.1", "ua")
|
|
).rejects.toMatchObject({ code: expectedCode });
|
|
});
|
|
|
|
test("accept returns ALREADY_MEMBER before pending checks", async () => {
|
|
invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(inviteSummary());
|
|
invitesModel.isGroupMember.mockResolvedValue(true);
|
|
|
|
const result = await invitesService.acceptInviteLink(
|
|
99,
|
|
"token-4",
|
|
"req-4",
|
|
"127.0.0.1",
|
|
"ua"
|
|
);
|
|
|
|
expect(result.status).toBe("ALREADY_MEMBER");
|
|
expect(invitesModel.getPendingJoinRequest).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("accept returns PENDING when request already exists", async () => {
|
|
invitesModel.getInviteLinkSummaryByToken.mockResolvedValue(inviteSummary());
|
|
invitesModel.isGroupMember.mockResolvedValue(false);
|
|
invitesModel.getPendingJoinRequest.mockResolvedValue({
|
|
id: 5,
|
|
status: "PENDING",
|
|
});
|
|
|
|
const result = await invitesService.acceptInviteLink(
|
|
99,
|
|
"token-5",
|
|
"req-5",
|
|
"127.0.0.1",
|
|
"ua"
|
|
);
|
|
|
|
expect(result.status).toBe("PENDING");
|
|
expect(invitesModel.addGroupMember).not.toHaveBeenCalled();
|
|
});
|
|
});
|