~evanj/cms

2f529fb0160332f973ee8423317edeb378742849 — Evan M Jones 4 months ago cea564e
WIP(c/space): Adding testing for space controller. Happy path is
covered.
M go.mod => go.mod +1 -0
@@ 4,6 4,7 @@ go 1.14

require (
	git.sr.ht/~evanj/embed v0.0.0-20200702023953-8a902e4e7e94
	git.sr.ht/~evanj/errors v0.0.0-20191127050903-ab51d2fcd90b
	git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997
	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
	github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b

M go.sum => go.sum +2 -0
@@ 1,5 1,7 @@
git.sr.ht/~evanj/embed v0.0.0-20200702023953-8a902e4e7e94 h1:uvmXsjtUvWzL3n3ghDejB3r6HV1Bl/EaDPmqs/F3cyA=
git.sr.ht/~evanj/embed v0.0.0-20200702023953-8a902e4e7e94/go.mod h1:o3sadZPJeN9scjfsbpsABLF2nNr6qUqMMkGLfCCsILc=
git.sr.ht/~evanj/errors v0.0.0-20191127050903-ab51d2fcd90b h1:9kLEC89SLTyODQVqNo8Eosb9kmaU+I4B42JG9HsaE3Y=
git.sr.ht/~evanj/errors v0.0.0-20191127050903-ab51d2fcd90b/go.mod h1:4wGegxiownNxYiC0wqz4nSJX2EkAiYi+KPHl8KgVfdE=
git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997 h1:fdAj8fR4mpS/OAvetjc9Xn96ZYrnJcQpbn/AUG0jihs=
git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997/go.mod h1:40791KVgThT97CT6mTsF4NUNPeX2BAVlsuH1RiiSrAs=
github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg=

M internal/c/c.go => internal/c/c.go +13 -1
@@ 90,12 90,24 @@ func (c *Controller) String(w http.ResponseWriter, r *http.Request, str string) 
	fmt.Fprintf(w, str)
}

func (c *Controller) Error(w http.ResponseWriter, r *http.Request, code int, str string) {
// TODO: Refactor the usage of these three functions and remove Error2.

func (c *Controller) ErrorString(w http.ResponseWriter, r *http.Request, code int, str string) {
	w.Header().Add("Content-Type", "text/plain")
	w.WriteHeader(code)
	fmt.Fprintf(w, str)
}

func (c *Controller) Error(w http.ResponseWriter, r *http.Request, code int, str string) {
	c.ErrorString(w, r, code, str)
}

func (c *Controller) Error2(w http.ResponseWriter, r *http.Request, code int, err error) {
	w.Header().Add("Content-Type", "text/plain")
	w.WriteHeader(code)
	fmt.Fprintf(w, err.Error())
}

// TODO: You know why this is bad, change it.
func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template.Template, data interface{}) {
	// Check JSON wanted.

A internal/c/space/mock/mock_space.go => internal/c/space/mock/mock_space.go +172 -0
@@ 0,0 1,172 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: space.go

// Package mock_space is a generated GoMock package.
package mock_space

import (
	reflect "reflect"

	contenttype "git.sr.ht/~evanj/cms/internal/m/contenttype"
	hook "git.sr.ht/~evanj/cms/internal/m/hook"
	spaceT "git.sr.ht/~evanj/cms/internal/m/space"
	user "git.sr.ht/~evanj/cms/internal/m/user"
	gomock "github.com/golang/mock/gomock"
)

// Mockdber is a mock of dber interface
type Mockdber struct {
	ctrl     *gomock.Controller
	recorder *MockdberMockRecorder
}

// MockdberMockRecorder is the mock recorder for Mockdber
type MockdberMockRecorder struct {
	mock *Mockdber
}

// NewMockdber creates a new mock instance
func NewMockdber(ctrl *gomock.Controller) *Mockdber {
	mock := &Mockdber{ctrl: ctrl}
	mock.recorder = &MockdberMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *Mockdber) EXPECT() *MockdberMockRecorder {
	return m.recorder
}

// UserGet mocks base method
func (m *Mockdber) UserGet(username, password string) (user.User, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "UserGet", username, password)
	ret0, _ := ret[0].(user.User)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// UserGet indicates an expected call of UserGet
func (mr *MockdberMockRecorder) UserGet(username, password interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserGet", reflect.TypeOf((*Mockdber)(nil).UserGet), username, password)
}

// UserGetFromToken mocks base method
func (m *Mockdber) UserGetFromToken(token string) (user.User, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "UserGetFromToken", token)
	ret0, _ := ret[0].(user.User)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// UserGetFromToken indicates an expected call of UserGetFromToken
func (mr *MockdberMockRecorder) UserGetFromToken(token interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserGetFromToken", reflect.TypeOf((*Mockdber)(nil).UserGetFromToken), token)
}

// SpaceNew mocks base method
func (m *Mockdber) SpaceNew(user user.User, name, desc string) (spaceT.Space, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "SpaceNew", user, name, desc)
	ret0, _ := ret[0].(spaceT.Space)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// SpaceNew indicates an expected call of SpaceNew
func (mr *MockdberMockRecorder) SpaceNew(user, name, desc interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpaceNew", reflect.TypeOf((*Mockdber)(nil).SpaceNew), user, name, desc)
}

// SpaceGet mocks base method
func (m *Mockdber) SpaceGet(user user.User, spaceID string) (spaceT.Space, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "SpaceGet", user, spaceID)
	ret0, _ := ret[0].(spaceT.Space)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// SpaceGet indicates an expected call of SpaceGet
func (mr *MockdberMockRecorder) SpaceGet(user, spaceID interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpaceGet", reflect.TypeOf((*Mockdber)(nil).SpaceGet), user, spaceID)
}

// SpaceUpdate mocks base method
func (m *Mockdber) SpaceUpdate(user user.User, space spaceT.Space, name, desc string) (spaceT.Space, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "SpaceUpdate", user, space, name, desc)
	ret0, _ := ret[0].(spaceT.Space)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// SpaceUpdate indicates an expected call of SpaceUpdate
func (mr *MockdberMockRecorder) SpaceUpdate(user, space, name, desc interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpaceUpdate", reflect.TypeOf((*Mockdber)(nil).SpaceUpdate), user, space, name, desc)
}

// SpaceCopy mocks base method
func (m *Mockdber) SpaceCopy(user user.User, space spaceT.Space, name, desc string) (spaceT.Space, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "SpaceCopy", user, space, name, desc)
	ret0, _ := ret[0].(spaceT.Space)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// SpaceCopy indicates an expected call of SpaceCopy
func (mr *MockdberMockRecorder) SpaceCopy(user, space, name, desc interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpaceCopy", reflect.TypeOf((*Mockdber)(nil).SpaceCopy), user, space, name, desc)
}

// SpaceDelete mocks base method
func (m *Mockdber) SpaceDelete(user user.User, space spaceT.Space) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "SpaceDelete", user, space)
	ret0, _ := ret[0].(error)
	return ret0
}

// SpaceDelete indicates an expected call of SpaceDelete
func (mr *MockdberMockRecorder) SpaceDelete(user, space interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpaceDelete", reflect.TypeOf((*Mockdber)(nil).SpaceDelete), user, space)
}

