mistykey/packages/backend/test/unit/SigninWithPasskeyApiService.ts
2025-11-05 19:29:50 -05:00

185 lines
6.9 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IncomingHttpHeaders } from 'node:http';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { FastifyReply, FastifyRequest } from 'fastify';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { HttpHeader } from 'fastify/types/utils.js';
import { MockMetadata, ModuleMocker } from 'jest-mock';
import { FakeSkRateLimiterService } from '../misc/FakeSkRateLimiterService.js';
import { MiUser } from '@/models/User.js';
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { DI } from '@/di-symbols.js';
import { CoreModule } from '@/core/CoreModule.js';
import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { SigninService } from '@/server/api/SigninService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { CacheManagementService } from '@/core/CacheManagementService.js';
import { ServerModule } from '@/server/ServerModule.js';
const moduleMocker = new ModuleMocker(global);
class FakeSigninService {
public signin(..._args: any): any {
return true;
}
}
class DummyFastifyReply {
public statusCode: number;
code(num: number): void {
this.statusCode = num;
}
header(_key: HttpHeader, _value: any): void {
}
}
class DummyFastifyRequest {
public ip: string;
public body: { credential: any, context: string };
public headers: IncomingHttpHeaders = { 'accept': 'application/json' };
constructor(body?: any) {
this.ip = '0.0.0.0';
this.body = body;
}
}
type ApiFastifyRequestType = FastifyRequest<{
Body: {
credential?: AuthenticationResponseJSON;
context?: string;
};
}>;
describe('SigninWithPasskeyApiService', () => {
let app: TestingModule;
let passkeyApiService: SigninWithPasskeyApiService;
let usersRepository: UsersRepository;
let userProfilesRepository: UserProfilesRepository;
let webAuthnService: WebAuthnService;
let idService: IdService;
let cacheManagementService: CacheManagementService;
let FakeWebauthnVerify: ()=>Promise<string>;
async function createUser(data: Partial<MiUser> = {}) {
await usersRepository.insert(data);
return await usersRepository.findOneByOrFail({ id: data.id });
}
async function createUserProfile(data: Partial<MiUserProfile> = {}) {
await userProfilesRepository.insert(data);
return await userProfilesRepository.findOneByOrFail({ userId: data.userId });
}
beforeAll(async () => {
app = await Test.createTestingModule({
imports: [GlobalModule, CoreModule, ServerModule],
}).useMocker((token) => {
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.overrideProvider(SkRateLimiterService).useClass(FakeSkRateLimiterService)
.overrideProvider(SigninService).useClass(FakeSigninService)
.compile();
await app.init();
app.enableShutdownHooks();
passkeyApiService = app.get<SigninWithPasskeyApiService>(SigninWithPasskeyApiService);
usersRepository = app.get<UsersRepository>(DI.usersRepository);
userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
webAuthnService = app.get<WebAuthnService>(WebAuthnService);
idService = app.get<IdService>(IdService);
cacheManagementService = app.get(CacheManagementService);
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
const uid = idService.gen();
FakeWebauthnVerify = async () => {
return uid;
};
jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify);
const dummyUser = {
id: uid, username: uid, usernameLower: uid.toLowerCase(), uri: null, host: null,
};
const dummyProfile = {
userId: uid,
password: 'qwerty',
usePasswordLessLogin: true,
};
await createUser(dummyUser);
await createUserProfile(dummyProfile);
});
afterEach(async () => {
await userProfilesRepository.delete({});
await usersRepository.delete({});
cacheManagementService.clear();
});
describe('Get Passkey Options', () => {
it('Should return passkey Auth Options', async () => {
const req = new DummyFastifyRequest({}) as ApiFastifyRequestType;
const res = new DummyFastifyReply() as unknown as FastifyReply;
const res_body = await passkeyApiService.signin(req, res);
expect(res.statusCode).toBe(200);
expect((res_body as any).option).toBeDefined();
expect(typeof (res_body as any).context).toBe('string');
});
});
describe('Try Passkey Auth', () => {
it('Should Success', async () => {
const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType;
const res = new DummyFastifyReply() as FastifyReply;
const res_body = await passkeyApiService.signin(req, res);
expect((res_body as any).signinResponse).toBeDefined();
});
it('Should return 400 Without Auth Context', async () => {
const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType;
const res = new DummyFastifyReply() as FastifyReply;
const res_body = await passkeyApiService.signin(req, res);
expect(res.statusCode).toBe(400);
expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1');
});
it('Should return 403 When Challenge Verify fail', async () => {
const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
const res = new DummyFastifyReply() as FastifyReply;
jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication')
.mockImplementation(async () => {
throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
});
const res_body = await passkeyApiService.signin(req, res);
expect(res.statusCode).toBe(403);
expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
});
it('Should return 403 When The user not Enabled Passwordless login', async () => {
const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
const res = new DummyFastifyReply() as FastifyReply;
const userId = await FakeWebauthnVerify();
const data = { userId: userId, usePasswordLessLogin: false };
await userProfilesRepository.update({ userId: userId }, data);
const res_body = await passkeyApiService.signin(req, res);
expect(res.statusCode).toBe(403);
expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912');
});
});
});