feat(t237): import freight orders synchronously
This commit is contained in:
@@ -3,6 +3,7 @@ package usecase
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -98,6 +99,176 @@ func TestCreateFreightOrderSyncStopsBeforePersistingWhenOCRIsInvalid(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFreightOrderSyncReturnsCommittedResult(t *testing.T) {
|
||||
repository := &syncTrackingRepository{}
|
||||
service, err := NewFreightService(
|
||||
repository,
|
||||
&recordingDateSource{},
|
||||
fakeClock{},
|
||||
&sequenceIDs{},
|
||||
time.Minute,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFreightService() error = %v", err)
|
||||
}
|
||||
if service.orderTimeout != 55*time.Second {
|
||||
t.Fatalf("order timeout = %s", service.orderTimeout)
|
||||
}
|
||||
result, err := service.CreateOrderSync(
|
||||
context.Background(),
|
||||
validFreightOrderSyncCommand("sync-success"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrderSync() error = %v", err)
|
||||
}
|
||||
if result.Replayed || result.Run.Status != domain.FreightSyncSucceeded ||
|
||||
result.Run.OrderCount != 1 || result.Run.ItemCount != 1 ||
|
||||
result.Run.StartedAt == nil || result.Run.FinishedAt == nil {
|
||||
t.Fatalf("CreateOrderSync() result = %+v", result)
|
||||
}
|
||||
repository.mu.Lock()
|
||||
defer repository.mu.Unlock()
|
||||
if repository.startCalls != 1 || repository.completeCalls != 1 ||
|
||||
repository.failCalls != 0 {
|
||||
t.Fatalf(
|
||||
"start/complete/fail calls = %d/%d/%d",
|
||||
repository.startCalls,
|
||||
repository.completeCalls,
|
||||
repository.failCalls,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFreightOrderSyncTimeoutUsesLiveCleanupContext(t *testing.T) {
|
||||
repository := &syncTrackingRepository{}
|
||||
source := &blockingOrderSource{}
|
||||
service, err := NewFreightService(
|
||||
repository,
|
||||
source,
|
||||
fakeClock{},
|
||||
&sequenceIDs{},
|
||||
time.Minute,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFreightService() error = %v", err)
|
||||
}
|
||||
service.orderTimeout = 10 * time.Millisecond
|
||||
_, err = service.CreateOrderSync(
|
||||
context.Background(),
|
||||
validFreightOrderSyncCommand("sync-timeout"),
|
||||
)
|
||||
assertUsecaseError(t, err, ErrorKindUnavailable, "FREIGHT_SYNC_TIMEOUT")
|
||||
repository.mu.Lock()
|
||||
defer repository.mu.Unlock()
|
||||
if repository.failCalls != 1 ||
|
||||
repository.failCode != "FREIGHT_SYNC_TIMEOUT" ||
|
||||
repository.failContextErr != nil {
|
||||
t.Fatalf(
|
||||
"failure calls/code/context = %d/%q/%v",
|
||||
repository.failCalls,
|
||||
repository.failCode,
|
||||
repository.failContextErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFreightOrderSyncCancellationUsesLiveCleanupContext(t *testing.T) {
|
||||
repository := &syncTrackingRepository{}
|
||||
source := &blockingOrderSource{started: make(chan struct{})}
|
||||
service, err := NewFreightService(
|
||||
repository,
|
||||
source,
|
||||
fakeClock{},
|
||||
&sequenceIDs{},
|
||||
time.Minute,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFreightService() error = %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, runErr := service.CreateOrderSync(
|
||||
ctx,
|
||||
validFreightOrderSyncCommand("sync-canceled"),
|
||||
)
|
||||
result <- runErr
|
||||
}()
|
||||
select {
|
||||
case <-source.started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("order sync did not reach source")
|
||||
}
|
||||
cancel()
|
||||
err = <-result
|
||||
assertUsecaseError(t, err, ErrorKindUnavailable, "FREIGHT_SYNC_CANCELED")
|
||||
repository.mu.Lock()
|
||||
defer repository.mu.Unlock()
|
||||
if repository.failCalls != 1 ||
|
||||
repository.failCode != "FREIGHT_SYNC_CANCELED" ||
|
||||
repository.failContextErr != nil {
|
||||
t.Fatalf(
|
||||
"failure calls/code/context = %d/%q/%v",
|
||||
repository.failCalls,
|
||||
repository.failCode,
|
||||
repository.failContextErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFreightOrderSyncRejectsConcurrentRequest(t *testing.T) {
|
||||
repository := &syncTrackingRepository{}
|
||||
source := &blockingOrderSource{
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
service, err := NewFreightService(
|
||||
repository,
|
||||
source,
|
||||
fakeClock{},
|
||||
&sequenceIDs{},
|
||||
time.Minute,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFreightService() error = %v", err)
|
||||
}
|
||||
firstResult := make(chan error, 1)
|
||||
go func() {
|
||||
_, runErr := service.CreateOrderSync(
|
||||
context.Background(),
|
||||
validFreightOrderSyncCommand("sync-first"),
|
||||
)
|
||||
firstResult <- runErr
|
||||
}()
|
||||
select {
|
||||
case <-source.started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("first order sync did not reach source")
|
||||
}
|
||||
startedAt := time.Now()
|
||||
_, err = service.CreateOrderSync(
|
||||
context.Background(),
|
||||
validFreightOrderSyncCommand("sync-second"),
|
||||
)
|
||||
assertUsecaseError(t, err, ErrorKindConflict, "FREIGHT_SYNC_BUSY")
|
||||
if time.Since(startedAt) > 100*time.Millisecond {
|
||||
t.Fatalf("busy response took %s", time.Since(startedAt))
|
||||
}
|
||||
close(source.release)
|
||||
if err := <-firstResult; err != nil {
|
||||
t.Fatalf("first CreateOrderSync() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validFreightOrderSyncCommand(idempotencyKey string) CreateFreightSyncCommand {
|
||||
return CreateFreightSyncCommand{
|
||||
CreatorSubject: "local-admin",
|
||||
ActorUserID: "00000000-0000-4000-8000-000000000099",
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrderNumber: "ORDER-123",
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreightDateQuerySplitsIntoSevenDayWindows(t *testing.T) {
|
||||
source := &recordingDateSource{}
|
||||
service := &FreightService{source: source}
|
||||
@@ -261,6 +432,144 @@ type recordingDateSource struct {
|
||||
ensureErr error
|
||||
}
|
||||
|
||||
type blockingOrderSource struct {
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (source *blockingOrderSource) QueryOrder(
|
||||
ctx context.Context,
|
||||
_ string,
|
||||
) (domain.FreightSourceBatch, error) {
|
||||
if source.started != nil {
|
||||
source.once.Do(func() {
|
||||
close(source.started)
|
||||
})
|
||||
}
|
||||
if source.release != nil {
|
||||
select {
|
||||
case <-source.release:
|
||||
case <-ctx.Done():
|
||||
return domain.FreightSourceBatch{}, ctx.Err()
|
||||
}
|
||||
} else {
|
||||
<-ctx.Done()
|
||||
return domain.FreightSourceBatch{}, ctx.Err()
|
||||
}
|
||||
return validFreightSource(), nil
|
||||
}
|
||||
|
||||
func (*blockingOrderSource) QueryCreatedRange(
|
||||
context.Context,
|
||||
string,
|
||||
string,
|
||||
) (domain.FreightSourceBatch, error) {
|
||||
return domain.FreightSourceBatch{}, errors.New("unexpected date query")
|
||||
}
|
||||
|
||||
type syncTrackingRepository struct {
|
||||
mu sync.Mutex
|
||||
startCalls int
|
||||
completeCalls int
|
||||
failCalls int
|
||||
failCode string
|
||||
failContextErr error
|
||||
}
|
||||
|
||||
func (*syncTrackingRepository) CreateFreightSync(
|
||||
_ context.Context,
|
||||
run domain.FreightSyncRun,
|
||||
_, _ string,
|
||||
) (domain.FreightSyncRun, bool, error) {
|
||||
return run, true, nil
|
||||
}
|
||||
|
||||
func (repository *syncTrackingRepository) StartFreightSync(
|
||||
context.Context,
|
||||
string,
|
||||
time.Time,
|
||||
) error {
|
||||
repository.mu.Lock()
|
||||
defer repository.mu.Unlock()
|
||||
repository.startCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repository *syncTrackingRepository) CompleteFreightSync(
|
||||
context.Context,
|
||||
domain.FreightSyncRun,
|
||||
domain.FreightImportBatch,
|
||||
time.Time,
|
||||
) error {
|
||||
repository.mu.Lock()
|
||||
defer repository.mu.Unlock()
|
||||
repository.completeCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*syncTrackingRepository) CompleteFreightDateSync(
|
||||
context.Context,
|
||||
domain.FreightSyncRun,
|
||||
domain.FreightImportBatch,
|
||||
time.Time,
|
||||
time.Time,
|
||||
) error {
|
||||
return errors.New("unexpected date completion")
|
||||
}
|
||||
|
||||
func (repository *syncTrackingRepository) FailFreightSync(
|
||||
ctx context.Context,
|
||||
_ string,
|
||||
code string,
|
||||
_ time.Time,
|
||||
) error {
|
||||
repository.mu.Lock()
|
||||
defer repository.mu.Unlock()
|
||||
repository.failCalls++
|
||||
repository.failCode = code
|
||||
repository.failContextErr = ctx.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*syncTrackingRepository) RecoverFreightSyncs(
|
||||
context.Context,
|
||||
time.Time,
|
||||
) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (*syncTrackingRepository) GetFreightSync(
|
||||
context.Context,
|
||||
string,
|
||||
string,
|
||||
) (domain.FreightSyncRun, error) {
|
||||
return domain.FreightSyncRun{}, nil
|
||||
}
|
||||
|
||||
func (*syncTrackingRepository) GetFreightSyncWatermark(
|
||||
context.Context,
|
||||
string,
|
||||
) (*domain.FreightSyncWatermark, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*syncTrackingRepository) ListFreightOrders(
|
||||
context.Context,
|
||||
string,
|
||||
int,
|
||||
) ([]domain.FreightOrder, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*syncTrackingRepository) GetFreightOrder(
|
||||
context.Context,
|
||||
string,
|
||||
string,
|
||||
) (domain.FreightOrderDetail, error) {
|
||||
return domain.FreightOrderDetail{}, nil
|
||||
}
|
||||
|
||||
func (source *recordingDateSource) EnsureAuthenticated(context.Context) error {
|
||||
source.ensureCalls++
|
||||
return source.ensureErr
|
||||
|
||||
Reference in New Issue
Block a user