// ContentTypesPerSpace mocks base method
func (m *Mockdber) ContentTypesPerSpace(space spaceT.Space, before int) (contenttype.ContentTypeList, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ContentTypesPerSpace", space, before)
	ret0, _ := ret[0].(contenttype.ContentTypeList)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ContentTypesPerSpace indicates an expected call of ContentTypesPerSpace
func (mr *MockdberMockRecorder) ContentTypesPerSpace(space, before interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContentTypesPerSpace", reflect.TypeOf((*Mockdber)(nil).ContentTypesPerSpace), space, before)
}

// HooksPerSpace mocks base method
func (m *Mockdber) HooksPerSpace(space spaceT.Space, before int) (hook.HookList, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "HooksPerSpace", space, before)
	ret0, _ := ret[0].(hook.HookList)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// HooksPerSpace indicates an expected call of HooksPerSpace
func (mr *MockdberMockRecorder) HooksPerSpace(space, before interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HooksPerSpace", reflect.TypeOf((*Mockdber)(nil).HooksPerSpace), space, before)
}

M internal/c/space/space.go => internal/c/space/space.go +9 -6
@@ 7,6 7,8 @@ import (
	"strconv"
	"strings"

	"git.sr.ht/~evanj/errors"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/hook"


@@ 16,7 18,8 @@ import (
)

var (
	spaceHTML = tmpl.MustParse("html/space.html")
	spaceHTML  = tmpl.MustParse("html/space.html")
	ErrNoLogin = errors.New("must be logged in")
)

type Space struct {


@@ 54,7 57,7 @@ func (s *Space) serve(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to view space")
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(ErrNoLogin, "can't get space"))
		return
	}



@@ 93,7 96,7 @@ func (s *Space) create(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to create space")
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(ErrNoLogin, "can't create space"))
		return
	}



@@ 116,7 119,7 @@ func (s *Space) copy(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to copy space")
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(ErrNoLogin, "can't copy space"))
		return
	}



@@ 145,7 148,7 @@ func (s *Space) update(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to delete space")
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(ErrNoLogin, "can't update space"))
		return
	}



@@ 172,7 175,7 @@ func (s *Space) delete(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, "must be logged in to delete space")
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(ErrNoLogin, "can't delete space"))
		return
	}


M internal/c/space/space_test.go => internal/c/space/space_test.go +212 -0
@@ 1,1 1,213 @@
package space_test

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"

	"git.sr.ht/~evanj/cms/internal/c/space"
	mock_space "git.sr.ht/~evanj/cms/internal/c/space/mock"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/hook"
	"github.com/bmizerany/assert"
	"github.com/golang/mock/gomock"
	"github.com/google/uuid"
)

func emptyreq(ts *httptest.Server, method string) (*http.Response, error) {
	req, err := newrequest(method, ts.URL, nil)
	if err != nil {
		return nil, err
	}
	return http.DefaultClient.Do(req)
}

type FakeUser struct{ u, p string }

