feat: add song crd and tests (#80)
Co-authored-by: Zoe Roux <zoe.roux@sdg.moe>
This commit is contained in:
5
back/.gitignore
vendored
5
back/.gitignore
vendored
@@ -33,3 +33,8 @@ lerna-debug.log*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Robots tests
|
||||
log.html
|
||||
output.xml
|
||||
report.html
|
||||
|
||||
61
back/prisma/migrations/20220924084638_/migration.sql
Normal file
61
back/prisma/migrations/20220924084638_/migration.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Song" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"artistId" INTEGER NOT NULL,
|
||||
"albumId" INTEGER,
|
||||
"genreId" INTEGER NOT NULL,
|
||||
"difficulties" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Song_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Genre" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Genre_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Artist" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Artist_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Album" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Album_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Song_name_key" ON "Song"("name");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_genreId_fkey" FOREIGN KEY ("genreId") REFERENCES "Genre"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
8
back/prisma/migrations/20220926080922_/migration.sql
Normal file
8
back/prisma/migrations/20220926080922_/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Song" DROP CONSTRAINT "Song_genreId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Song" ALTER COLUMN "genreId" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_genreId_fkey" FOREIGN KEY ("genreId") REFERENCES "Genre"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
8
back/prisma/migrations/20220926082047_/migration.sql
Normal file
8
back/prisma/migrations/20220926082047_/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Song" DROP CONSTRAINT "Song_artistId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Song" ALTER COLUMN "artistId" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
8
back/prisma/migrations/20220926084154_/migration.sql
Normal file
8
back/prisma/migrations/20220926084154_/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `description` on the `Song` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Song" DROP COLUMN "description";
|
||||
@@ -17,6 +17,39 @@ model User {
|
||||
LessonHistory LessonHistory[]
|
||||
}
|
||||
|
||||
model Song {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
artistId Int?
|
||||
artist Artist? @relation(fields: [artistId], references: [id])
|
||||
albumId Int?
|
||||
album Album? @relation(fields: [albumId], references: [id])
|
||||
genreId Int?
|
||||
genre Genre? @relation(fields: [genreId], references: [id])
|
||||
difficulties Json
|
||||
}
|
||||
|
||||
model Genre {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
||||
Song Song[]
|
||||
}
|
||||
|
||||
model Artist {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
||||
Song Song[]
|
||||
}
|
||||
|
||||
model Album {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
||||
Song Song[]
|
||||
}
|
||||
|
||||
model Lesson {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
||||
@@ -5,10 +5,11 @@ import { PrismaService } from './prisma/prisma.service';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { SongModule } from './song/song.module';
|
||||
import { LessonModule } from './lesson/lesson.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule, PrismaModule, AuthModule, LessonModule],
|
||||
imports: [UsersModule, PrismaModule, AuthModule, SongModule, LessonModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, PrismaService],
|
||||
})
|
||||
|
||||
@@ -10,7 +10,10 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, UsersModule, PassportModule,
|
||||
imports: [
|
||||
ConfigModule,
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
@@ -18,8 +21,9 @@ import { JwtStrategy } from './jwt.strategy';
|
||||
signOptions: { expiresIn: '1h' },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
})],
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
controllers: [AuthController]
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class Constants {
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getSecret = () => {
|
||||
return this.configService.get("JWT_SECRET");
|
||||
}
|
||||
return this.configService.get('JWT_SECRET');
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RegisterDto {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ import { PrismaService } from './prisma.service';
|
||||
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService]
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
|
||||
12
back/src/song/dto/create-song.dto.ts
Normal file
12
back/src/song/dto/create-song.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class CreateSongDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ApiProperty()
|
||||
difficulties: object;
|
||||
}
|
||||
18
back/src/song/song.controller.spec.ts
Normal file
18
back/src/song/song.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SongController } from './song.controller';
|
||||
|
||||
describe('SongController', () => {
|
||||
let controller: SongController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SongController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SongController>(SongController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
65
back/src/song/song.controller.ts
Normal file
65
back/src/song/song.controller.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
DefaultValuePipe,
|
||||
Delete,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { Plage } from 'src/models/plage';
|
||||
import { CreateSongDto } from './dto/create-song.dto';
|
||||
import { SongService } from './song.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
|
||||
@Controller('song')
|
||||
export class SongController {
|
||||
constructor(private readonly songService: SongService) {}
|
||||
|
||||
@Post()
|
||||
async create(@Body() createSongDto: CreateSongDto) {
|
||||
return await this.songService.createSong(createSongDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return await this.songService.deleteSong({ id });
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@Query() filter: Prisma.SongWhereInput,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Song>> {
|
||||
try {
|
||||
const ret = await this.songService.songs({
|
||||
skip,
|
||||
take,
|
||||
where: {
|
||||
...filter,
|
||||
id: filter.id ? +filter.id : undefined,
|
||||
},
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new BadRequestException(null, e?.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
let res = await this.songService.song({ id });
|
||||
|
||||
if (res === null) throw new NotFoundException('Song not found');
|
||||
return res;
|
||||
}
|
||||
}
|
||||
11
back/src/song/song.module.ts
Normal file
11
back/src/song/song.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SongService } from './song.service';
|
||||
import { SongController } from './song.controller';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [SongService],
|
||||
controllers: [SongController],
|
||||
})
|
||||
export class SongModule {}
|
||||
18
back/src/song/song.service.spec.ts
Normal file
18
back/src/song/song.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SongService } from './song.service';
|
||||
|
||||
describe('SongService', () => {
|
||||
let service: SongService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [SongService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SongService>(SongService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
45
back/src/song/song.service.ts
Normal file
45
back/src/song/song.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class SongService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async createSong(data: Prisma.SongCreateInput): Promise<Song> {
|
||||
return this.prisma.song.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async song(
|
||||
songWhereUniqueInput: Prisma.SongWhereUniqueInput,
|
||||
): Promise<Song | null> {
|
||||
return this.prisma.song.findUnique({
|
||||
where: songWhereUniqueInput,
|
||||
});
|
||||
}
|
||||
|
||||
async songs(params: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
cursor?: Prisma.SongWhereUniqueInput;
|
||||
where?: Prisma.SongWhereInput;
|
||||
orderBy?: Prisma.SongOrderByWithRelationInput;
|
||||
}): Promise<Song[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
return this.prisma.song.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSong(where: Prisma.SongWhereUniqueInput): Promise<Song> {
|
||||
return this.prisma.song.delete({
|
||||
where,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,6 @@ import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
imports: [PrismaModule],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService]
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
1
back/test/robot/env/lib64
vendored
Symbolic link
1
back/test/robot/env/lib64
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
lib
|
||||
84
back/test/robot/songs/songs.robot
Normal file
84
back/test/robot/songs/songs.robot
Normal file
@@ -0,0 +1,84 @@
|
||||
*** Settings ***
|
||||
Documentation Tests of the /song route.
|
||||
... Ensures that the songs CRUD works corectly.
|
||||
Resource ../rest.resource
|
||||
|
||||
|
||||
*** Keywords ***
|
||||
*** Test Cases ***
|
||||
Create a song
|
||||
[Documentation] Create a song
|
||||
&{res}= POST /song {"name": "Mama mia", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
[Teardown] DELETE /song/${res.body.id}
|
||||
|
||||
Find a song
|
||||
[Documentation] Create a song and find it
|
||||
&{res}= POST /song {"name": "Mama mia", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{get}= GET /song/${res.body.id}
|
||||
Output
|
||||
Integer response status 200
|
||||
Should Be Equal ${res.body} ${get.body}
|
||||
[Teardown] DELETE /song/${res.body.id}
|
||||
|
||||
|
||||
Find a song non existant
|
||||
[Documentation] Find non existant song
|
||||
&{get}= GET /song/9999
|
||||
Integer response status 404
|
||||
|
||||
Find multiples songs
|
||||
[Documentation] Create two songs and find them
|
||||
&{res}= POST /song {"name": "Mama mia", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{res2}= POST /song {"name": "Here we go again", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
|
||||
&{get}= GET /song
|
||||
Output
|
||||
Integer response status 200
|
||||
Should Contain ${get.body.data} ${res.body}
|
||||
Should Contain ${get.body.data} ${res2.body}
|
||||
[Teardown] Run Keywords DELETE /song/${res.body.id}
|
||||
... AND DELETE /song/${res2.body.id}
|
||||
|
||||
Find multiples songs filtered
|
||||
[Documentation] Create two songs and find them
|
||||
&{res}= POST /song {"name": "Mamamia", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{res2}= POST /song {"name": "Here we go again", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
|
||||
&{get}= GET /song?name=Mamamia
|
||||
Output
|
||||
Integer response status 200
|
||||
Should Contain ${get.body.data} ${res.body}
|
||||
Should Not Contain ${get.body.data} ${res2.body}
|
||||
[Teardown] Run Keywords DELETE /song/${res.body.id}
|
||||
... AND DELETE /song/${res2.body.id}
|
||||
|
||||
|
||||
|
||||
Find multiples songs filtered by type
|
||||
[Documentation] Create two songs and find them
|
||||
&{res}= POST /song {"name": "Mamamia", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{res2}= POST /song {"name": "Here we go again", "difficulties": {}}
|
||||
Output
|
||||
Integer response status 201
|
||||
|
||||
&{get}= GET /song?id=${res.body.id}
|
||||
Output
|
||||
Integer response status 200
|
||||
Should Contain ${get.body.data} ${res.body}
|
||||
Should Not Contain ${get.body.data} ${res2.body}
|
||||
[Teardown] Run Keywords DELETE /song/${res.body.id}
|
||||
... AND DELETE /song/${res2.body.id}
|
||||
Reference in New Issue
Block a user