You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

测试Passport Magic策略时createUser方法Jest断言失败

问题:NestJS Passport Magic策略测试中createUser断言失败但日志显示已执行

测试基于Magic SDK实现的NestJS Passport Strategy时出现异常:日志明确显示usersService.createUser已执行,但Jest断言该方法调用次数为0,而usersService.getAuthExistingUser的断言正常通过。推测是jest.spyOn未绑定到正确实例,相关代码、错误信息及测试日志如下:


magic.strategy.ts

import { Magic } from '@magic-sdk/admin'
import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { PassportStrategy } from '@nestjs/passport'
import { DoneFunc, Strategy } from 'passport-magic'
import { UserFromDid } from '../../types/magic.types'
import { UsersService } from '../../users/users.service'

@Injectable()
export class MagicStrategy extends PassportStrategy(Strategy, 'magic') {
    private readonly logger = new Logger(MagicStrategy.name)

    constructor(
        private readonly config: ConfigService, 
        private readonly userService: UsersService
    ) {
        const magic = new Magic(config.get('MAGIC_SECRET_KEY'))

        super({
            magicInstance: magic,
        },
        async function (user: UserFromDid, done: DoneFunc) {
            const userMetadata = await magic.users.getMetadataByIssuer(user.issuer)

            const existingUser = await userService.getAuthExistingUser(userMetadata.email)
            let createdUser: { user_id: string }

            if (!existingUser) {
                console.log('TEST IS PASSING HERE')
                createdUser = await userService.createUser({
                    email: userMetadata.email,
                    wallet_pubaddress: userMetadata.publicAddress,
                    last_login_at: user.claim.iat,
                    issuer: user.issuer,
                })
                console.log('CREATED USER', createdUser)

                done(null, existingUser ? { user_id: existingUser.user_id } : createdUser)
            } else {
                if (user.claim.iat <= existingUser.last_login_at.getTime() / 1000) {
                    this.logger.error(`Replay attack detected for user ${user.issuer}`)
                    return done(null, false, {
                        message: `Replay attack detected for user ${user.issuer}}.`,
                    })
                }

                await userService.updateLastLogin(user.issuer, user.claim.iat)

                return done(null, { user_id: existingUser.user_id })
            }
        },
        )
    }
}

magic.strategy.spec.ts

import { ConfigService } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { Request } from 'express'
import { PrismaService } from '../../prisma.service'
import { UsersService } from '../../users/users.service'
import { MagicStrategy } from './magic.strategy'

describe('MagicStrategy', () => {
    let magicStrategy: MagicStrategy
    let usersService: UsersService
    let configService: ConfigService

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [MagicStrategy, UsersService, ConfigService, PrismaService],
        })
        .overrideProvider(UsersService)
        .useValue({
            getAuthExistingUser: jest.fn(),
            createUser: jest.fn(),
        })
        .overrideProvider(ConfigService)
        .useValue({ get: jest.fn() })
        .compile()

        magicStrategy = module.get<MagicStrategy>(MagicStrategy)
        usersService = module.get<UsersService>(UsersService)
        configService = module.get<ConfigService>(ConfigService)
    })

    describe('validate', () => {
        it('should create a new user if the user does not exist', async () => {
            const user = {
                issuer: 'someissuer',
                claim: {
                    iat: 1234567890,
                },
            }
            const createdUser = { user_id: 'newuserid' }
            const done = jest.fn()
            jest.spyOn(usersService, 'getAuthExistingUser').mockResolvedValue(null)
            jest.spyOn(usersService, 'createUser').mockResolvedValue({ user_id: 'user-id' })

            await magicStrategy.authenticate({ headers: { authorization: 'bearer 1234556' } } as Request)

            expect(configService.get).toHaveBeenCalledWith('MAGIC_SECRET_KEY')
            expect(usersService.getAuthExistingUser).toHaveBeenCalledWith('email')
            expect(usersService.createUser).toHaveBeenCalledWith({
                email: 'email',
                wallet_pubaddress: 'publicAddress',
                last_login_at: user.claim.iat,
                issuer: user.issuer,
            })
            expect(done).toHaveBeenCalledWith(null, createdUser)
        })
    })
})

