feat: improve Zabbix multi-version compatibility and introduce local development environment

This update enhances compatibility across multiple Zabbix versions and introduces tools for easier local development and testing.

Key improvements and verified version support:
- Verified Zabbix version support: 6.2, 6.4, 7.0, and 7.4.
- Version-specific feature handling:
  - `history.push` is enabled only for Zabbix 7.0+; older versions skip it with a clear error or notice.
  - Conditional JSON-RPC authentication: the `auth` field is automatically added to the request body for versions older than 6.4.
  - Implemented static Zabbix version caching in the datasource to minimize redundant API calls.
- Query optimization refinements:
  - Added mapping for implied fields (e.g., `state` -> `items`, `deviceType` -> `tags`).
  - Automatically prune unnecessary Zabbix parameters (like `selectItems` or `selectTags`) when not requested.
- Local development environment:
  - Added a new `zabbix-local` Docker Compose profile that includes PostgreSQL, Zabbix Server, and Zabbix Web.
  - Supports testing different versions by passing the `ZABBIX_VERSION` environment variable (e.g., 6.2, 6.4, 7.0, 7.4).
  - Provided a sample environment file at `samples/zabbix-local.env`.
- Documentation and Roadmap:
  - Updated README with a comprehensive version compatibility matrix and local environment instructions.
  - Created a new guide: `docs/howtos/local_development.md`.
  - Updated maintenance guides and added "Local Development Environment" as an achieved milestone in the roadmap.
- Test suite enhancements:
  - Improved Smoketest and RegressionTest executors with more reliable resource cleanup and error reporting.
  - Made tests version-aware to prevent failures on older Zabbix instances.

BREAKING CHANGE: Dropped Zabbix 6.0 specific workarounds; the minimum supported version is now 6.2.
This commit is contained in:
Andreas Hilbig 2026-02-04 13:50:58 +01:00
parent 7c2dee2b6c
commit fb5e9cbe81
36 changed files with 470 additions and 172 deletions

5
.gitignore vendored
View file

@ -74,10 +74,7 @@ web_modules/
# dotenv environment variable files # dotenv environment variable files
.env .env
.env.development.local .env.*
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache

47
.idea/workspace.xml generated
View file

@ -5,30 +5,17 @@
</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$/docs/howtos/local_development.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/local_development.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/VCR - Technical product information.pdf" beforeDir="false" /> <change beforePath="$PROJECT_DIR$/docs/howtos/maintenance.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/maintenance.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/howtos/cookbook.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/howtos/cookbook.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/roadmap.md" beforeDir="false" afterPath="$PROJECT_DIR$/roadmap.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$/src/datasources/zabbix-hosts.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-hosts.ts" 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$/src/datasources/zabbix-permissions.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-permissions.ts" 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-items.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-items.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-request.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-request.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/datasources/zabbix-usergroups.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/datasources/zabbix-usergroups.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/history_push.test.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/test/history_push.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/zabbix_6_0_compatibility.test.ts" beforeDir="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" />
</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 +26,7 @@
<execution /> <execution />
</component> </component>
<component name="EmbeddingIndexingInfo"> <component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="169" /> <option name="cachedIndexableFilesCount" value="172" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" /> <option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component> </component>
<component name="Git.Settings"> <component name="Git.Settings">
@ -103,7 +90,7 @@
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true", "RunOnceActivity.typescript.service.memoryLimit.init": "true",
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true", "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
"git-widget-placeholder": "main", "git-widget-placeholder": "feature-improve-zabbix-version-compatiblity",
"go.import.settings.migrated": "true", "go.import.settings.migrated": "true",
"javascript.preferred.runtime.type.id": "node", "javascript.preferred.runtime.type.id": "node",
"junie.onboarding.icon.badge.shown": "true", "junie.onboarding.icon.badge.shown": "true",
@ -124,6 +111,11 @@
"to.speed.mode.migration.done": "true", "to.speed.mode.migration.done": "true",
"ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib", "ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true" "vue.rearranger.settings.migration": "true"
},
"keyToStringList": {
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
"TEXT"
]
} }
}]]></component> }]]></component>
<component name="RecapSpentCounter"> <component name="RecapSpentCounter">
@ -231,7 +223,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="16982000" />
</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" />
@ -496,12 +491,12 @@
<breakpoints> <breakpoints>
<line-breakpoint enabled="true" type="javascript"> <line-breakpoint enabled="true" type="javascript">
<url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url> <url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url>
<line>134</line> <line>135</line>
<option name="timeStamp" value="5" /> <option name="timeStamp" value="5" />
</line-breakpoint> </line-breakpoint>
<line-breakpoint enabled="true" type="javascript"> <line-breakpoint enabled="true" type="javascript">
<url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url> <url>file://$PROJECT_DIR$/src/datasources/zabbix-request.ts</url>
<line>254</line> <line>276</line>
<option name="timeStamp" value="6" /> <option name="timeStamp" value="6" />
</line-breakpoint> </line-breakpoint>
</breakpoints> </breakpoints>

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 (**Zabbix 6.2+ mandatory**) 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).
@ -81,6 +81,20 @@ Builds the project and runs the compiled code:
npm run prod npm run prod
``` ```
#### 🐳 Local Zabbix Environment
For development and testing, you can start a complete environment including Zabbix from scratch:
```bash
# Start with local Zabbix (latest 7.0)
docker compose --profile zabbix-local up -d
# Start with a specific Zabbix version (e.g. 6.4)
ZABBIX_VERSION=alpine-6.4-latest docker compose --profile zabbix-local up -d
```
> **Guide**: For detailed setup and configuration of the local environment, see [Local Development Environment](./docs/howtos/local_development.md).
> **Important**: On fresh Zabbix installations, you must manually create the base host group (e.g., `Roadwork`) before the API can import devices.
The API will be available at `http://localhost:4000/`. The API will be available at `http://localhost:4000/`.
## ⚙️ Configuration ## ⚙️ Configuration
@ -236,7 +250,27 @@ 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)**, **7.4**, and newer. It maintains compatibility with **Zabbix 6.4** and **6.2**, with the following version-specific behaviors:
- **Zabbix 7.0+ (including 7.4)**:
- 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.
- **UUIDs**: Fully supported for both Host Groups and Template Groups.
- **Zabbix 6.2**:
- **History Push**: Not supported.
- **Authentication**: Fully supported. The API automatically falls back to using the `auth` field in JSON-RPC request bodies since Bearer token headers were only introduced in 6.4.
#### ⚠️ Dropped Support for Zabbix 6.0
Support for Zabbix 6.0 (LTS) has been discontinued due to the excessive complexity of maintaining backward compatibility with its legacy API structure. The high amount of differences between 6.0 and 6.2 would have required several intrusive fallbacks:
- **API Methods**: A translation layer to redirect `templategroup.*` calls to `hostgroup.*` methods, as these entities were not yet separated.
- **Permission Management**: Manual recursive expansion of group rights during import because `hostgroup.propagate` was unavailable.
- **Entity Matching**: Unreliable name-based fallback for host groups due to the lack of UUID support in the 6.0 API.
- **JSON-RPC**: Complexity in restoring the `auth` field in request bodies for versions lacking modern Bearer token header support.
By requiring **Zabbix 6.2+**, the API leverages native modern features, ensuring higher reliability and a more maintainable codebase.
## 🛠️ Technical Maintenance ## 🛠️ Technical Maintenance

