mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
refactor: subscriber avatar store
This commit is contained in:
parent
ae8ae7ea07
commit
c56f176d85
@ -85,7 +85,7 @@ import { ConversationService } from './conversation.service';
|
||||
import { MessageService } from './message.service';
|
||||
import { SubscriberService } from './subscriber.service';
|
||||
|
||||
describe('BlockService', () => {
|
||||
describe('BotService', () => {
|
||||
let blockService: BlockService;
|
||||
let subscriberService: SubscriberService;
|
||||
let botService: BotService;
|
||||
|
@ -8,16 +8,8 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||
import mime from 'mime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { BotStatsType } from '@/analytics/schemas/bot-stats.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import {
|
||||
AttachmentAccess,
|
||||
AttachmentCreatedByRef,
|
||||
AttachmentResourceRef,
|
||||
} from '@/attachment/types';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { config } from '@/config';
|
||||
import { HelperService } from '@/helper/helper.service';
|
||||
@ -47,7 +39,6 @@ export class ChatService {
|
||||
private readonly botService: BotService,
|
||||
private readonly websocketGateway: WebsocketGateway,
|
||||
private readonly helperService: HelperService,
|
||||
private readonly attachmentService: AttachmentService,
|
||||
private readonly languageService: LanguageService,
|
||||
) {}
|
||||
|
||||
@ -281,33 +272,9 @@ export class ChatService {
|
||||
// Retrieve and store the subscriber avatar
|
||||
if (handler.getSubscriberAvatar) {
|
||||
try {
|
||||
const metadata = await handler.getSubscriberAvatar(event);
|
||||
if (metadata) {
|
||||
const { file, type, size } = metadata;
|
||||
const extension = mime.extension(type);
|
||||
|
||||
const avatar = await this.attachmentService.store(file, {
|
||||
name: `avatar-${uuidv4()}.${extension}`,
|
||||
size,
|
||||
type,
|
||||
resourceRef: AttachmentResourceRef.SubscriberAvatar,
|
||||
access: AttachmentAccess.Private,
|
||||
createdByRef: AttachmentCreatedByRef.Subscriber,
|
||||
createdBy: subscriber.id,
|
||||
});
|
||||
|
||||
if (avatar) {
|
||||
subscriber = await this.subscriberService.updateOne(
|
||||
subscriber.id,
|
||||
{
|
||||
avatar: avatar.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (!subscriber) {
|
||||
throw new Error('Unable to update the subscriber avatar');
|
||||
}
|
||||
}
|
||||
const file = await handler.getSubscriberAvatar(event);
|
||||
if (file) {
|
||||
await this.subscriberService.storeAvatar(subscriber.id, file);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
|
@ -7,10 +7,20 @@
|
||||
*/
|
||||
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import mime from 'mime';
|
||||
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentModel,
|
||||
} from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import {
|
||||
AttachmentAccess,
|
||||
AttachmentCreatedByRef,
|
||||
AttachmentFile,
|
||||
AttachmentResourceRef,
|
||||
} from '@/attachment/types';
|
||||
import { InvitationRepository } from '@/user/repositories/invitation.repository';
|
||||
import { RoleRepository } from '@/user/repositories/role.repository';
|
||||
import { UserRepository } from '@/user/repositories/user.repository';
|
||||
@ -37,11 +47,14 @@ import { Subscriber, SubscriberModel } from '../schemas/subscriber.schema';
|
||||
import { LabelService } from './label.service';
|
||||
import { SubscriberService } from './subscriber.service';
|
||||
|
||||
jest.mock('uuid', () => ({ v4: jest.fn(() => 'test-uuid') }));
|
||||
|
||||
describe('SubscriberService', () => {
|
||||
let subscriberRepository: SubscriberRepository;
|
||||
let labelRepository: LabelRepository;
|
||||
let userRepository: UserRepository;
|
||||
let subscriberService: SubscriberService;
|
||||
let attachmentService: AttachmentService;
|
||||
let allSubscribers: Subscriber[];
|
||||
let allLabels: Label[];
|
||||
let allUsers: User[];
|
||||
@ -74,13 +87,19 @@ describe('SubscriberService', () => {
|
||||
AttachmentRepository,
|
||||
],
|
||||
});
|
||||
[labelRepository, userRepository, subscriberService, subscriberRepository] =
|
||||
await getMocks([
|
||||
LabelRepository,
|
||||
UserRepository,
|
||||
SubscriberService,
|
||||
SubscriberRepository,
|
||||
]);
|
||||
[
|
||||
labelRepository,
|
||||
userRepository,
|
||||
subscriberService,
|
||||
subscriberRepository,
|
||||
attachmentService,
|
||||
] = await getMocks([
|
||||
LabelRepository,
|
||||
UserRepository,
|
||||
SubscriberService,
|
||||
SubscriberRepository,
|
||||
AttachmentService,
|
||||
]);
|
||||
allSubscribers = await subscriberRepository.findAll();
|
||||
allLabels = await labelRepository.findAll();
|
||||
allUsers = await userRepository.findAll();
|
||||
@ -146,4 +165,86 @@ describe('SubscriberService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeAvatar', () => {
|
||||
it('should persist the avatar and patch the subscriber', async () => {
|
||||
const subscriber = { ...allSubscribers[0], avatar: null };
|
||||
const avatarPayload: AttachmentFile = {
|
||||
file: Buffer.from('fake-png'),
|
||||
type: 'image/png',
|
||||
size: 8_192,
|
||||
};
|
||||
jest.spyOn(mime, 'extension').mockReturnValue('png');
|
||||
|
||||
const fakeAttachment = { id: '9'.repeat(24) } as Attachment;
|
||||
jest.spyOn(attachmentService, 'store').mockResolvedValue(fakeAttachment);
|
||||
const updateOneSpy = jest
|
||||
.spyOn(subscriberService, 'updateOne')
|
||||
.mockResolvedValue(allSubscribers[0]);
|
||||
|
||||
await subscriberService.storeAvatar(subscriber.id, avatarPayload);
|
||||
|
||||
expect(attachmentService.store).toHaveBeenCalledTimes(1);
|
||||
expect(attachmentService.store).toHaveBeenCalledWith(
|
||||
avatarPayload.file,
|
||||
expect.objectContaining({
|
||||
name: 'avatar-test-uuid.png',
|
||||
type: 'image/png',
|
||||
size: 8_192,
|
||||
resourceRef: AttachmentResourceRef.SubscriberAvatar,
|
||||
access: AttachmentAccess.Private,
|
||||
createdByRef: AttachmentCreatedByRef.Subscriber,
|
||||
createdBy: subscriber.id,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(updateOneSpy).toHaveBeenCalledWith(subscriber.id, {
|
||||
avatar: fakeAttachment.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should propagate an error from AttachmentService and leave the subscriber unchanged', async () => {
|
||||
const subscriber = allSubscribers[0];
|
||||
const avatarPayload: AttachmentFile = {
|
||||
file: Buffer.from('fake-jpg'),
|
||||
type: 'image/jpeg',
|
||||
size: 5_048,
|
||||
};
|
||||
jest.spyOn(mime, 'extension').mockReturnValue('jpg');
|
||||
|
||||
const failure = new Error('disk full');
|
||||
jest.spyOn(attachmentService, 'store').mockRejectedValue(failure);
|
||||
const updateOneSpy = jest
|
||||
.spyOn(subscriberService, 'updateOne')
|
||||
.mockResolvedValue(allSubscribers[0]);
|
||||
|
||||
await expect(
|
||||
subscriberService.storeAvatar(subscriber.id, avatarPayload),
|
||||
).rejects.toThrow(failure);
|
||||
|
||||
expect(updateOneSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should generate the filename with the proper extension', async () => {
|
||||
const subscriber = { ...allSubscribers[0], avatar: null };
|
||||
const avatarPayload: AttachmentFile = {
|
||||
file: Buffer.from('fake-png'),
|
||||
type: 'image/png',
|
||||
size: 1_024,
|
||||
};
|
||||
jest.spyOn(mime, 'extension').mockReturnValue('png');
|
||||
|
||||
jest
|
||||
.spyOn(attachmentService, 'store')
|
||||
.mockResolvedValue({ id: '9'.repeat(24) } as any);
|
||||
jest
|
||||
.spyOn(subscriberService, 'updateOne')
|
||||
.mockResolvedValue(allSubscribers[0]);
|
||||
|
||||
await subscriberService.storeAvatar(subscriber.id, avatarPayload);
|
||||
|
||||
const { name } = (attachmentService.store as jest.Mock).mock.calls[0][1]; // second arg in the first call
|
||||
expect(name).toBe('avatar-test-uuid.png');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,8 +12,16 @@ import {
|
||||
Optional,
|
||||
} from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import mime from 'mime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import {
|
||||
AttachmentAccess,
|
||||
AttachmentCreatedByRef,
|
||||
AttachmentFile,
|
||||
AttachmentResourceRef,
|
||||
} from '@/attachment/types';
|
||||
import { config } from '@/config';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import {
|
||||
@ -47,7 +55,7 @@ export class SubscriberService extends BaseService<
|
||||
|
||||
constructor(
|
||||
readonly repository: SubscriberRepository,
|
||||
protected attachmentService: AttachmentService,
|
||||
protected readonly attachmentService: AttachmentService,
|
||||
@Optional() gateway?: WebsocketGateway,
|
||||
) {
|
||||
super(repository);
|
||||
@ -135,6 +143,38 @@ export class SubscriberService extends BaseService<
|
||||
return await this.repository.handOverByForeignIdQuery(foreignId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a new avatar image for a given **Subscriber** and attach it to their profile.
|
||||
* The method is **idempotent** regarding subscriber updates: calling it again simply
|
||||
* replaces the existing avatar reference with the new one.
|
||||
*
|
||||
* @param subscriberId – The unique identifier of the subscriber
|
||||
* @param avatar – The uploaded avatar payload containing:
|
||||
* - `file` – Raw binary buffer
|
||||
* - `type` – MIME type (e.g. `image/png`)
|
||||
* - `size` – File size in bytes
|
||||
*
|
||||
* @returns Resolves once the subscriber avatar is stored
|
||||
*/
|
||||
async storeAvatar(subscriberId: string, avatar: AttachmentFile) {
|
||||
const { file, type, size } = avatar;
|
||||
const extension = mime.extension(type);
|
||||
|
||||
const attachment = await this.attachmentService.store(file, {
|
||||
name: `avatar-${uuidv4()}.${extension}`,
|
||||
size,
|
||||
type,
|
||||
resourceRef: AttachmentResourceRef.SubscriberAvatar,
|
||||
access: AttachmentAccess.Private,
|
||||
createdByRef: AttachmentCreatedByRef.Subscriber,
|
||||
createdBy: subscriberId,
|
||||
});
|
||||
|
||||
await this.updateOne(subscriberId, {
|
||||
avatar: attachment.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply updates on end-user such as :
|
||||
* - Assign labels to specific end-user
|
||||
|
@ -12,7 +12,6 @@ import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
|
||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
|
||||
import { LocalAuthController } from './controllers/auth.controller';
|
||||
import { ModelController } from './controllers/model.controller';
|
||||
@ -53,7 +52,6 @@ import { ValidateAccountService } from './services/validate-account.service';
|
||||
InvitationModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
]),
|
||||
PassportModule.register({
|
||||
session: true,
|
||||
|
Loading…
Reference in New Issue
Block a user