🔀 Merge branch 'feature/client' into develop

This commit is contained in:
Maxim Lebedev 2021-09-25 16:01:30 +05:00
commit 958e8ca438
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
11 changed files with 379 additions and 5 deletions

1
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
github.com/valyala/fasthttp v1.29.0
github.com/valyala/quicktemplate v1.6.3
go.etcd.io/bbolt v1.3.6

2
go.sum
View File

@ -254,6 +254,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=

View File

@ -0,0 +1,11 @@
package client
import (
"context"
"source.toby3d.me/website/oauth/internal/model"
)
type Repository interface {
Get(ctx context.Context, id string) (*model.Client, error)
}

View File

@ -0,0 +1,131 @@
package http
import (
"bytes"
"context"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/tomnomnom/linkheader"
http "github.com/valyala/fasthttp"
"source.toby3d.me/website/oauth/internal/client"
"source.toby3d.me/website/oauth/internal/model"
"willnorris.com/go/microformats"
)
type httpClientRepository struct {
client *http.Client
}
const (
HApp string = "h-app"
HXApp string = "h-x-app"
KeyName string = "name"
KeyLogo string = "logo"
KeyURL string = "url"
ValueValue string = "value"
RelRedirectURI string = "redirect_uri"
)
func NewHTTPClientRepository(c *http.Client) client.Repository {
return &httpClientRepository{
client: c,
}
}
func (repo *httpClientRepository) Get(ctx context.Context, id string) (*model.Client, error) {
req := http.AcquireRequest()
defer http.ReleaseRequest(req)
req.Header.SetMethod(http.MethodGet)
req.SetRequestURI(id)
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")
}
client := new(model.Client)
client.ID = model.URL(id)
client.RedirectURI = make([]model.URL, 0)
for _, l := range linkheader.Parse(string(resp.Header.Peek(http.HeaderLink))) {
if !strings.Contains(l.Rel, "redirect_uri") {
continue
}
client.RedirectURI = append(client.RedirectURI, model.URL(l.URL))
}
u, err := url.Parse(id)
if err != nil {
return nil, errors.Wrap(err, "failed to parse id as url")
}
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) {
continue
}
populateProperties(item.Properties, client)
}
populateRels(data.Rels, client)
return client, nil
}
func populateProperties(src map[string][]interface{}, dst *model.Client) {
for key, property := range src {
if len(property) == 0 {
continue
}
switch key {
case KeyName:
dst.Name = getString(property)
case KeyLogo:
for i := range property {
switch val := property[i].(type) {
case string:
dst.Logo = model.URL(val)
case map[string]string:
dst.Logo = model.URL(val[ValueValue])
}
}
case KeyURL:
dst.URL = model.URL(getString(property))
}
}
}
func populateRels(src map[string][]string, dst *model.Client) {
for key, values := range src {
if !strings.EqualFold(key, RelRedirectURI) {
continue
}
for i := range values {
dst.RedirectURI = append(dst.RedirectURI, model.URL(values[i]))
}
}
}
func getString(property []interface{}) string {
for i := range property {
val, _ := property[i].(string)
return val
}
return ""
}

View File

@ -0,0 +1,80 @@
package http_test
import (
"context"
"net"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
http "github.com/valyala/fasthttp"
httputil "github.com/valyala/fasthttp/fasthttputil"
repository "source.toby3d.me/website/oauth/internal/client/repository/http"
"source.toby3d.me/website/oauth/internal/common"
"source.toby3d.me/website/oauth/internal/model"
)
const testBody string = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example App</title>
<link rel="redirect_uri" href="/redirect">
</head>
<body>
<div class="h-app">
<img src="/logo.png" class="u-logo">
<a href="/" class="u-url p-name">Example App</a>
</div>
</body>
</html>
`
func TestGet(t *testing.T) {
t.Parallel()
ln := httputil.NewInmemoryListener()
u := http.AcquireURI()
u.SetScheme("http")
u.SetHost(ln.Addr().String())
t.Cleanup(func() {
http.ReleaseURI(u)
assert.NoError(t, ln.Close())
})
go func(t *testing.T) {
t.Helper()
require.NoError(t, http.Serve(ln, func(ctx *http.RequestCtx) {
ctx.SuccessString(common.MIMETextHTML, testBody)
ctx.Response.Header.Set(http.HeaderLink,
`<https://app.example.com/redirect>; rel="redirect_uri">`)
}))
}(t)
client := new(http.Client)
client.Dial = func(addr string) (net.Conn, error) {
conn, err := ln.Dial()
if err != nil {
return nil, errors.Wrap(err, "failed to dial the address")
}
return conn, nil
}
result, err := repository.NewHTTPClientRepository(client).Get(context.TODO(), u.String())
require.NoError(t, err)
assert.Equal(t, &model.Client{
ID: model.URL(u.String()),
Name: "Example App",
Logo: model.URL(u.String() + "logo.png"),
URL: model.URL(u.String()),
RedirectURI: []model.URL{
"https://app.example.com/redirect",
model.URL(u.String() + "redirect"),
},
}, result)
}

