diff options
| author | omnom62 <omnom62@outlook.com> | 2026-05-27 21:38:51 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-05-27 21:38:51 +1000 |
| commit | aeedc73003ddcf84bbf5c2880f982b6d4925e0c9 (patch) | |
| tree | b2bf9705e2fe6db7dbaa51afc5fd60ca0f7878da | |
| parent | 5a6f6976b49fd5ca8e964b2342493bc5ceee85d5 (diff) | |
| download | rest.vyos-aeedc73003ddcf84bbf5c2880f982b6d4925e0c9.tar.gz rest.vyos-aeedc73003ddcf84bbf5c2880f982b6d4925e0c9.zip | |
Test framework
27 files changed, 1714 insertions, 0 deletions
@@ -119,3 +119,5 @@ venv.bak/ changelogs/.plugin-cache.yaml # inventory for testing inventory.network + +tests/output diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..a16a5b0 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,121 @@ +# vyos.rest Test Framework + +Equivalent structure to `vyos.vyos` CLI collection tests, adapted for +the REST API connection model. + +## Structure + +``` +tests/ +├── unit/ +│ ├── modules/ +│ │ ├── base.py # VyOSModuleTestCase base class + load_fixture +│ │ ├── test_vyos_ntp_global.py +│ │ ├── test_vyos_logging_global.py +│ │ ├── test_vyos_prefix_lists.py +│ │ └── test_vyos_route_maps.py +│ └── fixtures/ +│ ├── ntp_global_running.json # Confirmed API responses from device +│ ├── logging_global_running.json +│ ├── prefix_lists_running.json +│ ├── route_maps_running.json +│ ├── snmp_server_running.json +│ └── lldp_global_running.json +└── integration/ + ├── network-integration.cfg + ├── inventory.network # Edit before running + └── targets/ + ├── vyos_ntp_global/tasks/main.yaml + ├── vyos_logging_global/tasks/main.yaml + ├── vyos_prefix_lists/tasks/main.yaml + ├── vyos_route_maps/tasks/main.yaml + ├── vyos_lldp_global/tasks/main.yaml + ├── vyos_snmp_server/tasks/main.yaml + ├── vyos_hostname/tasks/main.yaml + └── vyos_banner/tasks/main.yaml +``` + +## Unit Tests + +Unit tests cover: +- `normalize_config` / `normalize_running` — argspec and API parsing +- `build_commands` diff logic — all states (merged/replaced/overridden/deleted) +- Fixture-based parsing — confirmed API shapes from the actual device + +No device connection needed. Each test imports the module's functions +directly and calls them with mock data or fixture JSON. + +### Run all unit tests + +```bash +# From collection root +ansible-test units tests/unit/modules/ --python 3.12 + +# Or with pytest directly (no ansible-test required) +cd /path/to/ansible_collections/vyos/rest +python -m pytest tests/unit/modules/ -v +``` + +### Run a single module's tests + +```bash +ansible-test units tests/unit/modules/test_vyos_ntp_global.py --python 3.12 +python -m pytest tests/unit/modules/test_vyos_ntp_global.py -v +``` + +## Integration Tests + +Integration tests run against a live VyOS device. Each target: +1. Tears down any existing config for that resource +2. Tests merged (including idempotency) +3. Tests gathered +4. Tests replaced (including idempotency) +5. Tests deleted (including idempotency) +6. Tears down after + +### Prerequisites + +Edit `tests/integration/inventory.network` with your device details. + +### Run all integration tests + +```bash +ansible-test network-integration \ + --inventory tests/integration/inventory.network \ + vyos_* +``` + +### Run a single module's integration test + +```bash +ansible-test network-integration \ + --inventory tests/integration/inventory.network \ + vyos_ntp_global + +# Or with ansible-playbook directly +ansible-playbook \ + -i tests/integration/inventory.network \ + tests/integration/targets/vyos_ntp_global/tasks/main.yaml +``` + +## Adding Tests for a New Module + +1. Add a fixture: `tests/unit/fixtures/<module>_running.json` + - Copy the exact API response from a `curl showConfig` call + - This ensures tests reflect real device behaviour + +2. Add unit tests: `tests/unit/modules/test_<module>.py` + - Subclass `VyOSModuleTestCase` for fixture-based tests + - Test `normalize_*`, `get_running_config`, `build_commands` directly + +3. Add integration target: `tests/integration/targets/<module>/tasks/main.yaml` + - Follow the teardown → merged → idempotent → gathered → replaced → deleted pattern + +## Key Differences from CLI Collection Tests + +| CLI collection | REST collection | +|---|---| +| Mocks `connection.get` returning CLI text | Mocks `vyos.get_config()` returning JSON dict | +| Fixture files are `.cfg` text (CLI output) | Fixture files are `.json` (API response) | +| Tests parse CLI text via `rm_templates` | Tests parse API dicts via `normalize_running` | +| `ansible-test units --docker` for isolation | Can run with `pytest` directly, no docker needed | diff --git a/tests/integration/network-integration.cfg b/tests/integration/network-integration.cfg new file mode 100644 index 0000000..e510865 --- /dev/null +++ b/tests/integration/network-integration.cfg @@ -0,0 +1,3 @@ +[default] +# ansible-test network-integration config +testcase=* diff --git a/tests/integration/targets/vyos_banner/aliases b/tests/integration/targets/vyos_banner/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_banner/aliases @@ -0,0 +1 @@ +network diff --git a/tests/integration/targets/vyos_hostname/aliases b/tests/integration/targets/vyos_hostname/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_hostname/aliases @@ -0,0 +1 @@ +network diff --git a/tests/integration/targets/vyos_lldp_global/aliases b/tests/integration/targets/vyos_lldp_global/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_global/aliases @@ -0,0 +1 @@ +network diff --git a/tests/integration/targets/vyos_logging_global/aliases b/tests/integration/targets/vyos_logging_global/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_logging_global/aliases @@ -0,0 +1 @@ +network diff --git a/tests/integration/targets/vyos_logging_global/tasks/main.yaml b/tests/integration/targets/vyos_logging_global/tasks/main.yaml new file mode 100644 index 0000000..f5a1f31 --- /dev/null +++ b/tests/integration/targets/vyos_logging_global/tasks/main.yaml @@ -0,0 +1,133 @@ +--- +# tests/integration/targets/vyos_logging_global/tasks/main.yaml + +- name: TEARDOWN — delete all syslog config before tests + vyos.rest.vyos_logging_global: + state: deleted + +# ------------------------------------------------------------------ +# MERGED +# ------------------------------------------------------------------ + +- name: MERGED — set initial logging config + register: result + vyos.rest.vyos_logging_global: + config: + console: + facilities: + - facility: local7 + severity: err + hosts: + - hostname: 172.16.0.1 + port: 514 + facilities: + - facility: local7 + severity: all + global_params: + facilities: + - facility: cron + severity: debug + preserve_fqdn: true + state: merged + +- name: ASSERT — merged changed + assert: + that: + - result.changed == true + +- name: MERGED — idempotency check + register: result + vyos.rest.vyos_logging_global: + config: + console: + facilities: + - facility: local7 + severity: err + hosts: + - hostname: 172.16.0.1 + port: 514 + facilities: + - facility: local7 + severity: all + global_params: + facilities: + - facility: cron + severity: debug + preserve_fqdn: true + state: merged + +- name: ASSERT — merged idempotent + assert: + that: + - result.changed == false + - result.commands | length == 0 + +# ------------------------------------------------------------------ +# GATHERED +# ------------------------------------------------------------------ + +- name: GATHERED — read current config + register: result + vyos.rest.vyos_logging_global: + state: gathered + +- name: ASSERT — gathered returns expected data + assert: + that: + - result.gathered is defined + +# ------------------------------------------------------------------ +# REPLACED +# ------------------------------------------------------------------ + +- name: REPLACED — replace with minimal config + register: result + vyos.rest.vyos_logging_global: + config: + console: + facilities: + - facility: local6 + state: replaced + +- name: ASSERT — replaced changed + assert: + that: + - result.changed == true + +- name: REPLACED — idempotency check + register: result + vyos.rest.vyos_logging_global: + config: + console: + facilities: + - facility: local6 + state: replaced + +- name: ASSERT — replaced idempotent + assert: + that: + - result.changed == false + +# ------------------------------------------------------------------ +# DELETED +# ------------------------------------------------------------------ + +- name: DELETED — remove all syslog config + register: result + vyos.rest.vyos_logging_global: + state: deleted + +- name: ASSERT — deleted changed + assert: + that: + - result.changed == true + +- name: DELETED — idempotency check + register: result + vyos.rest.vyos_logging_global: + state: deleted + +- name: ASSERT — deleted idempotent + assert: + that: + - result.changed == false diff --git a/tests/integration/targets/vyos_ntp_global/aliases b/tests/integration/targets/vyos_ntp_global/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_ntp_global/aliases @@ -0,0 +1 @@ +network diff --git a/tests/integration/targets/vyos_ntp_global/tasks/main.yaml b/tests/integration/targets/vyos_ntp_global/tasks/main.yaml new file mode 100644 index 0000000..7c59a7f --- /dev/null +++ b/tests/integration/targets/vyos_ntp_global/tasks/main.yaml @@ -0,0 +1,149 @@ +--- +# tests/integration/targets/vyos_ntp_global/tasks/main.yaml +# Run with: ansible-test network-integration vyos_ntp_global --inventory <inv> + +- name: TEARDOWN — delete all NTP config before tests + vyos.rest.vyos_ntp_global: + state: deleted + +# ------------------------------------------------------------------ +# MERGED +# ------------------------------------------------------------------ + +- name: MERGED — set initial NTP config + register: result + vyos.rest.vyos_ntp_global: + config: + allow_clients: + - 10.6.6.0/24 + listen_addresses: + - 10.1.3.1 + servers: + - server: 203.0.113.0 + options: + - prefer + state: merged + +- name: ASSERT — merged changed + assert: + that: + - result.changed == true + - result.commands | length > 0 + +- name: MERGED — idempotency check + register: result + vyos.rest.vyos_ntp_global: + config: + allow_clients: + - 10.6.6.0/24 + listen_addresses: + - 10.1.3.1 + servers: + - server: 203.0.113.0 + options: + - prefer + state: merged + +- name: ASSERT — merged idempotent + assert: + that: + - result.changed == false + - result.commands | length == 0 + +# ------------------------------------------------------------------ +# GATHERED +# ------------------------------------------------------------------ + +- name: GATHERED — read current config + register: result + vyos.rest.vyos_ntp_global: + state: gathered + +- name: ASSERT — gathered returns correct data + assert: + that: + - "'10.6.6.0/24' in result.gathered.allow_clients" + - "'10.1.3.1' in result.gathered.listen_addresses" + - "'203.0.113.0' in result.gathered.servers" + +# ------------------------------------------------------------------ +# REPLACED +# ------------------------------------------------------------------ + +- name: REPLACED — replace with different config + register: result + vyos.rest.vyos_ntp_global: + config: + allow_clients: + - 10.99.99.0/24 + servers: + - server: time1.vyos.net + state: replaced + +- name: ASSERT — replaced changed + assert: + that: + - result.changed == true + +- name: REPLACED — idempotency check + register: result + vyos.rest.vyos_ntp_global: + config: + allow_clients: + - 10.99.99.0/24 + servers: + - server: time1.vyos.net + state: replaced + +- name: ASSERT — replaced idempotent + assert: + that: + - result.changed == false + +- name: GATHER — verify old allow_client removed + register: result + vyos.rest.vyos_ntp_global: + state: gathered + +- name: ASSERT — old allow_client not present + assert: + that: + - "'10.6.6.0/24' not in result.gathered.allow_clients" + - "'10.99.99.0/24' in result.gathered.allow_clients" + +# ------------------------------------------------------------------ +# DELETED +# ------------------------------------------------------------------ + +- name: DELETED — remove all NTP config + register: result + vyos.rest.vyos_ntp_global: + state: deleted + +- name: ASSERT — deleted changed + assert: + that: + - result.changed == true + +- name: DELETED — idempotency check + register: result + vyos.rest.vyos_ntp_global: + state: deleted + +- name: ASSERT — deleted idempotent + assert: + that: + - result.changed == false + +# ------------------------------------------------------------------ +# TEARDOWN +# ------------------------------------------------------------------ + +- name: TEARDOWN — restore default NTP servers + vyos.rest.vyos_ntp_global: + config: + servers: + - server: time1.vyos.net + - server: time2.vyos.net + - server: time3.vyos.net + state: merged diff --git a/tests/integration/targets/vyos_prefix_lists/aliases b/tests/integration/targets/vyos_prefix_lists/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/aliases @@ -0,0 +1 @@ +network diff --git a/tests/integration/targets/vyos_prefix_lists/tasks/main.yaml b/tests/integration/targets/vyos_prefix_lists/tasks/main.yaml new file mode 100644 index 0000000..e5c62cd --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tasks/main.yaml @@ -0,0 +1,144 @@ +--- +# tests/integration/targets/vyos_prefix_lists/tasks/main.yaml + +- name: TEARDOWN — delete all prefix lists before tests + vyos.rest.vyos_prefix_lists: + state: deleted + +# ------------------------------------------------------------------ +# MERGED +# ------------------------------------------------------------------ + +- name: MERGED — create prefix lists + register: result + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + description: PL configured by ansible + entries: + - sequence: 2 + action: permit + prefix: 92.168.10.0/26 + le: 32 + - sequence: 3 + action: deny + prefix: 72.168.2.0/24 + ge: 26 + - afi: ipv6 + prefix_lists: + - name: AllowIPv6Prefix + description: Configured by ansible for allowing IPv6 networks + entries: + - sequence: 5 + action: permit + prefix: 2001:db8:8000::/35 + le: 37 + state: merged + +- name: ASSERT — merged changed + assert: + that: + - result.changed == true + +- name: MERGED — idempotency check + register: result + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + description: PL configured by ansible + entries: + - sequence: 2 + action: permit + prefix: 92.168.10.0/26 + le: 32 + - sequence: 3 + action: deny + prefix: 72.168.2.0/24 + ge: 26 + state: merged + +- name: ASSERT — merged idempotent + assert: + that: + - result.changed == false + - result.commands | length == 0 + +# ------------------------------------------------------------------ +# GATHERED +# ------------------------------------------------------------------ + +- name: GATHERED — read current prefix lists + register: result + vyos.rest.vyos_prefix_lists: + state: gathered + +- name: ASSERT — gathered has ipv4 entry + assert: + that: + - result.gathered | selectattr('afi','eq','ipv4') | list | length > 0 + +# ------------------------------------------------------------------ +# REPLACED +# ------------------------------------------------------------------ + +- name: REPLACED — replace AnsibleIPv4PrefixList + register: result + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + entries: + - sequence: 10 + action: permit + prefix: 10.0.0.0/8 + state: replaced + +- name: ASSERT — replaced changed + assert: + that: + - result.changed == true + +- name: REPLACED — idempotency check + register: result + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + entries: + - sequence: 10 + action: permit + prefix: 10.0.0.0/8 + state: replaced + +- name: ASSERT — replaced idempotent + assert: + that: + - result.changed == false + +# ------------------------------------------------------------------ +# DELETED +# ------------------------------------------------------------------ + +- name: DELETED — remove specific prefix list + register: result + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + state: deleted + +- name: ASSERT — deleted changed + assert: + that: + - result.changed == true + +- name: DELETED — remove all remaining + vyos.rest.vyos_prefix_lists: + state: deleted diff --git a/tests/integration/targets/vyos_route_maps/aliases b/tests/integration/targets/vyos_route_maps/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_route_maps/aliases @@ -0,0 +1 @@ +network diff --git a/tests/integration/targets/vyos_route_maps/tasks/main.yaml b/tests/integration/targets/vyos_route_maps/tasks/main.yaml new file mode 100644 index 0000000..e8171dd --- /dev/null +++ b/tests/integration/targets/vyos_route_maps/tasks/main.yaml @@ -0,0 +1,133 @@ +--- +# tests/integration/targets/vyos_route_maps/tasks/main.yaml + +- name: TEARDOWN — delete all route maps before tests + vyos.rest.vyos_route_maps: + state: deleted + +# ------------------------------------------------------------------ +# MERGED +# ------------------------------------------------------------------ + +- name: MERGED — create route map + register: result + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + match: + peer: 192.0.2.32 + set: + metric: "5" + as_path_exclude: "111" + aggregator: + as: 100 + state: merged + +- name: ASSERT — merged changed + assert: + that: + - result.changed == true + +- name: MERGED — idempotency check + register: result + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + match: + peer: 192.0.2.32 + set: + metric: "5" + as_path_exclude: "111" + aggregator: + as: 100 + state: merged + +- name: ASSERT — merged idempotent + assert: + that: + - result.changed == false + - result.commands | length == 0 + +# ------------------------------------------------------------------ +# GATHERED +# ------------------------------------------------------------------ + +- name: GATHERED — read current route maps + register: result + vyos.rest.vyos_route_maps: + state: gathered + +- name: ASSERT — gathered has expected route map + assert: + that: + - result.gathered | selectattr('route_map','eq','RM-TEST-EXPORT-POLICY') | list | length == 1 + +# ------------------------------------------------------------------ +# REPLACED +# ------------------------------------------------------------------ + +- name: REPLACED — replace route map with updated set + register: result + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + set: + metric: "10" + state: replaced + +- name: ASSERT — replaced changed + assert: + that: + - result.changed == true + +- name: REPLACED — idempotency check + register: result + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + set: + metric: "10" + state: replaced + +- name: ASSERT — replaced idempotent + assert: + that: + - result.changed == false + +# ------------------------------------------------------------------ +# DELETED +# ------------------------------------------------------------------ + +- name: DELETED — remove specific route map + register: result + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + state: deleted + +- name: ASSERT — deleted changed + assert: + that: + - result.changed == true + +- name: DELETED — idempotency check + register: result + vyos.rest.vyos_route_maps: + state: deleted + +- name: ASSERT — deleted idempotent + assert: + that: + - result.changed == false diff --git a/tests/integration/targets/vyos_snmp_server/aliases b/tests/integration/targets/vyos_snmp_server/aliases new file mode 100644 index 0000000..992ae12 --- /dev/null +++ b/tests/integration/targets/vyos_snmp_server/aliases @@ -0,0 +1 @@ +network diff --git a/tests/unit/fixtures/lldp_global_running.json b/tests/unit/fixtures/lldp_global_running.json new file mode 100644 index 0000000..fdd6aa4 --- /dev/null +++ b/tests/unit/fixtures/lldp_global_running.json @@ -0,0 +1,10 @@ +{ + "management-address": { + "192.0.2.17": {} + }, + "snmp": {}, + "legacy-protocols": { + "cdp": {}, + "sonmp": {} + } +} diff --git a/tests/unit/fixtures/logging_global_running.json b/tests/unit/fixtures/logging_global_running.json new file mode 100644 index 0000000..557408d --- /dev/null +++ b/tests/unit/fixtures/logging_global_running.json @@ -0,0 +1,42 @@ +{ + "console": { + "facility": { + "all": {}, + "local7": { "level": "err" } + } + }, + "file": { + "logFile": { + "archive": { "file": "2" }, + "facility": { + "local6": { "level": "emerg" } + } + } + }, + "global": { + "archive": { "file": "2", "size": "111" }, + "facility": { + "cron": { "level": "debug" }, + "all": { "level": "info" }, + "local7": { "level": "debug" } + }, + "marker": { "interval": "111" }, + "preserve-fqdn": {} + }, + "host": { + "172.16.0.1": { + "facility": { + "local7": { "level": "all" }, + "all": { "protocol": "udp" } + }, + "port": "223" + } + }, + "user": { + "vyos": { + "facility": { + "local7": { "level": "debug" } + } + } + } +} diff --git a/tests/unit/fixtures/ntp_global_running.json b/tests/unit/fixtures/ntp_global_running.json new file mode 100644 index 0000000..95a9ff8 --- /dev/null +++ b/tests/unit/fixtures/ntp_global_running.json @@ -0,0 +1,17 @@ +{ + "allow-client": { + "address": { + "10.6.6.0/24": {} + } + }, + "listen-address": { + "10.1.3.1": {} + }, + "server": { + "time1.vyos.net": {}, + "time2.vyos.net": {}, + "203.0.113.0": { + "prefer": {} + } + } +} diff --git a/tests/unit/fixtures/prefix_lists_running.json b/tests/unit/fixtures/prefix_lists_running.json new file mode 100644 index 0000000..2a6ecd3 --- /dev/null +++ b/tests/unit/fixtures/prefix_lists_running.json @@ -0,0 +1,44 @@ +{ + "prefix-list": { + "AnsibleIPv4PrefixList": { + "description": "PL configured by ansible", + "rule": { + "2": { + "action": "permit", + "prefix": "92.168.10.0/26", + "le": "32", + "description": "Rule 2 given by ansible" + }, + "3": { + "action": "deny", + "prefix": "72.168.2.0/24", + "ge": "26", + "description": "Rule 3" + } + } + } + }, + "prefix-list6": { + "AllowIPv6Prefix": { + "description": "Configured by ansible for allowing IPv6 networks", + "rule": { + "5": { + "action": "permit", + "prefix": "2001:db8:8000::/35", + "le": "37", + "description": "Permit rule" + } + } + }, + "DenyIPv6Prefix": { + "description": "Configured by ansible for disallowing IPv6 networks", + "rule": { + "8": { + "action": "deny", + "prefix": "2001:db8:2000::/35", + "le": "37" + } + } + } + } +} diff --git a/tests/unit/fixtures/route_maps_running.json b/tests/unit/fixtures/route_maps_running.json new file mode 100644 index 0000000..acf733b --- /dev/null +++ b/tests/unit/fixtures/route_maps_running.json @@ -0,0 +1,31 @@ +{ + "route-map": { + "RM-TEST-EXPORT-POLICY": { + "rule": { + "10": { + "action": "permit", + "match": { "peer": "192.0.2.32" }, + "set": { + "aggregator": { "as": "100" }, + "as-path": { "exclude": "111" }, + "metric": "5" + } + } + } + }, + "rm1": { + "rule": { + "10": { + "action": "permit", + "match": { "peer": "192.0.2.32" }, + "set": { + "aggregator": { "as": "100" }, + "as-path": { "exclude": "111" }, + "large-community": { "none": {} }, + "metric": "5" + } + } + } + } + } +} diff --git a/tests/unit/fixtures/snmp_server_running.json b/tests/unit/fixtures/snmp_server_running.json new file mode 100644 index 0000000..7acc59f --- /dev/null +++ b/tests/unit/fixtures/snmp_server_running.json @@ -0,0 +1,35 @@ +{ + "community": { + "switches": { "authorization": "rw" }, + "bridges": { + "client": ["1.1.1.1", "12.1.1.10"] + } + }, + "contact": "admin@example.com", + "listen-address": { + "20.1.1.1": {}, + "100.1.2.1": { "port": "33" } + }, + "v3": { + "engineid": "000000000000000000000002", + "group": { + "default": { "mode": "ro", "view": "default" } + }, + "user": { + "admin_user": { + "auth": { + "encrypted-password": "abc123", + "type": "sha" + }, + "privacy": { + "encrypted-password": "abc123", + "type": "aes" + }, + "group": "default" + } + }, + "view": { + "default": { "oid": { "1": {} } } + } + } +} diff --git a/tests/unit/modules/base.py b/tests/unit/modules/base.py new file mode 100644 index 0000000..e5a602e --- /dev/null +++ b/tests/unit/modules/base.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock, patch # noqa: F401 + + +def load_fixture(filename): + """Load a JSON fixture file from tests/unit/fixtures/.""" + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + path = os.path.join(fixtures_dir, filename) + with open(path) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + """ + Base class for vyos.rest module unit tests. + + Provides a mock VyOSModule that returns fixture data from + get_config() without any device connection. + + Usage: + class TestVyOSNtpGlobal(VyOSModuleTestCase): + def setUp(self): + super().setUp() + self.fixture = load_fixture("ntp_global_running.json") + + def test_merged_adds_new_server(self): + have = self.module.get_running_config_from_fixture(self.fixture) + commands = build_commands(want, have, "merged") + self.assertIn(("set", ["service", "ntp", "server", "1.2.3.4"]), commands) + """ + + def setUp(self): + self.mock_module = MagicMock() + self.mock_module.params = {} + self.mock_module.check_mode = False + + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + """Configure mock get_config to return given data.""" + self.mock_vyos.get_config.return_value = data diff --git a/tests/unit/modules/conftest.py b/tests/unit/modules/conftest.py new file mode 100644 index 0000000..26b7d8b --- /dev/null +++ b/tests/unit/modules/conftest.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# conftest.py — shared fixtures for vyos.rest unit tests diff --git a/tests/unit/modules/test_vyos_logging_global.py b/tests/unit/modules/test_vyos_logging_global.py new file mode 100644 index 0000000..e9ecc8c --- /dev/null +++ b/tests/unit/modules/test_vyos_logging_global.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_logging_global import ( + build_commands, + normalize_config, + normalize_running, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSLoggingGlobalNormalize(unittest.TestCase): + + def test_normalize_config_console(self): + cfg = { + "console": { + "facilities": [{"facility": "local7", "severity": "err"}], + }, + } + result = normalize_config(cfg) + self.assertIn("local7", result["console"]["facilities"]) + self.assertEqual( + result["console"]["facilities"]["local7"], + {"severity": "err", "protocol": None}, + ) + + def test_normalize_config_hosts(self): + cfg = { + "hosts": [ + { + "hostname": "172.16.0.1", + "port": 514, + "facilities": [ + {"facility": "local7", "severity": "all"}, + {"facility": "all", "protocol": "udp"}, + ], + }, + ], + } + result = normalize_config(cfg) + self.assertIn("172.16.0.1", result["hosts"]) + host = result["hosts"]["172.16.0.1"] + self.assertEqual(host["port"], 514) + self.assertIn("local7", host["facilities"]) + self.assertEqual(host["facilities"]["local7"]["severity"], "all") + self.assertEqual(host["facilities"]["all"]["protocol"], "udp") + + def test_normalize_running_console(self): + raw = { + "console": { + "facility": { + "local7": {"level": "err"}, + "all": {}, + }, + }, + } + result = normalize_running(raw) + self.assertIn("local7", result["console"]["facilities"]) + self.assertEqual(result["console"]["facilities"]["local7"]["severity"], "err") + self.assertIsNone(result["console"]["facilities"]["all"]["severity"]) + + def test_normalize_running_global_archive(self): + raw = { + "global": { + "archive": {"file": "2", "size": "111"}, + "marker": {"interval": "111"}, + "preserve-fqdn": {}, + }, + } + result = normalize_running(raw) + self.assertEqual(result["global"]["archive"]["file_num"], 2) + self.assertEqual(result["global"]["archive"]["size"], 111) + self.assertEqual(result["global"]["marker_interval"], 111) + self.assertTrue(result["global"]["preserve_fqdn"]) + + def test_normalize_running_host_port_cast(self): + raw = { + "host": { + "172.16.0.1": { + "port": "514", + "facility": {}, + }, + }, + } + result = normalize_running(raw) + self.assertEqual(result["hosts"]["172.16.0.1"]["port"], 514) + + def test_normalize_running_empty(self): + result = normalize_running({}) + self.assertEqual(result["console"]["facilities"], {}) + self.assertEqual(result["hosts"], {}) + + +class TestVyOSLoggingGlobalBuildCommands(unittest.TestCase): + + def _empty_have(self): + return { + "console": {"facilities": {}}, + "global": {"facilities": {}}, + "hosts": {}, + "files": {}, + "users": {}, + } + + def test_merged_adds_console_facility(self): + want = self._empty_have() + want["console"]["facilities"]["local7"] = {"severity": "err", "protocol": None} + cmds = build_commands(want, self._empty_have(), "merged") + self.assertIn( + ("set", ["system", "syslog", "console", "facility", "local7", "level", "err"]), + cmds, + ) + + def test_merged_idempotent_console(self): + facs = {"local7": {"severity": "err", "protocol": None}} + want = self._empty_have() + have = self._empty_have() + want["console"]["facilities"] = facs + have["console"]["facilities"] = facs.copy() + cmds = build_commands(want, have, "merged") + self.assertEqual(cmds, []) + + def test_merged_adds_host_with_port(self): + want = self._empty_have() + want["hosts"]["172.16.0.1"] = { + "port": 514, + "facilities": {"local7": {"severity": "all", "protocol": None}}, + } + cmds = build_commands(want, self._empty_have(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["system", "syslog", "host", "172.16.0.1", "port", "514"], paths) + self.assertIn( + ["system", "syslog", "host", "172.16.0.1", "facility", "local7", "level", "all"], + paths, + ) + + def test_merged_adds_host_facility_protocol(self): + want = self._empty_have() + want["hosts"]["172.16.0.1"] = { + "port": None, + "facilities": {"all": {"severity": None, "protocol": "udp"}}, + } + cmds = build_commands(want, self._empty_have(), "merged") + self.assertIn( + ( + "set", + ["system", "syslog", "host", "172.16.0.1", "facility", "all", "protocol", "udp"], + ), + cmds, + ) + + def test_replaced_removes_extra_host(self): + want = self._empty_have() + have = self._empty_have() + have["hosts"]["172.16.0.1"] = {"port": None, "facilities": {}} + cmds = build_commands(want, have, "replaced") + self.assertIn(("delete", ["system", "syslog", "host", "172.16.0.1"]), cmds) + + def test_deleted_issues_single_delete(self): + have = self._empty_have() + have["console"]["facilities"]["all"] = {"severity": None, "protocol": None} + cmds = build_commands(self._empty_have(), have, "deleted") + self.assertIn(("delete", ["system", "syslog"]), cmds) + + def test_overridden_deletes_then_merges(self): + want = self._empty_have() + have = self._empty_have() + have["console"]["facilities"]["all"] = {"severity": None, "protocol": None} + cmds = build_commands(want, have, "overridden") + self.assertIn(("delete", ["system", "syslog"]), cmds) + + def test_global_preserve_fqdn_added(self): + want = self._empty_have() + want["global"]["preserve_fqdn"] = True + cmds = build_commands(want, self._empty_have(), "merged") + self.assertIn(("set", ["system", "syslog", "global", "preserve-fqdn"]), cmds) + + def test_global_archive(self): + want = self._empty_have() + want["global"]["archive"] = {"file_num": 2, "size": 111} + cmds = build_commands(want, self._empty_have(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["system", "syslog", "global", "archive", "file", "2"], paths) + self.assertIn(["system", "syslog", "global", "archive", "size", "111"], paths) + + +class TestVyOSLoggingGlobalFixture(VyOSModuleTestCase): + """Test parsing against the confirmed device fixture.""" + + def setUp(self): + super().setUp() + self.fixture = load_fixture("logging_global_running.json") + + def test_fixture_parses_console(self): + result = normalize_running(self.fixture) + self.assertIn("local7", result["console"]["facilities"]) + self.assertEqual(result["console"]["facilities"]["local7"]["severity"], "err") + + def test_fixture_parses_host_port(self): + result = normalize_running(self.fixture) + self.assertEqual(result["hosts"]["172.16.0.1"]["port"], 223) + + def test_fixture_parses_global_archive(self): + result = normalize_running(self.fixture) + self.assertEqual(result["global"]["archive"]["file_num"], 2) + self.assertEqual(result["global"]["archive"]["size"], 111) + + def test_fixture_parses_preserve_fqdn(self): + result = normalize_running(self.fixture) + self.assertTrue(result["global"]["preserve_fqdn"]) + + def test_fixture_parses_marker_interval(self): + result = normalize_running(self.fixture) + self.assertEqual(result["global"]["marker_interval"], 111) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_ntp_global.py b/tests/unit/modules/test_vyos_ntp_global.py new file mode 100644 index 0000000..d646b1a --- /dev/null +++ b/tests/unit/modules/test_vyos_ntp_global.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +# Allow importing collection modules without full ansible-test runner +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_ntp_global import ( + build_commands, + get_running_config, + normalize_config, + normalize_servers, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSNtpGlobalNormalize(unittest.TestCase): + """Test normalize_config and normalize_servers — no device needed.""" + + def test_normalize_config_empty(self): + result = normalize_config({}) + self.assertEqual(result["allow_clients"], []) + self.assertEqual(result["listen_addresses"], []) + self.assertEqual(result["servers"], {}) + + def test_normalize_config_servers_sorted(self): + config = { + "servers": [ + {"server": "b.example.com", "options": ["prefer", "noselect"]}, + {"server": "a.example.com"}, + ], + } + result = normalize_config(config) + self.assertIn("a.example.com", result["servers"]) + self.assertIn("b.example.com", result["servers"]) + self.assertEqual(result["servers"]["b.example.com"], ["noselect", "prefer"]) + + def test_normalize_servers_dict_with_options(self): + raw = { + "time1.vyos.net": {}, + "203.0.113.0": {"prefer": {}}, + } + result = normalize_servers(raw) + self.assertEqual(result["time1.vyos.net"], []) + self.assertEqual(result["203.0.113.0"], ["prefer"]) + + def test_normalize_servers_list(self): + raw = ["time1.vyos.net", "time2.vyos.net"] + result = normalize_servers(raw) + self.assertEqual(result["time1.vyos.net"], []) + + def test_normalize_servers_string(self): + result = normalize_servers("time1.vyos.net") + self.assertEqual(result["time1.vyos.net"], []) + + +class TestVyOSNtpGlobalGetRunning(VyOSModuleTestCase): + """Test get_running_config parsing against fixture API responses.""" + + def setUp(self): + super().setUp() + self.fixture = load_fixture("ntp_global_running.json") + + def test_parses_allow_clients(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertIn("10.6.6.0/24", result["allow_clients"]) + + def test_parses_listen_addresses(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertIn("10.1.3.1", result["listen_addresses"]) + + def test_parses_servers(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertIn("time1.vyos.net", result["servers"]) + self.assertIn("203.0.113.0", result["servers"]) + self.assertIn("prefer", result["servers"]["203.0.113.0"]) + + def test_empty_config_returns_empty(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result["allow_clients"], []) + self.assertEqual(result["servers"], {}) + + +class TestVyOSNtpGlobalBuildCommands(unittest.TestCase): + """Test build_commands diff logic — no device needed.""" + + def _have(self, **kwargs): + base = {"allow_clients": [], "listen_addresses": [], "servers": {}} + base.update(kwargs) + return base + + def _want(self, **kwargs): + return self._have(**kwargs) + + def test_merged_adds_new_server(self): + want = self._want(servers={"new.server.com": []}) + have = self._have(servers={}) + cmds = build_commands(want, have, "merged") + self.assertIn(("set", ["service", "ntp", "server", "new.server.com"]), cmds) + + def test_merged_idempotent_existing_server(self): + want = self._want(servers={"time1.vyos.net": []}) + have = self._have(servers={"time1.vyos.net": []}) + cmds = build_commands(want, have, "merged") + self.assertEqual(cmds, []) + + def test_merged_adds_server_option(self): + want = self._want(servers={"time1.vyos.net": ["prefer"]}) + have = self._have(servers={"time1.vyos.net": []}) + cmds = build_commands(want, have, "merged") + self.assertIn(("set", ["service", "ntp", "server", "time1.vyos.net", "prefer"]), cmds) + + def test_replaced_removes_extra_server(self): + want = self._want(servers={"time1.vyos.net": []}) + have = self._have(servers={"time1.vyos.net": [], "time2.vyos.net": []}) + cmds = build_commands(want, have, "replaced") + self.assertIn(("delete", ["service", "ntp", "server", "time2.vyos.net"]), cmds) + + def test_replaced_removes_extra_allow_client(self): + want = self._want(allow_clients=["10.1.0.0/24"]) + have = self._have(allow_clients=["10.1.0.0/24", "10.2.0.0/24"]) + cmds = build_commands(want, have, "replaced") + self.assertIn( + ("delete", ["service", "ntp", "allow-client", "address", "10.2.0.0/24"]), + cmds, + ) + + def test_deleted_removes_all(self): + want = self._want(servers={}, allow_clients=[], listen_addresses=[]) + have = self._have( + servers={"time1.vyos.net": []}, + allow_clients=["10.0.0.0/24"], + listen_addresses=["192.168.1.1"], + ) + cmds = build_commands(want, have, "deleted") + paths = [c[1] for c in cmds] + self.assertIn(["service", "ntp", "server", "time1.vyos.net"], paths) + self.assertIn(["service", "ntp", "allow-client", "address", "10.0.0.0/24"], paths) + + def test_overridden_deletes_then_merges(self): + want = self._want(servers={"new.server.com": []}) + have = self._have(servers={"old.server.com": []}) + cmds = build_commands(want, have, "overridden") + paths = [c[1] for c in cmds] + # overridden deletes existing server subtree first + self.assertIn(["service", "ntp", "server"], paths) + # then adds new server + self.assertIn(["service", "ntp", "server", "new.server.com"], paths) + + def test_no_commands_when_already_correct(self): + state = {"allow_clients": ["10.0.0.0/24"], "listen_addresses": [], "servers": {}} + cmds = build_commands(state, state, "merged") + self.assertEqual(cmds, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_prefix_lists.py b/tests/unit/modules/test_vyos_prefix_lists.py new file mode 100644 index 0000000..d8b75df --- /dev/null +++ b/tests/unit/modules/test_vyos_prefix_lists.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_prefix_lists import ( + _normalize, + build_commands, + get_running_config, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSPrefixListsGetRunning(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("prefix_lists_running.json") + + def test_parses_ipv4_prefix_list(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next((e for e in result if e["afi"] == "ipv4"), None) + self.assertIsNotNone(ipv4) + pl = next((p for p in ipv4["prefix_lists"] if p["name"] == "AnsibleIPv4PrefixList"), None) + self.assertIsNotNone(pl) + self.assertEqual(pl["description"], "PL configured by ansible") + + def test_parses_ipv4_rules(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + pl = ipv4["prefix_lists"][0] + seqs = [r["sequence"] for r in pl["entries"]] + self.assertIn(2, seqs) + self.assertIn(3, seqs) + + def test_parses_ipv4_rule_fields(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + rule2 = next(r for r in ipv4["prefix_lists"][0]["entries"] if r["sequence"] == 2) + self.assertEqual(rule2["action"], "permit") + self.assertEqual(rule2["prefix"], "92.168.10.0/26") + self.assertEqual(rule2["le"], 32) + + def test_parses_ipv6_prefix_lists(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + ipv6 = next((e for e in result if e["afi"] == "ipv6"), None) + self.assertIsNotNone(ipv6) + names = [p["name"] for p in ipv6["prefix_lists"]] + self.assertIn("AllowIPv6Prefix", names) + self.assertIn("DenyIPv6Prefix", names) + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSPrefixListsNormalize(unittest.TestCase): + + def test_normalize_ipv4(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [{"sequence": 10, "action": "permit", "prefix": "10.0.0.0/8"}], + }, + ], + }, + ] + result = _normalize(config) + self.assertIn("PL1", result["ipv4"]) + self.assertIn(10, result["ipv4"]["PL1"]["rules"]) + self.assertEqual(result["ipv4"]["PL1"]["rules"][10]["action"], "permit") + + def test_normalize_filters_none_values(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "prefix": "10.0.0.0/8", + "ge": None, + "le": None, + }, + ], + }, + ], + }, + ] + result = _normalize(config) + rule = result["ipv4"]["PL1"]["rules"][10] + self.assertNotIn("ge", rule) + self.assertNotIn("le", rule) + + +class TestVyOSPrefixListsBuildCommands(unittest.TestCase): + + def _have_empty(self): + return [] + + def _have_with_ipv4_pl(self): + return [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "prefix": "10.0.0.0/8", + }, + ], + }, + ], + }, + ] + + def test_merged_adds_new_prefix_list(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL-NEW", + "entries": [ + { + "sequence": 5, + "action": "permit", + "prefix": "192.168.0.0/24", + }, + ], + }, + ], + }, + ] + cmds = build_commands(config, self._have_empty(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["policy", "prefix-list", "PL-NEW", "rule", "5", "action", "permit"], paths) + self.assertIn( + ["policy", "prefix-list", "PL-NEW", "rule", "5", "prefix", "192.168.0.0/24"], + paths, + ) + + def test_merged_idempotent_existing_rule(self): + config = self._have_with_ipv4_pl() + cmds = build_commands(config, self._have_with_ipv4_pl(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_deletes_all(self): + cmds = build_commands([], self._have_with_ipv4_pl(), "deleted") + self.assertIn(("delete", ["policy", "prefix-list"]), cmds) + + def test_deleted_with_config_deletes_named(self): + config = [{"afi": "ipv4", "prefix_lists": [{"name": "PL1"}]}] + cmds = build_commands(config, self._have_with_ipv4_pl(), "deleted") + self.assertIn(("delete", ["policy", "prefix-list", "PL1"]), cmds) + + def test_replaced_deletes_then_resets(self): + config = [ + { + "afi": "ipv4", + "prefix_lists": [ + { + "name": "PL1", + "entries": [ + { + "sequence": 10, + "action": "deny", + "prefix": "10.0.0.0/8", + }, + ], + }, + ], + }, + ] + cmds = build_commands(config, self._have_with_ipv4_pl(), "replaced") + # Should delete PL1 first then re-add + ops = [(c[0], c[1]) for c in cmds] + delete_idx = next( + i for i, c in enumerate(ops) if c == ("delete", ["policy", "prefix-list", "PL1"]) + ) + set_idx = next(i for i, c in enumerate(ops) if c[0] == "set" and "deny" in c[1]) + self.assertLess(delete_idx, set_idx) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_route_maps.py b/tests/unit/modules/test_vyos_route_maps.py new file mode 100644 index 0000000..2d45bb7 --- /dev/null +++ b/tests/unit/modules/test_vyos_route_maps.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import sys +import unittest + + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from ansible_collections.vyos.rest.plugins.modules.vyos_route_maps import ( + _want_to_api_match, + _want_to_api_set, + build_commands, + get_running_config, +) + +from tests.unit.modules.base import VyOSModuleTestCase, load_fixture + + +class TestVyOSRouteMapsGetRunning(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("route_maps_running.json") + + def test_unwraps_route_map_nesting(self): + """API returns {"route-map": {"NAME": {...}}} — must unwrap.""" + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + names = [e["route_map"] for e in result] + self.assertIn("RM-TEST-EXPORT-POLICY", names) + self.assertIn("rm1", names) + # "route-map" itself must NOT appear as a route map name + self.assertNotIn("route-map", names) + + def test_parses_rule_action(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + rm = next(e for e in result if e["route_map"] == "RM-TEST-EXPORT-POLICY") + rule = rm["entries"][0] + self.assertEqual(rule["action"], "permit") + self.assertEqual(rule["sequence"], 10) + + def test_parses_match_peer(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + rm = next(e for e in result if e["route_map"] == "RM-TEST-EXPORT-POLICY") + rule = rm["entries"][0] + self.assertEqual(rule["match"]["peer"], "192.0.2.32") + + def test_parses_set_fields(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + rm = next(e for e in result if e["route_map"] == "RM-TEST-EXPORT-POLICY") + rule = rm["entries"][0] + self.assertEqual(rule["set"]["metric"], "5") + self.assertEqual(rule["set"]["aggregator"]["as"], "100") + self.assertEqual(rule["set"]["as-path"]["exclude"], "111") + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSRouteMapsWantToApi(unittest.TestCase): + + def test_as_path_exclude_nested(self): + """as_path_exclude maps to nested as-path.exclude.""" + result = _want_to_api_set({"as_path_exclude": "111"}) + self.assertEqual(result["as-path"]["exclude"], "111") + + def test_metric_flat(self): + result = _want_to_api_set({"metric": "5"}) + self.assertEqual(result["metric"], "5") + + def test_aggregator_as(self): + result = _want_to_api_set({"aggregator": {"as": 100}}) + self.assertEqual(result["aggregator"]["as"], "100") + + def test_aggregator_as_underscore(self): + """aggregator.as_ is an alias for aggregator.as.""" + result = _want_to_api_set({"aggregator": {"as_": 100}}) + self.assertEqual(result["aggregator"]["as"], "100") + + def test_large_community_presence_node(self): + result = _want_to_api_set({"large_community": "none"}) + self.assertEqual(result["large-community"], {"none": {}}) + + def test_match_peer(self): + result = _want_to_api_match({"peer": "192.0.2.32"}) + self.assertEqual(result["peer"], "192.0.2.32") + + +class TestVyOSRouteMapsBuildCommands(unittest.TestCase): + + def _have_empty(self): + return [] + + def _have_with_rm(self): + return [ + { + "route_map": "RM1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "match": {"peer": "192.0.2.32"}, + "set": {"metric": "5", "as-path": {"exclude": "111"}}, + }, + ], + }, + ] + + def test_merged_adds_new_rm(self): + config = [ + { + "route_map": "RM-NEW", + "entries": [{"sequence": 10, "action": "permit"}], + }, + ] + cmds = build_commands(config, self._have_empty(), "merged") + paths = [c[1] for c in cmds] + self.assertIn(["policy", "route-map", "RM-NEW", "rule", "10", "action", "permit"], paths) + + def test_merged_idempotent(self): + config = [ + { + "route_map": "RM1", + "entries": [ + { + "sequence": 10, + "action": "permit", + "match": {"peer": "192.0.2.32"}, + "set": {"metric": "5", "as_path_exclude": "111"}, + }, + ], + }, + ] + cmds = build_commands(config, self._have_with_rm(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_deletes_all(self): + cmds = build_commands([], self._have_with_rm(), "deleted") + self.assertIn(("delete", ["policy", "route-map"]), cmds) + + def test_deleted_with_config_deletes_named(self): + config = [{"route_map": "RM1"}] + cmds = build_commands(config, self._have_with_rm(), "deleted") + self.assertIn(("delete", ["policy", "route-map", "RM1"]), cmds) + + def test_replaced_deletes_then_resets(self): + config = [ + { + "route_map": "RM1", + "entries": [{"sequence": 10, "action": "deny"}], + }, + ] + cmds = build_commands(config, self._have_with_rm(), "replaced") + ops = [c[0] for c in cmds] + # delete must come before set + self.assertIn("delete", ops) + self.assertIn("set", ops) + delete_idx = ops.index("delete") + set_idx = ops.index("set") + self.assertLess(delete_idx, set_idx) + + def test_overridden_removes_extra_rm(self): + config = [{"route_map": "RM-NEW", "entries": []}] + have = self._have_with_rm() # has RM1 + cmds = build_commands(config, have, "overridden") + self.assertIn(("delete", ["policy", "route-map", "RM1"]), cmds) + + +if __name__ == "__main__": + unittest.main() |
