Add StoryRoutes.test.ts

This commit is contained in:
Jerko Steiner 2019-01-25 12:28:20 +01:00
parent 2b431bee78
commit 3b295b2e13
31 changed files with 372 additions and 49 deletions

View File

@ -5,6 +5,7 @@ import {ISite} from './ISite'
import {IStory} from './IStory'
import {ITeam} from './ITeam'
import {IUser} from './IUser'
import {IUserTeam} from './IUserTeam'
export interface IAPIDef {
'/auth/register': {
@ -59,7 +60,7 @@ export interface IAPIDef {
'/my/teams': {
get: {
response: ITeam[]
response: IUserTeam[]
}
}
@ -94,6 +95,7 @@ export interface IAPIDef {
}
body: {
name: string
domain: string
}
response: ISite
}

View File

@ -0,0 +1,4 @@
export interface IRole {
readonly id: number
readonly name: string
}

View File

@ -1,6 +1,7 @@
export interface ISite {
readonly id: number
readonly name: string
readonly domain: string
readonly teamId: number
readonly userId: number
}

View File

@ -0,0 +1,8 @@
import {ITeam} from './ITeam'
export interface IUserTeam {
userId: number
teamId: number
roleId: number
team?: ITeam
}

View File

@ -2,8 +2,9 @@ export * from './IAPIDef'
export * from './IComment'
export * from './ICommentTree'
export * from './ICredentials'
export * from './IRoutes'
export * from './IRequestParams'
export * from './IRole'
export * from './IRoutes'
export * from './ISite'
export * from './ITeam'
export * from './IUser'

View File

@ -5,6 +5,7 @@ import * as services from '../services'
import * as site from '../site'
import * as story from '../story'
import * as team from '../team'
import * as user from '../user'
import express from 'express'
import {AsyncRouter, TransactionalRouter} from '../router'
import {IApplication} from './IApplication'
@ -25,6 +26,7 @@ export class Application implements IApplication {
readonly siteService: site.ISiteService
readonly storyService: story.IStoryService
readonly commentService: comment.ICommentService
readonly userPermissions: user.IUserPermissions
readonly authenticator: middleware.Authenticator
@ -39,6 +41,7 @@ export class Application implements IApplication {
this.storyService = new story.StoryService(
this.transactionManager, this.siteService)
this.commentService = new comment.CommentService(this.transactionManager)
this.userPermissions = new user.UserPermissions(this.transactionManager)
this.authenticator = new middleware.Authenticator(this.userService)
@ -107,6 +110,7 @@ export class Application implements IApplication {
router.use('/api', new site.SiteRoutes(
this.siteService,
this.userPermissions,
this.createTransactionalRouter(),
).handle)

View File

@ -3,6 +3,6 @@ import {Column, Entity} from 'typeorm'
@Entity()
export class Role extends BaseEntity {
@Column()
@Column({ unique: true })
name!: string
}

View File

@ -8,8 +8,9 @@ export class RequestLogger implements IMiddleware {
handle: IHandler = (req, res, next) => {
const start = Date.now()
res.on('finish', () => {
const duration = Date.now() - start
const { method, originalUrl } = req
const duration = Date.now() - start
this.logger.debug('%s %s %j', method, originalUrl, req.body)
const { statusCode } = res
this.logger.info('%s %s %d %sms',
method, originalUrl, statusCode, duration)

View File

@ -0,0 +1,80 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class roleUnique1548412884820 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`DROP INDEX "IDX_28c5d1d16da7908c97c9bc2f74"`);
await queryRunner.query(`DROP INDEX "IDX_55a938fda82579fd3ec29b3c28"`);
await queryRunner.query(`DROP INDEX "IDX_4a06baede7d9cf51aef879fb0e"`);
await queryRunner.query(`DROP INDEX "IDX_e03827c061fbf85fd3aae454ae"`);
await queryRunner.query(`DROP INDEX "IDX_fe13edd1431a248a0eeac11ae4"`);
await queryRunner.query(`CREATE TABLE "temporary_spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "spam"`);
await queryRunner.query(`DROP TABLE "spam"`);
await queryRunner.query(`ALTER TABLE "temporary_spam" RENAME TO "spam"`);
await queryRunner.query(`CREATE TABLE "temporary_vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "vote"`);
await queryRunner.query(`DROP TABLE "vote"`);
await queryRunner.query(`ALTER TABLE "temporary_vote" RENAME TO "vote"`);
await queryRunner.query(`CREATE TABLE "temporary_role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL)`);
await queryRunner.query(`INSERT INTO "temporary_role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "role"`);
await queryRunner.query(`DROP TABLE "role"`);
await queryRunner.query(`ALTER TABLE "temporary_role" RENAME TO "role"`);
await queryRunner.query(`CREATE TABLE "temporary_role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, CONSTRAINT "UQ_d430b72bf1eaebce7f87068a431" UNIQUE ("name"))`);
await queryRunner.query(`INSERT INTO "temporary_role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "role"`);
await queryRunner.query(`DROP TABLE "role"`);
await queryRunner.query(`ALTER TABLE "temporary_role" RENAME TO "role"`);
await queryRunner.query(`CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session" ("expiredAt") `);
await queryRunner.query(`CREATE INDEX "IDX_55a938fda82579fd3ec29b3c28" ON "team" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_e03827c061fbf85fd3aae454ae" ON "site" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_4a06baede7d9cf51aef879fb0e" ON "site" ("teamId") `);
await queryRunner.query(`CREATE INDEX "IDX_fe13edd1431a248a0eeac11ae4" ON "comment" ("storyId") `);
await queryRunner.query(`CREATE TABLE "temporary_spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "spam_userid_commentid" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "spam"`);
await queryRunner.query(`DROP TABLE "spam"`);
await queryRunner.query(`ALTER TABLE "temporary_spam" RENAME TO "spam"`);
await queryRunner.query(`CREATE TABLE "temporary_vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "vote_userid_commentid" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "vote"`);
await queryRunner.query(`DROP TABLE "vote"`);
await queryRunner.query(`ALTER TABLE "temporary_vote" RENAME TO "vote"`);
await queryRunner.query(`INSERT INTO "role"("id", "name") VALUES (1, 'ADMIN')`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "vote" RENAME TO "temporary_vote"`);
await queryRunner.query(`CREATE TABLE "vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_vote"`);
await queryRunner.query(`DROP TABLE "temporary_vote"`);
await queryRunner.query(`ALTER TABLE "spam" RENAME TO "temporary_spam"`);
await queryRunner.query(`CREATE TABLE "spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_spam"`);
await queryRunner.query(`DROP TABLE "temporary_spam"`);
await queryRunner.query(`DROP INDEX "IDX_fe13edd1431a248a0eeac11ae4"`);
await queryRunner.query(`DROP INDEX "IDX_4a06baede7d9cf51aef879fb0e"`);
await queryRunner.query(`DROP INDEX "IDX_e03827c061fbf85fd3aae454ae"`);
await queryRunner.query(`DROP INDEX "IDX_55a938fda82579fd3ec29b3c28"`);
await queryRunner.query(`DROP INDEX "IDX_28c5d1d16da7908c97c9bc2f74"`);
await queryRunner.query(`ALTER TABLE "role" RENAME TO "temporary_role"`);
await queryRunner.query(`CREATE TABLE "role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL)`);
await queryRunner.query(`INSERT INTO "role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "temporary_role"`);
await queryRunner.query(`DROP TABLE "temporary_role"`);
await queryRunner.query(`ALTER TABLE "role" RENAME TO "temporary_role"`);
await queryRunner.query(`CREATE TABLE "role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL)`);
await queryRunner.query(`INSERT INTO "role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "temporary_role"`);
await queryRunner.query(`DROP TABLE "temporary_role"`);
await queryRunner.query(`ALTER TABLE "vote" RENAME TO "temporary_vote"`);
await queryRunner.query(`CREATE TABLE "vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "UQ_5ef3b030c86a67d7c3cce97a978" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_vote"`);
await queryRunner.query(`DROP TABLE "temporary_vote"`);
await queryRunner.query(`ALTER TABLE "spam" RENAME TO "temporary_spam"`);
await queryRunner.query(`CREATE TABLE "spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "UQ_885dac94f112af83664ccd06dd9" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_spam"`);
await queryRunner.query(`DROP TABLE "temporary_spam"`);
await queryRunner.query(`CREATE INDEX "IDX_fe13edd1431a248a0eeac11ae4" ON "comment" ("storyId") `);
await queryRunner.query(`CREATE INDEX "IDX_e03827c061fbf85fd3aae454ae" ON "site" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_4a06baede7d9cf51aef879fb0e" ON "site" ("teamId") `);
await queryRunner.query(`CREATE INDEX "IDX_55a938fda82579fd3ec29b3c28" ON "team" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session" ("expiredAt") `);
}
}

View File

@ -0,0 +1,5 @@
import {IRole} from '@rondo/common'
export interface IRoleService {
create(name: string): Promise<IRole>
}

View File

@ -0,0 +1,12 @@
import {BaseService} from '../services/BaseService'
import {IRoleService} from './IRoleService'
import {Role} from '../entities/Role'
export class RoleService extends BaseService implements IRoleService {
create(name: string) {
return this.getRepository(Role)
.save({
name,
})
}
}

View File

@ -0,0 +1,2 @@
export * from './IRoleService'
export * from './RoleService'

View File

@ -20,7 +20,7 @@ export class LoginRoutes extends BaseRoute<IAPIDef> {
password: req.body.password,
})
await req.logInPromise(user)
res.redirect(req.baseUrl)
return user
})
t.post('/auth/login', async (req, res, next) => {
@ -28,11 +28,10 @@ export class LoginRoutes extends BaseRoute<IAPIDef> {
.authenticate('local')(req, res, next)
if (!user) {
res.redirect(`${req.baseUrl}/auth/login`)
res.status(401)
return
}
await req.logInPromise(user)
res.redirect(req.baseUrl)
return user
})

View File

@ -0,0 +1,6 @@
export interface ISiteCreateParams {
name: string
domain: string
teamId: number
userId: number
}

View File

@ -1,7 +1,8 @@
import {ISite} from '@rondo/common'
import {ISiteCreateParams} from './ISiteCreateParams'
export interface ISiteService {
create(name: string, teamId: number, userId: number): Promise<ISite>
create(params: ISiteCreateParams): Promise<ISite>
findOne(id: number, teamId: number): Promise<ISite | undefined>

View File

@ -0,0 +1,54 @@
import {ITeam} from '@rondo/common'
import {UserTeam} from '../entities/UserTeam'
import {createTeam} from '../team/TeamTestUtils'
import {createSite} from './SiteTestUtils'
import {test} from '../test'
describe('team', () => {
test.withDatabase()
const t = test.request('/api')
let cookie!: string
let token!: string
let team!: ITeam
let userId: number
beforeEach(async () => {
const session = await test.registerAccount()
cookie = session.cookie
token = session.token
userId = session.userId
t.setHeaders({ cookie, 'x-csrf-token': token })
team = await createTeam(t, 'test')
})
describe('POST /teams/:teamId/sites', () => {
it('creates a new site', async () => {
await createSite(t, 'test.example.com')
})
describe('no team access', () => {
beforeEach(async () => {
await test.transactionManager
.getRepository(UserTeam)
.delete({
userId,
teamId: team.id,
})
})
it('results with 403 when user does not have team access ', async () => {
await t
.post('/teams/:teamId/sites', {teamId: team.id})
.send({
domain: 'test.example.com',
name: 'test',
})
.expect(403)
})
})
})
})

View File

@ -3,11 +3,13 @@ import {BaseRoute} from '../routes/BaseRoute'
import {IAPIDef} from '@rondo/common'
import {ISiteService} from './ISiteService'
import {ensureLoggedInApi} from '../middleware'
import {IUserPermissions} from '../user/IUserPermissions'
export class SiteRoutes extends BaseRoute<IAPIDef> {
constructor(
protected readonly siteService: ISiteService,
protected readonly t: AsyncRouter<IAPIDef>,
protected readonly permissions: IUserPermissions,
t: AsyncRouter<IAPIDef>,
) {
super(t)
}
@ -35,9 +37,19 @@ export class SiteRoutes extends BaseRoute<IAPIDef> {
})
t.post('/teams/:teamId/sites', async req => {
const {name} = req.body
const {name, domain} = req.body
const {teamId} = req.params
return this.siteService.create(name, teamId, req.user!.id)
await this.permissions.belongsToTeam({
teamId,
userId: req.user!.id,
})
return this.siteService.create({
name,
domain,
teamId,
userId: req.user!.id,
})
})
}

View File

@ -1,15 +1,12 @@
import {BaseService} from '../services/BaseService'
import {ISiteCreateParams} from './ISiteCreateParams'
import {ISiteService} from './ISiteService'
import {Site} from '../entities/Site'
export class SiteService extends BaseService implements ISiteService {
async create(name: string, teamId: number, userId: number) {
async create(params: ISiteCreateParams) {
// TODO check site limit per user
return this.getRepository(Site).save({
name,
teamId,
userId,
})
return this.getRepository(Site).save(params)
}
async findOne(id: number, teamId: number) {

View File

@ -0,0 +1,18 @@
import {RequestTester} from '../test-utils'
import {IAPIDef} from '@rondo/common'
import {createTeam} from '../team/TeamTestUtils'
export async function createSite(t: RequestTester<IAPIDef>, domain: string) {
const team = await createTeam(t, 'test')
const response = await t
.post('/teams/:teamId/sites', {
teamId: team.id,
})
.send({
domain,
name: 'test-site',
})
.expect(200)
expect(response.body.id).toBeTruthy()
return response.body
}

View File

@ -0,0 +1,40 @@
// import {ISite, IStory} from '@rondo/common'
// import {createTeam} from '../team/TeamTestUtils'
// import {test} from '../test'
// describe('team', () => {
// test.withDatabase()
// const t = test.request('/api')
// let cookie!: string
// let token!: string
// let team!: ITeam
// beforeEach(async () => {
// const session = await test.registerAccount()
// cookie = session.cookie
// token = session.token
// t.setHeaders({ cookie, 'x-csrf-token': token })
// team = await createTeam(t, 'test')
// })
// describe('/stories/by-url', () => {
// it('returns undefined when a site is not configured', async () => {
// })
// it('creates a story when it does not exist', async () => {
// })
// it('retrieves existing story after it is created', async () => {
// })
// it('prevents unique exceptions', async () => {
// })
// })
// })

View File

@ -1,11 +1,17 @@
import {Team} from '../entities/Team'
import {UserTeam} from '../entities/UserTeam'
import {IUserTeamParams} from './IUserTeamParams'
export interface ITeamService {
create(name: string, userId: number): Promise<Team>
create(params: {name: string, userId: number}): Promise<Team>
findOne(id: number, userId: number): Promise<Team | undefined>
addUser(params: IUserTeamParams): Promise<UserTeam>
find(userId: number): Promise<Team[]>
removeUser(params: IUserTeamParams): Promise<void>
findOne(id: number): Promise<Team | undefined>
find(userId: number): Promise<UserTeam[]>
// TODO add other methods
}

View File

@ -0,0 +1,5 @@
export interface IUserTeamParams {
teamId: number
userId: number
roleId: number
}

View File

@ -1,4 +1,5 @@
import {test} from '../test'
import {createTeam} from './TeamTestUtils'
describe('team', () => {
@ -14,27 +15,16 @@ describe('team', () => {
t.setHeaders({ cookie, 'x-csrf-token': token })
})
async function createTeam(name: string) {
const response = await t
.post('/teams')
.send({
name: 'test',
})
.expect(200)
expect(response.body.id).toBeTruthy()
return response.body
}
describe('POST /teams', () => {
it('creates a new team', async () => {
const team = await createTeam('test')
const team = await createTeam(t, 'test')
expect(team.name).toEqual('test')
})
})
describe('GET /teams/:id', () => {
it('retrieves a team by id', async () => {
const team = await createTeam('test')
const team = await createTeam(t, 'test')
const response = await t
.get('/teams/:id', {
id: team.id,
@ -46,11 +36,14 @@ describe('team', () => {
describe('GET /my/teams', () => {
it('retrieves all teams belonging to current user', async () => {
const team = await createTeam('test')
const team = await createTeam(t, 'test')
const response = await t
.get('/my/teams')
.expect(200)
expect(response.body).toContainEqual(team)
expect(response.body.map(ut => ({teamId: ut.teamId})))
.toContainEqual({
teamId: team.id,
})
})
})

View File

@ -16,7 +16,7 @@ export class TeamRoutes extends BaseRoute<IAPIDef> {
t.get('/teams/:id', async req => {
const {id} = req.params
return this.teamService.findOne(id, req.user!.id)
return this.teamService.findOne(id)
})
t.use(ensureLoggedInApi)
@ -27,7 +27,10 @@ export class TeamRoutes extends BaseRoute<IAPIDef> {
t.post('/teams', async req => {
const {name} = req.body
return this.teamService.create(name, req.user!.id)
return this.teamService.create({
name,
userId: req.user!.id,
})
})
}

View File

@ -1,23 +1,46 @@
import {BaseService} from '../services/BaseService'
import {ITeamService} from './ITeamService'
import {IUserTeamParams} from './IUserTeamParams'
import {Team} from '../entities/Team'
import {UserTeam} from '../entities/UserTeam'
export class TeamService extends BaseService implements ITeamService {
// TODO check team limit per user
async create(name: string, userId: number) {
return this.getRepository(Team).save({
async create({name, userId}: {name: string, userId: number}) {
const team = await this.getRepository(Team).save({
name,
userId,
})
await this.addUser({
teamId: team.id,
userId,
// ADMIN role
roleId: 1,
})
return team
}
findOne(id: number) {
async addUser(params: IUserTeamParams) {
const {userId, teamId, roleId} = params
return this.getRepository(UserTeam)
.save({userId, teamId, roleId})
}
async removeUser({teamId, userId, roleId}: IUserTeamParams) {
await this.getRepository(UserTeam)
.delete({userId, teamId, roleId})
}
async findOne(id: number) {
return this.getRepository(Team).findOne(id)
}
find(userId: number) {
async find(userId: number) {
// TODO find all teams via UserTeam instead of userId
return this.getRepository(Team).find({
return this.getRepository(UserTeam).find({
relations: ['team'],
where: {userId},
})
}

View File

@ -0,0 +1,13 @@
import {RequestTester} from '../test-utils'
import {IAPIDef} from '@rondo/common'
export async function createTeam(t: RequestTester<IAPIDef>, name: string) {
const response = await t
.post('/teams')
.send({
name: 'test',
})
.expect(200)
expect(response.body.id).toBeTruthy()
return response.body
}

View File

@ -71,6 +71,6 @@ export class RequestTester<R extends IRoutes> {
}
post<P extends keyof R & string>(path: P, params?: IRequestParams) {
return this.request('post', path)
return this.request('post', path, params)
}
}

View File

@ -7,6 +7,7 @@ import {
import {IRoutes} from '@rondo/common'
import {IBootstrap} from '../application/IBootstrap'
import {RequestTester} from './RequestTester'
import {Role} from '../entities/Role'
export class TestUtils<T extends IRoutes> {
readonly username = 'test@user.com'
@ -52,6 +53,12 @@ export class TestUtils<T extends IRoutes> {
})
}
async createRole(name: string) {
return this.transactionManager
.getRepository(Role)
.save({name})
}
async getError(promise: Promise<any>): Promise<Error> {
let error!: Error
try {
@ -95,12 +102,11 @@ export class TestUtils<T extends IRoutes> {
.post(`${context}/app/auth/register`)
.set('cookie', cookie)
.send(this.getLoginBody(token))
.expect(302)
.expect('location', `${context}/app`)
.expect(200)
return {
cookie: response.header['set-cookie'] as string,
userId: response.body.userId,
userId: response.body.id,
token,
}
}
@ -113,8 +119,7 @@ export class TestUtils<T extends IRoutes> {
.post(`${context}/app/auth/login`)
.set('cookie', cookie)
.send(`username=${_username}&password=${_password}&_csrf=${token}`)
.expect(302)
.expect('location', `${context}/app`)
.expect(200)
return {cookie: response.header['set-cookie'] as string, token}
}

View File

@ -0,0 +1,4 @@
export interface IUserPermissions {
// TODO check for role too
belongsToTeam(params: {userId: number, teamId: number}): void
}

View File

@ -0,0 +1,22 @@
import createError from 'http-errors'
import {BaseService} from '../services/BaseService'
import {UserTeam} from '../entities/UserTeam'
import {IUserPermissions} from './IUserPermissions'
export class UserPermissions extends BaseService implements IUserPermissions {
async belongsToTeam(params: {userId: number, teamId: number}) {
const {userId, teamId} = params
const result = await this.getRepository(UserTeam)
.findOne({
where: {userId, teamId},
})
this.assert(result)
}
protected assert(value: any) {
if (!value) {
throw createError(403, 'Forbidden')
}
}
}

View File

@ -0,0 +1,2 @@
export * from './IUserPermissions'
export * from './UserPermissions'