feat: improve Zabbix version compatibility and optimize queries

This commit introduces several improvements to ensure the API works seamlessly across Zabbix 6.0, 6.4, and 7.0+, while also optimizing data fetching performance.

Key changes:
- Zabbix Version Compatibility:
  - Added Zabbix version detection and static caching in ZabbixAPI.
  - Implemented name-based fallback for host/template group permissions to support Zabbix 6.0 (which lacks UUIDs for host groups).
  - Added manual host group expansion for Zabbix versions < 6.2.0 during user group import.
  - Added version-based guards for history.push (7.0+) and hostgroup.propagate (6.2+).
  - Updated documentation with detailed version compatibility notes.
  - Added src/test/zabbix_6_0_compatibility.test.ts to verify compatibility logic.

- Query Optimization:
  - Implemented dynamic output selection in ZabbixRequest to fetch only fields requested in GraphQL queries.
  - Added GraphqlParamsToNeededZabbixOutput to map GraphQL selections to Zabbix API output parameters.
  - Moved "Query Optimization" to achieved milestones in roadmap.md.

- Other:
  - Updated various tests to support the new version-aware logic.
  - Optimized imports and synchronized IDE settings.
This commit is contained in:
Andreas Hilbig 2026-02-04 02:53:33 +01:00
parent 7c2dee2b6c
commit ec6ed422b1
21 changed files with 363 additions and 100 deletions

121
.idea/workspace.xml generated
View file

