diff --git a/.gitignore b/.gitignore index bbb234f..e1555bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -# runtime / secrets -/keys +# runtime /data .env *.log diff --git a/client/src/App.tsx b/client/src/App.tsx index edf0f54..7fbfac1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,6 @@ import { NavLink, Navigate, Route, Routes } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Globe, KeyRound, Monitor, ClipboardList, Activity, KeySquare, Braces } from 'lucide-react'; +import { Globe, Monitor, ClipboardList, Activity, KeySquare, Braces } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import styles from './App.module.css'; import { SidePanel, ThemeSwitcher } from './ui'; @@ -9,7 +9,6 @@ import { EnvironmentsPage } from './pages/environment/EnvironmentsPage'; import { EnvironmentDetailPage } from './pages/environment/EnvironmentDetailPage'; import { CreateEnvironmentPage } from './pages/environment/CreateEnvironmentPage'; import { EditEnvironmentPage } from './pages/environment/EditEnvironmentPage'; -import { KeysPage } from './pages/KeysPage'; import { SessionsPage } from './pages/session/SessionsPage'; import { SessionDetailPage } from './pages/session/SessionDetailPage'; import { ScenariosPage } from './pages/scenario/ScenariosPage'; @@ -34,7 +33,6 @@ const NAV: { path: string; labelKey: string; Icon: LucideIcon }[] = [ { path: '/environments', labelKey: 'nav.environments', Icon: Globe }, { path: '/credentials', labelKey: 'nav.credentials', Icon: KeySquare }, { path: '/snippets', labelKey: 'nav.snippets', Icon: Braces }, - { path: '/keys', labelKey: 'nav.keys', Icon: KeyRound }, { path: '/sessions', labelKey: 'nav.sessions', Icon: Monitor }, { path: '/scenarios', labelKey: 'nav.scenarios', Icon: ClipboardList }, { path: '/runs', labelKey: 'nav.runs', Icon: Activity }, @@ -77,7 +75,6 @@ export default function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/client/src/api/client.ts b/client/src/api/client.ts index abf4e7a..e0e2bff 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -2,7 +2,6 @@ import type { PaginatedResponse, Credential, Environment, - KeysResponse, ScenarioCredential, Session, Scenario, @@ -13,7 +12,7 @@ import type { Snippet, } from './types'; -// In dev, Vite proxies /environments /sessions /scenarios /keys to localhost:3000. +// In dev, Vite proxies /environments /sessions /scenarios to localhost:3000. // In production (or when VITE_API_URL is set) we hit the configured origin directly. const BASE_URL: string = (import.meta.env.VITE_API_URL as string | undefined) ?? ''; @@ -132,14 +131,6 @@ export const environments = { }, }; -// ── Keys ────────────────────────────────────────────────────────────────────── - -export const keys = { - list(): Promise { - return request('/keys'); - }, -}; - // ── Sessions ────────────────────────────────────────────────────────────────── export const sessions = { diff --git a/client/src/api/types.ts b/client/src/api/types.ts index f2edcde..c3f980f 100644 --- a/client/src/api/types.ts +++ b/client/src/api/types.ts @@ -46,12 +46,6 @@ export interface Snippet { updatedAt: string; } -// ── Keys ────────────────────────────────────────────────────────────────────── - -export interface KeysResponse { - keys: string[]; -} - // ── Sessions ────────────────────────────────────────────────────────────────── export type SessionStatus = 'open' | 'closed'; diff --git a/client/src/pages/KeysPage.tsx b/client/src/pages/KeysPage.tsx deleted file mode 100644 index d628055..0000000 --- a/client/src/pages/KeysPage.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { keys } from '../api'; -import { Breadcrumbs, Table, type TableColumn } from '../ui'; -import styles from './Page.module.css'; - -interface KeyRow { - name: string; -} - -export function KeysPage() { - const { t } = useTranslation(); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const columns: TableColumn[] = [ - { key: 'name', header: t('keys.col_name'), render: (k) => k.name }, - ]; - - useEffect(() => { - keys - .list() - .then((res) => setItems(res.keys.map((name) => ({ name })))) - .catch((err: Error) => setError(err.message)) - .finally(() => setLoading(false)); - }, []); - - return ( -
- - {error &&

{error}

} - k.name} - loading={loading} - emptyMessage={t('keys.empty')} - pageSize={10} - pageSizeOptions={[10, 25, 50]} - /> - - ); -} diff --git a/client/vite.config.ts b/client/vite.config.ts index bb875d9..8830a2c 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -17,8 +17,6 @@ export default defineConfig({ '/snippets': 'http://localhost:13000', '/sessions': 'http://localhost:13000', '/scenarios': 'http://localhost:13000', - '/keys': 'http://localhost:13000', - '/login': 'http://localhost:13000', }, }, test: { diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 3d9b33e..7e7c17b 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -4,7 +4,6 @@ import { AppConfig, validateAppConfig } from "./config/app.config"; import { TypeOrmModule } from "@nestjs/typeorm"; import { ScheduleModule } from "@nestjs/schedule"; import { HealthModule } from "./health/health.module"; -import { AuthModule } from "./auth/auth.module"; import { BrowserModule } from "./browser/browser.module"; import { SessionEntity } from "./session/session.entity"; import { EnvironmentEntity } from "./environment/environment.entity"; @@ -50,7 +49,6 @@ import { SnippetModule } from "./snippet/snippet.module"; synchronize: true, }), }), - AuthModule, BrowserModule, EnvironmentModule, CredentialModule, diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts deleted file mode 100644 index be6f7df..0000000 --- a/server/src/auth/auth.controller.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Body, Controller, Get, Post } from "@nestjs/common"; -import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { AuthService } from "./auth.service"; -import { LoginDto } from "./dto/login.dto"; - -@ApiTags("auth") -@Controller() -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @Get("keys") - @ApiOperation({ - summary: "List available key identifiers from the keys directory", - }) - @ApiResponse({ - status: 200, - description: "List of key names", - schema: { - properties: { keys: { type: "array", items: { type: "string" } } }, - }, - }) - listKeys(): { keys: string[] } { - return { keys: this.authService.listKeys() }; - } - - @Post("login") - @ApiOperation({ - summary: "Log in using a file key and return the session token", - }) - @ApiResponse({ - status: 201, - description: "Login successful", - schema: { - properties: { - token: { type: "string" }, - sessionName: { type: "string" }, - }, - }, - }) - @ApiResponse({ status: 400, description: "Key not found or invalid" }) - @ApiResponse({ status: 500, description: "Automation failed" }) - login( - @Body() dto: LoginDto, - ): Promise<{ token: string; sessionName: string }> { - return this.authService.login( - dto.key, - dto.environmentName, - dto.sessionName, - ); - } -} diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts deleted file mode 100644 index c253584..0000000 --- a/server/src/auth/auth.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from "@nestjs/common"; -import { AuthController } from "./auth.controller"; -import { AuthService } from "./auth.service"; -import { SessionModule } from "../session/session.module"; -import { EnvironmentModule } from "../environment/environment.module"; - -@Module({ - imports: [SessionModule, EnvironmentModule], - controllers: [AuthController], - providers: [AuthService], - exports: [AuthService], -}) -export class AuthModule {} diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts deleted file mode 100644 index 66cea3b..0000000 --- a/server/src/auth/auth.service.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { - Injectable, - BadRequestException, - InternalServerErrorException, - NotFoundException, -} from "@nestjs/common"; -import { TraceLogger } from "../common/trace-logger"; -import { ConfigService } from "@nestjs/config"; -import { AppConfig } from "../config/app.config"; -import { chromium } from "playwright"; -import type { Page } from "playwright"; -import * as fs from "fs"; -import * as path from "path"; -import * as crypto from "crypto"; -import { SessionService } from "../session/session.service"; -import { SessionContextService } from "../session/session-context.service"; -import { EnvironmentService } from "../environment/environment.service"; - -interface KeyDescriptor { - keyFile?: string; - login?: string; - password: string; -} - -@Injectable() -export class AuthService { - private readonly logger = new TraceLogger(AuthService.name); - private readonly keysDir: string; - - constructor( - private readonly config: ConfigService, - private readonly sessionService: SessionService, - private readonly sessionContextService: SessionContextService, - private readonly environmentService: EnvironmentService, - ) { - this.keysDir = path.resolve(this.config.get("KEYS_DIR")); - } - - listKeys(): string[] { - if (!fs.existsSync(this.keysDir)) return []; - return fs - .readdirSync(this.keysDir) - .filter((f) => f.endsWith(".json")) - .map((f) => path.basename(f, ".json")); - } - - private loadKeyDescriptor(keyId: string): KeyDescriptor { - const keyJsonPath = path.join(this.keysDir, `${keyId}.json`); - if (!fs.existsSync(keyJsonPath)) { - throw new BadRequestException(`Key not found: ${keyId}`); - } - try { - return JSON.parse(fs.readFileSync(keyJsonPath, "utf-8")) as KeyDescriptor; - } catch (err) { - throw new BadRequestException(`Cannot read key descriptor: ${keyId}`, { - cause: err, - }); - } - } - - async login( - keyId: string, - environmentName: string, - sessionName?: string, - ): Promise<{ token: string; sessionName: string }> { - const resolvedSession = sessionName ?? crypto.randomUUID(); - - const env = await this.environmentService - .findAll() - .then(({ data }) => data.find((e) => e.name === environmentName)); - if (!env) - throw new NotFoundException(`Environment "${environmentName}" not found`); - - const loginUrl = env.urls.id_url; - const cabinetUrl = env.urls.cabinet_url; - if (!loginUrl) - throw new BadRequestException( - `Environment "${environmentName}" is missing id_url`, - ); - if (!cabinetUrl) - throw new BadRequestException( - `Environment "${environmentName}" is missing cabinet_url`, - ); - - const descriptor = this.loadKeyDescriptor(keyId); - const useLoginPassword = !!descriptor.login; - - if (!useLoginPassword) { - if (!descriptor.keyFile) { - throw new BadRequestException( - `Key descriptor for "${keyId}" must have either "login" or "keyFile"`, - ); - } - const keyFilePath = path.resolve(this.keysDir, descriptor.keyFile); - if (!fs.existsSync(keyFilePath)) { - throw new BadRequestException( - `Key file not found: ${descriptor.keyFile}`, - ); - } - } - - const browser = await chromium.launch({ - headless: true, - executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, - }); - try { - const context = await browser.newContext(); - const page = await context.newPage(); - - this.logger.log(`Navigating to ${loginUrl}`); - await page.goto(loginUrl, { waitUntil: "networkidle" }); - - if (useLoginPassword) { - // Click "Логін і пароль" auth method - await page.locator('p[aria-label="Логін і пароль"]').click(); - - // Fill login and password - await page.getByLabel("Електронна пошта").fill(descriptor.login!); - await page.getByLabel("Пароль").fill(descriptor.password); - - // Click "Увійти" - await page.locator('button:has-text("Увійти")').click(); - } else { - const keyFilePath = path.resolve(this.keysDir, descriptor.keyFile!); - - // Click "Файловий ключ" button - await page.getByText("Файловий ключ").click(); - - // Upload key file via hidden file input - const fileInput = page.locator( - 'input[accept=".dat,.pfx,.pk8,.zs2,.jks"][type="file"]', - ); - await fileInput.setInputFiles(keyFilePath); - - // Enter password - await page - .locator("#id-app-login-file-key-password") - .fill(descriptor.password); - - // Click "Продовжити" - await page.locator("#id-app-login-file-key-sign-button").click(); - } - - // Wait until redirected to cabinet - this.logger.log(`Waiting for redirect to ${cabinetUrl}`); - await page.waitForURL(cabinetUrl, { timeout: 30000 }); - - // Extract token from localStorage - const token = await page.evaluate(() => localStorage.getItem("token")); - - if (!token) { - throw new InternalServerErrorException( - "Login succeeded but token was not found in localStorage", - ); - } - - // Capture all cookies and localStorage from the browser context - const cookies = await context.cookies(); - const localStorageData = await page.evaluate(() => { - const entries: Record = {}; - for (let i = 0; i < window.localStorage.length; i++) { - const k = window.localStorage.key(i); - if (k !== null) entries[k] = window.localStorage.getItem(k) ?? ""; - } - return entries; - }); - - await this.sessionService.upsert( - resolvedSession, - token, - cookies, - localStorageData, - ); - - // Register the live Playwright context — browser stays open for reuse - this.sessionContextService.register( - resolvedSession, - browser, - context, - page, - ); - - this.logger.log( - `Login successful for key ${keyId}, session: ${resolvedSession}`, - ); - return { token, sessionName: resolvedSession }; - } catch (err) { - // Close browser only on failure — on success it is kept alive in SessionContextService - await browser.close().catch(() => {}); - if ( - err instanceof BadRequestException || - err instanceof InternalServerErrorException - ) { - throw err; - } - this.logger.error(`Login failed: ${(err as Error).message}`); - throw new InternalServerErrorException( - `Login automation failed: ${(err as Error).message}`, - { cause: err }, - ); - } - } - - async signWithKey(keyId: string, page: Page): Promise { - const descriptor = this.loadKeyDescriptor(keyId); - - if (!descriptor.keyFile) { - throw new BadRequestException( - `Key descriptor for "${keyId}" must have "keyFile" to sign`, - ); - } - - const keyFilePath = path.resolve(this.keysDir, descriptor.keyFile); - if (!fs.existsSync(keyFilePath)) { - throw new BadRequestException( - `Key file not found: ${descriptor.keyFile}`, - ); - } - - this.logger.log(`Signing with key ${keyId} on page: ${page.url()}`); - - // Open the EDS sign widget (skip if it's already open) - const isSignDialogOpen = await page - .locator('input[accept=".dat,.pfx,.pk8,.zs2,.jks"][type="file"]') - .isVisible() - .catch(() => false); - if (!isSignDialogOpen) { - await page - .locator("button") - .filter({ hasText: /підпис|sign/i }) - .first() - .click(); - await page.waitForTimeout(500); - } - - // Select the file key tab inside the widget - await page - .locator('button, [role="tab"], li') - .filter({ hasText: /файлов|file key/i }) - .first() - .click(); - await page.waitForTimeout(300); - - // Upload the key file - const fileInput = page.locator( - 'input[accept=".dat,.pfx,.pk8,.zs2,.jks"][type="file"]', - ); - await fileInput.setInputFiles(keyFilePath); - await page.waitForTimeout(300); - - // Enter password - await page.locator('input[type="password"]').fill(descriptor.password); - await page.waitForTimeout(200); - - // Submit - await page - .locator("button") - .filter({ hasText: /підпис|sign|підтвер/i }) - .last() - .click(); - await page.waitForLoadState("networkidle"); - - this.logger.log(`Sign completed for key ${keyId}`); - } -} diff --git a/server/src/auth/dto/login.dto.ts b/server/src/auth/dto/login.dto.ts deleted file mode 100644 index df85e55..0000000 --- a/server/src/auth/dto/login.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsNotEmpty, IsOptional, IsString } from "class-validator"; - -export class LoginDto { - @ApiProperty({ - description: - "Key identifier — filename (without extension) from the keys/ directory", - example: "3273334361", - }) - @IsString() - @IsNotEmpty() - key: string; - - @ApiProperty({ - description: "Environment name to resolve login/cabinet URLs from", - example: "liquio-diia-stg", - }) - @IsString() - @IsNotEmpty() - environmentName: string; - - @ApiPropertyOptional({ - description: - "Session name to store credentials under. Auto-generated UUID if omitted.", - example: "my-test-session", - }) - @IsOptional() - @IsString() - sessionName?: string; -} diff --git a/server/src/mcp/mcp.module.ts b/server/src/mcp/mcp.module.ts index 40793cf..608c35d 100644 --- a/server/src/mcp/mcp.module.ts +++ b/server/src/mcp/mcp.module.ts @@ -1,7 +1,6 @@ import { Module } from "@nestjs/common"; import { McpController } from "./mcp.controller"; import { McpService } from "./mcp.service"; -import { AuthModule } from "../auth/auth.module"; import { SessionModule } from "../session/session.module"; import { EnvironmentModule } from "../environment/environment.module"; import { BrowserModule } from "../browser/browser.module"; @@ -10,7 +9,6 @@ import { ScenarioModule } from "../scenario/scenario.module"; @Module({ imports: [ - AuthModule, SessionModule, EnvironmentModule, BrowserModule, diff --git a/server/src/mcp/mcp.service.ts b/server/src/mcp/mcp.service.ts index 3612294..6980a95 100644 --- a/server/src/mcp/mcp.service.ts +++ b/server/src/mcp/mcp.service.ts @@ -3,7 +3,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; import type { Request, Response } from "express"; -import { AuthService } from "../auth/auth.service"; import { SessionService } from "../session/session.service"; import { SessionContextService } from "../session/session-context.service"; import { EnvironmentService } from "../environment/environment.service"; @@ -17,7 +16,6 @@ import pkg from "../../package.json"; @Injectable() export class McpService { constructor( - private readonly authService: AuthService, private readonly sessionService: SessionService, private readonly sessionContextService: SessionContextService, private readonly environmentService: EnvironmentService, @@ -33,53 +31,6 @@ export class McpService { } private registerTools(server: McpServer): void { - // ── Auth ────────────────────────────────────────────────────────────────── - - server.registerTool( - "list_keys", - { description: "List available key identifiers from the keys directory" }, - async () => { - const keys = this.authService.listKeys(); - return { - content: [{ type: "text" as const, text: JSON.stringify(keys) }], - }; - }, - ); - - server.registerTool( - "login", - { - description: - "Log in using a file key against a named environment and store the session", - inputSchema: { - key: z - .string() - .describe( - "Key identifier (filename without extension from keys/ dir)", - ), - environmentName: z - .string() - .describe("Environment name to resolve login/cabinet URLs"), - sessionName: z - .string() - .optional() - .describe( - "Session name to store credentials under. Auto-UUID if omitted.", - ), - }, - }, - async ({ key, environmentName, sessionName }) => { - const result = await this.authService.login( - key, - environmentName, - sessionName, - ); - return { - content: [{ type: "text" as const, text: JSON.stringify(result) }], - }; - }, - ); - // ── Sessions ────────────────────────────────────────────────────────────── server.registerTool( diff --git a/server/src/scenario/dto/scenario-export.dto.ts b/server/src/scenario/dto/scenario-export.dto.ts index 90b7e80..f58bb53 100644 --- a/server/src/scenario/dto/scenario-export.dto.ts +++ b/server/src/scenario/dto/scenario-export.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsArray, + IsIn, IsInt, IsNotEmpty, IsOptional, diff --git a/server/src/scenario/scenario.module.ts b/server/src/scenario/scenario.module.ts index 8a5b9d5..a8728a1 100644 --- a/server/src/scenario/scenario.module.ts +++ b/server/src/scenario/scenario.module.ts @@ -10,7 +10,6 @@ import { CredentialEntity } from "../credential/credential.entity"; import { ScenarioService } from "./scenario.service"; import { ScenarioController } from "./scenario.controller"; import { ScenarioSchedulerService } from "./scenario-scheduler.service"; -import { AuthModule } from "../auth/auth.module"; import { CodeExecutorModule } from "../code-executor/code-executor.module"; import { SessionModule } from "../session/session.module"; import { EnvironmentModule } from "../environment/environment.module"; @@ -27,7 +26,6 @@ import { SnippetModule } from "../snippet/snippet.module"; ScenarioCredentialEntity, CredentialEntity, ]), - AuthModule, CodeExecutorModule, SessionModule, EnvironmentModule, diff --git a/server/src/scenario/scenario.service.ts b/server/src/scenario/scenario.service.ts index 842aad7..32ff1d6 100644 --- a/server/src/scenario/scenario.service.ts +++ b/server/src/scenario/scenario.service.ts @@ -334,7 +334,6 @@ export class ScenarioService { name: scenario.name, steps: scenario.steps.map((s) => ({ order: s.order, - type: s.type, title: s.title, execCode: s.execCode, validateCode: s.validateCode, @@ -367,7 +366,6 @@ export class ScenarioService { this.stepRepo.create({ scenarioId: scenario.id, order: s.order, - type: s.type, title: s.title ?? null, execCode: s.execCode ?? null, validateCode: s.validateCode ?? null, diff --git a/server/test/app.harness.ts b/server/test/app.harness.ts index 516a1e5..fdae80e 100644 --- a/server/test/app.harness.ts +++ b/server/test/app.harness.ts @@ -29,7 +29,6 @@ jest.mock("@nestjs/common", () => { return actual; }); -import { AuthModule } from "../src/auth/auth.module"; import { BrowserModule } from "../src/browser/browser.module"; import { SessionModule } from "../src/session/session.module"; import { EnvironmentModule } from "../src/environment/environment.module"; @@ -42,6 +41,9 @@ import { ScenarioStepEntity } from "../src/scenario/scenario-step.entity"; import { ScenarioRunEntity } from "../src/scenario/scenario-run.entity"; import { ScenarioRunStepEntity } from "../src/scenario/scenario-run-step.entity"; import { ScenarioRunLogEntity } from "../src/scenario/scenario-run-log.entity"; +import { ScenarioCredentialEntity } from "../src/scenario/scenario-credential.entity"; +import { CredentialEntity } from "../src/credential/credential.entity"; +import { SnippetEntity } from "../src/snippet/snippet.entity"; import { HealthController } from "../src/health/health.controller"; import { HttpExceptionFilter } from "../src/filters/http-exception.filter"; import { LoggingInterceptor } from "../src/interceptors/logging.interceptor"; @@ -66,11 +68,13 @@ export async function buildTestApp(): Promise { ScenarioRunEntity, ScenarioRunStepEntity, ScenarioRunLogEntity, + ScenarioCredentialEntity, + CredentialEntity, + SnippetEntity, ], synchronize: true, }), ScheduleModule.forRoot(), - AuthModule, BrowserModule, SessionModule, EnvironmentModule, diff --git a/server/test/auth.controller.spec.ts b/server/test/auth.controller.spec.ts deleted file mode 100644 index c477542..0000000 --- a/server/test/auth.controller.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { INestApplication } from "@nestjs/common"; -import request from "supertest"; -import { buildTestApp } from "./app.harness"; - -/** - * Auth controller integration tests. - * - * The login endpoint requires a live browser + real key files, so it is not - * exercised here (those belong to e2e tests with real credentials). - * We cover the parts that can be tested without external dependencies. - */ -describe("AuthController", () => { - let app: INestApplication; - - beforeAll(async () => { - app = await buildTestApp(); - }); - - afterAll(async () => { - await app.close(); - }); - - // ── GET /keys ────────────────────────────────────────────────────────────── - - describe("GET /keys", () => { - it("returns 200 with a keys array", async () => { - const res = await request(app.getHttpServer()).get("/keys").expect(200); - expect(res.body).toHaveProperty("keys"); - expect(Array.isArray(res.body.keys)).toBe(true); - }); - }); - - // ── POST /login ──────────────────────────────────────────────────────────── - - describe("POST /login", () => { - it("returns 400 when body is empty", async () => { - await request(app.getHttpServer()).post("/login").send({}).expect(400); - }); - - it("returns 400 when key is missing", async () => { - await request(app.getHttpServer()) - .post("/login") - .send({ environmentName: "test-env" }) - .expect(400); - }); - - it("returns 400 when environmentName is missing", async () => { - await request(app.getHttpServer()) - .post("/login") - .send({ key: "some-key" }) - .expect(400); - }); - - it("returns 400 when key file does not exist", async () => { - await request(app.getHttpServer()) - .post("/login") - .send({ key: "nonexistent-key", environmentName: "test-env" }) - .expect(404); // NotFoundException for missing environment - }); - }); -}); diff --git a/server/test/environment.controller.spec.ts b/server/test/environment.controller.spec.ts index 91428c6..f354acf 100644 --- a/server/test/environment.controller.spec.ts +++ b/server/test/environment.controller.spec.ts @@ -160,7 +160,7 @@ describe("EnvironmentController", () => { }); it("returns 404 for unknown id", async () => { - await request(app.getHttpServer()).get("/environments/99999").expect(404); + await request(app.getHttpServer()).get("/environments/00000000-0000-0000-0000-000000000001").expect(404); }); it("returns 400 for non-numeric id", async () => { @@ -187,7 +187,7 @@ describe("EnvironmentController", () => { it("returns 404 for unknown id", async () => { await request(app.getHttpServer()) - .patch("/environments/99999") + .patch("/environments/00000000-0000-0000-0000-000000000001") .send({ name: "x" }) .expect(404); }); @@ -213,7 +213,7 @@ describe("EnvironmentController", () => { it("returns 404 for unknown id", async () => { await request(app.getHttpServer()) - .delete("/environments/99999") + .delete("/environments/00000000-0000-0000-0000-000000000001") .expect(404); }); }); diff --git a/server/test/mcp.controller.spec.ts b/server/test/mcp.controller.spec.ts index bc58d90..9baa5d6 100644 --- a/server/test/mcp.controller.spec.ts +++ b/server/test/mcp.controller.spec.ts @@ -69,14 +69,14 @@ describe("McpController", () => { }); }); - // ── list_keys tool ───────────────────────────────────────────────────────── + // ── list_keys tool (removed) ────────────────────────────────────────────── describe("list_keys", () => { - it("returns a result with text content containing a JSON array", async () => { + it("returns MCP error for removed tool", async () => { const { status, rpc } = await mcpCall("list_keys"); expect(status).toBe(200); - const result = rpc.result as { content: { text: string }[] }; - expect(Array.isArray(JSON.parse(result.content[0].text))).toBe(true); + const result = rpc.result as { isError: boolean; content?: { text: string }[] }; + expect(result.isError).toBe(true); }); }); diff --git a/server/test/scenario-step-output.spec.ts b/server/test/scenario-step-output.spec.ts index 3be7c32..a6ce86b 100644 --- a/server/test/scenario-step-output.spec.ts +++ b/server/test/scenario-step-output.spec.ts @@ -42,21 +42,20 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { } /** Create a scenario and return its id. */ - async function createScenario(name = "output-scenario"): Promise { + async function createScenario(name = "output-scenario"): Promise { const sc = await scenarioService.create({ name }); return sc.id; } /** Create a step and return its id. */ async function createStep( - scenarioId: number, + scenarioId: string, order: number, execCode: string, sessionName = "output-test-session", - ): Promise { + ): Promise { const step = await scenarioService.createStep(scenarioId, { order, - type: "exec", sessionName, execCode, }); @@ -64,7 +63,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { } /** Trigger a run and process it to completion via the scheduler. */ - async function runScenario(scenarioId: number): Promise { + async function runScenario(scenarioId: string): Promise { const run = await scenarioService.createRun(scenarioId); // Drive the scheduler directly — keeps tests synchronous and fast. await scheduler.pickUpPendingRuns(); @@ -84,7 +83,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { const runId = await runScenario(scId); const row = await dataSource.query( - `SELECT output, status FROM scenario_run_steps WHERE runId = ${runId}`, + `SELECT output, status FROM scenario_run_steps WHERE runId = '${runId}'`, ); expect(row[0].status).toBe("pass"); expect(JSON.parse(row[0].output as string)).toBe(42); @@ -98,7 +97,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { const runId = await runScenario(scId); const row = await dataSource.query( - `SELECT output FROM scenario_run_steps WHERE runId = ${runId}`, + `SELECT output FROM scenario_run_steps WHERE runId = '${runId}'`, ); expect(JSON.parse(row[0].output as string)).toEqual({ foo: "bar", n: 7 }); }); @@ -111,7 +110,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { const runId = await runScenario(scId); const row = await dataSource.query( - `SELECT output FROM scenario_run_steps WHERE runId = ${runId}`, + `SELECT output FROM scenario_run_steps WHERE runId = '${runId}'`, ); expect(row[0].output).toBeNull(); }); @@ -140,7 +139,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { const runId = await runScenario(scId); const rows = await dataSource.query( - `SELECT "order", output FROM scenario_run_steps WHERE runId = ${runId} ORDER BY "order"`, + `SELECT "order", output FROM scenario_run_steps WHERE runId = '${runId}' ORDER BY "order"`, ); expect(JSON.parse(rows[0].output as string)).toBe(99); @@ -156,7 +155,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { const runId = await runScenario(scId); const rows = await dataSource.query( - `SELECT "order", output FROM scenario_run_steps WHERE runId = ${runId} ORDER BY "order"`, + `SELECT "order", output FROM scenario_run_steps WHERE runId = '${runId}' ORDER BY "order"`, ); expect(JSON.parse(rows[1].output as string)).toBe("step-zero"); @@ -170,7 +169,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { const runId = await runScenario(scId); const row = await dataSource.query( - `SELECT output FROM scenario_run_steps WHERE runId = ${runId}`, + `SELECT output FROM scenario_run_steps WHERE runId = '${runId}'`, ); expect(row[0].output).toBeNull(); @@ -193,7 +192,7 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => { const runId = await runScenario(scId); const rows = await dataSource.query( - `SELECT "order", output FROM scenario_run_steps WHERE runId = ${runId} ORDER BY "order"`, + `SELECT "order", output FROM scenario_run_steps WHERE runId = '${runId}' ORDER BY "order"`, ); expect(JSON.parse(rows[0].output as string)).toEqual([1, 2]); diff --git a/server/test/scenario.controller.spec.ts b/server/test/scenario.controller.spec.ts index 01552e9..cbdf210 100644 --- a/server/test/scenario.controller.spec.ts +++ b/server/test/scenario.controller.spec.ts @@ -21,24 +21,23 @@ describe("ScenarioController", () => { .post("/scenarios") .send({ name }) .expect(201); - return res.body as { id: number; name: string }; + return res.body as { id: string; name: string }; } async function createStep( - scenarioId: number, + scenarioId: string, overrides: Record = {}, ) { const res = await request(app.getHttpServer()) .post(`/scenarios/${scenarioId}/steps`) .send({ order: 0, - type: "exec", sessionName: "test-session", execCode: "return 1;", ...overrides, }) .expect(201); - return res.body as { id: number }; + return res.body as { id: string }; } // ── POST /scenarios ──────────────────────────────────────────────────────── @@ -142,7 +141,7 @@ describe("ScenarioController", () => { }); it("returns 404 for unknown id", async () => { - await request(app.getHttpServer()).get("/scenarios/99999").expect(404); + await request(app.getHttpServer()).get("/scenarios/00000000-0000-0000-0000-000000000001").expect(404); }); }); @@ -160,7 +159,7 @@ describe("ScenarioController", () => { it("returns 404 for unknown id", async () => { await request(app.getHttpServer()) - .patch("/scenarios/99999") + .patch("/scenarios/00000000-0000-0000-0000-000000000001") .send({ name: "x" }) .expect(404); }); @@ -178,7 +177,7 @@ describe("ScenarioController", () => { }); it("returns 404 for unknown id", async () => { - await request(app.getHttpServer()).delete("/scenarios/99999").expect(404); + await request(app.getHttpServer()).delete("/scenarios/00000000-0000-0000-0000-000000000001").expect(404); }); }); @@ -191,7 +190,6 @@ describe("ScenarioController", () => { .post(`/scenarios/${sc.id}/steps`) .send({ order: 0, - type: "exec", sessionName: "my-session", execCode: "return 1;", }) @@ -199,48 +197,50 @@ describe("ScenarioController", () => { expect(res.body.id).toBeDefined(); expect(res.body.order).toBe(0); - expect(res.body.type).toBe("exec"); expect(res.body.sessionName).toBe("my-session"); }); - it("creates a login step", async () => { + it("creates a step without execCode", async () => { const sc = await createScenario(); const res = await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) - .send({ order: 0, type: "login", sessionName: "session-x" }) + .send({ order: 0, sessionName: "session-x" }) .expect(201); - expect(res.body.type).toBe("login"); + expect(res.body.sessionName).toBe("session-x"); + expect(res.body.execCode).toBeNull(); }); it("returns 400 when order is missing", async () => { const sc = await createScenario(); await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) - .send({ type: "exec", sessionName: "x" }) + .send({ sessionName: "x" }) .expect(400); }); - it("returns 400 when type is invalid", async () => { + it("ignores unknown fields in payload", async () => { const sc = await createScenario(); - await request(app.getHttpServer()) + const res = await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) .send({ order: 0, type: "unknown", sessionName: "x" }) - .expect(400); + .expect(201); + + expect(res.body).not.toHaveProperty("type"); }); - it("returns 400 when sessionName is missing", async () => { + it("allows missing sessionName", async () => { const sc = await createScenario(); await request(app.getHttpServer()) .post(`/scenarios/${sc.id}/steps`) - .send({ order: 0, type: "exec" }) - .expect(400); + .send({ order: 0, execCode: "return 1;" }) + .expect(201); }); it("returns 404 for unknown scenario", async () => { await request(app.getHttpServer()) - .post("/scenarios/99999/steps") - .send({ order: 0, type: "exec", sessionName: "x" }) + .post("/scenarios/00000000-0000-0000-0000-000000000001/steps") + .send({ order: 0, sessionName: "x" }) .expect(404); }); @@ -276,7 +276,7 @@ describe("ScenarioController", () => { it("returns 404 for unknown step", async () => { const sc = await createScenario(); await request(app.getHttpServer()) - .get(`/scenarios/${sc.id}/steps/99999`) + .get(`/scenarios/${sc.id}/steps/00000000-0000-0000-0000-000000000001`) .expect(404); }); }); @@ -300,7 +300,7 @@ describe("ScenarioController", () => { it("returns 404 for unknown step", async () => { const sc = await createScenario(); await request(app.getHttpServer()) - .patch(`/scenarios/${sc.id}/steps/99999`) + .patch(`/scenarios/${sc.id}/steps/00000000-0000-0000-0000-000000000001`) .send({ order: 1 }) .expect(404); }); @@ -350,7 +350,7 @@ describe("ScenarioController", () => { it("returns 404 for unknown scenario", async () => { await request(app.getHttpServer()) - .post("/scenarios/99999/run") + .post("/scenarios/00000000-0000-0000-0000-000000000001/run") .expect(404); }); }); @@ -405,7 +405,7 @@ describe("ScenarioController", () => { it("returns 404 for unknown scenario", async () => { await request(app.getHttpServer()) - .get("/scenarios/99999/runs") + .get("/scenarios/00000000-0000-0000-0000-000000000001/runs") .expect(404); }); }); @@ -417,13 +417,11 @@ describe("ScenarioController", () => { const sc = await createScenario("export-me"); await createStep(sc.id, { order: 0, - type: "login", sessionName: "s", execCode: '{"keyId":"k","environmentName":"e"}', }); await createStep(sc.id, { order: 1, - type: "exec", sessionName: "s", execCode: "return 1;", validateCode: "return true;", @@ -484,7 +482,7 @@ describe("ScenarioController", () => { it("returns 404 for unknown scenario", async () => { await request(app.getHttpServer()) - .get("/scenarios/99999/export") + .get("/scenarios/00000000-0000-0000-0000-000000000001/export") .expect(404); }); }); @@ -498,21 +496,18 @@ describe("ScenarioController", () => { steps: [ { order: 0, - type: "login", sessionName: "s", execCode: '{"keyId":"k","environmentName":"e"}', validateCode: null, }, { order: 1, - type: "exec", sessionName: "s", execCode: "return 1;", validateCode: "return true;", }, { order: 2, - type: "sign", sessionName: "s", execCode: '{"keyId":"k"}', validateCode: null, @@ -528,12 +523,9 @@ describe("ScenarioController", () => { expect(res.body.id).toBeDefined(); expect(res.body.name).toBe("imported scenario"); expect(res.body.steps).toHaveLength(3); - expect(res.body.steps[0].type).toBe("login"); - expect(res.body.steps[1].type).toBe("exec"); - expect(res.body.steps[2].type).toBe("sign"); }); - it("assigns a new id (does not collide with source)", async () => { + it("preserves id when importing an exported scenario with id", async () => { const sc = await createScenario("original"); const exportRes = await request(app.getHttpServer()) .get(`/scenarios/${sc.id}/export`) @@ -544,14 +536,13 @@ describe("ScenarioController", () => { .send(exportRes.body) .expect(201); - expect(importRes.body.id).not.toBe(sc.id); + expect(importRes.body.id).toBe(sc.id); }); it("round-trips a scenario faithfully", async () => { const sc = await createScenario("roundtrip"); await createStep(sc.id, { order: 0, - type: "exec", sessionName: "rs", execCode: "return 42;", validateCode: "return true;", @@ -600,14 +591,14 @@ describe("ScenarioController", () => { .expect(400); }); - it("returns 400 when a step has an invalid type", async () => { + it("ignores unknown step fields during import", async () => { await request(app.getHttpServer()) .post("/scenarios/import") .send({ name: "bad-type", steps: [{ order: 0, type: "unknown", sessionName: "s" }], }) - .expect(400); + .expect(201); }); }); @@ -654,7 +645,7 @@ describe("ScenarioController", () => { it("returns 404 for unknown run", async () => { const sc = await createScenario(); await request(app.getHttpServer()) - .get(`/scenarios/${sc.id}/run/99999`) + .get(`/scenarios/${sc.id}/run/00000000-0000-0000-0000-000000000001`) .expect(404); }); @@ -674,7 +665,9 @@ describe("ScenarioController", () => { it("returns 404 for unknown scenario", async () => { await request(app.getHttpServer()) - .get("/scenarios/99999/run/1") + .get( + "/scenarios/00000000-0000-0000-0000-000000000001/run/00000000-0000-0000-0000-000000000001", + ) .expect(404); }); }); @@ -693,7 +686,7 @@ describe("ScenarioController", () => { // Manually mark run as pass so wait resolves immediately const dataSource = app.get(DataSource); await dataSource.query( - `UPDATE scenario_runs SET status='pass' WHERE id=${runId}`, + `UPDATE scenario_runs SET status='pass' WHERE id='${runId}'`, ); const res = await request(app.getHttpServer()) @@ -716,7 +709,7 @@ describe("ScenarioController", () => { const dataSource = app.get(DataSource); await dataSource.query( - `UPDATE scenario_runs SET status='fail' WHERE id=${runId}`, + `UPDATE scenario_runs SET status='fail' WHERE id='${runId}'`, ); const res = await request(app.getHttpServer()) @@ -729,13 +722,15 @@ describe("ScenarioController", () => { it("returns 404 for unknown run", async () => { const sc = await createScenario(); await request(app.getHttpServer()) - .post(`/scenarios/${sc.id}/run/99999/wait`) + .post(`/scenarios/${sc.id}/run/00000000-0000-0000-0000-000000000001/wait`) .expect(404); }); it("returns 404 for unknown scenario", async () => { await request(app.getHttpServer()) - .post("/scenarios/99999/run/1/wait") + .post( + "/scenarios/00000000-0000-0000-0000-000000000001/run/00000000-0000-0000-0000-000000000001/wait", + ) .expect(404); }); }); diff --git a/server/test/session.controller.spec.ts b/server/test/session.controller.spec.ts index 22d5b56..531b1e8 100644 --- a/server/test/session.controller.spec.ts +++ b/server/test/session.controller.spec.ts @@ -141,7 +141,7 @@ describe("SessionController", () => { }); it("returns 404 for unknown id", async () => { - await request(app.getHttpServer()).delete("/sessions/99999").expect(404); + await request(app.getHttpServer()).delete("/sessions/00000000-0000-0000-0000-000000000001").expect(404); }); it("returns 400 for non-numeric id", async () => {