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 () => {