🚚 Moved variuos utils into util package

This commit is contained in:
Maxim Lebedev 2024-05-07 00:42:52 +05:00
parent b0c51e1a25
commit 21a84d9809
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
21 changed files with 297 additions and 58 deletions

View File

@ -16,7 +16,7 @@ import (
"source.toby3d.me/toby3d/auth/internal/domain"
"source.toby3d.me/toby3d/auth/internal/middleware"
"source.toby3d.me/toby3d/auth/internal/profile"
"source.toby3d.me/toby3d/auth/internal/urlutil"
pathutil "source.toby3d.me/toby3d/auth/internal/util/path"
"source.toby3d.me/toby3d/auth/web/template"
"source.toby3d.me/toby3d/auth/web/template/layout"
)
@ -51,7 +51,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
chain := middleware.Chain{
middleware.CSRFWithConfig(middleware.CSRFConfig{
Skipper: func(_ http.ResponseWriter, r *http.Request) bool {
head, _ := urlutil.ShiftPath(r.URL.Path)
head, _ := pathutil.Shift(r.URL.Path)
return head == ""
},
@ -68,7 +68,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}),
middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{
Skipper: func(_ http.ResponseWriter, r *http.Request) bool {
head, _ := urlutil.ShiftPath(r.URL.Path)
head, _ := pathutil.Shift(r.URL.Path)
return r.Method != http.MethodPost || head != "verify" ||
r.PostFormValue("authorize") == "deny"
@ -85,7 +85,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}),
}
head, _ := urlutil.ShiftPath(r.URL.Path)
head, _ := pathutil.Shift(r.URL.Path)
switch r.Method {
default:

View File

@ -107,7 +107,6 @@ func NewAuthAuthorizationRequest() *AuthAuthorizationRequest {
}
}
//nolint:cyclop
func (r *AuthAuthorizationRequest) bind(req *http.Request) error {
indieAuthError := new(domain.Error)
@ -142,7 +141,6 @@ func NewAuthVerifyRequest() *AuthVerifyRequest {
}
}
//nolint:cyclop
func (r *AuthVerifyRequest) bind(req *http.Request) error {
indieAuthError := new(domain.Error)

View File

@ -10,7 +10,7 @@ import (
"source.toby3d.me/toby3d/auth/internal/common"
"source.toby3d.me/toby3d/auth/internal/domain"
"source.toby3d.me/toby3d/auth/internal/token"
"source.toby3d.me/toby3d/auth/internal/urlutil"
pathutil "source.toby3d.me/toby3d/auth/internal/util/path"
"source.toby3d.me/toby3d/auth/web/template"
"source.toby3d.me/toby3d/auth/web/template/layout"
)
@ -48,7 +48,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
var head string
head, r.URL.Path = urlutil.ShiftPath(r.URL.Path)
head, r.URL.Path = pathutil.Shift(r.URL.Path)
switch head {
default:

View File

@ -8,8 +8,8 @@ import (
"net/http"
"net/url"
"slices"
"strings"
"github.com/tomnomnom/linkheader"
"willnorris.com/go/microformats"
"source.toby3d.me/toby3d/auth/internal/client"
@ -19,6 +19,7 @@ import (
"source.toby3d.me/toby3d/auth/internal/domain/grant"
"source.toby3d.me/toby3d/auth/internal/domain/response"
"source.toby3d.me/toby3d/auth/internal/domain/scope"
httputil "source.toby3d.me/toby3d/auth/internal/util/http"
)
type (
@ -106,15 +107,18 @@ func (repo httpClientRepository) Get(ctx context.Context, cid domain.ClientID) (
}
// NOTE(toby3d): fetch redirect uri's from Link header
for _, link := range linkheader.Parse(resp.Header.Get(common.HeaderLink)) {
if link.Rel != common.RelRedirectURI {
links, err := httputil.ParseLink(resp.Header.Get(common.HeaderLink))
if err != nil {
return out, fmt.Errorf("cannot parse Link header value '%s': %w", resp.Header.Get(common.HeaderLink),
err)
}
for _, link := range links {
if !slices.Contains(strings.Fields(link.Params.Get("rel")), common.RelRedirectURI) {
continue
}
var u *url.URL
if u, err = url.Parse(link.URL); err == nil {
out.RedirectURI = append(out.RedirectURI, u)
}
out.RedirectURI = append(out.RedirectURI, link.URL)
}
return out, nil

View File

@ -1,7 +1,5 @@
package challenge_test
//nolint:gosec // support old clients
import (
"crypto/md5"
"crypto/sha1"

View File

@ -7,7 +7,6 @@ import (
"net/url"
"github.com/goccy/go-json"
"github.com/tomnomnom/linkheader"
"willnorris.com/go/microformats"
"source.toby3d.me/toby3d/auth/internal/common"
@ -17,6 +16,7 @@ import (
"source.toby3d.me/toby3d/auth/internal/domain/response"
"source.toby3d.me/toby3d/auth/internal/domain/scope"
"source.toby3d.me/toby3d/auth/internal/metadata"
httputil "source.toby3d.me/toby3d/auth/internal/util/http"
)
type (
@ -64,8 +64,22 @@ func (repo *httpMetadataRepository) Get(_ context.Context, u *url.URL) (*domain.
}
relVals := make(map[string][]string)
for _, link := range linkheader.Parse(resp.Header.Get(common.HeaderLink)) {
populateBuffer(relVals, link.Rel, link.URL)
links, err := httputil.ParseLink(resp.Header.Get(common.HeaderLink))
if err != nil {
return nil, fmt.Errorf("cannot parse Links header value '%s': %w", resp.Header.Get(common.HeaderLink),
err)
}
for _, link := range links {
rels, ok := link.Params["rel"]
if !ok {
continue
}
for _, rel := range rels {
populateBuffer(relVals, rel, link.URL.String())
}
}
if mf2 := microformats.Parse(resp.Body, resp.Request.URL); mf2 != nil {

View File

@ -9,7 +9,7 @@ import (
"source.toby3d.me/toby3d/auth/internal/domain"
repository "source.toby3d.me/toby3d/auth/internal/session/repository/sqlite3"
"source.toby3d.me/toby3d/auth/internal/testing/sqltest"
"source.toby3d.me/toby3d/auth/internal/util/testing/sqltest"
)
//nolint:gochecknoglobals // slices cannot be contants

View File

@ -11,7 +11,7 @@ import (
"source.toby3d.me/toby3d/auth/internal/domain/action"
"source.toby3d.me/toby3d/auth/internal/middleware"
"source.toby3d.me/toby3d/auth/internal/token"
"source.toby3d.me/toby3d/auth/internal/urlutil"
pathutil "source.toby3d.me/toby3d/auth/internal/util/path"
)
type Handler struct {
@ -31,7 +31,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//nolint:exhaustivestruct
middleware.JWTWithConfig(middleware.JWTConfig{
Skipper: func(_ http.ResponseWriter, r *http.Request) bool {
head, _ := urlutil.ShiftPath(r.URL.Path)
head, _ := pathutil.Shift(r.URL.Path)
return head == "token"
},
@ -50,7 +50,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
var head string
head, r.URL.Path = urlutil.ShiftPath(r.URL.Path)
head, r.URL.Path = pathutil.Shift(r.URL.Path)
switch head {
default:

View File

@ -8,8 +8,8 @@ import (
"github.com/DATA-DOG/go-sqlmock"
"source.toby3d.me/toby3d/auth/internal/domain"
"source.toby3d.me/toby3d/auth/internal/testing/sqltest"
repository "source.toby3d.me/toby3d/auth/internal/token/repository/sqlite3"
"source.toby3d.me/toby3d/auth/internal/util/testing/sqltest"
)
//nolint:gochecknoglobals // slices cannot be contants

View File

@ -1,22 +0,0 @@
package urlutil
import (
"path"
"strings"
)
// ShiftPath splits off the first component of p, which will be cleaned of
// relative components before processing. head will never contain a slash and
// tail will always be a rooted path without trailing slash.
//
// See: https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
func ShiftPath(p string) (head, tail string) {
p = path.Clean("/" + p)
i := strings.Index(p[1:], "/") + 1
if i <= 0 {
return p[1:], "/"
}
return p[1:i], p[i:]
}

View File

@ -58,7 +58,7 @@ func (h *Handler) handleFunc(w http.ResponseWriter, r *http.Request) {
if err != nil || tkn == nil {
// WARN(toby3d): If the token is not valid, the endpoint still
// MUST return a 200 Response.
_ = encoder.Encode(err) //nolint:errchkjson
_ = encoder.Encode(err)
w.WriteHeader(http.StatusOK)
@ -66,7 +66,6 @@ func (h *Handler) handleFunc(w http.ResponseWriter, r *http.Request) {
}
if !tkn.Scope.Has(scope.Profile) {
//nolint:errchkjson
_ = encoder.Encode(domain.NewError(
domain.ErrorCodeInsufficientScope,
"token with 'profile' scope is required to view profile data",
@ -78,7 +77,6 @@ func (h *Handler) handleFunc(w http.ResponseWriter, r *http.Request) {
return
}
//nolint:errchkjson
_ = encoder.Encode(NewUserInformationResponse(userInfo, tkn.Scope.Has(scope.Email)))
w.WriteHeader(http.StatusOK)

View File

@ -0,0 +1,57 @@
package http
import (
"fmt"
"net/url"
"strconv"
"strings"
)
type Link struct {
URL *url.URL
Params url.Values
}
// ParseLink parse Link HTTP header value into URL and it's params collection.
func ParseLink(raw string) ([]Link, error) {
links := strings.Split(raw, ",")
result := make([]Link, len(links))
for i := range links {
parts := strings.Split(links[i], ";")
start := strings.Index(parts[0], "<")
end := strings.Index(parts[0], ">")
if start == -1 || end == -1 {
continue
}
var err error
if result[i].URL, err = url.Parse(parts[0][start+1 : end]); err != nil {
return nil, fmt.Errorf("cannot parse Link header URL: %w", err)
}
result[i].Params = make(url.Values)
for _, param := range parts[1:] {
paramParts := strings.SplitN(strings.TrimSpace(param), "=", 2)
if unquotted, err := strconv.Unquote(paramParts[1]); err == nil {
result[i].Params.Add(paramParts[0], unquotted)
} else {
result[i].Params.Add(paramParts[0], paramParts[1])
}
}
}
return result, nil
}
func (l Link) String() string {
result := "<" + l.URL.String() + ">"
if len(l.Params) != 0 {
result += "; " + strings.ReplaceAll(l.Params.Encode(), "&", "; ")
}
return result
}

View File

@ -0,0 +1,47 @@
package http_test
import (
"net/url"
"testing"
"github.com/google/go-cmp/cmp"
httputil "source.toby3d.me/toby3d/auth/internal/util/http"
)
func TestParseLink(t *testing.T) {
t.Parallel()
for name, tc := range map[string]struct {
input string
expect []httputil.Link
}{
"param": {
input: `<https://example.com>; rel="preconnect"`,
expect: []httputil.Link{{
URL: &url.URL{Scheme: "https", Host: "example.com"},
Params: url.Values{"rel": []string{"preconnect"}},
}},
},
"params": {
input: `<https://example.com/%E8%8B%97%E6%9D%A1>; rel="preconnect"; priority=high`,
expect: []httputil.Link{{
URL: &url.URL{Scheme: "https", Host: "example.com", Path: "/苗条"},
Params: url.Values{"rel": []string{"preconnect"}, "priority": []string{"high"}},
}},
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
actual, err := httputil.ParseLink(tc.input)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(actual, tc.expect, cmp.AllowUnexported(url.URL{})); diff != "" {
t.Error(diff)
}
})
}
}

View File

@ -0,0 +1,22 @@
package path
import (
"path"
"strings"
)
// Shift splits off the first component of p, which will be cleaned of relative
// components before processing. head will never contain a slash and tail will
// always be a rooted path without trailing slash.
//
// See: https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
func Shift(p string) (head, tail string) {
p = path.Clean("/" + p)
i := strings.Index(p[1:], "/") + 1
if i <= 0 {
return p[1:], "/"
}
return p[1:i], p[i:]
}

View File

@ -1,12 +1,12 @@
package urlutil_test
package path_test
import (
"testing"
"source.toby3d.me/toby3d/auth/internal/urlutil"
pathutil "source.toby3d.me/toby3d/auth/internal/util/path"
)
func TestShiftPath(t *testing.T) {
func TestShift(t *testing.T) {
t.Parallel()
for in, out := range map[string][2]string{
@ -21,8 +21,7 @@ func TestShiftPath(t *testing.T) {
t.Run(in, func(t *testing.T) {
t.Parallel()
head, path := urlutil.ShiftPath(in)
head, path := pathutil.Shift(in)
if out[0] != head || out[1] != path {
t.Errorf("ShiftPath(%s) = '%s', '%s', want '%s', '%s'", in, head, path, out[0], out[1])
}

View File

@ -0,0 +1,76 @@
package testing
import (
"errors"
"flag"
"io"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
)
//nolint:gochecknoglobals // сompiler global flags cannot be read from within tests
var update = flag.Bool("update", false, "save current tests results as golden files")
// GoldenEqual compares the bytes of the provided output with the contents of
// the golden file for a exact match.
//
// When running go test with the -update flag, the contents of golden-files will
// be overwritten with the provided contents of output, creating the testdata/
// directory if it does not exist.
//
// Check TestGoldenEqual in testing_test.go and testdata/TestGoldenEqual.golden
// for example usage.
//
// See: https://youtu.be/8hQG7QlcLBk?t=749
//
//nolint:cyclop // no need for splitting
func GoldenEqual(tb testing.TB, output io.Reader) {
tb.Helper()
workDir, err := os.Getwd()
if err != nil {
tb.Fatal("cannot get current working directory path:", err)
}
actual, err := io.ReadAll(output)
if err != nil {
tb.Fatal("cannot read provided data:", err)
}
file := filepath.Join(workDir, "testdata", tb.Name()+".golden")
dir := filepath.Dir(file)
//nolint:nestif // errchecks for testdata folder first, then for output
if *update {
_, err = os.Stat(dir)
if err != nil && !errors.Is(err, os.ErrExist) && !errors.Is(err, os.ErrNotExist) {
tb.Fatal("cannot create testdata folder for golden files:", err)
}
if errors.Is(err, os.ErrNotExist) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
tb.Fatal("cannot create testdata folder for golden files:", err)
}
}
if err = os.WriteFile(file, actual, os.ModePerm); err != nil {
tb.Fatal("cannot write data into golden file:", err)
}
tb.Skip("skipped due force updating golden file")
return
}
expect, err := os.ReadFile(file)
if err != nil {
tb.Fatal("cannot read golden file data:", err)
}
if diff := cmp.Diff(actual, expect); diff != "" {
tb.Error(diff)
}
}

View File

@ -0,0 +1,36 @@
package testing_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
testutil "source.toby3d.me/toby3d/auth/internal/util/testing"
)
func TestGoldenEqual(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
w := httptest.NewRecorder()
testHandler := func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Testing</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a testing HTML page of %s website.</p>
</body>
</html>`, r.Host) // NOTE(toby3d): Host must be 'example.com'
}
testHandler(w, req)
// NOTE(toby3d): compare recorded response body against saved golden file
testutil.GoldenEqual(t, w.Result().Body)
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Testing</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a testing HTML page of example.com website.</p>
</body>
</html>

View File

@ -56,8 +56,8 @@ import (
tokenmemoryrepo "source.toby3d.me/toby3d/auth/internal/token/repository/memory"
tokensqlite3repo "source.toby3d.me/toby3d/auth/internal/token/repository/sqlite3"
tokenucase "source.toby3d.me/toby3d/auth/internal/token/usecase"
"source.toby3d.me/toby3d/auth/internal/urlutil"
userhttpdelivery "source.toby3d.me/toby3d/auth/internal/user/delivery/http"
pathutil "source.toby3d.me/toby3d/auth/internal/util/path"
)
type (
@ -325,7 +325,7 @@ func (app *App) Handler() http.Handler {
staticHandler := http.FileServer(http.FS(app.static))
return http.HandlerFunc(middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
head, tail := urlutil.ShiftPath(r.URL.Path)
head, tail := pathutil.Shift(r.URL.Path)
switch head {
default: // NOTE(toby3d): static or 404
@ -339,7 +339,7 @@ func (app *App) Handler() http.Handler {
switch head {
case ".well-known": // NOTE(toby3d): public server config
if head, _ = urlutil.ShiftPath(r.URL.Path); head == "oauth-authorization-server" {
if head, _ = pathutil.Shift(r.URL.Path); head == "oauth-authorization-server" {
metadata.ServeHTTP(w, r)
return