From 32e961873046a0e82d67fa24e84940f2f1d0da8b Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Thu, 30 Dec 2021 01:53:31 +0500 Subject: [PATCH] :recycle: Refactored client package --- internal/client/repository.go | 5 +- .../client/repository/http/http_client.go | 155 +++++++++++------- .../repository/http/http_client_test.go | 53 ++++-- .../client/repository/memory/memory_client.go | 22 ++- .../repository/memory/memory_client_test.go | 4 +- internal/client/usecase.go | 6 +- internal/client/usecase/client_ucase.go | 15 +- internal/client/usecase/client_ucase_test.go | 8 +- 8 files changed, 170 insertions(+), 98 deletions(-) diff --git a/internal/client/repository.go b/internal/client/repository.go index 71b5231..e4487bc 100644 --- a/internal/client/repository.go +++ b/internal/client/repository.go @@ -2,10 +2,13 @@ package client import ( "context" + "errors" "source.toby3d.me/website/oauth/internal/domain" ) type Repository interface { - Get(ctx context.Context, id string) (*domain.Client, error) + Get(ctx context.Context, id *domain.ClientID) (*domain.Client, error) } + +var ErrNotExist = errors.New("client with the specified ID does not exist") diff --git a/internal/client/repository/http/http_client.go b/internal/client/repository/http/http_client.go index 7d5aceb..e7b78f3 100644 --- a/internal/client/repository/http/http_client.go +++ b/internal/client/repository/http/http_client.go @@ -3,10 +3,10 @@ package http import ( "bytes" "context" + "fmt" "net/url" "strings" - "github.com/pkg/errors" "github.com/tomnomnom/linkheader" http "github.com/valyala/fasthttp" "willnorris.com/go/microformats" @@ -20,16 +20,14 @@ type httpClientRepository struct { } const ( - HApp string = "h-app" - HXApp string = "h-x-app" + relRedirectURI string = "redirect_uri" - KeyName string = "name" - KeyLogo string = "logo" - KeyURL string = "url" + hApp string = "h-app" + hXApp string = "h-x-app" - ValueValue string = "value" - - RelRedirectURI string = "redirect_uri" + propertyLogo string = "logo" + propertyName string = "name" + propertyURL string = "url" ) func NewHTTPClientRepository(c *http.Client) client.Repository { @@ -38,93 +36,134 @@ func NewHTTPClientRepository(c *http.Client) client.Repository { } } -func (repo *httpClientRepository) Get(ctx context.Context, id string) (*domain.Client, error) { - u, err := url.Parse(id) - if err != nil { - return nil, errors.Wrap(err, "failed to parse id as url") - } - +func (repo *httpClientRepository) Get(ctx context.Context, id *domain.ClientID) (*domain.Client, error) { req := http.AcquireRequest() defer http.ReleaseRequest(req) - req.SetRequestURI(u.String()) + req.SetRequestURI(id.String()) req.Header.SetMethod(http.MethodGet) resp := http.AcquireResponse() defer http.ReleaseResponse(resp) if err := repo.client.Do(req, resp); err != nil { - return nil, errors.Wrap(err, "failed to make a request to the client") + return nil, fmt.Errorf("failed to make a request to the client: %w", err) } - client := domain.NewClient() - client.ID = id + if resp.StatusCode() == http.StatusNotFound { + return nil, client.ErrNotExist + } - for _, l := range linkheader.Parse(string(resp.Header.Peek(http.HeaderLink))) { - if !strings.Contains(l.Rel, "redirect_uri") { + client := &domain.Client{ + ID: id, + Logo: make([]*domain.URL, 0), + Name: extractValues(resp, propertyName), + RedirectURI: extractEndpoints(resp, relRedirectURI), + URL: make([]*domain.URL, 0), + } + + for _, v := range extractValues(resp, propertyLogo) { + u, err := domain.NewURL(v) + if err != nil { continue } - client.RedirectURI = append(client.RedirectURI, l.URL) + client.Logo = append(client.Logo, u) } - data := microformats.Parse(bytes.NewReader(resp.Body()), u) - - for _, item := range data.Items { - if len(item.Type) == 0 && !strings.EqualFold(item.Type[0], HApp) && - !strings.EqualFold(item.Type[0], HXApp) { + for _, v := range extractValues(resp, propertyURL) { + u, err := domain.NewURL(v) + if err != nil { continue } - populateProperties(item.Properties, client) + client.URL = append(client.URL, u) } - populateRels(data.Rels, client) - return client, nil } -func populateProperties(src map[string][]interface{}, dst *domain.Client) { - for key, property := range src { - if len(property) == 0 { +func extractEndpoints(resp *http.Response, name string) []*domain.URL { + results := make([]*domain.URL, 0) + endpoints, _ := extractEndpointsFromHeader(resp, name) + results = append(results, endpoints...) + endpoints, _ = extractEndpointsFromBody(resp, name) + results = append(results, endpoints...) + + return results +} + +func extractValues(resp *http.Response, key string) []string { + results := make([]string, 0) + + for _, item := range microformats.Parse(bytes.NewReader(resp.Body()), nil).Items { + if len(item.Type) == 0 || (item.Type[0] != hApp && item.Type[0] != hXApp) { continue } - switch key { - case KeyName: - dst.Name = getString(property) - case KeyLogo: - for i := range property { - switch val := property[i].(type) { - case string: - dst.Logo = val - case map[string]string: - dst.Logo = val[ValueValue] + properties, ok := item.Properties[key] + if !ok || len(properties) == 0 { + return nil + } + + for j := range properties { + switch p := properties[j].(type) { + case string: + results = append(results, p) + case map[string][]interface{}: + for _, val := range p["value"] { + v, ok := val.(string) + if !ok { + continue + } + + results = append(results, v) } } - case KeyURL: - dst.URL = getString(property) } + + return results } + + return nil } -func populateRels(src map[string][]string, dst *domain.Client) { - for key, values := range src { - if !strings.EqualFold(key, RelRedirectURI) { +func extractEndpointsFromHeader(resp *http.Response, name string) ([]*domain.URL, error) { + results := make([]*domain.URL, 0) + + for _, link := range linkheader.Parse(string(resp.Header.Peek(http.HeaderLink))) { + if !strings.EqualFold(link.Rel, name) { continue } - for i := range values { - dst.RedirectURI = append(dst.RedirectURI, values[i]) + u := http.AcquireURI() + if err := u.Parse(resp.Header.Peek(http.HeaderHost), []byte(link.URL)); err != nil { + return nil, err } - } -} -func getString(property []interface{}) string { - for i := range property { - val, _ := property[i].(string) - - return val + results = append(results, &domain.URL{URI: u}) } - return "" + return results, nil +} + +func extractEndpointsFromBody(resp *http.Response, name string) ([]*domain.URL, error) { + host, err := url.Parse(string(resp.Header.Peek(http.HeaderHost))) + if err != nil { + return nil, fmt.Errorf("cannot parse host header: %w", err) + } + + endpoints, ok := microformats.Parse(bytes.NewReader(resp.Body()), host).Rels[name] + if !ok || len(endpoints) == 0 { + return nil, nil + } + + results := make([]*domain.URL, 0) + for i := range endpoints { + u := http.AcquireURI() + u.Update(endpoints[i]) + + results = append(results, &domain.URL{URI: u}) + } + + return results, nil } diff --git a/internal/client/repository/http/http_client_test.go b/internal/client/repository/http/http_client_test.go index a1c3706..972fb07 100644 --- a/internal/client/repository/http/http_client_test.go +++ b/internal/client/repository/http/http_client_test.go @@ -2,6 +2,7 @@ package http_test import ( "context" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -11,7 +12,7 @@ import ( repository "source.toby3d.me/website/oauth/internal/client/repository/http" "source.toby3d.me/website/oauth/internal/common" "source.toby3d.me/website/oauth/internal/domain" - "source.toby3d.me/website/oauth/internal/util" + "source.toby3d.me/website/oauth/internal/testing/httptest" ) const testBody string = ` @@ -20,13 +21,13 @@ const testBody string = ` - Example App - + %[1]s + -
- - Example App +
+ + %[1]s
@@ -35,17 +36,37 @@ const testBody string = ` func TestGet(t *testing.T) { t.Parallel() - client, _, cleanup := util.TestServe(t, func(ctx *http.RequestCtx) { - ctx.Response.Header.Set(http.HeaderLink, `; rel="redirect_uri">`) - ctx.SetStatusCode(http.StatusOK) - ctx.SetContentType(common.MIMETextHTML) - ctx.SetBodyString(testBody) - }) + client := domain.TestClient(t) + httpClient, _, cleanup := httptest.New(t, testHandler(t, client)) t.Cleanup(cleanup) - c := domain.TestClient(t) - - result, err := repository.NewHTTPClientRepository(client).Get(context.TODO(), c.ID) + result, err := repository.NewHTTPClientRepository(httpClient).Get(context.TODO(), client.ID) require.NoError(t, err) - assert.Equal(t, c, result) + + assert.Equal(t, client.Name, result.Name) + assert.Equal(t, client.ID.String(), result.ID.String()) + + for i := range client.URL { + assert.Equal(t, client.URL[i].String(), result.URL[i].String()) + } + + for i := range client.Logo { + assert.Equal(t, client.Logo[i].String(), result.Logo[i].String()) + } + + for i := range client.RedirectURI { + assert.Equal(t, client.RedirectURI[i].String(), result.RedirectURI[i].String()) + } +} + +func testHandler(tb testing.TB, client *domain.Client) http.RequestHandler { + tb.Helper() + + return func(ctx *http.RequestCtx) { + ctx.Response.Header.Set(http.HeaderLink, `<`+client.RedirectURI[0].String()+`>; rel="redirect_uri"`) + ctx.SuccessString(common.MIMETextHTMLCharsetUTF8, fmt.Sprintf( + testBody, client.Name[0], client.URL[0].String(), client.Logo[0].String(), + client.RedirectURI[1].String(), + )) + } } diff --git a/internal/client/repository/memory/memory_client.go b/internal/client/repository/memory/memory_client.go index b506611..1dfbf25 100644 --- a/internal/client/repository/memory/memory_client.go +++ b/internal/client/repository/memory/memory_client.go @@ -10,26 +10,32 @@ import ( ) type memoryClientRepository struct { - clients *sync.Map + store *sync.Map } -const Key string = "clients" +const DefaultPathPrefix string = "clients" -func NewMemoryClientRepository(clients *sync.Map) client.Repository { +func NewMemoryClientRepository(store *sync.Map) client.Repository { return &memoryClientRepository{ - clients: clients, + store: store, } } -func (repo *memoryClientRepository) Get(ctx context.Context, id string) (*domain.Client, error) { - src, ok := repo.clients.Load(path.Join(Key, id)) +func (repo *memoryClientRepository) Create(ctx context.Context, client *domain.Client) error { + repo.store.Store(path.Join(DefaultPathPrefix, client.ID.String()), client) + + return nil +} + +func (repo *memoryClientRepository) Get(ctx context.Context, id *domain.ClientID) (*domain.Client, error) { + src, ok := repo.store.Load(path.Join(DefaultPathPrefix, id.String())) if !ok { - return nil, nil + return nil, client.ErrNotExist } c, ok := src.(*domain.Client) if !ok { - return nil, nil + return nil, client.ErrNotExist } return c, nil diff --git a/internal/client/repository/memory/memory_client_test.go b/internal/client/repository/memory/memory_client_test.go index 35bafd4..2e33ec3 100644 --- a/internal/client/repository/memory/memory_client_test.go +++ b/internal/client/repository/memory/memory_client_test.go @@ -16,10 +16,10 @@ import ( func TestGet(t *testing.T) { t.Parallel() - store := new(sync.Map) client := domain.TestClient(t) - store.Store(path.Join(repository.Key, client.ID), client) + store := new(sync.Map) + store.Store(path.Join(repository.DefaultPathPrefix, client.ID.String()), client) result, err := repository.NewMemoryClientRepository(store).Get(context.TODO(), client.ID) require.NoError(t, err) diff --git a/internal/client/usecase.go b/internal/client/usecase.go index 8ef849a..d4ff7d1 100644 --- a/internal/client/usecase.go +++ b/internal/client/usecase.go @@ -2,10 +2,14 @@ package client import ( "context" + "errors" "source.toby3d.me/website/oauth/internal/domain" ) type UseCase interface { - Discovery(ctx context.Context, clientID string) (*domain.Client, error) + // Discovery returns client public information bu ClientID URL. + Discovery(ctx context.Context, id *domain.ClientID) (*domain.Client, error) } + +var ErrInvalidMe = errors.New("provided me is invalid") diff --git a/internal/client/usecase/client_ucase.go b/internal/client/usecase/client_ucase.go index e82310c..0c02b77 100644 --- a/internal/client/usecase/client_ucase.go +++ b/internal/client/usecase/client_ucase.go @@ -2,27 +2,26 @@ package usecase import ( "context" - - "github.com/pkg/errors" + "fmt" "source.toby3d.me/website/oauth/internal/client" "source.toby3d.me/website/oauth/internal/domain" ) type clientUseCase struct { - clients client.Repository + repo client.Repository } -func NewClientUseCase(clients client.Repository) client.UseCase { +func NewClientUseCase(repo client.Repository) client.UseCase { return &clientUseCase{ - clients: clients, + repo: repo, } } -func (useCase *clientUseCase) Discovery(ctx context.Context, clientID string) (*domain.Client, error) { - c, err := useCase.clients.Get(ctx, clientID) +func (useCase *clientUseCase) Discovery(ctx context.Context, id *domain.ClientID) (*domain.Client, error) { + c, err := useCase.repo.Get(ctx, id) if err != nil { - return nil, errors.Wrap(err, "failed to get client information") + return nil, fmt.Errorf("cannot discovery client by id: %w", err) } return c, nil diff --git a/internal/client/usecase/client_ucase_test.go b/internal/client/usecase/client_ucase_test.go index 0ad0fae..fb1b5d6 100644 --- a/internal/client/usecase/client_ucase_test.go +++ b/internal/client/usecase/client_ucase_test.go @@ -17,13 +17,13 @@ import ( func TestDiscovery(t *testing.T) { t.Parallel() - store := new(sync.Map) client := domain.TestClient(t) - store.Store(path.Join(repository.Key, client.ID), client) + store := new(sync.Map) + store.Store(path.Join(repository.DefaultPathPrefix, client.ID.String()), client) - result, err := usecase.NewClientUseCase(repository.NewMemoryClientRepository(store)).Discovery(context.TODO(), - client.ID) + result, err := usecase.NewClientUseCase(repository.NewMemoryClientRepository(store)). + Discovery(context.TODO(), client.ID) require.NoError(t, err) assert.Equal(t, client, result) }