PiHoleV6

PiHole V6 integration using the new REST API. PiHole is a network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole.

IT Services · PiHole

Details

IDPiHoleV6
ProviderOpen Source
CategoryIT Services
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

PiHole v6 integration using the new REST API. PiHole is a network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole and optionally a DHCP server, intended for use on a private network.
This integration was integrated and tested with PiHole v6.0 API.

Configure PiHole v6 in Cortex

Parameter Description Required
url Server URL (e.g. https://192.168.1.25) True
password PiHole admin password for API authentication True
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

pihole-get-summary


Get overview of PiHole activity including query stats, client counts, and gravity info.

Base Command

pihole-get-summary

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Summary.queries Unknown Query statistics including total, blocked, cached, forwarded counts.
PiHoleV6.Summary.clients Unknown Client statistics.
PiHoleV6.Summary.gravity Unknown Gravity list statistics.

pihole-get-top-domains


Get top permitted or blocked domains.

Base Command

pihole-get-top-domains

Input

Argument Name Description Required
count Number of top domains to return. Default is 10. Optional
blocked If true, return top blocked domains instead of top permitted domains. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
PiHoleV6.TopDomains.domains Unknown Array of top domains with counts.
PiHoleV6.TopDomains.total_queries Number Total number of queries.

pihole-get-top-clients


Get top clients by query count.

Base Command

pihole-get-top-clients

Input

Argument Name Description Required
count Number of top clients to return. Default is 10. Optional
blocked If true, return top blocked clients instead of top active clients. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
PiHoleV6.TopClients.clients Unknown Array of top clients with counts.
PiHoleV6.TopClients.total_queries Number Total number of queries.

pihole-get-upstreams


Get metrics about upstream DNS destinations.

Base Command

pihole-get-upstreams

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Upstreams.upstreams Unknown Array of upstream destinations with query counts and response times.
PiHoleV6.Upstreams.forwarded_queries Number Number of forwarded queries.

pihole-get-query-types


Get breakdown of DNS query types.

Base Command

pihole-get-query-types

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.QueryTypes.types Unknown Query type counts (A, AAAA, MX, etc).

pihole-get-recent-blocked


Get most recently blocked domain(s).

Base Command

pihole-get-recent-blocked

Input

Argument Name Description Required
count Number of recently blocked domains to return. Default is 1. Optional

Context Output

Path Type Description
PiHoleV6.RecentBlocked.blocked Unknown List of recently blocked domains.

pihole-get-history


Get activity graph data (queries over time).

Base Command

pihole-get-history

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.History.history Unknown Activity over time data with timestamps, total, cached, blocked, forwarded counts.

pihole-get-history-clients


Get per-client activity graph data.

Base Command

pihole-get-history-clients

Input

Argument Name Description Required
N Maximum number of clients to return. 0 returns all. Default is 20. Optional

Context Output

Path Type Description
PiHoleV6.HistoryClients.clients Unknown Client information with query counts.
PiHoleV6.HistoryClients.history Unknown Per-client activity over time.

pihole-get-queries


Get DNS query log with optional filtering.

Base Command

pihole-get-queries

Input

Argument Name Description Required
length Number of queries to return. Default is 100. Optional
domain Filter by domain (wildcards supported). Optional
client_ip Filter by client IP (wildcards supported). Optional
client_name Filter by client hostname (wildcards supported). Optional
upstream Filter by upstream server. Optional
type Filter by query type (A, AAAA, etc). Optional
status Filter by status (GRAVITY, FORWARDED, CACHE, etc). Optional

Context Output

Path Type Description
PiHoleV6.Queries.queries Unknown Array of DNS query records.
PiHoleV6.Queries.recordsTotal Number Total number of available queries.

pihole-get-blocking-status


Get current PiHole blocking status.

Base Command

pihole-get-blocking-status

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Blocking.blocking String Current blocking status (enabled/disabled).
PiHoleV6.Blocking.timer Number Remaining seconds until blocking mode changes automatically. Null if permanent.

pihole-set-blocking


Enable or disable PiHole blocking, optionally with a timer.

Base Command

pihole-set-blocking

Input

Argument Name Description Required
blocking Enable or disable blocking. Possible values are: true, false. Required
timer Seconds until blocking mode automatically reverts. Leave empty for permanent change. Optional

Context Output

Path Type Description
PiHoleV6.Blocking.blocking String New blocking status.
PiHoleV6.Blocking.timer Number Timer value if set.

pihole-get-domains


Get domains from allow/deny lists. Filter by type and kind.

Base Command

pihole-get-domains

Input

Argument Name Description Required
type Domain type filter. Possible values are: allow, deny. Optional
kind Domain kind filter. Possible values are: exact, regex. Optional

Context Output

Path Type Description
PiHoleV6.Domains.domains Unknown Array of domain entries.

pihole-add-domain


Add a domain to allow/deny list.

Base Command

pihole-add-domain

Input

Argument Name Description Required
type Domain type. Possible values are: allow, deny. Required
kind Domain kind. Possible values are: exact, regex. Required
domain Domain to add. Required
comment Optional comment. Optional

Context Output

Path Type Description
PiHoleV6.Domains.domains Unknown Added domain details.

pihole-delete-domain


Delete a domain from allow/deny list.

Base Command

pihole-delete-domain

Input

Argument Name Description Required
type Domain type. Possible values are: allow, deny. Required
kind Domain kind. Possible values are: exact, regex. Required
domain Domain to delete. Required

Context Output

There is no context output for this command.

pihole-get-version


Get PiHole component versions.

Base Command

pihole-get-version

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Version Unknown Version information for all PiHole components.

pihole-get-system-info


Get system information from PiHole host.

Base Command

pihole-get-system-info

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.SystemInfo Unknown System information (CPU, memory, disk, uptime, etc).

pihole-get-ftl-info


Get FTL (Faster Than Light) engine information.

Base Command

pihole-get-ftl-info

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.FTLInfo Unknown FTL engine information.

pihole-get-host-info


Get host information.

Base Command

pihole-get-host-info

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.HostInfo Unknown Host information.

pihole-get-sensors


Get sensor information from PiHole host.

Base Command

pihole-get-sensors

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Sensors Unknown Sensor readings (temperature, etc).

pihole-run-gravity


Update PiHole gravity (pull latest adlists).

Base Command

pihole-run-gravity

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Gravity.output String Gravity update output.

pihole-restart-dns


Restart the pihole-FTL DNS service.

Base Command

pihole-restart-dns

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.RestartDNS Unknown Restart result.

pihole-flush-logs


Flush DNS logs and purge last 24 hours from database.

Base Command

pihole-flush-logs

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.FlushLogs Unknown Flush result.

pihole-flush-network


Flush the network table (remove all known devices and addresses).

Base Command

pihole-flush-network

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.FlushNetwork Unknown Flush result.

pihole-get-network-devices


Get network devices known to PiHole.

Base Command

pihole-get-network-devices

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.NetworkDevices Unknown Network device information.

pihole-get-network-gateway


Get network gateway information.

Base Command

pihole-get-network-gateway

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Gateway Unknown Gateway information.

pihole-search-domain


Search for a domain across all PiHole lists and configuration.

Base Command

pihole-search-domain

Input

Argument Name Description Required
domain Domain to search for. Required

Context Output

Path Type Description
PiHoleV6.Search Unknown Search results showing where the domain appears in PiHole configuration.

pihole-get-dhcp-leases


Get current DHCP leases.

Base Command

pihole-get-dhcp-leases

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.DHCPLeases Unknown DHCP lease information.

pihole-get-groups


Get all groups.

Base Command

pihole-get-groups

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Groups Unknown Group information.

pihole-add-group


Create a new group.

Base Command

pihole-add-group

Input

Argument Name Description Required
name Group name. Required
comment Optional comment. Optional

Context Output

Path Type Description
PiHoleV6.Groups Unknown Created group details.

pihole-delete-group


Delete a group.

Base Command

pihole-delete-group

Input

Argument Name Description Required
name Group name to delete. Required

Context Output

There is no context output for this command.

pihole-get-lists


Get all configured adlists.

Base Command

pihole-get-lists

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
PiHoleV6.Lists Unknown Adlist information.

pihole-add-list


Add a new adlist.

Base Command

pihole-add-list

Input

Argument Name Description Required
address URL of the adlist to add. Required
comment Optional comment. Optional
enabled Whether the list should be enabled. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
PiHoleV6.Lists Unknown Added list details.

pihole-delete-list


Delete an adlist.

Base Command

pihole-delete-list

Input

Argument Name Description Required
address URL of the adlist to delete. Required

Context Output

There is no context output for this command.

Configuration parameters

  • url — Server URL (required)
  • password — Password (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (33)

  • pihole-add-domain

    Add a domain to allow/deny list.

  • pihole-add-group

    Create a new group.

  • pihole-add-list

    Add a new adlist.

  • pihole-delete-domain

    Delete a domain from allow/deny list.

  • pihole-delete-group

    Delete a group.

  • pihole-delete-list

    Delete an adlist.

  • pihole-flush-logs

    Flush DNS logs and purge last 24 hours from database.

  • pihole-flush-network

    Flush the network table (remove all known devices and addresses).

  • pihole-get-blocking-status

    Get current PiHole blocking status.

  • pihole-get-dhcp-leases

    Get current DHCP leases.

  • pihole-get-domains

    Get domains from allow/deny lists. Filter by type and kind.

  • pihole-get-ftl-info

    Get FTL (Faster Than Light) engine information.

  • pihole-get-groups

    Get all groups.

  • pihole-get-history

    Get activity graph data (queries over time).

  • pihole-get-history-clients

    Get per-client activity graph data.

  • pihole-get-host-info

    Get host information.

  • pihole-get-lists

    Get all configured adlists.

  • pihole-get-network-devices

    Get network devices known to PiHole.

  • pihole-get-network-gateway

    Get network gateway information.

  • pihole-get-queries

    Get DNS query log with optional filtering.

  • pihole-get-query-types

    Get breakdown of DNS query types.

  • pihole-get-recent-blocked

    Get most recently blocked domain(s).

  • pihole-get-sensors

    Get sensor information from PiHole host.

  • pihole-get-summary

    Get overview of PiHole activity including query stats, client counts, and gravity info.

  • pihole-get-system-info

    Get system information from PiHole host.

  • pihole-get-top-clients

    Get top clients by query count.

  • pihole-get-top-domains

    Get top permitted or blocked domains.

  • pihole-get-upstreams

    Get metrics about upstream DNS destinations.

  • pihole-get-version

    Get PiHole component versions.

  • pihole-restart-dns

    Restart the pihole-FTL DNS service.

  • pihole-run-gravity

    Update PiHole gravity (pull latest adlists).

  • pihole-search-domain

    Search for a domain across all PiHole lists and configuration.

  • pihole-set-blocking

    Enable or disable PiHole blocking, optionally with a timer.

"""PiHole V6 Integration - Unit Tests

Pytest Unit Tests: all function names must start with "test_"

Uses demistomock and mocker for mocking API calls.
"""

import pytest
from CommonServerPython import DemistoException
from PiHoleV6 import (
    PiHoleV6Client,
    test_module,
    get_summary_command,
    get_top_domains_command,
    get_top_clients_command,
    get_upstreams_command,
    get_query_types_command,
    get_recent_blocked_command,
    get_history_command,
    get_history_clients_command,
    get_queries_command,
    get_blocking_status_command,
    set_blocking_command,
    get_domains_command,
    add_domain_command,
    delete_domain_command,
    get_version_command,
    get_system_info_command,
    get_ftl_info_command,
    get_host_info_command,
    get_sensors_command,
    run_gravity_command,
    restart_dns_command,
    flush_logs_command,
    flush_network_command,
    get_network_devices_command,
    get_network_gateway_command,
    search_domain_command,
    get_dhcp_leases_command,
    get_groups_command,
    add_group_command,
    delete_group_command,
    get_lists_command,
    add_list_command,
    delete_list_command,
)


BASE_URL = "https://pihole.example.com/api"

AUTH_RESPONSE = {"session": {"valid": True, "sid": "test-session-id"}}


@pytest.fixture
def client(mocker) -> PiHoleV6Client:
    """Fixture to create a PiHoleV6Client instance with mocked authentication."""
    c = PiHoleV6Client(base_url=BASE_URL, password="testpass", verify=False, proxy=False)
    mocker.patch.object(c, "authenticate")
    c.sid = "test-session-id"
    return c


def test_test_module(mocker):
    """
    Given:
        - A PiHoleV6 client with valid credentials.
    When:
        - Running the test-module command.
    Then:
        - Should return 'ok' after successful authentication and logout.
    """
    client = PiHoleV6Client(base_url=BASE_URL, password="testpass", verify=False, proxy=False)
    mocker.patch.object(client, "authenticate")
    mocker.patch.object(client, "logout")
    result = test_module(client)
    assert result == "ok"
    client.authenticate.assert_called_once()
    client.logout.assert_called_once()


def test_authenticate_success(mocker):
    """
    Given:
        - Valid PiHole credentials.
    When:
        - Authenticating with the API.
    Then:
        - Should set the session ID.
    """
    client = PiHoleV6Client(base_url=BASE_URL, password="testpass", verify=False, proxy=False)
    mocker.patch.object(client, "_http_request", return_value=AUTH_RESPONSE)
    client.authenticate()
    assert client.sid == "test-session-id"


def test_authenticate_failure(mocker):
    """
    Given:
        - Invalid PiHole credentials.
    When:
        - Authenticating with the API.
    Then:
        - Should raise a DemistoException.
    """
    client = PiHoleV6Client(base_url=BASE_URL, password="wrong", verify=False, proxy=False)
    mocker.patch.object(client, "_http_request", return_value={"session": {"valid": False, "message": "Invalid password"}})
    with pytest.raises(DemistoException, match="Authentication failed"):
        client.authenticate()


def test_get_summary_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-summary command.
    Then:
        - Should return summary data with queries and gravity info.
    """
    mock_data = {
        "queries": {"total": 1000, "blocked": 200, "cached": 300, "forwarded": 500},
        "clients": {"total": 10, "active": 5},
        "gravity": {"domains_being_blocked": 50000},
    }
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_summary_command(client)
    results.assert_called_once()
    cmd_results = results.call_args[0][0]
    assert cmd_results.outputs["queries"]["total"] == 1000
    assert "Summary" in cmd_results.readable_output


def test_get_top_domains_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with count and blocked arguments.
    When:
        - Running pihole-get-top-domains command.
    Then:
        - Should return top domains data.
    """
    mock_data = {"domains": [{"domain": "example.com", "count": 100}], "total_queries": 1000}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_top_domains_command(client, {"count": "5", "blocked": "false"})
    results.assert_called_once()
    client.api_request.assert_called_with("GET", "/stats/top_domains", params={"count": 5, "blocked": False})


def test_get_top_clients_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-top-clients command.
    Then:
        - Should return top clients data.
    """
    mock_data = {"clients": [{"name": "pc1", "ip": "192.168.1.10", "count": 50}], "total_queries": 500}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_top_clients_command(client, {"count": "10", "blocked": "true"})
    results.assert_called_once()
    client.api_request.assert_called_with("GET", "/stats/top_clients", params={"count": 10, "blocked": True})


def test_get_upstreams_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-upstreams command.
    Then:
        - Should return upstream DNS data.
    """
    mock_data = {"upstreams": [{"ip": "8.8.8.8", "count": 300}], "forwarded_queries": 300}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_upstreams_command(client)
    results.assert_called_once()


def test_get_query_types_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-query-types command.
    Then:
        - Should return query type breakdown.
    """
    mock_data = {"types": [{"name": "A", "count": 500}, {"name": "AAAA", "count": 200}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_query_types_command(client)
    results.assert_called_once()


def test_get_recent_blocked_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with count argument.
    When:
        - Running pihole-get-recent-blocked command.
    Then:
        - Should return recently blocked domains.
    """
    mock_data = {"blocked": [{"domain": "ads.example.com"}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_recent_blocked_command(client, {"count": "5"})
    results.assert_called_once()
    client.api_request.assert_called_with("GET", "/stats/recent_blocked", params={"count": 5})


def test_get_history_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-history command.
    Then:
        - Should return history data.
    """
    mock_data = {"history": [{"timestamp": 1000, "total": 100, "blocked": 10}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_history_command(client)
    results.assert_called_once()


def test_get_history_clients_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with N argument.
    When:
        - Running pihole-get-history-clients command.
    Then:
        - Should return per-client history data.
    """
    mock_data = {"clients": [{"name": "pc1"}], "history": []}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_history_clients_command(client, {"N": "5"})
    results.assert_called_once()
    client.api_request.assert_called_with("GET", "/history/clients", params={"N": 5})


def test_get_queries_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with filter arguments.
    When:
        - Running pihole-get-queries command.
    Then:
        - Should return filtered query log.
    """
    mock_data = {"queries": [{"domain": "example.com", "type": "A"}], "recordsTotal": 1}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_queries_command(client, {"length": "10", "domain": "example.com"})
    results.assert_called_once()
    client.api_request.assert_called_with("GET", "/queries", params={"length": 10, "domain": "example.com"})


def test_get_blocking_status_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-blocking-status command.
    Then:
        - Should return current blocking status.
    """
    mock_data = {"blocking": "enabled", "timer": None}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_blocking_status_command(client)
    results.assert_called_once()
    cmd_results = results.call_args[0][0]
    assert cmd_results.outputs["blocking"] == "enabled"
    assert "Blocking Status" in cmd_results.readable_output


def test_set_blocking_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with blocking and timer arguments.
    When:
        - Running pihole-set-blocking command.
    Then:
        - Should set blocking status and return result.
    """
    mock_data = {"blocking": "enabled", "timer": 60}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    set_blocking_command(client, {"blocking": "true", "timer": "60"})
    results.assert_called_once()
    client.api_request.assert_called_with("POST", "/dns/blocking", json_data={"blocking": True, "timer": 60})


def test_set_blocking_permanent(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with blocking but no timer.
    When:
        - Running pihole-set-blocking without a timer.
    Then:
        - Should set blocking permanently (timer=None).
    """
    mock_data = {"blocking": "disabled", "timer": None}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    mocker.patch("PiHoleV6.return_results")
    set_blocking_command(client, {"blocking": "false"})
    client.api_request.assert_called_with("POST", "/dns/blocking", json_data={"blocking": False, "timer": None})


def test_get_domains_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with type and kind filters.
    When:
        - Running pihole-get-domains command.
    Then:
        - Should return filtered domain list.
    """
    mock_data = {"domains": [{"domain": "ads.com", "type": "deny", "kind": "exact"}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_domains_command(client, {"type": "deny", "kind": "exact"})
    results.assert_called_once()
    client.api_request.assert_called_with("GET", "/domains/deny/exact")


def test_get_domains_no_filter(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with no filters.
    When:
        - Running pihole-get-domains command without type/kind.
    Then:
        - Should return all domains.
    """
    mock_data = {"domains": []}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    mocker.patch("PiHoleV6.return_results")
    get_domains_command(client, {})
    client.api_request.assert_called_with("GET", "/domains")


def test_add_domain_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with domain details.
    When:
        - Running pihole-add-domain command.
    Then:
        - Should add the domain and return result.
    """
    mock_data = {"domains": [{"domain": "ads.com", "comment": "block ads"}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    add_domain_command(client, {"type": "deny", "kind": "exact", "domain": "ads.com", "comment": "block ads"})
    results.assert_called_once()
    client.api_request.assert_called_with("POST", "/domains/deny/exact", json_data={"domain": "ads.com", "comment": "block ads"})


def test_delete_domain_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with a domain to delete.
    When:
        - Running pihole-delete-domain command.
    Then:
        - Should delete the domain and return confirmation.
    """
    mocker.patch.object(client, "api_request")
    results = mocker.patch("PiHoleV6.return_results")
    delete_domain_command(client, {"type": "deny", "kind": "exact", "domain": "ads.com"})
    results.assert_called_once()
    cmd_results = results.call_args[0][0]
    assert "ads.com" in cmd_results.readable_output
    assert "deleted" in cmd_results.readable_output


def test_get_version_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-version command.
    Then:
        - Should return version info for all components.
    """
    mock_data = {
        "core": {"local": {"version": "v6.0"}},
        "web": {"local": {"version": "v6.0"}},
        "ftl": {"local": {"version": "v6.0"}},
    }
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_version_command(client)
    results.assert_called_once()
    cmd_results = results.call_args[0][0]
    assert "Version" in cmd_results.readable_output


def test_get_system_info_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-system-info command.
    Then:
        - Should return system information.
    """
    mock_data = {"cpu": {"percent": 5.0}, "memory": {"percent": 30.0}}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_system_info_command(client)
    results.assert_called_once()


def test_get_ftl_info_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-ftl-info command.
    Then:
        - Should return FTL engine information.
    """
    mock_data = {"version": "v6.0", "database": {"queries": 50000}}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_ftl_info_command(client)
    results.assert_called_once()


def test_get_host_info_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-host-info command.
    Then:
        - Should return host information.
    """
    mock_data = {"hostname": "pihole", "uname": "Linux"}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_host_info_command(client)
    results.assert_called_once()


def test_get_sensors_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-sensors command.
    Then:
        - Should return sensor readings.
    """
    mock_data = {"cpu_temp": 45.5}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_sensors_command(client)
    results.assert_called_once()


def test_run_gravity_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-run-gravity command.
    Then:
        - Should trigger gravity update and return output.
    """
    mocker.patch.object(client, "api_request", return_value="Gravity update completed")
    results = mocker.patch("PiHoleV6.return_results")
    run_gravity_command(client)
    results.assert_called_once()
    cmd_results = results.call_args[0][0]
    assert "Gravity" in cmd_results.readable_output


def test_restart_dns_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-restart-dns command.
    Then:
        - Should restart DNS and return result.
    """
    mock_data = {"status": "restarted"}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    restart_dns_command(client)
    results.assert_called_once()


def test_flush_logs_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-flush-logs command.
    Then:
        - Should flush logs and return result.
    """
    mock_data = {"status": "flushed"}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    flush_logs_command(client)
    results.assert_called_once()


def test_flush_network_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-flush-network command.
    Then:
        - Should flush network table and return result.
    """
    mock_data = {"status": "flushed"}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    flush_network_command(client)
    results.assert_called_once()


def test_get_network_devices_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-network-devices command.
    Then:
        - Should return network devices.
    """
    mock_data = {"devices": [{"ip": "192.168.1.10", "mac": "AA:BB:CC:DD:EE:FF"}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_network_devices_command(client)
    results.assert_called_once()


def test_get_network_gateway_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-network-gateway command.
    Then:
        - Should return gateway information.
    """
    mock_data = {"ip": "192.168.1.1"}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_network_gateway_command(client)
    results.assert_called_once()


def test_search_domain_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with a domain to search.
    When:
        - Running pihole-search-domain command.
    Then:
        - Should return search results for the domain.
    """
    mock_data = {"results": [{"domain": "example.com", "list": "gravity"}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    search_domain_command(client, {"domain": "example.com"})
    results.assert_called_once()
    client.api_request.assert_called_with("GET", "/search/example.com")


def test_get_dhcp_leases_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-dhcp-leases command.
    Then:
        - Should return DHCP lease information.
    """
    mock_data = {"leases": [{"ip": "192.168.1.100", "mac": "AA:BB:CC:DD:EE:FF", "hostname": "device1"}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_dhcp_leases_command(client)
    results.assert_called_once()


def test_get_groups_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-groups command.
    Then:
        - Should return all groups.
    """
    mock_data = {"groups": [{"name": "default", "enabled": True}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_groups_command(client)
    results.assert_called_once()


def test_add_group_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with group details.
    When:
        - Running pihole-add-group command.
    Then:
        - Should create the group and return result.
    """
    mock_data = {"groups": [{"name": "test-group", "comment": "test"}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    add_group_command(client, {"name": "test-group", "comment": "test"})
    results.assert_called_once()
    client.api_request.assert_called_with("POST", "/groups", json_data={"name": "test-group", "comment": "test"})


def test_delete_group_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with a group to delete.
    When:
        - Running pihole-delete-group command.
    Then:
        - Should delete the group and return confirmation.
    """
    mocker.patch.object(client, "api_request")
    results = mocker.patch("PiHoleV6.return_results")
    delete_group_command(client, {"name": "test-group"})
    results.assert_called_once()
    cmd_results = results.call_args[0][0]
    assert "test-group" in cmd_results.readable_output
    assert "deleted" in cmd_results.readable_output


def test_get_lists_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client.
    When:
        - Running pihole-get-lists command.
    Then:
        - Should return all adlists.
    """
    mock_data = {"lists": [{"address": "https://adlist.example.com", "enabled": True}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    get_lists_command(client)
    results.assert_called_once()


def test_add_list_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with adlist details.
    When:
        - Running pihole-add-list command.
    Then:
        - Should add the adlist and return result.
    """
    mock_data = {"lists": [{"address": "https://adlist.example.com", "enabled": True}]}
    mocker.patch.object(client, "api_request", return_value=mock_data)
    results = mocker.patch("PiHoleV6.return_results")
    add_list_command(client, {"address": "https://adlist.example.com", "comment": "ads", "enabled": "true"})
    results.assert_called_once()
    client.api_request.assert_called_with(
        "POST", "/lists", json_data={"address": "https://adlist.example.com", "comment": "ads", "enabled": True}
    )


def test_delete_list_command(client, mocker):
    """
    Given:
        - A connected PiHoleV6 client with an adlist to delete.
    When:
        - Running pihole-delete-list command.
    Then:
        - Should delete the adlist and return confirmation.
    """
    mocker.patch.object(client, "api_request")
    results = mocker.patch("PiHoleV6.return_results")
    delete_list_command(client, {"address": "https://adlist.example.com"})
    results.assert_called_once()
    cmd_results = results.call_args[0][0]
    assert "adlist.example.com" in cmd_results.readable_output
    assert "deleted" in cmd_results.readable_output