| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- //go:build unit
- package service
- import (
- "context"
- "database/sql"
- "testing"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/ent/authidentity"
- "github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
- "github.com/Wei-Shaw/sub2api/ent/enttest"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/stretchr/testify/require"
- "entgo.io/ent/dialect"
- entsql "entgo.io/ent/dialect/sql"
- _ "modernc.org/sqlite"
- )
- func newAdminServiceAuthIdentityBindingTestClient(t *testing.T) *dbent.Client {
- t.Helper()
- db, err := sql.Open("sqlite", "file:admin_service_auth_identity_binding?mode=memory&cache=shared&_fk=1")
- require.NoError(t, err)
- t.Cleanup(func() { _ = db.Close() })
- _, err = db.Exec("PRAGMA foreign_keys = ON")
- require.NoError(t, err)
- drv := entsql.OpenDB(dialect.SQLite, db)
- client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
- t.Cleanup(func() { _ = client.Close() })
- return client
- }
- func TestAdminServiceBindUserAuthIdentityCreatesCanonicalAndChannelBinding(t *testing.T) {
- client := newAdminServiceAuthIdentityBindingTestClient(t)
- ctx := context.Background()
- user, err := client.User.Create().
- SetEmail("bind-target@example.com").
- SetPasswordHash("hash").
- SetRole(RoleUser).
- SetStatus(StatusActive).
- Save(ctx)
- require.NoError(t, err)
- svc := &adminServiceImpl{
- userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
- entClient: client,
- }
- result, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
- ProviderType: "wechat",
- ProviderKey: "wechat-main",
- ProviderSubject: "union-123",
- Metadata: map[string]any{"source": "admin-repair"},
- Channel: &AdminBindAuthIdentityChannelInput{
- Channel: "open",
- ChannelAppID: "wx-open",
- ChannelSubject: "openid-123",
- Metadata: map[string]any{"scene": "migration"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, user.ID, result.UserID)
- require.Equal(t, "wechat", result.ProviderType)
- require.Equal(t, "wechat-main", result.ProviderKey)
- require.NotNil(t, result.VerifiedAt)
- require.NotNil(t, result.Channel)
- require.Equal(t, "open", result.Channel.Channel)
- identity, err := client.AuthIdentity.Query().
- Where(
- authidentity.ProviderTypeEQ("wechat"),
- authidentity.ProviderKeyEQ("wechat-main"),
- authidentity.ProviderSubjectEQ("union-123"),
- ).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, user.ID, identity.UserID)
- require.Equal(t, "admin-repair", identity.Metadata["source"])
- require.NotNil(t, identity.VerifiedAt)
- channel, err := client.AuthIdentityChannel.Query().
- Where(
- authidentitychannel.ProviderTypeEQ("wechat"),
- authidentitychannel.ProviderKeyEQ("wechat-main"),
- authidentitychannel.ChannelEQ("open"),
- authidentitychannel.ChannelAppIDEQ("wx-open"),
- authidentitychannel.ChannelSubjectEQ("openid-123"),
- ).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, identity.ID, channel.IdentityID)
- require.Equal(t, "migration", channel.Metadata["scene"])
- }
- func TestAdminServiceBindUserAuthIdentityRejectsOtherOwner(t *testing.T) {
- client := newAdminServiceAuthIdentityBindingTestClient(t)
- ctx := context.Background()
- owner, err := client.User.Create().
- SetEmail("owner@example.com").
- SetPasswordHash("hash").
- SetRole(RoleUser).
- SetStatus(StatusActive).
- Save(ctx)
- require.NoError(t, err)
- target, err := client.User.Create().
- SetEmail("target@example.com").
- SetPasswordHash("hash").
- SetRole(RoleUser).
- SetStatus(StatusActive).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.AuthIdentity.Create().
- SetUserID(owner.ID).
- SetProviderType("oidc").
- SetProviderKey("https://issuer.example").
- SetProviderSubject("subject-1").
- Save(ctx)
- require.NoError(t, err)
- svc := &adminServiceImpl{
- userRepo: &userRepoStub{user: &User{ID: target.ID, Email: target.Email, Status: StatusActive}},
- entClient: client,
- }
- _, err = svc.BindUserAuthIdentity(ctx, target.ID, AdminBindAuthIdentityInput{
- ProviderType: "oidc",
- ProviderKey: "https://issuer.example",
- ProviderSubject: "subject-1",
- })
- require.Error(t, err)
- require.Equal(t, "AUTH_IDENTITY_OWNERSHIP_CONFLICT", infraerrors.Reason(err))
- }
- func TestAdminServiceBindUserAuthIdentityIsIdempotentForSameUser(t *testing.T) {
- client := newAdminServiceAuthIdentityBindingTestClient(t)
- ctx := context.Background()
- user, err := client.User.Create().
- SetEmail("same-user@example.com").
- SetPasswordHash("hash").
- SetRole(RoleUser).
- SetStatus(StatusActive).
- Save(ctx)
- require.NoError(t, err)
- svc := &adminServiceImpl{
- userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
- entClient: client,
- }
- first, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
- ProviderType: "oidc",
- ProviderKey: "https://issuer.example",
- ProviderSubject: "subject-2",
- Metadata: map[string]any{"source": "first"},
- })
- require.NoError(t, err)
- second, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
- ProviderType: "oidc",
- ProviderKey: "https://issuer.example",
- ProviderSubject: "subject-2",
- Metadata: map[string]any{"source": "second"},
- })
- require.NoError(t, err)
- require.Equal(t, first.UserID, second.UserID)
- require.Equal(t, "second", second.Metadata["source"])
- identities, err := client.AuthIdentity.Query().
- Where(
- authidentity.ProviderTypeEQ("oidc"),
- authidentity.ProviderKeyEQ("https://issuer.example"),
- authidentity.ProviderSubjectEQ("subject-2"),
- ).
- All(ctx)
- require.NoError(t, err)
- require.Len(t, identities, 1)
- require.Equal(t, "second", identities[0].Metadata["source"])
- }
- func TestAdminServiceBindUserAuthIdentityReusesLegacyWeChatAliasRecords(t *testing.T) {
- client := newAdminServiceAuthIdentityBindingTestClient(t)
- ctx := context.Background()
- user, err := client.User.Create().
- SetEmail("wechat-alias@example.com").
- SetPasswordHash("hash").
- SetRole(RoleUser).
- SetStatus(StatusActive).
- Save(ctx)
- require.NoError(t, err)
- legacyIdentity, err := client.AuthIdentity.Create().
- SetUserID(user.ID).
- SetProviderType("wechat").
- SetProviderKey("wechat").
- SetProviderSubject("union-legacy-123").
- SetMetadata(map[string]any{"source": "legacy"}).
- Save(ctx)
- require.NoError(t, err)
- legacyChannel, err := client.AuthIdentityChannel.Create().
- SetIdentityID(legacyIdentity.ID).
- SetProviderType("wechat").
- SetProviderKey("wechat").
- SetChannel("open").
- SetChannelAppID("wx-open").
- SetChannelSubject("openid-legacy-123").
- SetMetadata(map[string]any{"scene": "legacy"}).
- Save(ctx)
- require.NoError(t, err)
- svc := &adminServiceImpl{
- userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
- entClient: client,
- }
- result, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
- ProviderType: "wechat",
- ProviderKey: "wechat-main",
- ProviderSubject: "union-legacy-123",
- Metadata: map[string]any{"source": "admin-repair"},
- Channel: &AdminBindAuthIdentityChannelInput{
- Channel: "open",
- ChannelAppID: "wx-open",
- ChannelSubject: "openid-legacy-123",
- Metadata: map[string]any{"scene": "admin-repair"},
- },
- })
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "wechat-main", result.ProviderKey)
- require.NotNil(t, result.Channel)
- require.Equal(t, "open", result.Channel.Channel)
- identity, err := client.AuthIdentity.Get(ctx, legacyIdentity.ID)
- require.NoError(t, err)
- require.Equal(t, "wechat-main", identity.ProviderKey)
- require.Equal(t, "admin-repair", identity.Metadata["source"])
- channel, err := client.AuthIdentityChannel.Get(ctx, legacyChannel.ID)
- require.NoError(t, err)
- require.Equal(t, "wechat-main", channel.ProviderKey)
- require.Equal(t, legacyIdentity.ID, channel.IdentityID)
- require.Equal(t, "admin-repair", channel.Metadata["scene"])
- identityCount, err := client.AuthIdentity.Query().
- Where(
- authidentity.ProviderTypeEQ("wechat"),
- authidentity.ProviderSubjectEQ("union-legacy-123"),
- ).
- Count(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, identityCount)
- channelCount, err := client.AuthIdentityChannel.Query().
- Where(
- authidentitychannel.ProviderTypeEQ("wechat"),
- authidentitychannel.ChannelEQ("open"),
- authidentitychannel.ChannelAppIDEQ("wx-open"),
- authidentitychannel.ChannelSubjectEQ("openid-legacy-123"),
- ).
- Count(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, channelCount)
- }
- func TestAdminServiceBindUserAuthIdentityRejectsInvalidProviderType(t *testing.T) {
- client := newAdminServiceAuthIdentityBindingTestClient(t)
- ctx := context.Background()
- user, err := client.User.Create().
- SetEmail("invalid-provider@example.com").
- SetPasswordHash("hash").
- SetRole(RoleUser).
- SetStatus(StatusActive).
- Save(ctx)
- require.NoError(t, err)
- svc := &adminServiceImpl{
- userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
- entClient: client,
- }
- _, err = svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
- ProviderType: "github",
- ProviderKey: "github-main",
- ProviderSubject: "subject-3",
- })
- require.Error(t, err)
- require.Equal(t, "INVALID_INPUT", infraerrors.Reason(err))
- }
|