错误信息

Error: expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: {"email": "email", "issuer": "someissuer", "last_login_at": 1234567890, "wallet_pubaddress": "publicAddress"}

Number of calls: 0Jest

测试日志

FAIL  src/auth/passport-strategies/magic.strategy.spec.ts
  ● Console

    console.log
      TEST IS PASSING HERE

      at MagicStrategy.log (auth/passport-strategies/magic.strategy.ts:26:14)

    console.log
      CREATED USER { user_id: 'user-id' }

      at MagicStrategy.log (auth/passport-strategies/magic.strategy.ts:33:14)

  ● MagicStrategy › validate › should create a new user if the user does not exist

    expect(jest.fn()).toHaveBeenCalledWith(...expected)

    Expected: {"email": "email", "issuer": "someissuer", "last_login_at": 1234567890, "wallet_pubaddress": "publicAddress"}

    Number of calls: 0

解决方案

1. 修正回调函数的this绑定与服务引用

构造函数中传给super的验证回调使用普通函数时,会丢失类实例上下文,且直接引用构造函数参数的userService可能导致测试mock无法捕获调用。改为箭头函数并使用this.userService确保引用注入的服务实例:

super({
    magicInstance: magic,
},
async (user: UserFromDid, done: DoneFunc) => { // 替换为箭头函数
    const userMetadata = await magic.users.getMetadataByIssuer(user.issuer)

    const existingUser = await this.userService.getAuthExistingUser(userMetadata.email) // 使用this.userService
    let createdUser: { user_id: string }

    if (!existingUser) {
        console.log('TEST IS PASSING HERE')
        createdUser = await this.userService.createUser({ // 使用this.userService
            email: userMetadata.email,
            wallet_pubaddress: userMetadata.publicAddress,
            last_login_at: user.claim.iat,
            issuer: user.issuer,
        })
        console.log('CREATED USER', createdUser)

        done(null, existingUser ? { user_id: existingUser.user_id } : createdUser)
    } else {
        if (user.claim.iat <= existingUser.last_login_at.getTime() / 1000) {
            this.logger.error(`Replay attack detected for user ${user.issuer}`)
            return done(null, false, {
                message: `Replay attack detected for user ${user.issuer}.`,
            })
        }

        await this.userService.updateLastLogin(user.issuer, user.claim.iat) // 使用this.userService

        return done(null, { user_id: existingUser.user_id })
    }
},
)

2. Mock Magic SDK避免真实API调用

测试中未mockMagic类,导致创建真实实例并调用API,可能引发不可预期的行为。在测试文件顶部添加mock:

import { Magic } from '@magic-sdk/admin'

// Mock Magic SDK
jest.mock('@magic-sdk/admin', () => {
    const mockUsersService = {
        getMetadataByIssuer: jest.fn().mockResolvedValue({
            email: 'email',
            publicAddress: 'publicAddress',
        }),
    }
    return {
        Magic: jest.fn().mockImplementation(() => ({
            users: mockUsersService,
        })),
    }
})

3. 完善测试中的mock配置

确保configService.get返回假密钥,避免创建Magic实例出错:

beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
        providers: [MagicStrategy, UsersService, ConfigService, PrismaService],
    })
    .overrideProvider(UsersService)
    .useValue({
        getAuthExistingUser: jest.fn(),
        createUser: jest.fn(),
    })
    .overrideProvider(ConfigService)
    .useValue({ get: jest.fn() })
    .compile()

    magicStrategy = module.get<MagicStrategy>(MagicStrategy)
    usersService = module.get<UsersService>(UsersService)
    configService = module.get<ConfigService>(ConfigService)
    
    // 配置configService返回假密钥
    jest.spyOn(configService, 'get').mockReturnValue('fake-magic-secret')
})

4. 修正done函数的断言问题

测试中定义的done函数未被authenticate使用,Passport内部会生成自己的done回调。若需断言用户返回结果,可通过authenticate的返回值或mock Passport的回调逻辑,或直接移除无效的done断言。


内容的提问来源于stack exchange,提问作者Jaro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 03:37:35