Files
geo/server/internal/tenant/app/publish_job_service_test.go
T
root 9d6181260a
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m49s
Backend CI / Backend (push) Failing after 7m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m4s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 28s
feat(media-supply): add media resource supply marketplace
Introduce an end-to-end media-supply feature: tenant-side resource sync
service/worker backed by a Meijiequan supplier client, ops-side management
APIs, and admin/ops web views for resources, orders, favorites and
submission. Adds a shared digitocr helper, MediaSupply config blocks for
tenant and ops, shared types, and migrations for supplier media resources,
price overrides, customer visibility and order refunds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 23:17:01 +08:00

335 lines
11 KiB
Go

package app
import (
"context"
"fmt"
"strings"
"testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
func TestResolveRetryArticleIDFromPayload(t *testing.T) {
t.Parallel()
cases := []struct {
name string
payload map[string]any
wantID int64
wantOK bool
}{
{
name: "top level article id",
payload: map[string]any{
"article_id": 78,
},
wantID: 78,
wantOK: true,
},
{
name: "nested content ref article id",
payload: map[string]any{
"content_ref": map[string]any{
"article_id": 79,
},
},
wantID: 79,
wantOK: true,
},
{
name: "nested content ref generic id",
payload: map[string]any{
"content_ref": map[string]any{
"id": 80,
},
},
wantID: 80,
wantOK: true,
},
{
name: "missing article id",
payload: map[string]any{
"publish_record_id": 999,
},
wantID: 0,
wantOK: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
gotID, gotOK := resolveRetryArticleIDFromPayload(tc.payload)
if gotID != tc.wantID || gotOK != tc.wantOK {
t.Fatalf("resolveRetryArticleIDFromPayload() = (%d, %v), want (%d, %v)", gotID, gotOK, tc.wantID, tc.wantOK)
}
})
}
}
func TestParseUniquePublishAccountIDsDedupesInRequestOrder(t *testing.T) {
t.Parallel()
first := uuid.MustParse("11111111-1111-1111-1111-111111111111")
second := uuid.MustParse("22222222-2222-2222-2222-222222222222")
got, err := parseUniquePublishAccountIDs([]CreatePublishJobAccountRequest{
{AccountID: " " + first.String() + " "},
{AccountID: second.String()},
{AccountID: first.String()},
})
if err != nil {
t.Fatalf("parseUniquePublishAccountIDs() error = %v", err)
}
if len(got) != 2 || got[0] != first || got[1] != second {
t.Fatalf("parseUniquePublishAccountIDs() = %v, want [%s %s]", got, first, second)
}
}
func TestDesktopPublishDedupKeyUsesArticlePlatformAndAccount(t *testing.T) {
t.Parallel()
accountID := uuid.MustParse("33333333-3333-3333-3333-333333333333")
got := desktopPublishDedupKey(7, 11, 897, " sohuhao ", accountID)
want := `["publish","7","11","897","sohuhao","33333333-3333-3333-3333-333333333333"]`
if got != want {
t.Fatalf("desktopPublishDedupKey() = %q, want %q", got, want)
}
}
func TestDesktopPublishDedupKeyEncodesAmbiguousPlatformIDs(t *testing.T) {
t.Parallel()
accountID := uuid.MustParse("33333333-3333-3333-3333-333333333333")
got := desktopPublishDedupKey(7, 11, 897, "sohu:hao", accountID)
want := `["publish","7","11","897","sohu:hao","33333333-3333-3333-3333-333333333333"]`
if got != want {
t.Fatalf("desktopPublishDedupKey() = %q, want %q", got, want)
}
}
func TestDesktopPublishDedupKeysFromTargetsSortsAndDedupes(t *testing.T) {
t.Parallel()
got := desktopPublishDedupKeysFromTargets([]publishTarget{
{dedupKey: "publish:1:1:2:b:account"},
{dedupKey: "publish:1:1:2:a:account"},
{dedupKey: "publish:1:1:2:b:account"},
{dedupKey: " "},
})
want := []string{"publish:1:1:2:a:account", "publish:1:1:2:b:account"}
if len(got) != len(want) {
t.Fatalf("desktopPublishDedupKeysFromTargets() length = %d, want %d; got %v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("desktopPublishDedupKeysFromTargets()[%d] = %q, want %q; got %v", i, got[i], want[i], got)
}
}
}
func TestPublishTargetPlatformsSortsAndDedupes(t *testing.T) {
t.Parallel()
got := publishTargetPlatforms([]publishTarget{
{accountSeed: &platformAccountSeed{PlatformID: "xiaohongshu"}},
{accountSeed: &platformAccountSeed{PlatformID: " weixin_channels "}},
{accountSeed: &platformAccountSeed{PlatformID: "xiaohongshu"}},
{accountSeed: &platformAccountSeed{PlatformID: " "}},
{},
})
want := []string{"weixin_channels", "xiaohongshu"}
if len(got) != len(want) {
t.Fatalf("publishTargetPlatforms() length = %d, want %d; got %v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("publishTargetPlatforms()[%d] = %q, want %q; got %v", i, got[i], want[i], got)
}
}
}
func TestShouldRequeuePublishTaskInsteadOfCreatingNewOnlyForUnknownPublish(t *testing.T) {
t.Parallel()
if !shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
Kind: "publish",
Status: "unknown",
}) {
t.Fatal("unknown publish tasks should be retried by requeueing the existing task")
}
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
Kind: "publish",
Status: "failed",
}) {
t.Fatal("failed publish tasks should keep create-new retry semantics")
}
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
Kind: "monitor",
Status: "unknown",
}) {
t.Fatal("monitor retry semantics are owned by desktop task reconcile")
}
if shouldRequeuePublishTaskInsteadOfCreatingNew(nil) {
t.Fatal("nil tasks should not be requeued")
}
}
func TestCreatePublishJobResponseForRequeuedTaskReportsRequeuedTaskID(t *testing.T) {
t.Parallel()
taskID := uuid.MustParse("55555555-5555-5555-5555-555555555555")
got := createPublishJobResponseForRequeuedTask(&repository.DesktopTask{DesktopID: taskID})
if got.JobID != "" {
t.Fatalf("JobID = %q, want empty for requeued task", got.JobID)
}
if len(got.TaskIDs) != 1 || got.TaskIDs[0] != taskID.String() {
t.Fatalf("TaskIDs = %v, want [%s]", got.TaskIDs, taskID)
}
if len(got.CreatedTaskIDs) != 0 {
t.Fatalf("CreatedTaskIDs = %v, want empty", got.CreatedTaskIDs)
}
if len(got.ExistingTaskIDs) != 0 {
t.Fatalf("ExistingTaskIDs = %v, want empty", got.ExistingTaskIDs)
}
if len(got.RequeuedTaskIDs) != 1 || got.RequeuedTaskIDs[0] != taskID.String() {
t.Fatalf("RequeuedTaskIDs = %v, want [%s]", got.RequeuedTaskIDs, taskID)
}
}
func TestLoadExistingPublishRecordTargetsDedupeUnknownTasks(t *testing.T) {
t.Parallel()
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
accountID := uuid.MustParse("44444444-4444-4444-4444-444444444444")
targets := []publishTarget{
{accountSeed: &platformAccountSeed{ID: 77}, account: &repository.DesktopAccount{DesktopID: accountID}},
}
_, err := loadExistingPublishRecordTargets(context.Background(), tx, 1, 2, 3, targets)
if err != nil {
t.Fatalf("loadExistingPublishRecordTargets() error = %v", err)
}
normalizedSQL := normalizeSQLWhitespace(tx.sql)
if !strings.Contains(normalizedSQL, "dt.status IN ('queued', 'in_progress', 'succeeded', 'unknown')") {
t.Fatalf("publish dedup lookup should include unknown task status; query:\n%s", tx.sql)
}
if !strings.Contains(normalizedSQL, "AND dt.tenant_id = $1") {
t.Fatalf("publish dedup lookup should constrain desktop_tasks by tenant for the payload index; query:\n%s", tx.sql)
}
if strings.Contains(normalizedSQL, "desktop_publish_jobs") {
t.Fatalf("publish dedup lookup should not load unused publish job metadata; query:\n%s", tx.sql)
}
}
func TestLockDesktopPublishDedupKeysUsesSingleArrayQuery(t *testing.T) {
t.Parallel()
tx := &capturePublishDedupLookupTx{}
err := lockDesktopPublishDedupKeys(context.Background(), tx, []string{"a", "b"})
if err != nil {
t.Fatalf("lockDesktopPublishDedupKeys() error = %v", err)
}
if tx.execCount != 1 {
t.Fatalf("lockDesktopPublishDedupKeys() exec count = %d, want 1", tx.execCount)
}
if !strings.Contains(tx.execSQL, "unnest($1::text[])") {
t.Fatalf("lockDesktopPublishDedupKeys() should use array unnest query; query:\n%s", tx.execSQL)
}
gotKeys, ok := tx.execArgs[0].([]string)
if !ok || len(gotKeys) != 2 || gotKeys[0] != "a" || gotKeys[1] != "b" {
t.Fatalf("lockDesktopPublishDedupKeys() args = %#v, want [a b]", tx.execArgs)
}
}
func TestLockDesktopPublishDedupKeysSkipsEmptyList(t *testing.T) {
t.Parallel()
tx := &capturePublishDedupLookupTx{}
err := lockDesktopPublishDedupKeys(context.Background(), tx, nil)
if err != nil {
t.Fatalf("lockDesktopPublishDedupKeys() error = %v", err)
}
if tx.execCount != 0 {
t.Fatalf("lockDesktopPublishDedupKeys() exec count = %d, want 0", tx.execCount)
}
}
func TestIsPostgresUniqueViolationMatchesConstraint(t *testing.T) {
t.Parallel()
err := fmt.Errorf("wrapped: %w", &pgconn.PgError{
Code: "23505",
ConstraintName: desktopPublishDedupActiveConstraint,
})
if !isPostgresUniqueViolation(err, desktopPublishDedupActiveConstraint) {
t.Fatal("isPostgresUniqueViolation() should match the dedup constraint")
}
if isPostgresUniqueViolation(&pgconn.PgError{Code: "23505", ConstraintName: "desktop_tasks_desktop_id_key"}, desktopPublishDedupActiveConstraint) {
t.Fatal("isPostgresUniqueViolation() should ignore other unique constraints")
}
if isPostgresUniqueViolation(&pgconn.PgError{Code: "23503", ConstraintName: desktopPublishDedupActiveConstraint}, desktopPublishDedupActiveConstraint) {
t.Fatal("isPostgresUniqueViolation() should require SQLSTATE 23505")
}
}
type capturePublishDedupLookupTx struct {
sql string
args []any
execSQL string
execArgs []any
execCount int
rows pgx.Rows
}
func (tx *capturePublishDedupLookupTx) Begin(context.Context) (pgx.Tx, error) { return nil, nil }
func (tx *capturePublishDedupLookupTx) Commit(context.Context) error { return nil }
func (tx *capturePublishDedupLookupTx) Rollback(context.Context) error { return nil }
func (tx *capturePublishDedupLookupTx) CopyFrom(context.Context, pgx.Identifier, []string, pgx.CopyFromSource) (int64, error) {
return 0, nil
}
func (tx *capturePublishDedupLookupTx) SendBatch(context.Context, *pgx.Batch) pgx.BatchResults {
return nil
}
func (tx *capturePublishDedupLookupTx) LargeObjects() pgx.LargeObjects { return pgx.LargeObjects{} }
func (tx *capturePublishDedupLookupTx) Prepare(context.Context, string, string) (*pgconn.StatementDescription, error) {
return nil, nil
}
func (tx *capturePublishDedupLookupTx) Exec(_ context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) {
tx.execCount++
tx.execSQL = sql
tx.execArgs = arguments
return pgconn.CommandTag{}, nil
}
func (tx *capturePublishDedupLookupTx) Query(_ context.Context, sql string, args ...any) (pgx.Rows, error) {
tx.sql = sql
tx.args = args
return tx.rows, nil
}
func (tx *capturePublishDedupLookupTx) QueryRow(context.Context, string, ...any) pgx.Row {
return nil
}
func (tx *capturePublishDedupLookupTx) Conn() *pgx.Conn { return nil }
type emptyPublishDedupRows struct{}
func (r emptyPublishDedupRows) Close() {}
func (r emptyPublishDedupRows) Err() error { return nil }
func (r emptyPublishDedupRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
func (r emptyPublishDedupRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
func (r emptyPublishDedupRows) Next() bool { return false }
func (r emptyPublishDedupRows) Scan(...any) error { return nil }
func (r emptyPublishDedupRows) Values() ([]any, error) { return nil, nil }
func (r emptyPublishDedupRows) RawValues() [][]byte { return nil }
func (r emptyPublishDedupRows) Conn() *pgx.Conn { return nil }
func normalizeSQLWhitespace(sql string) string {
return strings.Join(strings.Fields(sql), " ")
}