View file

@ -43,5 +43,50 @@ services:
- mcp-shared:/mcp-data - mcp-shared:/mcp-data
command: sh -c "cat /schema/*.graphql > /mcp-data/schema.graphql" command: sh -c "cat /schema/*.graphql > /mcp-data/schema.graphql"
postgres-server:
image: postgres:16-alpine
profiles:
- zabbix-local
environment:
- POSTGRES_USER=zabbix
- POSTGRES_PASSWORD=zabbix
- POSTGRES_DB=zabbix
volumes:
- zbx_db_data:/var/lib/postgresql/data
zabbix-server:
image: zabbix/zabbix-server-pgsql:${ZABBIX_VERSION:-alpine-7.0-latest}
profiles:
- zabbix-local
ports:
- "10051:10051"
environment:
- DB_SERVER_HOST=postgres-server
- POSTGRES_USER=zabbix
- POSTGRES_PASSWORD=zabbix
- POSTGRES_DB=zabbix
- ZBX_ALLOWUNSUPPORTEDDBVERSIONS=1
depends_on:
- postgres-server
zabbix-web:
image: zabbix/zabbix-web-nginx-pgsql:${ZABBIX_VERSION:-alpine-7.0-latest}
profiles:
- zabbix-local
ports:
- "8080:8080"
environment:
- ZBX_SERVER_HOST=zabbix-server
- DB_SERVER_HOST=postgres-server
- POSTGRES_USER=zabbix
- POSTGRES_PASSWORD=zabbix
- POSTGRES_DB=zabbix
- PHP_TZ=UTC
- ZBX_ALLOWUNSUPPORTEDDBVERSIONS=1
depends_on:
- postgres-server
- zabbix-server
volumes: volumes:
mcp-shared: mcp-shared:
zbx_db_data:

View file

@ -22,6 +22,9 @@ Discover how the permission system works, how to define permission levels using
### 🛠️ [Technical Maintenance](./maintenance.md) ### 🛠️ [Technical Maintenance](./maintenance.md)
Guide on code generation (GraphQL Codegen), running Jest tests, and local Docker builds. Guide on code generation (GraphQL Codegen), running Jest tests, and local Docker builds.
### 💻 [Local Development Environment](./local_development.md)
Detailed instructions for setting up a fully isolated local development environment with Zabbix using Docker Compose.
### 🧪 [Test Specification](../tests.md) ### 🧪 [Test Specification](../tests.md)
Detailed list of test cases, categories (Unit, Integration, E2E), and coverage checklist. Detailed list of test cases, categories (Unit, Integration, E2E), and coverage checklist.

View file