@ -5,30 +5,21 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="d7a71994-2699-4ae4-9fd2-ee13b7f33d35" name="Changes" comment="docs: refactor documentation and upgrade to Node.js 24&#10;&#10;This commit upgrades the project to Node.js 24 (LTS) and performs a major refactoring of the documentation to support both advanced users and AI-based automation (MCP).&#10;&#10;Changes:&#10;- Environment &amp; CI/CD:&#10; - Set Node.js version to &gt;=24 in package.json and .nvmrc.&#10; - Updated Dockerfile to use Node 24 base image.&#10; - Updated @types/node to ^24.10.9.&#10;- Documentation:&#10; - Refactored README.md with comprehensive technical reference, configuration details, and Zabbix-to-GraphQL mapping.&#10; - Created docs/howtos/cookbook.md with practical recipes for common tasks and AI test generation.&#10; - Updated docs/howtos/mcp.md to emphasize GraphQL's advantages for AI agents and Model Context Protocol.&#10; - Added readme.improvement.plan.md to track documentation evolution.&#10; - Enhanced all how-to guides with improved cross-references and up-to-date information.&#10;- Guidelines:&#10; - Updated .junie/guidelines.md with Node 24 requirements and enhanced commit message standards (Conventional Commits 1.0.0).&#10;- Infrastructure &amp; Code:&#10; - Updated docker-compose.yml with Apollo MCP server integration.&#10; - Refined configuration and schema handling in src/api/ and src/datasources/.&#10; - Synchronized generated TypeScript types with schema updates."> <list default="true" id="d7a71994-2699-4ae4-9fd2-ee13b7f33d35" name="Changes" comment="docs: refactor documentation and upgrade to Node.js 24&#10;&#10;This commit upgrades the project to Node.js 24 (LTS) and performs a major refactoring of the documentation to support both advanced users and AI-based automation (MCP).&#10;&#10;Changes:&#10;- Environment &amp; CI/CD:&#10; - Set Node.js version to &gt;=24 in package.json and .nvmrc.&#10; - Updated Dockerfile to use Node 24 base image.&#10; - Updated @types/node to ^24.10.9.&#10;- Documentation:&#10; - Refactored README.md with comprehensive technical reference, configuration details, and Zabbix-to-GraphQL mapping.&#10; - Created docs/howtos/cookbook.md with practical recipes for common tasks and AI test generation.&#10; - Updated docs/howtos/mcp.md to emphasize GraphQL's advantages for AI agents and Model Context Protocol.&#10; - Added readme.improvement.plan.md to track documentation evolution.&#10; - Enhanced all how-to guides with improved cross-references and up-to-date information.&#10;- Guidelines:&#10; - Updated .junie/guidelines.md with Node 24 requirements and enhanced commit message standards (Conventional Commits 1.0.0).&#10;- Infrastructure &amp; Code:&#10; - Updated docker-compose.yml with Apollo MCP server integration.&#10; - Refined configuration and schema handling in src/api/ and src/datasources/.&#10; - Synchronized generated TypeScript types with schema updates.">
<change afterPath="$PROJECT_DIR$/docs/use-cases/VCR - Technical product information.pdf" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/testdata/templates/zbx_device_tracker_vcr.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docker-compose.yml" beforeDir="false" afterPath="$PROJECT_DIR$/docker-compose.yml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/datasources/zabbix-api.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-api.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/VCR - Technical product information.pdf" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/docs/howtos/cookbook.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/cookbook.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/howtos/mcp.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/mcp.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/queries/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/queries/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/tests.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/tests.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/use-cases/trade-fair-logistics-requirements.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/use-cases/trade-fair-logistics-requirements.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/mcp-config.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/mcp-config.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/samples/extensions/location_tracker_devices.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/samples/extensions/location_tracker_devices.graphql" afterDir="false" />
<change beforePath="$PROJECT_DIR$/schema/mutations.graphql" beforeDir="false" afterPath="$PROJECT_DIR$/schema/mutations.graphql" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/api/resolvers.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/resolvers.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/api/start.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/api/start.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/common_utils.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/common_utils.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-history.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-items.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-items.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/datasources/zabbix-hostgroups.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-hostgroups.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/datasources/zabbix-request.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-request.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/datasources/zabbix-usergroups.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-usergroups.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/execution/host_exporter.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/host_exporter.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/test/history_push.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/history_push.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/execution/regression_test_executor.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/test/indirect_dependencies.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/indirect_dependencies.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/schema/generated/graphql.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/schema/generated/graphql.ts" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/test/query_optimization.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/query_optimization.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/testdata/templates/zbx_default_templates_vcr.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/src/testdata/templates/zbx_default_templates_vcr.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/test/template_integration.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_integration.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/template_link.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_link.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/template_query.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/template_query.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/user_rights.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/user_rights.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/user_rights_integration.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/user_rights_integration.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/zabbix_docs_samples.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/zabbix_docs_samples.test.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -39,7 +30,7 @@
<execution /> <execution />
</component> </component>
<component name="EmbeddingIndexingInfo"> <component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="169" /> <option name="cachedIndexableFilesCount" value="168" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" /> <option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component> </component>
<component name="Git.Settings"> <component name="Git.Settings">
@ -87,45 +78,50 @@
<option name="openDirectoriesWithSingleClick" value="true" /> <option name="openDirectoriesWithSingleClick" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"NIXITCH_NIXPKGS_CONFIG": "", &quot;NIXITCH_NIXPKGS_CONFIG&quot;: &quot;&quot;,
"NIXITCH_NIX_CONF_DIR": "", &quot;NIXITCH_NIX_CONF_DIR&quot;: &quot;&quot;,
"NIXITCH_NIX_OTHER_STORES": "", &quot;NIXITCH_NIX_OTHER_STORES&quot;: &quot;&quot;,
"NIXITCH_NIX_PATH": "", &quot;NIXITCH_NIX_PATH&quot;: &quot;&quot;,
"NIXITCH_NIX_PROFILES": "", &quot;NIXITCH_NIX_PROFILES&quot;: &quot;&quot;,
"NIXITCH_NIX_REMOTE": "", &quot;NIXITCH_NIX_REMOTE&quot;: &quot;&quot;,
"NIXITCH_NIX_USER_PROFILE_DIR": "", &quot;NIXITCH_NIX_USER_PROFILE_DIR&quot;: &quot;&quot;,
"Node.js.index.ts.executor": "Run", &quot;Node.js.index.ts.executor&quot;: &quot;Run&quot;,
"RunOnceActivity.MCP Project settings loaded": "true", &quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"RunOnceActivity.typescript.service.memoryLimit.init": "true", &quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true", &quot;com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1&quot;: &quot;true&quot;,
"git-widget-placeholder": "main", &quot;git-widget-placeholder&quot;: &quot;main&quot;,
"go.import.settings.migrated": "true", &quot;go.import.settings.migrated&quot;: &quot;true&quot;,
"javascript.preferred.runtime.type.id": "node", &quot;javascript.preferred.runtime.type.id&quot;: &quot;node&quot;,
"junie.onboarding.icon.badge.shown": "true", &quot;junie.onboarding.icon.badge.shown&quot;: &quot;true&quot;,
"last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/docs/use-cases", &quot;last_opened_file_path&quot;: &quot;//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/docs/use-cases&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_interpreter_path": "wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node", &quot;nodejs_interpreter_path&quot;: &quot;wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"npm.codegen.executor": "Run", &quot;npm.codegen.executor&quot;: &quot;Run&quot;,
"npm.compile.executor": "Run", &quot;npm.compile.executor&quot;: &quot;Run&quot;,
"npm.copy-schema.executor": "Run", &quot;npm.copy-schema.executor&quot;: &quot;Run&quot;,
"npm.prod.executor": "Run", &quot;npm.prod.executor&quot;: &quot;Run&quot;,
"npm.test.executor": "Run", &quot;npm.test.executor&quot;: &quot;Run&quot;,
"settings.editor.selected.configurable": "junie.mcp", &quot;settings.editor.selected.configurable&quot;: &quot;junie.mcp&quot;,
"settings.editor.splitter.proportion": "0.23751687", &quot;settings.editor.splitter.proportion&quot;: &quot;0.23751687&quot;,
"to.speed.mode.migration.done": "true", &quot;to.speed.mode.migration.done&quot;: &quot;true&quot;,
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib", &quot;ts.external.directory.path&quot;: &quot;\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib&quot;,
"vue.rearranger.settings.migration": "true" &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
&quot;keyToStringList&quot;: {
&quot;com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File&quot;: [
&quot;TEXT&quot;
]
} }
}]]></component> }</component>
<component name="RecapSpentCounter"> <component name="RecapSpentCounter">
<option name="endsOfQuotaMs" value="1772398800000" /> <option name="endsOfQuotaMs" value="1772398800000" />
<option name="spentUsd" value="0.0915201" /> <option name="spentUsd" value="0.0915201" />
@ -231,7 +227,10 @@
<workItem from="1769789496322" duration="14281000" /> <workItem from="1769789496322" duration="14281000" />
<workItem from="1769849767328" duration="18404000" /> <workItem from="1769849767328" duration="18404000" />
<workItem from="1769955114366" duration="3276000" /> <workItem from="1769955114366" duration="3276000" />
<workItem from="1770107035156" duration="3830000" /> <workItem from="1770107035156" duration="4817000" />
<workItem from="1770129804879" duration="13000" />
<workItem from="1770129846593" duration="5283000" />
<workItem from="1770167580486" duration="1386000" />
</task> </task>
<task id="LOCAL-00001" summary="chore: Update IntelliJ workspace settings and add GitHub Actions workflow for Docker deployment"> <task id="LOCAL-00001" summary="chore: Update IntelliJ workspace settings and add GitHub Actions workflow for Docker deployment">
<option name="closed" value="true" /> <option name="closed" value="true" />