func fakeuser(u, p string) FakeUser { return FakeUser{u, p} }
func (u FakeUser) ID() string       { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
func (u FakeUser) Name() string     { return u.u }
func (u FakeUser) Token() string    { return fmt.Sprintf("token-%s-%s", u.u, u.p) }

type FakeSpace struct{ n, d string }

func fakespace(n, d string) FakeSpace { return FakeSpace{n, d} }
func (u FakeSpace) ID() string        { return fmt.Sprintf("id-%s-%s", u.n, u.d) }
func (u FakeSpace) Name() string      { return u.n }
func (u FakeSpace) Desc() string      { return u.d }

type FakeContentTypeList struct{}

func fakecontenttypelist() FakeContentTypeList                    { return FakeContentTypeList{} }
func (_ FakeContentTypeList) List() (r []contenttype.ContentType) { return r }
func (_ FakeContentTypeList) More() bool                          { return false }
func (_ FakeContentTypeList) Before() int                         { return 0 }

type FakeHookList struct{}

func fakehooklist() FakeHookList             { return FakeHookList{} }
func (_ FakeHookList) List() (r []hook.Hook) { return r }
func (_ FakeHookList) More() bool            { return false }
func (_ FakeHookList) Before() int           { return 0 }

func newrequest(method, url string, bod url.Values) (*http.Request, error) {
	// Fake the method. This is used for two reasons.
	// 1. On the frontend we use a hidden method field in form to mock/use REST.
	// 		It ends up making cURL use much more ergonomic.
	// 2. When testing, net/http doesn't read full body of DELETE/PUT. Incredibly
	// 		annoying.
	if bod != nil {
		bod.Add("method", method)
	}
	return http.NewRequest("POST", url, strings.NewReader(bod.Encode()))
}

func TestNoUser(t *testing.T) {
	t.Parallel()

	var (
		ctrl    = gomock.NewController(t)
		db      = mock_space.NewMockdber(ctrl)
		l       *log.Logger
		s       = space.New(l, db)
		ts      = httptest.NewServer(s)
		methods = []string{"GET", "POST", "PUT", "PATCH", "DELETE"}
	)

	for _, method := range methods {
		res, err := emptyreq(ts, method)
		assert.Equal(t, err, nil)

		bytes, err := ioutil.ReadAll(res.Body)
		assert.Equal(t, err, nil)
		assert.Equal(t, true, strings.Contains(string(bytes), space.ErrNoLogin.Error()))
	}
}

func TestHappyPathAll(t *testing.T) {
	t.Parallel()

	var (
		uname = "test"
		upass = "test"
		uItem = fakeuser(uname, upass)

		sname = uuid.New().String()
		sdesc = uuid.New().String()
		sItem = fakespace(sname, sdesc)

		snameUpdate = uuid.New().String()
		sdescUpdate = uuid.New().String()
		sItemUpdate = fakespace(snameUpdate, sdescUpdate)

		snameCopy = uuid.New().String()
		sdescCopy = uuid.New().String()
		sItemCopy = fakespace(snameCopy, sdescCopy)

		contentTypeList = fakecontenttypelist()
		hookList        = fakehooklist()

		ctrl = gomock.NewController(t)
		db   = mock_space.NewMockdber(ctrl)
		l    = log.New(bytes.NewBufferString(os.DevNull), "", 0)
		s    = space.New(l, db)
		ts   = httptest.NewServer(s)
	)

	// Create
	{
		db.EXPECT().UserGet(uname, upass).Return(uItem, nil).AnyTimes()
		db.EXPECT().SpaceNew(uItem, sname, sdesc).Return(sItem, nil).AnyTimes()

		req, _ := newrequest("POST", ts.URL, url.Values{"name": []string{sname}, "desc": []string{sdesc}})
		req.SetBasicAuth(uname, upass)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("Accept", "text/html")

		res, _ := http.DefaultClient.Do(req)
		assert.Equal(t, res.StatusCode, http.StatusTemporaryRedirect)
	}

	// Get
	{
		db.EXPECT().UserGet(uname, upass).Return(uItem, nil).AnyTimes()
		db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
		db.EXPECT().ContentTypesPerSpace(sItem, 0).Return(contentTypeList, nil).AnyTimes()
		db.EXPECT().HooksPerSpace(sItem, 0).Return(hookList, nil).AnyTimes()

		req, _ := newrequest("GET", ts.URL, url.Values{"space": []string{sItem.ID()}})
		req.SetBasicAuth(uname, upass)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("Accept", "text/html")

		res, _ := http.DefaultClient.Do(req)
		assert.Equal(t, res.StatusCode, http.StatusOK)
	}

	// Update
	{
		db.EXPECT().UserGet(uname, upass).Return(uItem, nil).AnyTimes()
		db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
		db.EXPECT().SpaceUpdate(uItem, sItem, snameUpdate, sdescUpdate).Return(sItemUpdate, nil).AnyTimes()

		req, _ := newrequest("PATCH", ts.URL,
			url.Values{
				"space": []string{sItem.ID()},
				"name":  []string{snameUpdate},
				"desc":  []string{sdescUpdate}})

		req.SetBasicAuth(uname, upass)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("Accept", "text/html")

		res, _ := http.DefaultClient.Do(req)
		assert.Equal(t, res.StatusCode, http.StatusTemporaryRedirect)
	}

	// Copy
	{

		db.EXPECT().UserGet(uname, upass).Return(uItem, nil).AnyTimes()
		db.EXPECT().SpaceGet(uItem, sItemUpdate.ID()).Return(sItemUpdate, nil).AnyTimes()
		db.EXPECT().SpaceCopy(uItem, sItemUpdate, snameCopy, sdescCopy).Return(sItemCopy, nil).AnyTimes()

		req, _ := newrequest("PUT", ts.URL,
			url.Values{
				"space": []string{sItemUpdate.ID()},
				"name":  []string{snameCopy},
				"desc":  []string{sdescCopy}})
		req.SetBasicAuth(uname, upass)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("Accept", "text/html")

		res, _ := http.DefaultClient.Do(req)
		assert.Equal(t, res.StatusCode, http.StatusTemporaryRedirect)
	}

	// Delete
	{
		db.EXPECT().UserGet(uname, upass).Return(uItem, nil).AnyTimes()
		db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
		db.EXPECT().SpaceDelete(uItem, sItem).Return(nil).AnyTimes()

		req, _ := newrequest("DELETE", ts.URL,
			url.Values{
				"space":  []string{sItem.ID()},
				"method": []string{"DELETE"},
			})

		req.SetBasicAuth(uname, upass)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("Accept", "text/html")

		res, _ := http.DefaultClient.Do(req)
		assert.Equal(t, res.StatusCode, http.StatusTemporaryRedirect)
	}
}

A vendor/git.sr.ht/~evanj/errors/.gitignore => vendor/git.sr.ht/~evanj/errors/.gitignore +24 -0
@@ 0,0 1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof

A vendor/git.sr.ht/~evanj/errors/.travis.yml => vendor/git.sr.ht/~evanj/errors/.travis.yml +10 -0
@@ 0,0 1,10 @@
language: go
go_import_path: github.com/pkg/errors
go:
  - 1.11.x
  - 1.12.x
  - 1.13.x
  - tip

script:
  - make check

A vendor/git.sr.ht/~evanj/errors/LICENSE => vendor/git.sr.ht/~evanj/errors/LICENSE +23 -0
@@ 0,0 1,23 @@
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

A vendor/git.sr.ht/~evanj/errors/Makefile => vendor/git.sr.ht/~evanj/errors/Makefile +44 -0
@@ 0,0 1,44 @@
PKGS := github.com/pkg/errors
SRCDIRS := $(shell go list -f '{{.Dir}}' $(PKGS))
GO := go

check: test vet gofmt misspell unconvert staticcheck ineffassign unparam

test: 
	$(GO) test $(PKGS)

vet: | test
	$(GO) vet $(PKGS)

staticcheck:
	$(GO) get honnef.co/go/tools/cmd/staticcheck
	staticcheck -checks all $(PKGS)

misspell:
	$(GO) get github.com/client9/misspell/cmd/misspell
	misspell \
		-locale GB \
		-error \
		*.md *.go

unconvert:
	$(GO) get github.com/mdempsky/unconvert
	unconvert -v $(PKGS)

ineffassign:
	$(GO) get github.com/gordonklaus/ineffassign
	find $(SRCDIRS) -name '*.go' | xargs ineffassign

pedantic: check errcheck

unparam:
	$(GO) get mvdan.cc/unparam
	unparam ./...

errcheck:
	$(GO) get github.com/kisielk/errcheck
	errcheck $(PKGS)

gofmt:  
	@echo Checking code is gofmted
	@test -z "$(shell gofmt -s -l -d -e $(SRCDIRS) | tee /dev/stderr)"

A vendor/git.sr.ht/~evanj/errors/README.md => vendor/git.sr.ht/~evanj/errors/README.md +59 -0
@@ 0,0 1,59 @@
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge)

