~evanj/cms

eb76754c4568d83bf5f2b797175889a9e280b8fc — Evan M Jones 4 months ago f5e15b5
Feat(c/contenttype): Testing complete for contenttype controller.
M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +51 -40
@@ 1,6 1,7 @@
package contenttype

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


@@ 18,6 19,18 @@ import (

var (
	contenttypeHTML = tmpl.MustParse("html/contenttype.html")

	ErrNoLogin      = c.ErrNoLogin // TODO: Refactor.
	ErrNoSpace      = errors.New("failed to find required space")
	ErrNoCT         = errors.New("failed to find desired contenttype")
	ErrNoC          = errors.New("failed to find content list")
	ErrFailedCreate = errors.New("failed to create contenttype")
	ErrFailedUpdate = errors.New("failed to update contenttype")
	ErrFailedDelete = errors.New("failed to delete contenttype")
	ErrBadForm      = errors.New("form has malformed data")
	ErrNoFields     = errors.New("contenttype must have at least one field")
	ErrNoNameField  = errors.New("must have field of \"name\" for searchability")
	ErrBadOrder     = errors.New("invalid order value")
)

type ContentType struct {


@@ 45,13 58,13 @@ func New(log *log.Logger, db dber) *ContentType {
func (c *ContentType) tree(w http.ResponseWriter, r *http.Request) (user.User, space.Space, error) {
	user, err := c.GetCookieUser(w, r)
	if err != nil {
		return nil, nil, fmt.Errorf("must be logged in to perform this action")
		return nil, nil, ErrNoLogin
	}

	spaceID := r.FormValue("space")
	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to retrieve space for id %s", spaceID)
		return nil, nil, ErrNoSpace
	}

	return user, space, nil


@@ 60,6 73,12 @@ func (c *ContentType) tree(w http.ResponseWriter, r *http.Request) (user.User, s
func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")

	_, space, err := c.tree(w, r)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		return
	}

	i := 1
	for key := range r.Form {
		if strings.Contains(key, "field_type_") || strings.Contains(key, "field_name_") {


@@ 74,7 93,7 @@ func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
		valName := r.FormValue(keyName)
		valType := r.FormValue(keyType)
		if valName == "" || valType == "" {
			c.Error(w, r, http.StatusBadRequest, "form has malformed data")
			c.Error2(w, r, http.StatusBadRequest, ErrBadForm)
			return
		}
		params = append(params, db.ContentTypeNewParam{


@@ 84,7 103,7 @@ func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
	}

	if len(params) < 1 {
		c.Error(w, r, http.StatusBadRequest, "contenttype must have at least one field")
		c.Error2(w, r, http.StatusBadRequest, ErrNoFields)
		return
	}



@@ 96,25 115,17 @@ func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
		}
	}
	if !hasName {
		c.Error(w, r, http.StatusInternalServerError, "must have field of \"name\" for searchability")
		return
	}

	user, space, err := c.tree(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, err.Error())
		c.Error2(w, r, http.StatusBadRequest, ErrNoNameField)
		return
	}

	ct, err := c.db.ContentTypeNew(space, name, params)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to create contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrFailedCreate)
		return
	}

	url := fmt.Sprintf("/contenttype/%s/%s", space.ID(), ct.ID())
	c.log.Println("successfully created new contenttype for user", user.Name(), "in space", space.Name(), "redirecting to", url)
	c.Redirect(w, r, url)
}



@@ 122,15 133,15 @@ func (c *ContentType) update(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")
	ctID := r.FormValue("contenttype")

	user, space, err := c.tree(w, r)
	_, space, err := c.tree(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, err.Error())
		c.Error2(w, r, http.StatusBadRequest, err)
		return
	}

	old, err := c.db.ContentTypeGet(space, ctID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find required contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return
	}



@@ 150,7 161,7 @@ func (c *ContentType) update(w http.ResponseWriter, r *http.Request) {
		valName := r.FormValue(keyName)
		valType := r.FormValue(keyType)
		if valName == "" || valType == "" {
			c.Error(w, r, http.StatusBadRequest, "form has malformed data")
			c.Error2(w, r, http.StatusBadRequest, ErrBadForm)
			return
		}
		newParams = append(newParams, db.ContentTypeNewParam{


@@ 175,7 186,7 @@ func (c *ContentType) update(w http.ResponseWriter, r *http.Request) {
		}

		if valName == "" || valType == "" || valID == "" {
			c.Error(w, r, http.StatusBadRequest, "form has malformed data")
			c.Error2(w, r, http.StatusBadRequest, ErrBadForm)
			return
		}



@@ 187,37 198,40 @@ func (c *ContentType) update(w http.ResponseWriter, r *http.Request) {
	}

	if len(updateParams) < 1 {
		c.Error(w, r, http.StatusBadRequest, "contenttype must have at least one field")
		c.Error2(w, r, http.StatusBadRequest, ErrNoFields)
		return
	}

	// Enforce content always has a value for value type of "name"
	hasName := false
	for _, p := range newParams {
		if p.Name == "name" {
			hasName = true
		}
	}
	for _, p := range updateParams {
		if p.Name == "name" {
			hasName = true
		}
	}
	if !hasName {
		c.Error(w, r, http.StatusInternalServerError, "must have field of \"name\" for searchability")
		c.Error2(w, r, http.StatusBadRequest, ErrNoNameField)
		return
	}

	ct, err := c.db.ContentTypeGet(space, ctID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find required contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return
	}

	ct, err = c.db.ContentTypeUpdate(space, ct, name, newParams, updateParams)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to create contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrFailedUpdate)
		return
	}

	url := fmt.Sprintf("/contenttype/%s/%s", space.ID(), ct.ID())
	c.log.Println("successfully updated contenttype for user", user.Name(), "in space", space.Name(), "redirecting to", url)
	c.Redirect(w, r, url)
}



@@ 233,19 247,19 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, "must be logged in")
		c.Error2(w, r, http.StatusBadRequest, ErrNoLogin)
		return
	}

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

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return
	}



@@ 259,7 273,7 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {
	case "":
		o = db.OrderAsc
	default:
		c.Error(w, r, http.StatusBadRequest, "invalid order value")
		c.Error2(w, r, http.StatusBadRequest, ErrBadOrder)
		return
	}



@@ 272,8 286,7 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {
	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentPerContentType(space, ct, before, o, f)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to find content for contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrNoC)
		return
	}



@@ 291,30 304,28 @@ func (c *ContentType) delete(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, "must be logged in to create contenttype")
		c.Error2(w, r, http.StatusBadRequest, ErrNoLogin)
		return
	}

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

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return
	}

	if err := c.db.ContentTypeDelete(space, ct); err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to delete contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrFailedDelete)
		return
	}

	url := fmt.Sprintf("/space/%s", space.ID())
	c.log.Println("successfully deleted contenttype for user", user.Name(), "in space", space.Name(), "redirecting to", url)
	c.Redirect(w, r, url)
}



@@ 324,20 335,20 @@ func (c *ContentType) search(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, "must be logged in to create contenttype")
		c.Error2(w, r, http.StatusBadRequest, ErrNoLogin)
		return
	}

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

	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentTypeSearch(space, query, before)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired contenttype")
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return
	}


M internal/c/contenttype/contenttype_test.go => internal/c/contenttype/contenttype_test.go +393 -0
@@ 1,1 1,394 @@
package contenttype_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/contenttype"
	mock_contenttype "git.sr.ht/~evanj/cms/internal/c/contenttype/mock"
	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"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_contenttype.NewMockdber(ctrl)
		l       *log.Logger
		s       = contenttype.New(l, db)
		ts      = httptest.NewServer(s)
		methods = []string{"GET", "POST", "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 FakeContentType struct {
	id, name string
	fields   int
}

