diff --git a/apps/api/src/modules/accounts/accounts.controller.spec.ts b/apps/api/src/modules/accounts/accounts.controller.spec.ts index 13fb2bd..3fe9cdd 100644 --- a/apps/api/src/modules/accounts/accounts.controller.spec.ts +++ b/apps/api/src/modules/accounts/accounts.controller.spec.ts @@ -5,9 +5,12 @@ import { AccountsService } from './accounts.service'; const mockAccounts = [ { 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 = { list: jest.fn().mockResolvedValue(mockAccounts), getQr: jest.fn().mockResolvedValue({ status: 'DISCONNECTED', qrDataUrl: 'data:image/png;base64,fake' }), + create: jest.fn().mockResolvedValue(mockCreated), }; describe('AccountsController', () => { @@ -33,4 +36,15 @@ describe('AccountsController', () => { expect(mockService.getQr).toHaveBeenCalledWith('acc_1'); 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); + }); }); diff --git a/apps/api/src/modules/accounts/accounts.controller.ts b/apps/api/src/modules/accounts/accounts.controller.ts index 411e2dd..09ea552 100644 --- a/apps/api/src/modules/accounts/accounts.controller.ts +++ b/apps/api/src/modules/accounts/accounts.controller.ts @@ -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'; @Controller('accounts') @@ -14,4 +14,9 @@ export class AccountsController { getQr(@Param('id') id: string) { return this.service.getQr(id); } + + @Post() + create(@Body() body: { displayName?: string }) { + return this.service.create(body.displayName); + } } diff --git a/apps/api/src/modules/accounts/accounts.module.ts b/apps/api/src/modules/accounts/accounts.module.ts index 0e18141..1018a20 100644 --- a/apps/api/src/modules/accounts/accounts.module.ts +++ b/apps/api/src/modules/accounts/accounts.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { AccountsController } from './accounts.controller'; import { AccountsService } from './accounts.service'; @Module({ + imports: [ConfigModule], controllers: [AccountsController], providers: [AccountsService], }) diff --git a/apps/api/src/modules/accounts/accounts.service.spec.ts b/apps/api/src/modules/accounts/accounts.service.spec.ts index 1ecc065..005704e 100644 --- a/apps/api/src/modules/accounts/accounts.service.spec.ts +++ b/apps/api/src/modules/accounts/accounts.service.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; import { AccountsService } from './accounts.service'; import { PrismaService } from '../../prisma/prisma.service'; 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' }, ]; +const mockCreatedAccount = { + id: 'acc_new', + platform: 'whatsapp', + jid: 'pending_uuid@placeholder', + displayName: 'My Number', + status: 'ACTIVE', +}; + const mockPrisma = { account: { findMany: jest.fn().mockResolvedValue(mockAccounts), 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', () => { let service: AccountsService; @@ -27,6 +43,7 @@ describe('AccountsService', () => { providers: [ AccountsService, { provide: PrismaService, useValue: mockPrisma }, + { provide: ConfigService, useValue: mockConfig }, ], }).compile(); service = module.get(AccountsService); @@ -63,4 +80,42 @@ describe('AccountsService', () => { 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); + }); + }); }); diff --git a/apps/api/src/modules/accounts/accounts.service.ts b/apps/api/src/modules/accounts/accounts.service.ts index 47706ca..84285a5 100644 --- a/apps/api/src/modules/accounts/accounts.service.ts +++ b/apps/api/src/modules/accounts/accounts.service.ts @@ -1,4 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { randomUUID } from 'crypto'; import { PrismaService } from '../../prisma/prisma.service'; import * as QRCode from 'qrcode'; @@ -17,7 +19,10 @@ export interface AccountQr { @Injectable() export class AccountsService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly config: ConfigService, + ) {} list(): Promise { return this.prisma.account.findMany({ @@ -36,4 +41,19 @@ export class AccountsService { const qrDataUrl = await QRCode.toDataURL(account.qrCode); return { status: account.status, qrDataUrl }; } + + async create(displayName?: string): Promise { + const sessionBase = this.config.get('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 }, + }); + } }