costco-grocery-list/backend/tests/group-invites.service.test.js
Nico 77ae5be445
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
refactor
2026-02-22 01:27:03 -08:00

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();
});
});