From 701b16253f5d8520f4e75f4def5b20a5834c17da Mon Sep 17 00:00:00 2001 From: brage-andreas Date: Sun, 12 Apr 2026 00:29:47 +0200 Subject: [PATCH 1/3] test: add tests --- .../group/__test__/group-service.spec.ts | 12 +- .../simplify-group-memberships.spec.ts | 161 ++++++++++++ .../membership.spec.ts} | 2 +- .../modules/user/__test__/merge-users.spec.ts | 231 ++++++++++++++++++ 4 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts rename apps/rpc/src/modules/user/{membership.e2e-spec.ts => __test__/membership.spec.ts} (99%) create mode 100644 apps/rpc/src/modules/user/__test__/merge-users.spec.ts diff --git a/apps/rpc/src/modules/group/__test__/group-service.spec.ts b/apps/rpc/src/modules/group/__test__/group-service.spec.ts index acb607011f..50da6b3a99 100644 --- a/apps/rpc/src/modules/group/__test__/group-service.spec.ts +++ b/apps/rpc/src/modules/group/__test__/group-service.spec.ts @@ -3,13 +3,14 @@ import type { Group } from "@dotkomonline/types" import { PrismaClient } from "@prisma/client" import type { ManagementClient } from "auth0" import { randomUUID } from "node:crypto" -import { getFeideGroupsRepository } from "src/modules/feide/feide-groups-repository" -import { getMembershipService } from "src/modules/user/membership-service" -import { getUserRepository } from "src/modules/user/user-repository" -import { getUserService } from "src/modules/user/user-service" +import { getFeideGroupsRepository } from "../../feide/feide-groups-repository" +import { getMembershipService } from "../../user/membership-service" +import { getUserRepository } from "../../user/user-repository" +import { getUserService } from "../../user/user-service" import { mockDeep } from "vitest-mock-extended" import { getGroupRepository } from "../group-repository" import { getGroupService } from "../group-service" +import { describe, expect, it, vi } from "vitest" describe("GroupService", () => { const db = vi.mocked(PrismaClient.prototype) @@ -17,18 +18,19 @@ describe("GroupService", () => { const s3Client = mockDeep() const userRepository = getUserRepository() const feideGroupsRepository = getFeideGroupsRepository() + const groupRepository = getGroupRepository() const membershipService = getMembershipService() const userService = getUserService( userRepository, feideGroupsRepository, + groupRepository, auth0Client, membershipService, s3Client, "fake-aws-bucket" ) - const groupRepository = getGroupRepository() const groupService = getGroupService(groupRepository, userService, s3Client, "fake-aws-bucket") it("creates a new group", async () => { diff --git a/apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts b/apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts new file mode 100644 index 0000000000..f4af8fdf54 --- /dev/null +++ b/apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts @@ -0,0 +1,161 @@ +import { randomUUID } from "node:crypto" +import type { GroupMembership, GroupRole } from "@dotkomonline/types" +import { describe, expect, it } from "vitest" +import { simplifyGroupMemberships } from "../group-service" + +const roleA: GroupRole = { id: "role-a", name: "Role A", type: "COSMETIC", groupId: "group-1" } +const roleB: GroupRole = { id: "role-b", name: "Role B", type: "COSMETIC", groupId: "group-1" } +const roleC: GroupRole = { id: "role-c", name: "Role C", type: "COSMETIC", groupId: "group-1" } + +function makeMembership(overrides: Partial = {}): GroupMembership { + return { + id: randomUUID(), + createdAt: new Date(), + updatedAt: new Date(), + groupId: "group-1", + userId: "user-1", + roles: [], + start: new Date("2024-01-01"), + end: new Date("2024-06-01"), + ...overrides, + } +} + +describe("simplifyGroupMemberships", () => { + it("returns an empty array for no memberships", () => { + expect(simplifyGroupMemberships([])).toEqual([]) + }) + + it("returns a single membership unchanged", () => { + const m = makeMembership({ roles: [roleA] }) + const result = simplifyGroupMemberships([m]) + + expect(result).toHaveLength(1) + expect(result[0].start).toEqual(m.start) + expect(result[0].end).toEqual(m.end) + expect(result[0].roles).toEqual([roleA]) + }) + + it("merges adjacent memberships with identical roles into one", () => { + const m1 = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-03-01"), roles: [roleA] }) + const m2 = makeMembership({ start: new Date("2024-03-01"), end: new Date("2024-06-01"), roles: [roleA] }) + + const result = simplifyGroupMemberships([m1, m2]) + + expect(result).toHaveLength(1) + expect(result[0].start).toEqual(new Date("2024-01-01")) + expect(result[0].end).toEqual(new Date("2024-06-01")) + expect(result[0].roles).toEqual([roleA]) + }) + + it("does not merge adjacent memberships with different roles", () => { + const m1 = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-03-01"), roles: [roleA] }) + const m2 = makeMembership({ start: new Date("2024-03-01"), end: new Date("2024-06-01"), roles: [roleB] }) + + const result = simplifyGroupMemberships([m1, m2]) + + expect(result).toHaveLength(2) + expect(result[0].start).toEqual(new Date("2024-01-01")) + expect(result[0].end).toEqual(new Date("2024-03-01")) + expect(result[1].start).toEqual(new Date("2024-03-01")) + expect(result[1].end).toEqual(new Date("2024-06-01")) + }) + + it("splits overlapping memberships with different roles into segments", () => { + // A: Jan-Apr, B: Feb-Jun → segments: Jan-Feb(A), Feb-Apr(AB), Apr-Jun(B) + const mA = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-04-01"), roles: [roleA] }) + const mB = makeMembership({ start: new Date("2024-02-01"), end: new Date("2024-06-01"), roles: [roleB] }) + + const result = simplifyGroupMemberships([mA, mB]) + + expect(result).toHaveLength(3) + + expect(result[0].start).toEqual(new Date("2024-01-01")) + expect(result[0].end).toEqual(new Date("2024-02-01")) + expect(result[0].roles.map((r) => r.id)).toEqual([roleA.id]) + + expect(result[1].start).toEqual(new Date("2024-02-01")) + expect(result[1].end).toEqual(new Date("2024-04-01")) + expect(new Set(result[1].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + + expect(result[2].start).toEqual(new Date("2024-04-01")) + expect(result[2].end).toEqual(new Date("2024-06-01")) + expect(result[2].roles.map((r) => r.id)).toEqual([roleB.id]) + }) + + it("produces one segment for fully coincident memberships, combining roles", () => { + const mA = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-06-01"), roles: [roleA] }) + const mB = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-06-01"), roles: [roleB] }) + + const result = simplifyGroupMemberships([mA, mB]) + + expect(result).toHaveLength(1) + expect(result[0].start).toEqual(new Date("2024-01-01")) + expect(result[0].end).toEqual(new Date("2024-06-01")) + expect(new Set(result[0].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + }) + + it("preserves end=null for an ongoing membership", () => { + const m = makeMembership({ start: new Date("2024-01-01"), end: null, roles: [roleA] }) + + const result = simplifyGroupMemberships([m]) + + expect(result).toHaveLength(1) + expect(result[0].end).toBeNull() + }) + + it("correctly segments an overlapping membership against an ongoing one", () => { + // A: Jan-Apr, B: Feb-null → segments: Jan-Feb(A), Feb-Apr(AB), Apr-null(B) + const mA = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-04-01"), roles: [roleA] }) + const mB = makeMembership({ start: new Date("2024-02-01"), end: null, roles: [roleB] }) + + const result = simplifyGroupMemberships([mA, mB]) + + expect(result).toHaveLength(3) + + expect(result[0].start).toEqual(new Date("2024-01-01")) + expect(result[0].end).toEqual(new Date("2024-02-01")) + expect(result[0].roles.map((r) => r.id)).toEqual([roleA.id]) + + expect(result[1].start).toEqual(new Date("2024-02-01")) + expect(result[1].end).toEqual(new Date("2024-04-01")) + expect(new Set(result[1].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + + expect(result[2].start).toEqual(new Date("2024-04-01")) + expect(result[2].end).toBeNull() + expect(result[2].roles.map((r) => r.id)).toEqual([roleB.id]) + }) + + it("handles the docstring example: A(0-2), B(1-4), C(3-5) → 5 segments", () => { + // Using months as time units: + // A: Jan-Mar, B: Feb-May, C: Apr-Jun + // Expected: Jan-Feb(A), Feb-Mar(AB), Mar-Apr(B), Apr-May(BC), May-Jun(C) + const mA = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-03-01"), roles: [roleA] }) + const mB = makeMembership({ start: new Date("2024-02-01"), end: new Date("2024-05-01"), roles: [roleB] }) + const mC = makeMembership({ start: new Date("2024-04-01"), end: new Date("2024-06-01"), roles: [roleC] }) + + const result = simplifyGroupMemberships([mA, mB, mC]) + + expect(result).toHaveLength(5) + + expect(result[0].start).toEqual(new Date("2024-01-01")) + expect(result[0].end).toEqual(new Date("2024-02-01")) + expect(result[0].roles.map((r) => r.id)).toEqual([roleA.id]) + + expect(result[1].start).toEqual(new Date("2024-02-01")) + expect(result[1].end).toEqual(new Date("2024-03-01")) + expect(new Set(result[1].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + + expect(result[2].start).toEqual(new Date("2024-03-01")) + expect(result[2].end).toEqual(new Date("2024-04-01")) + expect(result[2].roles.map((r) => r.id)).toEqual([roleB.id]) + + expect(result[3].start).toEqual(new Date("2024-04-01")) + expect(result[3].end).toEqual(new Date("2024-05-01")) + expect(new Set(result[3].roles.map((r) => r.id))).toEqual(new Set([roleB.id, roleC.id])) + + expect(result[4].start).toEqual(new Date("2024-05-01")) + expect(result[4].end).toEqual(new Date("2024-06-01")) + expect(result[4].roles.map((r) => r.id)).toEqual([roleC.id]) + }) +}) diff --git a/apps/rpc/src/modules/user/membership.e2e-spec.ts b/apps/rpc/src/modules/user/__test__/membership.spec.ts similarity index 99% rename from apps/rpc/src/modules/user/membership.e2e-spec.ts rename to apps/rpc/src/modules/user/__test__/membership.spec.ts index d87042f034..2c12c163c5 100644 --- a/apps/rpc/src/modules/user/membership.e2e-spec.ts +++ b/apps/rpc/src/modules/user/__test__/membership.spec.ts @@ -1,4 +1,4 @@ -import { getMembershipService, MASTER_SEMESTER_OFFSET } from "./membership-service" +import { getMembershipService, MASTER_SEMESTER_OFFSET } from "../membership-service" import { describe, expect, it, beforeEach, afterEach, vitest as vi } from "vitest" describe("Membership integration tests", () => { diff --git a/apps/rpc/src/modules/user/__test__/merge-users.spec.ts b/apps/rpc/src/modules/user/__test__/merge-users.spec.ts new file mode 100644 index 0000000000..56cdae4161 --- /dev/null +++ b/apps/rpc/src/modules/user/__test__/merge-users.spec.ts @@ -0,0 +1,231 @@ +import { randomUUID } from "node:crypto" +import type { DBHandle } from "@dotkomonline/db" +import type { Membership, User } from "@dotkomonline/types" +import { beforeEach, describe, expect, it } from "vitest" +import { mockDeep } from "vitest-mock-extended" +import type { GroupRepository } from "../../group/group-repository" +import { mergeUsers } from "../merge-users" + +function makeUser(overrides: Partial = {}): User { + return { + id: randomUUID(), + profileSlug: randomUUID(), + name: null, + email: null, + imageUrl: null, + biography: null, + phone: null, + gender: null, + dietaryRestrictions: null, + ntnuUsername: null, + flags: [], + workspaceUserId: null, + privacyPermissionsId: null, + notificationPermissionsId: null, + memberships: [], + createdAt: new Date(), + updatedAt: new Date(), + ...overrides, + } +} + +function makeMembership(userId: string, overrides: Partial = {}): Membership { + return { + id: randomUUID(), + type: "BACHELOR_STUDENT", + specialization: null, + semester: 1, + start: new Date("2024-01-01"), + end: new Date("2024-06-01"), + userId, + ...overrides, + } +} + +describe("mergeUsers", () => { + let handle: ReturnType> + let groupRepository: ReturnType> + + beforeEach(() => { + handle = mockDeep() + groupRepository = mockDeep() + groupRepository.findManyGroupMemberships.mockResolvedValue([]) + groupRepository.deleteGroupMemberships.mockResolvedValue(undefined) + groupRepository.createManyGroupMemberships.mockResolvedValue(undefined) + }) + + describe("scalar backfill", () => { + it("backfills null scalar fields from consumed user", async () => { + const survivor = makeUser({ biography: null }) + const consumed = makeUser({ biography: "consuming bio" }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.user.update).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ biography: "consuming bio" }) }) + ) + }) + + it("keeps survivor's value when it is not null", async () => { + const survivor = makeUser({ biography: "my bio" }) + const consumed = makeUser({ biography: "their bio" }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + const [[updateArgs]] = (handle.user.update as ReturnType).mock.calls + expect(updateArgs.data).not.toHaveProperty("biography") + }) + }) + + describe("profileSlug custom merger", () => { + it("adopts consumed's custom slug when survivor has a UUID slug", async () => { + const survivor = makeUser({ profileSlug: randomUUID() }) + const consumed = makeUser({ profileSlug: "my-custom-slug" }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.user.update).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ profileSlug: "my-custom-slug" }) }) + ) + }) + + it("keeps survivor's custom slug when it is not a UUID", async () => { + const survivor = makeUser({ profileSlug: "survivor-slug" }) + const consumed = makeUser({ profileSlug: "other-slug" }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.user.update).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ profileSlug: "survivor-slug" }) }) + ) + }) + + it("keeps survivor's UUID slug when consumed also has a UUID slug", async () => { + const survivorSlug = randomUUID() + const survivor = makeUser({ profileSlug: survivorSlug }) + const consumed = makeUser({ profileSlug: randomUUID() }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.user.update).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ profileSlug: survivorSlug }) }) + ) + }) + }) + + describe("flags custom merger", () => { + it("concatenates and deduplicates flags from both users", async () => { + const survivor = makeUser({ flags: ["a", "b"] }) + const consumed = makeUser({ flags: ["b", "c"] }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + const [[updateArgs]] = (handle.user.update as ReturnType).mock.calls + expect(new Set(updateArgs.data.flags)).toEqual(new Set(["a", "b", "c"])) + }) + }) + + describe("one-to-one relation backfill", () => { + it("backfills privacyPermissionsId from consumed when survivor's is null", async () => { + const survivor = makeUser({ privacyPermissionsId: null }) + const consumed = makeUser({ privacyPermissionsId: "perm-123" }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.user.update).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ privacyPermissionsId: "perm-123" }) }) + ) + }) + + it("deletes consumed's orphaned privacyPermissions when both users have one", async () => { + const survivor = makeUser({ privacyPermissionsId: "survivor-perm" }) + const consumed = makeUser({ privacyPermissionsId: "consumed-perm" }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.privacyPermissions.delete).toHaveBeenCalledWith({ where: { id: "consumed-perm" } }) + }) + + it("does not delete privacyPermissions when consumed has none", async () => { + const survivor = makeUser({ privacyPermissionsId: "survivor-perm" }) + const consumed = makeUser({ privacyPermissionsId: null }) + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.privacyPermissions.delete).not.toHaveBeenCalled() + }) + }) + + describe("membership deduplication", () => { + it("transfers non-duplicate memberships from consumed to survivor", async () => { + const survivor = makeUser() + const consumed = makeUser() + + survivor.memberships = [makeMembership(survivor.id, { type: "BACHELOR_STUDENT", semester: 1 })] + const consumedMembership = makeMembership(consumed.id, { type: "MASTER_STUDENT", semester: 1 }) + consumed.memberships = [consumedMembership] + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.membership.updateMany).toHaveBeenCalledWith({ + where: { id: { in: [consumedMembership.id] } }, + data: { userId: survivor.id }, + }) + expect(handle.membership.deleteMany).toHaveBeenCalledWith({ where: { userId: consumed.id } }) + }) + + it("drops duplicate memberships without transferring them", async () => { + const survivor = makeUser() + const consumed = makeUser() + + survivor.memberships = [ + makeMembership(survivor.id, { type: "BACHELOR_STUDENT", specialization: null, semester: 1 }), + ] + consumed.memberships = [ + makeMembership(consumed.id, { type: "BACHELOR_STUDENT", specialization: null, semester: 1 }), + ] + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.membership.updateMany).not.toHaveBeenCalled() + expect(handle.membership.deleteMany).toHaveBeenCalledWith({ where: { userId: consumed.id } }) + }) + }) + + describe("relation reassignment", () => { + it("reassigns attendee records from consumed to survivor", async () => { + const survivor = makeUser() + const consumed = makeUser() + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.attendee.updateMany).toHaveBeenCalledWith({ + where: { userId: consumed.id }, + data: { userId: survivor.id }, + }) + }) + + it("reassigns auditLog records from consumed to survivor", async () => { + const survivor = makeUser() + const consumed = makeUser() + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.auditLog.updateMany).toHaveBeenCalledWith({ + where: { userId: consumed.id }, + data: { userId: survivor.id }, + }) + }) + }) + + describe("consumed user deletion", () => { + it("deletes the consumed user after the merge", async () => { + const survivor = makeUser() + const consumed = makeUser() + + await mergeUsers(handle, groupRepository, survivor, consumed) + + expect(handle.user.delete).toHaveBeenCalledWith({ where: { id: consumed.id } }) + }) + }) +}) From 239b5e868dd98e32076ff633fcdee9d71e8f45b3 Mon Sep 17 00:00:00 2001 From: brage-andreas Date: Wed, 15 Apr 2026 21:49:07 +0200 Subject: [PATCH 2/3] fix: type errors --- .../simplify-group-memberships.spec.ts | 33 ++++++++----------- .../modules/user/__test__/merge-users.spec.ts | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts b/apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts index f4af8fdf54..51aa7da38a 100644 --- a/apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts +++ b/apps/rpc/src/modules/group/__test__/simplify-group-memberships.spec.ts @@ -33,7 +33,7 @@ describe("simplifyGroupMemberships", () => { expect(result).toHaveLength(1) expect(result[0].start).toEqual(m.start) expect(result[0].end).toEqual(m.end) - expect(result[0].roles).toEqual([roleA]) + expect(result[0].roleIds).toEqual(new Set([roleA.id])) }) it("merges adjacent memberships with identical roles into one", () => { @@ -45,7 +45,7 @@ describe("simplifyGroupMemberships", () => { expect(result).toHaveLength(1) expect(result[0].start).toEqual(new Date("2024-01-01")) expect(result[0].end).toEqual(new Date("2024-06-01")) - expect(result[0].roles).toEqual([roleA]) + expect(result[0].roleIds).toEqual(new Set([roleA.id])) }) it("does not merge adjacent memberships with different roles", () => { @@ -62,7 +62,6 @@ describe("simplifyGroupMemberships", () => { }) it("splits overlapping memberships with different roles into segments", () => { - // A: Jan-Apr, B: Feb-Jun → segments: Jan-Feb(A), Feb-Apr(AB), Apr-Jun(B) const mA = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-04-01"), roles: [roleA] }) const mB = makeMembership({ start: new Date("2024-02-01"), end: new Date("2024-06-01"), roles: [roleB] }) @@ -72,15 +71,15 @@ describe("simplifyGroupMemberships", () => { expect(result[0].start).toEqual(new Date("2024-01-01")) expect(result[0].end).toEqual(new Date("2024-02-01")) - expect(result[0].roles.map((r) => r.id)).toEqual([roleA.id]) + expect(result[0].roleIds).toEqual(new Set([roleA.id])) expect(result[1].start).toEqual(new Date("2024-02-01")) expect(result[1].end).toEqual(new Date("2024-04-01")) - expect(new Set(result[1].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + expect(result[1].roleIds).toEqual(new Set([roleA.id, roleB.id])) expect(result[2].start).toEqual(new Date("2024-04-01")) expect(result[2].end).toEqual(new Date("2024-06-01")) - expect(result[2].roles.map((r) => r.id)).toEqual([roleB.id]) + expect(result[2].roleIds).toEqual(new Set([roleB.id])) }) it("produces one segment for fully coincident memberships, combining roles", () => { @@ -92,7 +91,7 @@ describe("simplifyGroupMemberships", () => { expect(result).toHaveLength(1) expect(result[0].start).toEqual(new Date("2024-01-01")) expect(result[0].end).toEqual(new Date("2024-06-01")) - expect(new Set(result[0].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + expect(result[0].roleIds).toEqual(new Set([roleA.id, roleB.id])) }) it("preserves end=null for an ongoing membership", () => { @@ -105,7 +104,6 @@ describe("simplifyGroupMemberships", () => { }) it("correctly segments an overlapping membership against an ongoing one", () => { - // A: Jan-Apr, B: Feb-null → segments: Jan-Feb(A), Feb-Apr(AB), Apr-null(B) const mA = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-04-01"), roles: [roleA] }) const mB = makeMembership({ start: new Date("2024-02-01"), end: null, roles: [roleB] }) @@ -115,21 +113,18 @@ describe("simplifyGroupMemberships", () => { expect(result[0].start).toEqual(new Date("2024-01-01")) expect(result[0].end).toEqual(new Date("2024-02-01")) - expect(result[0].roles.map((r) => r.id)).toEqual([roleA.id]) + expect(result[0].roleIds).toEqual(new Set([roleA.id])) expect(result[1].start).toEqual(new Date("2024-02-01")) expect(result[1].end).toEqual(new Date("2024-04-01")) - expect(new Set(result[1].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + expect(result[1].roleIds).toEqual(new Set([roleA.id, roleB.id])) expect(result[2].start).toEqual(new Date("2024-04-01")) expect(result[2].end).toBeNull() - expect(result[2].roles.map((r) => r.id)).toEqual([roleB.id]) + expect(result[2].roleIds).toEqual(new Set([roleB.id])) }) it("handles the docstring example: A(0-2), B(1-4), C(3-5) → 5 segments", () => { - // Using months as time units: - // A: Jan-Mar, B: Feb-May, C: Apr-Jun - // Expected: Jan-Feb(A), Feb-Mar(AB), Mar-Apr(B), Apr-May(BC), May-Jun(C) const mA = makeMembership({ start: new Date("2024-01-01"), end: new Date("2024-03-01"), roles: [roleA] }) const mB = makeMembership({ start: new Date("2024-02-01"), end: new Date("2024-05-01"), roles: [roleB] }) const mC = makeMembership({ start: new Date("2024-04-01"), end: new Date("2024-06-01"), roles: [roleC] }) @@ -140,22 +135,22 @@ describe("simplifyGroupMemberships", () => { expect(result[0].start).toEqual(new Date("2024-01-01")) expect(result[0].end).toEqual(new Date("2024-02-01")) - expect(result[0].roles.map((r) => r.id)).toEqual([roleA.id]) + expect(result[0].roleIds).toEqual(new Set([roleA.id])) expect(result[1].start).toEqual(new Date("2024-02-01")) expect(result[1].end).toEqual(new Date("2024-03-01")) - expect(new Set(result[1].roles.map((r) => r.id))).toEqual(new Set([roleA.id, roleB.id])) + expect(result[1].roleIds).toEqual(new Set([roleA.id, roleB.id])) expect(result[2].start).toEqual(new Date("2024-03-01")) expect(result[2].end).toEqual(new Date("2024-04-01")) - expect(result[2].roles.map((r) => r.id)).toEqual([roleB.id]) + expect(result[2].roleIds).toEqual(new Set([roleB.id])) expect(result[3].start).toEqual(new Date("2024-04-01")) expect(result[3].end).toEqual(new Date("2024-05-01")) - expect(new Set(result[3].roles.map((r) => r.id))).toEqual(new Set([roleB.id, roleC.id])) + expect(result[3].roleIds).toEqual(new Set([roleB.id, roleC.id])) expect(result[4].start).toEqual(new Date("2024-05-01")) expect(result[4].end).toEqual(new Date("2024-06-01")) - expect(result[4].roles.map((r) => r.id)).toEqual([roleC.id]) + expect(result[4].roleIds).toEqual(new Set([roleC.id])) }) }) diff --git a/apps/rpc/src/modules/user/__test__/merge-users.spec.ts b/apps/rpc/src/modules/user/__test__/merge-users.spec.ts index 56cdae4161..5c25d8daee 100644 --- a/apps/rpc/src/modules/user/__test__/merge-users.spec.ts +++ b/apps/rpc/src/modules/user/__test__/merge-users.spec.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto" import type { DBHandle } from "@dotkomonline/db" import type { Membership, User } from "@dotkomonline/types" -import { beforeEach, describe, expect, it } from "vitest" +import { beforeEach, describe, expect, it, type vi } from "vitest" import { mockDeep } from "vitest-mock-extended" import type { GroupRepository } from "../../group/group-repository" import { mergeUsers } from "../merge-users" From b82770d47a1f9f811cdb4d77a31b577696481001 Mon Sep 17 00:00:00 2001 From: brage-andreas Date: Fri, 24 Apr 2026 23:43:27 +0200 Subject: [PATCH 3/3] fix: rename profileSlug and rename file --- ...rge-users.spec.ts => user-merging.spec.ts} | 24 +++++++++---------- .../user/__test__/user-service.spec.ts | 5 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) rename apps/rpc/src/modules/user/__test__/{merge-users.spec.ts => user-merging.spec.ts} (92%) diff --git a/apps/rpc/src/modules/user/__test__/merge-users.spec.ts b/apps/rpc/src/modules/user/__test__/user-merging.spec.ts similarity index 92% rename from apps/rpc/src/modules/user/__test__/merge-users.spec.ts rename to apps/rpc/src/modules/user/__test__/user-merging.spec.ts index 5c25d8daee..ccd1e1ec93 100644 --- a/apps/rpc/src/modules/user/__test__/merge-users.spec.ts +++ b/apps/rpc/src/modules/user/__test__/user-merging.spec.ts @@ -4,12 +4,12 @@ import type { Membership, User } from "@dotkomonline/types" import { beforeEach, describe, expect, it, type vi } from "vitest" import { mockDeep } from "vitest-mock-extended" import type { GroupRepository } from "../../group/group-repository" -import { mergeUsers } from "../merge-users" +import { mergeUsers } from "../user-merging" function makeUser(overrides: Partial = {}): User { return { id: randomUUID(), - profileSlug: randomUUID(), + username: randomUUID(), name: null, email: null, imageUrl: null, @@ -77,38 +77,38 @@ describe("mergeUsers", () => { }) }) - describe("profileSlug custom merger", () => { + describe("username custom merger", () => { it("adopts consumed's custom slug when survivor has a UUID slug", async () => { - const survivor = makeUser({ profileSlug: randomUUID() }) - const consumed = makeUser({ profileSlug: "my-custom-slug" }) + const survivor = makeUser({ username: randomUUID() }) + const consumed = makeUser({ username: "my-custom-slug" }) await mergeUsers(handle, groupRepository, survivor, consumed) expect(handle.user.update).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ profileSlug: "my-custom-slug" }) }) + expect.objectContaining({ data: expect.objectContaining({ username: "my-custom-slug" }) }) ) }) it("keeps survivor's custom slug when it is not a UUID", async () => { - const survivor = makeUser({ profileSlug: "survivor-slug" }) - const consumed = makeUser({ profileSlug: "other-slug" }) + const survivor = makeUser({ username: "survivor-slug" }) + const consumed = makeUser({ username: "other-slug" }) await mergeUsers(handle, groupRepository, survivor, consumed) expect(handle.user.update).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ profileSlug: "survivor-slug" }) }) + expect.objectContaining({ data: expect.objectContaining({ username: "survivor-slug" }) }) ) }) it("keeps survivor's UUID slug when consumed also has a UUID slug", async () => { const survivorSlug = randomUUID() - const survivor = makeUser({ profileSlug: survivorSlug }) - const consumed = makeUser({ profileSlug: randomUUID() }) + const survivor = makeUser({ username: survivorSlug }) + const consumed = makeUser({ username: randomUUID() }) await mergeUsers(handle, groupRepository, survivor, consumed) expect(handle.user.update).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ profileSlug: survivorSlug }) }) + expect.objectContaining({ data: expect.objectContaining({ username: survivorSlug }) }) ) }) }) diff --git a/apps/rpc/src/modules/user/__test__/user-service.spec.ts b/apps/rpc/src/modules/user/__test__/user-service.spec.ts index 9f6e829ac9..db0354495e 100644 --- a/apps/rpc/src/modules/user/__test__/user-service.spec.ts +++ b/apps/rpc/src/modules/user/__test__/user-service.spec.ts @@ -5,13 +5,13 @@ import type { ManagementClient } from "auth0" import { mockDeep } from "vitest-mock-extended" import type { FeideGroupsRepository } from "../../feide/feide-groups-repository" import type { GroupRepository } from "../../group/group-repository" -import { mergeUsers as mergeUsersInDatabase } from "../merge-users" +import { mergeUsers as mergeUsersInDatabase } from "../user-merging" import type { MembershipService } from "../membership-service" import type { UserRepository } from "../user-repository" import { getUserService } from "../user-service" import { vi, expect, describe, afterEach, it } from "vitest" -vi.mock("../merge-users", () => ({ +vi.mock("../user-merging", () => ({ mergeUsers: vi.fn().mockResolvedValue(undefined), })) @@ -46,6 +46,7 @@ function makeMembership(overrides: Partial = {}): Membership { end: new Date("2026-12-31T00:00:00.000Z"), specialization: null, semester: 1, + userId: "auth0|user-123", ...overrides, } }