Package errors provides simple error handling primitives.

`go get github.com/pkg/errors`

The traditional error handling idiom in Go is roughly akin to
```go
if err != nil {
        return err
}
```
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.

## Adding context to an error

The errors.Wrap function returns a new error that adds context to the original error. For example
```go
_, err := ioutil.ReadAll(r)
if err != nil {
        return errors.Wrap(err, "read failed")
}
```
## Retrieving the cause of an error

Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
```go
type causer interface {
        Cause() error
}
```
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
```go
switch err := errors.Cause(err).(type) {
case *MyError:
        // handle specifically
default:
        // unknown error
}
```

[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).

## Roadmap

With the upcoming [Go2 error proposals](https://go.googlesource.com/proposal/+/master/design/go2draft.md) this package is moving into maintenance mode. The roadmap for a 1.0 release is as follows:

- 0.9. Remove pre Go 1.9 and Go 1.10 support, address outstanding pull requests (if possible)
- 1.0. Final release.

## Contributing

Because of the Go2 errors changes, this package is not accepting proposals for new functionality. With that said, we welcome pull requests, bug fixes and issue reports. 

Before sending a PR, please discuss your change by raising an issue.

## License

BSD-2-Clause

A vendor/git.sr.ht/~evanj/errors/appveyor.yml => vendor/git.sr.ht/~evanj/errors/appveyor.yml +32 -0
@@ 0,0 1,32 @@
version: build-{build}.{branch}

clone_folder: C:\gopath\src\github.com\pkg\errors
shallow_clone: true # for startup speed

environment:
  GOPATH: C:\gopath

platform:
  - x64

# http://www.appveyor.com/docs/installed-software
install:
  # some helpful output for debugging builds
  - go version
  - go env
  # pre-installed MinGW at C:\MinGW is 32bit only
  # but MSYS2 at C:\msys64 has mingw64
  - set PATH=C:\msys64\mingw64\bin;%PATH%
  - gcc --version
  - g++ --version

build_script:
  - go install -v ./...

test_script:
  - set PATH=C:\gopath\bin;%PATH%
  - go test -v ./...

#artifacts:
#  - path: '%GOPATH%\bin\*.exe'
deploy: off

A vendor/git.sr.ht/~evanj/errors/errors.go => vendor/git.sr.ht/~evanj/errors/errors.go +288 -0
@@ 0,0 1,288 @@
// Package errors provides simple error handling primitives.
//
// The traditional error handling idiom in Go is roughly akin to
//
//     if err != nil {
//             return err
//     }
//
// which when applied recursively up the call stack results in error reports
// without context or debugging information. The errors package allows
// programmers to add context to the failure path in their code in a way
// that does not destroy the original value of the error.
//
// Adding context to an error
//
// The errors.Wrap function returns a new error that adds context to the
// original error by recording a stack trace at the point Wrap is called,
// together with the supplied message. For example
//
//     _, err := ioutil.ReadAll(r)
//     if err != nil {
//             return errors.Wrap(err, "read failed")
//     }
//
// If additional control is required, the errors.WithStack and
// errors.WithMessage functions destructure errors.Wrap into its component
// operations: annotating an error with a stack trace and with a message,
// respectively.
//
// Retrieving the cause of an error
//
// Using errors.Wrap constructs a stack of errors, adding context to the
// preceding error. Depending on the nature of the error it may be necessary
// to reverse the operation of errors.Wrap to retrieve the original error
// for inspection. Any error value which implements this interface
//
//     type causer interface {
//             Cause() error
//     }
//
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
// the topmost error that does not implement causer, which is assumed to be
// the original cause. For example:
//
//     switch err := errors.Cause(err).(type) {
//     case *MyError:
//             // handle specifically
//     default:
//             // unknown error
//     }
//
// Although the causer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// Formatted printing of errors
//
// All error values returned from this package implement fmt.Formatter and can
// be formatted by the fmt package. The following verbs are supported:
//
//     %s    print the error. If the error has a Cause it will be
//           printed recursively.
//     %v    see %s
//     %+v   extended format. Each Frame of the error's StackTrace will
//           be printed in detail.
//
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface:
//
//     type stackTracer interface {
//             StackTrace() errors.StackTrace
//     }
//
// The returned errors.StackTrace type is defined as
//
//     type StackTrace []Frame
//
// The Frame type represents a call site in the stack trace. Frame supports
// the fmt.Formatter interface that can be used for printing information about
// the stack trace of this error. For example:
//
//     if err, ok := err.(stackTracer); ok {
//             for _, f := range err.StackTrace() {
//                     fmt.Printf("%+s:%d\n", f, f)
//             }
//     }
//
// Although the stackTracer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// See the documentation for Frame.Format for more details.
package errors

import (
	"fmt"
	"io"
)

// New returns an error with the supplied message.
// New also records the stack trace at the point it was called.
func New(message string) error {
	return &fundamental{
		msg:   message,
		stack: callers(),
	}
}

// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
// Errorf also records the stack trace at the point it was called.
func Errorf(format string, args ...interface{}) error {
	return &fundamental{
		msg:   fmt.Sprintf(format, args...),
		stack: callers(),
	}
}

// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
	msg string
	*stack
}

