Build image / build-and-push (push) Successful in 17s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
491 lines
17 KiB
Go
491 lines
17 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
|
||
"gitea.stevedudenhoeffer.com/steve/pansy/internal/domain"
|
||
)
|
||
|
||
// seedGarden creates a garden owned by owner and returns it.
|
||
func seedGarden(t *testing.T, s *Service, owner int64) *domain.Garden {
|
||
t.Helper()
|
||
g, err := s.CreateGarden(context.Background(), owner, GardenInput{Name: "Yard", WidthCM: 1000, HeightCM: 1000})
|
||
if err != nil {
|
||
t.Fatalf("seed garden: %v", err)
|
||
}
|
||
return g
|
||
}
|
||
|
||
func ptrBool(b bool) *bool { return &b }
|
||
func strPtr(s string) *string { return &s }
|
||
|
||
func TestCreateObjectDefaults(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
|
||
// A bed at the garden center; no shape/plantable given.
|
||
o, err := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, Name: " Bed 1 ", XCM: 500, YCM: 500, WidthCM: 200, HeightCM: 100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreateObject: %v", err)
|
||
}
|
||
if o.Shape != domain.ShapeRect {
|
||
t.Errorf("shape = %q, want default rect", o.Shape)
|
||
}
|
||
if !o.Plantable {
|
||
t.Error("a bed should default to plantable")
|
||
}
|
||
if o.Name != "Bed 1" {
|
||
t.Errorf("name = %q, want trimmed", o.Name)
|
||
}
|
||
if o.Version != 1 || o.GardenID != g.ID {
|
||
t.Errorf("unexpected object: %+v", o)
|
||
}
|
||
}
|
||
|
||
func TestObjectGridSettings(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
ctx := context.Background()
|
||
|
||
// An omitted grid size defaults; snap defaults off.
|
||
o, err := s.CreateObject(ctx, owner, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 200, HeightCM: 100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreateObject: %v", err)
|
||
}
|
||
if o.GridSizeCM != defaultObjectGridCM {
|
||
t.Errorf("grid = %v, want default %d", o.GridSizeCM, defaultObjectGridCM)
|
||
}
|
||
if o.SnapToGrid {
|
||
t.Error("snap should default off")
|
||
}
|
||
|
||
// Patching grid + snap round-trips and leaves other fields untouched.
|
||
grid := 15.0
|
||
snap := true
|
||
up, err := s.UpdateObject(ctx, owner, o.ID, ObjectPatch{GridSizeCM: &grid, SnapToGrid: &snap}, o.Version)
|
||
if err != nil {
|
||
t.Fatalf("UpdateObject: %v", err)
|
||
}
|
||
if up.GridSizeCM != 15 || !up.SnapToGrid {
|
||
t.Errorf("after patch grid=%v snap=%v, want 15/true", up.GridSizeCM, up.SnapToGrid)
|
||
}
|
||
if up.WidthCM != 200 || up.HeightCM != 100 {
|
||
t.Errorf("a grid patch changed dimensions: %vx%v", up.WidthCM, up.HeightCM)
|
||
}
|
||
}
|
||
|
||
func TestCreateObjectPlantableByKind(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
|
||
tree, err := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindTree, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create tree: %v", err)
|
||
}
|
||
if tree.Plantable {
|
||
t.Error("a tree should default to not plantable")
|
||
}
|
||
|
||
// Override: a plantable path.
|
||
path, err := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindPath, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, Plantable: ptrBool(true),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("create path: %v", err)
|
||
}
|
||
if !path.Plantable {
|
||
t.Error("plantable override should win")
|
||
}
|
||
}
|
||
|
||
func TestCreateObjectRotationNormalized(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
|
||
o, err := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, RotationDeg: 450,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreateObject: %v", err)
|
||
}
|
||
if o.RotationDeg != 90 {
|
||
t.Errorf("rotation 450 normalized to %v, want 90", o.RotationDeg)
|
||
}
|
||
}
|
||
|
||
func TestCreateObjectValidation(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner) // 1000×1000
|
||
|
||
bad := []ObjectInput{
|
||
{Kind: "spaceship", XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100}, // bad kind
|
||
{Kind: domain.KindBed, Shape: domain.ShapePolygon, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100}, // polygon reserved
|
||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 0, HeightCM: 100}, // zero width
|
||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: -5, HeightCM: 100}, // negative
|
||
{Kind: domain.KindBed, XCM: 5000, YCM: 5000, WidthCM: 100, HeightCM: 100}, // fully off-field
|
||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, Color: strPtr("nope")}, // bad color
|
||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, Props: strPtr("{not json")}, // bad props
|
||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, Name: strings.Repeat("x", maxObjectNameLen+1)}, // name too long
|
||
{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100, GridSizeCM: maxGardenCM + 1}, // grid over the cap
|
||
}
|
||
for i, in := range bad {
|
||
if _, err := s.CreateObject(context.Background(), owner, g.ID, in); !errors.Is(err, domain.ErrInvalidInput) {
|
||
t.Errorf("case %d: err = %v, want ErrInvalidInput", i, err)
|
||
}
|
||
}
|
||
|
||
// Partial overhang IS allowed (center near the edge).
|
||
if _, err := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, XCM: 990, YCM: 990, WidthCM: 200, HeightCM: 200,
|
||
}); err != nil {
|
||
t.Errorf("overhang at the edge should be allowed: %v", err)
|
||
}
|
||
|
||
// A valid hex color and valid props JSON pass.
|
||
if _, err := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100,
|
||
Color: strPtr("#3f8f4f"), Props: strPtr(`{"heightCm":40}`),
|
||
}); err != nil {
|
||
t.Errorf("valid color+props should pass: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestUpdateObjectPartialAndVersion(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
o, _ := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, Name: "Bed", XCM: 100, YCM: 100, WidthCM: 100, HeightCM: 100,
|
||
})
|
||
|
||
// Move + rotate only; name/dimensions untouched.
|
||
nx, ny, rot := 300.0, 400.0, -90.0
|
||
updated, err := s.UpdateObject(context.Background(), owner, o.ID, ObjectPatch{XCM: &nx, YCM: &ny, RotationDeg: &rot}, o.Version)
|
||
if err != nil {
|
||
t.Fatalf("UpdateObject: %v", err)
|
||
}
|
||
if updated.XCM != 300 || updated.YCM != 400 {
|
||
t.Errorf("move didn't apply: %+v", updated)
|
||
}
|
||
if updated.RotationDeg != 270 {
|
||
t.Errorf("rotation -90 normalized to %v, want 270", updated.RotationDeg)
|
||
}
|
||
if updated.Name != "Bed" || updated.WidthCM != 100 {
|
||
t.Errorf("untouched fields changed: %+v", updated)
|
||
}
|
||
if updated.Version != o.Version+1 {
|
||
t.Errorf("version = %d, want %d", updated.Version, o.Version+1)
|
||
}
|
||
|
||
// Stale version → conflict + current row.
|
||
current, err := s.UpdateObject(context.Background(), owner, o.ID, ObjectPatch{XCM: &nx}, o.Version)
|
||
if !errors.Is(err, domain.ErrVersionConflict) {
|
||
t.Fatalf("stale update err = %v, want ErrVersionConflict", err)
|
||
}
|
||
if current == nil || current.Version != 2 {
|
||
t.Errorf("conflict didn't return current row: %+v", current)
|
||
}
|
||
}
|
||
|
||
func TestObjectCrossUserIsNotFound(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
alice := seedUser(t, s, "[email protected]")
|
||
bob := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, alice)
|
||
o, _ := s.CreateObject(context.Background(), alice, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100,
|
||
})
|
||
|
||
nx := 1.0
|
||
if _, err := s.CreateObject(context.Background(), bob, g.ID, ObjectInput{Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100}); !errors.Is(err, domain.ErrNotFound) {
|
||
t.Errorf("bob create err = %v, want ErrNotFound", err)
|
||
}
|
||
if _, err := s.UpdateObject(context.Background(), bob, o.ID, ObjectPatch{XCM: &nx}, o.Version); !errors.Is(err, domain.ErrNotFound) {
|
||
t.Errorf("bob update err = %v, want ErrNotFound", err)
|
||
}
|
||
if err := s.DeleteObject(context.Background(), bob, o.ID); !errors.Is(err, domain.ErrNotFound) {
|
||
t.Errorf("bob delete err = %v, want ErrNotFound", err)
|
||
}
|
||
if _, err := s.GardenFull(context.Background(), bob, g.ID, nil); !errors.Is(err, domain.ErrNotFound) {
|
||
t.Errorf("bob /full err = %v, want ErrNotFound", err)
|
||
}
|
||
}
|
||
|
||
func TestDeleteObject(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
o, _ := s.CreateObject(context.Background(), owner, g.ID, ObjectInput{
|
||
Kind: domain.KindBed, XCM: 500, YCM: 500, WidthCM: 100, HeightCM: 100,
|
||
})
|
||
|
||
if err := s.DeleteObject(context.Background(), owner, o.ID); err != nil {
|
||
t.Fatalf("DeleteObject: %v", err)
|
||
}
|
||
full, err := s.GardenFull(context.Background(), owner, g.ID, nil)
|
||
if err != nil {
|
||
t.Fatalf("GardenFull: %v", err)
|
||
}
|
||
if len(full.Objects) != 0 {
|
||
t.Errorf("object still present after delete: %d", len(full.Objects))
|
||
}
|
||
}
|
||
|
||
func TestGardenFullShape(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
s.CreateObject(context.Background(), owner, g.ID, ObjectInput{Kind: domain.KindBed, XCM: 300, YCM: 300, WidthCM: 100, HeightCM: 100})
|
||
s.CreateObject(context.Background(), owner, g.ID, ObjectInput{Kind: domain.KindContainer, Shape: domain.ShapeCircle, XCM: 600, YCM: 600, WidthCM: 60, HeightCM: 60})
|
||
|
||
full, err := s.GardenFull(context.Background(), owner, g.ID, nil)
|
||
if err != nil {
|
||
t.Fatalf("GardenFull: %v", err)
|
||
}
|
||
if full.Garden == nil || full.Garden.ID != g.ID {
|
||
t.Error("full.Garden missing")
|
||
}
|
||
if len(full.Objects) != 2 {
|
||
t.Errorf("objects = %d, want 2", len(full.Objects))
|
||
}
|
||
// Plantings/plants are empty (non-nil) until #14.
|
||
if full.Plantings == nil || len(full.Plantings) != 0 {
|
||
t.Errorf("plantings = %v, want empty non-nil", full.Plantings)
|
||
}
|
||
if full.Plants == nil || len(full.Plants) != 0 {
|
||
t.Errorf("plants = %v, want empty non-nil", full.Plants)
|
||
}
|
||
}
|
||
|
||
// TestSeasonViewIncludesRemovedAndSpanningPlantings — a season is a date range
|
||
// over data that already existed; the point is seeing what WAS there.
|
||
func TestSeasonViewIncludesRemovedAndSpanningPlantings(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
bed := seedBed(t, s, owner, g.ID)
|
||
plant := seedOwnPlant(t, s, owner, 15)
|
||
ctx := context.Background()
|
||
|
||
// Planted and pulled inside 2025.
|
||
past, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{
|
||
PlantID: plant.ID, XCM: -50, YCM: 0, RadiusCM: 20, PlantedAt: strPtr("2025-04-01"),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreatePlanting: %v", err)
|
||
}
|
||
if _, err := s.UpdatePlanting(ctx, owner, past.ID, PlantingPatch{
|
||
SetRemovedAt: true, RemovedAt: strPtr("2025-09-01"),
|
||
}, past.Version); err != nil {
|
||
t.Fatalf("remove: %v", err)
|
||
}
|
||
|
||
// Garlic: in the ground October 2025, pulled July 2026. Belongs to both.
|
||
spanning, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{
|
||
PlantID: plant.ID, XCM: 0, YCM: 0, RadiusCM: 20, PlantedAt: strPtr("2025-10-15"),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreatePlanting: %v", err)
|
||
}
|
||
if _, err := s.UpdatePlanting(ctx, owner, spanning.ID, PlantingPatch{
|
||
SetRemovedAt: true, RemovedAt: strPtr("2026-07-10"),
|
||
}, spanning.Version); err != nil {
|
||
t.Fatalf("remove: %v", err)
|
||
}
|
||
|
||
// Undated: predates the feature, belongs to every year.
|
||
undated, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{
|
||
PlantID: plant.ID, XCM: 50, YCM: 0, RadiusCM: 20, PlantedAt: strPtr("2025-01-01"),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreatePlanting: %v", err)
|
||
}
|
||
if _, err := s.UpdatePlanting(ctx, owner, undated.ID, PlantingPatch{
|
||
SetPlantedAt: true, PlantedAt: nil,
|
||
}, undated.Version); err != nil {
|
||
t.Fatalf("clear planted_at: %v", err)
|
||
}
|
||
|
||
ids := func(year int) map[int64]bool {
|
||
t.Helper()
|
||
full, err := s.GardenFull(ctx, owner, g.ID, &year)
|
||
if err != nil {
|
||
t.Fatalf("GardenFull(%d): %v", year, err)
|
||
}
|
||
out := map[int64]bool{}
|
||
for _, p := range full.Plantings {
|
||
out[p.ID] = true
|
||
}
|
||
return out
|
||
}
|
||
|
||
y2025 := ids(2025)
|
||
if !y2025[past.ID] || !y2025[spanning.ID] || !y2025[undated.ID] {
|
||
t.Errorf("2025 = %v, want all three", y2025)
|
||
}
|
||
y2026 := ids(2026)
|
||
if y2026[past.ID] {
|
||
t.Error("a planting pulled in 2025 showed up in 2026")
|
||
}
|
||
if !y2026[spanning.ID] {
|
||
t.Error("a planting spanning the year boundary is missing from 2026")
|
||
}
|
||
if !y2026[undated.ID] {
|
||
t.Error("an undated planting must appear in every year")
|
||
}
|
||
y2024 := ids(2024)
|
||
if y2024[past.ID] || y2024[spanning.ID] {
|
||
t.Errorf("2024 predates both dated plantings but returned %v", y2024)
|
||
}
|
||
if !y2024[undated.ID] {
|
||
t.Error("an undated planting must appear in every year, including 2024")
|
||
}
|
||
|
||
// Without a year, /full is exactly what it always was: active plops only.
|
||
now, err := s.GardenFull(ctx, owner, g.ID, nil)
|
||
if err != nil {
|
||
t.Fatalf("GardenFull(now): %v", err)
|
||
}
|
||
if len(now.Plantings) != 1 || now.Plantings[0].ID != undated.ID {
|
||
t.Errorf("live view = %+v, want only the one still in the ground", now.Plantings)
|
||
}
|
||
|
||
// A past season's plops must still render, so their plants come along even
|
||
// though none of them is active.
|
||
if len(ids(2025)) > 0 {
|
||
y := 2025
|
||
full, _ := s.GardenFull(ctx, owner, g.ID, &y)
|
||
if len(full.Plants) == 0 {
|
||
t.Error("season view returned plops with no plants to render them")
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestGardenYearsOffersOnlyYearsWithData
|
||
func TestGardenYearsOffersOnlyYearsWithData(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
bed := seedBed(t, s, owner, g.ID)
|
||
plant := seedOwnPlant(t, s, owner, 15)
|
||
ctx := context.Background()
|
||
|
||
pl, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{
|
||
PlantID: plant.ID, XCM: 0, YCM: 0, RadiusCM: 20, PlantedAt: strPtr("2023-05-01"),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreatePlanting: %v", err)
|
||
}
|
||
if _, err := s.UpdatePlanting(ctx, owner, pl.ID, PlantingPatch{
|
||
SetRemovedAt: true, RemovedAt: strPtr("2024-06-01"),
|
||
}, pl.Version); err != nil {
|
||
t.Fatalf("remove: %v", err)
|
||
}
|
||
|
||
years, err := s.GardenYears(ctx, owner, g.ID)
|
||
if err != nil {
|
||
t.Fatalf("GardenYears: %v", err)
|
||
}
|
||
current := s.now().UTC().Year()
|
||
want := map[int]bool{2023: true, 2024: true, current: true}
|
||
if len(years) != len(want) {
|
||
t.Fatalf("years = %v, want exactly %v", years, want)
|
||
}
|
||
for _, y := range years {
|
||
if !want[y] {
|
||
t.Errorf("unexpected year %d in %v", y, years)
|
||
}
|
||
}
|
||
// Newest first, so the selector reads in the order people scan.
|
||
for i := 1; i < len(years); i++ {
|
||
if years[i-1] < years[i] {
|
||
t.Errorf("years not newest-first: %v", years)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestSeasonYearIsBounded — a typo'd year should be refused, not answered with a
|
||
// confidently empty garden.
|
||
func TestSeasonYearIsBounded(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
ctx := context.Background()
|
||
|
||
for _, y := range []int{0, 202, 20260, -5} {
|
||
if _, err := s.GardenFull(ctx, owner, g.ID, &y); !errors.Is(err, domain.ErrInvalidInput) {
|
||
t.Errorf("year %d accepted (err=%v)", y, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestSeasonPlantsAreScopedToTheYear — the plant list has to match the plops it
|
||
// is there to render. A garden with a decade of history shouldn't load a decade
|
||
// of catalog to draw one season, and a season shouldn't carry plants that had
|
||
// nothing in the ground that year.
|
||
func TestSeasonPlantsAreScopedToTheYear(t *testing.T) {
|
||
s := newTestService(t, openConfig())
|
||
owner := seedUser(t, s, "[email protected]")
|
||
g := seedGarden(t, s, owner)
|
||
bed := seedBed(t, s, owner, g.ID)
|
||
ctx := context.Background()
|
||
|
||
oldPlant := seedOwnPlant(t, s, owner, 15)
|
||
newPlant, err := s.CreatePlant(ctx, owner, PlantInput{
|
||
Name: "Later", Category: domain.CategoryHerb, SpacingCM: 20, Color: "#4a7c3f", Icon: "🌱",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreatePlant: %v", err)
|
||
}
|
||
|
||
plant2020, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{
|
||
PlantID: oldPlant.ID, XCM: -40, YCM: 0, RadiusCM: 20, PlantedAt: strPtr("2020-04-01"),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("CreatePlanting: %v", err)
|
||
}
|
||
if _, err := s.UpdatePlanting(ctx, owner, plant2020.ID, PlantingPatch{
|
||
SetRemovedAt: true, RemovedAt: strPtr("2020-09-01"),
|
||
}, plant2020.Version); err != nil {
|
||
t.Fatalf("remove: %v", err)
|
||
}
|
||
if _, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{
|
||
PlantID: newPlant.ID, XCM: 40, YCM: 0, RadiusCM: 20, PlantedAt: strPtr("2026-04-01"),
|
||
}); err != nil {
|
||
t.Fatalf("CreatePlanting: %v", err)
|
||
}
|
||
|
||
names := func(year int) []string {
|
||
t.Helper()
|
||
full, err := s.GardenFull(ctx, owner, g.ID, &year)
|
||
if err != nil {
|
||
t.Fatalf("GardenFull(%d): %v", year, err)
|
||
}
|
||
out := []string{}
|
||
for _, p := range full.Plants {
|
||
out = append(out, p.Name)
|
||
}
|
||
return out
|
||
}
|
||
if got := names(2020); len(got) != 1 || got[0] != oldPlant.Name {
|
||
t.Errorf("2020 plants = %v, want just %q", got, oldPlant.Name)
|
||
}
|
||
if got := names(2026); len(got) != 1 || got[0] != "Later" {
|
||
t.Errorf("2026 plants = %v, want just \"Later\"", got)
|
||
}
|
||
}
|