feat: add AccountsModule with list and QR endpoints
Implements GET /accounts (list all accounts) and GET /accounts/:id/qr (returns QR code as base64 data URL) using the qrcode package. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AccountsController } from './accounts.controller';
|
||||
import { AccountsService } from './accounts.service';
|
||||
|
||||
const mockAccounts = [
|
||||
{ id: 'acc_1', platform: 'whatsapp', jid: '111@s.whatsapp.net', displayName: 'Test', status: 'ACTIVE' },
|
||||
];
|
||||
const mockService = {
|
||||
list: jest.fn().mockResolvedValue(mockAccounts),
|
||||
getQr: jest.fn().mockResolvedValue({ status: 'DISCONNECTED', qrDataUrl: 'data:image/png;base64,fake' }),
|
||||
};
|
||||
|
||||
describe('AccountsController', () => {
|
||||
let controller: AccountsController;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AccountsController],
|
||||
providers: [{ provide: AccountsService, useValue: mockService }],
|
||||
}).compile();
|
||||
controller = module.get<AccountsController>(AccountsController);
|
||||
});
|
||||
|
||||
it('list() returns accounts from service', async () => {
|
||||
const result = await controller.list();
|
||||
expect(result).toEqual(mockAccounts);
|
||||
expect(mockService.list).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getQr() calls service with the account id', async () => {
|
||||
const result = await controller.getQr('acc_1');
|
||||
expect(mockService.getQr).toHaveBeenCalledWith('acc_1');
|
||||
expect(result.qrDataUrl).toBe('data:image/png;base64,fake');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { AccountsService } from './accounts.service';
|
||||
|
||||
@Controller('accounts')
|
||||
export class AccountsController {
|
||||
constructor(private readonly service: AccountsService) {}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.service.list();
|
||||
}
|
||||
|
||||
@Get(':id/qr')
|
||||
getQr(@Param('id') id: string) {
|
||||
return this.service.getQr(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AccountsController } from './accounts.controller';
|
||||
import { AccountsService } from './accounts.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AccountsController],
|
||||
providers: [AccountsService],
|
||||
})
|
||||
export class AccountsModule {}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AccountsService } from './accounts.service';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import * as QRCode from 'qrcode';
|
||||
|
||||
jest.mock('qrcode', () => ({
|
||||
toDataURL: jest.fn().mockResolvedValue('data:image/png;base64,fakedata'),
|
||||
}));
|
||||
|
||||
const mockAccounts = [
|
||||
{ id: 'acc_1', platform: 'whatsapp', jid: '111@s.whatsapp.net', displayName: 'Test Account', status: 'ACTIVE' },
|
||||
];
|
||||
|
||||
const mockPrisma = {
|
||||
account: {
|
||||
findMany: jest.fn().mockResolvedValue(mockAccounts),
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
describe('AccountsService', () => {
|
||||
let service: AccountsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AccountsService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<AccountsService>(AccountsService);
|
||||
});
|
||||
|
||||
describe('list()', () => {
|
||||
it('returns accounts from Prisma without qrCode field', async () => {
|
||||
const result = await service.list();
|
||||
expect(result).toEqual(mockAccounts);
|
||||
expect(mockPrisma.account.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ select: expect.not.objectContaining({ qrCode: true }) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQr()', () => {
|
||||
it('returns null qrDataUrl when account has no qrCode', async () => {
|
||||
mockPrisma.account.findUnique.mockResolvedValue({ status: 'ACTIVE', qrCode: null });
|
||||
const result = await service.getQr('acc_1');
|
||||
expect(result).toEqual({ status: 'ACTIVE', qrDataUrl: null });
|
||||
expect(QRCode.toDataURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('converts qrCode string to data URL when qrCode is present', async () => {
|
||||
mockPrisma.account.findUnique.mockResolvedValue({ status: 'DISCONNECTED', qrCode: 'raw-qr-string' });
|
||||
const result = await service.getQr('acc_1');
|
||||
expect(QRCode.toDataURL).toHaveBeenCalledWith('raw-qr-string');
|
||||
expect(result).toEqual({ status: 'DISCONNECTED', qrDataUrl: 'data:image/png;base64,fakedata' });
|
||||
});
|
||||
|
||||
it('returns not_found status when account does not exist', async () => {
|
||||
mockPrisma.account.findUnique.mockResolvedValue(null);
|
||||
const result = await service.getQr('nonexistent');
|
||||
expect(result).toEqual({ status: 'not_found', qrDataUrl: null });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import * as QRCode from 'qrcode';
|
||||
|
||||
export interface AccountSummary {
|
||||
id: string;
|
||||
platform: string;
|
||||
jid: string;
|
||||
displayName: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AccountQr {
|
||||
status: string;
|
||||
qrDataUrl: string | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AccountsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
list(): Promise<AccountSummary[]> {
|
||||
return this.prisma.account.findMany({
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: { id: true, platform: true, jid: true, displayName: true, status: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getQr(id: string): Promise<AccountQr> {
|
||||
const account = await this.prisma.account.findUnique({
|
||||
where: { id },
|
||||
select: { status: true, qrCode: true },
|
||||
});
|
||||
if (!account) return { status: 'not_found', qrDataUrl: null };
|
||||
if (!account.qrCode) return { status: account.status, qrDataUrl: null };
|
||||
const qrDataUrl = await QRCode.toDataURL(account.qrCode);
|
||||
return { status: account.status, qrDataUrl };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user