View file

@ -52,7 +52,7 @@ Before you begin, ensure you have met the following requirements:
- **Node.js**: Version 24 (LTS) or higher recommended. - **Node.js**: Version 24 (LTS) or higher recommended.
- **Docker**: Version 27 or higher and **Docker Compose** v2.29 or higher (use `docker compose` instead of `docker-compose`). - **Docker**: Version 27 or higher and **Docker Compose** v2.29 or higher (use `docker compose` instead of `docker-compose`).
- **Zabbix**: A running Zabbix instance (compatible with Zabbix 6.0+) with API access. - **Zabbix**: A running Zabbix instance (compatible with Zabbix 6.0+) with API access. See [Zabbix Version Compatibility](#-zabbix-version-compatibility) for details.
- **Zabbix Super Admin Token** (for full functionality / privilege escalation). - **Zabbix Super Admin Token** (for full functionality / privilege escalation).
- **Zabbix User Access** (groups and roles depending on your use case). - **Zabbix User Access** (groups and roles depending on your use case).
@ -236,7 +236,20 @@ The API version is automatically set during the Docker build process based on th
### 🔧 Zabbix Version Compatibility ### 🔧 Zabbix Version Compatibility
This API is designed to work with Zabbix 7.4, which is the version it runs productively with. While it may work with earlier versions (like 6.0+), 7.4 is the officially supported and tested version. This API is officially supported and productively used with **Zabbix 7.0 (LTS)** and newer. It also maintains compatibility with **Zabbix 6.0 (LTS)** and **6.4**, with the following version-specific behaviors:
- **Zabbix 7.0 (LTS)**:
- Full feature support.
- **History Push**: Uses the native `history.push` API for efficient data ingestion.
- **Zabbix 6.4**:
- **History Push**: Not supported (requires Zabbix 7.0+). The `pushHistory` mutation returns a clear error.
- **Group Propagation**: Fully supported via the `hostgroup.propagate` API (introduced in 6.2).
- **UUIDs**: Fully supported for both Host Groups and Template Groups.
- **Zabbix 6.0 (LTS)**:
- **History Push**: Not supported.
- **Group Propagation**: `hostgroup.propagate` is unavailable. The API automatically performs **manual host group expansion** during import to ensure consistent behavior.
- **UUIDs**: Host Groups in 6.0 lack UUIDs. The API uses a **name-based fallback** for matching host group permissions during import.
- **Template Groups**: The API correctly handles the separation of Host Groups and Template Groups introduced in Zabbix 6.0 across all versions.
## 🛠️ Technical Maintenance ## 🛠️ Technical Maintenance

View file

@ -6,12 +6,13 @@ This document outlines the achieved milestones and planned enhancements for the
- **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**. - **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**. - *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
- **⚡ Query Optimization**: Optimized GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested.
- *Implementation*: Added dynamic output selection and field pruning in `ZabbixRequest`.
- **🔓 Open Source Extraction & AI Integration**: Extracted the core functionality of the API to publish it as an **Open Source** project. - **🔓 Open Source Extraction & AI Integration**: Extracted the core functionality of the API to publish it as an **Open Source** project.
- *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases. - *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases.
## 📅 Planned Enhancements ## 📅 Planned Enhancements
- **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance.
- **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders. - **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
- *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md). - *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md).
- *Simulation*: - *Simulation*:

