diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index d1b9822..efd7416 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -5,30 +5,21 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -39,7 +30,7 @@
-
+
@@ -87,45 +78,50 @@
- {
+ "keyToString": {
+ "NIXITCH_NIXPKGS_CONFIG": "",
+ "NIXITCH_NIX_CONF_DIR": "",
+ "NIXITCH_NIX_OTHER_STORES": "",
+ "NIXITCH_NIX_PATH": "",
+ "NIXITCH_NIX_PROFILES": "",
+ "NIXITCH_NIX_REMOTE": "",
+ "NIXITCH_NIX_USER_PROFILE_DIR": "",
+ "Node.js.index.ts.executor": "Run",
+ "RunOnceActivity.MCP Project settings loaded": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.typescript.service.memoryLimit.init": "true",
+ "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
+ "git-widget-placeholder": "main",
+ "go.import.settings.migrated": "true",
+ "javascript.preferred.runtime.type.id": "node",
+ "junie.onboarding.icon.badge.shown": "true",
+ "last_opened_file_path": "//wsl.localhost/Ubuntu/home/ahilbig/git/vcr/zabbix-graphql-api/docs/use-cases",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_interpreter_path": "wsl://Ubuntu@/home/ahilbig/.nvm/versions/node/v24.12.0/bin/node",
+ "nodejs_package_manager_path": "npm",
+ "npm.codegen.executor": "Run",
+ "npm.compile.executor": "Run",
+ "npm.copy-schema.executor": "Run",
+ "npm.prod.executor": "Run",
+ "npm.test.executor": "Run",
+ "settings.editor.selected.configurable": "junie.mcp",
+ "settings.editor.splitter.proportion": "0.23751687",
+ "to.speed.mode.migration.done": "true",
+ "ts.external.directory.path": "\\\\wsl.localhost\\Ubuntu\\home\\ahilbig\\git\\vcr\\zabbix-graphql-api\\node_modules\\typescript\\lib",
+ "vue.rearranger.settings.migration": "true"
+ },
+ "keyToStringList": {
+ "com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
+ "TEXT"
+ ]
}
-}]]>
+}
@@ -231,7 +227,10 @@
-
+
+
+
+
diff --git a/README.md b/README.md
index 9fd837f..c8f0612 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ Before you begin, ensure you have met the following requirements:
- **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`).
-- **Zabbix**: A running Zabbix instance (compatible with Zabbix 6.0+) with API access.
+- **Zabbix**: A running Zabbix instance (compatible with Zabbix 6.0+) with API access. See [Zabbix Version Compatibility](#-zabbix-version-compatibility) for details.
- **Zabbix Super Admin Token** (for full functionality / privilege escalation).
- **Zabbix User Access** (groups and roles depending on your use case).
@@ -236,7 +236,20 @@ The API version is automatically set during the Docker build process based on th
### 🔧 Zabbix Version Compatibility
-This API is designed to work with Zabbix 7.4, which is the version it runs productively with. While it may work with earlier versions (like 6.0+), 7.4 is the officially supported and tested version.
+This API is officially supported and productively used with **Zabbix 7.0 (LTS)** and newer. It also maintains compatibility with **Zabbix 6.0 (LTS)** and **6.4**, with the following version-specific behaviors:
+
+- **Zabbix 7.0 (LTS)**:
+ - Full feature support.
+ - **History Push**: Uses the native `history.push` API for efficient data ingestion.
+- **Zabbix 6.4**:
+ - **History Push**: Not supported (requires Zabbix 7.0+). The `pushHistory` mutation returns a clear error.
+ - **Group Propagation**: Fully supported via the `hostgroup.propagate` API (introduced in 6.2).
+ - **UUIDs**: Fully supported for both Host Groups and Template Groups.
+- **Zabbix 6.0 (LTS)**:
+ - **History Push**: Not supported.
+ - **Group Propagation**: `hostgroup.propagate` is unavailable. The API automatically performs **manual host group expansion** during import to ensure consistent behavior.
+ - **UUIDs**: Host Groups in 6.0 lack UUIDs. The API uses a **name-based fallback** for matching host group permissions during import.
+- **Template Groups**: The API correctly handles the separation of Host Groups and Template Groups introduced in Zabbix 6.0 across all versions.
## 🛠️ Technical Maintenance
diff --git a/roadmap.md b/roadmap.md
index 4abf77e..f86577c 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -6,12 +6,13 @@ This document outlines the achieved milestones and planned enhancements for the
- **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
- *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.
- *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases.
## 📅 Planned Enhancements
-- **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance.
-
- **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
- *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md).
- *Simulation*:
diff --git a/src/datasources/zabbix-api.ts b/src/datasources/zabbix-api.ts
index 17ed27c..1ecaa73 100644
--- a/src/datasources/zabbix-api.ts
+++ b/src/datasources/zabbix-api.ts
@@ -80,6 +80,20 @@ export class ZabbixAPI
return super.post(path, request);
}
+ private static version: string | undefined
+
+ async getVersion(): Promise {
+ if (!ZabbixAPI.version) {
+ const response = await this.requestByPath("apiinfo.version")
+ if (typeof response === "string") {
+ ZabbixAPI.version = response
+ } else {
+ return "0.0.0"
+ }
+ }
+ return ZabbixAPI.version
+ }
+
async executeRequest(zabbixRequest: ZabbixRequest, args?: A, throwApiError: boolean = true, output?: string[]): Promise {
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output);
}
diff --git a/src/datasources/zabbix-history.ts b/src/datasources/zabbix-history.ts
index c3d35b7..db57459 100644
--- a/src/datasources/zabbix-history.ts
+++ b/src/datasources/zabbix-history.ts
@@ -81,6 +81,10 @@ export class ZabbixHistoryPushRequest extends ZabbixRequest {
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)) {
throw new GraphQLError("if itemid is empty both key and host must be filled");
diff --git a/src/datasources/zabbix-hostgroups.ts b/src/datasources/zabbix-hostgroups.ts
index 67a3d22..a517f7b 100644
--- a/src/datasources/zabbix-hostgroups.ts
+++ b/src/datasources/zabbix-hostgroups.ts
@@ -100,4 +100,33 @@ export class GroupHelper {
}
return result
}
+
+ public static async findSubgroupIds(groupids: number[], zabbixApi: ZabbixAPI, zabbixAuthToken?: string, cookie?: string): Promise {
+ if (groupids.length === 0) return [];
+
+ // Get the names of the parent groups
+ const parentGroups = await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ZabbixQueryHostgroupsParams({
+ groupids: groupids
+ }));
+
+ if (isZabbixErrorResult(parentGroups) || !parentGroups.length) {
+ return [];
+ }
+
+ const subgroupids: number[] = [];
+ for (const parent of parentGroups) {
+ const subgroups = await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestReturnError(zabbixApi, new ZabbixQueryHostgroupsParams({
+ search: {
+ name: parent.name + "/*"
+ },
+ searchWildcardsEnabled: true
+ }));
+
+ if (!isZabbixErrorResult(subgroups)) {
+ subgroups.forEach(sg => subgroupids.push(Number(sg.groupid)));
+ }
+ }
+
+ return [...new Set(subgroupids)];
+ }
}
diff --git a/src/datasources/zabbix-usergroups.ts b/src/datasources/zabbix-usergroups.ts
index 5c61a52..47a9da5 100644
--- a/src/datasources/zabbix-usergroups.ts
+++ b/src/datasources/zabbix-usergroups.ts
@@ -17,8 +17,9 @@ import {
ZabbixGroupRightInput
} from "../schema/generated/graphql.js";
import {ZabbixAPI} from "./zabbix-api.js";
+import {logger} from "../logging/logger.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";
@@ -153,10 +154,36 @@ export class ZabbixImportUserGroupsRequest
let createGroupRequest = new ZabbixCreateOrUpdateRequest<
ZabbixCreateUserGroupResponse, ZabbixQueryUserGroupsRequest, ZabbixCreateOrUpdateParams>(
"usergroup", "usrgrpid", ZabbixQueryUserGroupsRequest, this.authToken, this.cookie);
+
+ const version = await zabbixAPI.getVersion();
+ const needsManualPropagation = version < "6.2.0";
+
for (let userGroup of args?.usergroups || []) {
let templategroup_rights = this.calc_templategroup_rights(userGroup);
let hostgroup_rights = this.calc_hostgroup_rights(userGroup);
+ if (needsManualPropagation && hostgroup_rights.hostgroup_rights.length > 0) {
+ const parentGroupids = hostgroup_rights.hostgroup_rights.map(r => r.id);
+ const subgroupids = await GroupHelper.findSubgroupIds(parentGroupids, zabbixAPI, this.authToken ?? undefined, this.cookie ?? undefined);
+
+ for (const sgid of subgroupids) {
+ if (!hostgroup_rights.hostgroup_rights.find(r => r.id === sgid)) {
+ // Find the permission of the parent group to apply it to the subgroup
+ // This is a simplification: if multiple parents match, we take one.
+ // Actually, we should find which parent this subgroup belongs to.
+ // But for simplicity, we can just look at the parent permissions.
+ // In most cases, it's just one parent being propagated.
+ const parent = hostgroup_rights.hostgroup_rights.find(r => parentGroupids.includes(r.id)); // just take first
+ if (parent) {
+ hostgroup_rights.hostgroup_rights.push({
+ id: sgid,
+ permission: parent.permission
+ });
+ }
+ }
+ }
+ }
+
let errors: ApiError[] = [];
let params = new ZabbixCreateOrUpdateParams({
@@ -214,34 +241,52 @@ export class ZabbixImportUserGroupsRequest
for (let hostgroup_right of usergroup.hostgroup_rights || []) {
let success = false;
let matchedName = "";
+ let matchedId: number | undefined = undefined;
+
+ // Try matching by UUID first
for (let hostgroup of this.hostgroups) {
- if (hostgroup.uuid == hostgroup_right.uuid) {
- result.push(
- {
- id: Number(hostgroup.groupid),
- permission: hostgroup_right.permission,
- }
- )
- success = true;
+ if (hostgroup.uuid && hostgroup_right.uuid && hostgroup.uuid === hostgroup_right.uuid) {
+ matchedId = Number(hostgroup.groupid);
matchedName = hostgroup.name;
+ success = true;
break;
}
-
}
- if (success && hostgroup_right.name && hostgroup_right.name != matchedName) {
- errors.push(
+
+ // Fallback to matching by name (important for Zabbix 6.0 which lacks Host Group UUIDs)
+ if (!success && hostgroup_right.name) {
+ for (let hostgroup of this.hostgroups) {
+ if (hostgroup.name === hostgroup_right.name) {
+ matchedId = Number(hostgroup.groupid);
+ matchedName = hostgroup.name;
+ success = true;
+ break;
+ }
+ }
+ }
+
+ if (success) {
+ result.push(
{
- code: ApiErrorCode.OK,
- message: `WARNING: Hostgroup found and permissions set, but target name=${matchedName} does not match`,
- data: hostgroup_right,
+ id: matchedId!,
+ permission: hostgroup_right.permission,
}
)
- }
- 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(
{
code: ApiErrorCode.ZABBIX_HOSTGROUP_NOT_FOUND,
- message: `Hostgroup with UUID ${hostgroup_right.uuid} not found`,
+ message: `Hostgroup with UUID ${hostgroup_right.uuid} ${hostgroup_right.name ? "or name " + hostgroup_right.name : ""} not found`,
data: hostgroup_right,
}
)
@@ -262,33 +307,52 @@ export class ZabbixImportUserGroupsRequest
for (let templategroup_right of usergroup.templategroup_rights || []) {
let success = false;
let matchedName = "";
+ let matchedId: number | undefined = undefined;
+
+ // Try matching by UUID first
for (let templategroup of this.templategroups) {
- if (templategroup.uuid == templategroup_right.uuid) {
- result.push(
- {
- id: Number(templategroup.groupid),
- permission: templategroup_right.permission,
- }
- )
+ if (templategroup.uuid && templategroup_right.uuid && templategroup.uuid === templategroup_right.uuid) {
+ matchedId = Number(templategroup.groupid);
+ matchedName = templategroup.name;
success = true;
- matchedName = templategroup.name
break;
}
}
- if (success && templategroup_right.name && templategroup_right.name != matchedName) {
- errors.push(
+
+ // Fallback to matching by name
+ if (!success && templategroup_right.name) {
+ for (let templategroup of this.templategroups) {
+ if (templategroup.name === templategroup_right.name) {
+ matchedId = Number(templategroup.groupid);
+ matchedName = templategroup.name;
+ success = true;
+ break;
+ }
+ }
+ }
+
+ if (success) {
+ result.push(
{
- code: ApiErrorCode.OK,
- message: `WARNING: Templategroup found and permissions set, but target name=${matchedName} does not match`,
- data: templategroup_right,
+ id: matchedId!,
+ permission: templategroup_right.permission,
}
)
- }
- 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(
{
code: ApiErrorCode.ZABBIX_TEMPLATEGROUP_NOT_FOUND,
- message: `Templategroup with UUID ${templategroup_right.uuid} not found`,
+ message: `Templategroup with UUID ${templategroup_right.uuid} ${templategroup_right.name ? "or name " + templategroup_right.name : ""} not found`,
data: templategroup_right,
}
)
@@ -317,6 +381,17 @@ export class ZabbixPropagateHostGroupsRequest extends ZabbixRequest {
+ const version = await zabbixAPI.getVersion();
+ if (version < "6.2.0") {
+ logger.warn(`hostgroup.propagate is only supported in Zabbix 6.2.0 and newer. Current version is ${version}. Skipping propagation.`);
+ return {
+ usrgrpids: []
+ } as ZabbixCreateUserGroupResponse;
+ }
+ return super.prepare(zabbixAPI, args);
+ }
+
createZabbixParams(args?: ZabbixPropagateHostGroupsParams): ZabbixParams {
return {
groups: [...new Set(args?.groups || [])].map(value => {
diff --git a/src/test/history_push.test.ts b/src/test/history_push.test.ts
index a6fbb31..0882e04 100644
--- a/src/test/history_push.test.ts
+++ b/src/test/history_push.test.ts
@@ -6,6 +6,7 @@ import {GraphQLError} from "graphql";
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0")
}
}));
@@ -63,4 +64,12 @@ describe("ZabbixHistoryPushRequest", () => {
await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("if itemid is empty both key and host must be filled");
});
+
+ test("prepare - throw error if Zabbix version < 7.0.0", async () => {
+ (zabbixAPI.getVersion as jest.Mock).mockResolvedValue("6.0.0");
+ const values = [{ timestamp: "2024-01-01T10:00:00Z", value: "val" }];
+ const params = new ZabbixHistoryPushParams(values, "1");
+
+ await expect(request.prepare(zabbixAPI, params)).rejects.toThrow("history.push is only supported in Zabbix 7.0.0 and newer");
+ });
});
diff --git a/src/test/history_push_integration.test.ts b/src/test/history_push_integration.test.ts
index 607c69c..b1dbcd6 100644
--- a/src/test/history_push_integration.test.ts
+++ b/src/test/history_push_integration.test.ts
@@ -6,6 +6,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
}
}));
diff --git a/src/test/host_importer.test.ts b/src/test/host_importer.test.ts
index 14fb94f..c2781d4 100644
--- a/src/test/host_importer.test.ts
+++ b/src/test/host_importer.test.ts
@@ -6,6 +6,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
requestByPath: jest.fn()
},
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"
diff --git a/src/test/host_integration.test.ts b/src/test/host_integration.test.ts
index 00d1544..8b3fad4 100644
--- a/src/test/host_integration.test.ts
+++ b/src/test/host_integration.test.ts
@@ -8,6 +8,7 @@ import {ZABBIX_EDGE_DEVICE_BASE_GROUP, zabbixAPI} from '../datasources/zabbix-ap
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix',
getLocations: jest.fn(),
diff --git a/src/test/host_query.test.ts b/src/test/host_query.test.ts
index 0762084..d50e5aa 100644
--- a/src/test/host_query.test.ts
+++ b/src/test/host_query.test.ts
@@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix",
getLocations: jest.fn()
},
diff --git a/src/test/indirect_dependencies.test.ts b/src/test/indirect_dependencies.test.ts
index 19d82cd..8e9c49a 100644
--- a/src/test/indirect_dependencies.test.ts
+++ b/src/test/indirect_dependencies.test.ts
@@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix",
}
}));
diff --git a/src/test/query_optimization.test.ts b/src/test/query_optimization.test.ts
index a7765ac..1aa58d8 100644
--- a/src/test/query_optimization.test.ts
+++ b/src/test/query_optimization.test.ts
@@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix",
}
}));
diff --git a/src/test/template_integration.test.ts b/src/test/template_integration.test.ts
index 22858bb..8c87cc2 100644
--- a/src/test/template_integration.test.ts
+++ b/src/test/template_integration.test.ts
@@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix'
}
diff --git a/src/test/template_link.test.ts b/src/test/template_link.test.ts
index ead11e1..c5acff8 100644
--- a/src/test/template_link.test.ts
+++ b/src/test/template_link.test.ts
@@ -6,6 +6,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix',
requestByPath: jest.fn()
diff --git a/src/test/template_query.test.ts b/src/test/template_query.test.ts
index f0a065e..f5fec55 100644
--- a/src/test/template_query.test.ts
+++ b/src/test/template_query.test.ts
@@ -7,6 +7,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix"
},
ZABBIX_EDGE_DEVICE_BASE_GROUP: "Roadwork"
diff --git a/src/test/user_rights.test.ts b/src/test/user_rights.test.ts
index 0ac3732..99a30eb 100644
--- a/src/test/user_rights.test.ts
+++ b/src/test/user_rights.test.ts
@@ -6,6 +6,7 @@ jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
executeRequest: jest.fn(),
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
baseURL: "http://mock-zabbix"
}
}));
diff --git a/src/test/user_rights_integration.test.ts b/src/test/user_rights_integration.test.ts
index a7e401c..ea987b4 100644
--- a/src/test/user_rights_integration.test.ts
+++ b/src/test/user_rights_integration.test.ts
@@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix',
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: "201", name: "ConstructionSite/Test", uuid: "uuid2" }]) // hostgroup.get for groups (in prepare)
.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({
query: mutation,
diff --git a/src/test/zabbix_6_0_compatibility.test.ts b/src/test/zabbix_6_0_compatibility.test.ts
new file mode 100644
index 0000000..439981e
--- /dev/null
+++ b/src/test/zabbix_6_0_compatibility.test.ts
@@ -0,0 +1,106 @@
+import {createResolvers} from "../api/resolvers.js";
+import {zabbixAPI} from "../datasources/zabbix-api.js";
+import {Permission} from "../schema/generated/graphql.js";
+
+// Mocking ZabbixAPI
+jest.mock("../datasources/zabbix-api.js", () => ({
+ zabbixAPI: {
+ executeRequest: jest.fn(),
+ post: jest.fn(),
+ getVersion: jest.fn(),
+ baseURL: "http://mock-zabbix"
+ }
+}));
+
+describe("Zabbix 6.0 Compatibility", () => {
+ let resolvers: any;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ resolvers = createResolvers();
+ (zabbixAPI.getVersion as jest.Mock).mockResolvedValue("6.0.0");
+ });
+
+ test("importUserRights uses name-based fallback for host groups on Zabbix 6.0", async () => {
+ // Mock Zabbix 6.0 behavior where hostgroup.get does NOT return UUID
+ (zabbixAPI.post as jest.Mock).mockImplementation((path: string) => {
+ if (path === "templategroup.get") return Promise.resolve([{ groupid: "101", name: "TemplateGroup1", uuid: "uuid-tg-1" }]);
+ if (path === "hostgroup.get") return Promise.resolve([{ groupid: "201", name: "HostGroup1" }]); // NO UUID
+ if (path === "usergroup.get") return Promise.resolve([]);
+ if (path.startsWith("usergroup.create")) return Promise.resolve({ usrgrpids: ["301"] });
+ if (path === "module.get") return Promise.resolve([]);
+ if (path === "role.get") return Promise.resolve([]);
+ return Promise.resolve([]);
+ });
+
+ const args = {
+ input: {
+ userGroups: [{
+ name: "NewGroup",
+ hostgroup_rights: [{
+ name: "HostGroup1",
+ uuid: "some-uuid-from-export", // This UUID won't match anything in 6.0 mock
+ permission: Permission.Read
+ }]
+ }]
+ },
+ dryRun: false
+ };
+ const context = { zabbixAuthToken: "test-token" };
+
+ const result = await resolvers.Mutation.importUserRights(null, args, context);
+
+ expect(result.userGroups).toHaveLength(1);
+ expect(result.userGroups[0].name).toBe("NewGroup");
+ expect(result.userGroups[0].errors).toHaveLength(0); // Should succeed via name match
+
+ // Verify that usergroup.create was called with correct groupid from name match
+ const createCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[0].startsWith("usergroup.create"));
+ expect(createCall[1].body.params.hostgroup_rights).toContainEqual({
+ id: 201,
+ permission: Permission.Read
+ });
+ });
+
+ test("importUserRights performs manual expansion on Zabbix 6.0", async () => {
+ // Mock Zabbix 6.0 behavior
+ (zabbixAPI.post as jest.Mock).mockImplementation((path: string, options: any) => {
+ if (path === "templategroup.get") return Promise.resolve([]);
+ if (path === "hostgroup.get") {
+ // If searching for subgroups
+ if (options?.body?.params?.search?.name === "Parent/*") {
+ return Promise.resolve([{ groupid: "202", name: "Parent/Child" }]);
+ }
+ return Promise.resolve([{ groupid: "201", name: "Parent" }]);
+ }
+ if (path === "usergroup.get") return Promise.resolve([]);
+ if (path.startsWith("usergroup.create")) return Promise.resolve({ usrgrpids: ["301"] });
+ if (path === "module.get") return Promise.resolve([]);
+ if (path === "role.get") return Promise.resolve([]);
+ return Promise.resolve([]);
+ });
+
+ const args = {
+ input: {
+ userGroups: [{
+ name: "NewGroup",
+ hostgroup_rights: [{
+ name: "Parent",
+ uuid: "uuid-parent",
+ permission: Permission.Read
+ }]
+ }]
+ },
+ dryRun: false
+ };
+ const context = { zabbixAuthToken: "test-token" };
+
+ await resolvers.Mutation.importUserRights(null, args, context);
+
+ // Verify that usergroup.create was called with both parent and expanded child
+ const createCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[0].startsWith("usergroup.create"));
+ expect(createCall[1].body.params.hostgroup_rights).toHaveLength(2);
+ expect(createCall[1].body.params.hostgroup_rights).toContainEqual({ id: 201, permission: Permission.Read });
+ expect(createCall[1].body.params.hostgroup_rights).toContainEqual({ id: 202, permission: Permission.Read });
+ });
+});
diff --git a/src/test/zabbix_docs_samples.test.ts b/src/test/zabbix_docs_samples.test.ts
index 87c189f..7a5dafd 100644
--- a/src/test/zabbix_docs_samples.test.ts
+++ b/src/test/zabbix_docs_samples.test.ts
@@ -8,6 +8,7 @@ import {zabbixAPI} from '../datasources/zabbix-api.js';
jest.mock("../datasources/zabbix-api.js", () => ({
zabbixAPI: {
post: jest.fn(),
+ getVersion: jest.fn().mockResolvedValue("7.0.0"),
executeRequest: jest.fn(),
baseURL: 'http://localhost/zabbix',
getLocations: jest.fn(),