Compare commits
3 Commits
67c8df843d
...
a81e5195bc
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | a81e5195bc | |
Maxim Lebedev | d8080f25fa | |
Maxim Lebedev | c7dc4d027d |
|
@ -17,9 +17,129 @@ type (
|
||||||
Update(ctx context.Context, path string, update UpdateFunc) (*domain.Entry, error)
|
Update(ctx context.Context, path string, update UpdateFunc) (*domain.Entry, error)
|
||||||
Delete(ctx context.Context, path string) (bool, error)
|
Delete(ctx context.Context, path string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dummyRepository struct{}
|
||||||
|
|
||||||
|
stubRepository struct {
|
||||||
|
outputs []domain.Entry
|
||||||
|
output *domain.Entry
|
||||||
|
err error
|
||||||
|
ok bool
|
||||||
|
}
|
||||||
|
|
||||||
|
spyRepository struct {
|
||||||
|
subRepository Repository
|
||||||
|
Calls int
|
||||||
|
Creates int
|
||||||
|
Deletes int
|
||||||
|
Fetches int
|
||||||
|
Gets int
|
||||||
|
Updates int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(toby3d): fakeRepository is already provided by memory sub-package.
|
||||||
|
// NOTE(toby3d): mockRepository is complicated. Mocking too much is bad.
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrExist error = errors.New("this entry already exist")
|
ErrExist error = errors.New("this entry already exist")
|
||||||
ErrNotExist error = errors.New("this entry is not exist")
|
ErrNotExist error = errors.New("this entry is not exist")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewDummyMediaRepository creates an empty repository to satisfy contracts.
|
||||||
|
// It is used in tests where repository working is not important.
|
||||||
|
func NewDummyEntryRepository() Repository {
|
||||||
|
return &dummyRepository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dummyRepository) Create(_ context.Context, _ string, _ domain.Entry) error { return nil }
|
||||||
|
func (dummyRepository) Delete(_ context.Context, _ string) (bool, error) { return false, nil }
|
||||||
|
func (dummyRepository) Get(_ context.Context, _ string) (*domain.Entry, error) { return nil, nil }
|
||||||
|
|
||||||
|
func (dummyRepository) Fetch(_ context.Context, _ string) ([]domain.Entry, int, error) {
|
||||||
|
return make([]domain.Entry, 0), 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dummyRepository) Update(_ context.Context, _ string, _ UpdateFunc) (*domain.Entry, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStubEntryRepository creates a repository that always returns input as a
|
||||||
|
// output. It is used in tests where some dependency on the repository is
|
||||||
|
// required.
|
||||||
|
func NewStubEntryRepository(outputs []domain.Entry, output *domain.Entry, err error, ok bool) Repository {
|
||||||
|
return &stubRepository{
|
||||||
|
outputs: outputs,
|
||||||
|
output: output,
|
||||||
|
err: err,
|
||||||
|
ok: ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *stubRepository) Create(_ context.Context, _ string, _ domain.Entry) error {
|
||||||
|
return repo.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *stubRepository) Delete(ctx context.Context, path string) (bool, error) {
|
||||||
|
return repo.ok, repo.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *stubRepository) Fetch(ctx context.Context, path string) ([]domain.Entry, int, error) {
|
||||||
|
return repo.outputs, len(repo.outputs), repo.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *stubRepository) Get(ctx context.Context, path string) (*domain.Entry, error) {
|
||||||
|
return repo.output, repo.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *stubRepository) Update(ctx context.Context, path string, update UpdateFunc) (*domain.Entry, error) {
|
||||||
|
return repo.output, repo.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSpyEntryRepository creates a spy repository which count outside calls,
|
||||||
|
// based on provided subRepo. If subRepo is nil, then DummyRepository will be
|
||||||
|
// used.
|
||||||
|
func NewSpyEntryRepository(subRepo Repository) *spyRepository {
|
||||||
|
if subRepo == nil {
|
||||||
|
subRepo = NewDummyEntryRepository()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &spyRepository{
|
||||||
|
subRepository: subRepo,
|
||||||
|
Creates: 0,
|
||||||
|
Updates: 0,
|
||||||
|
Gets: 0,
|
||||||
|
Fetches: 0,
|
||||||
|
Deletes: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *spyRepository) Create(ctx context.Context, path string, e domain.Entry) error {
|
||||||
|
repo.Creates++
|
||||||
|
|
||||||
|
return repo.subRepository.Create(ctx, path, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *spyRepository) Delete(ctx context.Context, path string) (bool, error) {
|
||||||
|
repo.Deletes++
|
||||||
|
|
||||||
|
return repo.subRepository.Delete(ctx, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *spyRepository) Fetch(ctx context.Context, path string) ([]domain.Entry, int, error) {
|
||||||
|
repo.Fetches++
|
||||||
|
|
||||||
|
return repo.subRepository.Fetch(ctx, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *spyRepository) Get(ctx context.Context, path string) (*domain.Entry, error) {
|
||||||
|
repo.Gets++
|
||||||
|
|
||||||
|
return repo.subRepository.Get(ctx, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *spyRepository) Update(ctx context.Context, path string, update UpdateFunc) (*domain.Entry, error) {
|
||||||
|
repo.Updates++
|
||||||
|
|
||||||
|
return repo.subRepository.Update(ctx, path, update)
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
UpdateOptions struct {
|
||||||
|
Add *domain.Entry
|
||||||
|
Replace *domain.Entry
|
||||||
|
Delete *domain.Entry
|
||||||
|
}
|
||||||
|
|
||||||
UseCase interface {
|
UseCase interface {
|
||||||
// Create creates a new entry. Returns map or rel links, like Permalink
|
// Create creates a new entry. Returns map or rel links, like Permalink
|
||||||
// or created post, shortcode and syndication.
|
// or created post, shortcode and syndication.
|
||||||
|
@ -16,7 +22,7 @@ type (
|
||||||
// Update updates exist entry properties on provided u.
|
// Update updates exist entry properties on provided u.
|
||||||
//
|
//
|
||||||
// TODO(toby3d): return Location header if entry updates their URL.
|
// TODO(toby3d): return Location header if entry updates their URL.
|
||||||
Update(ctx context.Context, u *url.URL, e domain.Entry) (*domain.Entry, error)
|
Update(ctx context.Context, u *url.URL, options UpdateOptions) (*domain.Entry, error)
|
||||||
|
|
||||||
// Delete destroy entry on provided URL.
|
// Delete destroy entry on provided URL.
|
||||||
Delete(ctx context.Context, u *url.URL) (bool, error)
|
Delete(ctx context.Context, u *url.URL) (bool, error)
|
||||||
|
@ -41,17 +47,17 @@ func NewDummyUseCase() *dummyUseCase {
|
||||||
return &dummyUseCase{}
|
return &dummyUseCase{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dummyUseCase) Create(ctx context.Context, e domain.Entry) (map[string]*url.URL, error) {
|
func (dummyUseCase) Create(_ context.Context, _ domain.Entry) (map[string]*url.URL, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dummyUseCase) Update(ctx context.Context, u *url.URL, e domain.Entry) (*domain.Entry, error) {
|
func (dummyUseCase) Update(_ context.Context, _ *url.URL, _ UpdateOptions) (*domain.Entry, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dummyUseCase) Delete(ctx context.Context, u *url.URL) (bool, error) { return false, nil }
|
func (dummyUseCase) Delete(_ context.Context, _ *url.URL) (bool, error) { return false, nil }
|
||||||
func (dummyUseCase) Undelete(ctx context.Context, u *url.URL) (*domain.Entry, error) { return nil, nil }
|
func (dummyUseCase) Undelete(_ context.Context, _ *url.URL) (*domain.Entry, error) { return nil, nil }
|
||||||
func (dummyUseCase) Source(ctx context.Context, u *url.URL) (*domain.Entry, error) { return nil, nil }
|
func (dummyUseCase) Source(_ context.Context, _ *url.URL) (*domain.Entry, error) { return nil, nil }
|
||||||
|
|
||||||
func NewStubUseCase(err error, e *domain.Entry, ok bool) *stubUseCase {
|
func NewStubUseCase(err error, e *domain.Entry, ok bool) *stubUseCase {
|
||||||
return &stubUseCase{
|
return &stubUseCase{
|
||||||
|
@ -61,22 +67,22 @@ func NewStubUseCase(err error, e *domain.Entry, ok bool) *stubUseCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucase *stubUseCase) Create(ctx context.Context, e domain.Entry) (*domain.Entry, error) {
|
func (ucase *stubUseCase) Create(_ context.Context, _ domain.Entry) (*domain.Entry, error) {
|
||||||
return ucase.entry, ucase.err
|
return ucase.entry, ucase.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucase *stubUseCase) Update(ctx context.Context, u *url.URL, e domain.Entry) (*domain.Entry, error) {
|
func (ucase *stubUseCase) Update(_ context.Context, _ *url.URL, _ UpdateOptions) (*domain.Entry, error) {
|
||||||
return ucase.entry, ucase.err
|
return ucase.entry, ucase.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucase *stubUseCase) Delete(ctx context.Context, u *url.URL) (bool, error) {
|
func (ucase *stubUseCase) Delete(_ context.Context, _ *url.URL) (bool, error) {
|
||||||
return ucase.ok, ucase.err
|
return ucase.ok, ucase.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucase *stubUseCase) Undelete(ctx context.Context, u *url.URL) (*domain.Entry, error) {
|
func (ucase *stubUseCase) Undelete(_ context.Context, _ *url.URL) (*domain.Entry, error) {
|
||||||
return ucase.entry, ucase.err
|
return ucase.entry, ucase.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucase *stubUseCase) Source(ctx context.Context, u *url.URL) (*domain.Entry, error) {
|
func (ucase *stubUseCase) Source(_ context.Context, _ *url.URL) (*domain.Entry, error) {
|
||||||
return ucase.entry, ucase.err
|
return ucase.entry, ucase.err
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,16 @@ type entryUseCase struct {
|
||||||
|
|
||||||
// Create implements entry.UseCase.
|
// Create implements entry.UseCase.
|
||||||
func (ucase *entryUseCase) Create(ctx context.Context, e domain.Entry) (*domain.Entry, error) {
|
func (ucase *entryUseCase) Create(ctx context.Context, e domain.Entry) (*domain.Entry, error) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
if e.CreatedAt.IsZero() {
|
||||||
|
e.CreatedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.UpdatedAt.IsZero() {
|
||||||
|
e.UpdatedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
if err := ucase.entries.Create(ctx, e.URL.RequestURI(), e); err != nil {
|
if err := ucase.entries.Create(ctx, e.URL.RequestURI(), e); err != nil {
|
||||||
return nil, fmt.Errorf("cannot create entry: %w", err)
|
return nil, fmt.Errorf("cannot create entry: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -37,7 +47,7 @@ func (ucase *entryUseCase) Delete(ctx context.Context, u *url.URL) (bool, error)
|
||||||
e.DeletedAt = now
|
e.DeletedAt = now
|
||||||
e.UpdatedAt = now
|
e.UpdatedAt = now
|
||||||
|
|
||||||
return nil, nil
|
return e, nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, fmt.Errorf("cannot undelete entry: %w", err)
|
return false, fmt.Errorf("cannot undelete entry: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -73,14 +83,18 @@ func (ucase *entryUseCase) Undelete(ctx context.Context, u *url.URL) (*domain.En
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update implements entry.UseCase.
|
// Update implements entry.UseCase.
|
||||||
func (ucase *entryUseCase) Update(ctx context.Context, u *url.URL, e domain.Entry) (*domain.Entry, error) {
|
func (ucase *entryUseCase) Update(ctx context.Context, u *url.URL, opts entry.UpdateOptions) (*domain.Entry, error) {
|
||||||
result, err := ucase.entries.Update(ctx, u.RequestURI(), func(_ context.Context, e *domain.Entry) (
|
result, err := ucase.entries.Update(ctx, u.RequestURI(), func(_ context.Context, e *domain.Entry) (
|
||||||
*domain.Entry, error,
|
*domain.Entry, error,
|
||||||
) {
|
) {
|
||||||
e.DeletedAt = time.Time{}
|
e.DeletedAt = time.Time{}
|
||||||
e.UpdatedAt = time.Now().UTC()
|
e.UpdatedAt = time.Now().UTC()
|
||||||
|
|
||||||
return nil, nil
|
// TODO(toby3d): add
|
||||||
|
// TODO(toby3d): update
|
||||||
|
// TODO(toby3d): delete
|
||||||
|
|
||||||
|
return e, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot update entry: %w", err)
|
return nil, fmt.Errorf("cannot update entry: %w", err)
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
package usecase_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
|
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||||
|
"source.toby3d.me/toby3d/pub/internal/entry"
|
||||||
|
"source.toby3d.me/toby3d/pub/internal/entry/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
e := domain.TestEntry(t)
|
||||||
|
repo := entry.NewSpyEntryRepository(entry.NewStubEntryRepository(nil, e, nil, false))
|
||||||
|
|
||||||
|
if _, err := usecase.NewEntryUseCase(repo).Create(context.Background(), *e); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Creates == 0 {
|
||||||
|
t.Error("expect creation call")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
e := domain.TestEntry(t)
|
||||||
|
|
||||||
|
for name, tc := range map[string]struct {
|
||||||
|
options entry.UpdateOptions
|
||||||
|
expect func() *domain.Entry
|
||||||
|
}{
|
||||||
|
"add": {
|
||||||
|
options: entry.UpdateOptions{
|
||||||
|
Add: &domain.Entry{Tags: []string{"indieweb", "testing"}},
|
||||||
|
},
|
||||||
|
expect: func() *domain.Entry {
|
||||||
|
updated := *e
|
||||||
|
updated.Tags = append(updated.Tags, "indieweb", "testing")
|
||||||
|
|
||||||
|
return &updated
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO(toby3d): "update": {},
|
||||||
|
// TODO(toby3d): "delete": {},
|
||||||
|
} {
|
||||||
|
name, tc := name, tc
|
||||||
|
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
expect := tc.expect()
|
||||||
|
repo := entry.NewSpyEntryRepository(entry.NewStubEntryRepository(nil, expect, nil, false))
|
||||||
|
ucase := usecase.NewEntryUseCase(repo)
|
||||||
|
|
||||||
|
out, err := ucase.Update(context.Background(), e.URL, tc.options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(out, expect, cmp.AllowUnexported(e.RSVP)); diff != "" {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
e := domain.TestEntry(t)
|
||||||
|
deleted := *e
|
||||||
|
deleted.DeletedAt = time.Now().UTC()
|
||||||
|
|
||||||
|
repo := entry.NewSpyEntryRepository(entry.NewStubEntryRepository(nil, &deleted, nil, false))
|
||||||
|
|
||||||
|
ok, err := usecase.NewEntryUseCase(repo).Delete(context.Background(), e.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok || repo.Updates == 0 || repo.Deletes != 0 {
|
||||||
|
t.Errorf("expect update call without deleting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUndelete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
e := domain.TestEntry(t)
|
||||||
|
undeleted := *e
|
||||||
|
undeleted.DeletedAt = time.Now().UTC().AddDate(0, 0, -7)
|
||||||
|
|
||||||
|
repo := entry.NewSpyEntryRepository(entry.NewStubEntryRepository(nil, e, nil, false))
|
||||||
|
|
||||||
|
if _, err := usecase.NewEntryUseCase(repo).Undelete(context.Background(), e.URL); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Updates == 0 {
|
||||||
|
t.Error("expect update call")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSource(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
e := domain.TestEntry(t)
|
||||||
|
repo := entry.NewSpyEntryRepository(entry.NewStubEntryRepository(nil, e, nil, false))
|
||||||
|
|
||||||
|
if _, err := usecase.NewEntryUseCase(repo).Source(context.Background(), e.URL); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Gets == 0 {
|
||||||
|
t.Error("expect getting call")
|
||||||
|
}
|
||||||
|
}
|
|
@ -107,26 +107,26 @@ func NewSpyMediaRepository(subRepo Repository) *spyRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *spyRepository) Create(_ context.Context, _ string, _ domain.File) error {
|
func (repo *spyRepository) Create(ctx context.Context, path string, f domain.File) error {
|
||||||
repo.Creates++
|
repo.Creates++
|
||||||
|
|
||||||
return repo.subRepository.Create(context.TODO(), "", domain.File{})
|
return repo.subRepository.Create(ctx, path, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *spyRepository) Get(_ context.Context, _ string) (*domain.File, error) {
|
func (repo *spyRepository) Get(ctx context.Context, path string) (*domain.File, error) {
|
||||||
repo.Gets++
|
repo.Gets++
|
||||||
|
|
||||||
return repo.subRepository.Get(context.TODO(), "")
|
return repo.subRepository.Get(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *spyRepository) Update(_ context.Context, _ string, _ UpdateFunc) error {
|
func (repo *spyRepository) Update(ctx context.Context, path string, update UpdateFunc) error {
|
||||||
repo.Updates++
|
repo.Updates++
|
||||||
|
|
||||||
return repo.subRepository.Update(context.TODO(), "", nil)
|
return repo.subRepository.Update(ctx, path, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *spyRepository) Delete(_ context.Context, _ string) error {
|
func (repo *spyRepository) Delete(ctx context.Context, path string) error {
|
||||||
repo.Deletes++
|
repo.Deletes++
|
||||||
|
|
||||||
return repo.subRepository.Delete(context.TODO(), "")
|
return repo.subRepository.Delete(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue