diff --git a/netbox/client.go b/netbox/client.go index 2542c82758490c117ff0832715bc46856965e9fc..194079a2cbebf2eac8414ee5895dcac28b69d4be 100644 --- a/netbox/client.go +++ b/netbox/client.go @@ -35,6 +35,9 @@ type Client struct { // IPAM provides access to methods in NetBox's IPAM API. IPAM *IPAMService + // Tenancy provides access to methods in NetBox's Tenancy API. + Tenancy *TenancyService + u *url.URL client *http.Client } @@ -68,6 +71,7 @@ func NewClient(addr string, client *http.Client) (*Client, error) { c.DCIM = &DCIMService{c: c} c.IPAM = &IPAMService{c: c} + c.Tenancy = NewTenancyService(c) return c, nil } diff --git a/netbox/client_test.go b/netbox/client_test.go index 02d65f2ff7f1f4982a66248a0297e8e3fb5f884f..4170a338e1a19b4b25ca7353360e2c6cc3ea42d0 100644 --- a/netbox/client_test.go +++ b/netbox/client_test.go @@ -223,6 +223,10 @@ func testClient(t *testing.T, fn func(w http.ResponseWriter, r *http.Request)) ( } func testHandler(t *testing.T, method string, path string, v interface{}) http.HandlerFunc { + return testStatusHandler(t, method, path, v, 0) +} + +func testStatusHandler(t *testing.T, method string, path string, v interface{}, statusCode int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if want, got := method, r.Method; want != got { t.Fatalf("unexpected HTTP method:\n- want: %v\n- got: %v", want, got) @@ -232,8 +236,56 @@ func testHandler(t *testing.T, method string, path string, v interface{}) http.H t.Fatalf("unexpected URL path:\n- want: %v\n- got: %v", want, got) } + if statusCode > 0 { + w.WriteHeader(statusCode) + } + if err := json.NewEncoder(w).Encode(v); err != nil { t.Fatalf("error while encoding JSON: %v", err) } } } + +func testTenantGroupCreate(n int) *TenantGroup { + return &TenantGroup{ + Name: fmt.Sprintf("Tenant Group %d", n), + Slug: fmt.Sprintf("tenant-group-%d", n), + } +} + +func testTenantGroup(n int) *TenantGroup { + return &TenantGroup{ + ID: n, + Name: fmt.Sprintf("Tenant Group %d", n), + Slug: fmt.Sprintf("tenant-group-%d", n), + } +} + +func testTenant(n int) *Tenant { + return testTenantWithGroup(n, testTenantGroup(n)) +} + +func testTenantCreate(n int) *Tenant { + return testTenantWithGroupCreate(n, testTenantGroup(n)) +} + +func testTenantWithGroupCreate(n int, t *TenantGroup) *Tenant { + return &Tenant{ + Name: fmt.Sprintf("Tenant %d", n), + Slug: fmt.Sprintf("tenant-%d", n), + Description: fmt.Sprintf("Tenant %d Description", n), + Comments: fmt.Sprintf("Tenant %d Comments", n), + Group: t, + } +} + +func testTenantWithGroup(n int, t *TenantGroup) *Tenant { + return &Tenant{ + ID: n, + Name: fmt.Sprintf("Tenant %d", n), + Slug: fmt.Sprintf("tenant-%d", n), + Description: fmt.Sprintf("Tenant %d Description", n), + Comments: fmt.Sprintf("Tenant %d Comments", n), + Group: t, + } +} diff --git a/netbox/generate_basic_tests.go b/netbox/generate_basic_tests.go new file mode 100644 index 0000000000000000000000000000000000000000..35bd6ff668557f99e08f4a1a66d1cd66d83c4687 --- /dev/null +++ b/netbox/generate_basic_tests.go @@ -0,0 +1,383 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//+build ignore + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "log" + "os" + "text/template" + "time" +) + +func main() { + typeName := flag.String("type-name", "Example", "Name of the type to use (e.g. TenantGroup).") + serviceName := flag.String("service-name", "ExampleService", "Name of the service to create (e.g. TenantGroupsService).") + endpoint := flag.String("endpoint", "tenancy", "Name of the endpoint (e.g. dcim, ipam, tenancy).") + service := flag.String("service", "example", "Name of the service below endpoint (e.g. tenant-groups).") + clientEndpoint := flag.String("client-endpoint", "Tenancy", "Name of the client endpoint (e.g. DCIM, IPAM, Tenancy).") + clientService := flag.String("client-service", "TenantGroups", "Name of the client service (e.g. TenantGroups, Tenants).") + withoutListOpts := flag.Bool("without-list-opts", false, "Disable list options for this endpoint.") + + flag.Parse() + + b := &bytes.Buffer{} + functionsTemplate.Execute(b, struct { + Timestamp time.Time + TypeName string + ServiceName string + Endpoint string + Service string + ClientEndpoint string + ClientService string + ListOpts bool + JSONTag func(string) string + }{ + Timestamp: time.Now(), + TypeName: *typeName, + ServiceName: *serviceName, + Endpoint: *endpoint, + Service: *service, + ClientEndpoint: *clientEndpoint, + ClientService: *clientService, + ListOpts: !*withoutListOpts, + JSONTag: func(name string) string { return "`json:\"" + name + "\"`" }, + }) + + // go fmt + res, err := format.Source(b.Bytes()) + if err != nil { + b.WriteTo(os.Stderr) + log.Fatal(err) + } + + f, err := os.Create(fmt.Sprintf("%s_%s_basic_test.go", *endpoint, *service)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + f.Write(res) +} + +var functionsTemplate = template.Must(template.New("").Parse(`// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by robots at +// {{ .Timestamp }} + +package netbox + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "testing" +) + +// Using this to override MarshalJSON +// In all cases when posting data to netbox-API, the {{ .TypeName }}.MarshalJSON is what you want, +// but not here as a return in testHandler +type serverData{{ .TypeName }} {{ .TypeName }} + +func convertToServerData{{ .TypeName }}(data []*{{ .TypeName }}) []*serverData{{ .TypeName }} { + dataWant := make([]*serverData{{ .TypeName }}, len(data)) + for i := range data { + tmp := serverData{{ .TypeName }}(*data[i]) + dataWant[i] = &tmp + } + return dataWant +} + +func TestBasic{{ .TypeName }}Get(t *testing.T) { + var tests = []struct { + desc string + want *{{ .TypeName }} + }{ + { + desc: "Simple {{ .TypeName }}", + want: test{{ .TypeName }}(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverData{{ .TypeName }}(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/1/", &serverData)) + defer done() + + res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.{{ .ClientEndpoint }}.{{ .ClientService }}.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected {{ .TypeName }}:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasic{{ .TypeName }}Get404(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/1/", &struct { + Detail string {{ call .JSONTag "detail" }} + }{ + Detail: "Not found.", + }, + http.StatusNotFound)) + defer done() + + res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Get(1) + errstr := "404 - Not found." + if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error from Client.{{ .ClientEndpoint }}.{{ .ClientService }}.Get:\n- want: %v\n- got: %v", want, got) + } + + if res != nil { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", nil, res) + } +} + +func TestBasicListExtract{{ .TypeName }}(t *testing.T) { + want := []*{{ .TypeName }}{ + test{{ .TypeName }}(1), + test{{ .TypeName }}(2), + } + serverWant := convertToServerData{{ .TypeName }}(want) + serverData, _ := json.Marshal(serverWant) + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/", &pageData{ + Count: 2, + NextURL: "", + PreviousURL: "", + Results: serverData, + })) + defer done() + + {{ if .ListOpts -}} + page := c.{{ .ClientEndpoint }}.{{ .ClientService }}.List(nil) + {{ else }} + page := c.{{ .ClientEndpoint }}.{{ .ClientService }}.List() + {{ end }} + if page == nil { + t.Fatalf("unexpexted result from c.{{ .ClientEndpoint }}.{{ .ClientService }}.List.") + } + + got := []*{{ .TypeName }}{} + counter := 0 + for page.Next() { + var err error + got, err = c.{{ .ClientEndpoint }}.{{ .ClientService }}.Extract(page) + if err != nil { + t.Fatalf("unexpected error from c.{{ .ClientEndpoint }}.{{ .ClientService }}.Extract: %v", err) + } + counter = counter + 1 + if counter > 2 { // Safe guard + break + } + } + if counter != 1 { + t.Fatalf("unexpected page count:\n- want: 1\n- got: %d", counter) + } + + if !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", want, got) + } + + if page.Err() != nil { + t.Fatalf("unexpected error from page:\n- want: %v\n- got: %v", want, got) + } +} + +func TestBasicCreate{{ .TypeName }}(t *testing.T) { + var tests = []struct { + desc string + data *{{ .TypeName }} + want *{{ .TypeName }} + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 0", + data: test{{ .TypeName }}Create(1), + want: test{{ .TypeName }}(1), + status: 0, + errstr: "", + serverData: test{{ .TypeName }}(1), + }, + { + desc: "Create duplicate", + data: test{{ .TypeName }}Create(1), + want: nil, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"{{ .ServiceName }} with this name already exists.\"]}\n", + serverData: &struct { + Name []string {{ call .JSONTag "name" }} + }{ + Name: []string{"{{ .ServiceName }} with this name already exists."}, + }, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPost, "/api/{{ .Endpoint }}/{{ .Service }}/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Create(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected {{ .TypeName }}:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicUpdate{{ .TypeName }}(t *testing.T) { + var tests = []struct { + desc string + data *{{ .TypeName }} + want *{{ .TypeName }} + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 1", + data: test{{ .TypeName }}(1), + want: test{{ .TypeName }}(1), + serverData: test{{ .TypeName }}(1), + status: 0, + errstr: "", + }, + { + desc: "Update not found", + data: test{{ .TypeName }}(1), + want: nil, + serverData: &struct { + Detail string + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + { + desc: "Create duplicate", + data: test{{ .TypeName }}(1), + want: nil, + serverData: &struct { + Name []string {{ call .JSONTag "name" }} + }{ + Name: []string{"{{ .ServiceName }} with this name already exists."}, + }, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"{{ .ServiceName }} with this name already exists.\"]}\n", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPatch, "/api/{{ .Endpoint }}/{{ .Service }}/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Update(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected {{ .TypeName }}:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicDelete{{ .TypeName }}(t *testing.T) { + var tests = []struct { + desc string + data *{{ .TypeName }} + serverData interface{} + status int + errstr string + }{ + { + desc: "Delete ID 1", + data: test{{ .TypeName }}(1), + serverData: test{{ .TypeName }}(1), + status: 0, + errstr: "", + }, + { + desc: "Delete not Found", + data: test{{ .TypeName }}(1), + serverData: &struct { + Detail string {{ call .JSONTag "detail" }} + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodDelete, "/api/{{ .Endpoint }}/{{ .Service }}/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Delete(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + }) + } +} +`)) diff --git a/netbox/generate_functions.go b/netbox/generate_functions.go new file mode 100644 index 0000000000000000000000000000000000000000..84619282e883d89e94eabd1578951f174e6a61ad --- /dev/null +++ b/netbox/generate_functions.go @@ -0,0 +1,203 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//+build ignore + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "log" + "os" + "text/template" + "time" +) + +func main() { + typeName := flag.String("type-name", "Example", "Name of the type to use (e.g. TenantGroup).") + serviceName := flag.String("service-name", "ExampleService", "Name of the service to create (e.g. TenantGroupsService).") + endpoint := flag.String("endpoint", "tenancy", "Name of the endpoint (e.g. dcim, ipam, tenancy).") + service := flag.String("service", "example", "Name of the service below endpoint (e.g. tenant-groups).") + updateTypeName := flag.String("update-type-name", "", "Name of the type to use for creates and updates, to change the marshal behavior. Default typeName.") + withoutListOpts := flag.Bool("without-list-opts", false, "Disable list options for this endpoint.") + + flag.Parse() + + if *updateTypeName == "" { + *updateTypeName = *typeName + } + + b := &bytes.Buffer{} + functionsTemplate.Execute(b, struct { + Timestamp time.Time + TypeName string + UpdateTypeName string + ServiceName string + Endpoint string + Service string + ListOpts bool + }{ + Timestamp: time.Now(), + TypeName: *typeName, + UpdateTypeName: *updateTypeName, + ServiceName: *serviceName, + Endpoint: *endpoint, + Service: *service, + ListOpts: !*withoutListOpts, + }) + + // go fmt + res, err := format.Source(b.Bytes()) + if err != nil { + b.WriteTo(os.Stderr) + log.Fatal(err) + } + + f, err := os.Create(fmt.Sprintf("%s_%s.go", *endpoint, *service)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + f.Write(res) +} + +var functionsTemplate = template.Must(template.New("").Parse(`// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by robots at +// {{ .Timestamp }} + +package netbox + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// {{ .ServiceName }} is used in a Client to access NetBox's {{ .Endpoint }}/{{ .Service }} API methods. +type {{ .ServiceName }} struct { + c *Client +} + +// Get retrieves an {{ .TypeName }} object from NetBox by its ID. +func (s *{{ .ServiceName }}) Get(id int) (*{{ .TypeName }}, error) { + req, err := s.c.NewRequest( + http.MethodGet, + fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", id), + nil, + ) + if err != nil { + return nil, err + } + + t := new({{ .TypeName }}) + err = s.c.Do(req, t) + if err != nil { + return nil, err + } + return t, nil +} + +// List returns a Page associated with an NetBox API Endpoint. +{{ if .ListOpts -}} +func (s *{{ .ServiceName }}) List(options *List{{ .TypeName }}Options) *Page { + return NewPage(s.c, "api/{{ .Endpoint }}/{{ .Service }}/", options) +} +{{ else -}} +func (s *{{ .ServiceName }}) List() *Page { + return NewPage(s.c, "api/{{ .Endpoint }}/{{ .Service }}/", nil) +} +{{ end }} + +// Extract retrives a list of {{ .TypeName }} objects from page. +func (s *{{ .ServiceName }}) Extract(page *Page) ([]*{{ .TypeName }}, error) { + if err := page.Err(); err != nil { + return nil, err + } + + var groups []*{{ .TypeName }} + if err := json.Unmarshal(page.data.Results, &groups); err != nil { + return nil, err + } + return groups, nil +} + +// Create creates a new {{ .TypeName }} object in NetBox and returns the new object. +func (s *{{ .ServiceName }}) Create(data *{{ .TypeName }}) (*{{ .TypeName }}, error) { + req, err := s.c.NewJSONRequest(http.MethodPost, "api/{{ .Endpoint }}/{{ .Service }}/", nil, data) + if err != nil { + return nil, err + } + + g := new({{ .UpdateTypeName }}) + err = s.c.Do(req, g) + if err != nil { + return nil, err + } + data.ID = g.ID + return data, nil +} + +// Update changes an existing {{ .TypeName }} object in NetBox, and returns the new object. +func (s *{{ .ServiceName }}) Update(data *{{ .TypeName }}) (*{{ .TypeName }}, error) { + req, err := s.c.NewJSONRequest( + http.MethodPatch, + fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", data.ID), + nil, + data) + if err != nil { + return nil, err + } + + // g is just used to verify correct api result. + // data is not changed, because the g is not the full representation that one would + // get with Get. But if the response was unmarshaled into {{ .UpdateTypeName }} corretcly, + // everything went fine, and we do not need to update data. + g := new({{ .UpdateTypeName }}) + err = s.c.Do(req, g) + if err != nil { + return nil, err + } + return data, nil +} + +// Delete deletes an existing {{ .TypeName }} object from NetBox. +func (s *{{ .ServiceName }}) Delete(data *{{ .TypeName }}) error { + req, err := s.c.NewRequest( + http.MethodDelete, + fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", data.ID), + nil) + if err != nil { + return err + } + + return s.c.Do(req, nil) +} +`)) diff --git a/netbox/tenancy.go b/netbox/tenancy.go new file mode 100644 index 0000000000000000000000000000000000000000..e88eb630eb205d98e5e8e83f36eef60f3e01337c --- /dev/null +++ b/netbox/tenancy.go @@ -0,0 +1,35 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netbox + +// A TenancyService is udes in a Client to access NetBox's Tenancy API methods. +type TenancyService struct { + c *Client + TenantGroups *TenantGroupsService + Tenants *TenantsService +} + +// NewTenancyService returns a TenancyService initialized with all sub-service. +func NewTenancyService(client *Client) *TenancyService { + return &TenancyService{ + c: client, + TenantGroups: &TenantGroupsService{ + c: client, + }, + Tenants: &TenantsService{ + c: client, + }, + } +} diff --git a/netbox/tenancy_tenant-groups.go b/netbox/tenancy_tenant-groups.go new file mode 100644 index 0000000000000000000000000000000000000000..66feef51c397eeebb3f489051711bc2cc1a50aff --- /dev/null +++ b/netbox/tenancy_tenant-groups.go @@ -0,0 +1,119 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by robots at +// 2017-05-17 14:19:30.696198918 +0200 CEST + +package netbox + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// TenantGroupsService is used in a Client to access NetBox's tenancy/tenant-groups API methods. +type TenantGroupsService struct { + c *Client +} + +// Get retrieves an TenantGroup object from NetBox by its ID. +func (s *TenantGroupsService) Get(id int) (*TenantGroup, error) { + req, err := s.c.NewRequest( + http.MethodGet, + fmt.Sprintf("api/tenancy/tenant-groups/%d/", id), + nil, + ) + if err != nil { + return nil, err + } + + t := new(TenantGroup) + err = s.c.Do(req, t) + if err != nil { + return nil, err + } + return t, nil +} + +// List returns a Page associated with an NetBox API Endpoint. +func (s *TenantGroupsService) List() *Page { + return NewPage(s.c, "api/tenancy/tenant-groups/", nil) +} + +// Extract retrives a list of TenantGroup objects from page. +func (s *TenantGroupsService) Extract(page *Page) ([]*TenantGroup, error) { + if err := page.Err(); err != nil { + return nil, err + } + + var groups []*TenantGroup + if err := json.Unmarshal(page.data.Results, &groups); err != nil { + return nil, err + } + return groups, nil +} + +// Create creates a new TenantGroup object in NetBox and returns the new object. +func (s *TenantGroupsService) Create(data *TenantGroup) (*TenantGroup, error) { + req, err := s.c.NewJSONRequest(http.MethodPost, "api/tenancy/tenant-groups/", nil, data) + if err != nil { + return nil, err + } + + g := new(TenantGroup) + err = s.c.Do(req, g) + if err != nil { + return nil, err + } + data.ID = g.ID + return data, nil +} + +// Update changes an existing TenantGroup object in NetBox, and returns the new object. +func (s *TenantGroupsService) Update(data *TenantGroup) (*TenantGroup, error) { + req, err := s.c.NewJSONRequest( + http.MethodPatch, + fmt.Sprintf("api/tenancy/tenant-groups/%d/", data.ID), + nil, + data) + if err != nil { + return nil, err + } + + // g is just used to verify correct api result. + // data is not changed, because the g is not the full representation that one would + // get with Get. But if the response was unmarshaled into TenantGroup corretcly, + // everything went fine, and we do not need to update data. + g := new(TenantGroup) + err = s.c.Do(req, g) + if err != nil { + return nil, err + } + return data, nil +} + +// Delete deletes an existing TenantGroup object from NetBox. +func (s *TenantGroupsService) Delete(data *TenantGroup) error { + req, err := s.c.NewRequest( + http.MethodDelete, + fmt.Sprintf("api/tenancy/tenant-groups/%d/", data.ID), + nil) + if err != nil { + return err + } + + return s.c.Do(req, nil) +} diff --git a/netbox/tenancy_tenant-groups_basic_test.go b/netbox/tenancy_tenant-groups_basic_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9d3ab3cf0558076325a047a01b6dc94f55790e88 --- /dev/null +++ b/netbox/tenancy_tenant-groups_basic_test.go @@ -0,0 +1,301 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by robots at +// 2017-05-17 14:19:30.943187077 +0200 CEST + +package netbox + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "testing" +) + +// Using this to override MarshalJSON +// In all cases when posting data to netbox-API, the TenantGroup.MarshalJSON is what you want, +// but not here as a return in testHandler +type serverDataTenantGroup TenantGroup + +func convertToServerDataTenantGroup(data []*TenantGroup) []*serverDataTenantGroup { + dataWant := make([]*serverDataTenantGroup, len(data)) + for i := range data { + tmp := serverDataTenantGroup(*data[i]) + dataWant[i] = &tmp + } + return dataWant +} + +func TestBasicTenantGroupGet(t *testing.T) { + var tests = []struct { + desc string + want *TenantGroup + }{ + { + desc: "Simple TenantGroup", + want: testTenantGroup(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataTenantGroup(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/1/", &serverData)) + defer done() + + res, err := c.Tenancy.TenantGroups.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.Tenancy.TenantGroups.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicTenantGroupGet404(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/1/", &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + http.StatusNotFound)) + defer done() + + res, err := c.Tenancy.TenantGroups.Get(1) + errstr := "404 - Not found." + if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error from Client.Tenancy.TenantGroups.Get:\n- want: %v\n- got: %v", want, got) + } + + if res != nil { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", nil, res) + } +} + +func TestBasicListExtractTenantGroup(t *testing.T) { + want := []*TenantGroup{ + testTenantGroup(1), + testTenantGroup(2), + } + serverWant := convertToServerDataTenantGroup(want) + serverData, _ := json.Marshal(serverWant) + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/", &pageData{ + Count: 2, + NextURL: "", + PreviousURL: "", + Results: serverData, + })) + defer done() + + page := c.Tenancy.TenantGroups.List() + + if page == nil { + t.Fatalf("unexpexted result from c.Tenancy.TenantGroups.List.") + } + + got := []*TenantGroup{} + counter := 0 + for page.Next() { + var err error + got, err = c.Tenancy.TenantGroups.Extract(page) + if err != nil { + t.Fatalf("unexpected error from c.Tenancy.TenantGroups.Extract: %v", err) + } + counter = counter + 1 + if counter > 2 { // Safe guard + break + } + } + if counter != 1 { + t.Fatalf("unexpected page count:\n- want: 1\n- got: %d", counter) + } + + if !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", want, got) + } + + if page.Err() != nil { + t.Fatalf("unexpected error from page:\n- want: %v\n- got: %v", want, got) + } +} + +func TestBasicCreateTenantGroup(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + want *TenantGroup + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 0", + data: testTenantGroupCreate(1), + want: testTenantGroup(1), + status: 0, + errstr: "", + serverData: testTenantGroup(1), + }, + { + desc: "Create duplicate", + data: testTenantGroupCreate(1), + want: nil, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantGroupsService with this name already exists.\"]}\n", + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantGroupsService with this name already exists."}, + }, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPost, "/api/tenancy/tenant-groups/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.Tenancy.TenantGroups.Create(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicUpdateTenantGroup(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + want *TenantGroup + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 1", + data: testTenantGroup(1), + want: testTenantGroup(1), + serverData: testTenantGroup(1), + status: 0, + errstr: "", + }, + { + desc: "Update not found", + data: testTenantGroup(1), + want: nil, + serverData: &struct { + Detail string + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + { + desc: "Create duplicate", + data: testTenantGroup(1), + want: nil, + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantGroupsService with this name already exists."}, + }, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantGroupsService with this name already exists.\"]}\n", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPatch, "/api/tenancy/tenant-groups/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.Tenancy.TenantGroups.Update(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicDeleteTenantGroup(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + serverData interface{} + status int + errstr string + }{ + { + desc: "Delete ID 1", + data: testTenantGroup(1), + serverData: testTenantGroup(1), + status: 0, + errstr: "", + }, + { + desc: "Delete not Found", + data: testTenantGroup(1), + serverData: &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodDelete, "/api/tenancy/tenant-groups/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + err := c.Tenancy.TenantGroups.Delete(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + }) + } +} diff --git a/netbox/tenancy_tenant-groups_test.go b/netbox/tenancy_tenant-groups_test.go new file mode 100644 index 0000000000000000000000000000000000000000..351110344a90abdfce71e44f165528b4c22c95a8 --- /dev/null +++ b/netbox/tenancy_tenant-groups_test.go @@ -0,0 +1,83 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netbox + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "testing" +) + +func TestTenantGroupUnmarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data []byte + want *TenantGroup + }{ + { + desc: "full", + data: []byte(`{ "id": 1, "name": "Tenant Group 1", "slug": "tenant-group-1", "custom_fields": {} }`), + want: testTenantGroup(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result := new(TenantGroup) + err := json.Unmarshal(tt.data, result) + if err != nil { + t.Fatalf("unexpected error from TenantGroup.UnmarshalJSON: %v", err) + } + + if want, got := tt.want, result; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestTenantGroupMarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + want []byte + }{ + { + desc: "With TenantGroup.ID", + data: testTenantGroup(1), + want: []byte(`{"id":1,"name":"Tenant Group 1","slug":"tenant-group-1"}`), + }, + { + desc: "No TenantGroup.ID", + data: testTenantGroup(0), + want: []byte(`{"name":"Tenant Group 0","slug":"tenant-group-0"}`), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result, err := json.Marshal(tt.data) + if err != nil { + t.Fatalf("unexpected error from TenantGroup.MarshalJSON: %v", err) + } + + if want, got := tt.want, result; bytes.Compare(want, got) != 0 { + t.Fatalf("unexpected TenantGroup:\n- want: %s\n- got: %s", want, got) + } + }) + } +} diff --git a/netbox/tenancy_tenant-groups_types.go b/netbox/tenancy_tenant-groups_types.go new file mode 100644 index 0000000000000000000000000000000000000000..813ef83d5829e3894af75bf591d3016cb6bcaa8f --- /dev/null +++ b/netbox/tenancy_tenant-groups_types.go @@ -0,0 +1,25 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netbox + +// A TenantGroup is a representation of netbox tenant-groups +type TenantGroup struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +//go:generate go run generate_functions.go -type-name TenantGroup -service-name TenantGroupsService -endpoint tenancy -service tenant-groups -without-list-opts +//go:generate go run generate_basic_tests.go -type-name TenantGroup -service-name TenantGroupsService -endpoint tenancy -service tenant-groups -client-endpoint Tenancy -client-service TenantGroups -without-list-opts diff --git a/netbox/tenancy_tenants.go b/netbox/tenancy_tenants.go new file mode 100644 index 0000000000000000000000000000000000000000..c9cc00cb50577a45b0b59220e4d28b862b0f7978 --- /dev/null +++ b/netbox/tenancy_tenants.go @@ -0,0 +1,119 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by robots at +// 2017-05-17 14:19:31.190654936 +0200 CEST + +package netbox + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// TenantsService is used in a Client to access NetBox's tenancy/tenants API methods. +type TenantsService struct { + c *Client +} + +// Get retrieves an Tenant object from NetBox by its ID. +func (s *TenantsService) Get(id int) (*Tenant, error) { + req, err := s.c.NewRequest( + http.MethodGet, + fmt.Sprintf("api/tenancy/tenants/%d/", id), + nil, + ) + if err != nil { + return nil, err + } + + t := new(Tenant) + err = s.c.Do(req, t) + if err != nil { + return nil, err + } + return t, nil +} + +// List returns a Page associated with an NetBox API Endpoint. +func (s *TenantsService) List(options *ListTenantOptions) *Page { + return NewPage(s.c, "api/tenancy/tenants/", options) +} + +// Extract retrives a list of Tenant objects from page. +func (s *TenantsService) Extract(page *Page) ([]*Tenant, error) { + if err := page.Err(); err != nil { + return nil, err + } + + var groups []*Tenant + if err := json.Unmarshal(page.data.Results, &groups); err != nil { + return nil, err + } + return groups, nil +} + +// Create creates a new Tenant object in NetBox and returns the new object. +func (s *TenantsService) Create(data *Tenant) (*Tenant, error) { + req, err := s.c.NewJSONRequest(http.MethodPost, "api/tenancy/tenants/", nil, data) + if err != nil { + return nil, err + } + + g := new(updateTenant) + err = s.c.Do(req, g) + if err != nil { + return nil, err + } + data.ID = g.ID + return data, nil +} + +// Update changes an existing Tenant object in NetBox, and returns the new object. +func (s *TenantsService) Update(data *Tenant) (*Tenant, error) { + req, err := s.c.NewJSONRequest( + http.MethodPatch, + fmt.Sprintf("api/tenancy/tenants/%d/", data.ID), + nil, + data) + if err != nil { + return nil, err + } + + // g is just used to verify correct api result. + // data is not changed, because the g is not the full representation that one would + // get with Get. But if the response was unmarshaled into updateTenant corretcly, + // everything went fine, and we do not need to update data. + g := new(updateTenant) + err = s.c.Do(req, g) + if err != nil { + return nil, err + } + return data, nil +} + +// Delete deletes an existing Tenant object from NetBox. +func (s *TenantsService) Delete(data *Tenant) error { + req, err := s.c.NewRequest( + http.MethodDelete, + fmt.Sprintf("api/tenancy/tenants/%d/", data.ID), + nil) + if err != nil { + return err + } + + return s.c.Do(req, nil) +} diff --git a/netbox/tenancy_tenants_basic_test.go b/netbox/tenancy_tenants_basic_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f954536cffa5253c878124e1a601f32bac84ec5c --- /dev/null +++ b/netbox/tenancy_tenants_basic_test.go @@ -0,0 +1,301 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by robots at +// 2017-05-17 14:19:31.43602063 +0200 CEST + +package netbox + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "testing" +) + +// Using this to override MarshalJSON +// In all cases when posting data to netbox-API, the Tenant.MarshalJSON is what you want, +// but not here as a return in testHandler +type serverDataTenant Tenant + +func convertToServerDataTenant(data []*Tenant) []*serverDataTenant { + dataWant := make([]*serverDataTenant, len(data)) + for i := range data { + tmp := serverDataTenant(*data[i]) + dataWant[i] = &tmp + } + return dataWant +} + +func TestBasicTenantGet(t *testing.T) { + var tests = []struct { + desc string + want *Tenant + }{ + { + desc: "Simple Tenant", + want: testTenant(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataTenant(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &serverData)) + defer done() + + res, err := c.Tenancy.Tenants.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.Tenancy.Tenants.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicTenantGet404(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + http.StatusNotFound)) + defer done() + + res, err := c.Tenancy.Tenants.Get(1) + errstr := "404 - Not found." + if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error from Client.Tenancy.Tenants.Get:\n- want: %v\n- got: %v", want, got) + } + + if res != nil { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", nil, res) + } +} + +func TestBasicListExtractTenant(t *testing.T) { + want := []*Tenant{ + testTenant(1), + testTenant(2), + } + serverWant := convertToServerDataTenant(want) + serverData, _ := json.Marshal(serverWant) + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/", &pageData{ + Count: 2, + NextURL: "", + PreviousURL: "", + Results: serverData, + })) + defer done() + + page := c.Tenancy.Tenants.List(nil) + + if page == nil { + t.Fatalf("unexpexted result from c.Tenancy.Tenants.List.") + } + + got := []*Tenant{} + counter := 0 + for page.Next() { + var err error + got, err = c.Tenancy.Tenants.Extract(page) + if err != nil { + t.Fatalf("unexpected error from c.Tenancy.Tenants.Extract: %v", err) + } + counter = counter + 1 + if counter > 2 { // Safe guard + break + } + } + if counter != 1 { + t.Fatalf("unexpected page count:\n- want: 1\n- got: %d", counter) + } + + if !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", want, got) + } + + if page.Err() != nil { + t.Fatalf("unexpected error from page:\n- want: %v\n- got: %v", want, got) + } +} + +func TestBasicCreateTenant(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + want *Tenant + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 0", + data: testTenantCreate(1), + want: testTenant(1), + status: 0, + errstr: "", + serverData: testTenant(1), + }, + { + desc: "Create duplicate", + data: testTenantCreate(1), + want: nil, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantsService with this name already exists.\"]}\n", + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantsService with this name already exists."}, + }, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPost, "/api/tenancy/tenants/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.Tenancy.Tenants.Create(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicUpdateTenant(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + want *Tenant + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 1", + data: testTenant(1), + want: testTenant(1), + serverData: testTenant(1), + status: 0, + errstr: "", + }, + { + desc: "Update not found", + data: testTenant(1), + want: nil, + serverData: &struct { + Detail string + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + { + desc: "Create duplicate", + data: testTenant(1), + want: nil, + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantsService with this name already exists."}, + }, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantsService with this name already exists.\"]}\n", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPatch, "/api/tenancy/tenants/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.Tenancy.Tenants.Update(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicDeleteTenant(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + serverData interface{} + status int + errstr string + }{ + { + desc: "Delete ID 1", + data: testTenant(1), + serverData: testTenant(1), + status: 0, + errstr: "", + }, + { + desc: "Delete not Found", + data: testTenant(1), + serverData: &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodDelete, "/api/tenancy/tenants/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + err := c.Tenancy.Tenants.Delete(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + }) + } +} diff --git a/netbox/tenancy_tenants_test.go b/netbox/tenancy_tenants_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b46f9ead3bf34212a9b2ea5b7c1e0471825a3cde --- /dev/null +++ b/netbox/tenancy_tenants_test.go @@ -0,0 +1,194 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netbox + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "reflect" + "testing" +) + +func TestTenantGet(t *testing.T) { + var tests = []struct { + desc string + want *Tenant + wantGroup *TenantGroup + }{ + { + desc: "Without TenantGroup", + want: testTenantWithGroup(1, nil), + wantGroup: nil, + }, + { + desc: "With TenantGroup", + want: testTenantWithGroup(1, testTenantGroup(1)), + wantGroup: testTenantGroup(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataTenant(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &serverData)) + defer done() + + res, err := c.Tenancy.Tenants.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.Tenancy.Tenants.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected Tenant:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.wantGroup, res.Group; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestTenantUnmarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data []byte + want *Tenant + }{ + { + desc: "Nil Group", + data: []byte(`{ "id": 1, "name": "Tenant 1", "slug": "tenant-1", "group": null, "description": "Tenant 1 Description", "comments": "Tenant 1 Comments", "custom_fields": {} }`), + want: testTenantWithGroup(1, nil), + }, + { + desc: "With Group", + data: []byte(`{ "id": 1, "name": "Tenant 1", "slug": "tenant-1", "group": {"id": 1, "name": "Tenant Group 1", "slug": "tenant-group-1"}, "description": "Tenant 1 Description", "comments": "Tenant 1 Comments", "custom_fields": {} }`), + want: testTenant(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result := new(Tenant) + err := json.Unmarshal(tt.data, result) + if err != nil { + t.Fatalf("unexpected error from Tenant.UnmarshalJSON: %v", err) + } + + if want, got := tt.want, result; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestTenantMarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + want []byte + }{ + { + desc: "Nil Group", + data: testTenantWithGroup(1, nil), + want: []byte(`{"id":1,"name":"Tenant 1","slug":"tenant-1","description":"Tenant 1 Description","comments":"Tenant 1 Comments"}`), + }, + { + desc: "With Group", + data: testTenant(1), + want: []byte(`{"id":1,"name":"Tenant 1","slug":"tenant-1","description":"Tenant 1 Description","comments":"Tenant 1 Comments","group":1}`), + }, + { + desc: "No Tenant.ID", + data: testTenantWithGroup(0, nil), + want: []byte(`{"name":"Tenant 0","slug":"tenant-0","description":"Tenant 0 Description","comments":"Tenant 0 Comments"}`), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result, err := json.Marshal(tt.data) + if err != nil { + t.Fatalf("unexpected error from Tenant.MarshalJSON: %v", err) + } + + if want, got := tt.want, result; bytes.Compare(want, got) != 0 { + t.Fatalf("unexpected Tenant:\n- want: %s\n- got: %s", want, got) + } + }) + } +} + +func TestListTenantOptions(t *testing.T) { + var tests = []struct { + desc string + o *ListTenantOptions + v url.Values + }{ + { + desc: "empty options", + }, + { + desc: "full options", + o: &ListTenantOptions{ + Name: "Hello", + IDIn: "1,2,3", + GroupID: 1, + Query: "World", + }, + v: url.Values{ + "name": []string{"Hello"}, + "id__in": []string{"1,2,3"}, + "group_id": []string{"1"}, + "q": []string{"World"}, + }, + }, + { + desc: "group vs group_id", + o: &ListTenantOptions{ + GroupID: 1, + Group: "Group1", + }, + v: url.Values{ + "group_id": []string{"1"}, + }, + }, + { + desc: "group name", + o: &ListTenantOptions{ + Group: "Group 1", + }, + v: url.Values{ + "group": []string{"Group 1"}, + }, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + v, err := tt.o.Values() + if err != nil { + t.Fatalf("unexpected Values error: %v", err) + } + + if want, got := tt.v, v; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected url.Values map:\n- want: %v\n- got: %v", want, got) + } + }) + } +} diff --git a/netbox/tenancy_tenants_types.go b/netbox/tenancy_tenants_types.go new file mode 100644 index 0000000000000000000000000000000000000000..ea64d3de37e63a22a20e0a8c569eb76452e09eec --- /dev/null +++ b/netbox/tenancy_tenants_types.go @@ -0,0 +1,103 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netbox + +import ( + "encoding/json" + "net/url" + "strconv" +) + +// A Tenant is a representation of netbox tenants +type Tenant struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Comments string `json:"comments"` + Group *TenantGroup `json:"Group"` +} + +// A updateTenant is the internal representation of tenants +// needed to POST/PUT/PATCH to Netbox API +type updateTenant struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Comments string `json:"comments"` + Group int `json:"group,omitempty"` +} + +// MarshalJSON marshals an Tenant into JSON bytes. +func (t *Tenant) MarshalJSON() ([]byte, error) { + group := 0 + if t.Group != nil { + group = t.Group.ID + } + return json.Marshal(updateTenant{ + ID: t.ID, + Name: t.Name, + Slug: t.Slug, + Description: t.Description, + Comments: t.Comments, + Group: group, + }) +} + +// ListTenantOptions is used as an argument for Client.Tenancy.Tenant.List. +// Integer fields with an *ID suffix are preferred over their string +// counterparts, and if both are set, only the *ID field will be used. +type ListTenantOptions struct { + Name string + IDIn string + GroupID int + Group string + + Query string +} + +// Values generates a url.Values map from the data in ListTenantOptions. +func (o *ListTenantOptions) Values() (url.Values, error) { + if o == nil { + return nil, nil + } + + v := url.Values{} + + switch { + case o.GroupID != 0: + v.Set("group_id", strconv.Itoa(o.GroupID)) + case o.Group != "": + v.Set("group", o.Group) + } + + if o.Name != "" { + v.Set("name", o.Name) + } + + if o.IDIn != "" { + v.Set("id__in", o.IDIn) + } + + if o.Query != "" { + v.Set("q", o.Query) + } + + return v, nil +} + +//go:generate go run generate_functions.go -type-name Tenant -update-type-name updateTenant -service-name TenantsService -endpoint tenancy -service tenants +//go:generate go run generate_basic_tests.go -type-name Tenant -service-name TenantsService -endpoint tenancy -service tenants -client-endpoint Tenancy -client-service Tenants