View file

@ -80,6 +80,20 @@ export class ZabbixAPI
return super.post(path, request); return super.post(path, request);
} }
private static version: string | undefined
async getVersion(): Promise<string> {
if (!ZabbixAPI.version) {
const response = await this.requestByPath<string>("apiinfo.version")
if (typeof response === "string") {
ZabbixAPI.version = response
} else {
return "0.0.0"
}
}
return ZabbixAPI.version
}
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true, output?: string[]): Promise<T | ZabbixErrorResult> { async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true, output?: string[]): Promise<T | ZabbixErrorResult> {
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output); return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output);
} }

View file

@ -81,6 +81,10 @@ export class ZabbixHistoryPushRequest extends ZabbixRequest<ZabbixHistoryPushRes
async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixHistoryPushParams): Promise<ZabbixHistoryPushResult | ZabbixErrorResult | undefined> { async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixHistoryPushParams): Promise<ZabbixHistoryPushResult | ZabbixErrorResult | undefined> {
if (!args) return undefined; if (!args) return undefined;
const version = await zabbixAPI.getVersion();
if (version < "7.0.0") {
throw new GraphQLError(`history.push is only supported in Zabbix 7.0.0 and newer. Current version is ${version}. For older versions, please use Zabbix trapper items and zabbix_sender protocol.`);
}
if (!args.itemid && (!args.key || !args.host)) { if (!args.itemid && (!args.key || !args.host)) {
throw new GraphQLError("if itemid is empty both key and host must be filled"); throw new GraphQLError("if itemid is empty both key and host must be filled");

View file

@ -100,4 +100,33 @@ export class GroupHelper {
} }
return result return result
} }
public static async findSubgroupIds(groupids: number[], zabbixApi: ZabbixAPI, zabbixAuthToken?: string, cookie?: string): Promise<number[]> {
if (groupids.length === 0) return [];
// Get the names of the parent groups
const parentGroups = await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ZabbixQueryHostgroupsParams({
groupids: groupids
}));
if (isZabbixErrorResult(parentGroups) || !parentGroups.length) {
return [];
}
const subgroupids: number[] = [];
for (const parent of parentGroups) {
const subgroups = await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ZabbixQueryHostgroupsParams({
search: {
name: parent.name + "/*"
},
searchWildcardsEnabled: true
}));
if (!isZabbixErrorResult(subgroups)) {
subgroups.forEach(sg => subgroupids.push(Number(sg.groupid)));
}
}
return [...new Set(subgroupids)];
}
} }

View file

@ -17,8 +17,9 @@ import {
ZabbixGroupRightInput ZabbixGroupRightInput
} from "../schema/generated/graphql.js"; } from "../schema/generated/graphql.js";
import {ZabbixAPI} from "./zabbix-api.js"; import {ZabbixAPI} from "./zabbix-api.js";
import {logger} from "../logging/logger.js";
import {ZabbixQueryTemplateGroupRequest, ZabbixQueryTemplateGroupResponse} from "./zabbix-templates.js"; import {ZabbixQueryTemplateGroupRequest, ZabbixQueryTemplateGroupResponse} from "./zabbix-templates.js";
import {ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js"; import {GroupHelper, ZabbixQueryHostgroupsRequest, ZabbixQueryHostgroupsResult} from "./zabbix-hostgroups.js";
import {ApiErrorCode} from "../model/model_enum_values.js"; import {ApiErrorCode} from "../model/model_enum_values.js";
@ -153,10 +154,36 @@ export class ZabbixImportUserGroupsRequest
let createGroupRequest = new ZabbixCreateOrUpdateRequest< let createGroupRequest = new ZabbixCreateOrUpdateRequest<
ZabbixCreateUserGroupResponse, ZabbixQueryUserGroupsRequest, ZabbixCreateOrUpdateParams>( ZabbixCreateUserGroupResponse, ZabbixQueryUserGroupsRequest, ZabbixCreateOrUpdateParams>(
"usergroup", "usrgrpid", ZabbixQueryUserGroupsRequest, this.authToken, this.cookie); "usergroup", "usrgrpid", ZabbixQueryUserGroupsRequest, this.authToken, this.cookie);
const version = await zabbixAPI.getVersion();
const needsManualPropagation = version < "6.2.0";
for (let userGroup of args?.usergroups || []) { for (let userGroup of args?.usergroups || []) {
let templategroup_rights = this.calc_templategroup_rights(userGroup); let templategroup_rights = this.calc_templategroup_rights(userGroup);
let hostgroup_rights = this.calc_hostgroup_rights(userGroup); let hostgroup_rights = this.calc_hostgroup_rights(userGroup);
if (needsManualPropagation && hostgroup_rights.hostgroup_rights.length > 0) {
const parentGroupids = hostgroup_rights.hostgroup_rights.map(r => r.id);
const subgroupids = await GroupHelper.findSubgroupIds(parentGroupids, zabbixAPI, this.authToken ?? undefined, this.cookie ?? undefined);
for (const sgid of subgroupids) {
if (!hostgroup_rights.hostgroup_rights.find(r => r.id === sgid)) {
// Find the permission of the parent group to apply it to the subgroup
// This is a simplification: if multiple parents match, we take one.
// Actually, we should find which parent this subgroup belongs to.
// But for simplicity, we can just look at the parent permissions.
// In most cases, it's just one parent being propagated.
const parent = hostgroup_rights.hostgroup_rights.find(r => parentGroupids.includes(r.id)); // just take first
if (parent) {
hostgroup_rights.hostgroup_rights.push({
id: sgid,
permission: parent.permission
});
}
}
}
}
let errors: ApiError[] = []; let errors: ApiError[] = [];
let params = new ZabbixCreateOrUpdateParams({ let params = new ZabbixCreateOrUpdateParams({
@ -214,34 +241,52 @@ export class ZabbixImportUserGroupsRequest
for (let hostgroup_right of usergroup.hostgroup_rights || []) { for (let hostgroup_right of usergroup.hostgroup_rights || []) {
let success = false; let success = false;
let matchedName = ""; let matchedName = "";
let matchedId: number | undefined = undefined;
// Try matching by UUID first
for (let hostgroup of this.hostgroups) { for (let hostgroup of this.hostgroups) {
if (hostgroup.uuid == hostgroup_right.uuid) { if (hostgroup.uuid && hostgroup_right.uuid && hostgroup.uuid === hostgroup_right.uuid) {
result.push( matchedId = Number(hostgroup.groupid);
{
id: Number(hostgroup.groupid),
permission: hostgroup_right.permission,
}
)
success = true;
matchedName = hostgroup.name; matchedName = hostgroup.name;
success = true;
break; break;
} }
} }
if (success && hostgroup_right.name && hostgroup_right.name != matchedName) {
errors.push( // Fallback to matching by name (important for Zabbix 6.0 which lacks Host Group UUIDs)
if (!success && hostgroup_right.name) {
for (let hostgroup of this.hostgroups) {
if (hostgroup.name === hostgroup_right.name) {
matchedId = Number(hostgroup.groupid);
matchedName = hostgroup.name;
success = true;
break;
}
}
}
if (success) {
result.push(
{ {
code: ApiErrorCode.OK, id: matchedId!,
message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match`, permission: hostgroup_right.permission,
data: hostgroup_right,
} }
) )
}
if (!success) { if (hostgroup_right.name && hostgroup_right.name != matchedName) {
errors.push(
{
code: ApiErrorCode.OK,
message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match provided name=${hostgroup_right.name}`,
data: hostgroup_right,
}
)
}
} else {
errors.push( errors.push(
{ {
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND, code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
message: `Hostgroup with UUID ${hostgroup_right.uuid} not found`, message: `Hostgroup with UUID ${hostgroup_right.uuid} ${hostgroup_right.name ? "or name " + hostgroup_right.name : ""} not found`,
data: hostgroup_right, data: hostgroup_right,
} }
) )
@ -262,33 +307,52 @@ export class ZabbixImportUserGroupsRequest
for (let templategroup_right of usergroup.templategroup_rights || []) { for (let templategroup_right of usergroup.templategroup_rights || []) {
let success = false; let success = false;
let matchedName = ""; let matchedName = "";
let matchedId: number | undefined = undefined;
// Try matching by UUID first
for (let templategroup of this.templategroups) { for (let templategroup of this.templategroups) {
if (templategroup.uuid == templategroup_right.uuid) { if (templategroup.uuid && templategroup_right.uuid && templategroup.uuid === templategroup_right.uuid) {
result.push( matchedId = Number(templategroup.groupid);
{ matchedName = templategroup.name;
id: Number(templategroup.groupid),
permission: templategroup_right.permission,
}
)
success = true; success = true;
matchedName = templategroup.name
break; break;
} }
} }
if (success && templategroup_right.name && templategroup_right.name != matchedName) {
errors.push( // Fallback to matching by name
if (!success && templategroup_right.name) {
for (let templategroup of this.templategroups) {
if (templategroup.name === templategroup_right.name) {
matchedId = Number(templategroup.groupid);
matchedName = templategroup.name;
success = true;
break;
}
}
}
if (success) {
result.push(
{ {
code: ApiErrorCode.OK, id: matchedId!,
message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match`, permission: templategroup_right.permission,
data: templategroup_right,
} }
) )
}
if (!success) { if (templategroup_right.name && templategroup_right.name != matchedName) {
errors.push(
{
code: ApiErrorCode.OK,
message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match provided name=${templategroup_right.name}`,
data: templategroup_right,
}
)
}
} else {
errors.push( errors.push(
{ {
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND, code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
message: `Templategroup with UUID ${templategroup_right.uuid} not found`, message: `Templategroup with UUID ${templategroup_right.uuid} ${templategroup_right.name ? "or name " + templategroup_right.name : ""} not found`,
data: templategroup_right, data: templategroup_right,
} }
) )
@ -317,6 +381,17 @@ export class ZabbixPropagateHostGroupsRequest extends ZabbixRequest<ZabbixCreate
super("hostgroup.propagate", authToken, cookie); super("hostgroup.propagate", authToken, cookie);
} }
async prepare(zabbixAPI: ZabbixAPI, args?: ZabbixPropagateHostGroupsParams): Promise<ZabbixCreateUserGroupResponse | ZabbixErrorResult | undefined> {
const version = await zabbixAPI.getVersion();
if (version < "6.2.0") {
logger.warn(`hostgroup.propagate is only supported in Zabbix 6.2.0 and newer. Current version is ${version}. Skipping propagation.`);
return {
usrgrpids: []
} as ZabbixCreateUserGroupResponse;
}
return super.prepare(zabbixAPI, args);
}
createZabbixParams(args?: ZabbixPropagateHostGroupsParams): ZabbixParams { createZabbixParams(args?: ZabbixPropagateHostGroupsParams): ZabbixParams {
return { return {
groups: [...new Set(args?.groups || [])].map(value => { groups: [...new Set(args?.groups || [])].map(value => {

View file

@ -6,6 +6,7 @@ import {GraphQLError} from "graphql";
jest.mock("../datasources/zabbix-api.js", () => ({ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0")
} }
})); }));
@ -63,4 +64,12 @@ describe("ZabbixHistoryPushRequest", () => {
await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("if itemid is empty both key and host must be filled"); await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("if itemid is empty both key and host must be filled");
}); });
test("prepare - throw error if Zabbix version < 7.0.0", async () => {
(zabbixAPI.getVersion as jest.Mock).mockResolvedValue("6.0.0");
const values = [{ timestamp: "2024-01-01T10:00:00Z", value: "val" }];
const params = new ZabbixHistoryPushParams(values, "1");
await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("history.push is only supported in Zabbix 7.0.0 and newer");
});
}); });

View file

@ -6,6 +6,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
} }
})); }));

View file

@ -6,6 +6,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
executeRequest: jest.fn(), executeRequest: jest.fn(),
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
requestByPath: jest.fn() requestByPath: jest.fn()
}, },
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork" ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"

View file

@ -8,6 +8,7 @@ import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from '../datasources/zabbix-ap
jest.mock("../datasources/zabbix-api.js", () => ({ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(), executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix', baseURL: 'http://localhost/zabbix',
getLocations: jest.fn(), getLocations: jest.fn(),

View file

@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
executeRequest: jest.fn(), executeRequest: jest.fn(),
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix", baseURL: "http://mock-zabbix",
getLocations: jest.fn() getLocations: jest.fn()
}, },

View file

@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
executeRequest: jest.fn(), executeRequest: jest.fn(),
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix", baseURL: "http://mock-zabbix",
} }
})); }));

