feat(api): add terminal session persistence with Prisma model and CRUD (#517)
Some checks failed
ci/woodpecker/push/api Pipeline failed
Some checks failed
ci/woodpecker/push/api Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #517.
This commit is contained in:
229
apps/api/src/terminal/terminal-session.service.spec.ts
Normal file
229
apps/api/src/terminal/terminal-session.service.spec.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* TerminalSessionService Tests
|
||||
*
|
||||
* Unit tests for database-backed terminal session CRUD:
|
||||
* create, findByWorkspace, close, and findById.
|
||||
* PrismaService is mocked to isolate the service logic.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
import { TerminalSessionStatus } from "@prisma/client";
|
||||
import type { TerminalSession } from "@prisma/client";
|
||||
import { TerminalSessionService } from "./terminal-session.service";
|
||||
|
||||
// ==========================================
|
||||
// Helpers
|
||||
// ==========================================
|
||||
|
||||
function makeSession(overrides: Partial<TerminalSession> = {}): TerminalSession {
|
||||
return {
|
||||
id: "session-uuid-1",
|
||||
workspaceId: "workspace-uuid-1",
|
||||
name: "Terminal",
|
||||
status: TerminalSessionStatus.ACTIVE,
|
||||
createdAt: new Date("2026-02-25T00:00:00Z"),
|
||||
closedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Mock PrismaService
|
||||
// ==========================================
|
||||
|
||||
function makeMockPrisma() {
|
||||
return {
|
||||
terminalSession: {
|
||||
create: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Tests
|
||||
// ==========================================
|
||||
|
||||
describe("TerminalSessionService", () => {
|
||||
let service: TerminalSessionService;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mockPrisma: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPrisma = makeMockPrisma();
|
||||
service = new TerminalSessionService(mockPrisma);
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// create
|
||||
// ==========================================
|
||||
describe("create", () => {
|
||||
it("should call prisma.terminalSession.create with workspaceId only when no name provided", async () => {
|
||||
const session = makeSession();
|
||||
mockPrisma.terminalSession.create.mockResolvedValueOnce(session);
|
||||
|
||||
const result = await service.create("workspace-uuid-1");
|
||||
|
||||
expect(mockPrisma.terminalSession.create).toHaveBeenCalledWith({
|
||||
data: { workspaceId: "workspace-uuid-1" },
|
||||
});
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
|
||||
it("should include name in create data when name is provided", async () => {
|
||||
const session = makeSession({ name: "My Terminal" });
|
||||
mockPrisma.terminalSession.create.mockResolvedValueOnce(session);
|
||||
|
||||
const result = await service.create("workspace-uuid-1", "My Terminal");
|
||||
|
||||
expect(mockPrisma.terminalSession.create).toHaveBeenCalledWith({
|
||||
data: { workspaceId: "workspace-uuid-1", name: "My Terminal" },
|
||||
});
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
|
||||
it("should return the created session", async () => {
|
||||
const session = makeSession();
|
||||
mockPrisma.terminalSession.create.mockResolvedValueOnce(session);
|
||||
|
||||
const result = await service.create("workspace-uuid-1");
|
||||
|
||||
expect(result.id).toBe("session-uuid-1");
|
||||
expect(result.status).toBe(TerminalSessionStatus.ACTIVE);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// findByWorkspace
|
||||
// ==========================================
|
||||
describe("findByWorkspace", () => {
|
||||
it("should query for ACTIVE sessions in the given workspace, ordered by createdAt desc", async () => {
|
||||
const sessions = [makeSession(), makeSession({ id: "session-uuid-2" })];
|
||||
mockPrisma.terminalSession.findMany.mockResolvedValueOnce(sessions);
|
||||
|
||||
const result = await service.findByWorkspace("workspace-uuid-1");
|
||||
|
||||
expect(mockPrisma.terminalSession.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaceId: "workspace-uuid-1",
|
||||
status: TerminalSessionStatus.ACTIVE,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should return an empty array when no active sessions exist", async () => {
|
||||
mockPrisma.terminalSession.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.findByWorkspace("workspace-uuid-empty");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not include CLOSED sessions", async () => {
|
||||
// The where clause enforces ACTIVE status — verify it is present
|
||||
mockPrisma.terminalSession.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
await service.findByWorkspace("workspace-uuid-1");
|
||||
|
||||
const callArgs = mockPrisma.terminalSession.findMany.mock.calls[0][0] as {
|
||||
where: { status: TerminalSessionStatus };
|
||||
};
|
||||
expect(callArgs.where.status).toBe(TerminalSessionStatus.ACTIVE);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// close
|
||||
// ==========================================
|
||||
describe("close", () => {
|
||||
it("should set status to CLOSED and set closedAt when session exists", async () => {
|
||||
const existingSession = makeSession();
|
||||
const closedSession = makeSession({
|
||||
status: TerminalSessionStatus.CLOSED,
|
||||
closedAt: new Date("2026-02-25T01:00:00Z"),
|
||||
});
|
||||
|
||||
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(existingSession);
|
||||
mockPrisma.terminalSession.update.mockResolvedValueOnce(closedSession);
|
||||
|
||||
const result = await service.close("session-uuid-1");
|
||||
|
||||
expect(mockPrisma.terminalSession.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "session-uuid-1" },
|
||||
});
|
||||
expect(mockPrisma.terminalSession.update).toHaveBeenCalledWith({
|
||||
where: { id: "session-uuid-1" },
|
||||
data: {
|
||||
status: TerminalSessionStatus.CLOSED,
|
||||
closedAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
expect(result.status).toBe(TerminalSessionStatus.CLOSED);
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when session does not exist", async () => {
|
||||
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(service.close("nonexistent-id")).rejects.toThrow(NotFoundException);
|
||||
expect(mockPrisma.terminalSession.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should include a non-null closedAt timestamp on close", async () => {
|
||||
const existingSession = makeSession();
|
||||
const closedSession = makeSession({
|
||||
status: TerminalSessionStatus.CLOSED,
|
||||
closedAt: new Date(),
|
||||
});
|
||||
|
||||
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(existingSession);
|
||||
mockPrisma.terminalSession.update.mockResolvedValueOnce(closedSession);
|
||||
|
||||
const result = await service.close("session-uuid-1");
|
||||
|
||||
expect(result.closedAt).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// findById
|
||||
// ==========================================
|
||||
describe("findById", () => {
|
||||
it("should return the session when it exists", async () => {
|
||||
const session = makeSession();
|
||||
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(session);
|
||||
|
||||
const result = await service.findById("session-uuid-1");
|
||||
|
||||
expect(mockPrisma.terminalSession.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "session-uuid-1" },
|
||||
});
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
|
||||
it("should return null when session does not exist", async () => {
|
||||
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await service.findById("no-such-id");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should find CLOSED sessions as well as ACTIVE ones", async () => {
|
||||
const closedSession = makeSession({
|
||||
status: TerminalSessionStatus.CLOSED,
|
||||
closedAt: new Date(),
|
||||
});
|
||||
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(closedSession);
|
||||
|
||||
const result = await service.findById("session-uuid-1");
|
||||
|
||||
expect(result?.status).toBe(TerminalSessionStatus.CLOSED);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user