Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
package shopify
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestNormalizeShopDomainNameOnly(t *testing.T) {
|
||||
got, err := NormalizeShopDomain("My-Store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "my-store.myshopify.com" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeShopDomainFullHost(t *testing.T) {
|
||||
got, err := NormalizeShopDomain("https://demo-shop.myshopify.com/admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "demo-shop.myshopify.com" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeShopDomainRejectsCustomDomain(t *testing.T) {
|
||||
_, err := NormalizeShopDomain("https://shop.example.com")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeShopDomainRejectsIP(t *testing.T) {
|
||||
_, err := NormalizeShopDomain("192.168.1.10")
|
||||
if !errors.Is(err, ErrBlockedShopDomain) && !errors.Is(err, ErrInvalidShopDomain) {
|
||||
t.Fatalf("expected blocked/invalid, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeShopDomainRejectsLocalhost(t *testing.T) {
|
||||
_, err := NormalizeShopDomain("localhost")
|
||||
if !errors.Is(err, ErrBlockedShopDomain) {
|
||||
t.Fatalf("expected blocked, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuardAdminURL(t *testing.T) {
|
||||
if err := GuardAdminURL("https://demo.myshopify.com/admin/api/2024-10/shop.json", "demo.myshopify.com"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := GuardAdminURL("http://demo.myshopify.com/admin/api/2024-10/shop.json", "demo.myshopify.com"); err == nil {
|
||||
t.Fatal("expected http blocked")
|
||||
}
|
||||
if err := GuardAdminURL("https://evil.example.com/admin/api/2024-10/shop.json", "demo.myshopify.com"); err == nil {
|
||||
t.Fatal("expected host mismatch blocked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecryptRoundTrip(t *testing.T) {
|
||||
t.Setenv("APP_ENV", "development")
|
||||
key := DeriveKey("test-passphrase", "fallback")
|
||||
enc, err := EncryptSecret(key, "shpat_test_token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if enc == "shpat_test_token" {
|
||||
t.Fatal("expected ciphertext")
|
||||
}
|
||||
plain, err := DecryptSecret(key, enc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plain != "shpat_test_token" {
|
||||
t.Fatalf("got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSyncOptionsClampsLimits(t *testing.T) {
|
||||
raw := []byte(`{"sync_limit":99999,"batch_size":500,"orders_sync_limit":99999,"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.ScheduleIntervalHours != 0 {
|
||||
t.Fatalf("schedule_interval_hours=%d", opt.ScheduleIntervalHours)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSyncOptionsPrunesProductIDs(t *testing.T) {
|
||||
ids := make(map[string]int64, maxProductIDMap+50)
|
||||
for i := 0; i < maxProductIDMap+50; i++ {
|
||||
ids[fmt.Sprintf("sku-%d", i)] = int64(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":24,"match_strategy":"barcode","product_ids":{"SKU-1":11}}`)
|
||||
opt := parseSyncOptions(raw)
|
||||
if opt.ScheduleIntervalHours != 24 {
|
||||
t.Fatalf("schedule_interval_hours=%d want 24", opt.ScheduleIntervalHours)
|
||||
}
|
||||
if opt.MatchStrategy != "barcode" {
|
||||
t.Fatalf("match_strategy=%q", opt.MatchStrategy)
|
||||
}
|
||||
if opt.ProductIDs["SKU-1"] != 11 {
|
||||
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":-3,"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(12, time.Hour); got != 12*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(-30 * time.Minute)
|
||||
if isDueForSchedule(&recent, now, time.Hour) {
|
||||
t.Fatal("recent sync should not be due")
|
||||
}
|
||||
stale := now.Add(-2 * time.Hour)
|
||||
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.Fatalf("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: -5})
|
||||
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: 10})
|
||||
if got.Limit != 50 || got.Offset != 10 {
|
||||
t.Fatalf("over-max clamp got limit=%d offset=%d", got.Limit, got.Offset)
|
||||
}
|
||||
got = normalizeOrderListFilter(OrderListFilter{Limit: 25, Offset: 3, Status: "paid", Email: "a@b.c"})
|
||||
if got.Limit != 25 || got.Offset != 3 || got.Status != "paid" || got.Email != "a@b.c" {
|
||||
t.Fatalf("preserve got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneStringInt64Map(t *testing.T) {
|
||||
m := map[string]int64{"a": 1, "b": 2, "c": 3}
|
||||
pruneStringInt64Map(m, 2)
|
||||
if len(m) != 2 {
|
||||
t.Fatalf("len=%d want 2", len(m))
|
||||
}
|
||||
pruneStringInt64Map(m, 10)
|
||||
if len(m) != 2 {
|
||||
t.Fatalf("no-op prune changed len=%d", len(m))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAPIVersion(t *testing.T) {
|
||||
if got := normalizeAPIVersion("2024-10"); got != "2024-10" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
if got := normalizeAPIVersion("../evil"); got != defaultAPIVersion {
|
||||
t.Fatalf("expected default, got %q", got)
|
||||
}
|
||||
if got := normalizeAPIVersion(""); got != defaultAPIVersion {
|
||||
t.Fatalf("expected default, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopifySKUSearchQuery(t *testing.T) {
|
||||
got := shopifySKUSearchQuery(`ABC" OR sku:evil`)
|
||||
want := `sku:"ABC\" OR sku:evil"`
|
||||
if got != want {
|
||||
t.Fatalf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDryRunClient(t *testing.T) {
|
||||
c := NewClient("demo.myshopify.com", "dry-run", "2024-10", nil)
|
||||
if !c.DryRun {
|
||||
t.Fatal("expected dry-run")
|
||||
}
|
||||
shop, err := c.TestConnection(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if shop == nil || shop.Name == "" {
|
||||
t.Fatal("expected shop info")
|
||||
}
|
||||
p, err := c.CreateProduct(t.Context(), ProductPayload{Title: "Test"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.ID <= 0 {
|
||||
t.Fatalf("expected dry product id, got %d", p.ID)
|
||||
}
|
||||
orders, next, err := c.ListOrdersPage(t.Context(), "", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(orders) != 1 || next != "" {
|
||||
t.Fatalf("unexpected orders=%d next=%q", len(orders), next)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user