@homeflare/alchemy 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -8
- package/dist/ceph/auth-entity.d.ts +34 -0
- package/dist/ceph/auth-entity.d.ts.map +1 -0
- package/dist/ceph/ceph-argv-config.d.ts +10 -0
- package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
- package/dist/ceph/ceph-argv.d.ts +19 -0
- package/dist/ceph/ceph-argv.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-form.d.ts +54 -0
- package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-parse.d.ts +56 -0
- package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
- package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
- package/dist/ceph/ceph-config-options.d.ts +23 -0
- package/dist/ceph/ceph-config-options.d.ts.map +1 -0
- package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
- package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
- package/dist/ceph/ceph-transport.d.ts +37 -0
- package/dist/ceph/ceph-transport.d.ts.map +1 -0
- package/dist/ceph/fake-ceph-dial.d.ts +35 -0
- package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
- package/dist/ceph/index.d.ts +13 -0
- package/dist/ceph/index.d.ts.map +1 -0
- package/dist/ceph/index.js +1609 -0
- package/dist/ceph/index.js.map +38 -0
- package/dist/forgejo/index.js +4 -2
- package/dist/forgejo/index.js.map +3 -3
- package/dist/forgejo/team-member.d.ts +25 -0
- package/dist/forgejo/team-member.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
- package/dist/proxmox/index.d.ts +14 -3
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +574 -133
- package/dist/proxmox/index.js.map +33 -23
- package/dist/proxmox/qemu-form.d.ts +27 -18
- package/dist/proxmox/qemu-form.d.ts.map +1 -1
- package/dist/proxmox/qemu-judge.d.ts +30 -0
- package/dist/proxmox/qemu-judge.d.ts.map +1 -0
- package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
- package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu-net.d.ts +9 -0
- package/dist/proxmox/qemu-net.d.ts.map +1 -0
- package/dist/proxmox/qemu-props.d.ts +112 -0
- package/dist/proxmox/qemu-props.d.ts.map +1 -0
- package/dist/proxmox/qemu-volume.d.ts +27 -0
- package/dist/proxmox/qemu-volume.d.ts.map +1 -0
- package/dist/proxmox/qemu.d.ts +14 -27
- package/dist/proxmox/qemu.d.ts.map +1 -1
- package/dist/proxmox/storage-download-errors.d.ts +5 -0
- package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
- package/dist/proxmox/storage-download-identity.d.ts +27 -0
- package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/storage-download-props.d.ts +36 -0
- package/dist/proxmox/storage-download-props.d.ts.map +1 -0
- package/dist/proxmox/storage-download-task.d.ts +17 -0
- package/dist/proxmox/storage-download-task.d.ts.map +1 -0
- package/dist/proxmox/storage-download.d.ts +44 -0
- package/dist/proxmox/storage-download.d.ts.map +1 -0
- package/dist/talos/credentials-write.d.ts +46 -0
- package/dist/talos/credentials-write.d.ts.map +1 -0
- package/dist/talos/credentials.d.ts +51 -9
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/fake-process.d.ts +56 -0
- package/dist/talos/fake-process.d.ts.map +1 -0
- package/dist/talos/index.d.ts +19 -4
- package/dist/talos/index.d.ts.map +1 -1
- package/dist/talos/index.js +357 -173
- package/dist/talos/index.js.map +16 -10
- package/dist/talos/kubeconfig-attrs.d.ts +4 -0
- package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
- package/dist/talos/kubeconfig.d.ts +41 -23
- package/dist/talos/kubeconfig.d.ts.map +1 -1
- package/dist/talos/machine-config-fixtures.d.ts +60 -0
- package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
- package/dist/talos/machine-config-poll.d.ts +43 -0
- package/dist/talos/machine-config-poll.d.ts.map +1 -0
- package/dist/talos/machine-config-read.d.ts +49 -0
- package/dist/talos/machine-config-read.d.ts.map +1 -0
- package/dist/talos/resource.d.ts +18 -0
- package/dist/talos/resource.d.ts.map +1 -1
- package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
- package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap.d.ts +87 -0
- package/dist/talos/talos-bootstrap.d.ts.map +1 -1
- package/dist/talos/talos-cluster-health.d.ts +43 -1
- package/dist/talos/talos-cluster-health.d.ts.map +1 -1
- package/dist/talos/talos-errors.d.ts +68 -0
- package/dist/talos/talos-errors.d.ts.map +1 -0
- package/dist/talos/talos-machine-config.d.ts +51 -15
- package/dist/talos/talos-machine-config.d.ts.map +1 -1
- package/dist/talos/talosctl.d.ts +7 -0
- package/dist/talos/talosctl.d.ts.map +1 -1
- package/dist/talos/values.d.ts +23 -3
- package/dist/talos/values.d.ts.map +1 -1
- package/dist/telemetry/fake-collector.d.ts +36 -0
- package/dist/telemetry/fake-collector.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +223 -0
- package/dist/telemetry/index.js.map +12 -0
- package/dist/telemetry/layer.d.ts +86 -0
- package/dist/telemetry/layer.d.ts.map +1 -0
- package/dist/telemetry/redact-serialization.d.ts +21 -0
- package/dist/telemetry/redact-serialization.d.ts.map +1 -0
- package/dist/telemetry/redact.d.ts +46 -0
- package/dist/telemetry/redact.d.ts.map +1 -0
- package/dist/unifi/acl-rule-drift.d.ts +22 -0
- package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
- package/dist/unifi/acl-rule-form.d.ts +90 -0
- package/dist/unifi/acl-rule-form.d.ts.map +1 -0
- package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
- package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
- package/dist/unifi/acl-rule-ordering.d.ts +54 -0
- package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
- package/dist/unifi/acl-rule.d.ts +49 -0
- package/dist/unifi/acl-rule.d.ts.map +1 -0
- package/dist/unifi/dns-policy-drift.d.ts +21 -0
- package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
- package/dist/unifi/dns-policy-form.d.ts +97 -0
- package/dist/unifi/dns-policy-form.d.ts.map +1 -0
- package/dist/unifi/dns-policy.d.ts +55 -0
- package/dist/unifi/dns-policy.d.ts.map +1 -0
- package/dist/unifi/drift.d.ts +27 -0
- package/dist/unifi/drift.d.ts.map +1 -0
- package/dist/unifi/fake-unifi.d.ts +5 -1
- package/dist/unifi/fake-unifi.d.ts.map +1 -1
- package/dist/unifi/firewall-policy-drift.d.ts +26 -0
- package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-form.d.ts +102 -0
- package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
- package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
- package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
- package/dist/unifi/firewall-policy.d.ts +57 -0
- package/dist/unifi/firewall-policy.d.ts.map +1 -0
- package/dist/unifi/firewall-zone-form.d.ts +6 -0
- package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
- package/dist/unifi/firewall-zone.d.ts +1 -1
- package/dist/unifi/firewall-zone.d.ts.map +1 -1
- package/dist/unifi/index.d.ts +11 -3
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +725 -60
- package/dist/unifi/index.js.map +31 -9
- package/dist/unifi/network-drift.d.ts +26 -0
- package/dist/unifi/network-drift.d.ts.map +1 -0
- package/dist/unifi/network-form.d.ts +40 -7
- package/dist/unifi/network-form.d.ts.map +1 -1
- package/dist/unifi/network.d.ts +1 -0
- package/dist/unifi/network.d.ts.map +1 -1
- package/dist/unifi/paginate.d.ts +24 -0
- package/dist/unifi/paginate.d.ts.map +1 -0
- package/dist/unifi/providers.d.ts +1 -1
- package/dist/unifi/providers.d.ts.map +1 -1
- package/dist/unifi/resource.d.ts +36 -1
- package/dist/unifi/resource.d.ts.map +1 -1
- package/dist/unifi/scan-source.d.ts +11 -0
- package/dist/unifi/scan-source.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
- package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
- package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list.d.ts +51 -0
- package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
- package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
- package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast.d.ts +73 -0
- package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
- package/docs/telemetry-spike.md +94 -0
- package/docs/telemetry.md +109 -0
- package/docs/unifi-acl-rule.md +122 -0
- package/docs/unifi-dns-policy.md +75 -0
- package/docs/unifi-firewall-policy.md +109 -0
- package/docs/unifi-traffic-matching-list.md +82 -0
- package/docs/unifi-wifi.md +120 -0
- package/docs/unifi.md +64 -3
- package/package.json +10 -2
- package/src/ceph/auth-entity.ts +74 -0
- package/src/ceph/ceph-argv-config.ts +78 -0
- package/src/ceph/ceph-argv.test.ts +226 -0
- package/src/ceph/ceph-argv.ts +189 -0
- package/src/ceph/ceph-auth-form.test.ts +103 -0
- package/src/ceph/ceph-auth-form.ts +107 -0
- package/src/ceph/ceph-auth-parse.test.ts +155 -0
- package/src/ceph/ceph-auth-parse.ts +121 -0
- package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
- package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
- package/src/ceph/ceph-auth-reconcile.ts +119 -0
- package/src/ceph/ceph-config-options.test.ts +44 -0
- package/src/ceph/ceph-config-options.ts +36 -0
- package/src/ceph/ceph-key-leak-assert.ts +35 -0
- package/src/ceph/ceph-no-output-logging.test.ts +52 -0
- package/src/ceph/ceph-transport.test.ts +125 -0
- package/src/ceph/ceph-transport.ts +117 -0
- package/src/ceph/fake-ceph-dial.ts +55 -0
- package/src/ceph/index.ts +12 -0
- package/src/forgejo/team-member.test.ts +14 -0
- package/src/forgejo/team-member.ts +28 -1
- package/src/index.ts +1 -0
- package/src/proxmox/constraints-required.test.ts +1 -0
- package/src/proxmox/generated/constraints/index.ts +3 -1
- package/src/proxmox/generated/constraints/pve-access.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
- package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
- package/src/proxmox/index.test.ts +11 -6
- package/src/proxmox/index.ts +19 -3
- package/src/proxmox/qemu-form.ts +49 -13
- package/src/proxmox/qemu-identity.test.ts +30 -0
- package/src/proxmox/qemu-judge.test.ts +65 -0
- package/src/proxmox/qemu-judge.ts +84 -0
- package/src/proxmox/qemu-lifecycle.test.ts +78 -19
- package/src/proxmox/qemu-lifecycle.ts +131 -32
- package/src/proxmox/qemu-net.test.ts +48 -0
- package/src/proxmox/qemu-net.ts +78 -0
- package/src/proxmox/qemu-ownership.test.ts +44 -0
- package/src/proxmox/qemu-props.test.ts +87 -0
- package/src/proxmox/qemu-props.ts +157 -0
- package/src/proxmox/qemu-volume.test.ts +229 -0
- package/src/proxmox/qemu-volume.ts +178 -0
- package/src/proxmox/qemu.ts +15 -29
- package/src/proxmox/storage-download-errors.ts +7 -0
- package/src/proxmox/storage-download-identity.test.ts +54 -0
- package/src/proxmox/storage-download-identity.ts +50 -0
- package/src/proxmox/storage-download-lifecycle.ts +226 -0
- package/src/proxmox/storage-download-ownership.test.ts +50 -0
- package/src/proxmox/storage-download-props.ts +38 -0
- package/src/proxmox/storage-download-task.ts +72 -0
- package/src/proxmox/storage-download.test.ts +216 -0
- package/src/proxmox/storage-download.ts +58 -0
- package/src/talos/credentials-write.test.ts +81 -0
- package/src/talos/credentials-write.ts +95 -0
- package/src/talos/credentials.test.ts +157 -0
- package/src/talos/credentials.ts +160 -90
- package/src/talos/fake-process.ts +94 -0
- package/src/talos/index.ts +39 -4
- package/src/talos/kubeconfig-attrs.ts +55 -0
- package/src/talos/kubeconfig.test.ts +256 -0
- package/src/talos/kubeconfig.ts +170 -90
- package/src/talos/machine-config-fixtures.ts +86 -0
- package/src/talos/machine-config-poll.test.ts +57 -0
- package/src/talos/machine-config-poll.ts +82 -0
- package/src/talos/machine-config-read.ts +118 -0
- package/src/talos/resource.ts +19 -0
- package/src/talos/secrets-gate.test.ts +63 -0
- package/src/talos/talos-bootstrap-etcd.ts +71 -0
- package/src/talos/talos-bootstrap.test.ts +218 -0
- package/src/talos/talos-bootstrap.ts +151 -45
- package/src/talos/talos-cluster-health.test.ts +143 -0
- package/src/talos/talos-cluster-health.ts +91 -28
- package/src/talos/talos-errors.ts +85 -0
- package/src/talos/talos-machine-config.test.ts +189 -0
- package/src/talos/talos-machine-config.ts +102 -88
- package/src/talos/talosctl.ts +8 -0
- package/src/talos/values.test.ts +34 -6
- package/src/talos/values.ts +41 -7
- package/src/telemetry/fake-collector.ts +59 -0
- package/src/telemetry/http-client-sealing.test.ts +82 -0
- package/src/telemetry/index.ts +10 -0
- package/src/telemetry/layer.test.ts +204 -0
- package/src/telemetry/layer.ts +152 -0
- package/src/telemetry/redact-serialization.ts +178 -0
- package/src/telemetry/redact.test.ts +124 -0
- package/src/telemetry/redact.ts +147 -0
- package/src/unifi/acl-rule-drift.ts +57 -0
- package/src/unifi/acl-rule-form.ts +135 -0
- package/src/unifi/acl-rule-ordering-form.ts +79 -0
- package/src/unifi/acl-rule-ordering.test.ts +141 -0
- package/src/unifi/acl-rule-ordering.ts +70 -0
- package/src/unifi/acl-rule.test.ts +183 -0
- package/src/unifi/acl-rule.ts +61 -0
- package/src/unifi/discriminated-filter-decode.test.ts +134 -0
- package/src/unifi/dns-policy-drift.ts +48 -0
- package/src/unifi/dns-policy-form.ts +142 -0
- package/src/unifi/dns-policy.test.ts +154 -0
- package/src/unifi/dns-policy.ts +67 -0
- package/src/unifi/drift.test.ts +78 -0
- package/src/unifi/drift.ts +56 -0
- package/src/unifi/errors-and-secrets.test.ts +127 -0
- package/src/unifi/fake-unifi.ts +5 -1
- package/src/unifi/firewall-policy-drift.test.ts +151 -0
- package/src/unifi/firewall-policy-drift.ts +55 -0
- package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
- package/src/unifi/firewall-policy-form.ts +146 -0
- package/src/unifi/firewall-policy-ordering-form.ts +111 -0
- package/src/unifi/firewall-policy-ordering.test.ts +185 -0
- package/src/unifi/firewall-policy-ordering.ts +87 -0
- package/src/unifi/firewall-policy.test.ts +183 -0
- package/src/unifi/firewall-policy.ts +73 -0
- package/src/unifi/firewall-zone-form.ts +31 -2
- package/src/unifi/firewall-zone.test.ts +21 -1
- package/src/unifi/firewall-zone.ts +1 -1
- package/src/unifi/get-only-guard.test.ts +165 -0
- package/src/unifi/index.ts +78 -1
- package/src/unifi/network-drift.test.ts +207 -0
- package/src/unifi/network-drift.ts +80 -0
- package/src/unifi/network-form.ts +11 -39
- package/src/unifi/network.test.ts +7 -82
- package/src/unifi/network.ts +6 -6
- package/src/unifi/paginate.test.ts +252 -0
- package/src/unifi/paginate.ts +141 -0
- package/src/unifi/providers.ts +19 -1
- package/src/unifi/resource.ts +73 -3
- package/src/unifi/scan-source.ts +64 -0
- package/src/unifi/traffic-matching-list-drift.ts +40 -0
- package/src/unifi/traffic-matching-list-form.ts +77 -0
- package/src/unifi/traffic-matching-list.test.ts +202 -0
- package/src/unifi/traffic-matching-list.ts +78 -0
- package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
- package/src/unifi/wifi-broadcast-drift.ts +69 -0
- package/src/unifi/wifi-broadcast-form.ts +211 -0
- package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
- package/src/unifi/wifi-broadcast.test.ts +155 -0
- package/src/unifi/wifi-broadcast.ts +90 -0
- package/src/unifi/write-op-reference.test.ts +190 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.FirewallPolicyOrdering`'s `spec` against a fake UniFi Network API — mirrors
|
|
3
|
+
* `acl-rule-ordering.test.ts`. The two things this file exists to prove that no other family test
|
|
4
|
+
* does: the zone-pair QUERY identity actually reaches the wire (unlike `acl-rule-ordering.ts`'s
|
|
5
|
+
* one-per-site path with no query at all), and T5's "order IS the value, never `sortedSet`" rule
|
|
6
|
+
* holds independently for BOTH `beforeSystemDefined` and `afterSystemDefined`.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
import type * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
10
|
+
import * as Retry from '@distilled.cloud/unifi-network/Retry';
|
|
11
|
+
import * as Effect from 'effect/Effect';
|
|
12
|
+
import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
|
|
13
|
+
import { attributesOf, driftOf, matches } from './firewall-policy-ordering-form.ts';
|
|
14
|
+
import {
|
|
15
|
+
type FirewallPolicyOrderingProps,
|
|
16
|
+
declareFirewallPolicyOrdering,
|
|
17
|
+
spec,
|
|
18
|
+
} from './firewall-policy-ordering.ts';
|
|
19
|
+
import { unifiOperations } from './resource.ts';
|
|
20
|
+
|
|
21
|
+
const ORDERING_PATH = '/proxy/network/integration/v1/sites/site-1/firewall/policies/ordering';
|
|
22
|
+
|
|
23
|
+
const liveOrdering = (
|
|
24
|
+
overrides: Partial<firewall.IntegrationFirewallPolicyOrderingDto> = {},
|
|
25
|
+
): firewall.IntegrationFirewallPolicyOrderingDto => ({
|
|
26
|
+
orderedFirewallPolicyIds: {
|
|
27
|
+
beforeSystemDefined: ['fp-1', 'fp-2'],
|
|
28
|
+
afterSystemDefined: ['fp-9'],
|
|
29
|
+
},
|
|
30
|
+
...overrides,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const PROPS: FirewallPolicyOrderingProps = {
|
|
34
|
+
siteId: 'site-1',
|
|
35
|
+
sourceFirewallZoneId: 'zone-iot',
|
|
36
|
+
destinationFirewallZoneId: 'zone-external',
|
|
37
|
+
beforeSystemDefined: ['fp-1', 'fp-2'],
|
|
38
|
+
afterSystemDefined: ['fp-9'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe('Unifi.FirewallPolicyOrdering spec.fetchLive', () => {
|
|
42
|
+
test('a real GET response decodes into the typed live object, with the zone pair as a query', async () => {
|
|
43
|
+
const fake = fakeUnifi((method, url) =>
|
|
44
|
+
method === 'GET' && url.pathname === ORDERING_PATH
|
|
45
|
+
? Response.json(liveOrdering())
|
|
46
|
+
: fakeFailure(400, 'unexpected request'),
|
|
47
|
+
);
|
|
48
|
+
const live = await Effect.runPromise(
|
|
49
|
+
spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
50
|
+
);
|
|
51
|
+
expect(live?.orderedFirewallPolicyIds.beforeSystemDefined).toEqual(['fp-1', 'fp-2']);
|
|
52
|
+
expect(fake.seen).toHaveLength(1);
|
|
53
|
+
const sentUrl = new URL(`http://x${fake.seen[0]?.path}`);
|
|
54
|
+
expect(sentUrl.pathname).toBe(ORDERING_PATH);
|
|
55
|
+
expect(sentUrl.searchParams.get('sourceFirewallZoneId')).toBe('zone-iot');
|
|
56
|
+
expect(sentUrl.searchParams.get('destinationFirewallZoneId')).toBe('zone-external');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a genuine 404 folds to undefined', async () => {
|
|
60
|
+
const fake = fakeUnifi(() => fakeFailure(404, 'not found'));
|
|
61
|
+
const live = await Effect.runPromise(
|
|
62
|
+
spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
63
|
+
);
|
|
64
|
+
expect(live).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a 500 fails loudly -- only a genuine not-found may mean absent (T14)', async () => {
|
|
68
|
+
const fake = fakeUnifi(() => fakeFailure(500, 'boom'));
|
|
69
|
+
const failure = await Effect.runPromise(
|
|
70
|
+
Effect.flip(
|
|
71
|
+
spec.fetchLive(PROPS).pipe(Retry.none, Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
expect(failure._tag).toBe('InternalServerError');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('declareFirewallPolicyOrdering -- the declaration renderer', () => {
|
|
79
|
+
test('its output matches attributesOf(live) by construction, for any live object', () => {
|
|
80
|
+
const live = liveOrdering({
|
|
81
|
+
orderedFirewallPolicyIds: { beforeSystemDefined: ['fp-5'], afterSystemDefined: [] },
|
|
82
|
+
});
|
|
83
|
+
const declared = declareFirewallPolicyOrdering(live, 'site-1', 'zone-iot', 'zone-external');
|
|
84
|
+
const attrs = attributesOf(live, declared);
|
|
85
|
+
expect(matches(attrs, declared)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('T5 -- order IS the value, never sortedSet, independently on each half', () => {
|
|
90
|
+
test('an identical pair of lists is a noop', () => {
|
|
91
|
+
expect(driftOf(liveOrdering(), PROPS)).toEqual([]);
|
|
92
|
+
expect(matches(attributesOf(liveOrdering(), PROPS), PROPS)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('the SAME ids in a DIFFERENT order within beforeSystemDefined is real drift', () => {
|
|
96
|
+
const live = liveOrdering({
|
|
97
|
+
orderedFirewallPolicyIds: {
|
|
98
|
+
beforeSystemDefined: ['fp-2', 'fp-1'],
|
|
99
|
+
afterSystemDefined: ['fp-9'],
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
expect(matches(attributesOf(live, PROPS), PROPS)).toBe(false);
|
|
103
|
+
expect(driftOf(live, PROPS)).toEqual([
|
|
104
|
+
{ field: 'beforeSystemDefined', live: ['fp-2', 'fp-1'], declared: ['fp-1', 'fp-2'] },
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('a reorder within afterSystemDefined alone is caught, beforeSystemDefined untouched', () => {
|
|
109
|
+
const live = liveOrdering({
|
|
110
|
+
orderedFirewallPolicyIds: {
|
|
111
|
+
beforeSystemDefined: ['fp-1', 'fp-2'],
|
|
112
|
+
afterSystemDefined: ['fp-9', 'fp-10'],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const props: FirewallPolicyOrderingProps = { ...PROPS, afterSystemDefined: ['fp-10', 'fp-9'] };
|
|
116
|
+
expect(driftOf(live, props)).toEqual([
|
|
117
|
+
{ field: 'afterSystemDefined', live: ['fp-9', 'fp-10'], declared: ['fp-10', 'fp-9'] },
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('a policy moving from beforeSystemDefined to afterSystemDefined drifts on both fields', () => {
|
|
122
|
+
// The two halves can never swap through this endpoint by "reordering" alone -- membership moving
|
|
123
|
+
// across them is a genuine drift on each side independently, not one combined-list reorder.
|
|
124
|
+
const live = liveOrdering({
|
|
125
|
+
orderedFirewallPolicyIds: {
|
|
126
|
+
beforeSystemDefined: ['fp-1'],
|
|
127
|
+
afterSystemDefined: ['fp-2', 'fp-9'],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
expect(driftOf(live, PROPS)).toEqual([
|
|
131
|
+
{ field: 'beforeSystemDefined', live: ['fp-1'], declared: ['fp-1', 'fp-2'] },
|
|
132
|
+
{ field: 'afterSystemDefined', live: ['fp-2', 'fp-9'], declared: ['fp-9'] },
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('Unifi.FirewallPolicyOrdering write paths never reach the vendor API', () => {
|
|
138
|
+
const ops = unifiOperations(spec);
|
|
139
|
+
|
|
140
|
+
test('reconcile on an exact-match adoption (H6) makes no request but the one read', async () => {
|
|
141
|
+
const fake = fakeUnifi((method, url) =>
|
|
142
|
+
method === 'GET' && url.pathname === ORDERING_PATH
|
|
143
|
+
? Response.json(liveOrdering())
|
|
144
|
+
: fakeFailure(400, 'unexpected request'),
|
|
145
|
+
);
|
|
146
|
+
const exit = await Effect.runPromiseExit(
|
|
147
|
+
ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
148
|
+
);
|
|
149
|
+
expect(exit._tag).toBe('Success');
|
|
150
|
+
expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('reconcile on drift (a reorder) refuses instead of PUTting the whole pair', async () => {
|
|
154
|
+
const fake = fakeUnifi((method, url) =>
|
|
155
|
+
method === 'GET' && url.pathname === ORDERING_PATH
|
|
156
|
+
? Response.json(
|
|
157
|
+
liveOrdering({
|
|
158
|
+
orderedFirewallPolicyIds: {
|
|
159
|
+
beforeSystemDefined: ['fp-2', 'fp-1'],
|
|
160
|
+
afterSystemDefined: [],
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
: fakeFailure(400, 'unexpected request'),
|
|
165
|
+
);
|
|
166
|
+
const exit = await Effect.runPromiseExit(
|
|
167
|
+
ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
168
|
+
);
|
|
169
|
+
expect(exit._tag).toBe('Failure');
|
|
170
|
+
expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('delete always refuses -- no DELETE is ever sent', async () => {
|
|
174
|
+
const fake = fakeUnifi((method, url) =>
|
|
175
|
+
method === 'GET' && url.pathname === ORDERING_PATH
|
|
176
|
+
? Response.json(liveOrdering())
|
|
177
|
+
: fakeFailure(400, 'unexpected request'),
|
|
178
|
+
);
|
|
179
|
+
const exit = await Effect.runPromiseExit(
|
|
180
|
+
ops.destroy(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
181
|
+
);
|
|
182
|
+
expect(exit._tag).toBe('Failure');
|
|
183
|
+
expect(fake.seen).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.FirewallPolicyOrdering` — the ordered `before`/`afterSystemDefined` policy-id lists for
|
|
3
|
+
* ONE `(sourceFirewallZoneId, destinationFirewallZoneId)` zone pair (`getFirewallPolicyOrdering`,
|
|
4
|
+
* `GET /v1/sites/{siteId}/firewall/policies/ordering`), READ-ONLY (`policy.ts`). T5/I4's per-family
|
|
5
|
+
* variant: `docs/unifi.md`'s own header named this "a future `Unifi.FirewallPolicyOrdering`" as a
|
|
6
|
+
* separate, bigger PR (see `acl-rule-ordering.ts`'s header for the same forward reference, now
|
|
7
|
+
* resolved) — this is that resource.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ NOT ONE OBJECT PER SITE, UNLIKE `acl-rule-ordering.ts` — the identity is the TRIPLE
|
|
10
|
+
* `(siteId, sourceFirewallZoneId, destinationFirewallZoneId)`, both zone ids passed as QUERY
|
|
11
|
+
* parameters on the same path every time (`firewall.ts`'s `GetFirewallPolicyOrderingRequest`).
|
|
12
|
+
* There is no "list every zone pair with an ordering" endpoint — a caller (a future Lane C import)
|
|
13
|
+
* has to already know which pairs exist, typically by walking `firewall.getFirewallPolicies`
|
|
14
|
+
* (T9's own 424-live-policy scale — B0b's `pageAll`, see `firewall-policy.test.ts`'s own decode
|
|
15
|
+
* proof) and grouping by each policy's `source.zoneId`/`destination.zoneId`. Nothing here performs
|
|
16
|
+
* that discovery — this resource is one already-known pair's ordering, same as
|
|
17
|
+
* `firewall-policy.ts`/`acl-rule.ts` are one already-known object's own fields.
|
|
18
|
+
*/
|
|
19
|
+
import { Resource } from 'alchemy';
|
|
20
|
+
import { adopt } from 'alchemy/AdoptPolicy';
|
|
21
|
+
import * as Provider from 'alchemy/Provider';
|
|
22
|
+
import * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
23
|
+
import * as Effect from 'effect/Effect';
|
|
24
|
+
import {
|
|
25
|
+
type FirewallPolicyOrderingAttributes,
|
|
26
|
+
type FirewallPolicyOrderingProps,
|
|
27
|
+
attributesOf,
|
|
28
|
+
matches,
|
|
29
|
+
} from './firewall-policy-ordering-form.ts';
|
|
30
|
+
import type { UnifiRequirements, UnifiSpec } from './resource.ts';
|
|
31
|
+
import { unifiHandlers } from './resource.ts';
|
|
32
|
+
|
|
33
|
+
export type {
|
|
34
|
+
FirewallPolicyOrderingAttributes,
|
|
35
|
+
FirewallPolicyOrderingProps,
|
|
36
|
+
} from './firewall-policy-ordering-form.ts';
|
|
37
|
+
export { declareFirewallPolicyOrdering, driftOf } from './firewall-policy-ordering-form.ts';
|
|
38
|
+
|
|
39
|
+
export interface UnifiFirewallPolicyOrdering extends Resource<
|
|
40
|
+
'Unifi.FirewallPolicyOrdering',
|
|
41
|
+
FirewallPolicyOrderingProps,
|
|
42
|
+
FirewallPolicyOrderingAttributes,
|
|
43
|
+
never,
|
|
44
|
+
UnifiRequirements
|
|
45
|
+
> {}
|
|
46
|
+
|
|
47
|
+
export const UnifiFirewallPolicyOrdering = Resource<UnifiFirewallPolicyOrdering>(
|
|
48
|
+
'Unifi.FirewallPolicyOrdering',
|
|
49
|
+
{ defaultRemovalPolicy: 'retain' },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/** `firewallPolicyOrdering('iot-to-external', props)` — `adopt(true)` piped on by default (H5); see
|
|
53
|
+
* `network.ts`. */
|
|
54
|
+
export const firewallPolicyOrdering = (id: string, props: FirewallPolicyOrderingProps) =>
|
|
55
|
+
UnifiFirewallPolicyOrdering(id, props).pipe(adopt(true));
|
|
56
|
+
|
|
57
|
+
/** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
|
|
58
|
+
export const spec: UnifiSpec<
|
|
59
|
+
FirewallPolicyOrderingProps,
|
|
60
|
+
firewall.IntegrationFirewallPolicyOrderingDto,
|
|
61
|
+
FirewallPolicyOrderingAttributes,
|
|
62
|
+
firewall.GetFirewallPolicyOrderingError
|
|
63
|
+
> = {
|
|
64
|
+
type: 'Unifi.FirewallPolicyOrdering',
|
|
65
|
+
describe: (props) =>
|
|
66
|
+
`sites/${props.siteId}/firewall/policies/ordering` +
|
|
67
|
+
`?sourceFirewallZoneId=${props.sourceFirewallZoneId}` +
|
|
68
|
+
`&destinationFirewallZoneId=${props.destinationFirewallZoneId}`,
|
|
69
|
+
fetchLive: (props) =>
|
|
70
|
+
firewall
|
|
71
|
+
.getFirewallPolicyOrdering({
|
|
72
|
+
siteId: props.siteId,
|
|
73
|
+
sourceFirewallZoneId: props.sourceFirewallZoneId,
|
|
74
|
+
destinationFirewallZoneId: props.destinationFirewallZoneId,
|
|
75
|
+
})
|
|
76
|
+
.pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
|
|
77
|
+
attributes: attributesOf,
|
|
78
|
+
matches,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const handlers = unifiHandlers(spec);
|
|
82
|
+
|
|
83
|
+
export const UnifiFirewallPolicyOrderingProvider = () =>
|
|
84
|
+
Provider.effect(
|
|
85
|
+
UnifiFirewallPolicyOrdering,
|
|
86
|
+
Effect.succeed(UnifiFirewallPolicyOrdering.Provider.of(handlers)),
|
|
87
|
+
);
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.FirewallPolicy`'s `spec` against a fake UniFi Network API — mirrors `acl-rule.test.ts`/
|
|
3
|
+
* `dns-policy.test.ts`.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ `driftOf`/`matches` coverage lives in `firewall-policy-drift.test.ts` (the field-level table
|
|
6
|
+
* plus I2's per-field completeness check) and `firewall-policy-filter-roundtrip.test.ts` (I1's
|
|
7
|
+
* filter-bearing round trip) — both split out to keep this file under the house 250-line cap once
|
|
8
|
+
* that coverage grew, same split `network-drift.test.ts` is from `network.test.ts`, not a
|
|
9
|
+
* different family. Red team, 2026-09-26.
|
|
10
|
+
*
|
|
11
|
+
* The last `describe` block is this family's own decode-proof of B0b's `pageAll` (T9) — not used by
|
|
12
|
+
* `spec.fetchLive` itself (`getFirewallPolicy` reads one policy by id directly, same as
|
|
13
|
+
* `acl-rule.ts`), but a real console's FirewallPolicy list is the one T9 names by scale (424 live
|
|
14
|
+
* policies, `alchemy-ledger-network.md:13`) — `paginate.test.ts`'s own "against the real SDK" section
|
|
15
|
+
* proves the pager's decode path against `networks.getNetworksOverviewPage`; this proves it against
|
|
16
|
+
* `firewall.getFirewallPolicies`'s own page shape too, so a future Lane C import walking this
|
|
17
|
+
* family's full list is walking a shape this suite has already exercised end to end.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, expect, test } from 'bun:test';
|
|
20
|
+
import * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
21
|
+
import * as Retry from '@distilled.cloud/unifi-network/Retry';
|
|
22
|
+
import * as Effect from 'effect/Effect';
|
|
23
|
+
import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
|
|
24
|
+
import { attributesOf } from './firewall-policy-form.ts';
|
|
25
|
+
import { driftOf, matches } from './firewall-policy-drift.ts';
|
|
26
|
+
import { type FirewallPolicyProps, declareFirewallPolicy, spec } from './firewall-policy.ts';
|
|
27
|
+
import { pageAll } from './paginate.ts';
|
|
28
|
+
import { unifiOperations } from './resource.ts';
|
|
29
|
+
|
|
30
|
+
const POLICY_PATH = '/proxy/network/integration/v1/sites/site-1/firewall/policies/fp-1';
|
|
31
|
+
|
|
32
|
+
const liveFirewallPolicy = (
|
|
33
|
+
overrides: Partial<firewall.FirewallPolicy> = {},
|
|
34
|
+
): firewall.FirewallPolicy => ({
|
|
35
|
+
action: { type: 'ALLOW' },
|
|
36
|
+
connectionStateFilter: ['ESTABLISHED', 'RELATED'],
|
|
37
|
+
description: 'Allow IoT devices outbound to the internet',
|
|
38
|
+
destination: { zoneId: 'zone-external' },
|
|
39
|
+
enabled: true,
|
|
40
|
+
id: 'fp-1',
|
|
41
|
+
index: 3,
|
|
42
|
+
ipProtocolScope: { ipVersion: 'IPV4' },
|
|
43
|
+
loggingEnabled: false,
|
|
44
|
+
metadata: { origin: 'USER' },
|
|
45
|
+
name: 'Allow IoT to Internet',
|
|
46
|
+
source: { zoneId: 'zone-iot' },
|
|
47
|
+
...overrides,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const PROPS: FirewallPolicyProps = {
|
|
51
|
+
siteId: 'site-1',
|
|
52
|
+
firewallPolicyId: 'fp-1',
|
|
53
|
+
action: { type: 'ALLOW' },
|
|
54
|
+
connectionStateFilter: ['ESTABLISHED', 'RELATED'],
|
|
55
|
+
description: 'Allow IoT devices outbound to the internet',
|
|
56
|
+
destination: { zoneId: 'zone-external' },
|
|
57
|
+
enabled: true,
|
|
58
|
+
ipProtocolScope: { ipVersion: 'IPV4' },
|
|
59
|
+
loggingEnabled: false,
|
|
60
|
+
name: 'Allow IoT to Internet',
|
|
61
|
+
source: { zoneId: 'zone-iot' },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe('Unifi.FirewallPolicy spec.fetchLive', () => {
|
|
65
|
+
test('a real GET response decodes into the typed live object', async () => {
|
|
66
|
+
const fake = fakeUnifi((method, url) =>
|
|
67
|
+
method === 'GET' && url.pathname === POLICY_PATH
|
|
68
|
+
? Response.json(liveFirewallPolicy())
|
|
69
|
+
: fakeFailure(400, 'unexpected request'),
|
|
70
|
+
);
|
|
71
|
+
const live = await Effect.runPromise(
|
|
72
|
+
spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
73
|
+
);
|
|
74
|
+
expect(live?.id).toBe('fp-1');
|
|
75
|
+
expect(fake.seen).toEqual([{ method: 'GET', path: POLICY_PATH }]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('a genuine 404 folds to undefined -- never a false "unreadable" on anything else', async () => {
|
|
79
|
+
const fake = fakeUnifi(() => fakeFailure(404, 'not found'));
|
|
80
|
+
const live = await Effect.runPromise(
|
|
81
|
+
spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
82
|
+
);
|
|
83
|
+
expect(live).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a 500 fails loudly -- only a genuine not-found may mean absent (T14)', async () => {
|
|
87
|
+
// ⚠️ `Retry.none`: the default policy retries a 500 indefinitely with backoff -- this test
|
|
88
|
+
// asserts the FAILURE, not the retry schedule.
|
|
89
|
+
const fake = fakeUnifi(() => fakeFailure(500, 'boom'));
|
|
90
|
+
const failure = await Effect.runPromise(
|
|
91
|
+
Effect.flip(
|
|
92
|
+
spec.fetchLive(PROPS).pipe(Retry.none, Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
expect(failure._tag).toBe('InternalServerError');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('declareFirewallPolicy -- the declaration renderer', () => {
|
|
100
|
+
test('its output matches attributesOf(live) by construction, for any live object', () => {
|
|
101
|
+
const live = liveFirewallPolicy({
|
|
102
|
+
connectionStateFilter: ['RELATED', 'ESTABLISHED'],
|
|
103
|
+
index: 9,
|
|
104
|
+
});
|
|
105
|
+
const declared = declareFirewallPolicy(live, 'site-1');
|
|
106
|
+
const attrs = attributesOf(live, declared);
|
|
107
|
+
expect(matches(attrs, declared)).toBe(true);
|
|
108
|
+
expect(driftOf(live, declared)).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('Unifi.FirewallPolicy write paths never reach the vendor API', () => {
|
|
113
|
+
const ops = unifiOperations(spec);
|
|
114
|
+
|
|
115
|
+
test('reconcile on an exact-match adoption (H6) makes no request but the one read', async () => {
|
|
116
|
+
const fake = fakeUnifi((method, url) =>
|
|
117
|
+
method === 'GET' && url.pathname === POLICY_PATH
|
|
118
|
+
? Response.json(liveFirewallPolicy())
|
|
119
|
+
: fakeFailure(400, 'unexpected request'),
|
|
120
|
+
);
|
|
121
|
+
const exit = await Effect.runPromiseExit(
|
|
122
|
+
ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
123
|
+
);
|
|
124
|
+
expect(exit._tag).toBe('Success');
|
|
125
|
+
expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('reconcile on drift refuses instead of PATCHing/PUTting a merged body', async () => {
|
|
129
|
+
const fake = fakeUnifi((method, url) =>
|
|
130
|
+
method === 'GET' && url.pathname === POLICY_PATH
|
|
131
|
+
? Response.json(liveFirewallPolicy({ enabled: false }))
|
|
132
|
+
: fakeFailure(400, 'unexpected request'),
|
|
133
|
+
);
|
|
134
|
+
const exit = await Effect.runPromiseExit(
|
|
135
|
+
ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
136
|
+
);
|
|
137
|
+
expect(exit._tag).toBe('Failure');
|
|
138
|
+
expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('delete always refuses -- no DELETE is ever sent', async () => {
|
|
142
|
+
const fake = fakeUnifi((method, url) =>
|
|
143
|
+
method === 'GET' && url.pathname === POLICY_PATH
|
|
144
|
+
? Response.json(liveFirewallPolicy())
|
|
145
|
+
: fakeFailure(400, 'unexpected request'),
|
|
146
|
+
);
|
|
147
|
+
const exit = await Effect.runPromiseExit(
|
|
148
|
+
ops.destroy(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
149
|
+
);
|
|
150
|
+
expect(exit._tag).toBe('Failure');
|
|
151
|
+
expect(fake.seen).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("T9/B0b -- pageAll walks this family's own page shape (decode proof)", () => {
|
|
156
|
+
test('every row across several pages of getFirewallPolicies decodes, in order', async () => {
|
|
157
|
+
const rows = Array.from({ length: 7 }, (_, i) =>
|
|
158
|
+
liveFirewallPolicy({ id: `fp-${i}`, index: i }),
|
|
159
|
+
);
|
|
160
|
+
const fake = fakeUnifi((method, url) => {
|
|
161
|
+
if (
|
|
162
|
+
method !== 'GET' ||
|
|
163
|
+
url.pathname !== '/proxy/network/integration/v1/sites/site-1/firewall/policies'
|
|
164
|
+
) {
|
|
165
|
+
return fakeFailure(400, 'unexpected request');
|
|
166
|
+
}
|
|
167
|
+
const offset = Number(url.searchParams.get('offset') ?? '0');
|
|
168
|
+
const limit = Number(url.searchParams.get('limit') ?? '3');
|
|
169
|
+
const data = rows.slice(offset, offset + limit);
|
|
170
|
+
return Response.json({ count: data.length, data, limit, offset, totalCount: rows.length });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = await Effect.runPromise(
|
|
174
|
+
pageAll(firewall.getFirewallPolicies, { siteId: 'site-1', limit: 3 }).pipe(
|
|
175
|
+
Effect.provide(fakeUnifiLayer(fake.fetch)),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(result.map((p) => p.id)).toEqual(rows.map((r) => r.id));
|
|
180
|
+
expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
|
|
181
|
+
expect(fake.seen).toHaveLength(3);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.FirewallPolicy` — one firewall policy on a UniFi Network site, READ-ONLY (`policy.ts`).
|
|
3
|
+
* `Unifi.FirewallPolicyOrdering` (`firewall-policy-ordering.ts`) is the sibling resource for policy
|
|
4
|
+
* ORDER within a zone pair (T5) — one per `(sourceFirewallZoneId, destinationFirewallZoneId)`, never
|
|
5
|
+
* this file's concern. `docs/unifi.md`'s own header names this pair as "a bigger, separate PR";
|
|
6
|
+
* this is that PR — see `docs/unifi-firewall-policy.md` for the full breakdown.
|
|
7
|
+
*
|
|
8
|
+
* ★ T11 GATE, CHECKED 2026-09-26: the `Firewall` tag's 13 operations (5 already covered for
|
|
9
|
+
* `Unifi.FirewallZone` in `docs/unifi.md`, 8 new here for `FirewallPolicy` — `getFirewallPolicies`,
|
|
10
|
+
* `createFirewallPolicy`, `getFirewallPolicyOrdering`, `updateFirewallPolicyOrdering`,
|
|
11
|
+
* `deleteFirewallPolicy`, `getFirewallPolicy`, `patchFirewallPolicy`, `updateFirewallPolicy`) are
|
|
12
|
+
* byte-identical between 10.4.57 and 10.6.97, full raw operation objects; the 106-schema closure
|
|
13
|
+
* reachable from all 13 (`$ref` + `discriminator.mapping`) shares zero names with the 14 schemas
|
|
14
|
+
* that DID change elsewhere in the document. Corroborated independently by the A3 changeset's own
|
|
15
|
+
* broader 25-operation/139-schema re-check (`.changeset/unifi-network-a3-typed-discriminator-filters.md`).
|
|
16
|
+
* Full breakdown: `docs/unifi-firewall-policy.md`.
|
|
17
|
+
*/
|
|
18
|
+
import { Resource } from 'alchemy';
|
|
19
|
+
import { adopt } from 'alchemy/AdoptPolicy';
|
|
20
|
+
import * as Provider from 'alchemy/Provider';
|
|
21
|
+
import * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
22
|
+
import * as Effect from 'effect/Effect';
|
|
23
|
+
import {
|
|
24
|
+
type FirewallPolicyAttributes,
|
|
25
|
+
type FirewallPolicyProps,
|
|
26
|
+
attributesOf,
|
|
27
|
+
} from './firewall-policy-form.ts';
|
|
28
|
+
import { matches } from './firewall-policy-drift.ts';
|
|
29
|
+
import type { UnifiRequirements, UnifiSpec } from './resource.ts';
|
|
30
|
+
import { unifiHandlers } from './resource.ts';
|
|
31
|
+
|
|
32
|
+
export type { FirewallPolicyAttributes, FirewallPolicyProps } from './firewall-policy-form.ts';
|
|
33
|
+
export { declareFirewallPolicy } from './firewall-policy-form.ts';
|
|
34
|
+
export { driftOf } from './firewall-policy-drift.ts';
|
|
35
|
+
|
|
36
|
+
export interface UnifiFirewallPolicy extends Resource<
|
|
37
|
+
'Unifi.FirewallPolicy',
|
|
38
|
+
FirewallPolicyProps,
|
|
39
|
+
FirewallPolicyAttributes,
|
|
40
|
+
never,
|
|
41
|
+
UnifiRequirements
|
|
42
|
+
> {}
|
|
43
|
+
|
|
44
|
+
export const UnifiFirewallPolicy = Resource<UnifiFirewallPolicy>('Unifi.FirewallPolicy', {
|
|
45
|
+
defaultRemovalPolicy: 'retain',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** `firewallPolicy('block-iot-wan', props)` — `adopt(true)` piped on by default (H5); see
|
|
49
|
+
* `network.ts`. */
|
|
50
|
+
export const firewallPolicy = (id: string, props: FirewallPolicyProps) =>
|
|
51
|
+
UnifiFirewallPolicy(id, props).pipe(adopt(true));
|
|
52
|
+
|
|
53
|
+
/** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
|
|
54
|
+
export const spec: UnifiSpec<
|
|
55
|
+
FirewallPolicyProps,
|
|
56
|
+
firewall.FirewallPolicy,
|
|
57
|
+
FirewallPolicyAttributes,
|
|
58
|
+
firewall.GetFirewallPolicyError
|
|
59
|
+
> = {
|
|
60
|
+
type: 'Unifi.FirewallPolicy',
|
|
61
|
+
describe: (props) => `sites/${props.siteId}/firewall/policies/${props.firewallPolicyId}`,
|
|
62
|
+
fetchLive: (props) =>
|
|
63
|
+
firewall
|
|
64
|
+
.getFirewallPolicy({ siteId: props.siteId, firewallPolicyId: props.firewallPolicyId })
|
|
65
|
+
.pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
|
|
66
|
+
attributes: attributesOf,
|
|
67
|
+
matches,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const handlers = unifiHandlers(spec);
|
|
71
|
+
|
|
72
|
+
export const UnifiFirewallPolicyProvider = () =>
|
|
73
|
+
Provider.effect(UnifiFirewallPolicy, Effect.succeed(UnifiFirewallPolicy.Provider.of(handlers)));
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* the same reasoning `proxmox/user-wire.ts`'s `groupSet` gives for PVE's `groups`.
|
|
14
14
|
*/
|
|
15
15
|
import type * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
16
|
+
import { makeDriftOf } from './drift.ts';
|
|
16
17
|
|
|
17
18
|
export interface FirewallZoneProps {
|
|
18
19
|
siteId: string;
|
|
@@ -45,9 +46,37 @@ export const attributesOf = (
|
|
|
45
46
|
metadataOrigin: live.metadata.origin,
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* B6: `fieldDrift` (`makeDriftOf`, `drift.ts`) is now the ONLY comparison this family has — MEDIUM-4
|
|
51
|
+
* (red team, 2026-09-26): a hand-written `matches` kept beside `fieldDrift` is two field lists an
|
|
52
|
+
* edit to one can silently leave out of sync with the other (a mutant deleting a `fieldDrift` entry
|
|
53
|
+
* left every test green, because `matches` never consulted it). `matches(attrs, props) ===
|
|
54
|
+
* (driftOf(live, props).length === 0)` is now true BY CONSTRUCTION, not just by a test that happens
|
|
55
|
+
* to check both. `networkIds`' custom `equal` is the set comparison the family has always used —
|
|
56
|
+
* see the header — and its reported `live`/`declared` values in a `driftOf` result are the SORTED
|
|
57
|
+
* arrays, not the joined strings this `equal` compares internally, so a real diff reads as a set
|
|
58
|
+
* difference, not two opaque strings.
|
|
59
|
+
*/
|
|
60
|
+
const fieldDrift = makeDriftOf<FirewallZoneAttributes, FirewallZoneProps>([
|
|
61
|
+
{ field: 'name', live: (a) => a.name, declared: (p) => p.name },
|
|
62
|
+
{
|
|
63
|
+
field: 'networkIds',
|
|
64
|
+
live: (a) => a.networkIds,
|
|
65
|
+
declared: (p) => sortedSet(p.networkIds),
|
|
66
|
+
equal: (live, declared) => (live as string[]).join(',') === (declared as string[]).join(','),
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
|
|
48
70
|
export const matches = (attributes: FirewallZoneAttributes, props: FirewallZoneProps): boolean =>
|
|
49
|
-
attributes.
|
|
50
|
-
|
|
71
|
+
fieldDrift(attributes, props).length === 0;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Per-field live-vs-declared drift straight from one live read — the task's own API
|
|
75
|
+
* (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
|
|
76
|
+
* `attributesOf` itself just to call this.
|
|
77
|
+
*/
|
|
78
|
+
export const driftOf = (live: firewall.FirewallZone, props: FirewallZoneProps) =>
|
|
79
|
+
fieldDrift(attributesOf(live, props), props);
|
|
51
80
|
|
|
52
81
|
/**
|
|
53
82
|
* The declaration renderer (task spec: "given one live object, return the props a declaration
|
|
@@ -5,7 +5,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
5
5
|
import type * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
6
6
|
import * as Effect from 'effect/Effect';
|
|
7
7
|
import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
|
|
8
|
-
import { attributesOf, matches } from './firewall-zone-form.ts';
|
|
8
|
+
import { attributesOf, driftOf, matches } from './firewall-zone-form.ts';
|
|
9
9
|
import { type FirewallZoneProps, declareFirewallZone, spec } from './firewall-zone.ts';
|
|
10
10
|
import { unifiOperations } from './resource.ts';
|
|
11
11
|
|
|
@@ -59,6 +59,26 @@ describe('declareFirewallZone -- the declaration renderer', () => {
|
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
describe('driftOf -- B6 field-level drift, straight from one live read', () => {
|
|
63
|
+
test('a live object that matches its declaration drifts on nothing, order included', () => {
|
|
64
|
+
// `networkIds` in a different order is a noop for `matches` (the header's own rule) --
|
|
65
|
+
// `driftOf` must agree, not report a set that only LOOKS different because of order.
|
|
66
|
+
expect(driftOf(liveZone(), PROPS)).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a real name change reports its own field, live and declared', () => {
|
|
70
|
+
const live = liveZone({ name: 'Garage' });
|
|
71
|
+
expect(driftOf(live, PROPS)).toEqual([{ field: 'name', live: 'Garage', declared: 'Cameras' }]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('a genuine networkIds difference reports SORTED arrays, not joined strings', () => {
|
|
75
|
+
const live = liveZone({ networkIds: ['net-1'] });
|
|
76
|
+
expect(driftOf(live, PROPS)).toEqual([
|
|
77
|
+
{ field: 'networkIds', live: ['net-1'], declared: ['net-1', 'net-2'] },
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
62
82
|
describe('Unifi.FirewallZone write paths never reach the vendor API', () => {
|
|
63
83
|
test('reconcile on an exact-match adoption (H6) sends only the one read', async () => {
|
|
64
84
|
const fake = fakeUnifi((method, url) =>
|
|
@@ -25,7 +25,7 @@ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
|
|
|
25
25
|
import { unifiHandlers } from './resource.ts';
|
|
26
26
|
|
|
27
27
|
export type { FirewallZoneAttributes, FirewallZoneProps } from './firewall-zone-form.ts';
|
|
28
|
-
export { declareFirewallZone } from './firewall-zone-form.ts';
|
|
28
|
+
export { declareFirewallZone, driftOf } from './firewall-zone-form.ts';
|
|
29
29
|
|
|
30
30
|
export interface UnifiFirewallZone extends Resource<
|
|
31
31
|
'Unifi.FirewallZone',
|