View File

@ -0,0 +1,33 @@
package memory
import (
"context"
"sync"
"source.toby3d.me/website/oauth/internal/client"
"source.toby3d.me/website/oauth/internal/model"
)
type memoryClientRepository struct {
clients *sync.Map
}
func NewMemoryClientRepository(clients *sync.Map) client.Repository {
return &memoryClientRepository{
clients: clients,
}
}
func (repo *memoryClientRepository) Get(ctx context.Context, id string) (*model.Client, error) {
src, ok := repo.clients.Load(id)
if !ok {
return nil, nil
}
c, ok := src.(*model.Client)
if !ok {
return nil, nil
}
return c, nil
}

View File

@ -0,0 +1,34 @@
package memory_test
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"source.toby3d.me/website/oauth/internal/client/repository/memory"
"source.toby3d.me/website/oauth/internal/model"
)
func TestGet(t *testing.T) {
t.Parallel()
store := new(sync.Map)
client := &model.Client{
ID: "http://127.0.0.1:2368/",
Name: "Example App",
Logo: "http://127.0.0.1:2368/logo.png",
URL: "http://127.0.0.1:2368/",
RedirectURI: []model.URL{
"https://app.example.com/redirect",
"http://127.0.0.1:2368/redirect",
},
}
store.Store(string(client.ID), client)
result, err := memory.NewMemoryClientRepository(store).Get(context.TODO(), string(client.ID))
require.NoError(t, err)
assert.Equal(t, client, result)
}

View File

@ -0,0 +1,11 @@
package client
import (
"context"
"source.toby3d.me/website/oauth/internal/model"
)
type UseCase interface {
Discovery(ctx context.Context, clientID model.URL) (*model.Client, error)
}

View File

@ -0,0 +1,28 @@
package usecase
import (
"context"
"github.com/pkg/errors"
"source.toby3d.me/website/oauth/internal/client"
"source.toby3d.me/website/oauth/internal/model"
)
type clientUseCase struct {
clients client.Repository
}
func NewClientUseCase(clients client.Repository) client.UseCase {
return &clientUseCase{
clients: clients,
}
}
func (useCase *clientUseCase) Discovery(ctx context.Context, clientID model.URL) (*model.Client, error) {
c, err := useCase.clients.Get(ctx, string(clientID))
if err != nil {
return nil, errors.Wrap(err, "failed to get client information")
}
return c, nil
}

View File

@ -0,0 +1,38 @@
package usecase_test
import (
"context"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
repository "source.toby3d.me/website/oauth/internal/client/repository/memory"
"source.toby3d.me/website/oauth/internal/client/usecase"
"source.toby3d.me/website/oauth/internal/model"
)
func TestDiscovery(t *testing.T) {
t.Parallel()
require := require.New(t)
assert := assert.New(t)
store := new(sync.Map)
client := &model.Client{
ID: "http://127.0.0.1:2368/",
Name: "Example App",
Logo: "http://127.0.0.1:2368/logo.png",
URL: "http://127.0.0.1:2368/",
RedirectURI: []model.URL{
"https://app.example.com/redirect",
"http://127.0.0.1:2368/redirect",
},
}
store.Store(string(client.ID), client)
result, err := usecase.NewClientUseCase(repository.NewMemoryClientRepository(store)).Discovery(context.TODO(),
client.ID)
require.NoError(err)
assert.Equal(client, result)
}

View File

@ -1,11 +1,16 @@
package model
type Client struct {
ID string
URL string
ID URL
Name string
Logo string
RedirectURI []string
Logo URL
URL URL
RedirectURI []URL
}
func (Client) Bucket() []byte { return []byte("clients") }
func NewClient() *Client {
c := new(Client)
c.RedirectURI = make([]URL, 0)
return c
}