Repository: grafana/grafana-zabbix

Issue:
Standardize in the backend health check for the Zabbix datasource
I noticed that the health check for the Zabbix datasource is failing for the [Advisor](https://grafana.com/docs/grafana/latest/administration/grafana-advisor/) but in the data source configuration page, it does work. This is because the Advisor is running the [backend version](https://github.com/grafana/grafana-zabbix/blob/main/pkg/datasource/datasource.go#L82) of the health check, while the Config editor uses the [frontend one](https://github.com/grafana/grafana-zabbix/blob/main/src/datasource/zabbix/zabbix.ts#L210).

Since the backend health check is something that can be used for external tooling, it makes sense to use that one by default. For doing so, it will be needed to remove the method `testDatasource` from the frontend and extend the DatasourceWithBackend class that @grafana/runtime exposes or manually implement it [as the library does](https://github.com/grafana/grafana/blob/installCliCheck/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts#L376).

Hints from issue comments:
@apoeCivir: Hi believe that what you mean is what I have encountered when trying to monitor the zabbix datasource with zabbix itself.
In the frontend everything looks good (grafana 12.2, zabbix 7.2.5 and zabbix plugin 6.0.2)

<img width="1481" height="714" alt="Image" src="https://github.com/user-attachments/assets/e4227915-b96d-48d8-b2a3-80f63d65c3f1" />

But when calling the datasource health API it returns:

`curl -X GET -k -u admin:<admin-password> https://localhost:3000/api/datasources/uid/bf0wuhjjprsw0a/health
{"message":"Invalid request. Invalid parameter \"/params\": an array or object is expected.","status":"ERROR"}
`
Same happens if I use deprecated call with id, instead of uid.

Solution patch:
diff --git a/.changeset/orange-owls-tie.md b/.changeset/orange-owls-tie.md
new file mode 100644
index 0000000..13be162
--- /dev/null
+++ b/.changeset/orange-owls-tie.md
@@ -0,0 +1,5 @@
+---
+'grafana-zabbix': minor
+---
+
+Moves health check to the backend
diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go
index 4fe9a73..29723f0 100644
--- a/pkg/datasource/datasource.go
+++ b/pkg/datasource/datasource.go
@@ -3,6 +3,7 @@ package datasource
 import (
 	"context"
 	"errors"
+	"fmt"
 
 	"github.com/alexanderzobnin/grafana-zabbix/pkg/httpclient"
 	"github.com/alexanderzobnin/grafana-zabbix/pkg/metrics"
@@ -99,7 +100,7 @@ func (ds *ZabbixDatasource) CheckHealth(ctx context.Context, req *backend.CheckH
 	}
 
 	res.Status = backend.HealthStatusOk
-	res.Message = message
+	res.Message = fmt.Sprintf("Zabbix API version %s", message)
 	return res, nil
 }
 
diff --git a/pkg/datasource/zabbix.go b/pkg/datasource/zabbix.go
index 5c4e42f..e990d8c 100644
--- a/pkg/datasource/zabbix.go
+++ b/pkg/datasource/zabbix.go
@@ -36,13 +36,12 @@ func (ds *ZabbixDatasourceInstance) TestConnection(ctx context.Context) (string,
 		return "", err
 	}
 
-	response, err := ds.zabbix.Request(ctx, &zabbix.ZabbixAPIRequest{Method: "apiinfo.version"})
+	zabbixVersion, err := ds.zabbix.GetFullVersion(ctx)
 	if err != nil {
 		return "", err
 	}
 
-	resultByte, _ := response.MarshalJSON()
-	return string(resultByte), nil
+	return zabbixVersion, nil
 }
 
 func (ds *ZabbixDatasourceInstance) queryNumericItems(ctx context.Context, query *QueryModel) ([]*data.Frame, error) {
diff --git a/pkg/settings/models.go b/pkg/settings/models.go
index 6067961..e230529 100644
--- a/pkg/settings/models.go
+++ b/pkg/settings/models.go
@@ -16,8 +16,8 @@ type ZabbixDatasourceSettingsDTO struct {
 	CacheTTL    string      `json:"cacheTTL"`
 	Timeout     interface{} `json:"timeout"`
 
-	DisableDataAlignment    bool `json:"disableDataAlignment"`
-	DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"`
+	DisableDataAlignment     	bool 	`json:"disableDataAlignment"`
+	DisableReadOnlyUsersAck  	bool 	`json:"disableReadOnlyUsersAck"`
 }
 
 // ZabbixDatasourceSettings model
@@ -29,6 +29,6 @@ type ZabbixDatasourceSettings struct {
 	CacheTTL    time.Duration
 	Timeout     time.Duration
 
-	DisableDataAlignment    bool `json:"disableDataAlignment"`
-	DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"`
+	DisableDataAlignment     bool `json:"disableDataAlignment"`
+	DisableReadOnlyUsersAck  bool `json:"disableReadOnlyUsersAck"`
 }
diff --git a/pkg/zabbix/methods.go b/pkg/zabbix/methods.go
index 554e720..fb53581 100644
--- a/pkg/zabbix/methods.go
+++ b/pkg/zabbix/methods.go
@@ -524,19 +524,28 @@ func (ds *Zabbix) GetValueMappings(ctx context.Context) ([]ValueMap, error) {
 	return valuemaps, err
 }
 
-func (ds *Zabbix) GetVersion(ctx context.Context) (int, error) {
+func (ds *Zabbix) GetFullVersion(ctx context.Context) (string, error) {
 	result, err := ds.request(ctx, "apiinfo.version", ZabbixAPIParams{})
 	if err != nil {
-		return 0, err
+		return "", err
 	}
 
 	var version string
 	err = convertTo(result, &version)
+	if err != nil {
+		return "", err
+	}
+	
+	return version, nil
+}
+
+func (ds *Zabbix) GetVersion(ctx context.Context) (int, error) {
+	fullStringVersion, err := ds.GetFullVersion(ctx)
 	if err != nil {
 		return 0, err
 	}
 
-	version = strings.Replace(version[0:3], ".", "", 1)
+	version := strings.Replace(fullStringVersion[0:3], ".", "", 1)
 	versionNum, err := strconv.Atoi(version)
 	return versionNum, err
 }
diff --git a/src/datasource/datasource.ts b/src/datasource/datasource.ts
index ec9dba6..8d18d28 100644
--- a/src/datasource/datasource.ts
+++ b/src/datasource/datasource.ts
@@ -21,6 +21,7 @@ import {
   toDataQueryResponse,
   getDataSourceSrv,
   HealthCheckError,
+  DataSourceWithBackend,
 } from '@grafana/runtime';
 import {
   DataFrame,
@@ -55,6 +56,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
   dbConnectionRetentionPolicy: string;
   enableDebugLog: boolean;
   datasourceId: number;
+  instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>;
   zabbix: Zabbix;
 
   replaceTemplateVars: (target: any, scopedVars?: any) => any;
@@ -62,6 +64,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
   constructor(instanceSettings: DataSourceInstanceSettings<ZabbixDSOptions>) {
     super(instanceSettings);
 
+    this.instanceSettings = instanceSettings;
     this.enableDebugLog = config.buildInfo.env === 'development';
 
     this.annotations = {
@@ -767,17 +770,20 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
    * Test connection to Zabbix API and external history DB.
    */
   async testDatasource() {
+    const backendDS = new DataSourceWithBackend(this.instanceSettings);
     try {
-      const { zabbixVersion, dbConnectorStatus } = await this.zabbix.testDataSource();
-      let message = `Zabbix API version: ${zabbixVersion}`;
-      if (dbConnectorStatus) {
-        message += `, DB connector type: ${dbConnectorStatus.dsType}`;
-      }
-      return {
-        status: 'success',
-        title: 'Success',
-        message: message,
-      };
+      const testResult = await backendDS.testDatasource();
+      return this.zabbix.testDataSource().then((dbConnectorStatus) => {
+        let message = testResult.message;
+        if (dbConnectorStatus) {
+          message += `, DB connector type: ${dbConnectorStatus.dsType}`;
+        }
+        return {
+          status: testResult.status,
+          message: message,
+          title: testResult.status,
+        };
+      });
     } catch (error: any) {
       if (error instanceof ZabbixAPIError) {
         return Promise.reject({
diff --git a/src/datasource/zabbix/zabbix.ts b/src/datasource/zabbix/zabbix.ts
index 8ee899e..7561d06 100644
--- a/src/datasource/zabbix/zabbix.ts
+++ b/src/datasource/zabbix/zabbix.ts
@@ -208,32 +208,21 @@ export class Zabbix implements ZabbixConnector {
    * ```
    */
   testDataSource() {
-    let zabbixVersion;
     let dbConnectorStatus;
-    return this.getVersion()
-      .then((version) => {
-        zabbixVersion = version;
-        return this.getAllGroups();
-      })
-      .then(() => {
-        if (this.enableDirectDBConnection) {
-          return this.dbConnector.testDataSource();
-        } else {
-          return Promise.resolve();
-        }
-      })
-      .catch((error) => {
-        return Promise.reject(error);
-      })
-      .then((testResult) => {
+
+    if (this.enableDirectDBConnection) {
+      return this.dbConnector.testDataSource().then((testResult) => {
         if (testResult) {
           dbConnectorStatus = {
             dsType: this.dbConnector.datasourceTypeName || this.dbConnector.datasourceTypeId,
             dsName: this.dbConnector.datasourceName,
           };
+          return dbConnectorStatus;
         }
-        return { zabbixVersion, dbConnectorStatus };
       });
+    } else {
+      return Promise.resolve();
+    }
   }
 
   async getVersion() {



Test patch:
diff --git a/pkg/zabbix/methods_test.go b/pkg/zabbix/methods_test.go
new file mode 100644
index 0000000..b7236a4
--- /dev/null
+++ b/pkg/zabbix/methods_test.go
@@ -0,0 +1,400 @@
+package zabbix
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetFullVersion(t *testing.T) {
+	zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":"5.0.12"}`, 200)
+	require.NoError(t, err)
+
+	version, err := zabbixClient.GetFullVersion(context.Background())
+	assert.NoError(t, err)
+	assert.Equal(t, "5.0.12", version)
+}
+
+func TestGetFullVersionInvalidPayload(t *testing.T) {
+	zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":{"version":"5.0.12"}}`, 200)
+	require.NoError(t, err)
+
+	version, err := zabbixClient.GetFullVersion(context.Background())
+	assert.Error(t, err)
+	assert.Equal(t, "", version)
+}
+
+func TestGetVersion(t *testing.T) {
+	zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":"5.4.3"}`, 200)
+	require.NoError(t, err)
+
+	version, err := zabbixClient.GetVersion(context.Background())
+	assert.NoError(t, err)
+	assert.Equal(t, 54, version)
+}
+
+func TestGetVersionReturnsError(t *testing.T) {
+	zabbixClient, err := MockZabbixClient(BasicDatasourceInfo, `{"result":"a.b.c"}`, 200)
+	require.NoError(t, err)
+
+	version, err := zabbixClient.GetVersion(context.Background())
+	assert.Error(t, err)
+	assert.Equal(t, 0, version)
+}
+
+func TestGetHistory(t *testing.T) {
+	var historyCalls []int
+	client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string {
+		if payload.Method == "history.get" {
+			historyCalls = append(historyCalls, int(payload.Params["history"].(float64)))
+			return `{"result":[{"itemid":"1","clock":"1","value":"1.2","ns":"0"}]}`
+		}
+		return `{"result":null}`
+	})
+
+	items := []*Item{
+		{ID: "10", ValueType: 0},
+		{ID: "20", ValueType: 3},
+	}
+	tr := backend.TimeRange{
+		From: time.Unix(0, 0),
+		To:   time.Unix(10, 0),
+	}
+
+	history, err := client.GetHistory(context.Background(), items, tr)
+	require.NoError(t, err)
+	assert.Len(t, history, 2)
+	assert.ElementsMatch(t, []int{0, 3}, historyCalls)
+}
+
+func TestGetTrend(t *testing.T) {
+	var capturedIDs []string
+	client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string {
+		if payload.Method == "trend.get" {
+			for _, raw := range payload.Params["itemids"].([]interface{}) {
+				capturedIDs = append(capturedIDs, raw.(string))
+			}
+			return `{"result":[{"itemid":"1","clock":"1","value_min":"0","value_avg":"1","value_max":"2"}]}`
+		}
+		return `{"result":null}`
+	})
+
+	items := []*Item{{ID: "100"}, {ID: "200"}}
+	tr := backend.TimeRange{
+		From: time.Unix(0, 0),
+		To:   time.Unix(10, 0),
+	}
+
+	trend, err := client.GetTrend(context.Background(), items, tr)
+	require.NoError(t, err)
+	assert.Len(t, trend, 1)
+	assert.ElementsMatch(t, []string{"100", "200"}, capturedIDs)
+}
+
+func TestGetItems(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`,
+		"host.get":      `{"result":[{"hostid":"10","name":"web01"}]}`,
+		"item.get": `{
+			"result":[
+				{"itemid":"100","name":"CPU usage"},
+				{"itemid":"200","name":"Memory usage"}
+			]
+		}`,
+	})
+
+	items, err := client.GetItems(context.Background(), "Servers", "web01", "", "/CPU/", "num", false)
+	require.NoError(t, err)
+	if assert.Len(t, items, 1) {
+		assert.Equal(t, "100", items[0].ID)
+	}
+}
+
+func TestGetItemsBefore54(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`,
+		"host.get":      `{"result":[{"hostid":"10","name":"web01"}]}`,
+		"application.get": `{
+			"result":[
+				{"applicationid":"50","name":"Databases"},
+				{"applicationid":"60","name":"Apps"}
+			]
+		}`,
+		"item.get": `{
+			"result":[
+				{"itemid":"500","name":"DB Size"},
+				{"itemid":"600","name":"API latency"}
+			]
+		}`,
+	})
+
+	items, err := client.GetItemsBefore54(context.Background(), "Servers", "web01", "Databases", "/DB/", "num", false)
+	require.NoError(t, err)
+	if assert.Len(t, items, 1) {
+		assert.Equal(t, "500", items[0].ID)
+	}
+}
+
+func TestFilterItemsByQuery(t *testing.T) {
+	items := []*Item{
+		{Name: "CPU usage"},
+		{Name: "Memory usage"},
+	}
+
+	filtered, err := filterItemsByQuery(items, "/CPU/")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "CPU usage", filtered[0].Name)
+
+	filtered, err = filterItemsByQuery(items, "Memory usage")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "Memory usage", filtered[0].Name)
+}
+
+func TestGetApps(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`,
+		"host.get":      `{"result":[{"hostid":"10","name":"web01"}]}`,
+		"application.get": `{
+			"result":[
+				{"applicationid":"50","name":"API"},
+				{"applicationid":"60","name":"DB"}
+			]
+		}`,
+	})
+
+	apps, err := client.GetApps(context.Background(), "Servers", "web01", "/^API/")
+	require.NoError(t, err)
+	if assert.Len(t, apps, 1) {
+		assert.Equal(t, "50", apps[0].ID)
+	}
+}
+
+func TestFilterAppsByQuery(t *testing.T) {
+	apps := []Application{
+		{Name: "API"},
+		{Name: "DB"},
+	}
+
+	filtered, err := filterAppsByQuery(apps, "/^A/")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "API", filtered[0].Name)
+
+	filtered, err = filterAppsByQuery(apps, "DB")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "DB", filtered[0].Name)
+}
+
+func TestGetItemTags(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`,
+		"host.get":      `{"result":[{"hostid":"10","name":"web01"}]}`,
+		"item.get": `{
+			"result":[
+				{"itemid":"1","name":"CPU","tags":[{"tag":"Env","value":"prod"},{"tag":"Application","value":"api"}]},
+				{"itemid":"2","name":"Mem","tags":[{"tag":"Env","value":"stage"},{"tag":"Env","value":"prod"}]}
+			]
+		}`,
+	})
+
+	tags, err := client.GetItemTags(context.Background(), "Servers", "web01", "/^Env/")
+	require.NoError(t, err)
+	assert.ElementsMatch(t, []ItemTag{
+		{Tag: "Env", Value: "prod"},
+		{Tag: "Env", Value: "stage"},
+	}, tags)
+}
+
+func TestFilterTags(t *testing.T) {
+	tags := []ItemTag{
+		{Tag: "Env", Value: "prod"},
+		{Tag: "Application", Value: "api"},
+	}
+
+	filtered, err := filterTags(tags, "/^Env/")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "Env", filtered[0].Tag)
+
+	filtered, err = filterTags(tags, "Application: api")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "Application", filtered[0].Tag)
+}
+
+func TestGetHosts(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`,
+		"host.get": `{
+			"result":[
+				{"hostid":"10","name":"web01"},
+				{"hostid":"20","name":"db01"}
+			]
+		}`,
+	})
+
+	hosts, err := client.GetHosts(context.Background(), "Servers", "/web/")
+	require.NoError(t, err)
+	if assert.Len(t, hosts, 1) {
+		assert.Equal(t, "web01", hosts[0].Name)
+	}
+}
+
+func TestFilterHostsByQuery(t *testing.T) {
+	hosts := []Host{
+		{Name: "web01"},
+		{Name: "db01"},
+	}
+
+	filtered, err := filterHostsByQuery(hosts, "/^web/")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "web01", filtered[0].Name)
+
+	filtered, err = filterHostsByQuery(hosts, "db01")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "db01", filtered[0].Name)
+}
+
+func TestGetGroups(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"},{"groupid":"2","name":"Apps"}]}`,
+	})
+
+	groups, err := client.GetGroups(context.Background(), "/Apps/")
+	require.NoError(t, err)
+	if assert.Len(t, groups, 1) {
+		assert.Equal(t, "Apps", groups[0].Name)
+	}
+}
+
+func TestFilterGroupsByQuery(t *testing.T) {
+	groups := []Group{
+		{Name: "Servers"},
+		{Name: "Apps"},
+	}
+
+	filtered, err := filterGroupsByQuery(groups, "/Apps/")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "Apps", filtered[0].Name)
+
+	filtered, err = filterGroupsByQuery(groups, "Servers")
+	require.NoError(t, err)
+	assert.Len(t, filtered, 1)
+	assert.Equal(t, "Servers", filtered[0].Name)
+}
+
+func TestGetAllItemsBuildsParams(t *testing.T) {
+	var lastRequest ApiRequestPayload
+	client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string {
+		if payload.Method == "item.get" {
+			lastRequest = payload
+			return `{"result":[{"itemid":"1","name":"CPU $1","key_":"system.cpu[user]","value_type":"0","hosts":[{"hostid":"10","name":"web"}]}]}`
+		}
+		return `{"result":null}`
+	})
+
+	items, err := client.GetAllItems(
+		context.Background(),
+		[]string{"10"},
+		nil,
+		"num",
+		false,
+		"Env: prod, Application: api",
+	)
+	require.NoError(t, err)
+	if assert.Len(t, items, 1) {
+		assert.Equal(t, "CPU user", items[0].Name)
+	}
+
+	require.Equal(t, "item.get", lastRequest.Method)
+	params := lastRequest.Params
+	assert.Equal(t, []interface{}{"10"}, params["hostids"])
+	assert.Equal(t, true, params["monitored"])
+	filter := params["filter"].(map[string]interface{})
+	assert.ElementsMatch(t, []interface{}{float64(0), float64(3)}, filter["value_type"].([]interface{}))
+	tags := params["tags"].([]interface{})
+	if assert.Len(t, tags, 2) {
+		first := tags[0].(map[string]interface{})
+		second := tags[1].(map[string]interface{})
+		assert.Equal(t, "Application", first["tag"])
+		assert.Equal(t, "Env", second["tag"])
+	}
+}
+
+func TestGetItemsByIDs(t *testing.T) {
+	var lastRequest ApiRequestPayload
+	client := NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string {
+		if payload.Method == "item.get" {
+			lastRequest = payload
+			return `{"result":[{"itemid":"1","name":"CPU"}]}`
+		}
+		return `{"result":null}`
+	})
+
+	items, err := client.GetItemsByIDs(context.Background(), []string{"1"})
+	require.NoError(t, err)
+	assert.Len(t, items, 1)
+	assert.Equal(t, []interface{}{"1"}, lastRequest.Params["itemids"].([]interface{}))
+}
+
+func TestGetAllApps(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"application.get": `{"result":[{"applicationid":"10","name":"API"}]}`,
+	})
+
+	apps, err := client.GetAllApps(context.Background(), []string{"1"})
+	require.NoError(t, err)
+	assert.Len(t, apps, 1)
+	assert.Equal(t, "API", apps[0].Name)
+}
+
+func TestGetAllHosts(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"host.get": `{"result":[{"hostid":"10","name":"web01"}]}`,
+	})
+
+	hosts, err := client.GetAllHosts(context.Background(), []string{"1"})
+	require.NoError(t, err)
+	assert.Len(t, hosts, 1)
+	assert.Equal(t, "web01", hosts[0].Name)
+}
+
+func TestGetAllGroups(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"hostgroup.get": `{"result":[{"groupid":"1","name":"Servers"}]}`,
+	})
+
+	groups, err := client.GetAllGroups(context.Background())
+	require.NoError(t, err)
+	assert.Len(t, groups, 1)
+	assert.Equal(t, "Servers", groups[0].Name)
+}
+
+func TestGetValueMappings(t *testing.T) {
+	client := NewZabbixClientWithResponses(t, map[string]string{
+		"valuemap.get": `{
+			"result":[
+				{
+					"valuemapid":"1",
+					"name":"Status",
+					"mappings":[{"value":"0","newvalue":"down"}]
+				}
+			]
+		}`,
+	})
+
+	valueMaps, err := client.GetValueMappings(context.Background())
+	require.NoError(t, err)
+	assert.Len(t, valueMaps, 1)
+	assert.Equal(t, "Status", valueMaps[0].Name)
+}
diff --git a/pkg/zabbix/testing.go b/pkg/zabbix/testing.go
index f5e0d83..3210059 100644
--- a/pkg/zabbix/testing.go
+++ b/pkg/zabbix/testing.go
@@ -1,12 +1,31 @@
 package zabbix
 
 import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
 	"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
 	"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi"
 	"github.com/grafana/grafana-plugin-sdk-go/backend"
