Skip to content
Snippets Groups Projects
Commit 93500c8d authored by Matt Layher's avatar Matt Layher Committed by GitHub
Browse files

Merge pull request #11 from chrigl/v2-paging

Added page and client to v2
parents af425304 92d1634b
No related branches found
No related tags found
No related merge requests found
language: go language: go
go: go:
- 1.6.2 - 1.x
before_install: before_install:
- go get github.com/golang/lint/golint - go get github.com/golang/lint/golint
before_script: before_script:
......
// 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"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
)
// A Client is a NetBox client. It can be used to retrieve network and
// datacenter infrastructure information from a NetBox server.
type Client struct {
// DCIM provides access to methods in NetBox's DCIM API.
DCIM *DCIMService
// IPAM provides access to methods in NetBox's IPAM API.
IPAM *IPAMService
u *url.URL
client *http.Client
}
// NewClient returns a new instance of a NetBox client. addr specifies the address
// of the NetBox server, and client specifies an optional HTTP client to use
// for requests.
//
// If client is nil, a default HTTP client will be used.
func NewClient(addr string, client *http.Client) (*Client, error) {
if client == nil {
client = &http.Client{}
}
u, err := url.Parse(addr)
if err != nil {
return nil, err
}
c := &Client{
u: u,
client: client,
}
c.DCIM = &DCIMService{c: c}
c.IPAM = &IPAMService{c: c}
return c, nil
}
// NewRequest creates a HTTP request using the input HTTP method, URL
// endpoint, and a Valuer which creates URL parameters for the request.
//
// If a nil Valuer is specified, no query parameters will be sent with the
// request.
func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) {
rel, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
// Allow specifying a base path for API requests, so if a NetBox server
// resides at a path like http://example.com/netbox/, API requests will
// be sent to http://example.com/netbox/api/...
//
// Enables support of: https://github.com/digitalocean/netbox/issues/212.
if c.u.Path != "" {
rel.Path = path.Join(c.u.Path, rel.Path)
}
u := c.u.ResolveReference(rel)
// If no valuer specified, create a request with no query parameters
if options == nil {
return http.NewRequest(method, u.String(), nil)
}
values, err := options.Values()
if err != nil {
return nil, err
}
u.RawQuery = values.Encode()
return http.NewRequest(method, u.String(), nil)
}
// Do executes an HTTP request and if v is not nil, Do unmarshals result
// JSON onto v.
func (c *Client) Do(req *http.Request, v interface{}) error {
res, err := c.client.Do(req)
if err != nil {
return err
}
defer func() {
_ = res.Body.Close()
}()
// Test if the request was successful.
// If not, do not try to decode the body. This would result in
// misleading error messages
err = httpStatusOK(res)
if err != nil {
return err
}
if v == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(v)
}
// httpStatusOK tests if the StatusCode of res is smaller than 300. Tries to
// Unmarshal the response into json, and returns only the detail
// if this exists. Otherwise returns the status code with the raw Body data.
func httpStatusOK(res *http.Response) error {
if res.StatusCode >= http.StatusMultipleChoices {
errDetail := struct {
Detail string `json:"detail"`
}{}
bodyData, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("%d - %v", res.StatusCode, err)
}
err = json.Unmarshal(bodyData, &errDetail)
if err == nil && errDetail.Detail != "" {
return fmt.Errorf("%d - %s", res.StatusCode, errDetail.Detail)
}
return fmt.Errorf("%d - %s", res.StatusCode, bodyData)
}
return nil
}
// 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"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"testing"
)
func TestClientBadJSON(t *testing.T) {
c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foo"))
})
defer done()
req, err := c.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal("expected no error, but an error returned")
}
// Pass empty struct to trigger JSON unmarshaling path
var v struct{}
err = c.Do(req, &v)
if _, ok := err.(*json.SyntaxError); !ok {
t.Fatalf("unexpected error type: %T", err)
}
}
func TestClientBadStatusCode(t *testing.T) {
var tests = []struct {
desc string
data []byte
statusCode int
want error
}{
{
desc: "403, but no json result",
data: []byte("foo"),
statusCode: http.StatusForbidden,
want: errors.New("403 - foo"),
},
{
desc: "403, with json, but without detail",
data: []byte(`{"error_msg": "some error occurred"}`),
statusCode: http.StatusForbidden,
want: errors.New(`403 - {"error_msg": "some error occurred"}`),
},
{
desc: "500, but correct json",
data: []byte(`{"detail": "some error occurred"}`),
statusCode: http.StatusInternalServerError,
want: errors.New("500 - some error occurred"),
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.statusCode)
w.Write(tt.data)
})
defer done()
req, err := c.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal("expected no error, but an error returned")
}
var v struct{}
err = c.Do(req, &v)
if want, got := tt.want, err; !reflect.DeepEqual(want, got) {
t.Fatalf("expected error:\n- want: %v\n- got: %v", want, got)
}
})
}
}
func TestClientQueryParameters(t *testing.T) {
c := &Client{
u: &url.URL{},
client: &http.Client{},
}
const (
wantFoo = "foo"
wantBar = 1
)
req, err := c.NewRequest(http.MethodGet, "/", testValuer{
Foo: wantFoo,
Bar: wantBar,
})
if err != nil {
t.Fatal("expected an error, but no error returned")
}
q := req.URL.Query()
if want, got := 2, len(q); want != got {
t.Fatalf("unexpected number of query parameters:\n- want: %v\n- got: %v",
want, got)
}
if want, got := wantFoo, q.Get("foo"); want != got {
t.Fatalf("unexpected foo:\n- want: %v\n- got: %v", want, got)
}
if want, got := strconv.Itoa(wantBar), q.Get("bar"); want != got {
t.Fatalf("unexpected bar:\n- want: %v\n- got: %v", want, got)
}
}
func TestClientPrependBaseURLPath(t *testing.T) {
u, err := url.Parse("http://example.com/netbox/")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
c := &Client{
u: u,
client: &http.Client{},
}
req, err := c.NewRequest(http.MethodGet, "/api/ipam/vlans", nil)
if err != nil {
t.Fatal("expected an error, but no error returned")
}
if want, got := "/netbox/api/ipam/vlans", req.URL.Path; want != got {
t.Fatalf("unexpected URL path:\n- want: %q\n- got: %q",
want, got)
}
}
type testValuer struct {
Foo string
Bar int
}
func (q testValuer) Values() (url.Values, error) {
v := url.Values{}
if q.Foo != "" {
v.Set("foo", q.Foo)
}
if q.Bar != 0 {
v.Set("bar", strconv.Itoa(q.Bar))
}
return v, nil
}
func testClient(t *testing.T, fn func(w http.ResponseWriter, r *http.Request)) (*Client, func()) {
s := httptest.NewServer(http.HandlerFunc(fn))
c, err := NewClient(s.URL, nil)
if err != nil {
t.Fatalf("error creating Client: %v", err)
}
return c, func() { s.Close() }
}
func testHandler(t *testing.T, method string, path string, v interface{}) 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)
}
if want, got := path, r.URL.Path; want != got {
t.Fatalf("unexpected URL path:\n- want: %v\n- got: %v", want, got)
}
if err := json.NewEncoder(w).Encode(v); err != nil {
t.Fatalf("error while encoding JSON: %v", err)
}
}
}
// 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 DCIMService is used in a Client to access NetBox's DCIM API methods.
type DCIMService struct {
c *Client
}
// SimpleIdentifier represents a simple object that consists of only an ID,
// name, and slug.
type SimpleIdentifier struct {
ID int `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
// 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
// An IPAMService is used in a Client to access NetBox's IPAM API methods.
type IPAMService struct {
c *Client
}
// 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 provides an API client for DigitalOcean's NetBox IPAM and
// DCIM service.
package netbox
import "net/url"
// A Valuer is an object which can generate a url.Values map from itself.
// Valuer implementations are used to generate request URL parameters
// that NetBox can use to filter data.
type Valuer interface {
Values() (url.Values, error)
}
// A Family is an IP address family, used by NetBox to filter either IPv4
// or IPv6 addresses. Use its Valid method or compare against the predefined
// Family constants to determine if the contained value is a valid Family.
type Family int
// Family constants which can be used with NetBox.
const (
FamilyIPv4 Family = 4
FamilyIPv6 Family = 6
)
// String returns the string representation of a Family.
func (f Family) String() string {
switch f {
case FamilyIPv4:
return "IPv4"
case FamilyIPv6:
return "IPv6"
default:
return "Unknown"
}
}
// Valid determines if a Family is valid.
func (f Family) Valid() bool {
return f == FamilyIPv4 || f == FamilyIPv6
}
// A Status is an operational status of an object.
type Status int
const (
// StatusContainer indicates an object is a summary of child prefixes.
StatusContainer Status = 0
// StatusActive indicates an object is active and in use.
StatusActive Status = 1
// StatusReserved indicates an object is reserved for future use.
StatusReserved Status = 2
// StatusDeprecated indicates an object is no longer in use.
StatusDeprecated Status = 3
)
// String returns the string representation of a Status.
func (s Status) String() string {
switch s {
case StatusContainer:
return "Container"
case StatusActive:
return "Active"
case StatusReserved:
return "Reserved"
case StatusDeprecated:
return "Deprecated"
default:
return "Unknown"
}
}
// 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"
"fmt"
"math"
"net/http"
"net/http/httptest"
"testing"
)
func TestFamilyValid(t *testing.T) {
var tests = []struct {
desc string
f Family
ok bool
}{
{
desc: "Test math.MinInt64",
f: math.MinInt64,
},
{
desc: "Test math.MaxInt64",
f: math.MaxInt64,
},
{
desc: "Test FamilyIPv4",
f: FamilyIPv4,
ok: true,
},
{
desc: "Test FamilyIPv6",
f: FamilyIPv6,
ok: true,
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
if want, got := tt.ok, tt.f.Valid(); want != got {
t.Fatalf("unexpected Family(%d).Valid():\n- want: %v\n- got: %v",
tt.f, want, got)
}
})
}
}
type testSimple struct {
ID int `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
// ExampleNewClient demonstrates usage of the Client type.
func ExampleNewClient() {
// Sets up a minimal, mocked NetBox server
addr, done := exampleServer()
defer done()
// Creates a client configured to use the test server
c, err := NewClient(addr, nil)
if err != nil {
panic(fmt.Sprintf("failed to create netbox.Client: %v", err))
}
res := testSimple{}
req, err := c.NewRequest(http.MethodGet, "/", nil)
if err != nil {
panic(err)
}
err = c.Do(req, &res)
if err != nil {
panic(err)
}
fmt.Printf("%v\n", res)
}
// exampleServer creates a test HTTP server which returns its address and
// can be closed using the returned closure.
func exampleServer() (string, func()) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
simple := testSimple{
ID: 1,
Name: "Test 1",
Slug: "test-1",
}
_ = json.NewEncoder(w).Encode(simple)
}))
return s.URL, func() { s.Close() }
}
// 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"
"errors"
"net/http"
"net/url"
"strconv"
)
// A Page contains all necessary information about the current position
// in a paged result. It is used to walk over all pages from List calls.
type Page struct {
limit int
offset int
done bool
c *Client
data pageData
endpoint string
options Valuer
err error
}
// NewPage Returns a new Page to walk over List calls.
func NewPage(client *Client, endpoint string, options Valuer) *Page {
return &Page{
limit: 50, // netbox server default
c: client,
endpoint: endpoint,
options: options,
}
}
// pageData is the internal representation of a page.
type pageData struct {
Count int `json:"count"`
NextURL string `json:"next"`
PreviousURL string `json:"previous"`
Results json.RawMessage `json:"results"`
}
// Values implements the Valuer interface. One could pass options
// to a List call. These must be extended by limit and offset to
// get the appropriate next page.
func (p *Page) Values() (url.Values, error) {
if p == nil {
return nil, errors.New("page not defined")
}
v := url.Values{}
if p.options != nil {
opts, err := p.options.Values()
if err != nil {
return nil, err
}
if opts != nil {
v = opts
}
}
v.Set("limit", strconv.Itoa(p.limit))
v.Set("offset", strconv.Itoa(p.offset))
return v, nil
}
// Next advances to the next page of API results. When Next returns false,
// no more results are available.
func (p *Page) Next() bool {
if p == nil {
return false
}
if p.done {
return false
}
req, err := p.c.NewRequest(http.MethodGet, p.endpoint, p)
if err != nil {
p.err = err
return false
}
p.data = pageData{}
err = p.c.Do(req, &p.data)
if err != nil {
p.err = err
return false
}
if p.data.NextURL == "" {
// We are on the last page, so we still need to return true to
// indicate that there is still data to process. But we set
// p.done to true, so the next "Next()" returns false.
p.done = true
} else {
p.setNextURL(p.data.NextURL)
}
return true
}
// setNext sets limit and offset parameter for the next page.
func (p *Page) setNext(limit int, offset int) {
p.limit = limit
p.offset = offset
}
// setNextURL extracts limit and offset from the nextURL, obtained from the result.
// Under the hood, it uses setNext to finally set those parameters.
func (p *Page) setNextURL(urlStr string) {
nextURL, err := url.Parse(urlStr)
if err != nil {
// We dont want to cancel this run, since there is data,
// but do not want to run into Next
p.setErr(err)
return
}
query, err := url.ParseQuery(nextURL.RawQuery)
if err != nil {
// Same like above
p.setErr(err)
return
}
limits := query["limit"]
offsets := query["offset"]
if len(limits) == 0 {
p.setErr(errors.New("no such query parameter limit"))
return
}
if len(offsets) == 0 {
p.setErr(errors.New("no such query parameter offset"))
return
}
limit, err := strconv.Atoi(limits[0])
if err != nil {
p.setErr(err)
return
}
offset, err := strconv.Atoi(offsets[0])
if err != nil {
p.setErr(err)
return
}
p.setNext(limit, offset)
}
// Err returns the internal err field. This should be called right after
// the for to get any errors occured during Next()
func (p *Page) Err() error {
return p.err
}
// setErr sets an internal err field.
func (p *Page) setErr(err error) {
if p.err == nil {
p.err = err
}
}
// 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 (
"errors"
"fmt"
"net/http"
"net/url"
"reflect"
"strconv"
"testing"
)
type testOptions struct{}
func (t *testOptions) Values() (url.Values, error) {
return url.Values{
"Hello": []string{"World"},
"limit": []string{"30"},
}, nil
}
func TestPageValues(t *testing.T) {
var tests = []struct {
desc string
page *Page
want url.Values
err error
}{
{
desc: "nil page",
page: nil,
want: nil,
err: errors.New("page not defined"),
},
{
desc: "options nil",
page: NewPage(nil, "/", nil),
want: url.Values{
"limit": []string{"50"},
"offset": []string{"0"},
},
err: nil,
},
{
desc: "options set",
page: NewPage(nil, "/", &testOptions{}),
want: url.Values{
"Hello": []string{"World"},
"limit": []string{"50"},
"offset": []string{"0"},
},
err: nil,
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
res, err := tt.page.Values()
if want, got := tt.err, err; !reflect.DeepEqual(want, got) {
t.Fatalf("unecpected error:\n- want: %v\n- got: %v", want, got)
}
if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
t.Fatalf("unexpected values:\n- want: %v\n- got: %v", want, got)
}
})
}
}
func TestNext(t *testing.T) {
var pages = []pageData{
{
Count: 99,
NextURL: "http://example.com/?limit=50&offset=50",
PreviousURL: "",
Results: []byte("{\"Hello\": \"World\"}"),
},
{
Count: 99,
NextURL: "",
PreviousURL: "http://example.com/?limit=50&offset=0",
Results: []byte("{\"Hello\": \"World\"}"),
},
}
c, done := testClient(t, testHandler(t, http.MethodGet, "/", pages[0]))
defer done()
p := NewPage(c, "/", nil)
if !p.Next() {
t.Fatal("Expected another page, got none.")
}
p.c, done = testClient(t, testHandler(t, http.MethodGet, "/", pages[1]))
defer done()
if !p.Next() {
t.Fatal("Expected another page, got none.")
}
if p.Next() {
t.Fatal("Did not expect another page, but got one.")
}
if err := p.Err(); err != nil {
t.Fatalf("Did not expect an error: %v", err)
}
}
func TestPageWithError(t *testing.T) {
c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("foo"))
})
defer done()
p := NewPage(c, "/", &testOptions{})
if p.Next() {
t.Fatal("Did not expect any page, but got one.")
}
if want, got := errors.New("403 - foo"), p.Err(); !reflect.DeepEqual(want, got) {
t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got)
}
}
func TestErr(t *testing.T) {
var tests = []struct {
desc string
set error
want error
}{
{
desc: "Err nil",
set: nil,
want: nil,
},
{
desc: "Err set",
set: errors.New("This is broken"),
want: errors.New("This is broken"),
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
p := NewPage(nil, "/", nil)
p.setErr(tt.set)
if want, got := tt.want, p.Err(); !reflect.DeepEqual(want, got) {
t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got)
}
})
}
}
func TestSetNext(t *testing.T) {
p := NewPage(nil, "/", nil)
p.setNext(99, 88)
if want, got := 99, p.limit; want != got {
t.Fatalf("unexpected limit:\n- want: %v\n- got: %v", want, got)
}
if want, got := 88, p.offset; want != got {
t.Fatalf("unexpected offset:\n- want: %v\n- got: %v", want, got)
}
}
func TestSetNextURL(t *testing.T) {
var tests = []struct {
desc string
url string
wantOffset int
wantLimit int
err error
}{
{
desc: "No Params returned",
url: "http://example.com",
wantOffset: 0,
wantLimit: 50,
err: errors.New("no such query parameter limit"),
},
{
desc: "No Offset returned",
url: "http://example.com?limit=20",
wantOffset: 0,
wantLimit: 50,
err: errors.New("no such query parameter offset"),
},
{
desc: "Limit not an int",
url: "http://example.com?limit=hello&offset=world",
wantOffset: 0,
wantLimit: 50,
err: &strconv.NumError{
Func: "Atoi",
Num: "hello",
Err: strconv.ErrSyntax,
},
},
{
desc: "Offset not an int",
url: "http://example.com?limit=50&offset=world",
wantOffset: 0,
wantLimit: 50,
err: &strconv.NumError{
Func: "Atoi",
Num: "world",
Err: strconv.ErrSyntax,
},
},
{
desc: "Correct limit and offset",
url: "http://example.com?limit=99&offset=88",
wantOffset: 88,
wantLimit: 99,
err: nil,
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
p := NewPage(nil, "/", nil)
p.setNextURL(tt.url)
if want, got := tt.wantOffset, p.offset; want != got {
t.Fatalf("unexpected offset:\n- want: %v\n- got: %v", want, got)
}
if want, got := tt.wantLimit, p.limit; want != got {
t.Fatalf("unexpected limit:\n- want: %v\n- got: %v", want, got)
}
if want, got := tt.err, p.Err(); reflect.TypeOf(want) != reflect.TypeOf(got) {
t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got)
}
})
}
}
#!/bin/bash #!/bin/bash
# Verify that all files are correctly golint'd. # Verify that all files are correctly golint'd.
EXIT=0 set -e -o nounset -o pipefail
GOLINT=$(golint ./...) counter=0
while read -r line; do
echo $line
: $((counter++))
done < <(golint ./...)
if [[ ! -z $GOLINT ]]; then if ((counter == 0)); then
echo $GOLINT exit 0
EXIT=1
fi fi
exit 1
exit $EXIT
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# Verify that the correct license block is present in all Go source # Verify that the correct license block is present in all Go source
# files. # files.
read -r -d '' EXPECTED <<EndOfLicense IFS=$'\n' read -r -d '' -a EXPECTED <<EndOfLicense
// Copyright 2017 The go-netbox Authors. // Copyright 2017 The go-netbox Authors.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
...@@ -17,15 +17,22 @@ read -r -d '' EXPECTED <<EndOfLicense ...@@ -17,15 +17,22 @@ read -r -d '' EXPECTED <<EndOfLicense
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
EndOfLicense EndOfLicense
AUTHOR_REGEX='^// Copyright 20[0-9][0-9] The go-netbox Authors\.$'
# Scan each Go source file for license. # Scan each Go source file for license.
EXIT=0 EXIT=0
GOFILES=$(find . -name "*.go") GOFILES=$(find . -name "*.go")
for FILE in $GOFILES; do for FILE in $GOFILES; do
BLOCK=$(head -n 14 $FILE) IFS=$'\n' read -r -d '' -a BLOCK < <(head -n 14 $FILE)
if [ "$BLOCK" != "$EXPECTED" ]; then tmp_block=${BLOCK[@]:1}
tmp_expected=${EXPECTED[@]:1}
if [[ $tmp_block != $tmp_expected ]]; then
echo "file missing license: $FILE"
EXIT=1
fi
if ! [[ "${BLOCK[0]}" =~ $AUTHOR_REGEX ]]; then
echo "file missing license: $FILE" echo "file missing license: $FILE"
EXIT=1 EXIT=1
fi fi
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment