diff --git a/.gitignore b/.gitignore index 78ded7b..a612f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index c8f0612..023f0e2 100644 --- a/README.md +++ b/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 diff --git a/docker-compose.yml b/docker-compose.yml index 4a56b5c..c938d1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/docs/howtos/README.md b/docs/howtos/README.md index 43ed518..d619569 100644 --- a/docs/howtos/README.md +++ b/docs/howtos/README.md @@ -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. diff --git a/docs/howtos/local_development.md b/docs/howtos/local_development.md new file mode 100644 index 0000000..e8c47d4 --- /dev/null +++ b/docs/howtos/local_development.md @@ -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) diff --git a/docs/howtos/maintenance.md b/docs/howtos/maintenance.md index 2d9b7cd..617d76f 100644 --- a/docs/howtos/maintenance.md +++ b/docs/howtos/maintenance.md @@ -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. diff --git a/roadmap.md b/roadmap.md index f86577c..88f6341 100644 --- a/roadmap.md +++ b/roadmap.md @@ -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). diff --git a/samples/zabbix-local.env b/samples/zabbix-local.env new file mode 100644 index 0000000..b304fc5 --- /dev/null +++ b/samples/zabbix-local.env @@ -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 diff --git a/src/api/resolvers.ts b/src/api/resolvers.ts index 44e38d0..d0d3bc4 100644 --- a/src/api/resolvers.ts +++ b/src/api/resolvers.ts @@ -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: { diff --git a/src/datasources/zabbix-hosts.ts b/src/datasources/zabbix-hosts.ts index 4c18c6e..4232f0e 100644 --- a/src/datasources/zabbix-hosts.ts +++ b/src/datasources/zabbix-hosts.ts @@ -25,8 +25,23 @@ export class ZabbixQueryHostsGenericRequest { + 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 { + 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 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; diff --git a/src/datasources/zabbix-request.ts b/src/datasources/zabbix-request.ts index fb54110..343ef62 100644 --- a/src/datasources/zabbix-request.ts +++ b/src/datasources/zabbix-request.ts @@ -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 { (result).search.name = this.name_pattern; (result).search.host = this.name_pattern; (result).searchByAny = true; + (result).searchWildcardsEnabled = true; } return result @@ -177,6 +179,12 @@ export class ZabbixRequest 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 { + 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; diff --git a/src/datasources/zabbix-usergroups.ts b/src/datasources/zabbix-usergroups.ts index 47a9da5..bd576a3 100644 --- a/src/datasources/zabbix-usergroups.ts +++ b/src/datasources/zabbix-usergroups.ts @@ -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 { + 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)) { diff --git a/src/execution/regression_test_executor.ts b/src/execution/regression_test_executor.ts index da89787..d3c49db 100644 --- a/src/execution/regression_test_executor.ts +++ b/src/execution/regression_test_executor.ts @@ -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(); + + 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"; - // 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" - })); + // Create host + const pushHostResult = await HostImporter.importHosts([{ + deviceKey: pushHostName, + deviceType: "RegressionHost", + groupNames: [hostGroupName], + templateNames: [] + }], zabbixAuthToken, cookie); - 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 - ); + if (pushHostResult?.length && pushHostResult[0].hostid) { + const pushHostId = pushHostResult[0].hostid; - const pushDataResult = await pushRequest.executeRequestReturnError(zabbixAPI, pushParams); - pushSuccess = !isZabbixErrorResult(pushDataResult) && pushDataResult.response === "success"; + // 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 { diff --git a/src/execution/smoketest_executor.ts b/src/execution/smoketest_executor.ts index 497789f..f2a3598 100644 --- a/src/execution/smoketest_executor.ts +++ b/src/execution/smoketest_executor.ts @@ -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 { diff --git a/src/test/misc_resolvers.test.ts b/src/test/misc_resolvers.test.ts index 4c6f588..90a5609 100644 --- a/src/test/misc_resolvers.test.ts +++ b/src/test/misc_resolvers.test.ts @@ -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"), } })); diff --git a/src/test/query_optimization.test.ts b/src/test/query_optimization.test.ts index 1aa58d8..2fa6259 100644 --- a/src/test/query_optimization.test.ts +++ b/src/test/query_optimization.test.ts @@ -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) }) }) diff --git a/src/test/template_deleter.test.ts b/src/test/template_deleter.test.ts index 68e4164..b1d09e9 100644 --- a/src/test/template_deleter.test.ts +++ b/src/test/template_deleter.test.ts @@ -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"), } })); diff --git a/src/test/template_importer.test.ts b/src/test/template_importer.test.ts index 6258c0e..976ef88 100644 --- a/src/test/template_importer.test.ts +++ b/src/test/template_importer.test.ts @@ -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"), } })); diff --git a/src/test/template_query.test.ts b/src/test/template_query.test.ts index f5fec55..22c5e97 100644 --- a/src/test/template_query.test.ts +++ b/src/test/template_query.test.ts @@ -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 }) }) })); diff --git a/src/test/zabbix_6_0_compatibility.test.ts b/src/test/zabbix_6_0_compatibility.test.ts index 439981e..6535d81 100644 --- a/src/test/zabbix_6_0_compatibility.test.ts +++ b/src/test/zabbix_6_0_compatibility.test.ts @@ -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 }); }); });