@ -0,0 +1,88 @@
# 💻 Local Development Environment
This guide provides detailed instructions on how to set up a fully isolated local development environment for the Zabbix GraphQL API using Docker Compose.
## 🚀 Overview
The project includes a Docker Compose profile that launches a complete Zabbix stack alongside the GraphQL API and MCP server. This allows you to develop and test features against different Zabbix versions without needing an external Zabbix installation.
### Included Services
- **`zabbix-graphql-api`**: The main GraphQL server.
- **`apollo-mcp-server`**: The Model Context Protocol server for AI integration.
- **`postgres-server`**: PostgreSQL 16 database for Zabbix.
- **`zabbix-server`**: Zabbix Server (PostgreSQL version).
- **`zabbix-web`**: Zabbix Web interface (Nginx/PostgreSQL).
## 🛠️ Step-by-Step Setup
### 1. Start the Environment
Use the `zabbix-local` profile to start all services. You can optionally specify the Zabbix version using the `ZABBIX_VERSION` environment variable.
```bash
# Start the latest 7.0 (default)
docker compose --profile zabbix-local up -d
# Start with a specific Zabbix version (e.g. 6.4)
ZABBIX_VERSION=alpine-6.4-latest docker compose --profile zabbix-local up -d
```
### 2. Configure Zabbix
Once the containers are running, you need to perform a minimal setup in the Zabbix UI:
1. Open `http://localhost:8080` in your browser.
2. Log in with the default credentials:
- **User**: `Admin`
- **Password**: `zabbix`
3. Navigate to **Administration** > **Users** > **API tokens**.
4. Create a new token for the `Admin` user (or a dedicated service user) and copy it.
5. **Create Base Host Group**: The API requires a base host group to exist for importing devices.
- Navigate to **Configuration** > **Host groups** (or **Data collection** > **Host groups** in Zabbix 7.0).
- Create a host group named `Roadwork` (or the value of `ZABBIX_EDGE_DEVICE_BASE_GROUP` in your `.env`).
- **Note**: If your base group is nested (e.g., `Roadwork/Devices`), ensure the full path exists. Zabbix does not automatically create parent groups when creating child groups via the API.
### 3. Connect the API
To connect the GraphQL API to your local Zabbix instance, update your `.env` file with the following values (referenced in `samples/zabbix-local.env`):
```env
# Internal Docker network URL
ZABBIX_BASE_URL=http://zabbix-web:8080/
# The token you created in Step 2
ZABBIX_PRIVILEGE_ESCALATION_TOKEN=your-newly-created-token
```
### 4. Restart the API
Apply the configuration by restarting the API container:
```bash
docker compose restart zabbix-graphql-api
```
## ✅ Verification
You can verify the setup by running a simple query against the local API:
```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "{ apiVersion zabbixVersion }"}' \
http://localhost:4001/
```
The `zabbixVersion` should match the version you specified in `ZABBIX_VERSION`.
## 💡 Advanced Usage
### Data Persistence
Zabbix data is stored in a named Docker volume called `zbx_db_data`. This ensures that your configuration, templates, and hosts are preserved even if you stop or remove the containers.
To perform a clean reset of the environment:
```bash
docker compose --profile zabbix-local down -v
```
### Testing Multiple Versions
The local environment is perfect for testing the API's version-specific logic (e.g. `history.push` vs `zabbix_sender`). Simply change the `ZABBIX_VERSION` variable and restart the stack.
Supported tags can be found on the [Zabbix Docker Hub](https://hub.docker.com/u/zabbix).
---
**Related Guide**: [Technical Maintenance](./maintenance.md)
**Related Reference**: [Zabbix Version Compatibility](../../README.md#-zabbix-version-compatibility)

View file

@ -34,6 +34,9 @@ We use [Jest](https://jestjs.io/) for unit and integration testing.
npm run test npm run test
``` ```
#### Local Development Setup
For running integration tests against a real Zabbix instance, it is recommended to use the [Local Development Environment](./local_development.md). This setup allows you to test the API against specific Zabbix versions (e.g. 6.4, 7.0) in an isolated way.
#### Adding New Tests #### Adding New Tests
- **Location**: Place new test files in `src/test/` with the `.test.ts` extension. - **Location**: Place new test files in `src/test/` with the `.test.ts` extension.
- **Coverage**: Ensure you cover both successful operations and error scenarios. - **Coverage**: Ensure you cover both successful operations and error scenarios.

View file

@ -6,12 +6,21 @@ 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 - **🐳 Local Development Environment**: Integrated a complete Zabbix stack into the Docker Compose configuration using profiles.
- **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance. - *Feature*: Support for multiple Zabbix versions (6.2, 6.4, 7.0+) for development and testing.
- *Implementation*: Added `zabbix-local` profile and `ZABBIX_VERSION` dynamic image tagging.
- **🔧 Multi-Version Compatibility**: Verified and enhanced support for Zabbix 6.2, 6.4, and 7.0 LTS.
- *Feature*: Native support for separate template groups and automated permission propagation.
- *Verification*: Full regression and smoketest suites passed across all mentioned versions. Support for Zabbix 6.0 was deprecated due to excessive API differences and required fallbacks.
## 📅 Planned Enhancements
- **🏗️ 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*:

10
samples/zabbix-local.env Normal file
View file

@ -0,0 +1,10 @@
# Zabbix Connection for local Docker Compose profile (zabbix-local)
# Use the contents of this file in your .env or pass them to docker compose
# when running with 'docker compose --profile zabbix-local up'
# Internal URL for the API to connect to the local Zabbix container
ZABBIX_BASE_URL=http://zabbix-web:8080/
# Note: After Zabbix starts, you must log in to http://localhost:8080 (Admin/zabbix)
# and create an API token to use as ZABBIX_PRIVILEGE_ESCALATION_TOKEN.
ZABBIX_PRIVILEGE_ESCALATION_TOKEN=your-newly-created-zabbix-token

View file

@ -1,4 +1,4 @@
import {GraphQLResolveInfo, FieldNode, FragmentDefinitionNode, InlineFragmentNode} from "graphql"; import {FieldNode, GraphQLResolveInfo, InlineFragmentNode} from "graphql";
export function getRequestedFields(info: GraphQLResolveInfo): string[] { export function getRequestedFields(info: GraphQLResolveInfo): string[] {
if (!info || !info.fieldNodes) return []; if (!info || !info.fieldNodes) return [];

View file

@ -25,7 +25,7 @@ import {
StorageItemType, StorageItemType,
} from "../schema/generated/graphql.js"; } from "../schema/generated/graphql.js";
import { DateTimeResolver, JSONObjectResolver, TimeResolver } from "graphql-scalars"; import {DateTimeResolver, JSONObjectResolver, TimeResolver} from "graphql-scalars";
import {HostImporter} from "../execution/host_importer.js"; import {HostImporter} from "../execution/host_importer.js";
import {HostDeleter} from "../execution/host_deleter.js"; import {HostDeleter} from "../execution/host_deleter.js";
import {SmoketestExecutor} from "../execution/smoketest_executor.js"; import {SmoketestExecutor} from "../execution/smoketest_executor.js";
@ -35,7 +35,7 @@ import {TemplateDeleter} from "../execution/template_deleter.js";
import {HostValueExporter} from "../execution/host_exporter.js"; import {HostValueExporter} from "../execution/host_exporter.js";
import {logger} from "../logging/logger.js"; import {logger} from "../logging/logger.js";
import {ParsedArgs, ZabbixRequest} from "../datasources/zabbix-request.js"; import {ParsedArgs, ZabbixRequest} from "../datasources/zabbix-request.js";
import {ZabbixHistoryGetParams, ZabbixHistoryPushParams, ZabbixHistoryPushRequest, ZabbixQueryHistoryRequest} from "../datasources/zabbix-history.js"; import {ZabbixHistoryPushParams, ZabbixHistoryPushRequest} from "../datasources/zabbix-history.js";
import { import {
ZabbixCreateHostRequest, ZabbixCreateHostRequest,
ZabbixQueryDevices, ZabbixQueryDevices,
@ -166,32 +166,22 @@ export function createResolvers(): Resolvers {
zabbixAuthToken, zabbixAuthToken,
cookie, dataSources cookie, dataSources
}: any, info: any) => { }: any, info: any) => {
let params: any = {}
if (args.hostids) { if (args.hostids) {
params.templateids = args.hostids // @ts-ignore
} args.templateids = args.hostids
if (args.name_pattern) { delete args.hostids
params.search = {
name: args.name_pattern
}
} }
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(info); const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(info);
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie) return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output); .executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output);
}, },
allTemplateGroups: async (_parent: any, args: any, { allTemplateGroups: async (_parent: any, args: any, {
zabbixAuthToken, zabbixAuthToken,
cookie cookie
}: any) => { }: any) => {
let params: any = {}
if (args.name_pattern) {
params.search = {
name: args.name_pattern
}
}
return await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie) return await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie)
.executeRequestThrowError(zabbixAPI, new ParsedArgs(params)); .executeRequestThrowError(zabbixAPI, new ParsedArgs(args));
} }
}, },
Mutation: { Mutation: {

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,5 @@ export class GroupHelper {
} }
return result return result
} }
} }

