Initial commit
This commit is contained in:
commit
afed023152
8152 files changed
+2086461
No files matched your search
@@ -0,0 +1,231 @@
|
||||
package items_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/mats/fullstack-template/internal/items"
|
||||
)
|
||||
|
||||
// fakeRepo is an in-memory items.Repository — the point of the Repository
|
||||
// interface is that these tests never touch a database.
|
||||
type fakeRepo struct {
|
||||
mu sync.Mutex
|
||||
nextID int64
|
||||
byID map[int64]items.Item
|
||||
}
|
||||
|
||||
func newFakeRepo() *fakeRepo {
|
||||
return &fakeRepo{nextID: 1, byID: map[int64]items.Item{}}
|
||||
}
|
||||
|
||||
func (f *fakeRepo) List(_ context.Context, fl items.Filter) ([]items.Item, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
ids := make([]int64, 0, len(f.byID))
|
||||
for id := range f.byID {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
|
||||
out := []items.Item{}
|
||||
for _, id := range ids {
|
||||
item := f.byID[id]
|
||||
if fl.Status != "" && item.Status != fl.Status {
|
||||
continue
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
if fl.Offset < len(out) {
|
||||
out = out[fl.Offset:]
|
||||
} else {
|
||||
out = []items.Item{}
|
||||
}
|
||||
if fl.Limit < len(out) {
|
||||
out = out[:fl.Limit]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepo) Get(_ context.Context, id int64) (items.Item, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
item, ok := f.byID[id]
|
||||
if !ok {
|
||||
return items.Item{}, items.ErrNotFound
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepo) Create(_ context.Context, item *items.Item) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
item.ID = f.nextID
|
||||
f.nextID++
|
||||
now := time.Now()
|
||||
item.CreatedAt, item.UpdatedAt = now, now
|
||||
f.byID[item.ID] = *item
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRepo) Update(_ context.Context, item *items.Item) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if _, ok := f.byID[item.ID]; !ok {
|
||||
return items.ErrNotFound
|
||||
}
|
||||
item.UpdatedAt = time.Now()
|
||||
f.byID[item.ID] = *item
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRepo) Delete(_ context.Context, id int64) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if _, ok := f.byID[id]; !ok {
|
||||
return items.ErrNotFound
|
||||
}
|
||||
delete(f.byID, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newService() (*items.Service, *fakeRepo) {
|
||||
repo := newFakeRepo()
|
||||
return items.NewService(repo), repo
|
||||
}
|
||||
|
||||
func TestCreateValidation(t *testing.T) {
|
||||
svc, _ := newService()
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in items.CreateInput
|
||||
want string // expected ValidationError.Field
|
||||
}{
|
||||
{"empty title", items.CreateInput{Title: " "}, "title"},
|
||||
{"long title", items.CreateInput{Title: strings.Repeat("x", 201)}, "title"},
|
||||
{"bad status", items.CreateInput{Title: "ok", Status: "nope"}, "status"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := svc.Create(ctx, tt.in)
|
||||
var vErr *items.ValidationError
|
||||
if !errors.As(err, &vErr) {
|
||||
t.Fatalf("got error %v, want *items.ValidationError", err)
|
||||
}
|
||||
if vErr.Field != tt.want {
|
||||
t.Errorf("field = %q, want %q", vErr.Field, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDefaults(t *testing.T) {
|
||||
svc, _ := newService()
|
||||
|
||||
item, err := svc.Create(context.Background(), items.CreateInput{Title: " hello "})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if item.Title != "hello" {
|
||||
t.Errorf("title = %q, want trimmed %q", item.Title, "hello")
|
||||
}
|
||||
if item.Status != items.StatusActive {
|
||||
t.Errorf("status = %q, want default %q", item.Status, items.StatusActive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatchesOnlyProvidedFields(t *testing.T) {
|
||||
svc, _ := newService()
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := svc.Create(ctx, items.CreateInput{Title: "original", Description: "keep me"})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
|
||||
archived := items.StatusArchived
|
||||
updated, err := svc.Update(ctx, created.ID, items.UpdateInput{Status: &archived})
|
||||
if err != nil {
|
||||
t.Fatalf("Update: %v", err)
|
||||
}
|
||||
if updated.Status != items.StatusArchived {
|
||||
t.Errorf("status = %q, want %q", updated.Status, items.StatusArchived)
|
||||
}
|
||||
if updated.Title != "original" || updated.Description != "keep me" {
|
||||
t.Errorf("untouched fields changed: %+v", updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRequiresField(t *testing.T) {
|
||||
svc, _ := newService()
|
||||
ctx := context.Background()
|
||||
|
||||
created, _ := svc.Create(ctx, items.CreateInput{Title: "x"})
|
||||
_, err := svc.Update(ctx, created.ID, items.UpdateInput{})
|
||||
var vErr *items.ValidationError
|
||||
if !errors.As(err, &vErr) || vErr.Field != "body" {
|
||||
t.Fatalf("got %v, want validation error on body", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNotFound(t *testing.T) {
|
||||
svc, _ := newService()
|
||||
title := "x"
|
||||
_, err := svc.Update(context.Background(), 999, items.UpdateInput{Title: &title})
|
||||
if !errors.Is(err, items.ErrNotFound) {
|
||||
t.Fatalf("got %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFilterAndPaging(t *testing.T) {
|
||||
svc, _ := newService()
|
||||
ctx := context.Background()
|
||||
|
||||
svc.Create(ctx, items.CreateInput{Title: "one"})
|
||||
svc.Create(ctx, items.CreateInput{Title: "two", Status: items.StatusArchived})
|
||||
svc.Create(ctx, items.CreateInput{Title: "three", Status: items.StatusArchived})
|
||||
|
||||
got, err := svc.List(ctx, items.Filter{Status: items.StatusArchived})
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(got))
|
||||
}
|
||||
|
||||
if _, err := svc.List(ctx, items.Filter{Status: "bogus"}); err == nil {
|
||||
t.Error("List with bogus status: want validation error")
|
||||
}
|
||||
|
||||
got, err = svc.List(ctx, items.Filter{Limit: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(got) != items.DefaultLimit && len(got) != 1 {
|
||||
t.Fatalf("len = %d, want 1", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFlow(t *testing.T) {
|
||||
svc, _ := newService()
|
||||
ctx := context.Background()
|
||||
|
||||
item, _ := svc.Create(ctx, items.CreateInput{Title: "gone"})
|
||||
if err := svc.Delete(ctx, item.ID); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
if _, err := svc.Get(ctx, item.ID); !errors.Is(err, items.ErrNotFound) {
|
||||
t.Fatalf("Get after delete: got %v, want ErrNotFound", err)
|
||||
}
|
||||
if err := svc.Delete(ctx, item.ID); !errors.Is(err, items.ErrNotFound) {
|
||||
t.Fatalf("second delete: got %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user