package api import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "sync/atomic" "testing" "time" "git.ipao.vip/rogee/creator-hub/internal/account" "git.ipao.vip/rogee/creator-hub/internal/creator" hub "git.ipao.vip/rogee/creator-hub/internal/environment" ) func privateWorkerFixture(t *testing.T) (*creator.Store, creator.ListenerState) { t.Helper() databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL") if databaseURL == "" { t.Skip("requires CREATORHUB_POSTGRES_TEST_URL") } store, accounts, ctx := openCreatorIntegrationStoreForAPITest(t, databaseURL) owner := account.Account{ID: "dm-owner", Platform: creator.PlatformDouyin, Name: "本人", PlatformAccountKey: "123", CredentialReference: account.CredentialReference{ID: "dm-credential", Provider: "os_keyring"}, CredentialKey: "creatorhub/dm-owner/cookies"} if err := accounts.CreateAccount(ctx, owner, &testCredentialBridge{values: make(map[string]string)}); err != nil { t.Fatal(err) } if _, err := store.RecordVerifiedLoginResult(ctx, owner.ID, "123"); err != nil { t.Fatal(err) } state, err := store.SetListenerEnabled(ctx, owner.ID, true) if err != nil { t.Fatal(err) } return store, state } func TestPrivateGatewaySendAndSync(t *testing.T) { store, state := privateWorkerFixture(t) ctx := context.Background() var mode atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if mode.Load() == 1 { w.WriteHeader(502) return } var payload map[string]any json.NewDecoder(r.Body).Decode(&payload) if payload["expected_uid"] != "123" { t.Errorf("identity=%v", payload) } if r.URL.Path == "/v1/browsers/browser/douyin/action" { w.Write([]byte(`{"status":"succeeded","success":true,"message":{"server_id":"100"}}`)) return } if r.URL.Path != "/v1/browsers/browser/douyin/inbox" { t.Errorf("path=%s", r.URL.Path) } if mode.Load() == 2 { w.Write([]byte(`{"status":"succeeded","account_uid":"123","messages":[]}`)) return } if mode.Load() == 3 { w.Write([]byte(`{"status":"failed","code":"IM_SDK_NOT_READY"}`)) return } w.Write([]byte(`{"status":"succeeded","account_uid":"123","messages":[{"peer_uid":"456","server_id":"101","sender_uid":"456","message_type":"7","content":"{\"aweType\":700,\"text\":\"你好\"}","created_at":"1700000000000"}],"checkpoints":{"456":"101"}}`)) })) defer server.Close() accounts := listenerAccountsStub{gateway: hub.Gateway{Endpoint: server.URL, Token: "token"}, environment: hub.EnvironmentContext{Env: hub.Env{Alias: "browser", Gateway: "gateway"}, RuntimeID: "runtime", RuntimeNetworkID: "network", BindingVersion: 7}} input := creator.PrivateMessageSendInput{RequestID: "send-one", AccountID: state.AccountID, PeerUID: "456", Text: "你好"} reservation, err := store.BeginPrivateMessage(ctx, input) if err != nil { t.Fatal(err) } result := gatewayPrivateMessageSender(accounts, store)(ctx, input, reservation) if result.State != "succeeded" || result.ServerID != "100" { t.Fatalf("send=%+v", result) } checkpoints, err := syncPrivateInbox(ctx, accounts, store, state, map[string]string{"456": "99"}) if err != nil || checkpoints["456"] != "101" { t.Fatalf("sync=%v %v", checkpoints, err) } page, err := store.ListPrivateMessages(ctx, state.AccountID, "456", 1, 25) if err != nil || page.Total != 2 { t.Fatalf("page=%+v %v", page, err) } for _, m := range []int32{1, 2, 3} { mode.Store(m) if _, err := syncPrivateInbox(ctx, accounts, store, state, checkpoints); err == nil { t.Fatalf("accepted mode %d", m) } } mode.Store(1) result = gatewayPrivateMessageSender(accounts, store)(ctx, input, reservation) if result.State != "unknown" { t.Fatalf("HTTP error=%+v", result) } invalid := accounts invalid.environmentErr = errors.New("missing environment") result = gatewayPrivateMessageSender(invalid, store)(ctx, input, reservation) if result.State != "failed" { t.Fatalf("missing environment=%+v", result) } if _, err := syncPrivateInbox(ctx, invalid, store, state, nil); err == nil { t.Fatal("missing environment accepted") } if _, err := store.SetListenerEnabled(ctx, state.AccountID, false); err != nil { t.Fatal(err) } result = gatewayPrivateMessageSender(accounts, store)(ctx, input, reservation) if result.State != "failed" { t.Fatalf("disabled=%+v", result) } if _, err := privateListenerState(ctx, store, "missing"); !errors.Is(err, creator.ErrNotFound) { t.Fatalf("missing=%v", err) } } func TestPrivateSyncWorkerRecoveryAndErrors(t *testing.T) { for _, failure := range []bool{false, true} { t.Run(map[bool]string{false: "success", true: "failure"}[failure], func(t *testing.T) { store, state := privateWorkerFixture(t) input := creator.PrivateMessageSendInput{RequestID: "interrupted", AccountID: state.AccountID, PeerUID: "456", Text: "你好"} if _, err := store.BeginPrivateMessage(context.Background(), input); err != nil { t.Fatal(err) } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if failure { w.WriteHeader(502) return } w.Write([]byte(`{"status":"succeeded","account_uid":"123","messages":[],"checkpoints":{}}`)) })) defer server.Close() accounts := listenerAccountsStub{gateway: hub.Gateway{Endpoint: server.URL}, environment: hub.EnvironmentContext{Env: hub.Env{Alias: "browser", Gateway: "gateway"}, RuntimeID: "runtime", RuntimeNetworkID: "network", BindingVersion: 7}} ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan struct{}) go func() { defer close(done); RunPrivateMessageSync(ctx, accounts, store) }() deadline := time.Now().Add(3 * time.Second) matched := false for time.Now().Before(deadline) { statuses, err := store.ListPrivateSyncStatus(context.Background()) if err != nil { t.Fatal(err) } if len(statuses) == 1 && (failure && statuses[0].Error != "" || !failure && statuses[0].SyncedAt != nil) { matched = true break } time.Sleep(10 * time.Millisecond) } if !matched { t.Fatal("sync result not persisted") } cancel() select { case <-done: case <-time.After(time.Second): t.Fatal("worker did not stop") } page, err := store.ListPrivateMessages(context.Background(), state.AccountID, "456", 1, 25) if err != nil || page.Data[0].State != "unknown" { t.Fatalf("interrupted=%+v %v", page, err) } }) } }