203 lines
6.5 KiB
Go
203 lines
6.5 KiB
Go
package woocommerce
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
||
|
|
t.Setenv("APP_ENV", "development")
|
||
|
|
key := DeriveKey("test-passphrase", "fallback")
|
||
|
|
enc, err := EncryptSecret(key, "ck_secret_value")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if enc == "" || enc == "ck_secret_value" {
|
||
|
|
t.Fatalf("expected ciphertext, got %q", enc)
|
||
|
|
}
|
||
|
|
plain, err := DecryptSecret(key, enc)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if plain != "ck_secret_value" {
|
||
|
|
t.Fatalf("got %q", plain)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDecryptLegacyPlaintext(t *testing.T) {
|
||
|
|
t.Setenv("APP_ENV", "development")
|
||
|
|
key := DeriveKey("x", "y")
|
||
|
|
plain, err := DecryptSecret(key, "legacy-plain")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if plain != "legacy-plain" {
|
||
|
|
t.Fatalf("got %q", plain)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDecryptLegacyPlaintextRejectedInProduction(t *testing.T) {
|
||
|
|
t.Setenv("APP_ENV", "production")
|
||
|
|
key := DeriveKey("x", "y")
|
||
|
|
if _, err := DecryptSecret(key, "legacy-plain"); err == nil {
|
||
|
|
t.Fatal("expected plaintext decrypt rejected in production")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDeriveKeyRejectsFallbackInProduction(t *testing.T) {
|
||
|
|
t.Setenv("APP_ENV", "production")
|
||
|
|
if key := DeriveKey("", "postgres://local"); key != nil {
|
||
|
|
t.Fatalf("expected nil key without explicit material in production, got len=%d", len(key))
|
||
|
|
}
|
||
|
|
t.Setenv("APP_ENV", "development")
|
||
|
|
if key := DeriveKey("", "postgres://local"); len(key) != 32 {
|
||
|
|
t.Fatalf("expected fallback key in development, got len=%d", len(key))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseSyncOptionsClampsLimits(t *testing.T) {
|
||
|
|
raw := []byte(`{"sync_limit":99999,"batch_size":500,"orders_sync_limit":99999,"reviews_sync_limit":-1,"schedule_interval_hours":999}`)
|
||
|
|
opt := parseSyncOptions(raw)
|
||
|
|
if opt.SyncLimit != defaultSyncLimit {
|
||
|
|
t.Fatalf("sync_limit=%d", opt.SyncLimit)
|
||
|
|
}
|
||
|
|
if opt.BatchSize != defaultBatchSize {
|
||
|
|
t.Fatalf("batch_size=%d", opt.BatchSize)
|
||
|
|
}
|
||
|
|
if opt.OrdersSyncLimit != 0 {
|
||
|
|
t.Fatalf("orders_sync_limit=%d", opt.OrdersSyncLimit)
|
||
|
|
}
|
||
|
|
if opt.ReviewsSyncLimit != 0 {
|
||
|
|
t.Fatalf("reviews_sync_limit=%d", opt.ReviewsSyncLimit)
|
||
|
|
}
|
||
|
|
if opt.ScheduleIntervalHours != 0 {
|
||
|
|
t.Fatalf("schedule_interval_hours=%d", opt.ScheduleIntervalHours)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseSyncOptionsPrunesProductIDs(t *testing.T) {
|
||
|
|
ids := make(map[string]int, maxProductIDMap+50)
|
||
|
|
for i := 0; i < maxProductIDMap+50; i++ {
|
||
|
|
ids[fmt.Sprintf("sku-%d", i)] = i + 1
|
||
|
|
}
|
||
|
|
raw, err := json.Marshal(map[string]any{"product_ids": ids})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
opt := parseSyncOptions(raw)
|
||
|
|
if len(opt.ProductIDs) != maxProductIDMap {
|
||
|
|
t.Fatalf("product_ids len=%d want %d", len(opt.ProductIDs), maxProductIDMap)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseSyncOptionsScheduleAndFilterParams(t *testing.T) {
|
||
|
|
raw := []byte(`{
|
||
|
|
"schedule_interval_hours":48,
|
||
|
|
"match_strategy":"barcode",
|
||
|
|
"orders_modified_after":"2026-01-01T00:00:00Z",
|
||
|
|
"product_ids":{"SKU-1":7}
|
||
|
|
}`)
|
||
|
|
opt := parseSyncOptions(raw)
|
||
|
|
if opt.ScheduleIntervalHours != 48 {
|
||
|
|
t.Fatalf("schedule_interval_hours=%d want 48", opt.ScheduleIntervalHours)
|
||
|
|
}
|
||
|
|
if opt.MatchStrategy != "barcode" {
|
||
|
|
t.Fatalf("match_strategy=%q", opt.MatchStrategy)
|
||
|
|
}
|
||
|
|
if opt.OrdersModifiedAfter != "2026-01-01T00:00:00Z" {
|
||
|
|
t.Fatalf("orders_modified_after=%q", opt.OrdersModifiedAfter)
|
||
|
|
}
|
||
|
|
if opt.ProductIDs["SKU-1"] != 7 {
|
||
|
|
t.Fatalf("product_ids=%v", opt.ProductIDs)
|
||
|
|
}
|
||
|
|
|
||
|
|
maxRaw := []byte(fmt.Sprintf(`{"schedule_interval_hours":%d}`, maxScheduleIntervalH))
|
||
|
|
if got := parseSyncOptions(maxRaw).ScheduleIntervalHours; got != maxScheduleIntervalH {
|
||
|
|
t.Fatalf("max schedule kept=%d want %d", got, maxScheduleIntervalH)
|
||
|
|
}
|
||
|
|
neg := parseSyncOptions([]byte(`{"schedule_interval_hours":-1,"match_strategy":""}`))
|
||
|
|
if neg.ScheduleIntervalHours != 0 {
|
||
|
|
t.Fatalf("negative schedule=%d", neg.ScheduleIntervalHours)
|
||
|
|
}
|
||
|
|
if neg.MatchStrategy != "sku" {
|
||
|
|
t.Fatalf("empty match_strategy default=%q", neg.MatchStrategy)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestResolveScheduleIntervalAndDue(t *testing.T) {
|
||
|
|
if got := resolveScheduleInterval(0, 0); got != 6*time.Hour {
|
||
|
|
t.Fatalf("default interval=%s", got)
|
||
|
|
}
|
||
|
|
if got := resolveScheduleInterval(8, 2*time.Hour); got != 8*time.Hour {
|
||
|
|
t.Fatalf("custom interval=%s", got)
|
||
|
|
}
|
||
|
|
now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
|
||
|
|
if !isDueForSchedule(nil, now, time.Hour) {
|
||
|
|
t.Fatal("nil last should be due")
|
||
|
|
}
|
||
|
|
recent := now.Add(-15 * time.Minute)
|
||
|
|
if isDueForSchedule(&recent, now, time.Hour) {
|
||
|
|
t.Fatal("recent sync should not be due")
|
||
|
|
}
|
||
|
|
stale := now.Add(-90 * time.Minute)
|
||
|
|
if !isDueForSchedule(&stale, now, time.Hour) {
|
||
|
|
t.Fatal("stale sync should be due")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUpdateScheduleRejectsInvalidInterval(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Service{} // Pool nil — validation must fail before any DB I/O
|
||
|
|
cid := uuid.MustParse("11111111-1111-1111-1111-111111111111")
|
||
|
|
_, err := s.UpdateSchedule(t.Context(), cid, -1, false)
|
||
|
|
if !errors.Is(err, ErrInvalidScheduleInterval) {
|
||
|
|
t.Fatalf("negative hours: err=%v", err)
|
||
|
|
}
|
||
|
|
_, err = s.UpdateSchedule(t.Context(), cid, maxScheduleIntervalH+1, false)
|
||
|
|
if !errors.Is(err, ErrInvalidScheduleInterval) {
|
||
|
|
t.Fatalf("over-max hours: err=%v", err)
|
||
|
|
}
|
||
|
|
msg, ok := ClientError(ErrInvalidScheduleInterval)
|
||
|
|
if !ok || msg == "" {
|
||
|
|
t.Fatal("ClientError mapping missing for ErrInvalidScheduleInterval")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestShouldEnqueueScheduledPaused(t *testing.T) {
|
||
|
|
now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
|
||
|
|
if shouldEnqueueScheduled(true, 6, nil, now, 6*time.Hour) {
|
||
|
|
t.Fatal("paused schedule must not enqueue")
|
||
|
|
}
|
||
|
|
if !shouldEnqueueScheduled(false, 6, nil, now, 6*time.Hour) {
|
||
|
|
t.Fatal("unpaused with nil last should enqueue")
|
||
|
|
}
|
||
|
|
recent := now.Add(-30 * time.Minute)
|
||
|
|
if shouldEnqueueScheduled(false, 6, &recent, now, 6*time.Hour) {
|
||
|
|
t.Fatal("recent sync within interval must not enqueue")
|
||
|
|
}
|
||
|
|
opt := parseSyncOptions([]byte(`{"schedule_interval_hours":12,"schedule_paused":true}`))
|
||
|
|
if !opt.SchedulePaused || opt.ScheduleIntervalHours != 12 {
|
||
|
|
t.Fatalf("parse paused=%v hours=%d", opt.SchedulePaused, opt.ScheduleIntervalHours)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNormalizeOrderListFilter(t *testing.T) {
|
||
|
|
got := normalizeOrderListFilter(OrderListFilter{Limit: 0, Offset: -2})
|
||
|
|
if got.Limit != 50 || got.Offset != 0 {
|
||
|
|
t.Fatalf("defaults got limit=%d offset=%d", got.Limit, got.Offset)
|
||
|
|
}
|
||
|
|
got = normalizeOrderListFilter(OrderListFilter{Limit: 500, Offset: 4})
|
||
|
|
if got.Limit != 200 || got.Offset != 4 {
|
||
|
|
t.Fatalf("over-max clamp got limit=%d offset=%d", got.Limit, got.Offset)
|
||
|
|
}
|
||
|
|
got = normalizeOrderListFilter(OrderListFilter{Limit: 40, Offset: 1, Status: "processing", Email: "x@y.z"})
|
||
|
|
if got.Limit != 40 || got.Offset != 1 || got.Status != "processing" || got.Email != "x@y.z" {
|
||
|
|
t.Fatalf("preserve got %+v", got)
|
||
|
|
}
|
||
|
|
}
|