From 8c58d97b828151acff943e568b277673155dc3b2 Mon Sep 17 00:00:00 2001 From: GPlay97 Date: Sun, 28 Sep 2025 14:17:38 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20require=20premium=20for=20runni?= =?UTF-8?q?ng=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log-requires-premium.exception.ts | 7 ++++ src/logs/logs.controller.ts | 6 +++ src/logs/logs.service.ts | 41 ++++++++++++++++++- .../exceptions/premium-required.exception.ts | 4 +- 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/logs/exceptions/log-requires-premium.exception.ts diff --git a/src/logs/exceptions/log-requires-premium.exception.ts b/src/logs/exceptions/log-requires-premium.exception.ts new file mode 100644 index 0000000..c02836d --- /dev/null +++ b/src/logs/exceptions/log-requires-premium.exception.ts @@ -0,0 +1,7 @@ +import { Exception } from '../../utils/exception'; + +export class LogRequiresPremiumException extends Exception { + constructor() { + super('This log requires a premium account to access because it is either still running or has been archived.'); + } +} diff --git a/src/logs/logs.controller.ts b/src/logs/logs.controller.ts index 9b22264..5a6dfdc 100644 --- a/src/logs/logs.controller.ts +++ b/src/logs/logs.controller.ts @@ -27,6 +27,8 @@ import { LogNotRunningException } from './exceptions/log-not-running.exception'; import { HISTORY_TYPE } from './entities/history-type.entity'; import { PremiumGuard } from '../premium/premium.guard'; import { Premium } from '../premium/decorators/premium.decorator'; +import { LogRequiresPremiumException } from './exceptions/log-requires-premium.exception'; +import { PremiumRequiredException } from '../premium/exceptions/premium-required.exception'; @Controller('logs') @UseGuards(AuthGuard) @@ -69,6 +71,8 @@ export class LogsController { } catch (error) { if (error instanceof LogNotExistsException) { throw new NotFoundException(error.message); + } else if (error instanceof LogRequiresPremiumException) { + throw new PremiumRequiredException(error.message); } throw new InternalServerErrorException(); @@ -87,6 +91,8 @@ export class LogsController { } catch (error) { if (error instanceof LogNotExistsException) { throw new NotFoundException(error.message); + } else if (error instanceof LogRequiresPremiumException) { + throw new PremiumRequiredException(error.message); } throw new InternalServerErrorException(); diff --git a/src/logs/logs.service.ts b/src/logs/logs.service.ts index 221dbb0..52b4434 100644 --- a/src/logs/logs.service.ts +++ b/src/logs/logs.service.ts @@ -20,12 +20,15 @@ import { Sync } from './schemas/sync.schema'; import { TYPE } from './entities/type.entity'; import { LogNotRunningException } from './exceptions/log-not-running.exception'; import { HISTORY_TYPE } from './entities/history-type.entity'; +import { PremiumService } from '../premium/premium.service'; +import { LogRequiresPremiumException } from './exceptions/log-requires-premium.exception'; @Injectable() export class LogsService { constructor( @InjectModel(Log.name) private logModel: Model, @InjectModel(LastSync.name) private lastSyncModel: Model, + private readonly premiumService: PremiumService, private readonly eventEmitter: EventEmitter2, ) {} @@ -117,7 +120,19 @@ export class LogsService { logs.where({ type }); } - return Promise.resolve((await logs).map((log) => new LogDto(log))); + const isPremium = await this.premiumService.getExpiryDate(akey); + + const logsToReturn = []; + + (await logs).forEach((log) => { + if (log.status === STATUS.RUNNING && !isPremium) { + // Skip running logs for non-premium users + } else { + logsToReturn.push(log); + } + }); + + return Promise.resolve(logsToReturn.map((log) => new LogDto(log))); } async findOne(akey: string, id: string): Promise { @@ -129,12 +144,36 @@ export class LogsService { throw new LogNotExistsException(); } + if (log.status === STATUS.RUNNING) { + const isPremium = await this.premiumService.getExpiryDate(akey); + + if (!isPremium) { + throw new LogRequiresPremiumException(); + } + } + return Promise.resolve(new LogDto(log)); } async findOneWithHistory(akey: string, id: string, type: HISTORY_TYPE = HISTORY_TYPE.ALL): Promise { let history = []; + const logStatus = await this.logModel + .findOne({ akey, _id: id }) + .select('status'); + + if (!logStatus) { + throw new LogNotExistsException(); + } + + if (logStatus.status === STATUS.RUNNING) { + const isPremium = await this.premiumService.getExpiryDate(akey); + + if (!isPremium) { + throw new LogRequiresPremiumException(); + } + } + switch (type) { case HISTORY_TYPE.ALL: const logWithAllHistory = await this.logModel diff --git a/src/premium/exceptions/premium-required.exception.ts b/src/premium/exceptions/premium-required.exception.ts index d27474c..6ce3ad7 100644 --- a/src/premium/exceptions/premium-required.exception.ts +++ b/src/premium/exceptions/premium-required.exception.ts @@ -1,7 +1,7 @@ import { HttpException, HttpStatus } from '@nestjs/common'; export class PremiumRequiredException extends HttpException { - constructor() { - super('Active premium status required.', HttpStatus.PAYMENT_REQUIRED); + constructor( message?: string) { + super(message || 'Active premium status required.', HttpStatus.PAYMENT_REQUIRED); } } From fc9c6e654af4775fa9683edba158dee671d755a8 Mon Sep 17 00:00:00 2001 From: GPlay97 Date: Sun, 28 Sep 2025 23:30:31 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20adjusted=20logs=20tests=20for?= =?UTF-8?q?=20new=20premium=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/logs/logs.controller.spec.ts | 101 ++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/src/logs/logs.controller.spec.ts b/src/logs/logs.controller.spec.ts index 067e703..49192d3 100644 --- a/src/logs/logs.controller.spec.ts +++ b/src/logs/logs.controller.spec.ts @@ -16,6 +16,8 @@ import { STATUS } from './entities/status.entity'; import { LastSyncDto } from './dto/last-sync.dto'; import { TYPE } from './entities/type.entity'; import { HISTORY_TYPE } from './entities/history-type.entity'; +import { PremiumService } from '../premium/premium.service'; +import { PremiumRequiredException } from '../premium/exceptions/premium-required.exception'; describe('LogsController', () => { let accountService: AccountService; @@ -25,6 +27,10 @@ describe('LogsController', () => { let chargeLogId: string; let syncTimestamp: Date; + const mockPremiumService = { + getExpiryDate: jest.fn(), + }; + async function createAccount() { const dto = new CreateAccountDto(); @@ -35,6 +41,8 @@ describe('LogsController', () => { } beforeEach(async () => { + mockPremiumService.getExpiryDate.mockReset(); + const module: TestingModule = await Test.createTestingModule({ imports: [ LogsModule, @@ -42,7 +50,10 @@ describe('LogsController', () => { MongooseModule.forRoot(process.env.DATABASE_URI), EventEmitterModule.forRoot(), ], - }).compile(); + }) + .overrideProvider(PremiumService) + .useValue(mockPremiumService) + .compile(); accountService = module.get(AccountService); controller = module.get(LogsController); @@ -113,7 +124,15 @@ describe('LogsController', () => { expect(response).toBeUndefined(); }); - it('should be able to retrieve log in list', async () => { + it('should not be able to retrieve running log in list when not premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(null); + const response = await controller.findAll(testAccount.akey); + + expect(response).toHaveLength(0); + }); + + it('should be able to retrieve running log in list when premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findAll(testAccount.akey); expect(response).toHaveLength(1); @@ -126,19 +145,36 @@ describe('LogsController', () => { }); it('should be able to filter out log in list', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findAll(testAccount.akey, TYPE.DRIVE); expect(response).toHaveLength(0); }); - it('should be able to retrieve it', async () => { + it('should not be able to retrieve running log when not premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(null); + await expect(async () => { + await controller.findOne(testAccount.akey, logId); + }).rejects.toThrow(PremiumRequiredException); + }); + + it('should be able to retrieve log explicitly', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOne(testAccount.akey, logId); expect(response).toBeInstanceOf(LogDto); expect(response).not.toHaveProperty('history'); }); - it('should be able to retrieve log history', async () => { + it('should not be able to retrieve log history when not premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(null); + await expect(async () => { + await controller.findOneWithHistory(testAccount.akey, logId); + }).rejects.toThrow(PremiumRequiredException); + }); + + it('should be able to retrieve log history when premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOneWithHistory( testAccount.akey, logId, @@ -149,7 +185,20 @@ describe('LogsController', () => { expect(response[0]).toHaveProperty('timestamp'); }); - it('should be able to retrieve last sync data', async () => { + it('should be able to retrieve last sync data when not premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(null); + const response = await controller.lastSync(testAccount.akey); + + expect(response).toBeInstanceOf(LastSyncDto); + expect(response).toHaveProperty('updatedAt'); + expect(response).toHaveProperty('socDisplay', 80); + expect(new Date(response.updatedAt).getTime()).toBeGreaterThan( + syncTimestamp.getTime(), + ); + }); + + it('should be able to retrieve last sync data when premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.lastSync(testAccount.akey); expect(response).toBeInstanceOf(LastSyncDto); @@ -173,6 +222,7 @@ describe('LogsController', () => { }); it('should be able to retrieve log history with timestamp', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOneWithHistory( testAccount.akey, logId, @@ -188,6 +238,7 @@ describe('LogsController', () => { }); it('should be able to retrieve location log history', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOneWithHistory( testAccount.akey, logId, @@ -204,6 +255,7 @@ describe('LogsController', () => { }); it('should be able to retrieve battery log history', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOneWithHistory( testAccount.akey, logId, @@ -215,6 +267,7 @@ describe('LogsController', () => { }); it('should be able to retrieve all log history', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOneWithHistory( testAccount.akey, logId, @@ -236,6 +289,7 @@ describe('LogsController', () => { }); it('should contain new title', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOne(testAccount.akey, logId); expect(response).toHaveProperty('title', 'My title'); @@ -260,8 +314,13 @@ describe('LogsController', () => { await controller.syncData(testAccount.akey, dto); - const response = await controller.findAll(testAccount.akey); + let response = await controller.findAll(testAccount.akey); + expect(response).toHaveLength(0); + + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); + + response = await controller.findAll(testAccount.akey); expect(response).toHaveLength(1); expect(response.at(0)).toHaveProperty('type', TYPE.UNKNOWN); @@ -278,6 +337,7 @@ describe('LogsController', () => { }); it('should contain two history records', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOneWithHistory( testAccount.akey, logId, @@ -288,7 +348,20 @@ describe('LogsController', () => { expect(response.at(1)).toHaveProperty('socDisplay', 81); }); - it('should be able to retrieve updated last sync data', async () => { + it('should be able to retrieve updated last sync data when not premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(null); + const response = await controller.lastSync(testAccount.akey); + + expect(response).toBeInstanceOf(LastSyncDto); + expect(response).toHaveProperty('updatedAt'); + expect(response).toHaveProperty('socDisplay', 81); + expect(new Date(response.updatedAt).getTime()).toBeGreaterThan( + syncTimestamp.getTime(), + ); + }); + + it('should be able to retrieve updated last sync data when premium', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.lastSync(testAccount.akey); expect(response).toBeInstanceOf(LastSyncDto); @@ -306,6 +379,7 @@ describe('LogsController', () => { await controller.syncData(testAccount.akey, dto); + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findAll(testAccount.akey); expect(response).toHaveLength(1); @@ -324,8 +398,13 @@ describe('LogsController', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); - const response = await controller.findAll(testAccount.akey); + mockPremiumService.getExpiryDate.mockResolvedValue(null); + let response = await controller.findAll(testAccount.akey); + + expect(response).toHaveLength(1); + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); + response = await controller.findAll(testAccount.akey); expect(response).toHaveLength(2); expect(response.at(0)).toHaveProperty('type', TYPE.CHARGE); chargeLogId = response[0].id; @@ -340,6 +419,7 @@ describe('LogsController', () => { }); it('should contain metadata', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOne(testAccount.akey, chargeLogId); expect(response).toBeInstanceOf(LogDto); @@ -366,6 +446,7 @@ describe('LogsController', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOne(testAccount.akey, chargeLogId); expect(response).toBeInstanceOf(LogDto); @@ -386,6 +467,7 @@ describe('LogsController', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOne(testAccount.akey, chargeLogId); expect(response).toBeInstanceOf(LogDto); @@ -401,6 +483,7 @@ describe('LogsController', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOne(testAccount.akey, chargeLogId); expect(response).toBeInstanceOf(LogDto); @@ -417,6 +500,7 @@ describe('LogsController', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findOne(testAccount.akey, chargeLogId); expect(response).toBeInstanceOf(LogDto); @@ -424,6 +508,7 @@ describe('LogsController', () => { }); it('should find current running log', async () => { + mockPremiumService.getExpiryDate.mockResolvedValue(new Date('2099-12-31')); const response = await controller.findRunning(testAccount.akey); expect(response).toBeInstanceOf(LogDto);