-	"time"
+	"github.com/stretchr/testify/require"
 )
 
+var BasicDatasourceInfo = &backend.DataSourceInstanceSettings{
+	ID:       1,
+	Name:     "TestDatasource",
+	URL:      "http://zabbix.org/zabbix",
+	JSONData: []byte(`{"username":"username", "password":"password", "cacheTTL":"10m"}`),
+}
+
+type ApiRequestPayload struct {
+	Method string                 `json:"method"`
+	Params map[string]interface{} `json:"params"`
+}
+
 func MockZabbixClient(dsInfo *backend.DataSourceInstanceSettings, body string, statusCode int) (*Zabbix, error) {
 	zabbixAPI, err := zabbixapi.MockZabbixAPI(body, statusCode)
 	if err != nil {
@@ -34,3 +53,50 @@ func MockZabbixClientResponse(client *Zabbix, body string, statusCode int) (*Zab
 
 	return client, nil
 }
+
+func NewZabbixClientWithHandler(t *testing.T, handler func(ApiRequestPayload) string) *Zabbix {
+	t.Helper()
+
+	httpClient := zabbixapi.NewTestClient(func(req *http.Request) *http.Response {
+		body, err := io.ReadAll(req.Body)
+		require.NoError(t, err)
+
+		var payload ApiRequestPayload
+		require.NoError(t, json.Unmarshal(body, &payload))
+
+		responseBody := handler(payload)
+		if responseBody == "" {
+			responseBody = `{"result":null}`
+		}
+
+		return &http.Response{
+			StatusCode: 200,
+			Body:       io.NopCloser(strings.NewReader(responseBody)),
+			Header:     make(http.Header),
+		}
+	})
+
+	dsSettings := *BasicDatasourceInfo
+	zabbixAPI, err := zabbixapi.New(dsSettings, httpClient)
+	require.NoError(t, err)
+
+	zabbixSettings := &settings.ZabbixDatasourceSettings{
+		Timeout: 10 * time.Second,
+	}
+	client, err := New(BasicDatasourceInfo, zabbixSettings, zabbixAPI)
+	require.NoError(t, err)
+
+	client.api.SetAuth("test")
+	client.version = 60
+
+	return client
+}
+
+func NewZabbixClientWithResponses(t *testing.T, responses map[string]string) *Zabbix {
+	return NewZabbixClientWithHandler(t, func(payload ApiRequestPayload) string {
+		if resp, ok := responses[payload.Method]; ok {
+			return resp
+		}
+		return `{"result":null}`
+	})
+}
diff --git a/pkg/zabbix/zabbix_test.go b/pkg/zabbix/zabbix_test.go
index 25cabeb..2091d0e 100644
--- a/pkg/zabbix/zabbix_test.go
+++ b/pkg/zabbix/zabbix_test.go
@@ -7,21 +7,12 @@ import (
 	"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
 
 	"github.com/stretchr/testify/assert"
-
-	"github.com/grafana/grafana-plugin-sdk-go/backend"
 )
 
-var basicDatasourceInfo = &backend.DataSourceInstanceSettings{
-	ID:       1,
-	Name:     "TestDatasource",
-	URL:      "http://zabbix.org/zabbix",
-	JSONData: []byte(`{"username":"username", "password":"password", "cacheTTL":"10m"}`),
-}
-
 var emptyParams = map[string]interface{}{}
 
 func TestLogin(t *testing.T) {
-	zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"secretauth"}`, 200)
+	zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"secretauth"}`, 200)
 	err := zabbixClient.Authenticate(context.Background())
 
 	assert.NoError(t, err)
@@ -29,7 +20,7 @@ func TestLogin(t *testing.T) {
 }
 
 func TestLoginError(t *testing.T) {
-	zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":""}`, 500)
+	zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":""}`, 500)
 	err := zabbixClient.Authenticate(context.Background())
 
 	assert.Error(t, err)
@@ -37,7 +28,7 @@ func TestLoginError(t *testing.T) {
 }
 
 func TestZabbixAPIQuery(t *testing.T) {
-	zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"test"}`, 200)
+	zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"test"}`, 200)
 	resp, err := zabbixClient.Request(context.Background(), &ZabbixAPIRequest{Method: "test.get", Params: emptyParams})
 
 	assert.NoError(t, err)
@@ -50,7 +41,7 @@ func TestZabbixAPIQuery(t *testing.T) {
 func TestCachedQuery(t *testing.T) {
 	// Using methods with caching enabled
 	query := &ZabbixAPIRequest{Method: "host.get", Params: emptyParams}
-	zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"testOld"}`, 200)
+	zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"testOld"}`, 200)
 
 	// Run query first time
 	resp, err := zabbixClient.Request(context.Background(), query)
@@ -72,7 +63,7 @@ func TestCachedQuery(t *testing.T) {
 func TestNonCachedQuery(t *testing.T) {
 	// Using methods with caching disabled
 	query := &ZabbixAPIRequest{Method: "history.get", Params: emptyParams}
-	zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"testOld"}`, 200)
+	zabbixClient, _ := MockZabbixClient(BasicDatasourceInfo, `{"result":"testOld"}`, 200)
 
 	// Run query first time
 	resp, err := zabbixClient.Request(context.Background(), query)
@@ -93,7 +84,7 @@ func TestNonCachedQuery(t *testing.T) {
 
 func TestItemTagCache(t *testing.T) {
 	zabbixClient, _ := MockZabbixClient(
-		basicDatasourceInfo,
+		BasicDatasourceInfo,
 		`{"result":[{"itemid":"1","name":"test1"}]}`,
 		200,
 	)