func (f *fundamental) Error() string { return f.msg }

func (f *fundamental) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			io.WriteString(s, f.msg)
			f.stack.Format(s, verb)
			return
		}
		fallthrough
	case 's':
		io.WriteString(s, f.msg)
	case 'q':
		fmt.Fprintf(s, "%q", f.msg)
	}
}

// WithStack annotates err with a stack trace at the point WithStack was called.
// If err is nil, WithStack returns nil.
func WithStack(err error) error {
	if err == nil {
		return nil
	}
	return &withStack{
		err,
		callers(),
	}
}

type withStack struct {
	error
	*stack
}

func (w *withStack) Cause() error { return w.error }

// Unwrap provides compatibility for Go 1.13 error chains.
func (w *withStack) Unwrap() error { return w.error }

func (w *withStack) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			fmt.Fprintf(s, "%+v", w.Cause())
			w.stack.Format(s, verb)
			return
		}
		fallthrough
	case 's':
		io.WriteString(s, w.Error())
	case 'q':
		fmt.Fprintf(s, "%q", w.Error())
	}
}

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns New(message).
func Wrap(err error, message string) error {
	if err == nil {
		return New(message)
	}
	err = &withMessage{
		cause: err,
		msg:   message,
	}
	return &withStack{
		err,
		callers(),
	}
}

// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is called, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
	if err == nil {
		return nil
	}
	err = &withMessage{
		cause: err,
		msg:   fmt.Sprintf(format, args...),
	}
	return &withStack{
		err,
		callers(),
	}
}

// WithMessage annotates err with a new message.
// If err is nil, WithMessage returns nil.
func WithMessage(err error, message string) error {
	if err == nil {
		return nil
	}
	return &withMessage{
		cause: err,
		msg:   message,
	}
}

// WithMessagef annotates err with the format specifier.
// If err is nil, WithMessagef returns nil.
func WithMessagef(err error, format string, args ...interface{}) error {
	if err == nil {
		return nil
	}
	return &withMessage{
		cause: err,
		msg:   fmt.Sprintf(format, args...),
	}
}

type withMessage struct {
	cause error
	msg   string
}

func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
func (w *withMessage) Cause() error  { return w.cause }

// Unwrap provides compatibility for Go 1.13 error chains.
func (w *withMessage) Unwrap() error { return w.cause }

func (w *withMessage) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			fmt.Fprintf(s, "%+v\n", w.Cause())
			io.WriteString(s, w.msg)
			return
		}
		fallthrough
	case 's', 'q':
		io.WriteString(s, w.Error())
	}
}

// Cause returns the underlying cause of the error, if possible.
// An error value has a cause if it implements the following
// interface:
//
//     type causer interface {
//            Cause() error
//     }
//
// If the error does not implement Cause, the original error will
// be returned. If the error is nil, nil will be returned without further
// investigation.
func Cause(err error) error {
	type causer interface {
		Cause() error
	}

	for err != nil {
		cause, ok := err.(causer)
		if !ok {
			break
		}
		err = cause.Cause()
	}
	return err
}

A vendor/git.sr.ht/~evanj/errors/stack.go => vendor/git.sr.ht/~evanj/errors/stack.go +177 -0
@@ 0,0 1,177 @@
package errors

import (
	"fmt"
	"io"
	"path"
	"runtime"
	"strconv"
	"strings"
)

// Frame represents a program counter inside a stack frame.
// For historical reasons if Frame is interpreted as a uintptr
// its value represents the program counter + 1.
type Frame uintptr

// pc returns the program counter for this frame;
// multiple frames may have the same PC value.
func (f Frame) pc() uintptr { return uintptr(f) - 1 }

// file returns the full path to the file that contains the
// function for this Frame's pc.
func (f Frame) file() string {
	fn := runtime.FuncForPC(f.pc())
	if fn == nil {
		return "unknown"
	}
	file, _ := fn.FileLine(f.pc())
	return file
}

// line returns the line number of source code of the
// function for this Frame's pc.
func (f Frame) line() int {
	fn := runtime.FuncForPC(f.pc())
	if fn == nil {
		return 0
	}
	_, line := fn.FileLine(f.pc())
	return line
}

// name returns the name of this function, if known.
func (f Frame) name() string {
	fn := runtime.FuncForPC(f.pc())
	if fn == nil {
		return "unknown"
	}
	return fn.Name()
}

// Format formats the frame according to the fmt.Formatter interface.
//
//    %s    source file
//    %d    source line
//    %n    function name
//    %v    equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
//    %+s   function name and path of source file relative to the compile time
//          GOPATH separated by \n\t (<funcname>\n\t<path>)
//    %+v   equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
	switch verb {
	case 's':
		switch {
		case s.Flag('+'):
			io.WriteString(s, f.name())
			io.WriteString(s, "\n\t")
			io.WriteString(s, f.file())
		default:
			io.WriteString(s, path.Base(f.file()))
		}
	case 'd':
		io.WriteString(s, strconv.Itoa(f.line()))
	case 'n':
		io.WriteString(s, funcname(f.name()))
	case 'v':
		f.Format(s, 's')
		io.WriteString(s, ":")
		f.Format(s, 'd')
	}
}

// MarshalText formats a stacktrace Frame as a text string. The output is the
// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs.
func (f Frame) MarshalText() ([]byte, error) {
	name := f.name()
	if name == "unknown" {
		return []byte(name), nil
	}
	return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil
}

// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame

// Format formats the stack of Frames according to the fmt.Formatter interface.
//
//    %s	lists source files for each Frame in the stack
//    %v	lists the source file and line number for each Frame in the stack
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
//    %+v   Prints filename, function, and line number for each Frame in the stack.
func (st StackTrace) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		switch {
		case s.Flag('+'):
			for _, f := range st {
				io.WriteString(s, "\n")
				f.Format(s, verb)
			}
		case s.Flag('#'):
			fmt.Fprintf(s, "%#v", []Frame(st))
		default:
			st.formatSlice(s, verb)
		}
	case 's':
		st.formatSlice(s, verb)
	}
}

// formatSlice will format this StackTrace into the given buffer as a slice of
// Frame, only valid when called with '%s' or '%v'.
func (st StackTrace) formatSlice(s fmt.State, verb rune) {
	io.WriteString(s, "[")
	for i, f := range st {
		if i > 0 {
			io.WriteString(s, " ")
		}
		f.Format(s, verb)
	}
	io.WriteString(s, "]")
}

// stack represents a stack of program counters.
type stack []uintptr

func (s *stack) Format(st fmt.State, verb rune) {
	switch verb {
	case 'v':
		switch {
		case st.Flag('+'):
			for _, pc := range *s {
				f := Frame(pc)
				fmt.Fprintf(st, "\n%+v", f)
			}
		}
	}
}

func (s *stack) StackTrace() StackTrace {
	f := make([]Frame, len(*s))
	for i := 0; i < len(f); i++ {
		f[i] = Frame((*s)[i])
	}
	return f
}

func callers() *stack {
	const depth = 32
	var pcs [depth]uintptr
	n := runtime.Callers(3, pcs[:])
	var st stack = pcs[0:n]
	return &st
}

// funcname removes the path prefix component of a function's name reported by func.Name().
func funcname(name string) string {
	i := strings.LastIndex(name, "/")
	name = name[i+1:]
	i = strings.Index(name, ".")
	return name[i+1:]
}

M vendor/modules.txt => vendor/modules.txt +3 -0
@@ 1,6 1,9 @@
# git.sr.ht/~evanj/embed v0.0.0-20200702023953-8a902e4e7e94
## explicit
git.sr.ht/~evanj/embed
# git.sr.ht/~evanj/errors v0.0.0-20191127050903-ab51d2fcd90b
## explicit
git.sr.ht/~evanj/errors
# git.sr.ht/~evanj/security v0.0.0-20200228044358-9b9bc6682997
## explicit
git.sr.ht/~evanj/security