diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-08-05 19:25:03 -0230 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-08-05 19:25:03 -0230 |
| commit | a2668014405c27671adeda4921431734afa8bec1 (patch) | |
| tree | 87fae4038552c90372ae7a8a0887e3ac8cef4aad /lulutest | |
| parent | abd51689cef03fa3daecb9abbb54b762e3ab02ac (diff) | |
| download | lulu-a2668014405c27671adeda4921431734afa8bec1.zip | |
Diffstat (limited to 'lulutest')
| -rw-r--r-- | lulutest/lulutest.go | 2 | ||||
| -rw-r--r-- | lulutest/mock.go | 58 | ||||
| -rw-r--r-- | lulutest/mock_test.go | 315 |
3 files changed, 355 insertions, 20 deletions
diff --git a/lulutest/lulutest.go b/lulutest/lulutest.go index 273128b..707a77d 100644 --- a/lulutest/lulutest.go +++ b/lulutest/lulutest.go @@ -11,7 +11,7 @@ import ( var ( DummyEmailAddr = lulu.MustParseEmailAddress("test@example.com") DummyPkgId = lulu.PkgId{lulu.UsTrade, lulu.Mono, lulu.Standard, lulu.Perfect, lulu.P60UncoatedWhite, lulu.Matte, lulu.NoLinen, lulu.NoFoil} - DummyShipAddr = lulu.ShippingAddress{ + DummyShipAddr = lulu.ShippingAddress{ City: "Lübeck", CountryCode: "DE", Name: "Hans Dampf", diff --git a/lulutest/mock.go b/lulutest/mock.go index 15d3e36..95406d6 100644 --- a/lulutest/mock.go +++ b/lulutest/mock.go @@ -13,7 +13,7 @@ import ( "git.samanthony.xyz/lulu" ) -type mockClient struct { +type MockClient struct { mu sync.Mutex ints []lulu.InteriorValidation covs []lulu.CoverValidation @@ -21,13 +21,13 @@ type mockClient struct { printables map[lulu.PrintableId]lulu.Printable } -func NewMockClient() lulu.Client { - return &mockClient{ +func NewMockClient() *MockClient { + return &MockClient{ printables: make(map[lulu.PrintableId]lulu.Printable), } } -func (c *mockClient) StartInteriorValidation(srcUrl string, mfg lulu.PkgId) (uint, error) { +func (c *MockClient) StartInteriorValidation(srcUrl string, mfg lulu.PkgId) (uint, error) { c.mu.Lock() defer c.mu.Unlock() id := uint(len(c.ints)) @@ -36,11 +36,11 @@ func (c *mockClient) StartInteriorValidation(srcUrl string, mfg lulu.PkgId) (uin return id, nil } -func (c *mockClient) StartInteriorValidationBasic(srcUrl string) (uint, error) { +func (c *MockClient) StartInteriorValidationBasic(srcUrl string) (uint, error) { return c.StartInteriorValidation(srcUrl, lulu.PkgId{}) } -func (c *mockClient) GetInteriorValidation(id uint) (lulu.InteriorValidation, error) { +func (c *MockClient) GetInteriorValidation(id uint) (lulu.InteriorValidation, error) { c.mu.Lock() defer c.mu.Unlock() if id >= uint(len(c.ints)) { @@ -49,11 +49,11 @@ func (c *mockClient) GetInteriorValidation(id uint) (lulu.InteriorValidation, er return c.ints[id], nil } -func (c *mockClient) CoverDimensions(mfg lulu.PkgId, npages uint, unit lulu.Unit) (lulu.CoverDimensions, error) { +func (c *MockClient) CoverDimensions(mfg lulu.PkgId, npages uint, unit lulu.Unit) (lulu.CoverDimensions, error) { return lulu.CoverDimensions{123.4, 56.78, unit}, nil } -func (c *mockClient) StartCoverValidation(srcUrl string, mfg lulu.PkgId, npages uint) (uint, error) { +func (c *MockClient) StartCoverValidation(srcUrl string, mfg lulu.PkgId, npages uint) (uint, error) { c.mu.Lock() defer c.mu.Unlock() id := uint(len(c.covs)) @@ -62,7 +62,7 @@ func (c *mockClient) StartCoverValidation(srcUrl string, mfg lulu.PkgId, npages return id, nil } -func (c *mockClient) GetCoverValidation(id uint) (lulu.CoverValidation, error) { +func (c *MockClient) GetCoverValidation(id uint) (lulu.CoverValidation, error) { c.mu.Lock() defer c.mu.Unlock() if id >= uint(len(c.covs)) { @@ -71,7 +71,7 @@ func (c *mockClient) GetCoverValidation(id uint) (lulu.CoverValidation, error) { return c.covs[id], nil } -func (c *mockClient) Cost(items []lulu.CostLineItem, addr lulu.ShippingAddress, opt lulu.ShippingLevel) (lulu.Cost, lulu.ShippingAddressValidation, error) { +func (c *MockClient) Cost(items []lulu.CostLineItem, addr lulu.ShippingAddress, opt lulu.ShippingLevel) (lulu.Cost, lulu.ShippingAddressValidation, error) { var cost lulu.Cost var subtotal decimal.Decimal var tax decimal.Decimal @@ -122,12 +122,21 @@ func (c *mockClient) Cost(items []lulu.CostLineItem, addr lulu.ShippingAddress, type job struct { j <-chan lulu.PrintJob cancel chan chan bool + quit chan struct{} } +const ( + shipDelay = time.Hour + deliverDelay = 56 * time.Hour +) + func print(id uint64, eid string, contact lulu.EmailAddress, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, addrVal lulu.ShippingAddressValidation, items []lulu.LineItem, cost lulu.Cost) job { jc := make(chan lulu.PrintJob) cancel := make(chan chan bool) + quit := make(chan struct{}) go func() { + defer close(jc) + defer close(cancel) j := lulu.PrintJob{ Contact: contact, Cost: cost, @@ -158,8 +167,8 @@ func print(id uint64, eid string, contact lulu.EmailAddress, delay time.Duration } setStatus(lulu.OrderProductionDelayed) prod := time.After(delay) - ship := time.After(delay + time.Hour) - arrive := time.After(delay + time.Hour + 56*time.Hour) + ship := time.After(delay + shipDelay) + arrive := time.After(delay + shipDelay + deliverDelay) for { select { case jc <- j: @@ -182,13 +191,24 @@ func print(id uint64, eid string, contact lulu.EmailAddress, delay time.Duration if j.Status.Status == lulu.OrderShipped { setStatus(lulu.OrderDelivered) } + case <-quit: + return } } }() - return job{jc, cancel} + return job{jc, cancel, quit} +} + +// Close terminates the client's goroutines. +func (c *MockClient) Close() { + c.mu.Lock() + defer c.mu.Unlock() + for _, j := range c.jobs { + close(j.quit) + } } -func (c *mockClient) Print(contact lulu.EmailAddress, eid string, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, items []lulu.Printable) (lulu.PrintJob, error) { +func (c *MockClient) Print(contact lulu.EmailAddress, eid string, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, items []lulu.Printable) (lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() @@ -213,7 +233,7 @@ func (c *mockClient) Print(contact lulu.EmailAddress, eid string, delay time.Dur return c.print(id, eid, contact, delay, addr, opt, lineItems, costItems) } -func (c *mockClient) Reprint(contact lulu.EmailAddress, eid string, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, items []lulu.Reprintable) (lulu.PrintJob, error) { +func (c *MockClient) Reprint(contact lulu.EmailAddress, eid string, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, items []lulu.Reprintable) (lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() @@ -241,7 +261,7 @@ func (c *mockClient) Reprint(contact lulu.EmailAddress, eid string, delay time.D return c.print(id, eid, contact, delay, addr, opt, lineItems, costItems) } -func (c *mockClient) print(id uint64, eid string, contact lulu.EmailAddress, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, lineItems []lulu.LineItem, costItems []lulu.CostLineItem) (lulu.PrintJob, error) { +func (c *MockClient) print(id uint64, eid string, contact lulu.EmailAddress, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, lineItems []lulu.LineItem, costItems []lulu.CostLineItem) (lulu.PrintJob, error) { cost, addrVal, err := c.Cost(costItems, addr, opt) if err != nil { return lulu.PrintJob{}, err @@ -251,7 +271,7 @@ func (c *mockClient) print(id uint64, eid string, contact lulu.EmailAddress, del return <-j.j, nil } -func (c *mockClient) Job(id uint64) (lulu.PrintJob, error) { +func (c *MockClient) Job(id uint64) (lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() if id < uint64(len(c.jobs)) { @@ -261,7 +281,7 @@ func (c *mockClient) Job(id uint64) (lulu.PrintJob, error) { } // Ignores queries -func (c *mockClient) Jobs(qs ...lulu.PrintJobQuery) ([]lulu.PrintJob, error) { +func (c *MockClient) Jobs(qs ...lulu.PrintJobQuery) ([]lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() js := make([]lulu.PrintJob, len(c.jobs)) @@ -271,7 +291,7 @@ func (c *mockClient) Jobs(qs ...lulu.PrintJobQuery) ([]lulu.PrintJob, error) { return js, nil } -func (c *mockClient) Cancel(id uint64) error { +func (c *MockClient) Cancel(id uint64) error { c.mu.Lock() defer c.mu.Unlock() if id < uint64(len(c.jobs)) { diff --git a/lulutest/mock_test.go b/lulutest/mock_test.go new file mode 100644 index 0000000..c1d4b89 --- /dev/null +++ b/lulutest/mock_test.go @@ -0,0 +1,315 @@ +package lulutest + +import ( + "fmt" + "testing" + "testing/synctest" + "time" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/require" + + "git.samanthony.xyz/lulu" +) + +const ( + interiorUrl = "https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1" + coverUrl = "https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1" +) + +func TestStartAndGetInteriorValidation(t *testing.T) { + c := NewMockClient() + defer c.Close() + id, err := c.StartInteriorValidation(interiorUrl, DummyPkgId) + require.NoError(t, err) + + val, err := c.GetInteriorValidation(id) + require.NoError(t, err) + require.Equal(t, id, val.Id) + require.Equal(t, interiorUrl, val.SrcUrl) + require.Equal(t, lulu.InteriorStatusNormalized, val.Status) +} + +func TestStartInteriorValidationBasic(t *testing.T) { + c := NewMockClient() + defer c.Close() + id, err := c.StartInteriorValidationBasic(interiorUrl) + require.NoError(t, err) + + val, err := c.GetInteriorValidation(id) + require.NoError(t, err) + require.Equal(t, id, val.Id) + require.Equal(t, interiorUrl, val.SrcUrl) +} + +func TestGetInteriorValidationNotFound(t *testing.T) { + c := NewMockClient() + defer c.Close() + _, err := c.GetInteriorValidation(0) + require.Error(t, err) +} + +func TestCoverDimensions(t *testing.T) { + c := NewMockClient() + defer c.Close() + dims, err := c.CoverDimensions(DummyPkgId, 210, lulu.Points) + require.NoError(t, err) + require.Equal(t, lulu.Points, dims.Unit) + require.NotZero(t, dims.Width) + require.NotZero(t, dims.Height) +} + +func TestStartAndGetCoverValidation(t *testing.T) { + c := NewMockClient() + defer c.Close() + id, err := c.StartCoverValidation(coverUrl, DummyPkgId, 210) + require.NoError(t, err) + + val, err := c.GetCoverValidation(id) + require.NoError(t, err) + require.Equal(t, id, val.Id) + require.Equal(t, coverUrl, val.SrcUrl) + require.Equal(t, uint(210), val.NPages) + require.Equal(t, lulu.CoverStatusNormalized, val.Status) +} + +func TestGetCoverValidationNotFound(t *testing.T) { + c := NewMockClient() + defer c.Close() + _, err := c.GetCoverValidation(0) + require.Error(t, err) +} + +func TestCost(t *testing.T) { + c := NewMockClient() + defer c.Close() + items := []lulu.CostLineItem{ + {Mfg: DummyPkgId, Quantity: 2}, + {Mfg: DummyPkgId, Quantity: 1}, + } + addr := DummyShipAddr + addr.Street1 = "1 Main St" + + cost, val, err := c.Cost(items, addr, lulu.Mail) + require.NoError(t, err) + + require.Equal(t, "USD", cost.Currency) + require.Len(t, cost.LineItemCosts, len(items)) + for i, it := range cost.LineItemCosts { + require.Equal(t, items[i].Quantity, it.Quantity) + require.True(t, it.TotalCostInclTax.GreaterThan(decimal.Zero)) + } + require.True(t, cost.TotalCostInclTax.GreaterThan(cost.TotalCostExclTax)) + + require.Equal(t, addr, val.Address) + require.Equal(t, "1 Main rd", val.Suggested.Street1) +} + +func TestPrint(t *testing.T) { + c := NewMockClient() + defer c.Close() + items := MakePrintables(c, 2) + + j, err := c.Print(DummyEmailAddr, "ext-1", 5*time.Minute, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + + require.Zero(t, j.Id) + require.Equal(t, "ext-1", j.ExternalId) + require.Equal(t, DummyEmailAddr, j.Contact) + require.Equal(t, lulu.OrderProductionDelayed, j.Status.Status) + require.Len(t, j.Items, len(items)) + + seen := make(map[lulu.PrintableId]bool) + for i, it := range j.Items { + require.Equal(t, items[i].ExternalId, it.ExternalId) + require.Equal(t, items[i].Mfg, it.Mfg) + require.Equal(t, items[i].Quantity, it.Quantity) + require.NotEmpty(t, it.PrintableId) + require.False(t, seen[it.PrintableId], "duplicate printable id %q", it.PrintableId) + seen[it.PrintableId] = true + } + + // A second job should get the next sequential id. + j2, err := c.Print(DummyEmailAddr, "ext-2", 5*time.Minute, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + require.Equal(t, j.Id+1, j2.Id) +} + +func TestReprint(t *testing.T) { + c := NewMockClient() + defer c.Close() + items := MakePrintables(c, 2) + + j, err := c.Print(DummyEmailAddr, "orig", 5*time.Minute, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + + rpbs := make([]lulu.Reprintable, len(j.Items)) + for i, it := range j.Items { + rpbs[i] = lulu.Reprintable{ + ExternalId: it.ExternalId, + PrintableId: it.PrintableId, + Quantity: it.Quantity, + Title: it.Title, + } + } + + rj, err := c.Reprint(DummyEmailAddr, "reprint", 5*time.Minute, DummyShipAddr, lulu.Mail, rpbs) + require.NoError(t, err) + require.Equal(t, j.Id+1, rj.Id) + require.Len(t, rj.Items, len(rpbs)) + for i, it := range rj.Items { + require.Equal(t, rpbs[i].PrintableId, it.PrintableId) + require.Equal(t, items[i].Mfg, it.Mfg) + } +} + +func TestMakeReprintables(t *testing.T) { + c := NewMockClient() + defer c.Close() + rpbs, err := MakeReprintables(c, 2) + require.NoError(t, err) + require.Len(t, rpbs, 2) + for _, r := range rpbs { + require.NotEmpty(t, r.PrintableId) + require.NotZero(t, r.Quantity) + } + + j, err := c.Reprint(DummyEmailAddr, "eid", 5*time.Minute, DummyShipAddr, lulu.Mail, rpbs) + require.NoError(t, err) + require.Len(t, j.Items, 2) +} + +func TestReprintUnknownPrintable(t *testing.T) { + c := NewMockClient() + defer c.Close() + rpbs := []lulu.Reprintable{{ + ExternalId: "ext", + PrintableId: lulu.PrintableId("does-not-exist"), + Quantity: 1, + Title: "Ghost", + }} + _, err := c.Reprint(DummyEmailAddr, "eid", 5*time.Minute, DummyShipAddr, lulu.Mail, rpbs) + require.Error(t, err) +} + +func TestJob(t *testing.T) { + c := NewMockClient() + defer c.Close() + items := MakePrintables(c, 1) + j, err := c.Print(DummyEmailAddr, "eid", 5*time.Minute, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + + got, err := c.Job(j.Id) + require.NoError(t, err) + require.Equal(t, j.Id, got.Id) + require.Equal(t, j.ExternalId, got.ExternalId) +} + +func TestJobNotFound(t *testing.T) { + c := NewMockClient() + defer c.Close() + _, err := c.Job(0) + require.Error(t, err) +} + +func TestJobs(t *testing.T) { + c := NewMockClient() + defer c.Close() + items := MakePrintables(c, 1) + + want := make(map[uint64]string) + for i := range 3 { + eid := fmt.Sprintf("eid-%d", i) + j, err := c.Print(DummyEmailAddr, eid, 5*time.Minute, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + want[j.Id] = eid + } + + js, err := c.Jobs() + require.NoError(t, err) + require.Len(t, js, len(want)) + for _, j := range js { + require.Equal(t, want[j.Id], j.ExternalId) + } +} + +func TestJobsEmpty(t *testing.T) { + c := NewMockClient() + defer c.Close() + js, err := c.Jobs() + require.NoError(t, err) + require.Empty(t, js) +} + +func TestCancelWhileDelayed(t *testing.T) { + c := NewMockClient() + defer c.Close() + items := MakePrintables(c, 1) + j, err := c.Print(DummyEmailAddr, "eid", 5*time.Minute, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + + require.NoError(t, c.Cancel(j.Id)) + + got, err := c.Job(j.Id) + require.NoError(t, err) + require.Equal(t, lulu.OrderCanceled, got.Status.Status) +} + +func TestCancelAfterProduction(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + c := NewMockClient() + defer c.Close() + + items := MakePrintables(c, 1) + delay := 10 * time.Minute + j, err := c.Print(DummyEmailAddr, "eid", delay, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + + time.Sleep(delay + time.Millisecond) // let the prod timer fire + + got, err := c.Job(j.Id) + require.NoError(t, err) + require.Equal(t, lulu.OrderInProduction, got.Status.Status) + + err = c.Cancel(j.Id) + require.Error(t, err) + + got, err = c.Job(j.Id) + require.NoError(t, err) + require.Equal(t, lulu.OrderInProduction, got.Status.Status) + }) +} + +func TestCancelNotFound(t *testing.T) { + c := NewMockClient() + defer c.Close() + require.Error(t, c.Cancel(0)) +} + +func TestJobLifecycle(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + c := NewMockClient() + defer c.Close() + + items := MakePrintables(c, 1) + delay := 10 * time.Minute + j, err := c.Print(DummyEmailAddr, "eid", delay, DummyShipAddr, lulu.Mail, items) + require.NoError(t, err) + require.Equal(t, lulu.OrderProductionDelayed, j.Status.Status) + + time.Sleep(delay + time.Millisecond) // let the prod timer fire + got, err := c.Job(j.Id) + require.NoError(t, err) + require.Equal(t, lulu.OrderInProduction, got.Status.Status) + + time.Sleep(shipDelay) // let the ship timer fire + got, err = c.Job(j.Id) + require.NoError(t, err) + require.Equal(t, lulu.OrderShipped, got.Status.Status) + + time.Sleep(deliverDelay) // let the arrive timer fire + got, err = c.Job(j.Id) + require.NoError(t, err) + require.Equal(t, lulu.OrderDelivered, got.Status.Status) + }) +} |