audiobookshelf/test/server/controllers/ReviewController.test.js
fannta1990 41e8906312 Add review and rating features with sorting and filtering options
- Implemented a new ReviewController to handle review creation, updates, and retrieval for library items.
- Added pagination, sorting, and filtering capabilities for reviews in the API.
- Updated frontend components to support review display, including a new ReviewsTable and enhanced ratings UI.
- Introduced new strings for user interface elements related to reviews and ratings.
- Added tests for the ReviewController and Review model to ensure functionality and validation.
- Enabled the option to toggle the review feature in server settings.
2026-02-09 21:08:18 +08:00

154 lines
5.2 KiB
JavaScript

const { expect } = require('chai')
const { Sequelize } = require('sequelize')
const sinon = require('sinon')
const Database = require('../../../server/Database')
const ApiRouter = require('../../../server/routers/ApiRouter')
const ReviewController = require('../../../server/controllers/ReviewController')
const ApiCacheManager = require('../../../server/managers/ApiCacheManager')
const Auth = require('../../../server/Auth')
const Logger = require('../../../server/Logger')
describe('ReviewController', () => {
/** @type {ApiRouter} */
let apiRouter
beforeEach(async () => {
Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '')
await Database.buildModels()
Database.serverSettings = {
enableReviews: true
}
apiRouter = new ApiRouter({
auth: new Auth(),
apiCacheManager: new ApiCacheManager()
})
sinon.stub(Logger, 'info')
sinon.stub(Logger, 'error')
})
afterEach(async () => {
sinon.restore()
await Database.sequelize.close()
})
async function createTestLibraryItem() {
const library = await Database.libraryModel.create({ name: 'Test', mediaType: 'book' })
const book = await Database.bookModel.create({ title: 'Test Book' })
return await Database.libraryItemModel.create({ mediaId: book.id, mediaType: 'book', libraryId: library.id })
}
describe('createUpdate', () => {
it('should create a new review', async () => {
const user = await Database.userModel.create({ username: 'testuser', type: 'root' })
const libraryItem = await createTestLibraryItem()
const fakeReq = {
params: { id: libraryItem.id },
body: { rating: 5, reviewText: 'Great book!' },
user
}
const fakeRes = {
json: sinon.spy(),
status: sinon.stub().returns({ send: sinon.spy() })
}
await ReviewController.createUpdate(fakeReq, fakeRes)
expect(fakeRes.json.calledOnce).to.be.true
const review = fakeRes.json.firstCall.args[0]
expect(review.rating).to.equal(5)
expect(review.reviewText).to.equal('Great book!')
expect(review.userId).to.equal(user.id)
})
it('should update an existing review', async () => {
const user = await Database.userModel.create({ username: 'testuser', type: 'root' })
const libraryItem = await createTestLibraryItem()
await Database.reviewModel.create({ userId: user.id, libraryItemId: libraryItem.id, rating: 3 })
const fakeReq = {
params: { id: libraryItem.id },
body: { rating: 4, reviewText: 'Actually better' },
user
}
const fakeRes = {
json: sinon.spy()
}
await ReviewController.createUpdate(fakeReq, fakeRes)
const review = fakeRes.json.firstCall.args[0]
expect(review.rating).to.equal(4)
expect(review.reviewText).to.equal('Actually better')
})
it('should return 400 for invalid rating', async () => {
const fakeReq = {
params: { id: 'some-id' },
body: { rating: 6 },
user: { id: 'u1' }
}
const fakeRes = {
status: sinon.stub().returns({ send: sinon.spy() })
}
await ReviewController.createUpdate(fakeReq, fakeRes)
expect(fakeRes.status.calledWith(400)).to.be.true
})
})
describe('findAllForItem', () => {
it('should return all reviews for an item', async () => {
const user1 = await Database.userModel.create({ username: 'u1', type: 'user' })
const user2 = await Database.userModel.create({ username: 'u2', type: 'user' })
const libraryItem = await createTestLibraryItem()
await Database.reviewModel.create({ userId: user1.id, libraryItemId: libraryItem.id, rating: 5 })
await Database.reviewModel.create({ userId: user2.id, libraryItemId: libraryItem.id, rating: 4 })
const fakeReq = { params: { id: libraryItem.id } }
const fakeRes = { json: sinon.spy() }
await ReviewController.findAllForItem(fakeReq, fakeRes)
expect(fakeRes.json.calledOnce).to.be.true
expect(fakeRes.json.firstCall.args[0]).to.have.lengthOf(2)
})
})
describe('delete', () => {
it('should delete a review', async () => {
const user = await Database.userModel.create({ username: 'u1', type: 'user' })
const libraryItem = await createTestLibraryItem()
await Database.reviewModel.create({ userId: user.id, libraryItemId: libraryItem.id, rating: 5 })
const fakeReq = { params: { id: libraryItem.id }, user }
const fakeRes = { sendStatus: sinon.spy() }
await ReviewController.delete(fakeReq, fakeRes)
expect(fakeRes.sendStatus.calledWith(200)).to.be.true
const count = await Database.reviewModel.count()
expect(count).to.equal(0)
})
})
describe('middleware', () => {
it('should block when enableReviews is false', async () => {
Database.serverSettings.enableReviews = false
const fakeReq = {}
const fakeRes = { status: sinon.stub().returns({ send: sinon.spy() }) }
const next = sinon.spy()
await ReviewController.middleware(fakeReq, fakeRes, next)
expect(fakeRes.status.calledWith(403)).to.be.true
expect(next.called).to.be.false
})
})
})