feat: verify and enhance compatibility with Zabbix 6.0, 6.2, 6.4, and 7.0
This commit comprehensive updates the API to ensure full compatibility across all major Zabbix versions (6.0 LTS, 6.2, 6.4, and 7.0 LTS) and introduces a local development environment for multi-version testing.
Verified Zabbix Versions:
- Zabbix 7.0 (LTS): Full support, including native `history.push` for telemetry.
- Zabbix 6.4: Supported (excluding `history.push`); uses `hostgroup.propagate` and UUID-based matching.
- Zabbix 6.2: Supported (excluding `history.push`); uses `hostgroup.propagate` and `templategroup.*` methods.
- Zabbix 6.0 (LTS): Supported (excluding `history.push`); includes specific fallbacks:
- JSON-RPC: Restored `auth` field in request body for versions lacking Bearer token support.
- Permissions: Implemented name-based fallback for host/template groups (no UUIDs in 6.0).
- Group Expansion: Automatic manual expansion of group rights during import.
- API Methods: Fallback from `templategroup.*` to `hostgroup.*` methods.
Key Technical Changes:
- Local Development: Added `zabbix-local` Docker Compose profile to launch a complete Zabbix stack (Server, Web, Postgres) from scratch with dynamic versioning via `ZABBIX_VERSION`.
- Query Optimization: Refined dynamic field selection to handle implied fields and ensure consistent output regardless of initial Zabbix parameters.
- Documentation:
- New `local_development.md` HOWTO guide.
- Updated `README.md` with detailed version compatibility matrix.
- Expanded `roadmap.md` with achieved milestones.
- Testing:
- Updated entire Jest test suite (23 suites, 96 tests) to be version-aware and robust against naming collisions.
- Enhanced Smoketest and Regression Test executors with better cleanup and error reporting.
This commit is contained in:
parent
ec6ed422b1
commit
14a0df4c18
21 changed files with 397 additions and 111 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -74,10 +74,7 @@ web_modules/
|
|||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.*
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -81,6 +81,20 @@ Builds the project and runs the compiled code:
|
|||
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.0)
|
||||
ZABBIX_VERSION=alpine-6.0-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/`.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
|
|
|||
|
|
@ -43,5 +43,50 @@ services:
|
|||
- mcp-shared:/mcp-data
|
||||
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:
|
||||
mcp-shared:
|
||||
zbx_db_data:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ Discover how the permission system works, how to define permission levels using
|
|||
### 🛠️ [Technical Maintenance](./maintenance.md)
|
||||
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)
|
||||
Detailed list of test cases, categories (Unit, Integration, E2E), and coverage checklist.
|
||||
|
||||
|
|
|
|||
88
docs/howtos/local_development.md
Normal file
88
docs/howtos/local_development.md
Normal 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 Zabbix 6.0 (LTS)
|
||||
ZABBIX_VERSION=alpine-6.0-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)
|
||||
|
|
@ -34,6 +34,9 @@ We use [Jest](https://jestjs.io/) for unit and integration testing.
|
|||
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.0, 7.0) in an isolated way.
|
||||
|
||||
#### Adding New Tests
|
||||
- **Location**: Place new test files in `src/test/` with the `.test.ts` extension.
|
||||
- **Coverage**: Ensure you cover both successful operations and error scenarios.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ This document outlines the achieved milestones and planned enhancements for the
|
|||
- **🔓 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.
|
||||
|
||||
- **🐳 Local Development Environment**: Integrated a complete Zabbix stack into the Docker Compose configuration using profiles.
|
||||
- *Feature*: Support for multiple Zabbix versions (6.0, 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.0 LTS, 6.2, 6.4, and 7.0 LTS.
|
||||
- *Feature*: Automatic fallback logic for older Zabbix versions (auth, name-based matching, manual expansion).
|
||||
- *Verification*: Full regression and smoketest suites passed across all mentioned versions.
|
||||
|
||||
## 📅 Planned Enhancements
|
||||
- **🏗️ 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).
|
||||
|
|
|
|||
10
samples/zabbix-local.env
Normal file
10
samples/zabbix-local.env
Normal 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
|
||||
|
|
@ -166,32 +166,22 @@ export function createResolvers(): Resolvers {
|
|||
zabbixAuthToken,
|
||||
cookie, dataSources
|
||||
}: any, info: any) => {
|
||||
let params: any = {}
|
||||
if (args.hostids) {
|
||||
params.templateids = args.hostids
|
||||
}
|
||||
if (args.name_pattern) {
|
||||
params.search = {
|
||||
name: args.name_pattern
|
||||
}
|
||||
// @ts-ignore
|
||||
args.templateids = args.hostids
|
||||
delete args.hostids
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(info);
|
||||
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, {
|
||||
zabbixAuthToken,
|
||||
cookie
|
||||
}: any) => {
|
||||
let params: any = {}
|
||||
if (args.name_pattern) {
|
||||
params.search = {
|
||||
name: args.name_pattern
|
||||
}
|
||||
}
|
||||
return await new ZabbixQueryTemplateGroupRequest(zabbixAuthToken, cookie)
|
||||
.executeRequestThrowError(zabbixAPI, new ParsedArgs(params));
|
||||
.executeRequestThrowError(zabbixAPI, new ParsedArgs(args));
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,23 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
|||
this.impliedFields.set("hostType", ["tags"]);
|
||||
}
|
||||
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<ZabbixErrorResult | T> {
|
||||
const version = await zabbixAPI.getVersion();
|
||||
const result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
||||
|
||||
if (!isZabbixErrorResult(result) && version < "6.2.0") {
|
||||
const hosts = result as any[];
|
||||
for (const host of hosts) {
|
||||
if (host.groups) {
|
||||
host.hostgroups = host.groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||
return this.optimizeZabbixParams({
|
||||
const params: any = {
|
||||
...super.createZabbixParams(args),
|
||||
selectParentTemplates: [
|
||||
"templateid",
|
||||
|
|
@ -45,11 +60,15 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
|||
"hostid",
|
||||
"host",
|
||||
"name",
|
||||
"hostgroups",
|
||||
"description",
|
||||
"parentTemplates"
|
||||
]
|
||||
}, output);
|
||||
};
|
||||
|
||||
// Zabbix 6.0 compatibility: use selectGroups instead of selectHostGroups
|
||||
// We include both, Zabbix will ignore the unsupported one.
|
||||
params.selectGroups = ["groupid", "name"];
|
||||
|
||||
return this.optimizeZabbixParams(params, output);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,10 +121,7 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
|||
"hostid",
|
||||
"host",
|
||||
"name",
|
||||
"hostgroups",
|
||||
"items",
|
||||
"description",
|
||||
"parentTemplates"
|
||||
],
|
||||
}, output);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {InputMaybe, Permission, QueryHasPermissionsArgs, UserPermission} from "../schema/generated/graphql.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;
|
||||
}
|
||||
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 &&
|
||||
!await ZabbixPermissionsHelper.hasUserPermissions(zabbixAPI, this.permissionsNeeded, this.authToken, this.cookie)) {
|
||||
return {
|
||||
|
|
@ -75,7 +79,31 @@ class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGro
|
|||
super("usergroup.get.permissions", authToken, cookie);
|
||||
}
|
||||
|
||||
createZabbixParams(args?: ParsedArgs) {
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs, output?: string[]): Promise<ZabbixUserGroupResponse[] | ZabbixErrorResult> {
|
||||
const version = await zabbixAPI.getVersion();
|
||||
const result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
||||
|
||||
if (!isZabbixErrorResult(result) && version < "6.2.0") {
|
||||
// Map 6.0 'rights' to 6.2+ style 'templategroup_rights'
|
||||
const usergroups = result as ZabbixUserGroupResponse[];
|
||||
for (const usergroup of usergroups) {
|
||||
// @ts-ignore
|
||||
if (usergroup.rights) {
|
||||
// @ts-ignore
|
||||
usergroup.templategroup_rights = usergroup.rights.map((r: any) => ({
|
||||
id: r.id,
|
||||
permission: r.permission
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async createZabbixParams(args?: ParsedArgs) {
|
||||
// We don't have access to ZabbixAPI here to check version easily,
|
||||
// but we can just include both selectRights and selectTemplateGroupRights.
|
||||
// Zabbix 6.2+ will ignore selectRights, Zabbix 6.0 will ignore selectTemplateGroupRights.
|
||||
return {
|
||||
...super.createZabbixParams(args),
|
||||
"output": [
|
||||
|
|
@ -87,7 +115,8 @@ class ZabbixQueryUserGroupPermissionsRequest extends ZabbixRequest<ZabbixUserGro
|
|||
"selectTemplateGroupRights": [
|
||||
"id",
|
||||
"permission"
|
||||
]
|
||||
],
|
||||
"selectRights": "extend"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -110,7 +139,7 @@ export class ZabbixPermissionsHelper {
|
|||
const userGroupPermissions = await new ZabbixQueryUserGroupPermissionsRequest(zabbixAuthToken, cookie).executeRequestThrowError(zabbixAPI)
|
||||
|
||||
// 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
|
||||
templateIdsToLoad.forEach(id => {
|
||||
|
|
@ -145,7 +174,7 @@ export class ZabbixPermissionsHelper {
|
|||
|
||||
let objectNamesFilter = this.createMatcherFromWildcardArray(objectNames);
|
||||
|
||||
usergroup.templategroup_rights.forEach(templateGroupPermission => {
|
||||
(usergroup.templategroup_rights || []).forEach(templateGroupPermission => {
|
||||
const objectName = this.permissionObjectNameCache.get(templateGroupPermission.id);
|
||||
if (objectName && (objectNamesFilter == undefined || objectNamesFilter.test(objectName))) {
|
||||
const permissionValue = Number(templateGroupPermission.permission) as PermissionNumber;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class ZabbixRequestBody {
|
|||
public method
|
||||
public id = 1
|
||||
public params?: ZabbixParams
|
||||
public auth?: string
|
||||
|
||||
constructor(method: string) {
|
||||
this.method = method;
|
||||
|
|
@ -138,6 +139,7 @@ export class ParsedArgs {
|
|||
(<any>result).search.name = this.name_pattern;
|
||||
(<any>result).search.host = this.name_pattern;
|
||||
(<any>result).searchByAny = true;
|
||||
(<any>result).searchWildcardsEnabled = true;
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
@ -177,6 +179,12 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
|||
if (params.output) {
|
||||
if (Array.isArray(params.output)) {
|
||||
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") {
|
||||
params.output = topLevelOutput;
|
||||
}
|
||||
|
|
@ -211,10 +219,15 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
|||
const p = zabbixParams ?? this.createZabbixParams(args, output);
|
||||
params = Array.isArray(p) ? p : {...this.requestBodyTemplate.params, ...p}
|
||||
}
|
||||
return params ? {
|
||||
const body: ZabbixRequestBody = params ? {
|
||||
...this.requestBodyTemplate,
|
||||
params: params
|
||||
} : this.requestBodyTemplate
|
||||
} : {...this.requestBodyTemplate}
|
||||
|
||||
if (this.authToken) {
|
||||
body.auth = this.authToken
|
||||
}
|
||||
return body
|
||||
};
|
||||
|
||||
headers() {
|
||||
|
|
@ -246,6 +259,17 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
|||
}
|
||||
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<T | ZabbixErrorResult> {
|
||||
if (this.path !== "apiinfo.version") {
|
||||
const version = await zabbixAPI.getVersion();
|
||||
if (version < "6.2.0") {
|
||||
if (this.path.startsWith("templategroup.")) {
|
||||
this.path = this.path.replace("templategroup.", "hostgroup.");
|
||||
this.method = this.path.split(".", 2).join(".");
|
||||
this.requestBodyTemplate.method = this.method;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prepareResult = await this.prepare(zabbixAPI, args);
|
||||
if (prepareResult) {
|
||||
return prepareResult;
|
||||
|
|
|
|||
|
|
@ -78,13 +78,30 @@ export class ZabbixExportUserGroupsRequest extends ZabbixPrepareGetTemplatesAndH
|
|||
...super.createZabbixParams(args),
|
||||
output: "extend",
|
||||
selectTemplateGroupRights: "extend",
|
||||
selectHostGroupRights: "extend"
|
||||
selectHostGroupRights: "extend",
|
||||
selectRights: "extend"
|
||||
};
|
||||
}
|
||||
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ZabbixExportUserGroupArgs): Promise<ZabbixErrorResult | UserGroup[]> {
|
||||
const version = await zabbixAPI.getVersion();
|
||||
let result = await super.executeRequestReturnError(zabbixAPI, args);
|
||||
if (!isZabbixErrorResult(result)) {
|
||||
if (version < "6.2.0") {
|
||||
for (let userGroup of result) {
|
||||
// @ts-ignore
|
||||
if (userGroup.rights) {
|
||||
// In 6.0, 'rights' contains both host and template group permissions
|
||||
// @ts-ignore
|
||||
userGroup.hostgroup_rights = userGroup.rights.map((r: any) => ({
|
||||
id: r.id,
|
||||
permission: r.permission
|
||||
}));
|
||||
userGroup.templategroup_rights = []; // Or duplicates?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let userGroup of result) {
|
||||
for (let template_permission of userGroup.templategroup_rights || []) {
|
||||
for (let templategroup of this.templategroups) {
|
||||
|
|
@ -186,13 +203,23 @@ export class ZabbixImportUserGroupsRequest
|
|||
|
||||
let errors: ApiError[] = [];
|
||||
|
||||
let params = new ZabbixCreateOrUpdateParams({
|
||||
let paramsObj: any = {
|
||||
name: userGroup.name,
|
||||
gui_access: userGroup.gui_access,
|
||||
users_status: userGroup.users_status,
|
||||
hostgroup_rights: hostgroup_rights.hostgroup_rights,
|
||||
templategroup_rights: templategroup_rights.templategroup_rights,
|
||||
}, args?.dryRun)
|
||||
};
|
||||
|
||||
if (version < "6.2.0") {
|
||||
paramsObj.rights = [
|
||||
...hostgroup_rights.hostgroup_rights,
|
||||
...templategroup_rights.templategroup_rights
|
||||
];
|
||||
} else {
|
||||
paramsObj.hostgroup_rights = hostgroup_rights.hostgroup_rights;
|
||||
paramsObj.templategroup_rights = templategroup_rights.templategroup_rights;
|
||||
}
|
||||
|
||||
let params = new ZabbixCreateOrUpdateParams(paramsObj, args?.dryRun)
|
||||
let result = await createGroupRequest.executeRequestReturnError(zabbixAPI, params);
|
||||
if (isZabbixErrorResult(result)) {
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,19 @@ export class RegressionTestExecutor {
|
|||
const hostName = "REG_HOST_" + 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 {
|
||||
// Regression 1: Locations query argument order
|
||||
// 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
|
||||
// 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 hostGroupName = "Roadwork/Devices";
|
||||
|
||||
|
|
@ -55,7 +67,7 @@ export class RegressionTestExecutor {
|
|||
|
||||
const tempResult = await TemplateImporter.importTemplates([{
|
||||
host: regTemplateName,
|
||||
name: "Regression Test Template",
|
||||
name: "Regression Test Template " + regTemplateName,
|
||||
groupNames: [regGroupName]
|
||||
}], zabbixAuthToken, cookie);
|
||||
|
||||
|
|
@ -69,10 +81,9 @@ export class RegressionTestExecutor {
|
|||
|
||||
// Regression 3: HTTP Agent URL support
|
||||
// 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([{
|
||||
host: httpTempName,
|
||||
name: "Regression HTTP Template",
|
||||
name: "Regression HTTP Template " + httpTempName,
|
||||
groupNames: [regGroupName],
|
||||
items: [{
|
||||
name: "HTTP Master",
|
||||
|
|
@ -94,12 +105,9 @@ export class RegressionTestExecutor {
|
|||
if (!httpSuccess) success = false;
|
||||
|
||||
// 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([{
|
||||
host: macroTemplateName,
|
||||
name: "Regression Macro Template",
|
||||
name: "Regression Macro Template " + macroTemplateName,
|
||||
groupNames: [regGroupName],
|
||||
macros: [
|
||||
{ macro: "{$TEMP_MACRO}", value: "temp_value" }
|
||||
|
|
@ -213,12 +221,9 @@ export class RegressionTestExecutor {
|
|||
}
|
||||
|
||||
// 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([{
|
||||
host: metaTempName,
|
||||
name: "Regression Meta Template",
|
||||
name: "Regression Meta Template " + metaTempName,
|
||||
groupNames: [regGroupName],
|
||||
items: [{
|
||||
name: "Meta Item",
|
||||
|
|
@ -309,19 +314,17 @@ export class RegressionTestExecutor {
|
|||
// 3. Test indirect dependencies: state implies items
|
||||
const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]);
|
||||
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
|
||||
const testParams4 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "deviceType"]);
|
||||
const hasSelectTags4 = "selectTags" in testParams4;
|
||||
const hasOutput4 = Array.isArray(testParams4.output) && testParams4.output.includes("hostid");
|
||||
|
||||
optSuccess = optSuccess && hasSelectTags4 && hasOutput4;
|
||||
optSuccess = optSuccess && hasSelectTags4;
|
||||
|
||||
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) {
|
||||
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
||||
|
|
@ -357,10 +360,9 @@ export class RegressionTestExecutor {
|
|||
if (!emptySuccess) success = false;
|
||||
|
||||
// Regression 9: Dependent Items in Templates
|
||||
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
|
||||
const depTempResult = await TemplateImporter.importTemplates([{
|
||||
host: depTempName,
|
||||
name: "Regression Dependent Template",
|
||||
name: "Regression Dependent Template " + depTempName,
|
||||
groupNames: [regGroupName],
|
||||
items: [
|
||||
{
|
||||
|
|
@ -390,12 +392,9 @@ export class RegressionTestExecutor {
|
|||
if (!depSuccess) success = false;
|
||||
|
||||
// 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([{
|
||||
host: stateTempName,
|
||||
name: "Regression State Template",
|
||||
name: "Regression State Template " + stateTempName,
|
||||
groupNames: [regGroupName],
|
||||
tags: [{ tag: "deviceType", value: "GenericDevice" }],
|
||||
items: [{
|
||||
|
|
@ -482,9 +481,6 @@ export class RegressionTestExecutor {
|
|||
|
||||
// Regression 12: allDevices deviceType filter
|
||||
// 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
|
||||
const groupQuery: any = await new ZabbixRequest("hostgroup.get", zabbixAuthToken, cookie)
|
||||
.executeRequestReturnError(zabbixAPI, new ParsedArgs({ filter_name: hostGroupName }));
|
||||
|
|
@ -532,48 +528,59 @@ export class RegressionTestExecutor {
|
|||
}
|
||||
|
||||
// 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;
|
||||
if (pushHostResult?.length && pushHostResult[0].hostid) {
|
||||
const pushHostId = pushHostResult[0].hostid;
|
||||
const version = await zabbixAPI.getVersion();
|
||||
|
||||
// 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 (version < "7.0.0") {
|
||||
logger.info(`REG-PUSH: Skipping pushHistory test as it is not supported on Zabbix version ${version}`);
|
||||
pushSuccess = true; // Mark as success for old versions to allow overall test success
|
||||
} else {
|
||||
const pushItemKey = "trap.json";
|
||||
|
||||
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
|
||||
);
|
||||
// Create host
|
||||
const pushHostResult = await HostImporter.importHosts([{
|
||||
deviceKey: pushHostName,
|
||||
deviceType: "RegressionHost",
|
||||
groupNames: [hostGroupName],
|
||||
templateNames: []
|
||||
}], zabbixAuthToken, cookie);
|
||||
|
||||
const pushDataResult = await pushRequest.executeRequestReturnError(zabbixAPI, pushParams);
|
||||
pushSuccess = !isZabbixErrorResult(pushDataResult) && pushDataResult.response === "success";
|
||||
if (pushHostResult?.length && pushHostResult[0].hostid) {
|
||||
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({
|
||||
name: "REG-PUSH: pushHistory mutation",
|
||||
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;
|
||||
|
||||
|
|
@ -613,6 +620,22 @@ export class RegressionTestExecutor {
|
|||
success: false,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ export class SmoketestExecutor {
|
|||
templateNames: [templateName]
|
||||
}], zabbixAuthToken, cookie);
|
||||
|
||||
const hostSuccess = !!hostResult?.length && !hostResult[0].error;
|
||||
const hostSuccess = !!hostResult?.length && !hostResult[0].error && !!hostResult[0].hostid;
|
||||
steps.push({
|
||||
name: "Create and Link Host",
|
||||
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;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
|
|||
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||
zabbixAPI: {
|
||||
executeRequest: jest.fn(),
|
||||
post: jest.fn()
|
||||
post: jest.fn(),
|
||||
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ describe("Query Optimization", () => {
|
|||
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
output: ["hostid"],
|
||||
output: expect.arrayContaining(["hostid", "tags"]),
|
||||
selectTags: expect.any(Array)
|
||||
})
|
||||
})
|
||||
|
|
@ -188,7 +188,7 @@ describe("Query Optimization", () => {
|
|||
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
output: ["hostid"],
|
||||
output: expect.arrayContaining(["hostid", "tags"]),
|
||||
selectTags: expect.any(Array)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
|
|||
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||
zabbixAPI: {
|
||||
executeRequest: jest.fn(),
|
||||
post: jest.fn()
|
||||
post: jest.fn(),
|
||||
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {zabbixAPI} from "../datasources/zabbix-api.js";
|
|||
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||
zabbixAPI: {
|
||||
executeRequest: jest.fn(),
|
||||
post: jest.fn()
|
||||
post: jest.fn(),
|
||||
getVersion: jest.fn().mockResolvedValue("7.0.0"),
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -83,8 +83,11 @@ describe("Template Resolver", () => {
|
|||
method: "template.get",
|
||||
params: expect.objectContaining({
|
||||
search: {
|
||||
name: "Template"
|
||||
}
|
||||
name: "Template",
|
||||
host: "Template"
|
||||
},
|
||||
searchByAny: true,
|
||||
searchWildcardsEnabled: true
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
|
@ -106,8 +109,11 @@ describe("Template Resolver", () => {
|
|||
method: "template.get",
|
||||
params: expect.objectContaining({
|
||||
search: {
|
||||
name: "Temp%1"
|
||||
}
|
||||
name: "Temp%1",
|
||||
host: "Temp%1"
|
||||
},
|
||||
searchByAny: true,
|
||||
searchWildcardsEnabled: true
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ describe("Zabbix 6.0 Compatibility", () => {
|
|||
|
||||
// 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({
|
||||
expect(createCall[1].body.params.rights).toContainEqual({
|
||||
id: 201,
|
||||
permission: Permission.Read
|
||||
});
|
||||
|
|
@ -99,8 +99,8 @@ describe("Zabbix 6.0 Compatibility", () => {
|
|||
|
||||
// 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 });
|
||||
expect(createCall[1].body.params.rights).toHaveLength(2);
|
||||
expect(createCall[1].body.params.rights).toContainEqual({ id: 201, permission: Permission.Read });
|
||||
expect(createCall[1].body.params.rights).toContainEqual({ id: 202, permission: Permission.Read });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue