~evanj/cms

f5e15b55ed6a393014ce45ab6f96561426e57708 — Evan M Jones 4 months ago 9c6ee95
Feat(c/hook): Testing complete for hook controller. 100%.
M internal/c/c.go => internal/c/c.go +6 -1
@@ 3,6 3,7 @@ package c
import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io"


@@ 16,7 17,11 @@ import (

type KeyCookie = string

var KeyUserLogin KeyCookie = "KeyUserLogin"
var (
	KeyUserLogin KeyCookie = "KeyUserLogin"

	ErrNoLogin = errors.New("must be logged in")
)

type Controller struct {
	log *log.Logger

M internal/c/hook/hook.go => internal/c/hook/hook.go +28 -27
@@ 1,6 1,7 @@
package hook

import (
	"errors"
	"fmt"
	"log"
	"net/http"


@@ 15,6 16,11 @@ import (

var (
	hookHTML = tmpl.MustParse("html/hook.html")

	ErrNoSpace      = errors.New("failed to find required space")
	ErrNoHook       = errors.New("failed to find desired webhook")
	ErrFailedCreate = errors.New("failed to create webhook")
	ErrFailedDelete = errors.New("failed to delete webhook")
)

type Content struct {


@@ 40,54 46,50 @@ func New(log *log.Logger, db dber) *Content {
	}
}

func (c *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user, err := c.GetCookieUser(w, r)
func (h *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user, err := h.GetCookieUser(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, "must be logged int")
		h.Error2(w, r, http.StatusBadRequest, c.ErrNoLogin)
		return
	}

	switch c.Method(r) {
	switch h.Method(r) {

	case "POST":
		space, err := c.db.SpaceGet(user, r.FormValue("space"))
		space, err := h.db.SpaceGet(user, r.FormValue("space"))
		if err != nil {
			c.Error(w, r, http.StatusBadRequest, "failed to find required space")
			h.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
			return
		}

		hook, err := c.db.HookNew(space, r.FormValue("url"))
		hook, err := h.db.HookNew(space, r.FormValue("url"))
		if err != nil {
			c.log.Println(err)
			c.Error(w, r, http.StatusInternalServerError, "failed to create webhook")
			h.Error2(w, r, http.StatusInternalServerError, ErrFailedCreate)
			return
		}

		c.Redirect(w, r, fmt.Sprintf("/hook/%s/%s", space.ID(), hook.ID()))
		h.Redirect(w, r, fmt.Sprintf("/hook/%s/%s", space.ID(), hook.ID()))
		return

	case "DELETE":
		space, err := c.db.SpaceGet(user, r.FormValue("space"))
		space, err := h.db.SpaceGet(user, r.FormValue("space"))
		if err != nil {
			c.log.Println(err)
			c.Error(w, r, http.StatusBadRequest, "failed to find required space")
			h.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
			return
		}

		hook, err := c.db.HookGet(space, r.FormValue("hook"))
		hook, err := h.db.HookGet(space, r.FormValue("hook"))
		if err != nil {
			c.log.Println(err)
			c.Error(w, r, http.StatusBadRequest, "failed to find desired webhook")
			h.Error2(w, r, http.StatusBadRequest, ErrNoHook)
			return
		}

		if err := c.db.HookDelete(space, hook); err != nil {
			c.log.Println(err)
			c.Error(w, r, http.StatusBadRequest, "failed to delete webhook")
		if err := h.db.HookDelete(space, hook); err != nil {
			h.Error2(w, r, http.StatusInternalServerError, ErrFailedDelete)
			return
		}

		c.Redirect(w, r, fmt.Sprintf("/space/%s", space.ID()))
		h.Redirect(w, r, fmt.Sprintf("/space/%s", space.ID()))
		return

	case "GET":


@@ 95,25 97,24 @@ func (c *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
		hookID := r.FormValue("hook")

		parts := strings.Split(r.URL.Path, "/")
		if len(parts) > 2 {
			c.log.Println(parts)
		if len(parts) > 3 {
			spaceID = parts[2]
			hookID = parts[3]
		}

		space, err := c.db.SpaceGet(user, spaceID)
		space, err := h.db.SpaceGet(user, spaceID)
		if err != nil {
			c.Error(w, r, http.StatusBadRequest, "failed to find required space")
			h.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
			return
		}

		hook, err := c.db.HookGet(space, hookID)
		hook, err := h.db.HookGet(space, hookID)
		if err != nil {
			c.Error(w, r, http.StatusBadRequest, "failed to find desired webhook")
			h.Error2(w, r, http.StatusBadRequest, ErrNoHook)
			return
		}

		c.HTML(w, r, hookHTML, map[string]interface{}{
		h.HTML(w, r, hookHTML, map[string]interface{}{
			"User":  user,
			"Space": space,
			"Hook":  hook,

M internal/c/hook/hook_test.go => internal/c/hook/hook_test.go +343 -0
@@ 1,1 1,344 @@
package hook_test

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

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

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

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

	for _, method := range methods {
		req, _ := http.NewRequest(method, ts.URL, nil)
		res, _ := http.DefaultClient.Do(req)
		bytes, _ := ioutil.ReadAll(res.Body)
		assert.Equal(t, true, strings.Contains(string(bytes), c.ErrNoLogin.Error()))
	}
}

type FakeUser struct{ u, p string }

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 (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 FakeHook struct{ id, url string }

func (u FakeHook) ID() string  { return u.id }
func (u FakeHook) URL() string { return u.url }

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

	var (
		uItem = FakeUser{uuid.New().String(), uuid.New().String()}
		sItem = FakeSpace{uuid.New().String(), uuid.New().String()}
		hItem = FakeHook{uuid.New().String(), uuid.New().String()}
	)

	type spec struct {
		expect int
		form   url.Values
		mock   func(db *mock_hook.Mockdber)
	}

	tests := []spec{
		{
			http.StatusTemporaryRedirect,
			url.Values{
				"url":    {hItem.URL()},
				"space":  {sItem.ID()},
				"method": {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookNew(sItem, hItem.URL()).Return(hItem, nil).AnyTimes()
			},
		},
		{
			http.StatusOK,
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"GET"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookGet(sItem, hItem.ID()).Return(hItem, nil).AnyTimes()
			},
		},
		{
			http.StatusTemporaryRedirect,
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"DELETE"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookGet(sItem, hItem.ID()).Return(hItem, nil).AnyTimes()
				db.EXPECT().HookDelete(sItem, hItem).Return(nil).AnyTimes()
			},
		},
	}

	for _, test := range tests {
		var (
			ctrl = gomock.NewController(t)
			db   = mock_hook.NewMockdber(ctrl)
			l    *log.Logger
			s    = hook.New(l, db)
			ts   = httptest.NewServer(s)
		)

		test.mock(db)

		req, _ := http.NewRequest("POST", ts.URL,
			strings.NewReader(test.form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.SetBasicAuth(uItem.Name(), uItem.p)
		req.Header.Set("Accept", "text/html")
		res, _ := http.DefaultClient.Do(req)

		assert.Equal(t, test.expect, res.StatusCode)
	}
}

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

	var (
		uItem = FakeUser{uuid.New().String(), uuid.New().String()}
		sItem = FakeSpace{uuid.New().String(), uuid.New().String()}
		hItem = FakeHook{uuid.New().String(), uuid.New().String()}

		err = errors.New("bogus")
	)

	type spec struct {
		expect int
		err    error
		form   url.Values
		mock   func(db *mock_hook.Mockdber)
	}

	tests := []spec{
		// POST
		{
			http.StatusBadRequest,
			hook.ErrNoSpace,
			url.Values{
				"url":    {hItem.URL()},
				"space":  {sItem.ID()},
				"method": {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(nil, err).AnyTimes()
				// db.EXPECT().HookNew(sItem, hItem.URL()).Return(hItem, nil).AnyTimes()
			},
		},
		{
			http.StatusInternalServerError,
			hook.ErrFailedCreate,
			url.Values{
				"url":    {hItem.URL()},
				"space":  {sItem.ID()},
				"method": {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookNew(sItem, hItem.URL()).Return(nil, err).AnyTimes()
			},
		},
		// GET
		{
			http.StatusBadRequest,
			hook.ErrNoSpace,
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"GET"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(nil, err).AnyTimes()
				// db.EXPECT().HookGet(sItem, hItem.ID()).Return(hItem, nil).AnyTimes()
			},
		},
		{
			http.StatusBadRequest,
			hook.ErrNoHook,
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"GET"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookGet(sItem, hItem.ID()).Return(nil, err).AnyTimes()
			},
		},
		// DELETE
		{
			http.StatusBadRequest,
			hook.ErrNoSpace,
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"DELETE"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(nil, err).AnyTimes()
				// db.EXPECT().HookGet(sItem, hItem.ID()).Return(hItem, nil).AnyTimes()
				// db.EXPECT().HookDelete(sItem, hItem).Return(nil).AnyTimes()
			},
		},
		{
			http.StatusBadRequest,
			hook.ErrNoHook,
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"DELETE"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookGet(sItem, hItem.ID()).Return(nil, err).AnyTimes()
				// db.EXPECT().HookDelete(sItem, hItem).Return(nil).AnyTimes()
			},
		},
		{
			http.StatusInternalServerError,
			hook.ErrFailedDelete,
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"DELETE"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookGet(sItem, hItem.ID()).Return(hItem, nil).AnyTimes()
				db.EXPECT().HookDelete(sItem, hItem).Return(err).AnyTimes()
			},
		},
		// PATCH / NOT FOUND
		{
			http.StatusNotFound,
			errors.New("404 page not found"),
			url.Values{
				"hook":   {hItem.ID()},
				"space":  {sItem.ID()},
				"method": {"PATCH"}, // By default net/http doesn't parse body on DELETE.
			},
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookGet(sItem, hItem.ID()).Return(hItem, nil).AnyTimes()
				db.EXPECT().HookDelete(sItem, hItem).Return(err).AnyTimes()
			},
		},
	}

	for _, test := range tests {
		var (
			ctrl = gomock.NewController(t)
			db   = mock_hook.NewMockdber(ctrl)
			l    *log.Logger
			s    = hook.New(l, db)
			ts   = httptest.NewServer(s)
		)

		test.mock(db)

		req, _ := http.NewRequest("POST", ts.URL,
			strings.NewReader(test.form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.SetBasicAuth(uItem.Name(), uItem.p)
		req.Header.Set("Accept", "text/html")
		res, _ := http.DefaultClient.Do(req)
		byt, _ := ioutil.ReadAll(res.Body)
		assert.Equal(t, test.expect, res.StatusCode)
		assert.Equal(t, true, strings.Contains(string(byt), test.err.Error()))
	}
}

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

	var (
		uItem = FakeUser{uuid.New().String(), uuid.New().String()}
		sItem = FakeSpace{uuid.New().String(), uuid.New().String()}
		hItem = FakeHook{uuid.New().String(), uuid.New().String()}
	)

	type spec struct {
		expect int
		mock   func(db *mock_hook.Mockdber)
	}

	tests := []spec{
		{
			http.StatusOK,
			func(db *mock_hook.Mockdber) {
				db.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				db.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				db.EXPECT().HookGet(sItem, hItem.ID()).Return(hItem, nil).AnyTimes()
			},
		},
	}

	for _, test := range tests {
		var (
			ctrl = gomock.NewController(t)
			db   = mock_hook.NewMockdber(ctrl)
			l    *log.Logger
			s    = hook.New(l, db)
			ts   = httptest.NewServer(s)
		)

		test.mock(db)

		req, _ := http.NewRequest("GET", ts.URL+"/hook/"+sItem.ID()+"/"+hItem.ID(), nil)
		req.SetBasicAuth(uItem.Name(), uItem.p)
		res, _ := http.DefaultClient.Do(req)
		assert.Equal(t, test.expect, res.StatusCode)
	}
}

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

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

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

// 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)
}

// SpaceGet mocks base method
func (m *Mockdber) SpaceGet(user user.User, spaceID string) (space.Space, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "SpaceGet", user, spaceID)
	ret0, _ := ret[0].(space.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)
}

// HookNew mocks base method
func (m *Mockdber) HookNew(space space.Space, url string) (hook.Hook, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "HookNew", space, url)
	ret0, _ := ret[0].(hook.Hook)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

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

// HookGet mocks base method
func (m *Mockdber) HookGet(space space.Space, id string) (hook.Hook, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "HookGet", space, id)
	ret0, _ := ret[0].(hook.Hook)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

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

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

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

M internal/c/space/space.go => internal/c/space/space.go +5 -6
@@ 20,7 20,6 @@ import (
var (
	spaceHTML = tmpl.MustParse("html/space.html")

	ErrNoLogin = errors.New("must be logged in")
	ErrNoSpace = errors.New("failed to find space")
)



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

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, ErrNoLogin)
		s.Error2(w, r, http.StatusBadRequest, c.ErrNoLogin)
		return
	}



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

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



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

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



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

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(ErrNoLogin, "can't update space"))
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(c.ErrNoLogin, "can't update space"))
		return
	}



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

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


M internal/c/space/space_test.go => internal/c/space/space_test.go +3 -2
@@ 12,6 12,7 @@ import (
	"strings"
	"testing"

	"git.sr.ht/~evanj/cms/internal/c"
	"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"


@@ 87,7 88,7 @@ func TestNoUser(t *testing.T) {

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



@@ 166,7 167,7 @@ func TestAll(t *testing.T) {
		SpaceTest{
			createSC: http.StatusBadRequest,
			create: func(db *mock_space.Mockdber) {
				db.EXPECT().UserGet(uname, upass).Return(nil, space.ErrNoLogin).AnyTimes()
				db.EXPECT().UserGet(uname, upass).Return(nil, c.ErrNoLogin).AnyTimes()
			},
			getSC: http.StatusBadRequest,
			get: func(db *mock_space.Mockdber) {

M makefile => makefile +1 -0
@@ 5,6 5,7 @@ all: setup vendor gen build

setup:
	@go get git.sr.ht/~evanj/embed/cmd/embed
	@go get github.com/golang/mock/mockgen

vendor: go.mod go.sum
	@go mod tidy