func (u FakeContentType) ID() string   { return u.id }
func (u FakeContentType) Name() string { return u.name }
func (u FakeContentType) Fields() (r []valuetype.ValueType) {
	for i := 0; i < u.fields; i++ {
		var val valuetype.ValueType
		r = append(r, val)
	}
	return
}
func (u FakeContentType) FieldsWithRefCount() (r int) { return }

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

	var (
		uItem        = FakeUser{uuid.New().String(), uuid.New().String()}
		sItem        = FakeSpace{uuid.New().String(), uuid.New().String()}
		ctItem       = FakeContentType{uuid.New().String(), uuid.New().String(), 0}
		ctItemUpdate = FakeContentType{uuid.New().String(), uuid.New().String(), 1}

		cl  content.ContentList
		ctl content.ContentList
	)

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

	tests := []spec{
		{
			http.StatusOK,
			url.Values{
				"name":         {ctItem.Name()},
				"field_name_1": {"name"},
				"field_type_1": {string(valuetype.StringSmall)},
				"space":        {sItem.ID()},
				"method":       {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeNew(sItem, ctItem.Name(), newParams).Return(ctItem, nil).AnyTimes()
			},
		},
		{
			http.StatusOK,
			url.Values{
				"name":                {ctItemUpdate.Name()},
				"contenttype":         {ctItemUpdate.ID()},
				"field_name_2":        {"name"},
				"field_type_2":        {string(valuetype.StringSmall)},
				"field_update_id_1":   {"1234"},
				"field_update_name_1": {"desc"},
				"field_update_type_1": {string(valuetype.StringBig)},
				"space":               {sItem.ID()},
				"method":              {"PATCH"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				updateParams := []db.ContentTypeUpdateParam{
					{"1234", "desc", string(valuetype.StringBig)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeUpdate(sItem, ctItemUpdate, ctItemUpdate.Name(), newParams, updateParams).Return(ctItemUpdate, nil).AnyTimes()
				m.EXPECT().ContentTypeGet(sItem, ctItemUpdate.ID()).Return(ctItemUpdate, nil).AnyTimes()
			},
		},
		{
			http.StatusOK,
			url.Values{
				"contenttype": {ctItem.ID()},
				"space":       {sItem.ID()},
				"method":      {"GET"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeGet(sItem, ctItem.ID()).Return(ctItem, nil).AnyTimes()
				m.EXPECT().ContentPerContentType(sItem, ctItem, 0, db.OrderAsc, "name").Return(cl, nil).AnyTimes()
			},
		},
		{
			http.StatusOK,
			url.Values{
				"query":  {"post"},
				"space":  {sItem.ID()},
				"method": {"GET"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeGet(sItem, ctItem.ID()).Return(ctItem, nil).AnyTimes()
				m.EXPECT().ContentTypeSearch(sItem, "post", 0).Return(ctl, nil).AnyTimes()
			},
		},
		{
			http.StatusOK,
			url.Values{
				"contenttype": {ctItem.ID()},
				"space":       {sItem.ID()},
				"order":       {"desc"},
				"method":      {"GET"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeGet(sItem, ctItem.ID()).Return(ctItem, nil).AnyTimes()
				m.EXPECT().ContentPerContentType(sItem, ctItem, 0, db.OrderDesc, "name").Return(cl, nil).AnyTimes()
			},
		},
		{
			http.StatusOK,
			url.Values{
				"contenttype": {ctItem.ID()},
				"space":       {sItem.ID()},
				"method":      {"DELETE"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeGet(sItem, ctItem.ID()).Return(ctItem, nil).AnyTimes()
				m.EXPECT().ContentTypeDelete(sItem, ctItem).Return(nil).AnyTimes()
			},
		},
	}

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

		test.mock(db)

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

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

	var (
		uItem        = FakeUser{uuid.New().String(), uuid.New().String()}
		sItem        = FakeSpace{uuid.New().String(), uuid.New().String()}
		ctItem       = FakeContentType{uuid.New().String(), uuid.New().String(), 0}
		ctItemUpdate = FakeContentType{uuid.New().String(), uuid.New().String(), 1}
		err          = errors.New("placeholder")
	)

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

	tests := []spec{
		{
			http.StatusBadRequest,
			contenttype.ErrNoFields,
			url.Values{
				"name":   {ctItem.Name()},
				"space":  {sItem.ID()},
				"method": {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeNew(sItem, ctItem.Name(), newParams).Return(ctItem, nil).AnyTimes()
			},
		},
		{
			http.StatusBadRequest,
			contenttype.ErrNoNameField,
			url.Values{
				"name":         {ctItem.Name()},
				"field_name_1": {"desc"},
				"field_type_1": {string(valuetype.StringSmall)},
				"space":        {sItem.ID()},
				"method":       {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeNew(sItem, ctItem.Name(), newParams).Return(ctItem, nil).AnyTimes()
			},
		},
		{
			http.StatusBadRequest,
			contenttype.ErrBadForm,
			url.Values{
				"name":         {ctItem.Name()},
				"field_name_2": {"desc"},
				"field_type_1": {string(valuetype.StringSmall)},
				"space":        {sItem.ID()},
				"method":       {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeNew(sItem, ctItem.Name(), newParams).Return(ctItem, nil).AnyTimes()
			},
		},
		{
			http.StatusInternalServerError,
			errors.New("failed to create contenttype"),
			url.Values{
				"name":         {ctItem.Name()},
				"field_name_1": {"name"},
				"field_type_1": {string(valuetype.StringSmall)},
				"space":        {sItem.ID()},
				"method":       {"POST"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeNew(sItem, ctItem.Name(), newParams).Return(nil, err).AnyTimes()
			},
		},
		{
			http.StatusBadRequest,
			contenttype.ErrBadForm,
			url.Values{
				"name":                {ctItemUpdate.Name()},
				"contenttype":         {ctItemUpdate.ID()},
				"field_name_3":        {"name"},
				"field_type_2":        {string(valuetype.StringSmall)},
				"field_update_id_1":   {"1234"},
				"field_update_name_1": {"desc"},
				"field_update_type_1": {string(valuetype.StringBig)},
				"space":               {sItem.ID()},
				"method":              {"PATCH"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				updateParams := []db.ContentTypeUpdateParam{
					{"1234", "desc", string(valuetype.StringBig)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeUpdate(sItem, ctItemUpdate, ctItemUpdate.Name(), newParams, updateParams).Return(ctItemUpdate, nil).AnyTimes()
				m.EXPECT().ContentTypeGet(sItem, ctItemUpdate.ID()).Return(ctItemUpdate, nil).AnyTimes()
			},
		},
		{
			http.StatusBadRequest,
			contenttype.ErrBadForm,
			url.Values{
				"name":                {ctItemUpdate.Name()},
				"contenttype":         {ctItemUpdate.ID()},
				"field_name_2":        {"name"},
				"field_type_2":        {string(valuetype.StringSmall)},
				"field_update_id_2":   {"1234"},
				"field_update_name_1": {"desc"},
				"field_update_type_1": {string(valuetype.StringBig)},
				"space":               {sItem.ID()},
				"method":              {"PATCH"}, // By default net/http doesn't parse body on DELETE.
			},
			func(m *mock_contenttype.Mockdber) {
				newParams := []db.ContentTypeNewParam{
					{"name", string(valuetype.StringSmall)},
				}
				updateParams := []db.ContentTypeUpdateParam{
					{"1234", "desc", string(valuetype.StringBig)},
				}
				m.EXPECT().UserGet(uItem.Name(), uItem.p).Return(uItem, nil).AnyTimes()
				m.EXPECT().SpaceGet(uItem, sItem.ID()).Return(sItem, nil).AnyTimes()
				m.EXPECT().ContentTypeUpdate(sItem, ctItemUpdate, ctItemUpdate.Name(), newParams, updateParams).Return(ctItemUpdate, nil).AnyTimes()
				m.EXPECT().ContentTypeGet(sItem, ctItemUpdate.ID()).Return(ctItemUpdate, nil).AnyTimes()
			},
		},
	}

	for _, test := range tests {
		var (
			ctrl = gomock.NewController(t)
			db   = mock_contenttype.NewMockdber(ctrl)
			l    *log.Logger
			s    = contenttype.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)
		res, _ := http.DefaultClient.Do(req)
		byt, _ := ioutil.ReadAll(res.Body)
		if !strings.Contains(test.err.Error(), string(byt)) {
			panic(string(byt))
		}
		assert.Equal(t, true, strings.Contains(test.err.Error(), string(byt)))
		assert.Equal(t, test.expect, res.StatusCode)
	}
}

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

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

import (
	reflect "reflect"

	content "git.sr.ht/~evanj/cms/internal/m/content"
	contenttype "git.sr.ht/~evanj/cms/internal/m/contenttype"
	space "git.sr.ht/~evanj/cms/internal/m/space"
	user "git.sr.ht/~evanj/cms/internal/m/user"
	db "git.sr.ht/~evanj/cms/internal/s/db"
	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)
}

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

// ContentTypeNew mocks base method
func (m *Mockdber) ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ContentTypeNew", space, name, params)
	ret0, _ := ret[0].(contenttype.ContentType)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

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

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

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

// ContentTypeUpdate mocks base method
func (m *Mockdber) ContentTypeUpdate(space space.Space, ct contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ContentTypeUpdate", space, ct, name, newParams, updateParams)
	ret0, _ := ret[0].(contenttype.ContentType)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

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

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

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

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

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

// ContentPerContentType mocks base method
func (m *Mockdber) ContentPerContentType(space space.Space, ct contenttype.ContentType, before int, order db.OrderType, sortField string) (content.ContentList, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ContentPerContentType", space, ct, before, order, sortField)
	ret0, _ := ret[0].(content.ContentList)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

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