mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
363 lines
9.9 KiB
Go
363 lines
9.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
func TestParseAdjustmentPriceFile_ValidSingleRow(t *testing.T) {
|
|
filePath := createWorkbook(
|
|
t,
|
|
"adjustment_prices",
|
|
[]string{"adjustment_id", "weight"},
|
|
[][]string{{"101", "12.345"}},
|
|
)
|
|
|
|
sheet, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if sheet != "adjustment_prices" {
|
|
t.Fatalf("expected selected sheet adjustment_prices, got %q", sheet)
|
|
}
|
|
if len(issues) != 0 {
|
|
t.Fatalf("expected no issues, got %+v", issues)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("expected 1 row, got %d", len(rows))
|
|
}
|
|
if rows[0].AdjustmentID != 101 {
|
|
t.Fatalf("expected adjustment_id 101, got %d", rows[0].AdjustmentID)
|
|
}
|
|
if rows[0].Weight != 12.345 {
|
|
t.Fatalf("expected weight 12.345, got %v", rows[0].Weight)
|
|
}
|
|
}
|
|
|
|
func TestParseAdjustmentPriceFile_ValidMultiRow(t *testing.T) {
|
|
filePath := createWorkbook(
|
|
t,
|
|
"adjustment_prices",
|
|
[]string{" Adjustment_ID ", "WEIGHT"},
|
|
[][]string{{"101", "10"}, {"102", "11.5"}},
|
|
)
|
|
|
|
_, rows, issues, err := parseAdjustmentPriceFile(filePath, "adjustment_prices")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(issues) != 0 {
|
|
t.Fatalf("expected no issues, got %+v", issues)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 rows, got %d", len(rows))
|
|
}
|
|
}
|
|
|
|
func TestParseAdjustmentPriceFile_MissingRequiredHeader(t *testing.T) {
|
|
filePath := createWorkbook(
|
|
t,
|
|
"adjustment_prices",
|
|
[]string{"adjustment_id", "price"},
|
|
[][]string{{"101", "12"}},
|
|
)
|
|
|
|
_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(rows) != 0 {
|
|
t.Fatalf("expected 0 parsed rows when header invalid, got %d", len(rows))
|
|
}
|
|
if !hasIssue(issues, 0, "weight", "required header is missing") {
|
|
t.Fatalf("expected missing weight header issue, got %+v", issues)
|
|
}
|
|
}
|
|
|
|
func TestParseAdjustmentPriceFile_InvalidAdjustmentID(t *testing.T) {
|
|
filePath := createWorkbook(
|
|
t,
|
|
"adjustment_prices",
|
|
[]string{"adjustment_id", "weight"},
|
|
[][]string{{"abc", "10"}, {"0", "12"}},
|
|
)
|
|
|
|
_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(rows) != 0 {
|
|
t.Fatalf("expected no valid rows, got %d", len(rows))
|
|
}
|
|
if !hasIssue(issues, 2, "adjustment_id", "must be a positive integer") {
|
|
t.Fatalf("expected non numeric adjustment_id issue, got %+v", issues)
|
|
}
|
|
if !hasIssue(issues, 3, "adjustment_id", "must be greater than 0") {
|
|
t.Fatalf("expected adjustment_id >0 issue, got %+v", issues)
|
|
}
|
|
}
|
|
|
|
func TestParseAdjustmentPriceFile_InvalidWeight(t *testing.T) {
|
|
filePath := createWorkbook(
|
|
t,
|
|
"adjustment_prices",
|
|
[]string{"adjustment_id", "weight"},
|
|
[][]string{{"101", "abc"}, {"102", "-1"}},
|
|
)
|
|
|
|
_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(rows) != 0 {
|
|
t.Fatalf("expected no valid rows, got %d", len(rows))
|
|
}
|
|
if !hasIssue(issues, 2, "weight", "must be numeric") {
|
|
t.Fatalf("expected weight numeric issue, got %+v", issues)
|
|
}
|
|
if !hasIssue(issues, 3, "weight", "must be greater than or equal to 0") {
|
|
t.Fatalf("expected weight >=0 issue, got %+v", issues)
|
|
}
|
|
}
|
|
|
|
func TestParseAdjustmentPriceFile_DuplicateAdjustmentID_LastRowWins(t *testing.T) {
|
|
filePath := createWorkbook(
|
|
t,
|
|
"adjustment_prices",
|
|
[]string{"adjustment_id", "weight"},
|
|
[][]string{{"101", "10"}, {"102", "20"}, {"101", "30"}},
|
|
)
|
|
|
|
_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(issues) != 0 {
|
|
t.Fatalf("expected no issues, got %+v", issues)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 deduped rows, got %d", len(rows))
|
|
}
|
|
|
|
row101, ok := findRowByAdjustmentID(rows, 101)
|
|
if !ok {
|
|
t.Fatalf("expected adjustment_id 101 to exist, got %+v", rows)
|
|
}
|
|
if row101.Weight != 30 {
|
|
t.Fatalf("expected duplicate adjustment_id to keep last weight 30, got %v", row101.Weight)
|
|
}
|
|
if row101.RowNumber != 4 {
|
|
t.Fatalf("expected duplicate adjustment_id to keep last row number 4, got %d", row101.RowNumber)
|
|
}
|
|
}
|
|
|
|
func TestSplitRowsByExistingIDs_SkipMissing(t *testing.T) {
|
|
rows := []adjustmentPriceImportRow{
|
|
{RowNumber: 2, AdjustmentID: 101, Weight: 10},
|
|
{RowNumber: 3, AdjustmentID: 102, Weight: 11},
|
|
{RowNumber: 4, AdjustmentID: 103, Weight: 12},
|
|
}
|
|
existing := map[uint]struct{}{101: {}, 103: {}}
|
|
|
|
processable, skipped := splitRowsByExistingIDs(rows, existing)
|
|
if len(processable) != 2 {
|
|
t.Fatalf("expected 2 processable rows, got %d", len(processable))
|
|
}
|
|
if len(skipped) != 1 {
|
|
t.Fatalf("expected 1 skipped row, got %d", len(skipped))
|
|
}
|
|
if skipped[0].AdjustmentID != 102 {
|
|
t.Fatalf("expected adjustment_id 102 skipped, got %+v", skipped)
|
|
}
|
|
}
|
|
|
|
func TestApplyIfRequested_DryRunDoesNotWrite(t *testing.T) {
|
|
runner := &fakeTransactionRunner{}
|
|
rows := []adjustmentPriceImportRow{{RowNumber: 2, AdjustmentID: 101, Weight: 10}}
|
|
|
|
results, err := applyIfRequested(context.Background(), false, runner, rows)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if results != nil {
|
|
t.Fatalf("expected nil results on dry-run, got %+v", results)
|
|
}
|
|
if runner.txCalls != 0 {
|
|
t.Fatalf("expected no transaction call during dry-run, got %d", runner.txCalls)
|
|
}
|
|
}
|
|
|
|
func TestApplyImportRows_Success(t *testing.T) {
|
|
runner := &fakeTransactionRunner{
|
|
changedByID: map[uint]bool{101: true, 102: false},
|
|
}
|
|
rows := []adjustmentPriceImportRow{
|
|
{RowNumber: 2, AdjustmentID: 101, Weight: 10},
|
|
{RowNumber: 3, AdjustmentID: 102, Weight: 11},
|
|
}
|
|
|
|
results, err := applyImportRows(context.Background(), runner, rows)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if runner.txCalls != 1 {
|
|
t.Fatalf("expected 1 transaction call, got %d", runner.txCalls)
|
|
}
|
|
if len(runner.committedCalls) != 2 {
|
|
t.Fatalf("expected 2 committed updates, got %d", len(runner.committedCalls))
|
|
}
|
|
if len(results) != 2 {
|
|
t.Fatalf("expected 2 row results, got %d", len(results))
|
|
}
|
|
if !results[0].Changed || results[1].Changed {
|
|
t.Fatalf("unexpected changed flags: %+v", results)
|
|
}
|
|
}
|
|
|
|
func TestApplyImportRows_RollbackOnError(t *testing.T) {
|
|
runner := &fakeTransactionRunner{
|
|
errByID: map[uint]error{102: errors.New("boom")},
|
|
}
|
|
rows := []adjustmentPriceImportRow{
|
|
{RowNumber: 2, AdjustmentID: 101, Weight: 10},
|
|
{RowNumber: 3, AdjustmentID: 102, Weight: 11},
|
|
}
|
|
|
|
_, err := applyImportRows(context.Background(), runner, rows)
|
|
if err == nil {
|
|
t.Fatal("expected error due to update failure")
|
|
}
|
|
if !strings.Contains(err.Error(), "row 3 adjustment_id=102 update failed") {
|
|
t.Fatalf("unexpected error message: %v", err)
|
|
}
|
|
if runner.txCalls != 1 {
|
|
t.Fatalf("expected 1 transaction call, got %d", runner.txCalls)
|
|
}
|
|
if len(runner.committedCalls) != 0 {
|
|
t.Fatalf("expected no committed updates on rollback, got %d", len(runner.committedCalls))
|
|
}
|
|
}
|
|
|
|
func createWorkbook(t *testing.T, sheetName string, headers []string, rows [][]string) string {
|
|
t.Helper()
|
|
|
|
f := excelize.NewFile()
|
|
defaultSheet := f.GetSheetName(f.GetActiveSheetIndex())
|
|
if sheetName == "" {
|
|
sheetName = defaultSheet
|
|
} else if sheetName != defaultSheet {
|
|
f.SetSheetName(defaultSheet, sheetName)
|
|
}
|
|
|
|
for idx, header := range headers {
|
|
cell, err := excelize.CoordinatesToCellName(idx+1, 1)
|
|
if err != nil {
|
|
t.Fatalf("failed resolving header cell: %v", err)
|
|
}
|
|
if err := f.SetCellValue(sheetName, cell, header); err != nil {
|
|
t.Fatalf("failed setting header cell: %v", err)
|
|
}
|
|
}
|
|
|
|
for rowIdx, row := range rows {
|
|
for colIdx, value := range row {
|
|
cell, err := excelize.CoordinatesToCellName(colIdx+1, rowIdx+2)
|
|
if err != nil {
|
|
t.Fatalf("failed resolving data cell: %v", err)
|
|
}
|
|
if err := f.SetCellValue(sheetName, cell, value); err != nil {
|
|
t.Fatalf("failed setting data cell: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
path := filepath.Join(t.TempDir(), "adjustment_prices.xlsx")
|
|
if err := f.SaveAs(path); err != nil {
|
|
t.Fatalf("failed saving workbook: %v", err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatalf("failed closing workbook: %v", err)
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
func hasIssue(issues []validationIssue, row int, field, messageContains string) bool {
|
|
for _, issue := range issues {
|
|
if issue.Row != row {
|
|
continue
|
|
}
|
|
if issue.Field != field {
|
|
continue
|
|
}
|
|
if strings.Contains(issue.Message, messageContains) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func findRowByAdjustmentID(rows []adjustmentPriceImportRow, adjustmentID uint) (adjustmentPriceImportRow, bool) {
|
|
for _, row := range rows {
|
|
if row.AdjustmentID == adjustmentID {
|
|
return row, true
|
|
}
|
|
}
|
|
return adjustmentPriceImportRow{}, false
|
|
}
|
|
|
|
type updateCall struct {
|
|
adjustmentID uint
|
|
price float64
|
|
}
|
|
|
|
type fakeAdjustmentPriceStore struct {
|
|
changedByID map[uint]bool
|
|
errByID map[uint]error
|
|
calls []updateCall
|
|
}
|
|
|
|
func (s *fakeAdjustmentPriceStore) UpdatePrice(_ context.Context, adjustmentID uint, price float64) (bool, error) {
|
|
s.calls = append(s.calls, updateCall{adjustmentID: adjustmentID, price: price})
|
|
if err, exists := s.errByID[adjustmentID]; exists {
|
|
return false, fmt.Errorf("forced update failure for adjustment_id=%d: %w", adjustmentID, err)
|
|
}
|
|
if changed, exists := s.changedByID[adjustmentID]; exists {
|
|
return changed, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
type fakeTransactionRunner struct {
|
|
txCalls int
|
|
changedByID map[uint]bool
|
|
errByID map[uint]error
|
|
committedCalls []updateCall
|
|
}
|
|
|
|
func (r *fakeTransactionRunner) InTx(ctx context.Context, fn func(store adjustmentPriceStore) error) error {
|
|
r.txCalls++
|
|
|
|
txStore := &fakeAdjustmentPriceStore{
|
|
changedByID: r.changedByID,
|
|
errByID: r.errByID,
|
|
calls: make([]updateCall, 0),
|
|
}
|
|
|
|
if err := fn(txStore); err != nil {
|
|
return err
|
|
}
|
|
|
|
r.committedCalls = append(r.committedCalls, txStore.calls...)
|
|
return nil
|
|
}
|
|
|
|
var _ txRunner = (*fakeTransactionRunner)(nil)
|
|
var _ adjustmentPriceStore = (*fakeAdjustmentPriceStore)(nil)
|