refactor(environment): rename urls payload to data

- replace hardcoded URL keys with a generic key-value data map

- align backend DTOs, MCP schemas, and service import/export mapping

- update environment UI forms to edit JSON data instead of fixed fields
This commit is contained in:
2026-04-10 21:22:46 +03:00
parent b45f95d8cf
commit fd515ee459
15 changed files with 141 additions and 143 deletions
+3 -3
View File
@@ -118,13 +118,13 @@ export const environments = {
get(id: string): Promise<Environment> { get(id: string): Promise<Environment> {
return request(`/environments/${id}`); return request(`/environments/${id}`);
}, },
create(name: string, urls: Environment['urls']): Promise<Environment> { create(name: string, data: Environment['data']): Promise<Environment> {
return request('/environments', { return request('/environments', {
method: 'POST', method: 'POST',
body: JSON.stringify({ name, urls }), body: JSON.stringify({ name, data }),
}); });
}, },
update(id: string, patch: Partial<Pick<Environment, 'name' | 'urls'>>): Promise<Environment> { update(id: string, patch: Partial<Pick<Environment, 'name' | 'data'>>): Promise<Environment> {
return request(`/environments/${id}`, { return request(`/environments/${id}`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(patch), body: JSON.stringify(patch),
+2 -6
View File
@@ -9,17 +9,14 @@ export interface PaginatedResponse<T> {
// ── Environments ────────────────────────────────────────────────────────────── // ── Environments ──────────────────────────────────────────────────────────────
export interface EnvironmentUrls { export interface EnvironmentData {
id_url?: string;
cabinet_url?: string;
admin_url?: string;
[key: string]: string | undefined; [key: string]: string | undefined;
} }
export interface Environment { export interface Environment {
id: string; id: string;
name: string; name: string;
urls: EnvironmentUrls; data: EnvironmentData;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -67,7 +64,6 @@ export interface ScenarioStep {
scenarioId: string; scenarioId: string;
order: number; order: number;
title: string | null; title: string | null;
sessionName: string | null;
execCode: string | null; execCode: string | null;
validateCode: string | null; validateCode: string | null;
createdAt: string; createdAt: string;
+5 -6
View File
@@ -21,11 +21,11 @@
"title": "Environments", "title": "Environments",
"col_id": "ID", "col_id": "ID",
"col_name": "Name", "col_name": "Name",
"col_urls": "URLs", "col_data": "Data",
"col_updated": "Updated", "col_updated": "Updated",
"empty": "No environments yet.", "empty": "No environments yet.",
"loading": "Loading…", "loading": "Loading…",
"no_urls": "No URLs configured.", "no_data": "No data configured.",
"menu_label": "Environment options", "menu_label": "Environment options",
"action_view": "View details", "action_view": "View details",
"action_delete": "Delete", "action_delete": "Delete",
@@ -41,14 +41,13 @@
"form_name": "Name", "form_name": "Name",
"form_name_placeholder": "e.g. staging, production", "form_name_placeholder": "e.g. staging, production",
"form_name_required": "Name is required", "form_name_required": "Name is required",
"form_id_url": "ID URL", "form_data": "Data (JSON)",
"form_cabinet_url": "Cabinet URL", "form_data_invalid_json": "Must be a valid JSON object",
"form_admin_url": "Admin URL",
"back": "Environments", "back": "Environments",
"field_id": "ID", "field_id": "ID",
"field_created": "Created", "field_created": "Created",
"field_updated": "Updated", "field_updated": "Updated",
"section_urls": "URLs" "section_data": "Data"
}, },
"credentials": { "credentials": {
"title": "Credentials", "title": "Credentials",
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { X, Globe, Save } from 'lucide-react'; import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api'; import { environments } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui'; import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css'; import styles from '../Page.module.css';
export function CreateEnvironmentPage() { export function CreateEnvironmentPage() {
@@ -11,10 +11,9 @@ export function CreateEnvironmentPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [name, setName] = useState(''); const [name, setName] = useState('');
const [idUrl, setIdUrl] = useState(''); const [dataJson, setDataJson] = useState('{\n "id": "https://id.example.com"\n}');
const [cabinetUrl, setCabinetUrl] = useState('');
const [adminUrl, setAdminUrl] = useState('');
const [nameError, setNameError] = useState(''); const [nameError, setNameError] = useState('');
const [dataError, setDataError] = useState('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -24,14 +23,27 @@ export function CreateEnvironmentPage() {
setNameError(t('environments.form_name_required')); setNameError(t('environments.form_name_required'));
return; return;
} }
let parsedData: Record<string, string | undefined>;
try {
const raw = dataJson.trim() ? JSON.parse(dataJson.trim()) : {};
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
throw new Error('invalid');
}
parsedData = Object.fromEntries(
Object.entries(raw as Record<string, unknown>).map(([k, v]) => [
k,
v == null ? undefined : String(v),
]),
);
setDataError('');
} catch {
setDataError(t('environments.form_data_invalid_json'));
return;
}
setSaving(true); setSaving(true);
setError(null); setError(null);
try { try {
const env = await environments.create(name.trim(), { const env = await environments.create(name.trim(), parsedData);
id_url: idUrl.trim() || undefined,
cabinet_url: cabinetUrl.trim() || undefined,
admin_url: adminUrl.trim() || undefined,
});
navigate(`/environments/${env.id}`); navigate(`/environments/${env.id}`);
} catch (err) { } catch (err) {
setError((err as Error).message); setError((err as Error).message);
@@ -68,27 +80,20 @@ export function CreateEnvironmentPage() {
required required
autoFocus autoFocus
/> />
<Input <div className={styles.formField}>
label={t('environments.form_id_url')} <label className={styles.fieldLabel}>{t('environments.form_data')}</label>
type="url" <CodeEditor
placeholder="https://…" language="json"
value={idUrl} value={dataJson}
onChange={(e) => setIdUrl(e.target.value)} onChange={(v) => {
/> setDataJson(v);
<Input setDataError('');
label={t('environments.form_cabinet_url')} }}
type="url" rows={8}
placeholder="https://…" error={!!dataError}
value={cabinetUrl}
onChange={(e) => setCabinetUrl(e.target.value)}
/>
<Input
label={t('environments.form_admin_url')}
type="url"
placeholder="https://…"
value={adminUrl}
onChange={(e) => setAdminUrl(e.target.value)}
/> />
{dataError && <span className={styles.fieldError}>{dataError}</span>}
</div>
</div> </div>
<div className={styles.formActions}> <div className={styles.formActions}>
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { X, Globe, Save } from 'lucide-react'; import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api'; import { environments } from '../../api';
import type { Environment } from '../../api'; import type { Environment } from '../../api';
import { Breadcrumbs, Button, Card, Input } from '../../ui'; import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
import styles from '../Page.module.css'; import styles from '../Page.module.css';
export function EditEnvironmentPage() { export function EditEnvironmentPage() {
@@ -14,10 +14,9 @@ export function EditEnvironmentPage() {
const [env, setEnv] = useState<Environment | null>(null); const [env, setEnv] = useState<Environment | null>(null);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [idUrl, setIdUrl] = useState(''); const [dataJson, setDataJson] = useState('{}');
const [cabinetUrl, setCabinetUrl] = useState('');
const [adminUrl, setAdminUrl] = useState('');
const [nameError, setNameError] = useState(''); const [nameError, setNameError] = useState('');
const [dataError, setDataError] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -29,9 +28,7 @@ export function EditEnvironmentPage() {
.then((data) => { .then((data) => {
setEnv(data); setEnv(data);
setName(data.name); setName(data.name);
setIdUrl(data.urls.id_url ?? ''); setDataJson(JSON.stringify(data.data ?? {}, null, 2));
setCabinetUrl(data.urls.cabinet_url ?? '');
setAdminUrl(data.urls.admin_url ?? '');
}) })
.catch((err: Error) => setError(err.message)) .catch((err: Error) => setError(err.message))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -43,16 +40,29 @@ export function EditEnvironmentPage() {
setNameError(t('environments.form_name_required')); setNameError(t('environments.form_name_required'));
return; return;
} }
let parsedData: Record<string, string | undefined>;
try {
const raw = dataJson.trim() ? JSON.parse(dataJson.trim()) : {};
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
throw new Error('invalid');
}
parsedData = Object.fromEntries(
Object.entries(raw as Record<string, unknown>).map(([k, v]) => [
k,
v == null ? undefined : String(v),
]),
);
setDataError('');
} catch {
setDataError(t('environments.form_data_invalid_json'));
return;
}
setSaving(true); setSaving(true);
setError(null); setError(null);
try { try {
await environments.update(id!, { await environments.update(id!, {
name: name.trim(), name: name.trim(),
urls: { data: parsedData,
id_url: idUrl.trim() || undefined,
cabinet_url: cabinetUrl.trim() || undefined,
admin_url: adminUrl.trim() || undefined,
},
}); });
navigate(`/environments/${id}`); navigate(`/environments/${id}`);
} catch (err) { } catch (err) {
@@ -96,27 +106,20 @@ export function EditEnvironmentPage() {
required required
autoFocus autoFocus
/> />
<Input <div className={styles.formField}>
label={t('environments.form_id_url')} <label className={styles.fieldLabel}>{t('environments.form_data')}</label>
type="url" <CodeEditor
placeholder="https://…" language="json"
value={idUrl} value={dataJson}
onChange={(e) => setIdUrl(e.target.value)} onChange={(v) => {
/> setDataJson(v);
<Input setDataError('');
label={t('environments.form_cabinet_url')} }}
type="url" rows={8}
placeholder="https://…" error={!!dataError}
value={cabinetUrl}
onChange={(e) => setCabinetUrl(e.target.value)}
/>
<Input
label={t('environments.form_admin_url')}
type="url"
placeholder="https://…"
value={adminUrl}
onChange={(e) => setAdminUrl(e.target.value)}
/> />
{dataError && <span className={styles.fieldError}>{dataError}</span>}
</div>
</div> </div>
<div className={styles.formActions}> <div className={styles.formActions}>
@@ -105,14 +105,14 @@ export function EnvironmentDetailPage() {
</Card> </Card>
<div className={styles.stepsSection}> <div className={styles.stepsSection}>
<h2 className={styles.sectionHeading}>{t('environments.section_urls')}</h2> <h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
<Card> <Card>
{Object.entries(env.urls).filter(([, v]) => v).length === 0 ? ( {Object.entries(env.data).filter(([, v]) => v).length === 0 ? (
<p className={styles.muted}>{t('environments.no_urls')}</p> <p className={styles.muted}>{t('environments.no_data')}</p>
) : ( ) : (
<DescriptionList <DescriptionList
layout="grid" layout="grid"
items={Object.entries(env.urls) items={Object.entries(env.data)
.filter(([, v]) => v) .filter(([, v]) => v)
.map(([key, value]) => ({ .map(([key, value]) => ({
term: key, term: key,
@@ -20,7 +20,7 @@ import styles from '../Page.module.css';
function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: string) => void }) { function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: string) => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const urlEntries = Object.entries(env.urls).filter(([, v]) => v); const dataEntries = Object.entries(env.data).filter(([, v]) => v);
const handleExport = async () => { const handleExport = async () => {
const data = await environments.exportEnvironment(env.id); const data = await environments.exportEnvironment(env.id);
@@ -87,15 +87,15 @@ function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: s
footer={footer} footer={footer}
onClick={() => navigate(`/environments/${env.id}`)} onClick={() => navigate(`/environments/${env.id}`)}
> >
{urlEntries.length > 0 && ( {dataEntries.length > 0 && (
<DescriptionList <DescriptionList
layout="compact" layout="compact"
truncate truncate
items={urlEntries.map(([key, value]) => ({ term: key, detail: value }))} items={dataEntries.map(([key, value]) => ({ term: key, detail: value }))}
/> />
)} )}
{urlEntries.length === 0 && ( {dataEntries.length === 0 && (
<p className={styles.envCardEmpty}>{t('environments.no_urls')}</p> <p className={styles.envCardEmpty}>{t('environments.no_data')}</p>
)} )}
</Card> </Card>
); );
@@ -7,7 +7,7 @@ import { TraceLogger } from "../common/trace-logger";
import { parse } from "acorn"; import { parse } from "acorn";
import type { Page, BrowserContext } from "playwright"; import type { Page, BrowserContext } from "playwright";
import { dumpDom } from "./dom-helpers"; import { dumpDom } from "./dom-helpers";
import type { EnvironmentUrls } from "../environment/environment.entity"; import type { EnvironmentData } from "../environment/environment.entity";
export interface ExecResult { export interface ExecResult {
result: unknown; result: unknown;
@@ -49,7 +49,7 @@ export class CodeExecutorService {
log?: ScriptLogger, log?: ScriptLogger,
getStepOutput?: (order: number) => Promise<unknown>, getStepOutput?: (order: number) => Promise<unknown>,
credentials?: Record<string, unknown>, credentials?: Record<string, unknown>,
environment?: EnvironmentUrls | null, environment?: EnvironmentData | null,
snippets?: Record<string, string> | null, snippets?: Record<string, string> | null,
result?: unknown, result?: unknown,
): Promise<ExecResult> { ): Promise<ExecResult> {
@@ -61,7 +61,7 @@ export class CodeExecutorService {
.join(" "); .join(" ");
const credMap: Record<string, unknown> = credentials ?? {}; const credMap: Record<string, unknown> = credentials ?? {};
const envUrls: EnvironmentUrls = environment ?? {}; const envData: EnvironmentData = environment ?? {};
const snippetMap: Record<string, string> = snippets ?? {}; const snippetMap: Record<string, string> = snippets ?? {};
// pageHelpers is referenced by runSnippet, so we declare it as a var first. // pageHelpers is referenced by runSnippet, so we declare it as a var first.
@@ -91,14 +91,14 @@ export class CodeExecutorService {
} }
return credMap[alias]; return credMap[alias];
}, },
/** All URLs defined for the current environment (may be empty if no environment is set). */ /** All values defined for the current environment (may be empty if no environment is set). */
env: { ...envUrls }, env: { ...envData },
/** Returns the URL for the given key, or throws if it is not defined. */ /** Returns the value for the given key, or throws if it is not defined. */
getEnvUrl: (key: string): string => { getEnvUrl: (key: string): string => {
const value = envUrls[key]; const value = envData[key];
if (value == null) { if (value == null) {
throw new Error( throw new Error(
`Environment URL "${key}" is not defined for this environment`, `Environment value "${key}" is not defined for this environment`,
); );
} }
return value; return value;
@@ -1,6 +1,6 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsObject, IsString } from "class-validator"; import { IsNotEmpty, IsObject, IsString } from "class-validator";
import { EnvironmentUrls } from "../environment.entity"; import { EnvironmentData } from "../environment.entity";
export class CreateEnvironmentDto { export class CreateEnvironmentDto {
@ApiProperty({ example: "liquio-diia-stg" }) @ApiProperty({ example: "liquio-diia-stg" })
@@ -9,13 +9,13 @@ export class CreateEnvironmentDto {
name: string; name: string;
@ApiProperty({ @ApiProperty({
description: "Map of URL identifiers to URL strings", description: "Generic key-value map for environment metadata",
example: { example: {
id_url: "https://id-liquio-diia-stg.kitsoft.ua/", id: "https://id-liquio-diia-stg.kitsoft.ua/",
cabinet_url: "https://cabinet-liquio-diia-stg.kitsoft.ua/", cabinet: "https://cabinet-liquio-diia-stg.kitsoft.ua/",
admin_url: "https://admin-liquio-diia-stg.kitsoft.ua/", feature_flag: "enabled",
}, },
}) })
@IsObject() @IsObject()
urls: EnvironmentUrls; data: EnvironmentData;
} }
@@ -7,7 +7,7 @@ import {
IsString, IsString,
IsUUID, IsUUID,
} from "class-validator"; } from "class-validator";
import { EnvironmentUrls } from "../environment.entity"; import { EnvironmentData } from "../environment.entity";
export class EnvironmentExportDto { export class EnvironmentExportDto {
@ApiPropertyOptional() @ApiPropertyOptional()
@@ -27,5 +27,5 @@ export class EnvironmentExportDto {
@ApiProperty() @ApiProperty()
@IsObject() @IsObject()
urls: EnvironmentUrls; data: EnvironmentData;
} }
+2 -5
View File
@@ -6,10 +6,7 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from "typeorm"; } from "typeorm";
export interface EnvironmentUrls { export interface EnvironmentData {
id_url?: string;
cabinet_url?: string;
admin_url?: string;
[key: string]: string | undefined; [key: string]: string | undefined;
} }
@@ -22,7 +19,7 @@ export class EnvironmentEntity {
name: string; name: string;
@Column("simple-json") @Column("simple-json")
urls: EnvironmentUrls; data: EnvironmentData;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@@ -71,7 +71,7 @@ export class EnvironmentService {
kind: "environment", kind: "environment",
id: env.id, id: env.id,
name: env.name, name: env.name,
urls: env.urls, data: env.data,
}; };
} }
@@ -79,18 +79,18 @@ export class EnvironmentService {
if (dto.id) { if (dto.id) {
const existing = await this.repo.findOneBy({ id: dto.id }); const existing = await this.repo.findOneBy({ id: dto.id });
if (existing) { if (existing) {
Object.assign(existing, { name: dto.name, urls: dto.urls }); Object.assign(existing, { name: dto.name, data: dto.data });
return this.repo.save(existing); return this.repo.save(existing);
} }
return this.repo.save( return this.repo.save(
this.repo.create({ id: dto.id, name: dto.name, urls: dto.urls }), this.repo.create({ id: dto.id, name: dto.name, data: dto.data }),
); );
} }
const byName = await this.repo.findOneBy({ name: dto.name }); const byName = await this.repo.findOneBy({ name: dto.name });
if (byName) { if (byName) {
Object.assign(byName, { urls: dto.urls }); Object.assign(byName, { data: dto.data });
return this.repo.save(byName); return this.repo.save(byName);
} }
return this.repo.save(this.repo.create({ name: dto.name, urls: dto.urls })); return this.repo.save(this.repo.create({ name: dto.name, data: dto.data }));
} }
} }
+11 -13
View File
@@ -6,7 +6,7 @@ import type { Request, Response } from "express";
import { SessionService } from "../session/session.service"; import { SessionService } from "../session/session.service";
import { SessionContextService } from "../session/session-context.service"; import { SessionContextService } from "../session/session-context.service";
import { EnvironmentService } from "../environment/environment.service"; import { EnvironmentService } from "../environment/environment.service";
import type { EnvironmentUrls } from "../environment/environment.entity"; import type { EnvironmentData } from "../environment/environment.entity";
import { BrowserService } from "../browser/browser.service"; import { BrowserService } from "../browser/browser.service";
import { CodeExecutorService } from "../code-executor/code-executor.service"; import { CodeExecutorService } from "../code-executor/code-executor.service";
import { ScenarioService } from "../scenario/scenario.service"; import { ScenarioService } from "../scenario/scenario.service";
@@ -174,23 +174,21 @@ export class McpService {
server.registerTool( server.registerTool(
"create_environment", "create_environment",
{ {
description: "Create a new named environment with a set of URLs", description: "Create a new named environment with generic data",
inputSchema: { inputSchema: {
name: z name: z
.string() .string()
.describe("Unique environment name, e.g. liquio-diia-stg"), .describe("Unique environment name, e.g. liquio-diia-stg"),
urls: z data: z
.record(z.string(), z.string()) .record(z.string(), z.string())
.describe( .describe("Map of string keys to string values"),
"Map of URL keys to URL strings (id_url, cabinet_url, admin_url, …)",
),
}, },
}, },
async ({ name, urls }) => { async ({ name, data }) => {
try { try {
const env = await this.environmentService.create({ const env = await this.environmentService.create({
name, name,
urls: urls as EnvironmentUrls, data: data as EnvironmentData,
}); });
return { return {
content: [{ type: "text" as const, text: JSON.stringify(env) }], content: [{ type: "text" as const, text: JSON.stringify(env) }],
@@ -207,21 +205,21 @@ export class McpService {
server.registerTool( server.registerTool(
"update_environment", "update_environment",
{ {
description: "Update an existing environment (name and/or urls)", description: "Update an existing environment (name and/or data)",
inputSchema: { inputSchema: {
id: z.string().uuid().describe("Environment ID to update"), id: z.string().uuid().describe("Environment ID to update"),
name: z.string().optional().describe("New name"), name: z.string().optional().describe("New name"),
urls: z data: z
.record(z.string(), z.string()) .record(z.string(), z.string())
.optional() .optional()
.describe("New URLs map"), .describe("New data map"),
}, },
}, },
async ({ id, name, urls }) => { async ({ id, name, data }) => {
try { try {
const env = await this.environmentService.update(id, { const env = await this.environmentService.update(id, {
name, name,
urls: urls as EnvironmentUrls | undefined, data: data as EnvironmentData | undefined,
}); });
return { return {
content: [{ type: "text" as const, text: JSON.stringify(env) }], content: [{ type: "text" as const, text: JSON.stringify(env) }],
+16 -16
View File
@@ -19,44 +19,44 @@ describe("EnvironmentController", () => {
it("creates an environment and returns 201", async () => { it("creates an environment and returns 201", async () => {
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-a", urls: { id_url: "https://id.example.com" } }) .send({ name: "env-a", data: { id: "https://id.example.com" } })
.expect(201); .expect(201);
expect(res.body.id).toBeDefined(); expect(res.body.id).toBeDefined();
expect(res.body.name).toBe("env-a"); expect(res.body.name).toBe("env-a");
expect(res.body.urls.id_url).toBe("https://id.example.com"); expect(res.body.data.id).toBe("https://id.example.com");
}); });
it("returns 400 when name is missing", async () => { it("returns 400 when name is missing", async () => {
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ urls: { id_url: "https://id.example.com" } }) .send({ data: { id: "https://id.example.com" } })
.expect(400); .expect(400);
}); });
it("returns 400 when urls is missing", async () => { it("returns 400 when data is missing", async () => {
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-no-urls" }) .send({ name: "env-no-data" })
.expect(400); .expect(400);
}); });
it("returns 400 when urls is not an object", async () => { it("returns 400 when data is not an object", async () => {
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-bad-urls", urls: "not-an-object" }) .send({ name: "env-bad-data", data: "not-an-object" })
.expect(400); .expect(400);
}); });
it("returns 409 when name already exists", async () => { it("returns 409 when name already exists", async () => {
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-duplicate", urls: {} }) .send({ name: "env-duplicate", data: {} })
.expect(201); .expect(201);
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-duplicate", urls: {} }) .send({ name: "env-duplicate", data: {} })
.expect(409); .expect(409);
}); });
}); });
@@ -78,11 +78,11 @@ describe("EnvironmentController", () => {
// seed two extra environments // seed two extra environments
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-page-1", urls: {} }) .send({ name: "env-page-1", data: {} })
.expect(201); .expect(201);
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-page-2", urls: {} }) .send({ name: "env-page-2", data: {} })
.expect(201); .expect(201);
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
@@ -109,10 +109,10 @@ describe("EnvironmentController", () => {
it("orders by name ASC", async () => { it("orders by name ASC", async () => {
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "zzz-env", urls: {} }); .send({ name: "zzz-env", data: {} });
await request(app.getHttpServer()) await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "aaa-env", urls: {} }); .send({ name: "aaa-env", data: {} });
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.get("/environments?orderBy=name&orderDir=ASC") .get("/environments?orderBy=name&orderDir=ASC")
@@ -148,7 +148,7 @@ describe("EnvironmentController", () => {
.post("/environments") .post("/environments")
.send({ .send({
name: "env-get-one", name: "env-get-one",
urls: { cabinet_url: "https://cabinet.example.com" }, data: { cabinet: "https://cabinet.example.com" },
}) })
.expect(201); .expect(201);
@@ -174,7 +174,7 @@ describe("EnvironmentController", () => {
it("updates name and returns 200", async () => { it("updates name and returns 200", async () => {
const created = await request(app.getHttpServer()) const created = await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-patch-me", urls: {} }) .send({ name: "env-patch-me", data: {} })
.expect(201); .expect(201);
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
@@ -199,7 +199,7 @@ describe("EnvironmentController", () => {
it("deletes and returns 204", async () => { it("deletes and returns 204", async () => {
const created = await request(app.getHttpServer()) const created = await request(app.getHttpServer())
.post("/environments") .post("/environments")
.send({ name: "env-delete-me", urls: {} }) .send({ name: "env-delete-me", data: {} })
.expect(201); .expect(201);
await request(app.getHttpServer()) await request(app.getHttpServer())
+1 -1
View File
@@ -118,7 +118,7 @@ describe("McpController", () => {
it("creates an environment via MCP", async () => { it("creates an environment via MCP", async () => {
const { status, rpc } = await mcpCall("create_environment", { const { status, rpc } = await mcpCall("create_environment", {
name: "mcp-test-env", name: "mcp-test-env",
urls: { id_url: "https://id.example.com" }, data: { id: "https://id.example.com" },
}); });
expect(status).toBe(200); expect(status).toBe(200);
const result = rpc.result as { content: { text: string }[] }; const result = rpc.result as { content: { text: string }[] };