|
|
@@ -49,6 +49,12 @@ function buildCursor(lastId) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+function buildOffsetCursor(sort, offset) {
|
|
|
+ return Buffer.from(JSON.stringify({ v: 2, sort, offset }), "utf8").toString(
|
|
|
+ "base64url",
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
function createRequestStub(body) {
|
|
|
return {
|
|
|
async json() {
|
|
|
@@ -178,6 +184,256 @@ describe("GET /api/admin/users", () => {
|
|
|
|
|
|
expect(body.nextCursor).toBe(buildCursor("507f1f77bcf86cd799439012"));
|
|
|
});
|
|
|
+
|
|
|
+ it("returns 400 for invalid sort parameter", async () => {
|
|
|
+ getSession.mockResolvedValue({
|
|
|
+ userId: "u2",
|
|
|
+ role: "superadmin",
|
|
|
+ branchId: null,
|
|
|
+ email: "superadmin@example.com",
|
|
|
+ });
|
|
|
+
|
|
|
+ const res = await GET(
|
|
|
+ new Request("http://localhost/api/admin/users?sort=unknown"),
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(res.status).toBe(400);
|
|
|
+ expect(await res.json()).toEqual({
|
|
|
+ error: {
|
|
|
+ message: "Invalid sort",
|
|
|
+ code: "VALIDATION_INVALID_FIELD",
|
|
|
+ details: {
|
|
|
+ field: "sort",
|
|
|
+ value: "unknown",
|
|
|
+ allowed: ["default", "role_rights", "branch_asc"],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it("returns sorted users for sort=role_rights with stable nextCursor", async () => {
|
|
|
+ getSession.mockResolvedValue({
|
|
|
+ userId: "u2",
|
|
|
+ role: "superadmin",
|
|
|
+ branchId: null,
|
|
|
+ email: "superadmin@example.com",
|
|
|
+ });
|
|
|
+
|
|
|
+ const docs = [
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439011",
|
|
|
+ username: "branchb",
|
|
|
+ email: "branchb@example.com",
|
|
|
+ role: "branch",
|
|
|
+ branchId: "NL10",
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439012",
|
|
|
+ username: "admin",
|
|
|
+ email: "admin@example.com",
|
|
|
+ role: "admin",
|
|
|
+ branchId: null,
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439013",
|
|
|
+ username: "dev",
|
|
|
+ email: "dev@example.com",
|
|
|
+ role: "dev",
|
|
|
+ branchId: null,
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439014",
|
|
|
+ username: "super",
|
|
|
+ email: "super@example.com",
|
|
|
+ role: "superadmin",
|
|
|
+ branchId: null,
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const chain = {
|
|
|
+ select: vi.fn().mockReturnThis(),
|
|
|
+ exec: vi.fn().mockResolvedValue(docs),
|
|
|
+ };
|
|
|
+
|
|
|
+ User.find.mockReturnValue(chain);
|
|
|
+
|
|
|
+ const res = await GET(
|
|
|
+ new Request("http://localhost/api/admin/users?sort=role_rights&limit=2"),
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(res.status).toBe(200);
|
|
|
+ expect(chain.select).toHaveBeenCalledTimes(1);
|
|
|
+ expect(chain.exec).toHaveBeenCalledTimes(1);
|
|
|
+
|
|
|
+ const body = await res.json();
|
|
|
+ expect(body.items.map((x) => x.role)).toEqual(["superadmin", "dev"]);
|
|
|
+ expect(body.nextCursor).toBe(buildOffsetCursor("role_rights", 2));
|
|
|
+ });
|
|
|
+
|
|
|
+ it("returns sorted users for sort=branch_asc with null branches at the end", async () => {
|
|
|
+ getSession.mockResolvedValue({
|
|
|
+ userId: "u2",
|
|
|
+ role: "superadmin",
|
|
|
+ branchId: null,
|
|
|
+ email: "superadmin@example.com",
|
|
|
+ });
|
|
|
+
|
|
|
+ const docs = [
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439011",
|
|
|
+ username: "branch10",
|
|
|
+ email: "branch10@example.com",
|
|
|
+ role: "branch",
|
|
|
+ branchId: "NL10",
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439012",
|
|
|
+ username: "admin",
|
|
|
+ email: "admin@example.com",
|
|
|
+ role: "admin",
|
|
|
+ branchId: null,
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439013",
|
|
|
+ username: "branch2",
|
|
|
+ email: "branch2@example.com",
|
|
|
+ role: "branch",
|
|
|
+ branchId: "NL2",
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439014",
|
|
|
+ username: "branch1",
|
|
|
+ email: "branch1@example.com",
|
|
|
+ role: "branch",
|
|
|
+ branchId: "NL01",
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const chain = {
|
|
|
+ select: vi.fn().mockReturnThis(),
|
|
|
+ exec: vi.fn().mockResolvedValue(docs),
|
|
|
+ };
|
|
|
+
|
|
|
+ User.find.mockReturnValue(chain);
|
|
|
+
|
|
|
+ const res = await GET(
|
|
|
+ new Request("http://localhost/api/admin/users?sort=branch_asc&limit=10"),
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(res.status).toBe(200);
|
|
|
+ const body = await res.json();
|
|
|
+ expect(body.items.map((x) => x.branchId)).toEqual([
|
|
|
+ "NL01",
|
|
|
+ "NL2",
|
|
|
+ "NL10",
|
|
|
+ null,
|
|
|
+ ]);
|
|
|
+ expect(body.nextCursor).toBe(null);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("returns 400 when cursor sort context does not match sort parameter", async () => {
|
|
|
+ getSession.mockResolvedValue({
|
|
|
+ userId: "u2",
|
|
|
+ role: "superadmin",
|
|
|
+ branchId: null,
|
|
|
+ email: "superadmin@example.com",
|
|
|
+ });
|
|
|
+
|
|
|
+ const cursor = buildOffsetCursor("role_rights", 2);
|
|
|
+ const res = await GET(
|
|
|
+ new Request(
|
|
|
+ `http://localhost/api/admin/users?sort=branch_asc&cursor=${encodeURIComponent(cursor)}`,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(res.status).toBe(400);
|
|
|
+ expect(await res.json()).toEqual({
|
|
|
+ error: {
|
|
|
+ message: "Invalid cursor",
|
|
|
+ code: "VALIDATION_INVALID_FIELD",
|
|
|
+ details: { field: "cursor" },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it("keeps pagination stable within the same custom sort mode", async () => {
|
|
|
+ getSession.mockResolvedValue({
|
|
|
+ userId: "u2",
|
|
|
+ role: "superadmin",
|
|
|
+ branchId: null,
|
|
|
+ email: "superadmin@example.com",
|
|
|
+ });
|
|
|
+
|
|
|
+ const docs = [
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439011",
|
|
|
+ username: "branchb",
|
|
|
+ email: "branchb@example.com",
|
|
|
+ role: "branch",
|
|
|
+ branchId: "NL10",
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439012",
|
|
|
+ username: "admin",
|
|
|
+ email: "admin@example.com",
|
|
|
+ role: "admin",
|
|
|
+ branchId: null,
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439013",
|
|
|
+ username: "dev",
|
|
|
+ email: "dev@example.com",
|
|
|
+ role: "dev",
|
|
|
+ branchId: null,
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ _id: "507f1f77bcf86cd799439014",
|
|
|
+ username: "super",
|
|
|
+ email: "super@example.com",
|
|
|
+ role: "superadmin",
|
|
|
+ branchId: null,
|
|
|
+ mustChangePassword: false,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const chain = {
|
|
|
+ select: vi.fn().mockReturnThis(),
|
|
|
+ exec: vi.fn().mockResolvedValue(docs),
|
|
|
+ };
|
|
|
+
|
|
|
+ User.find.mockReturnValue(chain);
|
|
|
+
|
|
|
+ const firstRes = await GET(
|
|
|
+ new Request("http://localhost/api/admin/users?sort=role_rights&limit=2"),
|
|
|
+ );
|
|
|
+ expect(firstRes.status).toBe(200);
|
|
|
+
|
|
|
+ const firstBody = await firstRes.json();
|
|
|
+ expect(firstBody.items.map((x) => x.role)).toEqual(["superadmin", "dev"]);
|
|
|
+ expect(firstBody.nextCursor).toBe(buildOffsetCursor("role_rights", 2));
|
|
|
+
|
|
|
+ const secondRes = await GET(
|
|
|
+ new Request(
|
|
|
+ `http://localhost/api/admin/users?sort=role_rights&limit=2&cursor=${encodeURIComponent(firstBody.nextCursor)}`,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ expect(secondRes.status).toBe(200);
|
|
|
+
|
|
|
+ const secondBody = await secondRes.json();
|
|
|
+ expect(secondBody.items.map((x) => x.role)).toEqual(["admin", "branch"]);
|
|
|
+ expect(secondBody.nextCursor).toBe(null);
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
describe("POST /api/admin/users", () => {
|