Skip to content

Commit

Permalink
Merge pull request #474 from CircleCI-Public/CIRCLE-29501-add-create-…
Browse files Browse the repository at this point in the history
…runner-token

[CIRCLE-29501] Add commands to manage runner types
  • Loading branch information
pete-woods authored Oct 2, 2020
2 parents b12429a + e700b78 commit 75976b1
Show file tree
Hide file tree
Showing 23 changed files with 1,056 additions and 2 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ jobs:
test_windows:
executor: windows/default
steps:
- run: git config --global core.autocrlf false
- checkout
- run: setx GOPATH %USERPROFILE%\go
- run: go get gotest.tools/gotestsum
Expand Down
111 changes: 111 additions & 0 deletions api/rest/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package rest

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)

type Client struct {
baseURL *url.URL
circleToken string
client *http.Client
}

func New(host, endpoint, circleToken string) *Client {
// Ensure endpoint ends with a slash
if !strings.HasSuffix(endpoint, "/") {
endpoint += "/"
}

u, _ := url.Parse(host)
return &Client{
baseURL: u.ResolveReference(&url.URL{Path: endpoint}),
circleToken: circleToken,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}

func (c *Client) NewRequest(method string, u *url.URL, payload interface{}) (req *http.Request, err error) {
var r io.Reader
if payload != nil {
buf := &bytes.Buffer{}
r = buf
err = json.NewEncoder(buf).Encode(payload)
if err != nil {
return nil, err
}
}

req, err = http.NewRequest(method, c.baseURL.ResolveReference(u).String(), r)
if err != nil {
return nil, err
}

req.Header.Set("Circle-Token", c.circleToken)
req.Header.Set("Accept-Type", "application/json")
req.Header.Set("User-Agent", "circleci-cli")
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}

return req, nil
}

func (c *Client) DoRequest(req *http.Request, resp interface{}) (statusCode int, err error) {
httpResp, err := c.client.Do(req)
if err != nil {
return 0, err
}
defer httpResp.Body.Close()

if httpResp.StatusCode >= 300 {
httpError := struct {
Message string `json:"message"`
}{}
err = json.NewDecoder(httpResp.Body).Decode(&httpError)
if err != nil {
return httpResp.StatusCode, err
}
return httpResp.StatusCode, &HTTPError{Code: httpResp.StatusCode, Err: errors.New(httpError.Message)}
}

if resp != nil {
if !strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") {
return httpResp.StatusCode, errors.New("wrong content type received")
}

err = json.NewDecoder(httpResp.Body).Decode(resp)
if err != nil {
return httpResp.StatusCode, err
}
}
return httpResp.StatusCode, nil
}

type HTTPError struct {
Code int
Err error
}

func (e *HTTPError) Error() string {
if e.Code == 0 {
e.Code = http.StatusInternalServerError
}
if e.Err != nil {
return fmt.Sprintf("%v (%d-%s)", e.Err, e.Code, http.StatusText(e.Code))
}
return fmt.Sprintf("response %d (%s)", e.Code, http.StatusText(e.Code))
}

func (e *HTTPError) Unwrap() error {
return e.Err
}
169 changes: 169 additions & 0 deletions api/rest/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package rest

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"

"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
)

func TestClient_DoRequest(t *testing.T) {
t.Run("PUT with req and resp", func(t *testing.T) {
fix := &fixture{}
c, cleanup := fix.Run(http.StatusCreated, `{"key": "value"}`)
defer cleanup()

t.Run("Check result", func(t *testing.T) {
r, err := c.NewRequest("PUT", &url.URL{Path: "my/endpoint"}, struct {
A string
B int
}{
A: "aaa",
B: 123,
})
assert.NilError(t, err)

resp := make(map[string]interface{})
statusCode, err := c.DoRequest(r, &resp)
assert.NilError(t, err)
assert.Equal(t, statusCode, http.StatusCreated)
assert.Check(t, cmp.DeepEqual(resp, map[string]interface{}{
"key": "value",
}))
})

t.Run("Check request", func(t *testing.T) {
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/my/endpoint"}))
assert.Check(t, cmp.Equal(fix.Method(), "PUT"))
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"Content-Length": {"20"},
"Content-Type": {"application/json"},
"User-Agent": {"circleci-cli"},
}))
assert.Check(t, cmp.Equal(fix.Body(), `{"A":"aaa","B":123}`+"\n"))
})
})

t.Run("GET with error status", func(t *testing.T) {
fix := &fixture{}
c, cleanup := fix.Run(http.StatusBadRequest, `{"message": "the error message"}`)
defer cleanup()

t.Run("Check result", func(t *testing.T) {
r, err := c.NewRequest("GET", &url.URL{Path: "my/error/endpoint"}, nil)
assert.NilError(t, err)

resp := make(map[string]interface{})
statusCode, err := c.DoRequest(r, &resp)
assert.Error(t, err, "the error message (400-Bad Request)")
assert.Equal(t, statusCode, http.StatusBadRequest)
assert.Check(t, cmp.DeepEqual(resp, map[string]interface{}{}))
})

t.Run("Check request", func(t *testing.T) {
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/my/error/endpoint"}))
assert.Check(t, cmp.Equal(fix.Method(), "GET"))
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
}))
assert.Check(t, cmp.Equal(fix.Body(), ""))
})
})

t.Run("GET with resp only", func(t *testing.T) {
fix := &fixture{}
c, cleanup := fix.Run(http.StatusCreated, `{"a": "abc", "b": true}`)
defer cleanup()

t.Run("Check result", func(t *testing.T) {
r, err := c.NewRequest("GET", &url.URL{Path: "path"}, nil)
assert.NilError(t, err)

resp := make(map[string]interface{})
statusCode, err := c.DoRequest(r, &resp)
assert.NilError(t, err)
assert.Equal(t, statusCode, http.StatusCreated)
assert.Check(t, cmp.DeepEqual(resp, map[string]interface{}{
"a": "abc",
"b": true,
}))
})

t.Run("Check request", func(t *testing.T) {
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/path"}))
assert.Check(t, cmp.Equal(fix.Method(), "GET"))
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
}))
assert.Check(t, cmp.Equal(fix.Body(), ""))
})
})
}

type fixture struct {
mu sync.Mutex
url url.URL
method string
header http.Header
body bytes.Buffer
}

func (f *fixture) URL() url.URL {
f.mu.Lock()
defer f.mu.Unlock()
return f.url
}

func (f *fixture) Method() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.method
}

func (f *fixture) Header() http.Header {
f.mu.Lock()
defer f.mu.Unlock()
return f.header
}

func (f *fixture) Body() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.body.String()
}

func (f *fixture) Run(statusCode int, respBody string) (c *Client, cleanup func()) {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()

defer r.Body.Close()
_, _ = io.Copy(&f.body, r.Body)
f.url = *r.URL
f.header = r.Header
f.method = r.Method

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_, _ = io.WriteString(w, respBody)
})
server := httptest.NewServer(mux)

return New(server.URL, "api/v2", "fake-token"), server.Close
}
Loading

0 comments on commit 75976b1

Please sign in to comment.