api + basic logic + hydra client crud + validation + happy path integration test
This commit is contained in:
149
hydra/client.go
Normal file
149
hydra/client.go
Normal file
@ -0,0 +1,149 @@
|
||||
package hydra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
HydraURL url.URL
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func (c *Client) GetOAuth2Client(id string) (*OAuth2ClientJSON, bool, error) {
|
||||
|
||||
var jsonClient *OAuth2ClientJSON
|
||||
|
||||
req, err := c.newRequest(http.MethodGet, id, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
resp, err := c.do(req, &jsonClient)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return jsonClient, true, nil
|
||||
case http.StatusNotFound:
|
||||
return nil, false, nil
|
||||
default:
|
||||
return nil, false, fmt.Errorf("%s %s http request returned unexpected status code %s", req.Method, req.URL.String(), resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) PostOAuth2Client(o *OAuth2ClientJSON) (*OAuth2ClientJSON, error) {
|
||||
|
||||
var jsonClient *OAuth2ClientJSON
|
||||
|
||||
req, err := c.newRequest(http.MethodPost, "", o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.do(req, &jsonClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusCreated:
|
||||
return jsonClient, nil
|
||||
case http.StatusConflict:
|
||||
return nil, fmt.Errorf(" %s %s http request failed: requested ID already exists", req.Method, req.URL)
|
||||
default:
|
||||
return nil, fmt.Errorf("%s %s http request returned unexpected status code: %s", req.Method, req.URL, resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) PutOAuth2Client(o *OAuth2ClientJSON) (*OAuth2ClientJSON, error) {
|
||||
|
||||
var jsonClient *OAuth2ClientJSON
|
||||
|
||||
req, err := c.newRequest(http.MethodPut, *o.ClientID, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.do(req, &jsonClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s %s http request returned unexpected status code: %s", req.Method, req.URL, resp.Status)
|
||||
}
|
||||
|
||||
return jsonClient, nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteOAuth2Client(id string) error {
|
||||
|
||||
req, err := c.newRequest(http.MethodDelete, id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.do(req, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusNoContent:
|
||||
return nil
|
||||
case http.StatusNotFound:
|
||||
fmt.Printf("client with id %s does not exist", id)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%s %s http request returned unexpected status code %s", req.Method, req.URL.String(), resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(method, relativePath string, body interface{}) (*http.Request, error) {
|
||||
|
||||
var buf io.ReadWriter
|
||||
if body != nil {
|
||||
buf = new(bytes.Buffer)
|
||||
err := json.NewEncoder(buf).Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
u := c.HydraURL
|
||||
u.Path = path.Join(u.Path, relativePath)
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
return req, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) {
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
if v != nil {
|
||||
err = json.NewDecoder(resp.Body).Decode(v)
|
||||
}
|
||||
return resp, err
|
||||
}
|
269
hydra/client_test.go
Normal file
269
hydra/client_test.go
Normal file
@ -0,0 +1,269 @@
|
||||
package hydra_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/ory/hydra-maester/hydra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
testID = "test-id"
|
||||
schemeHTTP = "http"
|
||||
testClient = `{"client_id":"test-id","client_name":"test-name","scope":"some,scopes","grant_types":["type1"]}`
|
||||
testClientCreated = `{"client_id":"test-id-2", "client_secret": "TmGkvcY7k526","client_name":"test-name-2","scope":"some,other,scopes","grant_types":["type2"]}`
|
||||
testClientUpdated = `{"client_id":"test-id-3", "client_secret": "xFoPPm654por","client_name":"test-name-3","scope":"yet,another,scope","grant_types":["type3"]}`
|
||||
emptyBody = `{}`
|
||||
clientsEndpoint = "/clients"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
statusCode int
|
||||
respBody string
|
||||
err error
|
||||
}
|
||||
|
||||
var testOAuthJSONPost = &hydra.OAuth2ClientJSON{
|
||||
Name: "test-name-2",
|
||||
Scope: "some,other,scopes",
|
||||
GrantTypes: []string{"type2"},
|
||||
}
|
||||
|
||||
var testOAuthJSONPut = &hydra.OAuth2ClientJSON{
|
||||
ClientID: pointer.StringPtr("test-id-3"),
|
||||
Name: "test-name-3",
|
||||
Scope: "yet,another,scope",
|
||||
GrantTypes: []string{"type3"},
|
||||
}
|
||||
|
||||
func TestCRUD(t *testing.T) {
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
c := hydra.Client{
|
||||
HTTPClient: &http.Client{},
|
||||
HydraURL: url.URL{Scheme: schemeHTTP},
|
||||
}
|
||||
|
||||
t.Run("method=get", func(t *testing.T) {
|
||||
|
||||
for d, tc := range map[string]server{
|
||||
"getting registered client": {
|
||||
http.StatusOK,
|
||||
testClient,
|
||||
nil,
|
||||
},
|
||||
"getting unregistered client": {
|
||||
http.StatusNotFound,
|
||||
emptyBody,
|
||||
nil,
|
||||
},
|
||||
"internal server error when requesting": {
|
||||
http.StatusInternalServerError,
|
||||
emptyBody,
|
||||
errors.New("http request returned unexpected status code"),
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
|
||||
|
||||
//given
|
||||
shouldFind := tc.statusCode == http.StatusOK
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(fmt.Sprintf("%s/%s", c.HydraURL.String(), testID), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
|
||||
assert.Equal(http.MethodGet, req.Method)
|
||||
w.WriteHeader(tc.statusCode)
|
||||
w.Write([]byte(tc.respBody))
|
||||
if shouldFind {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
}
|
||||
})
|
||||
runServer(&c, h)
|
||||
|
||||
//when
|
||||
o, found, err := c.GetOAuth2Client(testID)
|
||||
|
||||
//then
|
||||
if tc.err == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Contains(err.Error(), tc.err.Error())
|
||||
}
|
||||
|
||||
assert.Equal(shouldFind, found)
|
||||
if shouldFind {
|
||||
require.NotNil(t, o)
|
||||
var expected hydra.OAuth2ClientJSON
|
||||
json.Unmarshal([]byte(testClient), &expected)
|
||||
assert.Equal(&expected, o)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("method=post", func(t *testing.T) {
|
||||
|
||||
for d, tc := range map[string]server{
|
||||
"with new client": {
|
||||
http.StatusCreated,
|
||||
testClientCreated,
|
||||
nil,
|
||||
},
|
||||
"with existing client": {
|
||||
http.StatusConflict,
|
||||
emptyBody,
|
||||
errors.New("requested ID already exists"),
|
||||
},
|
||||
"internal server error when requesting": {
|
||||
http.StatusInternalServerError,
|
||||
emptyBody,
|
||||
errors.New("http request returned unexpected status code"),
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
|
||||
|
||||
//given
|
||||
new := tc.statusCode == http.StatusCreated
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(c.HydraURL.String(), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
|
||||
assert.Equal(http.MethodPost, req.Method)
|
||||
w.WriteHeader(tc.statusCode)
|
||||
w.Write([]byte(tc.respBody))
|
||||
if new {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
}
|
||||
})
|
||||
runServer(&c, h)
|
||||
|
||||
//when
|
||||
o, err := c.PostOAuth2Client(testOAuthJSONPost)
|
||||
|
||||
//then
|
||||
if tc.err == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Contains(err.Error(), tc.err.Error())
|
||||
}
|
||||
|
||||
if new {
|
||||
require.NotNil(t, o)
|
||||
|
||||
assert.Equal(testOAuthJSONPost.Name, o.Name)
|
||||
assert.Equal(testOAuthJSONPost.Scope, o.Scope)
|
||||
assert.Equal(testOAuthJSONPost.GrantTypes, o.GrantTypes)
|
||||
assert.NotNil(o.Secret)
|
||||
assert.NotNil(o.ClientID)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("method=put", func(t *testing.T) {
|
||||
for d, tc := range map[string]server{
|
||||
"with registered client": {
|
||||
http.StatusOK,
|
||||
testClientUpdated,
|
||||
nil,
|
||||
},
|
||||
"internal server error when requesting": {
|
||||
http.StatusInternalServerError,
|
||||
emptyBody,
|
||||
errors.New("http request returned unexpected status code"),
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
|
||||
|
||||
ok := tc.statusCode == http.StatusOK
|
||||
|
||||
//given
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(fmt.Sprintf("%s/%s", c.HydraURL.String(), *testOAuthJSONPut.ClientID), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
|
||||
assert.Equal(http.MethodPut, req.Method)
|
||||
w.WriteHeader(tc.statusCode)
|
||||
w.Write([]byte(tc.respBody))
|
||||
if ok {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
}
|
||||
})
|
||||
runServer(&c, h)
|
||||
|
||||
//when
|
||||
o, err := c.PutOAuth2Client(testOAuthJSONPut)
|
||||
|
||||
//then
|
||||
if tc.err == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Contains(err.Error(), tc.err.Error())
|
||||
}
|
||||
|
||||
if ok {
|
||||
require.NotNil(t, o)
|
||||
|
||||
assert.Equal(testOAuthJSONPut.Name, o.Name)
|
||||
assert.Equal(testOAuthJSONPut.Scope, o.Scope)
|
||||
assert.Equal(testOAuthJSONPut.GrantTypes, o.GrantTypes)
|
||||
assert.Equal(testOAuthJSONPut.ClientID, o.ClientID)
|
||||
assert.NotNil(o.Secret)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("method=delete", func(t *testing.T) {
|
||||
|
||||
for d, tc := range map[string]server{
|
||||
"with registered client": {
|
||||
statusCode: http.StatusNoContent,
|
||||
},
|
||||
"with unregistered client": {
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
"internal server error when requesting": {
|
||||
statusCode: http.StatusInternalServerError,
|
||||
err: errors.New("http request returned unexpected status code"),
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
|
||||
|
||||
//given
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(fmt.Sprintf("%s/%s", c.HydraURL.String(), testID), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
|
||||
assert.Equal(http.MethodDelete, req.Method)
|
||||
w.WriteHeader(tc.statusCode)
|
||||
})
|
||||
runServer(&c, h)
|
||||
|
||||
//when
|
||||
err := c.DeleteOAuth2Client(testID)
|
||||
|
||||
//then
|
||||
if tc.err == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Contains(err.Error(), tc.err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func runServer(c *hydra.Client, h http.HandlerFunc) {
|
||||
s := httptest.NewServer(h)
|
||||
serverUrl, _ := url.Parse(s.URL)
|
||||
c.HydraURL = *serverUrl.ResolveReference(&url.URL{Path: clientsEndpoint})
|
||||
}
|
11
hydra/types.go
Normal file
11
hydra/types.go
Normal file
@ -0,0 +1,11 @@
|
||||
package hydra
|
||||
|
||||
// OAuth2ClientJSON represents an OAuth2 client digestible by ORY Hydra
|
||||
type OAuth2ClientJSON struct {
|
||||
ClientID *string `json:"client_id,omitempty"`
|
||||
Name string `json:"client_name"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
ResponseTypes []string `json:"response_types,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Secret *string `json:"client_secret,omitempty"`
|
||||
}
|
Reference in New Issue
Block a user