Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/account/account.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
UnauthorizedException,
BadRequestException,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Exception } from '../utils/exception';
Expand Down Expand Up @@ -79,7 +80,7 @@ export class AccountController {

@Post(':akey/login')
@Guest()
@HttpCode(200)
@HttpCode(HttpStatus.OK)
async login(@Param('akey') akey: string, @Body() loginDto: LoginPasswordDto) {
try {
const account = await this.accountService.loginWithPassword(
Expand Down
3 changes: 3 additions & 0 deletions src/account/schemas/account.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class Account {

@Prop({ required: true, minlength: TOKEN_LENGTH, maxlength: TOKEN_LENGTH })
token: string;

@Prop({ type: Date, default: null })
premiumUntil?: Date;
}

export const AccountSchema = SchemaFactory.createForClass(Account);
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SettingsModule } from './settings/settings.module';
import { LogsModule } from './logs/logs.module';
import { ScheduleModule } from '@nestjs/schedule';
import { MigrationModule } from './migration/migration.module';
import { PremiumModule } from './premium/premium.module';

@Module({
imports: [
Expand All @@ -18,6 +19,7 @@ import { MigrationModule } from './migration/migration.module';
SettingsModule,
LogsModule,
MigrationModule,
PremiumModule,
],
controllers: [],
providers: [],
Expand Down
2 changes: 1 addition & 1 deletion src/migration/dto/migrate-soc.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Type } from "class-transformer";
import { IsNotEmptyObject, IsNumber, IsOptional, Length, Max, Min, ValidateNested } from "class-validator";
import { AKEY_LENGTH, TOKEN_LENGTH } from "src/account/dto/account.dto";
import { AKEY_LENGTH, TOKEN_LENGTH } from "../../account/dto/account.dto";

class SocData {
@IsOptional()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Exception } from "src/utils/exception";
import { Exception } from "../../utils/exception";

export class MigrationAccountNotFound extends Exception {
constructor() {
Expand Down
6 changes: 3 additions & 3 deletions src/migration/migration.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Module } from "@nestjs/common";
import { MigrationController } from "./migration.controller";
import { LogsService } from "src/logs/logs.service";
import { LogsService } from "../logs/logs.service";
import { MongooseModule } from "@nestjs/mongoose";
import { Log, LogSchema } from "src/logs/schemas/log.schema";
import { LastSync, LastSyncSchema } from "src/logs/schemas/last-sync.schema";
import { Log, LogSchema } from "../logs/schemas/log.schema";
import { LastSync, LastSyncSchema } from "../logs/schemas/last-sync.schema";
import { MigrationService } from "./migration.service";
import { MigrationAccount, MigrationAccountSchema } from "./schemas/migration-account.schema";

Expand Down
4 changes: 2 additions & 2 deletions src/migration/migration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { MigrationAccount } from "./schemas/migration-account.schema";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { MigrationAccountNotFound } from "./exceptions/migration-account-not-found.exception";
import { LogsService } from "src/logs/logs.service";
import { LogsService } from "../logs/logs.service";
import { MigrateSocDto } from "./dto/migrate-soc.dto";
import { SyncDto } from "src/logs/dto/sync.dto";
import { SyncDto } from "../logs/dto/sync.dto";

@Injectable()
export class MigrationService {
Expand Down
2 changes: 1 addition & 1 deletion src/migration/schemas/migration-account.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { AKEY_LENGTH, TOKEN_LENGTH } from "src/account/dto/account.dto";
import { AKEY_LENGTH, TOKEN_LENGTH } from "../../account/dto/account.dto";

@Schema()
export class MigrationAccount {
Expand Down
5 changes: 5 additions & 0 deletions src/premium/decorators/premium.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';

export const PREMIUM_ROLE_NAME = 'premium';

export const Premium = () => SetMetadata('role', PREMIUM_ROLE_NAME);
7 changes: 7 additions & 0 deletions src/premium/dto/premium-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class PremiumStatusDto {
constructor(premiumUntil: Date | null) {
this.premiumUntil = premiumUntil;
}

premiumUntil: Date | null;
}
6 changes: 6 additions & 0 deletions src/premium/entities/premium-duration.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum PREMIUM_DURATION {
FIVE_MINUTES = 5,
ONE_WEEK = 60 * 24 * 7,
ONE_MONTH = 60 * 24 * 30,
ONE_YEAR = 60 * 24 * 365,
}
7 changes: 7 additions & 0 deletions src/premium/exceptions/ad-not-redeemable.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Exception } from '../../utils/exception';

export class AdNotRedeemableException extends Exception {
constructor() {
super('Ad could not be redeemed. Most likely because you are already premium or ad was not able to be verified.');
}
}
7 changes: 7 additions & 0 deletions src/premium/exceptions/premium-required.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { HttpException, HttpStatus } from '@nestjs/common';

export class PremiumRequiredException extends HttpException {
constructor() {
super('Active premium status required.', HttpStatus.PAYMENT_REQUIRED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Exception } from '../../utils/exception';

export class VoucherAlreadyRedeemedException extends Exception {
constructor() {
super('Entered voucher code was already redeemed.');
}
}
7 changes: 7 additions & 0 deletions src/premium/exceptions/voucher-not-exists.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Exception } from '../../utils/exception';

export class VoucherNotExistsException extends Exception {
constructor() {
super('Voucher code does not exists.');
}
}
172 changes: 172 additions & 0 deletions src/premium/premium.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { Test, TestingModule } from "@nestjs/testing";
import { PremiumModule } from "./premium.module";
import { ConfigModule } from "@nestjs/config";
import { MongooseModule } from "@nestjs/mongoose";
import { PremiumService } from "./premium.service";
import { PremiumController } from "./premium.controller";
import { AccountDto } from "../account/dto/account.dto";
import { CreateAccountDto } from "../account/dto/create-account.dto";
import { AccountService } from "../account/account.service";
import mongoose from "mongoose";
import { PremiumStatusDto } from "./dto/premium-status.dto";
import { ConflictException, NotFoundException } from "@nestjs/common";
import { PREMIUM_DURATION } from "./entities/premium-duration.entity";
import { randomBytes } from "crypto";
import { Voucher, VOUCHER_LENGTH } from "./schemas/voucher.schema";

describe('PremiumController', () => {
let premiumService: PremiumService;
let accountService: AccountService;
let controller: PremiumController;
let testAccount: AccountDto;
let voucher: Voucher;

async function createAccount() {
const dto = new CreateAccountDto();

dto.akey = accountService.akey();
dto.password = 'password';

testAccount = await accountService.create(dto);
}

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
PremiumModule,
ConfigModule.forRoot(),
MongooseModule.forRoot(process.env.DATABASE_URI),
],
}).compile();

accountService = module.get<AccountService>(AccountService);
premiumService = module.get<PremiumService>(PremiumService);
controller = module.get<PremiumController>(PremiumController);

voucher = await premiumService.generateVoucher(PREMIUM_DURATION.ONE_WEEK);

await createAccount();
});

afterAll(async () => {
const timer = setTimeout(async () => await mongoose.disconnect(), 1000);

timer.unref();
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

it('should not be able to retrieve status of non-existing account', async () => {
await expect(async () => {
await controller.status(
testAccount.akey.replace(/.$/, "1"),
);
}).rejects.toThrow(NotFoundException);
});

it('should be able to retrieve status of existing account', async () => {
const response = await controller.status(testAccount.akey);

expect(response).toBeInstanceOf(PremiumStatusDto);
expect(response).toHaveProperty('premiumUntil', null);
});

it('should be able to extend premium of account by watching an ad', async () => {
const response = await controller.redeemAd(testAccount.akey);

const now = new Date();
const expectedDate = new Date(now.getTime() + PREMIUM_DURATION.FIVE_MINUTES * 60000);
const toleranceInMs = 2000;

expect(response).toBeInstanceOf(PremiumStatusDto);
expect(response).toHaveProperty('premiumUntil');

expect(response.premiumUntil.getTime()).toBeGreaterThan(expectedDate.getTime() - toleranceInMs);
expect(response.premiumUntil.getTime()).toBeLessThan(expectedDate.getTime() + toleranceInMs);
});

it('should not be able to extend premium by watching an ad again', async () => {
await expect(async () => {
await controller.redeemAd(
testAccount.akey,
);
}).rejects.toThrow(ConflictException);
});

it('should be able to retrieve premium status of account', async () => {
const response = await controller.status(testAccount.akey);

const now = new Date();
const expectedDate = new Date(now.getTime() + PREMIUM_DURATION.FIVE_MINUTES * 60000);

const toleranceInMs = 2000;

expect(response).toBeInstanceOf(PremiumStatusDto);
expect(response).toHaveProperty('premiumUntil');

expect(response.premiumUntil.getTime()).toBeGreaterThan(expectedDate.getTime() - toleranceInMs);
expect(response.premiumUntil.getTime()).toBeLessThan(expectedDate.getTime() + toleranceInMs);
});

it('should be able to extend premium of account by subscribing', async () => {
const response = await controller.redeemSubscription(testAccount.akey);

const now = new Date();
const expectedDate = new Date(now.getTime() + PREMIUM_DURATION.FIVE_MINUTES * 60000 + PREMIUM_DURATION.ONE_MONTH * 60000);
const toleranceInMs = 2000;

expect(response).toBeInstanceOf(PremiumStatusDto);
expect(response).toHaveProperty('premiumUntil');

expect(response.premiumUntil.getTime()).toBeGreaterThan(expectedDate.getTime() - toleranceInMs);
expect(response.premiumUntil.getTime()).toBeLessThan(expectedDate.getTime() + toleranceInMs);
});

it('should be able to extend premium of account by subscribing again', async () => {
const response = await controller.redeemSubscription(testAccount.akey);

const now = new Date();
const expectedDate = new Date(now.getTime() + PREMIUM_DURATION.FIVE_MINUTES * 60000 + (PREMIUM_DURATION.ONE_MONTH * 2) * 60000);
const toleranceInMs = 2000;

expect(response).toBeInstanceOf(PremiumStatusDto);
expect(response).toHaveProperty('premiumUntil');

expect(response.premiumUntil.getTime()).toBeGreaterThan(expectedDate.getTime() - toleranceInMs);
expect(response.premiumUntil.getTime()).toBeLessThan(expectedDate.getTime() + toleranceInMs);
});

it('should not be able to redeem non-existing voucher', async () => {
await expect(async () => {
await controller.redeemVoucher(
testAccount.akey,
randomBytes(VOUCHER_LENGTH / 2).toString('hex'),
);
}).rejects.toThrow(NotFoundException);
});

it('should be able to redeem existing voucher', async () => {
const response = await controller.redeemVoucher(testAccount.akey, voucher.code);

const now = new Date();
const expectedDate = new Date(now.getTime() + PREMIUM_DURATION.FIVE_MINUTES * 60000 + (PREMIUM_DURATION.ONE_MONTH * 2) * 60000 + voucher.durationInMinutes * 60000);
const toleranceInMs = 2000;

expect(response).toBeInstanceOf(PremiumStatusDto);
expect(response).toHaveProperty('premiumUntil');

expect(response.premiumUntil.getTime()).toBeGreaterThan(expectedDate.getTime() - toleranceInMs);
expect(response.premiumUntil.getTime()).toBeLessThan(expectedDate.getTime() + toleranceInMs);
});

it('should not be able to redeem already used voucher', async () => {
await expect(async () => {
await controller.redeemVoucher(
testAccount.akey,
voucher.code,
);
}).rejects.toThrow(ConflictException);
});
});
Loading