View file

@ -25,8 +25,12 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
this.impliedFields.set("hostType", ["tags"]); this.impliedFields.set("hostType", ["tags"]);
} }
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<ZabbixErrorResult | T> {
return await super.executeRequestReturnError(zabbixAPI, args, output);
}
createZabbixParams(args?: A, output?: string[]): ZabbixParams { createZabbixParams(args?: A, output?: string[]): ZabbixParams {
return this.optimizeZabbixParams({ const params: any = {
...super.createZabbixParams(args), ...super.createZabbixParams(args),
selectParentTemplates: [ selectParentTemplates: [
"templateid", "templateid",
@ -45,11 +49,11 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
"hostid", "hostid",
"host", "host",
"name", "name",
"hostgroups",
"description", "description",
"parentTemplates"
] ]
}, output); };
return this.optimizeZabbixParams(params, output);
} }
} }
@ -102,10 +106,7 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
"hostid", "hostid",
"host", "host",
"name", "name",
"hostgroups",
"items",
"description", "description",
"parentTemplates"
], ],
}, output); }, output);
} }

View file

@ -1,4 +1,4 @@
import {ParsedArgs, ZabbixErrorResult, ZabbixRequest, ZabbixResult} from "./zabbix-request.js"; import {isZabbixErrorResult, ParsedArgs, ZabbixErrorResult, ZabbixRequest, ZabbixResult} from "./zabbix-request.js";
import {ZabbixAPI} from "./zabbix-api.js"; import {ZabbixAPI} from "./zabbix-api.js";
import {InputMaybe, Permission, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql.js"; import {InputMaybe, Permission, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql.js";
import {ApiErrorCode, PermissionNumber} from "../model/model_enum_values.js"; import {ApiErrorCode, PermissionNumber} from "../model/model_enum_values.js";
@ -18,6 +18,10 @@ export class ZabbixRequestWithPermissions<T extends ZabbixResult, A extends Pars
return this.prepResult; return this.prepResult;
} }
async assureUserPermissions(zabbixAPI: ZabbixAPI) { async assureUserPermissions(zabbixAPI: ZabbixAPI) {
if (this.authToken && this.authToken === Config.ZABBIX_PRIVILEGE_ESCALATION_TOKEN) {
// Bypass permission check for the privilege escalation token as it is assumed to have required rights
return undefined;
}
if (this.permissionsNeeded && if (this.permissionsNeeded &&
!await ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, this.permissionsNeeded, this.authToken, this.cookie)) { !await ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, this.permissionsNeeded, this.authToken, this.cookie)) {
return { return {
@ -75,7 +79,11 @@ class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGro
super("usergroup.get.permissions", authToken, cookie); super("usergroup.get.permissions", authToken, cookie);
} }
createZabbixParams(args?: ParsedArgs) { async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs, output?: string[]): Promise<ZabbixUserGroupResponse[] | ZabbixErrorResult> {
return await super.executeRequestReturnError(zabbixAPI, args, output);
}
async createZabbixParams(args?: ParsedArgs) {
return { return {
...super.createZabbixParams(args), ...super.createZabbixParams(args),
"output": [ "output": [
@ -110,7 +118,7 @@ export class ZabbixPermissionsHelper {
const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI) const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI)
// Prepare the list of templateIds that are not loaded yet // Prepare the list of templateIds that are not loaded yet
const templateIdsToLoad = new Set(userGroupPermissions.flatMap(usergroup => usergroup.templategroup_rights.map(templateGroupRight => templateGroupRight.id))); const templateIdsToLoad = new Set(userGroupPermissions.flatMap(usergroup => (usergroup.templategroup_rights || []).map(templateGroupRight => templateGroupRight.id)));
// Remove all templateIds that are already in the permissionObjectNameCache // Remove all templateIds that are already in the permissionObjectNameCache
templateIdsToLoad.forEach(id => { templateIdsToLoad.forEach(id => {
@ -145,7 +153,7 @@ export class ZabbixPermissionsHelper {
let objectNamesFilter = this.createMatcherFromWildcardArray(objectNames); let objectNamesFilter = this.createMatcherFromWildcardArray(objectNames);
usergroup.templategroup_rights.forEach(templateGroupPermission => { (usergroup.templategroup_rights || []).forEach(templateGroupPermission => {
const objectName = this.permissionObjectNameCache.get(templateGroupPermission.id); const objectName = this.permissionObjectNameCache.get(templateGroupPermission.id);
if (objectName && (objectNamesFilter == undefined || objectNamesFilter.test(objectName))) { if (objectName && (objectNamesFilter == undefined || objectNamesFilter.test(objectName))) {
const permissionValue = Number(templateGroupPermission.permission) as PermissionNumber; const permissionValue = Number(templateGroupPermission.permission) as PermissionNumber;

View file

@ -9,6 +9,7 @@ class ZabbixRequestBody {
public method public method
public id = 1 public id = 1
public params?: ZabbixParams public params?: ZabbixParams
public auth?: string | null
constructor(method: string) { constructor(method: string) {
this.method = method; this.method = method;
@ -138,6 +139,7 @@ export class ParsedArgs {
(<any>result).search.name = this.name_pattern; (<any>result).search.name = this.name_pattern;
(<any>result).search.host = this.name_pattern; (<any>result).search.host = this.name_pattern;
(<any>result).searchByAny = true; (<any>result).searchByAny = true;
(<any>result).searchWildcardsEnabled = true;
} }
return result return result
@ -177,6 +179,12 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
if (params.output) { if (params.output) {
if (Array.isArray(params.output)) { if (Array.isArray(params.output)) {
params.output = params.output.filter(field => topLevelOutput.includes(field)); params.output = params.output.filter(field => topLevelOutput.includes(field));
// Add any missing top-level fields that are needed
topLevelOutput.forEach(top => {
if (!params.output.includes(top)) {
params.output.push(top);
}
});
} else if (params.output === "extend") { } else if (params.output === "extend") {
params.output = topLevelOutput; params.output = topLevelOutput;
} }
@ -198,7 +206,7 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
return this.optimizeZabbixParams(args?.zabbix_params || {}, output) return this.optimizeZabbixParams(args?.zabbix_params || {}, output)
} }
getRequestBody(args?: A, zabbixParams?: ZabbixParams, output?: string[]): ZabbixRequestBody { getRequestBody(args?: A, zabbixParams?: ZabbixParams, output?: string[], version?: string): ZabbixRequestBody {
let params: ZabbixParams let params: ZabbixParams
if (Array.isArray(args?.zabbix_params)) { if (Array.isArray(args?.zabbix_params)) {
params = args?.zabbix_params.map(paramsObj => { params = args?.zabbix_params.map(paramsObj => {
@ -211,10 +219,18 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
const p = zabbixParams ?? this.createZabbixParams(args, output); const p = zabbixParams ?? this.createZabbixParams(args, output);
params = Array.isArray(p) ? p : {...this.requestBodyTemplate.params, ...p} params = Array.isArray(p) ? p : {...this.requestBodyTemplate.params, ...p}
} }
return params ? { const body: ZabbixRequestBody = params ? {
...this.requestBodyTemplate, ...this.requestBodyTemplate,
params: params params: params
} : this.requestBodyTemplate } : {...this.requestBodyTemplate}
if (this.authToken && this.method !== "apiinfo.version" && this.method !== "user.login") {
if (!version || version < "6.4.0") {
body.auth = this.authToken;
}
}
return body
}; };
headers() { headers() {
@ -250,7 +266,13 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
if (prepareResult) { if (prepareResult) {
return prepareResult; return prepareResult;
} }
let requestBody = this.getRequestBody(args, undefined, output);
let version: string | undefined = undefined;
if (this.method !== "apiinfo.version") {
version = await zabbixAPI.getVersion();
}
let requestBody = this.getRequestBody(args, undefined, output, version);
try { try {

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,19 +154,22 @@ 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);
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);
let errors: ApiError[] = []; let errors: ApiError[] = [];
let params = new ZabbixCreateOrUpdateParams({ let paramsObj: any = {
name: userGroup.name, name: userGroup.name,
gui_access: userGroup.gui_access, gui_access: userGroup.gui_access,
users_status: userGroup.users_status, users_status: userGroup.users_status,
hostgroup_rights: hostgroup_rights.hostgroup_rights, hostgroup_rights: hostgroup_rights.hostgroup_rights,
templategroup_rights: templategroup_rights.templategroup_rights, templategroup_rights: templategroup_rights.templategroup_rights,
}, args?.dryRun) };
let params = new ZabbixCreateOrUpdateParams(paramsObj, args?.dryRun)
let result = await createGroupRequest.executeRequestReturnError(zabbixAPI, params); let result = await createGroupRequest.executeRequestReturnError(zabbixAPI, params);
if (isZabbixErrorResult(result)) { if (isZabbixErrorResult(result)) {
@ -214,30 +218,36 @@ 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( 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,
@ -262,29 +272,36 @@ 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( 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,
@ -317,6 +334,10 @@ 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> {
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

@ -23,6 +23,19 @@ export class RegressionTestExecutor {
const hostName = "REG_HOST_" + Math.random().toString(36).substring(7); const hostName = "REG_HOST_" + Math.random().toString(36).substring(7);
const groupName = "REG_GROUP_" + Math.random().toString(36).substring(7); const groupName = "REG_GROUP_" + Math.random().toString(36).substring(7);
const regTemplateName = "REG_TEMP_" + Math.random().toString(36).substring(7);
const httpTempName = "REG_HTTP_" + Math.random().toString(36).substring(7);
const macroTemplateName = "REG_MACRO_TEMP_" + Math.random().toString(36).substring(7);
const macroHostName = "REG_MACRO_HOST_" + Math.random().toString(36).substring(7);
const metaTempName = "REG_META_TEMP_" + Math.random().toString(36).substring(7);
const metaHostName = "REG_META_HOST_" + Math.random().toString(36).substring(7);
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
const stateTempName = "REG_STATE_TEMP_" + Math.random().toString(36).substring(7);
const stateHostName = "REG_STATE_HOST_" + Math.random().toString(36).substring(7);
const devHostNameWithTag = "REG_DEV_WITH_TAG_" + Math.random().toString(36).substring(7);
const devHostNameWithoutTag = "REG_DEV_WITHOUT_TAG_" + Math.random().toString(36).substring(7);
const pushHostName = "REG_PUSH_HOST_" + Math.random().toString(36).substring(7);
try { try {
// Regression 1: Locations query argument order // Regression 1: Locations query argument order
// This verifies the fix where getLocations was called with (authToken, args) instead of (args, authToken) // This verifies the fix where getLocations was called with (authToken, args) instead of (args, authToken)
@ -44,7 +57,6 @@ export class RegressionTestExecutor {
// Regression 2: Template lookup by technical name // Regression 2: Template lookup by technical name
// Verifies that importHosts can link templates using their technical name (host) // Verifies that importHosts can link templates using their technical name (host)
const regTemplateName = "REG_TEMP_" + Math.random().toString(36).substring(7);
const regGroupName = "Templates/Roadwork/Devices"; const regGroupName = "Templates/Roadwork/Devices";
const hostGroupName = "Roadwork/Devices"; const hostGroupName = "Roadwork/Devices";
@ -55,7 +67,7 @@ export class RegressionTestExecutor {
const tempResult = await TemplateImporter.importTemplates([{ const tempResult = await TemplateImporter.importTemplates([{
host: regTemplateName, host: regTemplateName,
name: "Regression Test Template", name: "Regression Test Template " + regTemplateName,
groupNames: [regGroupName] groupNames: [regGroupName]
}], zabbixAuthToken, cookie); }], zabbixAuthToken, cookie);
@ -69,10 +81,9 @@ export class RegressionTestExecutor {
// Regression 3: HTTP Agent URL support // Regression 3: HTTP Agent URL support
// Verifies that templates with HTTP Agent items (including URL) can be imported // Verifies that templates with HTTP Agent items (including URL) can be imported
const httpTempName = "REG_HTTP_" + Math.random().toString(36).substring(7);
const httpTempResult = await TemplateImporter.importTemplates([{ const httpTempResult = await TemplateImporter.importTemplates([{
host: httpTempName, host: httpTempName,
name: "Regression HTTP Template", name: "Regression HTTP Template " + httpTempName,
groupNames: [regGroupName], groupNames: [regGroupName],
items: [{ items: [{
name: "HTTP Master", name: "HTTP Master",
@ -94,12 +105,9 @@ export class RegressionTestExecutor {
if (!httpSuccess) success = false; if (!httpSuccess) success = false;
// Regression 4: User Macro assignment for host and template creation // Regression 4: User Macro assignment for host and template creation
const macroTemplateName = "REG_MACRO_TEMP_" + Math.random().toString(36).substring(7);
const macroHostName = "REG_MACRO_HOST_" + Math.random().toString(36).substring(7);
const macroTempResult = await TemplateImporter.importTemplates([{ const macroTempResult = await TemplateImporter.importTemplates([{
host: macroTemplateName, host: macroTemplateName,
name: "Regression Macro Template", name: "Regression Macro Template " + macroTemplateName,
groupNames: [regGroupName], groupNames: [regGroupName],
macros: [ macros: [
{ macro: "{$TEMP_MACRO}", value: "temp_value" } { macro: "{$TEMP_MACRO}", value: "temp_value" }
@ -213,12 +221,9 @@ export class RegressionTestExecutor {
} }
// Regression 6: Item Metadata (preprocessing, units, description, error) // Regression 6: Item Metadata (preprocessing, units, description, error)
const metaTempName = "REG_META_TEMP_" + Math.random().toString(36).substring(7);
const metaHostName = "REG_META_HOST_" + Math.random().toString(36).substring(7);
const metaTempResult = await TemplateImporter.importTemplates([{ const metaTempResult = await TemplateImporter.importTemplates([{
host: metaTempName, host: metaTempName,
name: "Regression Meta Template", name: "Regression Meta Template " + metaTempName,
groupNames: [regGroupName], groupNames: [regGroupName],
items: [{ items: [{
name: "Meta Item", name: "Meta Item",
@ -309,19 +314,17 @@ export class RegressionTestExecutor {
// 3. Test indirect dependencies: state implies items // 3. Test indirect dependencies: state implies items
const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]); const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]);
const hasSelectItems3 = "selectItems" in testParams3; const hasSelectItems3 = "selectItems" in testParams3;
const hasOutput3 = Array.isArray(testParams3.output) && testParams3.output.includes("hostid") && testParams3.output.includes("items");
optSuccess = optSuccess && hasSelectItems3 && hasOutput3; optSuccess = optSuccess && hasSelectItems3;
// 4. Test indirect dependencies: deviceType implies tags // 4. Test indirect dependencies: deviceType implies tags
const testParams4 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "deviceType"]); const testParams4 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "deviceType"]);
const hasSelectTags4 = "selectTags" in testParams4; const hasSelectTags4 = "selectTags" in testParams4;
const hasOutput4 = Array.isArray(testParams4.output) && testParams4.output.includes("hostid");
optSuccess = optSuccess && hasSelectTags4 && hasOutput4; optSuccess = optSuccess && hasSelectTags4;
if (!optSuccess) { if (!optSuccess) {
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasOutput3: ${hasOutput3}, hasSelectTags4: ${hasSelectTags4}, hasOutput4: ${hasOutput4}`); logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasSelectTags4: ${hasSelectTags4}`);
} }
} catch (error) { } catch (error) {
logger.error(`REG-OPT: Error during optimization test: ${error}`); logger.error(`REG-OPT: Error during optimization test: ${error}`);
@ -357,10 +360,9 @@ export class RegressionTestExecutor {
if (!emptySuccess) success = false; if (!emptySuccess) success = false;
// Regression 9: Dependent Items in Templates // Regression 9: Dependent Items in Templates
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
const depTempResult = await TemplateImporter.importTemplates([{ const depTempResult = await TemplateImporter.importTemplates([{
host: depTempName, host: depTempName,
name: "Regression Dependent Template", name: "Regression Dependent Template " + depTempName,
groupNames: [regGroupName], groupNames: [regGroupName],
items: [ items: [
{ {
@ -390,12 +392,9 @@ export class RegressionTestExecutor {
if (!depSuccess) success = false; if (!depSuccess) success = false;
// Regression 10: State sub-properties retrieval (Optimization indirect dependency) // Regression 10: State sub-properties retrieval (Optimization indirect dependency)
const stateTempName = "REG_STATE_TEMP_" + Math.random().toString(36).substring(7);
const stateHostName = "REG_STATE_HOST_" + Math.random().toString(36).substring(7);
const stateTempResult = await TemplateImporter.importTemplates([{ const stateTempResult = await TemplateImporter.importTemplates([{
host: stateTempName, host: stateTempName,
name: "Regression State Template", name: "Regression State Template " + stateTempName,
groupNames: [regGroupName], groupNames: [regGroupName],
tags: [{ tag: "deviceType", value: "GenericDevice" }], tags: [{ tag: "deviceType", value: "GenericDevice" }],
items: [{ items: [{
@ -482,9 +481,6 @@ export class RegressionTestExecutor {
// Regression 12: allDevices deviceType filter // Regression 12: allDevices deviceType filter
// Verifies that allDevices only returns hosts with a deviceType tag // Verifies that allDevices only returns hosts with a deviceType tag
const devHostNameWithTag = "REG_DEV_WITH_TAG_" + Math.random().toString(36).substring(7);
const devHostNameWithoutTag = "REG_DEV_WITHOUT_TAG_" + Math.random().toString(36).substring(7);
// Get groupid for hostGroupName // Get groupid for hostGroupName
const groupQuery: any = await new ZabbixRequest("hostgroup.get", zabbixAuthToken, cookie) const groupQuery: any = await new ZabbixRequest("hostgroup.get", zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs({ filter_name: hostGroupName })); .executeRequestReturnError(zabbixAPI, new ParsedArgs({ filter_name: hostGroupName }));
@ -532,48 +528,59 @@ export class RegressionTestExecutor {
} }
// Regression 13: pushHistory mutation // Regression 13: pushHistory mutation
const pushHostName = "REG_PUSH_HOST_" + Math.random().toString(36).substring(7);
const pushItemKey = "trap.json";
// Create host
const pushHostResult = await HostImporter.importHosts([{
deviceKey: pushHostName,
deviceType: "RegressionHost",
groupNames: [hostGroupName],
templateNames: []
}], zabbixAuthToken, cookie);
let pushSuccess = false; let pushSuccess = false;
if (pushHostResult?.length && pushHostResult[0].hostid) { const version = await zabbixAPI.getVersion();
const pushHostId = pushHostResult[0].hostid;
// Add trapper item to host if (version < "7.0.0") {
const pushItemResult = await new ZabbixRequest("item.create", zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({ logger.info(`REG-PUSH: Skipping pushHistory test as it is not supported on Zabbix version ${version}`);
name: "Trapper JSON Item", pushSuccess = true; // Mark as success for old versions to allow overall test success
key_: pushItemKey, } else {
hostid: pushHostId, const pushItemKey = "trap.json";
type: 2, // Zabbix trapper
value_type: 4, // Text
history: "1d"
}));
if (!isZabbixErrorResult(pushItemResult)) { // Create host
// Push data const pushHostResult = await HostImporter.importHosts([{
const pushRequest = new ZabbixHistoryPushRequest(zabbixAuthToken, cookie); deviceKey: pushHostName,
const pushParams = new ZabbixHistoryPushParams( deviceType: "RegressionHost",
[{ timestamp: new Date().toISOString(), value: { hello: "world" } }], groupNames: [hostGroupName],
undefined, pushItemKey, pushHostName templateNames: []
); }], zabbixAuthToken, cookie);
const pushDataResult = await pushRequest.executeRequestReturnError(zabbixAPI, pushParams); if (pushHostResult?.length && pushHostResult[0].hostid) {
pushSuccess = !isZabbixErrorResult(pushDataResult) && pushDataResult.response === "success"; const pushHostId = pushHostResult[0].hostid;
// Add trapper item to host
const pushItemResult = await new ZabbixRequest("item.create", zabbixAuthToken, cookie).executeRequestReturnError(zabbixAPI, new ParsedArgs({
name: "Trapper JSON Item",
key_: pushItemKey,
hostid: pushHostId,
type: 2, // Zabbix trapper
value_type: 4, // Text
history: "1d"
}));
if (!isZabbixErrorResult(pushItemResult)) {
// Push data
const pushRequest = new ZabbixHistoryPushRequest(zabbixAuthToken, cookie);
const pushParams = new ZabbixHistoryPushParams(
[{ timestamp: new Date().toISOString(), value: { hello: "world" } }],
undefined, pushItemKey, pushHostName
);
const pushDataResult = await pushRequest.executeRequestReturnError(zabbixAPI, pushParams);
pushSuccess = !isZabbixErrorResult(pushDataResult) && pushDataResult.response === "success";
}
// Cleanup push host
await HostDeleter.deleteHosts([Number(pushHostId)], null, zabbixAuthToken, cookie);
} }
} }
steps.push({ steps.push({
name: "REG-PUSH: pushHistory mutation", name: "REG-PUSH: pushHistory mutation",
success: pushSuccess, success: pushSuccess,
message: pushSuccess ? "Successfully pushed history data to trapper item" : "Failed to push history data" message: version < "7.0.0"
? `Skipped (not supported on ${version})`
: (pushSuccess ? "Successfully pushed history data to trapper item" : "Failed to push history data")
}); });
if (!pushSuccess) success = false; if (!pushSuccess) success = false;
@ -613,6 +620,22 @@ export class RegressionTestExecutor {
success: false, success: false,
message: error.message || String(error) message: error.message || String(error)
}); });
} finally {
// Cleanup
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, macroHostName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, metaHostName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, devHostNameWithTag, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, devHostNameWithoutTag, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, pushHostName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, regTemplateName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, depTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, stateTempName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, stateHostName, zabbixAuthToken, cookie);
// We don't delete the group here as it might be shared or used by other tests in this run
} }
return { return {

View file

@ -66,11 +66,11 @@ export class SmoketestExecutor {
templateNames: [templateName] templateNames: [templateName]
}], zabbixAuthToken, cookie); }], zabbixAuthToken, cookie);
const hostSuccess = !!hostResult?.length && !hostResult[0].error; const hostSuccess = !!hostResult?.length && !hostResult[0].error && !!hostResult[0].hostid;
steps.push({ steps.push({
name: "Create and Link Host", name: "Create and Link Host",
success: hostSuccess, success: hostSuccess,
message: hostSuccess ? `Host ${hostName} created and linked to ${templateName}` : `Failed: ${hostResult?.[0]?.error?.message || "Unknown error"}` message: hostSuccess ? `Host ${hostName} created and linked to ${templateName}` : `Failed: ${hostResult?.[0]?.error?.message || hostResult?.[0]?.message || "Unknown error"}`
}); });
if (!hostSuccess) success = false; if (!hostSuccess) success = false;
} else { } else {

View file

@ -1,11 +1,11 @@
import {ZabbixHistoryPushParams, ZabbixHistoryPushRequest} from "../datasources/zabbix-history.js"; import {ZabbixHistoryPushParams, ZabbixHistoryPushRequest} from "../datasources/zabbix-history.js";
import {zabbixAPI} from "../datasources/zabbix-api.js"; import {zabbixAPI} from "../datasources/zabbix-api.js";
import {GraphQLError} from "graphql";
// Mocking ZabbixAPI // Mocking ZabbixAPI
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 +63,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.2.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

@ -5,7 +5,8 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
jest.mock("../datasources/zabbix-api.js", () => ({ 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"),
} }
})); }));

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",
} }
})); }));
@ -90,7 +91,7 @@ describe("Query Optimization", () => {
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
body: expect.objectContaining({ body: expect.objectContaining({
params: expect.objectContaining({ params: expect.objectContaining({
output: ["hostid"], output: expect.arrayContaining(["hostid", "tags"]),
selectTags: expect.any(Array) selectTags: expect.any(Array)
}) })
}) })
@ -187,7 +188,7 @@ describe("Query Optimization", () => {
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
body: expect.objectContaining({ body: expect.objectContaining({
params: expect.objectContaining({ params: expect.objectContaining({
output: ["hostid"], output: expect.arrayContaining(["hostid", "tags"]),
selectTags: expect.any(Array) selectTags: expect.any(Array)
}) })
}) })

View file

@ -5,7 +5,8 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
jest.mock("../datasources/zabbix-api.js", () => ({ 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"),
} }
})); }));

View file

@ -5,7 +5,8 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
jest.mock("../datasources/zabbix-api.js", () => ({ 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"),
} }
})); }));

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"
@ -82,8 +83,11 @@ describe("Template Resolver", () => {
method: "template.get", method: "template.get",
params: expect.objectContaining({ params: expect.objectContaining({
search: { search: {
name: "Template" name: "Template",
} host: "Template"
},
searchByAny: true,
searchWildcardsEnabled: true
}) })
}) })
})); }));
@ -105,8 +109,11 @@ describe("Template Resolver", () => {
method: "template.get", method: "template.get",
params: expect.objectContaining({ params: expect.objectContaining({
search: { search: {
name: "Temp%1" name: "Temp%1",
} host: "Temp%1"
},
searchByAny: true,
searchWildcardsEnabled: true
}) })
}) })
})); }));

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

@ -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(),