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