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 {IStory} from './IStory'
import {ITeam} from './ITeam' import {ITeam} from './ITeam'
import {IUser} from './IUser' import {IUser} from './IUser'
import {IUserTeam} from './IUserTeam'
export interface IAPIDef { export interface IAPIDef {
'/auth/register': { '/auth/register': {
@ -59,7 +60,7 @@ export interface IAPIDef {
'/my/teams': { '/my/teams': {
get: { get: {
response: ITeam[] response: IUserTeam[]
} }
} }
@ -94,6 +95,7 @@ export interface IAPIDef {
} }
body: { body: {
name: string name: string
domain: string
} }
response: ISite 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 { export interface ISite {
readonly id: number readonly id: number
readonly name: string readonly name: string
readonly domain: string
readonly teamId: number readonly teamId: number
readonly userId: 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 './IComment'
export * from './ICommentTree' export * from './ICommentTree'
export * from './ICredentials' export * from './ICredentials'
export * from './IRoutes'
export * from './IRequestParams' export * from './IRequestParams'
export * from './IRole'
export * from './IRoutes'
export * from './ISite' export * from './ISite'
export * from './ITeam' export * from './ITeam'
export * from './IUser' export * from './IUser'

View File

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

View File

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

View File

@ -8,8 +8,9 @@ export class RequestLogger implements IMiddleware {
handle: IHandler = (req, res, next) => { handle: IHandler = (req, res, next) => {
const start = Date.now() const start = Date.now()
res.on('finish', () => { res.on('finish', () => {
const duration = Date.now() - start
const { method, originalUrl } = req const { method, originalUrl } = req
const duration = Date.now() - start
this.logger.debug('%s %s %j', method, originalUrl, req.body)
const { statusCode } = res const { statusCode } = res
this.logger.info('%s %s %d %sms', this.logger.info('%s %s %d %sms',
method, originalUrl, statusCode, duration) 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, password: req.body.password,
}) })
await req.logInPromise(user) await req.logInPromise(user)
res.redirect(req.baseUrl) return user
}) })
t.post('/auth/login', async (req, res, next) => { t.post('/auth/login', async (req, res, next) => {
@ -28,11 +28,10 @@ export class LoginRoutes extends BaseRoute<IAPIDef> {
.authenticate('local')(req, res, next) .authenticate('local')(req, res, next)
if (!user) { if (!user) {
res.redirect(`${req.baseUrl}/auth/login`) res.status(401)
return return
} }
await req.logInPromise(user) await req.logInPromise(user)
res.redirect(req.baseUrl)
return user 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 {ISite} from '@rondo/common'
import {ISiteCreateParams} from './ISiteCreateParams'
export interface ISiteService { 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> 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 {IAPIDef} from '@rondo/common'
import {ISiteService} from './ISiteService' import {ISiteService} from './ISiteService'
import {ensureLoggedInApi} from '../middleware' import {ensureLoggedInApi} from '../middleware'
import {IUserPermissions} from '../user/IUserPermissions'
export class SiteRoutes extends BaseRoute<IAPIDef> { export class SiteRoutes extends BaseRoute<IAPIDef> {
constructor( constructor(
protected readonly siteService: ISiteService, protected readonly siteService: ISiteService,
protected readonly t: AsyncRouter<IAPIDef>, protected readonly permissions: IUserPermissions,
t: AsyncRouter<IAPIDef>,
) { ) {
super(t) super(t)
} }
@ -35,9 +37,19 @@ export class SiteRoutes extends BaseRoute<IAPIDef> {
}) })
t.post('/teams/:teamId/sites', async req => { t.post('/teams/:teamId/sites', async req => {
const {name} = req.body const {name, domain} = req.body
const {teamId} = req.params 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 {BaseService} from '../services/BaseService'
import {ISiteCreateParams} from './ISiteCreateParams'
import {ISiteService} from './ISiteService' import {ISiteService} from './ISiteService'
import {Site} from '../entities/Site' import {Site} from '../entities/Site'
export class SiteService extends BaseService implements ISiteService { 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 // TODO check site limit per user
return this.getRepository(Site).save({ return this.getRepository(Site).save(params)
name,
teamId,
userId,
})
} }
async findOne(id: number, teamId: number) { 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 {Team} from '../entities/Team'
import {UserTeam} from '../entities/UserTeam'
import {IUserTeamParams} from './IUserTeamParams'
export interface ITeamService { 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 // 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 {test} from '../test'
import {createTeam} from './TeamTestUtils'
describe('team', () => { describe('team', () => {
@ -14,27 +15,16 @@ describe('team', () => {
t.setHeaders({ cookie, 'x-csrf-token': token }) 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', () => { describe('POST /teams', () => {
it('creates a new team', async () => { it('creates a new team', async () => {
const team = await createTeam('test') const team = await createTeam(t, 'test')
expect(team.name).toEqual('test') expect(team.name).toEqual('test')
}) })
}) })
describe('GET /teams/:id', () => { describe('GET /teams/:id', () => {
it('retrieves a team by id', async () => { it('retrieves a team by id', async () => {
const team = await createTeam('test') const team = await createTeam(t, 'test')
const response = await t const response = await t
.get('/teams/:id', { .get('/teams/:id', {
id: team.id, id: team.id,
@ -46,11 +36,14 @@ describe('team', () => {
describe('GET /my/teams', () => { describe('GET /my/teams', () => {
it('retrieves all teams belonging to current user', async () => { it('retrieves all teams belonging to current user', async () => {
const team = await createTeam('test') const team = await createTeam(t, 'test')
const response = await t const response = await t
.get('/my/teams') .get('/my/teams')
.expect(200) .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 => { t.get('/teams/:id', async req => {
const {id} = req.params const {id} = req.params
return this.teamService.findOne(id, req.user!.id) return this.teamService.findOne(id)
}) })
t.use(ensureLoggedInApi) t.use(ensureLoggedInApi)
@ -27,7 +27,10 @@ export class TeamRoutes extends BaseRoute<IAPIDef> {
t.post('/teams', async req => { t.post('/teams', async req => {
const {name} = req.body const {name} = req.body
return this.teamService.create(name, req.user!.id) return this.teamService.create({
name,
userId: req.user!.id,
})
}) })
} }

View File

@ -1,24 +1,47 @@
import {BaseService} from '../services/BaseService' import {BaseService} from '../services/BaseService'
import {ITeamService} from './ITeamService' import {ITeamService} from './ITeamService'
import {IUserTeamParams} from './IUserTeamParams'
import {Team} from '../entities/Team' import {Team} from '../entities/Team'
import {UserTeam} from '../entities/UserTeam'
export class TeamService extends BaseService implements ITeamService { export class TeamService extends BaseService implements ITeamService {
// TODO check team limit per user // TODO check team limit per user
async create(name: string, userId: number) { async create({name, userId}: {name: string, userId: number}) {
return this.getRepository(Team).save({ const team = await this.getRepository(Team).save({
name, name,
userId, 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) return this.getRepository(Team).findOne(id)
} }
find(userId: number) { async find(userId: number) {
// TODO find all teams via UserTeam instead of userId // TODO find all teams via UserTeam instead of userId
return this.getRepository(Team).find({ return this.getRepository(UserTeam).find({
where: { userId }, 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) { 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 {IRoutes} from '@rondo/common'
import {IBootstrap} from '../application/IBootstrap' import {IBootstrap} from '../application/IBootstrap'
import {RequestTester} from './RequestTester' import {RequestTester} from './RequestTester'
import {Role} from '../entities/Role'
export class TestUtils<T extends IRoutes> { export class TestUtils<T extends IRoutes> {
readonly username = 'test@user.com' 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> { async getError(promise: Promise<any>): Promise<Error> {
let error!: Error let error!: Error
try { try {
@ -95,12 +102,11 @@ export class TestUtils<T extends IRoutes> {
.post(`${context}/app/auth/register`) .post(`${context}/app/auth/register`)
.set('cookie', cookie) .set('cookie', cookie)
.send(this.getLoginBody(token)) .send(this.getLoginBody(token))
.expect(302) .expect(200)
.expect('location', `${context}/app`)
return { return {
cookie: response.header['set-cookie'] as string, cookie: response.header['set-cookie'] as string,
userId: response.body.userId, userId: response.body.id,
token, token,
} }
} }
@ -113,8 +119,7 @@ export class TestUtils<T extends IRoutes> {
.post(`${context}/app/auth/login`) .post(`${context}/app/auth/login`)
.set('cookie', cookie) .set('cookie', cookie)
.send(`username=${_username}&password=${_password}&_csrf=${token}`) .send(`username=${_username}&password=${_password}&_csrf=${token}`)
.expect(302) .expect(200)
.expect('location', `${context}/app`)
return {cookie: response.header['set-cookie'] as string, token} 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'