♻️ Refactored token revokation
This commit is contained in:
parent
08c34c3328
commit
451da06bc6
|
@ -0,0 +1,7 @@
|
|||
package common
|
||||
|
||||
const (
|
||||
MIMEApplicationJSON string = "application/json"
|
||||
MIMETextHTML string = "text/html"
|
||||
MIMEApplicationXWWWFormUrlencoded string = "application/x-www-form-urlencoded"
|
||||
)
|
|
@ -1,12 +1,10 @@
|
|||
package model
|
||||
|
||||
type Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
Me string `json:"me"`
|
||||
ClientID string `json:"client_id"`
|
||||
Profile *Profile `json:"profile,omitempty"`
|
||||
AccessToken string
|
||||
ClientID string
|
||||
Me string
|
||||
Profile *Profile
|
||||
Scopes []string
|
||||
Type string
|
||||
}
|
||||
|
||||
func (Token) Bucket() []byte { return []byte("tokens") }
|
||||
|
|
|
@ -4,183 +4,96 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/fasthttp/router"
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/goccy/go-json"
|
||||
http "github.com/valyala/fasthttp"
|
||||
"gitlab.com/toby3d/indieauth/internal/model"
|
||||
"gitlab.com/toby3d/indieauth/internal/token"
|
||||
|
||||
"source.toby3d.me/website/oauth/internal/common"
|
||||
"source.toby3d.me/website/oauth/internal/model"
|
||||
"source.toby3d.me/website/oauth/internal/token"
|
||||
)
|
||||
|
||||
type (
|
||||
Handler struct {
|
||||
RequestHandler struct {
|
||||
useCase token.UseCase
|
||||
}
|
||||
|
||||
ExchangeRequest struct {
|
||||
GrantType string
|
||||
Code string
|
||||
ClientID string
|
||||
RedirectURI string
|
||||
CodeVerifier string
|
||||
}
|
||||
|
||||
RevokeRequest struct {
|
||||
RevocationRequest struct {
|
||||
Action string
|
||||
Token string
|
||||
}
|
||||
|
||||
ExchangeResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Me string `json:"me"`
|
||||
Scope string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
VerificationResponse struct {
|
||||
Me string `json:"me"`
|
||||
ClientID string `json:"client_id"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
RevocationResponse struct{}
|
||||
)
|
||||
|
||||
func NewTokenHandler(useCase token.UseCase) *Handler {
|
||||
return &Handler{
|
||||
const (
|
||||
Action string = "action"
|
||||
ActionRevoke string = "revoke"
|
||||
)
|
||||
|
||||
func NewRequestHandler(useCase token.UseCase) *RequestHandler {
|
||||
return &RequestHandler{
|
||||
useCase: useCase,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Register(r *router.Router) {
|
||||
r.GET("/token", h.Verification)
|
||||
func (h *RequestHandler) Register(r *router.Router) {
|
||||
r.POST("/token", h.Update)
|
||||
}
|
||||
|
||||
func (h *Handler) Verification(ctx *http.RequestCtx) {
|
||||
func (h *RequestHandler) Update(ctx *http.RequestCtx) {
|
||||
ctx.SetStatusCode(http.StatusBadRequest)
|
||||
|
||||
switch string(ctx.FormValue(Action)) {
|
||||
case ActionRevoke:
|
||||
h.Revocation(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RequestHandler) Revocation(ctx *http.RequestCtx) {
|
||||
ctx.SetContentType(common.MIMEApplicationJSON)
|
||||
ctx.SetStatusCode(http.StatusOK)
|
||||
|
||||
encoder := json.NewEncoder(ctx)
|
||||
|
||||
token, err := h.useCase.Verify(ctx,
|
||||
strings.TrimPrefix(string(ctx.Request.Header.Peek(http.HeaderAuthorization)), "Bearer "),
|
||||
)
|
||||
if err != nil {
|
||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetContentType("application/json")
|
||||
_ = encoder.Encode(&VerificationResponse{
|
||||
Me: token.Me,
|
||||
ClientID: token.ClientID,
|
||||
Scope: token.Scope,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) Update(ctx *http.RequestCtx) {
|
||||
if ctx.PostArgs().Has("action") {
|
||||
h.Revoke(ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.Exchange(ctx)
|
||||
}
|
||||
|
||||
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
|
||||
if r.GrantType = string(ctx.PostArgs().Peek("grant_type")); r.GrantType != "authorization_code" {
|
||||
return model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'grant_type' must be 'authorization_code'",
|
||||
}
|
||||
}
|
||||
|
||||
if r.Code = string(ctx.PostArgs().Peek("code")); r.Code == "" {
|
||||
return model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'code' query is required",
|
||||
}
|
||||
}
|
||||
|
||||
if r.ClientID = string(ctx.PostArgs().Peek("client_id")); r.ClientID == "" {
|
||||
return model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'client_id' query is required",
|
||||
}
|
||||
}
|
||||
|
||||
if r.RedirectURI = string(ctx.PostArgs().Peek("redirect_uri")); r.RedirectURI == "" {
|
||||
return model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'redirect_uri' query is required",
|
||||
}
|
||||
}
|
||||
|
||||
r.CodeVerifier = string(ctx.PostArgs().Peek("code_verifier"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) Exchange(ctx *http.RequestCtx) {
|
||||
encoder := json.NewEncoder(ctx)
|
||||
req := new(ExchangeRequest)
|
||||
|
||||
req := new(RevocationRequest)
|
||||
if err := req.bind(ctx); err != nil {
|
||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
||||
ctx.SetStatusCode(http.StatusBadRequest)
|
||||
encoder.Encode(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.useCase.Exchange(ctx, &model.ExchangeRequest{
|
||||
ClientID: req.ClientID,
|
||||
Code: req.Code,
|
||||
CodeVerifier: req.CodeVerifier,
|
||||
RedirectURI: req.RedirectURI,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(err.Error(), http.StatusInternalServerError)
|
||||
if err := h.useCase.Revoke(ctx, req.Token); err != nil {
|
||||
ctx.SetStatusCode(http.StatusBadRequest)
|
||||
encoder.Encode(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
ctx.Error(model.ErrUnauthorizedClient.Error(), http.StatusUnauthorized)
|
||||
if err := encoder.Encode(RevocationResponse{}); err != nil {
|
||||
ctx.SetStatusCode(http.StatusInternalServerError)
|
||||
encoder.Encode(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetContentType("application/json")
|
||||
_ = encoder.Encode(&ExchangeResponse{
|
||||
AccessToken: token.AccessToken,
|
||||
Me: token.Me,
|
||||
Scope: token.Scope,
|
||||
TokenType: token.TokenType,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RevokeRequest) bind(ctx *http.RequestCtx) error {
|
||||
if r.Action = string(ctx.PostArgs().Peek("action")); r.Action != "revoke" {
|
||||
func (r *RevocationRequest) bind(ctx *http.RequestCtx) error {
|
||||
if r.Action = string(ctx.FormValue(Action)); !strings.EqualFold(r.Action, ActionRevoke) {
|
||||
return model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'action' must be 'revoke'",
|
||||
Code: model.ErrInvalidRequest,
|
||||
Description: "request MUST contain 'action' key with value 'revoke'",
|
||||
URI: "https://indieauth.spec.indieweb.org/#token-revocation-request",
|
||||
}
|
||||
}
|
||||
|
||||
if r.Token = string(ctx.PostArgs().Peek("token")); r.Token == "" {
|
||||
if r.Token = string(ctx.FormValue("token")); r.Token == "" {
|
||||
return model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'token' query is required",
|
||||
Code: model.ErrInvalidRequest,
|
||||
Description: "request MUST contain the 'token' key with the valid access token as its value",
|
||||
URI: "https://indieauth.spec.indieweb.org/#token-revocation-request",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) Revoke(ctx *http.RequestCtx) {
|
||||
req := new(RevokeRequest)
|
||||
if err := req.bind(ctx); err != nil {
|
||||
ctx.Error(err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.useCase.Revoke(ctx, string(ctx.PostArgs().Peek("token")))
|
||||
|
||||
ctx.SuccessString("application/json", "{}")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package http_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/brianvoe/gofakeit"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
http "github.com/valyala/fasthttp"
|
||||
|
||||
"source.toby3d.me/website/oauth/internal/common"
|
||||
"source.toby3d.me/website/oauth/internal/model"
|
||||
delivery "source.toby3d.me/website/oauth/internal/token/delivery/http"
|
||||
repository "source.toby3d.me/website/oauth/internal/token/repository/memory"
|
||||
"source.toby3d.me/website/oauth/internal/token/usecase"
|
||||
"source.toby3d.me/website/oauth/internal/util"
|
||||
)
|
||||
|
||||
func TestRevocation(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
repo := repository.NewMemoryTokenRepository()
|
||||
accessToken := gofakeit.Password(true, true, true, true, false, 32)
|
||||
|
||||
require.NoError(repo.Create(context.TODO(), &model.Token{
|
||||
AccessToken: accessToken,
|
||||
Type: "Bearer",
|
||||
ClientID: "https://app.example.com/",
|
||||
Me: "https://user.example.net/",
|
||||
Scopes: []string{"create", "update", "delete"},
|
||||
}))
|
||||
|
||||
req := http.AcquireRequest()
|
||||
defer http.ReleaseRequest(req)
|
||||
|
||||
req.Header.SetMethod(http.MethodPost)
|
||||
req.SetRequestURI("http://localhost/token")
|
||||
req.Header.SetContentType(common.MIMEApplicationXWWWFormUrlencoded)
|
||||
req.Header.Set(http.HeaderAccept, common.MIMEApplicationJSON)
|
||||
req.PostArgs().Set("action", "revoke")
|
||||
req.PostArgs().Set("token", accessToken)
|
||||
|
||||
resp := http.AcquireResponse()
|
||||
defer http.ReleaseResponse(resp)
|
||||
|
||||
require.NoError(util.Serve(delivery.NewRequestHandler(usecase.NewTokenUseCase(repo)).Revocation, req, resp))
|
||||
assert.Equal(http.StatusOK, resp.StatusCode())
|
||||
assert.Equal(`{}`, strings.TrimSpace(string(resp.Body())))
|
||||
|
||||
token, err := repo.Get(context.TODO(), accessToken)
|
||||
require.NoError(err)
|
||||
assert.Nil(token)
|
||||
}
|
|
@ -3,7 +3,7 @@ package token
|
|||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/toby3d/indieauth/internal/model"
|
||||
"source.toby3d.me/website/oauth/internal/model"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
json "github.com/goccy/go-json"
|
||||
"gitlab.com/toby3d/indieauth/internal/model"
|
||||
"gitlab.com/toby3d/indieauth/internal/token"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type boltTokenRepository struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) {
|
||||
if err := db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists(model.Token{}.Bucket())
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &boltTokenRepository{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *boltTokenRepository) Create(ctx context.Context, token *model.Token) error {
|
||||
jsonToken, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repo.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(model.Token{}.Bucket()).Put([]byte(token.AccessToken), jsonToken)
|
||||
})
|
||||
}
|
||||
|
||||
func (repo *boltTokenRepository) Get(ctx context.Context, token string) (*model.Token, error) {
|
||||
t := new(model.Token)
|
||||
|
||||
if err := repo.db.View(func(tx *bolt.Tx) error {
|
||||
return json.Unmarshal(tx.Bucket(model.Token{}.Bucket()).Get([]byte(token)), t)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (repo *boltTokenRepository) Delete(ctx context.Context, token string) error {
|
||||
return repo.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(model.Token{}.Bucket()).Delete([]byte(token))
|
||||
})
|
||||
}
|
|
@ -4,37 +4,72 @@ import (
|
|||
"context"
|
||||
"sync"
|
||||
|
||||
"gitlab.com/toby3d/indieauth/internal/model"
|
||||
"gitlab.com/toby3d/indieauth/internal/token"
|
||||
"source.toby3d.me/website/oauth/internal/model"
|
||||
"source.toby3d.me/website/oauth/internal/token"
|
||||
)
|
||||
|
||||
type memoryTokenRepository struct {
|
||||
tokens *sync.Map
|
||||
mutex *sync.RWMutex
|
||||
tokens []*model.Token
|
||||
}
|
||||
|
||||
func NewMemoryTokenRepository() token.Repository {
|
||||
return &memoryTokenRepository{
|
||||
tokens: new(sync.Map),
|
||||
mutex: new(sync.RWMutex),
|
||||
tokens: make([]*model.Token, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *memoryTokenRepository) Create(ctx context.Context, token *model.Token) error {
|
||||
repo.tokens.Store(token.AccessToken, token)
|
||||
repo.mutex.Lock()
|
||||
|
||||
return nil
|
||||
}
|
||||
repo.tokens = append(repo.tokens, token)
|
||||
|
||||
func (repo *memoryTokenRepository) Delete(ctx context.Context, token string) error {
|
||||
repo.tokens.Delete(token)
|
||||
repo.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *memoryTokenRepository) Get(ctx context.Context, token string) (*model.Token, error) {
|
||||
t, ok := repo.tokens.Load(token)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
repo.mutex.RLock()
|
||||
defer repo.mutex.RUnlock()
|
||||
|
||||
for i := range repo.tokens {
|
||||
if repo.tokens[i].AccessToken != token {
|
||||
continue
|
||||
}
|
||||
|
||||
return repo.tokens[i], nil
|
||||
}
|
||||
|
||||
return t.(*model.Token), nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (repo *memoryTokenRepository) Delete(ctx context.Context, token string) error {
|
||||
repo.mutex.RLock()
|
||||
|
||||
for i := range repo.tokens {
|
||||
if repo.tokens[i].AccessToken != token {
|
||||
continue
|
||||
}
|
||||
|
||||
repo.mutex.RUnlock()
|
||||
repo.mutex.Lock()
|
||||
|
||||
if i < len(repo.tokens)-1 {
|
||||
copy(repo.tokens[i:], repo.tokens[i+1:])
|
||||
}
|
||||
|
||||
repo.tokens[len(repo.tokens)-1] = nil
|
||||
repo.tokens = repo.tokens[:len(repo.tokens)-1]
|
||||
|
||||
repo.mutex.Unlock()
|
||||
repo.mutex.RLock()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
repo.mutex.RUnlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,12 +2,8 @@ package token
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/toby3d/indieauth/internal/model"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
Exchange(ctx context.Context, req *model.ExchangeRequest) (*model.Token, error)
|
||||
Verify(ctx context.Context, token string) (*model.Token, error)
|
||||
Revoke(ctx context.Context, token string) error
|
||||
}
|
||||
|
|
|
@ -2,93 +2,20 @@ package usecase
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitlab.com/toby3d/indieauth/internal/auth"
|
||||
"gitlab.com/toby3d/indieauth/internal/model"
|
||||
"gitlab.com/toby3d/indieauth/internal/pkce"
|
||||
"gitlab.com/toby3d/indieauth/internal/random"
|
||||
"gitlab.com/toby3d/indieauth/internal/token"
|
||||
"source.toby3d.me/website/oauth/internal/token"
|
||||
)
|
||||
|
||||
type tokenUseCase struct {
|
||||
authRepo auth.Repository
|
||||
tokenRepo token.Repository
|
||||
tokens token.Repository
|
||||
}
|
||||
|
||||
func NewTokenUseCase(authRepo auth.Repository, tokenRepo token.Repository) token.UseCase {
|
||||
func NewTokenUseCase(tokens token.Repository) token.UseCase {
|
||||
return &tokenUseCase{
|
||||
authRepo: authRepo,
|
||||
tokenRepo: tokenRepo,
|
||||
tokens: tokens,
|
||||
}
|
||||
}
|
||||
|
||||
func (useCase *tokenUseCase) Exchange(ctx context.Context, req *model.ExchangeRequest) (*model.Token, error) {
|
||||
login, err := useCase.authRepo.Get(ctx, req.Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if login == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_ = useCase.authRepo.Delete(ctx, login.Code)
|
||||
|
||||
if time.Now().UTC().After(time.Unix(login.CreatedAt, 0).Add(10 * time.Minute)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if login.ClientID != req.ClientID {
|
||||
return nil, model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'client_id' value must be identical to the value at the start of the authorization flow",
|
||||
}
|
||||
}
|
||||
|
||||
if login.RedirectURI != req.RedirectURI {
|
||||
return nil, model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "'client_id' value must be identical to the value at the start of the authorization flow",
|
||||
}
|
||||
}
|
||||
|
||||
if login.CodeChallenge != "" {
|
||||
code, err := pkce.New(login.CodeChallengeMethod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code.Verifier = req.CodeVerifier
|
||||
|
||||
code.Generate()
|
||||
|
||||
if login.CodeChallenge != code.Challenge {
|
||||
return nil, model.Error{
|
||||
Code: model.ErrInvalidRequest.Code,
|
||||
Description: "PKCE validation failed, please go through the authorization flow again",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token := new(model.Token)
|
||||
token.AccessToken = random.New().String(64)
|
||||
token.ClientID = login.ClientID
|
||||
token.Me = login.Me
|
||||
token.Scope = login.Scope
|
||||
token.TokenType = "Bearer"
|
||||
|
||||
if err := useCase.tokenRepo.Create(ctx, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (useCase *tokenUseCase) Verify(ctx context.Context, token string) (*model.Token, error) {
|
||||
return useCase.tokenRepo.Get(ctx, token)
|
||||
}
|
||||
|
||||
func (useCase *tokenUseCase) Revoke(ctx context.Context, token string) error {
|
||||
return useCase.tokenRepo.Delete(ctx, token)
|
||||
return useCase.tokens.Delete(ctx, token)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package usecase_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/brianvoe/gofakeit"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"source.toby3d.me/website/oauth/internal/model"
|
||||
repository "source.toby3d.me/website/oauth/internal/token/repository/memory"
|
||||
"source.toby3d.me/website/oauth/internal/token/usecase"
|
||||
)
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
repo := repository.NewMemoryTokenRepository()
|
||||
ucase := usecase.NewTokenUseCase(repo)
|
||||
accessToken := gofakeit.Password(true, true, true, true, false, 32)
|
||||
|
||||
require.NoError(repo.Create(context.TODO(), &model.Token{
|
||||
AccessToken: accessToken,
|
||||
Type: "Bearer",
|
||||
ClientID: "https://app.example.com/",
|
||||
Me: "https://user.example.net/",
|
||||
Scopes: []string{"create", "update", "delete"},
|
||||
}))
|
||||
|
||||
token, err := repo.Get(context.TODO(), accessToken)
|
||||
require.NoError(err)
|
||||
assert.NotNil(token)
|
||||
|
||||
require.NoError(ucase.Revoke(context.TODO(), token.AccessToken))
|
||||
|
||||
token, err = repo.Get(context.TODO(), token.AccessToken)
|
||||
require.NoError(err)
|
||||
assert.Nil(token)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
http "github.com/valyala/fasthttp"
|
||||
httputil "github.com/valyala/fasthttp/fasthttputil"
|
||||
)
|
||||
|
||||
func Serve(handler http.RequestHandler, req *http.Request, res *http.Response) error {
|
||||
ln := httputil.NewInmemoryListener()
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
if err := http.Serve(ln, handler); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
client := http.Client{
|
||||
Dial: func(addr string) (net.Conn, error) {
|
||||
return ln.Dial()
|
||||
},
|
||||
}
|
||||
|
||||
return client.Do(req, res)
|
||||
}
|
Loading…
Reference in New Issue