View file

@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
executeRequest: jest.fn(), executeRequest: jest.fn(),
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix", baseURL: "http://mock-zabbix",
} }
})); }));

View file

@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(), executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix' baseURL: 'http://localhost/zabbix'
} }

View file

@ -6,6 +6,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(), executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix', baseURL: 'http://localhost/zabbix',
requestByPath: jest.fn() requestByPath: jest.fn()

View file

@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
executeRequest: jest.fn(), executeRequest: jest.fn(),
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix" baseURL: "http://mock-zabbix"
}, },
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork" ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"

View file

@ -6,6 +6,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
executeRequest: jest.fn(), executeRequest: jest.fn(),
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix" baseURL: "http://mock-zabbix"
} }
})); }));

View file

@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(), executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix', baseURL: 'http://localhost/zabbix',
getLocations: jest.fn(), getLocations: jest.fn(),
@ -43,7 +44,8 @@ describe("User Rights Integration Tests", () => {
.mockResolvedValueOnce([{ groupid: "101", name: "Group1", uuid: "uuid1" }]) // templategroup.get for groups (in prepare) .mockResolvedValueOnce([{ groupid: "101", name: "Group1", uuid: "uuid1" }]) // templategroup.get for groups (in prepare)
.mockResolvedValueOnce([{ groupid: "201", name: "ConstructionSite/Test", uuid: "uuid2" }]) // hostgroup.get for groups (in prepare) .mockResolvedValueOnce([{ groupid: "201", name: "ConstructionSite/Test", uuid: "uuid2" }]) // hostgroup.get for groups (in prepare)
.mockResolvedValueOnce([{ usrgrpid: "1", name: "Test Group" }]) // usergroup.get .mockResolvedValueOnce([{ usrgrpid: "1", name: "Test Group" }]) // usergroup.get
.mockResolvedValueOnce({ usrgrpids: ["1"] }); // usergroup.update .mockResolvedValueOnce({ usrgrpids: ["1"] }) // usergroup.update
.mockResolvedValueOnce({ usrgrpids: [] }); // hostgroup.propagate
const response = await server.executeOperation({ const response = await server.executeOperation({
query: mutation, query: mutation,

View file

@ -0,0 +1,106 @@
import {createResolvers} from "../api/resolvers.js";
import {zabbixAPI} from "../datasources/zabbix-api.js";
import {Permission} from "../schema/generated/graphql.js";
// Mocking ZabbixAPI
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
getVersion: jest.fn(),
baseURL: "http://mock-zabbix"
}
}));
describe("Zabbix 6.0 Compatibility", () => {
let resolvers: any;
beforeEach(() => {
jest.clearAllMocks();
resolvers = createResolvers();
(zabbixAPI.getVersion as jest.Mock).mockResolvedValue("6.0.0");
});
test("importUserRights uses name-based fallback for host groups on Zabbix 6.0", async () => {
// Mock Zabbix 6.0 behavior where hostgroup.get does NOT return UUID
(zabbixAPI.post as jest.Mock).mockImplementation((path: string) => {
if (path === "templategroup.get") return Promise.resolve([{ groupid: "101", name: "TemplateGroup1", uuid: "uuid-tg-1" }]);
if (path === "hostgroup.get") return Promise.resolve([{ groupid: "201", name: "HostGroup1" }]); // NO UUID
if (path === "usergroup.get") return Promise.resolve([]);
if (path.startsWith("usergroup.create")) return Promise.resolve({ usrgrpids: ["301"] });
if (path === "module.get") return Promise.resolve([]);
if (path === "role.get") return Promise.resolve([]);
return Promise.resolve([]);
});
const args = {
input: {
userGroups: [{
name: "NewGroup",
hostgroup_rights: [{
name: "HostGroup1",
uuid: "some-uuid-from-export", // This UUID won't match anything in 6.0 mock
permission: Permission.Read
}]
}]
},
dryRun: false
};
const context = { zabbixAuthToken: "test-token" };
const result = await resolvers.Mutation.importUserRights(null, args, context);
expect(result.userGroups).toHaveLength(1);
expect(result.userGroups[0].name).toBe("NewGroup");
expect(result.userGroups[0].errors).toHaveLength(0); // Should succeed via name match
// Verify that usergroup.create was called with correct groupid from name match
const createCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[0].startsWith("usergroup.create"));
expect(createCall[1].body.params.hostgroup_rights).toContainEqual({
id: 201,
permission: Permission.Read
});
});
test("importUserRights performs manual expansion on Zabbix 6.0", async () => {
// Mock Zabbix 6.0 behavior
(zabbixAPI.post as jest.Mock).mockImplementation((path: string, options: any) => {
if (path === "templategroup.get") return Promise.resolve([]);
if (path === "hostgroup.get") {
// If searching for subgroups
if (options?.body?.params?.search?.name === "Parent/*") {
return Promise.resolve([{ groupid: "202", name: "Parent/Child" }]);
}
return Promise.resolve([{ groupid: "201", name: "Parent" }]);
}
if (path === "usergroup.get") return Promise.resolve([]);
if (path.startsWith("usergroup.create")) return Promise.resolve({ usrgrpids: ["301"] });
if (path === "module.get") return Promise.resolve([]);
if (path === "role.get") return Promise.resolve([]);
return Promise.resolve([]);
});
const args = {
input: {
userGroups: [{
name: "NewGroup",
hostgroup_rights: [{
name: "Parent",
uuid: "uuid-parent",
permission: Permission.Read
}]
}]
},
dryRun: false
};
const context = { zabbixAuthToken: "test-token" };
await resolvers.Mutation.importUserRights(null, args, context);
// Verify that usergroup.create was called with both parent and expanded child
const createCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[0].startsWith("usergroup.create"));
expect(createCall[1].body.params.hostgroup_rights).toHaveLength(2);
expect(createCall[1].body.params.hostgroup_rights).toContainEqual({ id: 201, permission: Permission.Read });
expect(createCall[1].body.params.hostgroup_rights).toContainEqual({ id: 202, permission: Permission.Read });
});
});

View file

@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: { zabbixAPI: {
post: jest.fn(), post: jest.fn(),
getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(), executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix', baseURL: 'http://localhost/zabbix',
getLocations: jest.fn(), getLocations: jest.fn(),