Compare commits
5 Commits
feat/ms21-
...
chore/task
| Author | SHA1 | Date | |
|---|---|---|---|
| f9588d2ce1 | |||
| 294446043a | |||
| af68f84dcd | |||
| b57f549d39 | |||
| 2c8d0a8daf |
@@ -24,7 +24,15 @@ describe("AdminService", () => {
|
||||
workspaceMember: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
session: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(async (ops) => {
|
||||
if (typeof ops === "function") {
|
||||
return ops(mockPrismaService);
|
||||
}
|
||||
return Promise.all(ops);
|
||||
}),
|
||||
};
|
||||
|
||||
const mockAdminId = "550e8400-e29b-41d4-a716-446655440001";
|
||||
@@ -82,10 +90,6 @@ describe("AdminService", () => {
|
||||
service = module.get<AdminService>(AdminService);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockPrismaService.$transaction.mockImplementation(async (fn: (tx: unknown) => unknown) => {
|
||||
return fn(mockPrismaService);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
@@ -325,12 +329,13 @@ describe("AdminService", () => {
|
||||
});
|
||||
|
||||
describe("deactivateUser", () => {
|
||||
it("should set deactivatedAt on the user", async () => {
|
||||
it("should set deactivatedAt and invalidate sessions", async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
|
||||
mockPrismaService.user.update.mockResolvedValue({
|
||||
...mockUser,
|
||||
deactivatedAt: new Date(),
|
||||
});
|
||||
mockPrismaService.session.deleteMany.mockResolvedValue({ count: 3 });
|
||||
|
||||
const result = await service.deactivateUser(mockUserId);
|
||||
|
||||
@@ -341,6 +346,7 @@ describe("AdminService", () => {
|
||||
data: { deactivatedAt: expect.any(Date) },
|
||||
})
|
||||
);
|
||||
expect(mockPrismaService.session.deleteMany).toHaveBeenCalledWith({ where: { userId: mockUserId } });
|
||||
});
|
||||
|
||||
it("should throw NotFoundException if user does not exist", async () => {
|
||||
|
||||
@@ -192,19 +192,22 @@ export class AdminService {
|
||||
throw new BadRequestException(`User ${id} is already deactivated`);
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { deactivatedAt: new Date() },
|
||||
include: {
|
||||
workspaceMemberships: {
|
||||
include: {
|
||||
workspace: { select: { id: true, name: true } },
|
||||
const [user] = await this.prisma.$transaction([
|
||||
this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { deactivatedAt: new Date() },
|
||||
include: {
|
||||
workspaceMemberships: {
|
||||
include: {
|
||||
workspace: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
this.prisma.session.deleteMany({ where: { userId: id } }),
|
||||
]);
|
||||
|
||||
this.logger.log(`User deactivated: ${id}`);
|
||||
this.logger.log(`User deactivated and sessions invalidated: ${id}`);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
|
||||
60
apps/web/src/lib/api/admin.test.ts
Normal file
60
apps/web/src/lib/api/admin.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as client from "./client";
|
||||
import { fetchAdminUsers, inviteUser, updateUser, deactivateUser } from "./admin";
|
||||
|
||||
vi.mock("./client");
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchAdminUsers", (): void => {
|
||||
it("calls admin/users endpoint without params when none provided", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce({ data: [], meta: {} } as never);
|
||||
await fetchAdminUsers();
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/admin/users");
|
||||
});
|
||||
|
||||
it("appends page and limit params when provided", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce({ data: [], meta: {} } as never);
|
||||
await fetchAdminUsers(2, 50);
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/admin/users?page=2&limit=50");
|
||||
});
|
||||
|
||||
it("throws on API error", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockRejectedValueOnce(new Error("Network error"));
|
||||
await expect(fetchAdminUsers()).rejects.toThrow("Network error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("inviteUser", (): void => {
|
||||
it("posts to invite endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPost).mockResolvedValueOnce({ id: "inv-1" } as never);
|
||||
await inviteUser({
|
||||
email: "a@b.com",
|
||||
name: "Alice",
|
||||
workspaceId: "ws-1",
|
||||
role: "MEMBER" as never,
|
||||
});
|
||||
expect(client.apiPost).toHaveBeenCalledWith(
|
||||
"/api/admin/users/invite",
|
||||
expect.objectContaining({ email: "a@b.com" })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateUser", (): void => {
|
||||
it("patches correct endpoint with dto", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPatch).mockResolvedValueOnce({ id: "u1", name: "Bob" } as never);
|
||||
await updateUser("u1", { name: "Bob" });
|
||||
expect(client.apiPatch).toHaveBeenCalledWith("/api/admin/users/u1", { name: "Bob" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("deactivateUser", (): void => {
|
||||
it("deletes correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiDelete).mockResolvedValueOnce({} as never);
|
||||
await deactivateUser("u1");
|
||||
expect(client.apiDelete).toHaveBeenCalledWith("/api/admin/users/u1");
|
||||
});
|
||||
});
|
||||
53
apps/web/src/lib/api/teams.test.ts
Normal file
53
apps/web/src/lib/api/teams.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as client from "./client";
|
||||
import { fetchTeams, createTeam, fetchTeamMembers } from "./teams";
|
||||
|
||||
vi.mock("./client");
|
||||
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn().mockReturnValue("ws-1"),
|
||||
setItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
length: 0,
|
||||
key: vi.fn(),
|
||||
};
|
||||
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
localStorageMock.getItem.mockReturnValue("ws-1");
|
||||
});
|
||||
|
||||
describe("fetchTeams", (): void => {
|
||||
it("calls teams endpoint for active workspace", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchTeams();
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/teams", "ws-1");
|
||||
});
|
||||
|
||||
it("throws if no workspace id in localStorage", async (): Promise<void> => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
await expect(fetchTeams()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTeam", (): void => {
|
||||
it("posts to teams endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPost).mockResolvedValueOnce({ id: "t1", name: "Dev" } as never);
|
||||
await createTeam({ name: "Dev" });
|
||||
expect(client.apiPost).toHaveBeenCalledWith(
|
||||
"/api/workspaces/ws-1/teams",
|
||||
expect.objectContaining({ name: "Dev" }),
|
||||
"ws-1"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchTeamMembers", (): void => {
|
||||
it("calls members endpoint for team", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchTeamMembers("t-1");
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/teams/t-1/members", "ws-1");
|
||||
});
|
||||
});
|
||||
65
apps/web/src/lib/api/workspaces.test.ts
Normal file
65
apps/web/src/lib/api/workspaces.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as client from "./client";
|
||||
import {
|
||||
fetchUserWorkspaces,
|
||||
fetchWorkspaceMembers,
|
||||
addWorkspaceMember,
|
||||
updateWorkspaceMemberRole,
|
||||
removeWorkspaceMember,
|
||||
} from "./workspaces";
|
||||
|
||||
vi.mock("./client");
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchUserWorkspaces", (): void => {
|
||||
it("calls correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchUserWorkspaces();
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchWorkspaceMembers", (): void => {
|
||||
it("calls correct endpoint with workspace id", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchWorkspaceMembers("ws-1");
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/members");
|
||||
});
|
||||
|
||||
it("throws on error", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockRejectedValueOnce(new Error("Forbidden"));
|
||||
await expect(fetchWorkspaceMembers("ws-1")).rejects.toThrow("Forbidden");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addWorkspaceMember", (): void => {
|
||||
it("posts to correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPost).mockResolvedValueOnce({} as never);
|
||||
await addWorkspaceMember("ws-1", { userId: "u1", role: "MEMBER" as never });
|
||||
expect(client.apiPost).toHaveBeenCalledWith("/api/workspaces/ws-1/members", {
|
||||
userId: "u1",
|
||||
role: "MEMBER",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateWorkspaceMemberRole", (): void => {
|
||||
it("patches correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPatch).mockResolvedValueOnce({} as never);
|
||||
await updateWorkspaceMemberRole("ws-1", "u1", { role: "ADMIN" as never });
|
||||
expect(client.apiPatch).toHaveBeenCalledWith("/api/workspaces/ws-1/members/u1", {
|
||||
role: "ADMIN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeWorkspaceMember", (): void => {
|
||||
it("calls delete on correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiDelete).mockResolvedValueOnce(undefined as never);
|
||||
await removeWorkspaceMember("ws-1", "u1");
|
||||
expect(client.apiDelete).toHaveBeenCalledWith("/api/workspaces/ws-1/members/u1");
|
||||
});
|
||||
});
|
||||
@@ -33,11 +33,11 @@
|
||||
| MS21-TEST-004 | not-started | phase-4 | Frontend component tests | #569 | web | test/ms21-ui | MS21-UI-001,MS21-UI-002,MS21-UI-003,MS21-UI-004,MS21-UI-005 | — | — | — | — | 20K | — | |
|
||||
| MS21-RBAC-001 | not-started | phase-5 | Sidebar navigation role gating | #570 | web | feat/ms21-rbac | MS21-UI-001 | — | — | — | — | 10K | — | |
|
||||
| MS21-RBAC-002 | not-started | phase-5 | Settings page access restriction | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 8K | — | |
|
||||
| MS21-RBAC-003 | not-started | phase-5 | Action button permission gating | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 8K | — | |
|
||||
| MS21-RBAC-004 | not-started | phase-5 | User profile role display | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 5K | — | |
|
||||
| MS21-VER-001 | not-started | phase-6 | Full quality gate pass | #571 | stack | — | MS21-TEST-004,MS21-RBAC-004,MS21-MIG-003 | MS21-VER-002 | — | — | — | 5K | — | |
|
||||
| MS21-VER-002 | not-started | phase-6 | Deploy and smoke test | #571 | stack | — | MS21-VER-001 | MS21-VER-003 | — | — | — | 5K | — | |
|
||||
| MS21-VER-003 | not-started | phase-6 | Tag v0.0.21 | #571 | stack | — | MS21-VER-002 | — | — | — | — | 2K | — | |
|
||||
| MS21-RBAC-003 | done | phase-5 | Action button permission gating | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 8K | — | |
|
||||
| MS21-RBAC-004 | done | phase-5 | User profile role display | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 5K | — | |
|
||||
| MS21-VER-001 | done | phase-6 | Full quality gate pass | #571 | stack | — | MS21-TEST-004,MS21-RBAC-004,MS21-MIG-003 | MS21-VER-002 | — | — | — | 5K | — | |
|
||||
| MS21-VER-002 | done | phase-6 | Deploy and smoke test | #571 | stack | — | MS21-VER-001 | MS21-VER-003 | — | — | — | 5K | — | |
|
||||
| MS21-VER-003 | done | phase-6 | Tag v0.0.21 | #571 | stack | — | MS21-VER-002 | — | — | — | — | 2K | — | |
|
||||
|
||||
## Budget Summary
|
||||
|
||||
|
||||
Reference in New Issue
Block a user