Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ce217ffe9 | ||
| 47a3b5d3d1 | |||
|
|
b034ee8854 | ||
| 5b9e013947 | |||
|
|
590bfa9968 | ||
| 008b1498ab | |||
|
|
38819660ef |
11 changed files with 437 additions and 29 deletions
|
|
@ -19,6 +19,8 @@ go vet ./... # vet check
|
|||
- `go test -race ./...` and `go vet ./...` must pass before commit
|
||||
- Conventional commits: `type(scope): description`
|
||||
- Co-Author: `Co-Authored-By: Virgil <virgil@lethean.io>`
|
||||
- Errors must use `log.E()` from `go-log`, never `fmt.Errorf`
|
||||
- Config tests must isolate from real `~/.core/config.yaml` via `t.Setenv("HOME", t.TempDir())`
|
||||
|
||||
## Docs
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ config resolution from file, environment, and CLI flags.
|
|||
|
||||
```
|
||||
go-netops/
|
||||
├── cmd/unifi/ # CLI commands (cobra)
|
||||
│ ├── cmd_unifi.go # Root 'unifi' command registration
|
||||
│ ├── cmd_clients.go # 'unifi clients' command
|
||||
│ ├── cmd_config.go # 'unifi config' command
|
||||
│ ├── cmd_devices.go # 'unifi devices' command
|
||||
│ ├── cmd_networks.go # 'unifi networks' command
|
||||
│ ├── cmd_routes.go # 'unifi routes' command
|
||||
│ └── cmd_sites.go # 'unifi sites' command
|
||||
├── unifi/
|
||||
│ ├── client.go # Client struct and constructor (New)
|
||||
│ ├── clients.go # Connected client queries with filtering
|
||||
|
|
@ -22,7 +30,9 @@ go-netops/
|
|||
│ ├── routes.go # Gateway routing table queries
|
||||
│ ├── sites.go # Site listing
|
||||
│ ├── client_test.go # Client creation tests
|
||||
│ └── config_test.go # Config resolution and persistence tests
|
||||
│ ├── config_test.go # Config resolution and persistence tests
|
||||
│ ├── networks_test.go # Network query tests
|
||||
│ └── routes_test.go # Route query and type name tests
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ Module: `forge.lthn.ai/core/go-netops`
|
|||
|
||||
| Tool | Version | Notes |
|
||||
|------------|-----------|-----------------------------------|
|
||||
| Go | 1.25+ | Module uses `go 1.25.5` |
|
||||
| core/go | local | `replace` directive in go.mod |
|
||||
| Go | 1.26+ | Module uses `go 1.26.0` |
|
||||
|
||||
The `go.mod` contains a `replace` directive pointing at `../go` for the
|
||||
`forge.lthn.ai/core/go` dependency. Ensure the `core/go` repository is
|
||||
checked out alongside this one (the standard workspace layout handles this).
|
||||
Dependencies are resolved from the Forge registry (`forge.lthn.ai`).
|
||||
|
||||
## Build and Test
|
||||
|
||||
|
|
@ -60,19 +57,12 @@ Config tests isolate the environment by:
|
|||
|
||||
### What Is Not Tested
|
||||
|
||||
The query methods (`GetClients`, `GetDevices`, `GetDeviceList`, `GetNetworks`,
|
||||
`GetRoutes`, `GetSites`) require a live or fully-mocked UniFi controller and
|
||||
are not covered by unit tests. Current coverage is **39%**.
|
||||
The client-discovery methods (`GetClients`, `GetDevices`, `GetDeviceList`,
|
||||
`GetSites`) delegate to the unpoller SDK and would require integration tests
|
||||
against a real or emulated controller to cover meaningfully.
|
||||
|
||||
## Coverage
|
||||
|
||||
```
|
||||
ok forge.lthn.ai/core/go-netops/unifi coverage: 39.0% of statements
|
||||
```
|
||||
|
||||
Coverage is concentrated on the config resolution and client construction paths.
|
||||
The query methods delegate to the unpoller SDK and would require integration
|
||||
tests against a real or emulated controller to cover meaningfully.
|
||||
`GetNetworks`, `GetRoutes`, and `RouteTypeName` are covered by unit tests
|
||||
using httptest mocks and direct assertions.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
|
|
|
|||
16
go.mod
16
go.mod
|
|
@ -1,20 +1,20 @@
|
|||
module forge.lthn.ai/core/go-netops
|
||||
module dappco.re/go/core/netops
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/cli v0.3.5
|
||||
forge.lthn.ai/core/config v0.1.6
|
||||
forge.lthn.ai/core/go-log v0.0.4
|
||||
dappco.re/go/core/cli v0.3.5
|
||||
dappco.re/go/core/config v0.1.6
|
||||
dappco.re/go/core/log v0.0.4
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/unpoller/unifi/v5 v5.20.0
|
||||
)
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/go v0.3.1 // indirect
|
||||
forge.lthn.ai/core/go-i18n v0.1.5 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.4 // indirect
|
||||
forge.lthn.ai/core/go-io v0.1.5 // indirect
|
||||
dappco.re/go/core v0.3.1 // indirect
|
||||
dappco.re/go/core/i18n v0.1.5 // indirect
|
||||
dappco.re/go/core/inference v0.1.4 // indirect
|
||||
dappco.re/go/core/io v0.1.5 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/brianvoe/gofakeit/v6 v6.28.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||
|
|
|
|||
12
specs/cmd/unifi.md
Normal file
12
specs/cmd/unifi.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# unifi
|
||||
**Import:** `forge.lthn.ai/core/go-netops/cmd/unifi`
|
||||
**Files:** 7
|
||||
|
||||
## Types
|
||||
|
||||
This package exports no structs, interfaces, or type aliases.
|
||||
|
||||
## Functions
|
||||
|
||||
### `AddUniFiCommands(root *cli.Command)`
|
||||
Registers the top-level `unifi` CLI command on `root` and attaches the package's `config`, `clients`, `devices`, `networks`, `routes`, and `sites` subcommands.
|
||||
98
specs/unifi.md
Normal file
98
specs/unifi.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# unifi
|
||||
**Import:** `forge.lthn.ai/core/go-netops/unifi`
|
||||
**Files:** 11
|
||||
|
||||
## Types
|
||||
|
||||
### `Client`
|
||||
Wraps the underlying `*github.com/unpoller/unifi/v5.Unifi` client together with the controller URL used to create it. The struct has no exported fields.
|
||||
|
||||
Methods:
|
||||
- `API() *uf.Unifi`: Returns the underlying SDK client stored on the wrapper.
|
||||
- `URL() string`: Returns the controller URL captured when the client was constructed.
|
||||
- `GetClients(filter ClientFilter) ([]*uf.Client, error)`: Resolves the requested site scope with `GetSites`, fetches clients from the SDK, and optionally filters the result by wired or wireless state using `uf.Client.IsWired.Val`.
|
||||
- `GetDevices(siteName string) (*uf.Devices, error)`: Resolves the site filter and returns the raw `*uf.Devices` container from the SDK.
|
||||
- `GetDeviceList(siteName, deviceType string) ([]DeviceInfo, error)`: Flattens the `UAPs`, `USWs`, `USGs`, `UDMs`, and `UXGs` slices from `*uf.Devices` into `DeviceInfo` values. `deviceType` accepts `uap`, `usw`, `usg`, `udm`, or `uxg`; an empty value includes every supported device group.
|
||||
- `GetNetworks(siteName string) ([]NetworkConf, error)`: Calls the controller endpoint `/api/s/{site}/rest/networkconf` directly, defaults an empty site name to `default`, unmarshals the JSON `data` payload, and returns the resulting `[]NetworkConf`.
|
||||
- `GetRoutes(siteName string) ([]Route, error)`: Calls the controller endpoint `/api/s/{site}/stat/routing` directly, defaults an empty site name to `default`, path-escapes the site name, unmarshals the JSON `data` payload, and returns the resulting `[]Route`.
|
||||
- `GetSites() ([]*uf.Site, error)`: Returns the sites exposed by the controller through the underlying SDK client.
|
||||
|
||||
### `ClientFilter`
|
||||
Controls how `(*Client).GetClients` scopes and filters client results.
|
||||
|
||||
Fields:
|
||||
- `Site string`: Site name to query. An empty value means all sites returned by `GetSites`.
|
||||
- `Wired bool`: When true, include wired clients.
|
||||
- `Wireless bool`: When true, include wireless clients.
|
||||
|
||||
### `DeviceInfo`
|
||||
Flat infrastructure-device view returned by `(*Client).GetDeviceList`.
|
||||
|
||||
Fields:
|
||||
- `Name string`: Device name from the controller.
|
||||
- `IP string`: Device IP address.
|
||||
- `Mac string`: Device MAC address.
|
||||
- `Model string`: UniFi hardware model identifier.
|
||||
- `Version string`: Firmware version string.
|
||||
- `Type string`: Device family tag populated by this package as `uap`, `usw`, `usg`, `udm`, or `uxg`.
|
||||
- `Status int`: Raw device state converted with `State.Int()`. The code treats `1` as online.
|
||||
|
||||
### `NetworkConf`
|
||||
Direct mapping of a UniFi `networkconf` entry returned by the controller API.
|
||||
|
||||
Fields:
|
||||
- `ID string`: Raw network identifier from `_id`.
|
||||
- `Name string`: Network name.
|
||||
- `Purpose string`: Network purpose such as `wan`, `corporate`, or `remote-user-vpn`.
|
||||
- `IPSubnet string`: CIDR subnet string from `ip_subnet`.
|
||||
- `VLAN int`: VLAN ID from `vlan`. `0` means untagged.
|
||||
- `VLANEnabled bool`: Whether VLAN tagging is enabled.
|
||||
- `Enabled bool`: Whether the network entry is enabled.
|
||||
- `NetworkGroup string`: Raw network group value such as `LAN`, `WAN`, or `WAN2`.
|
||||
- `NetworkIsolationEnabled bool`: Whether UniFi network isolation is enabled.
|
||||
- `InternetAccessEnabled bool`: Whether internet access is enabled for the network.
|
||||
- `IsNAT bool`: Whether NAT is enabled.
|
||||
- `DHCPEnabled bool`: Whether the controller DHCP server is enabled for the network.
|
||||
- `DHCPStart string`: DHCP range start address.
|
||||
- `DHCPStop string`: DHCP range end address.
|
||||
- `DHCPDNS1 string`: Primary DHCP DNS server.
|
||||
- `DHCPDNS2 string`: Secondary DHCP DNS server.
|
||||
- `DHCPDNSEnabled bool`: Whether custom DHCP DNS servers are enabled.
|
||||
- `MDNSEnabled bool`: Whether mDNS is enabled.
|
||||
- `FirewallZoneID string`: Raw firewall zone identifier.
|
||||
- `GatewayType string`: Raw gateway type value.
|
||||
- `VPNType string`: Raw VPN type value for VPN networks.
|
||||
- `WANType string`: WAN type such as `pppoe`, `dhcp`, or `static`.
|
||||
- `WANNetworkGroup string`: Raw WAN network-group value from `wan_networkgroup`.
|
||||
|
||||
### `Route`
|
||||
Single routing-table entry returned by the controller gateway routing endpoint.
|
||||
|
||||
Fields:
|
||||
- `Network string`: Route prefix in CIDR form from `pfx`.
|
||||
- `NextHop string`: Next-hop address or interface from `nh`.
|
||||
- `Interface string`: Interface name from `intf`.
|
||||
- `Type string`: Raw route type code from `type`.
|
||||
- `Distance int`: Administrative distance.
|
||||
- `Metric int`: Route metric.
|
||||
- `Uptime int`: Route uptime in seconds.
|
||||
- `Selected bool`: Whether the route is installed in the forwarding table, taken from `fib`.
|
||||
|
||||
This package exports no interfaces or type aliases.
|
||||
|
||||
## Functions
|
||||
|
||||
### `New(url, user, pass, apikey string, insecure bool) (*Client, error)`
|
||||
Creates a UniFi client from explicit controller settings. It builds a `uf.Config`, creates the underlying SDK client, replaces its HTTP client with one that enforces TLS 1.2, and sets `InsecureSkipVerify` when `insecure` is true.
|
||||
|
||||
### `NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure *bool) (*Client, error)`
|
||||
Resolves controller settings through `ResolveConfig`, requires either a username or an API key to be present, and then delegates to `New`.
|
||||
|
||||
### `ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure *bool) (url, user, pass, apikey string, insecure bool, err error)`
|
||||
Builds the effective controller configuration in three layers: config-file values from `forge.lthn.ai/core/config`, environment overrides from `UNIFI_URL`, `UNIFI_USER`, `UNIFI_PASS`, `UNIFI_APIKEY`, and `UNIFI_INSECURE`, then non-empty flag overrides plus a non-nil `flagInsecure`. If no URL is resolved, it falls back to `DefaultURL`. If `config.New()` fails, the function still returns values from env, flags, and the default URL without surfacing that config-load error.
|
||||
|
||||
### `RouteTypeName(code string) string`
|
||||
Maps UniFi route type codes to human-readable names: `S` to `static`, `C` to `connected`, `K` to `kernel`, `B` to `bgp`, and `O` to `ospf`. Any other code is returned unchanged.
|
||||
|
||||
### `SaveConfig(url, user, pass, apikey string, insecure *bool) error`
|
||||
Persists non-empty controller settings to the standard config store. The function writes only the values that were provided, writes the insecure flag only when the pointer is non-nil, and commits the config file after all updates succeed.
|
||||
98
specs/unifi/RFC.md
Normal file
98
specs/unifi/RFC.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# unifi
|
||||
**Import:** `forge.lthn.ai/core/go-netops/unifi`
|
||||
**Files:** 11
|
||||
|
||||
## Types
|
||||
|
||||
### `Client`
|
||||
Wraps the underlying `*github.com/unpoller/unifi/v5.Unifi` client together with the controller URL used to construct it. The struct has no exported fields.
|
||||
|
||||
Methods:
|
||||
- `API() *uf.Unifi`: Returns the underlying SDK client stored on the wrapper.
|
||||
- `URL() string`: Returns the controller URL captured when the client was constructed.
|
||||
- `GetClients(filter ClientFilter) ([]*uf.Client, error)`: Resolves the site scope with `GetSites`, fetches clients from the SDK, and optionally filters the result by wired or wireless state using `uf.Client.IsWired.Val`.
|
||||
- `GetDevices(siteName string) (*uf.Devices, error)`: Resolves the site scope and returns the raw `*uf.Devices` container from the SDK.
|
||||
- `GetDeviceList(siteName, deviceType string) ([]DeviceInfo, error)`: Flattens the `UAPs`, `USWs`, `USGs`, `UDMs`, and `UXGs` slices from `*uf.Devices` into `DeviceInfo` values. `deviceType` accepts `uap`, `usw`, `usg`, `udm`, or `uxg`; an empty value includes every supported device group.
|
||||
- `GetNetworks(siteName string) ([]NetworkConf, error)`: Calls the controller endpoint `/api/s/{site}/rest/networkconf`, defaults an empty site name to `default`, unmarshals the JSON `data` payload, and returns the resulting `[]NetworkConf`.
|
||||
- `GetRoutes(siteName string) ([]Route, error)`: Calls the controller endpoint `/api/s/{site}/stat/routing`, defaults an empty site name to `default`, path-escapes the site name, unmarshals the JSON `data` payload, and returns the resulting `[]Route`.
|
||||
- `GetSites() ([]*uf.Site, error)`: Returns the sites exposed by the controller through the underlying SDK client.
|
||||
|
||||
### `ClientFilter`
|
||||
Controls which clients `(*Client).GetClients` returns.
|
||||
|
||||
Fields:
|
||||
- `Site string`: Site name filter. An empty value means all sites returned by `GetSites`.
|
||||
- `Wired bool`: Whether wired clients should be included when connection-type filtering is active.
|
||||
- `Wireless bool`: Whether wireless clients should be included when connection-type filtering is active.
|
||||
|
||||
### `DeviceInfo`
|
||||
Flat representation of a UniFi infrastructure device returned by `(*Client).GetDeviceList`.
|
||||
|
||||
Fields:
|
||||
- `Name string`: Device name from the controller.
|
||||
- `IP string`: Device IP address.
|
||||
- `Mac string`: Device MAC address.
|
||||
- `Model string`: UniFi hardware model identifier.
|
||||
- `Version string`: Firmware version string.
|
||||
- `Type string`: Device family tag set by this package as `uap`, `usw`, `usg`, `udm`, or `uxg`.
|
||||
- `Status int`: Raw device state converted with `State.Int()`. The code comment marks `1` as online.
|
||||
|
||||
### `NetworkConf`
|
||||
Direct mapping of a UniFi `networkconf` entry returned by the controller API.
|
||||
|
||||
Fields:
|
||||
- `ID string`: Raw network identifier from `_id`.
|
||||
- `Name string`: Network name.
|
||||
- `Purpose string`: Network purpose from `purpose`, such as `wan`, `corporate`, or `remote-user-vpn`.
|
||||
- `IPSubnet string`: CIDR subnet string from `ip_subnet`.
|
||||
- `VLAN int`: VLAN ID from `vlan`. `0` means untagged.
|
||||
- `VLANEnabled bool`: Whether VLAN tagging is enabled.
|
||||
- `Enabled bool`: Whether the network entry is enabled.
|
||||
- `NetworkGroup string`: Network group from `networkgroup`, such as `LAN`, `WAN`, or `WAN2`.
|
||||
- `NetworkIsolationEnabled bool`: Whether network isolation is enabled.
|
||||
- `InternetAccessEnabled bool`: Whether internet access is enabled for the network.
|
||||
- `IsNAT bool`: Whether NAT is enabled.
|
||||
- `DHCPEnabled bool`: Whether the controller DHCP server is enabled for the network.
|
||||
- `DHCPStart string`: DHCP range start address.
|
||||
- `DHCPStop string`: DHCP range end address.
|
||||
- `DHCPDNS1 string`: Primary DHCP DNS server.
|
||||
- `DHCPDNS2 string`: Secondary DHCP DNS server.
|
||||
- `DHCPDNSEnabled bool`: Whether custom DHCP DNS servers are enabled.
|
||||
- `MDNSEnabled bool`: Whether mDNS is enabled.
|
||||
- `FirewallZoneID string`: Firewall zone identifier from `firewall_zone_id`.
|
||||
- `GatewayType string`: Gateway type from `gateway_type`.
|
||||
- `VPNType string`: VPN type from `vpn_type`.
|
||||
- `WANType string`: WAN type from `wan_type`, such as `pppoe`, `dhcp`, or `static`.
|
||||
- `WANNetworkGroup string`: WAN network-group value from `wan_networkgroup`.
|
||||
|
||||
### `Route`
|
||||
Single routing-table entry returned by the controller gateway routing endpoint.
|
||||
|
||||
Fields:
|
||||
- `Network string`: Route prefix in CIDR form from `pfx`.
|
||||
- `NextHop string`: Next-hop address or interface from `nh`.
|
||||
- `Interface string`: Interface name from `intf`.
|
||||
- `Type string`: Raw route type code from `type`.
|
||||
- `Distance int`: Administrative distance.
|
||||
- `Metric int`: Route metric.
|
||||
- `Uptime int`: Route uptime in seconds.
|
||||
- `Selected bool`: Whether the route is installed in the forwarding table, taken from `fib`.
|
||||
|
||||
This package exports no interfaces or type aliases.
|
||||
|
||||
## Functions
|
||||
|
||||
### `New(url, user, pass, apikey string, insecure bool) (*Client, error)`
|
||||
Creates a UniFi client from explicit controller settings. It builds a `uf.Config`, creates the underlying SDK client, replaces its HTTP client with one that enforces TLS 1.2, and sets `InsecureSkipVerify` from `insecure`.
|
||||
|
||||
### `NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure *bool) (*Client, error)`
|
||||
Resolves controller settings through `ResolveConfig`, requires either a username or an API key to be present, and then delegates to `New`.
|
||||
|
||||
### `ResolveConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure *bool) (url, user, pass, apikey string, insecure bool, err error)`
|
||||
Builds the effective controller configuration in three layers: config-file values from `forge.lthn.ai/core/config`, environment overrides from `UNIFI_URL`, `UNIFI_USER`, `UNIFI_PASS`, `UNIFI_APIKEY`, and `UNIFI_INSECURE`, then non-empty flag overrides plus a non-nil `flagInsecure`. If no URL is resolved, it falls back to `DefaultURL`. If `config.New()` fails, the function still returns values from env, flags, and the default URL without surfacing that config-load error.
|
||||
|
||||
### `RouteTypeName(code string) string`
|
||||
Maps UniFi route type codes to human-readable names: `S` to `static`, `C` to `connected`, `K` to `kernel`, `B` to `bgp`, and `O` to `ospf`. Any other code is returned unchanged.
|
||||
|
||||
### `SaveConfig(url, user, pass, apikey string, insecure *bool) error`
|
||||
Persists non-empty controller settings to the standard config store. The function writes only the values that were provided, writes the insecure flag only when the pointer is non-nil, and commits the config file after all updates succeed.
|
||||
|
|
@ -141,5 +141,9 @@ func SaveConfig(url, user, pass, apikey string, insecure *bool) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := cfg.Commit(); err != nil {
|
||||
return log.E("unifi.SaveConfig", "failed to write config file", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import (
|
|||
)
|
||||
|
||||
func TestResolveConfig(t *testing.T) {
|
||||
// Clear environment variables to start clean
|
||||
// Isolate from real config file and environment
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
os.Unsetenv("UNIFI_URL")
|
||||
os.Unsetenv("UNIFI_USER")
|
||||
os.Unsetenv("UNIFI_PASS")
|
||||
|
|
@ -74,6 +75,14 @@ func TestResolveConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewFromConfig(t *testing.T) {
|
||||
// Isolate from real config file and environment
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
os.Unsetenv("UNIFI_URL")
|
||||
os.Unsetenv("UNIFI_USER")
|
||||
os.Unsetenv("UNIFI_PASS")
|
||||
os.Unsetenv("UNIFI_APIKEY")
|
||||
os.Unsetenv("UNIFI_INSECURE")
|
||||
|
||||
// Mock UniFi controller
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
@ -88,8 +97,6 @@ func TestNewFromConfig(t *testing.T) {
|
|||
assert.Equal(t, ts.URL, client.URL())
|
||||
|
||||
// 2. Error case: No credentials
|
||||
os.Unsetenv("UNIFI_USER")
|
||||
os.Unsetenv("UNIFI_APIKEY")
|
||||
client, err = NewFromConfig("", "", "", "", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
|
|
|
|||
95
unifi/networks_test.go
Normal file
95
unifi/networks_test.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package unifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNetworks(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// New-style controller: paths are prefixed with /proxy/network
|
||||
if r.URL.Path == "/proxy/network/api/s/default/rest/networkconf" {
|
||||
fmt.Fprintln(w, `{"data":[{"_id":"abc123","name":"LAN","purpose":"corporate","ip_subnet":"10.69.1.0/24","vlan":0,"vlan_enabled":false,"enabled":true,"networkgroup":"LAN","dhcpd_enabled":true,"dhcpd_start":"10.69.1.100","dhcpd_stop":"10.69.1.254"}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"meta":{"rc":"ok"}, "data": []}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := New(ts.URL, "user", "pass", "", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
networks, err := client.GetNetworks("")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, networks, 1) {
|
||||
assert.Equal(t, "abc123", networks[0].ID)
|
||||
assert.Equal(t, "LAN", networks[0].Name)
|
||||
assert.Equal(t, "corporate", networks[0].Purpose)
|
||||
assert.Equal(t, "10.69.1.0/24", networks[0].IPSubnet)
|
||||
assert.True(t, networks[0].Enabled)
|
||||
assert.True(t, networks[0].DHCPEnabled)
|
||||
assert.Equal(t, "10.69.1.100", networks[0].DHCPStart)
|
||||
assert.Equal(t, "10.69.1.254", networks[0].DHCPStop)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetworks_CustomSite(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/proxy/network/api/s/office/rest/networkconf" {
|
||||
fmt.Fprintln(w, `{"data":[{"_id":"def456","name":"Office LAN","purpose":"corporate","ip_subnet":"172.16.0.0/24"}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"meta":{"rc":"ok"}, "data": []}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := New(ts.URL, "user", "pass", "", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
networks, err := client.GetNetworks("office")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, networks, 1) {
|
||||
assert.Equal(t, "Office LAN", networks[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetworks_Empty(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"meta":{"rc":"ok"}, "data": []}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := New(ts.URL, "user", "pass", "", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
networks, err := client.GetNetworks("")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, networks)
|
||||
}
|
||||
|
||||
func TestGetNetworks_InvalidJSON(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/proxy/network/api/s/default/rest/networkconf" {
|
||||
fmt.Fprintln(w, `not valid json`)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"meta":{"rc":"ok"}, "data": []}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := New(ts.URL, "user", "pass", "", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
networks, err := client.GetNetworks("")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, networks)
|
||||
assert.Contains(t, err.Error(), "failed to parse networks")
|
||||
}
|
||||
92
unifi/routes_test.go
Normal file
92
unifi/routes_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package unifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRouteTypeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
expected string
|
||||
}{
|
||||
{"S", "static"},
|
||||
{"C", "connected"},
|
||||
{"K", "kernel"},
|
||||
{"B", "bgp"},
|
||||
{"O", "ospf"},
|
||||
{"X", "X"},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.expected, RouteTypeName(tt.code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRoutes(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// New-style controller: paths are prefixed with /proxy/network
|
||||
if r.URL.Path == "/proxy/network/api/s/default/stat/routing" {
|
||||
fmt.Fprintln(w, `{"data":[{"pfx":"10.0.0.0/24","nh":"10.0.0.1","intf":"br0","type":"C","distance":0,"metric":0,"uptime":3600,"fib":true}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"meta":{"rc":"ok"}, "data": []}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := New(ts.URL, "user", "pass", "", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
routes, err := client.GetRoutes("")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, routes, 1) {
|
||||
assert.Equal(t, "10.0.0.0/24", routes[0].Network)
|
||||
assert.Equal(t, "10.0.0.1", routes[0].NextHop)
|
||||
assert.Equal(t, "br0", routes[0].Interface)
|
||||
assert.Equal(t, "C", routes[0].Type)
|
||||
assert.True(t, routes[0].Selected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRoutes_CustomSite(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/proxy/network/api/s/mysite/stat/routing" {
|
||||
fmt.Fprintln(w, `{"data":[{"pfx":"192.168.1.0/24","nh":"192.168.1.1","intf":"eth0","type":"S","distance":1,"metric":0,"uptime":0,"fib":true}]}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"meta":{"rc":"ok"}, "data": []}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := New(ts.URL, "user", "pass", "", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
routes, err := client.GetRoutes("mysite")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, routes, 1) {
|
||||
assert.Equal(t, "192.168.1.0/24", routes[0].Network)
|
||||
assert.Equal(t, "S", routes[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRoutes_Empty(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, `{"meta":{"rc":"ok"}, "data": []}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := New(ts.URL, "user", "pass", "", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
routes, err := client.GetRoutes("")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, routes)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue