thread-master/apps/backend/internal/module/threads/usecase/service_test.go

171 lines
6.0 KiB
Go

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)
}