package usecase_test import ( "context" "testing" "apps/backend/internal/module/threads/domain" "apps/backend/internal/module/threads/provider" "apps/backend/internal/module/threads/repository" "apps/backend/internal/module/threads/usecase" "github.com/stretchr/testify/require" ) func newThreadsSvc(failEx, failRef bool) (*usecase.Service, *repository.MemoryStore, *provider.FakeProvider) { store := repository.NewMemory() fake := &provider.FakeProvider{ CallbackBase: "http://127.0.0.1:8888/api/v1/threads-accounts/oauth/callback", FailExchange: failEx, FailRefresh: failRef, } svc := usecase.New(store, fake, "test-secret-key") svc.APIPublicBase = "http://127.0.0.1:8888" svc.WebBase = "http://127.0.0.1:5173" return svc, store, fake } func TestTH_01_OAuthStart(t *testing.T) { svc, _, _ := newThreadsSvc(false, false) url, state, err := svc.OAuthStart(context.Background(), 1_000_001) require.NoError(t, err) require.NotEmpty(t, state) require.Contains(t, url, "state=") require.Contains(t, url, "code=fake") } func TestTH_02_CallbackSuccess(t *testing.T) { svc, store, _ := newThreadsSvc(false, false) uid := int64(1_000_002) _, state, err := svc.OAuthStart(context.Background(), uid) require.NoError(t, err) acc, err := svc.OAuthCallback(context.Background(), "fake", state, "") require.NoError(t, err) require.Equal(t, domain.ConnectionConnected, acc.Connection) require.True(t, acc.IsUsable) require.Equal(t, uid, acc.OwnerUID) require.NotEmpty(t, acc.AccessTokenEnc) // token not equal plaintext in public sense require.NotContains(t, acc.AccessTokenEnc, "fake-access") list, err := store.ListByOwner(context.Background(), uid) require.NoError(t, err) require.Len(t, list, 1) } func TestTH_03_UserDenied(t *testing.T) { svc, _, _ := newThreadsSvc(false, false) _, state, err := svc.OAuthStart(context.Background(), 1_000_003) require.NoError(t, err) _, err = svc.OAuthCallback(context.Background(), "", state, "access_denied") require.ErrorIs(t, err, domain.ErrOAuthDenied) } func TestTH_04_TokenExchangeFail(t *testing.T) { svc, _, _ := newThreadsSvc(true, false) _, state, err := svc.OAuthStart(context.Background(), 1_000_004) require.NoError(t, err) _, err = svc.OAuthCallback(context.Background(), "fake", state, "") require.ErrorIs(t, err, domain.ErrOAuthExchange) } func TestTH_05_List(t *testing.T) { svc, _, _ := newThreadsSvc(false, false) uid := int64(1_000_005) _, st, _ := svc.OAuthStart(context.Background(), uid) _, err := svc.OAuthCallback(context.Background(), "fake", st, "") require.NoError(t, err) list, err := svc.List(context.Background(), uid) require.NoError(t, err) require.Len(t, list, 1) require.True(t, list[0].SessionExpiresAt > 0) } func TestTH_06_RefreshOk(t *testing.T) { svc, _, _ := newThreadsSvc(false, false) uid := int64(1_000_006) _, st, _ := svc.OAuthStart(context.Background(), uid) acc, err := svc.OAuthCallback(context.Background(), "fake", st, "") require.NoError(t, err) out, err := svc.Refresh(context.Background(), uid, acc.ID) require.NoError(t, err) require.True(t, out.IsUsable) require.Equal(t, domain.ConnectionConnected, out.Connection) require.True(t, out.SessionRefreshedAt >= acc.SessionRefreshedAt) } func TestTH_07_RefreshFail(t *testing.T) { svc, store, fake := newThreadsSvc(false, false) uid := int64(1_000_007) _, st, _ := svc.OAuthStart(context.Background(), uid) acc, err := svc.OAuthCallback(context.Background(), "fake", st, "") require.NoError(t, err) fake.FailRefresh = true // poison refresh token acc.RefreshTokenEnc, _ = domain.Seal("test-secret-key", "bad-token") require.NoError(t, store.Update(context.Background(), acc)) out, err := svc.Refresh(context.Background(), uid, acc.ID) require.ErrorIs(t, err, domain.ErrRefreshFailed) require.False(t, out.IsUsable) require.Equal(t, domain.ConnectionError, out.Connection) } func TestTH_08_Disconnect(t *testing.T) { svc, _, _ := newThreadsSvc(false, false) uid := int64(1_000_008) _, st, _ := svc.OAuthStart(context.Background(), uid) acc, err := svc.OAuthCallback(context.Background(), "fake", st, "") require.NoError(t, err) require.NoError(t, svc.Disconnect(context.Background(), uid, acc.ID)) list, err := svc.List(context.Background(), uid) require.NoError(t, err) require.Empty(t, list) } func TestTH_11_ReconnectUpsertsSession(t *testing.T) { svc, _, fake := newThreadsSvc(false, false) fake.FixedUserID = "threads_uid_fixed" fake.FixedUsername = "harbor_main" uid := int64(1_000_012) _, st1, _ := svc.OAuthStart(context.Background(), uid) acc1, err := svc.OAuthCallback(context.Background(), "fake", st1, "") require.NoError(t, err) oldTok := acc1.AccessTokenEnc _, st2, _ := svc.OAuthStart(context.Background(), uid) acc2, err := svc.OAuthCallback(context.Background(), "fake", st2, "") require.NoError(t, err) require.Equal(t, acc1.ID, acc2.ID, "same Threads user must keep same account id") require.NotEqual(t, oldTok, acc2.AccessTokenEnc, "session tokens must be refreshed") require.True(t, acc2.IsUsable) require.Equal(t, domain.ConnectionConnected, acc2.Connection) list, err := svc.List(context.Background(), uid) require.NoError(t, err) require.Len(t, list, 1) } func TestTH_09_RequiresCallbackNotBypass(t *testing.T) { // Live completion requires OAuthCallback path (has encrypted token). svc, _, _ := newThreadsSvc(false, false) uid := int64(1_000_009) _, st, _ := svc.OAuthStart(context.Background(), uid) acc, err := svc.OAuthCallback(context.Background(), "fake", st, "") require.NoError(t, err) require.NotEmpty(t, acc.AccessTokenEnc) require.True(t, acc.IsUsable) } func TestTH_10_CrossUidDenied(t *testing.T) { svc, _, _ := newThreadsSvc(false, false) uidA := int64(1_000_010) uidB := int64(1_000_011) _, st, _ := svc.OAuthStart(context.Background(), uidA) acc, err := svc.OAuthCallback(context.Background(), "fake", st, "") require.NoError(t, err) _, err = svc.Refresh(context.Background(), uidB, acc.ID) require.ErrorIs(t, err, domain.ErrForbidden) err = svc.Disconnect(context.Background(), uidB, acc.ID) require.ErrorIs(t, err, domain.ErrForbidden) }