feat: add POST /accounts endpoint to create new WhatsApp account records
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,12 @@ import { AccountsService } from './accounts.service';
|
|||||||
const mockAccounts = [
|
const mockAccounts = [
|
||||||
{ id: 'acc_1', platform: 'whatsapp', jid: '111@s.whatsapp.net', displayName: 'Test', status: 'ACTIVE' },
|
{ id: 'acc_1', platform: 'whatsapp', jid: '111@s.whatsapp.net', displayName: 'Test', status: 'ACTIVE' },
|
||||||
];
|
];
|
||||||
|
const mockCreated = { id: 'acc_new', platform: 'whatsapp', jid: 'pending_x@placeholder', displayName: 'New', status: 'ACTIVE' };
|
||||||
|
|
||||||
const mockService = {
|
const mockService = {
|
||||||
list: jest.fn().mockResolvedValue(mockAccounts),
|
list: jest.fn().mockResolvedValue(mockAccounts),
|
||||||
getQr: jest.fn().mockResolvedValue({ status: 'DISCONNECTED', qrDataUrl: 'data:image/png;base64,fake' }),
|
getQr: jest.fn().mockResolvedValue({ status: 'DISCONNECTED', qrDataUrl: 'data:image/png;base64,fake' }),
|
||||||
|
create: jest.fn().mockResolvedValue(mockCreated),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AccountsController', () => {
|
describe('AccountsController', () => {
|
||||||
@@ -33,4 +36,15 @@ describe('AccountsController', () => {
|
|||||||
expect(mockService.getQr).toHaveBeenCalledWith('acc_1');
|
expect(mockService.getQr).toHaveBeenCalledWith('acc_1');
|
||||||
expect(result.qrDataUrl).toBe('data:image/png;base64,fake');
|
expect(result.qrDataUrl).toBe('data:image/png;base64,fake');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('create() calls service with displayName from body', async () => {
|
||||||
|
const result = await controller.create({ displayName: 'New' });
|
||||||
|
expect(mockService.create).toHaveBeenCalledWith('New');
|
||||||
|
expect(result).toEqual(mockCreated);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create() calls service with undefined when no displayName', async () => {
|
||||||
|
await controller.create({});
|
||||||
|
expect(mockService.create).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get, Param } from '@nestjs/common';
|
import { Controller, Get, Param, Post, Body } from '@nestjs/common';
|
||||||
import { AccountsService } from './accounts.service';
|
import { AccountsService } from './accounts.service';
|
||||||
|
|
||||||
@Controller('accounts')
|
@Controller('accounts')
|
||||||
@@ -14,4 +14,9 @@ export class AccountsController {
|
|||||||
getQr(@Param('id') id: string) {
|
getQr(@Param('id') id: string) {
|
||||||
return this.service.getQr(id);
|
return this.service.getQr(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() body: { displayName?: string }) {
|
||||||
|
return this.service.create(body.displayName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AccountsController } from './accounts.controller';
|
import { AccountsController } from './accounts.controller';
|
||||||
import { AccountsService } from './accounts.service';
|
import { AccountsService } from './accounts.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [ConfigModule],
|
||||||
controllers: [AccountsController],
|
controllers: [AccountsController],
|
||||||
providers: [AccountsService],
|
providers: [AccountsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AccountsService } from './accounts.service';
|
import { AccountsService } from './accounts.service';
|
||||||
import { PrismaService } from '../../prisma/prisma.service';
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
@@ -11,13 +12,28 @@ const mockAccounts = [
|
|||||||
{ id: 'acc_1', platform: 'whatsapp', jid: '111@s.whatsapp.net', displayName: 'Test Account', status: 'ACTIVE' },
|
{ id: 'acc_1', platform: 'whatsapp', jid: '111@s.whatsapp.net', displayName: 'Test Account', status: 'ACTIVE' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mockCreatedAccount = {
|
||||||
|
id: 'acc_new',
|
||||||
|
platform: 'whatsapp',
|
||||||
|
jid: 'pending_uuid@placeholder',
|
||||||
|
displayName: 'My Number',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
};
|
||||||
|
|
||||||
const mockPrisma = {
|
const mockPrisma = {
|
||||||
account: {
|
account: {
|
||||||
findMany: jest.fn().mockResolvedValue(mockAccounts),
|
findMany: jest.fn().mockResolvedValue(mockAccounts),
|
||||||
findUnique: jest.fn(),
|
findUnique: jest.fn(),
|
||||||
|
create: jest.fn().mockResolvedValue(mockCreatedAccount),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockConfig = {
|
||||||
|
get: jest.fn().mockImplementation((key: string, def: string) =>
|
||||||
|
key === 'WHATSAPP_SESSION_PATH' ? './sessions' : def,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
describe('AccountsService', () => {
|
describe('AccountsService', () => {
|
||||||
let service: AccountsService;
|
let service: AccountsService;
|
||||||
|
|
||||||
@@ -27,6 +43,7 @@ describe('AccountsService', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
AccountsService,
|
AccountsService,
|
||||||
{ provide: PrismaService, useValue: mockPrisma },
|
{ provide: PrismaService, useValue: mockPrisma },
|
||||||
|
{ provide: ConfigService, useValue: mockConfig },
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
service = module.get<AccountsService>(AccountsService);
|
service = module.get<AccountsService>(AccountsService);
|
||||||
@@ -63,4 +80,42 @@ describe('AccountsService', () => {
|
|||||||
expect(result).toEqual({ status: 'not_found', qrDataUrl: null });
|
expect(result).toEqual({ status: 'not_found', qrDataUrl: null });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('create()', () => {
|
||||||
|
it('creates account with platform whatsapp and status ACTIVE', async () => {
|
||||||
|
await service.create('My Number');
|
||||||
|
expect(mockPrisma.account.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
platform: 'whatsapp',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
displayName: 'My Number',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates a unique sessionPath under WHATSAPP_SESSION_PATH', async () => {
|
||||||
|
await service.create();
|
||||||
|
const call = mockPrisma.account.create.mock.calls[0][0];
|
||||||
|
expect(call.data.sessionPath).toMatch(/^\.\/sessions\/.+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates a placeholder jid prefixed with pending_', async () => {
|
||||||
|
await service.create();
|
||||||
|
const call = mockPrisma.account.create.mock.calls[0][0];
|
||||||
|
expect(call.data.jid).toMatch(/^pending_/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets displayName to null when not provided', async () => {
|
||||||
|
await service.create();
|
||||||
|
const call = mockPrisma.account.create.mock.calls[0][0];
|
||||||
|
expect(call.data.displayName).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the created account summary', async () => {
|
||||||
|
const result = await service.create('My Number');
|
||||||
|
expect(result).toEqual(mockCreatedAccount);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
import { PrismaService } from '../../prisma/prisma.service';
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
@@ -17,7 +19,10 @@ export interface AccountQr {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountsService {
|
export class AccountsService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
list(): Promise<AccountSummary[]> {
|
list(): Promise<AccountSummary[]> {
|
||||||
return this.prisma.account.findMany({
|
return this.prisma.account.findMany({
|
||||||
@@ -36,4 +41,19 @@ export class AccountsService {
|
|||||||
const qrDataUrl = await QRCode.toDataURL(account.qrCode);
|
const qrDataUrl = await QRCode.toDataURL(account.qrCode);
|
||||||
return { status: account.status, qrDataUrl };
|
return { status: account.status, qrDataUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async create(displayName?: string): Promise<AccountSummary> {
|
||||||
|
const sessionBase = this.config.get<string>('WHATSAPP_SESSION_PATH', './sessions');
|
||||||
|
const uid = randomUUID();
|
||||||
|
return this.prisma.account.create({
|
||||||
|
data: {
|
||||||
|
platform: 'whatsapp',
|
||||||
|
jid: `pending_${uid}@placeholder`,
|
||||||
|
sessionPath: `${sessionBase}/${uid}`,
|
||||||
|
displayName: displayName ?? null,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
select: { id: true, platform: true, jid: true, displayName: true, status: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user