Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

207 api getwhyranksandpointsdiscussionidround mostids leastids cb ranks points #245

181 changes: 181 additions & 0 deletions app/socket-apis/__tests__/get-why-ranks-and-points.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// https://github.com/EnCiv/civil-pursuit/issues/207

import getWhyRanksAndPoints from '../get-why-ranks-and-points'
import Points from '../../models/points'
import Ranks from '../../models/ranks'
import { Mongo } from '@enciv/mongo-collections'
import { MongoMemoryServer } from 'mongodb-memory-server'
import { ObjectId } from 'mongodb'
import { expect, test } from '@jest/globals'

let MemoryServer

beforeAll(async () => {
MemoryServer = await MongoMemoryServer.create()
const uri = MemoryServer.getUri()
await Mongo.connect(uri)
})

afterAll(async () => {
await Mongo.disconnect()
await MemoryServer.stop()
})

beforeEach(async () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
await Points.deleteMany({})
await Ranks.deleteMany({})
})

afterEach(() => {
console.error.mockRestore()
})

// Config
const discussionId = '66a174b0c3f2051ad387d2a6'
const userId = '6667d5a33da5d19ddc304a6b'
const synuser = { synuser: { id: userId } }
const round = 1
const mostIds = [new ObjectId(), new ObjectId()]
const leastIds = [new ObjectId()]

// Test 1: User not logged in
test('Fail if user is not logged in', async () => {
const cb = jest.fn()
await getWhyRanksAndPoints.call({}, discussionId, round, mostIds, leastIds, cb)
expect(cb).toHaveBeenCalledTimes(1)
expect(cb).toHaveBeenCalledWith({ ranks: [], whys: [] })
expect(console.error).toHaveBeenCalledWith('Cannot retrieve whys - user is not logged in.')
})

// Test 2: No ranks or points found
test('Return empty ranks and points if nothing found', async () => {
const cb = jest.fn()
await getWhyRanksAndPoints.call(synuser, discussionId, round, mostIds, leastIds, cb)
expect(cb).toHaveBeenCalledTimes(1)
expect(cb).toHaveBeenCalledWith({ ranks: [], whys: [] })
})

// Test 3: Case where there are 5 why-points for each mostId and leastId
test('Return why-points if they exist for mostIds and leastIds', async () => {
const cb = jest.fn()

// Insert 5 why-points for each ID
const whys = []
mostIds.concat(leastIds).forEach(parentId => {
for (let i = 0; i < 5; i++) {
whys.push({
_id: new ObjectId(),
parentId: parentId.toString(),
userId,
round,
title: `Why ${parentId}-${i}`,
description: `Description ${i}`,
})
}
})

await Points.insertMany(whys)

// Fetch ranks and points
await getWhyRanksAndPoints.call(synuser, discussionId, round, mostIds, leastIds, cb)

const expectedWhys = await Points.find({}).toArray()
expect(cb).toHaveBeenCalledTimes(1)
expect(cb).toHaveBeenCalledWith({ ranks: [], whys: expectedWhys })
})

// Test 4: Rankings exist for each mostId and leastId
test('Return ranks and points when rankings exist for each ID', async () => {
const cb = jest.fn()

// Insert ranks for each mostId and leastId
const ranks = []
mostIds.concat(leastIds).forEach(parentId => {
for (let i = 0; i < 5; i++) {
ranks.push({
_id: new ObjectId(),
parentId: parentId.toString(),
round,
stage: 'why',
category: 'most',
discussionId,
userId,
})
}
})

// Insert corresponding why-points
const whys = []
mostIds.concat(leastIds).forEach(parentId => {
for (let i = 0; i < 5; i++) {
whys.push({
_id: new ObjectId(),
parentId: parentId.toString(),
userId,
round,
title: `Why ${parentId}-${i}`,
description: `Description ${i}`,
})
}
})

await Ranks.insertMany(ranks)
await Points.insertMany(whys)

// Fetch ranks and points
await getWhyRanksAndPoints.call(synuser, discussionId, round, mostIds, leastIds, cb)

const expectedRanks = await Ranks.find({}).toArray()
const expectedWhys = await Points.find({}).toArray()

expect(cb).toHaveBeenCalledTimes(1)
expect(cb).toHaveBeenCalledWith({ ranks: expectedRanks, whys: expectedWhys })
})

// Test 5: Rankings exist for one ID but not all
test('Handle partial rankings and return appropriate data', async () => {
const cb = jest.fn()

// Insert ranks for only one mostId
const ranks = []
for (let i = 0; i < 5; i++) {
ranks.push({
_id: new ObjectId(),
parentId: mostIds[0].toString(),
round,
rank: i,
stage: 'why',
category: 'most',
discussionId,
userId,
})
}

// Insert why-points for all IDs
const whys = []
mostIds.concat(leastIds).forEach(parentId => {
for (let i = 0; i < 5; i++) {
whys.push({
_id: new ObjectId(),
parentId: parentId.toString(),
userId,
round,
title: `Why ${parentId}-${i}`,
description: `Description ${i}`,
})
}
})

await Ranks.insertMany(ranks)
await Points.insertMany(whys)

// Fetch ranks and points
await getWhyRanksAndPoints.call(synuser, discussionId, round, mostIds, leastIds, cb)

const expectedRanks = await Ranks.find({}).toArray()
const expectedWhys = await Points.find({}).toArray()

expect(cb).toHaveBeenCalledTimes(1)
expect(cb).toHaveBeenCalledWith({ ranks: expectedRanks, whys: expectedWhys })
})
82 changes: 82 additions & 0 deletions app/socket-apis/get-why-ranks-and-points.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// https://github.com/EnCiv/civil-pursuit/issues/207

const Points = require('../models/points')
const Ranks = require('../models/ranks')
const getRandomWhys = require('./get-random-whys')

const WHY_FETCH_COUNT = 5 // Number of "whys" to fetch when an ID has none

async function getWhyRanksAndPoints(discussionId, round, mostIds, leastIds, cb) {
const cbFailure = errorMsg => {
if (errorMsg) console.error(errorMsg)
if (cb) cb({ ranks: [], whys: [] })
}

if (!this.synuser || !this.synuser.id) {
return cbFailure('Cannot retrieve whys - user is not logged in.')
}

if (!discussionId || typeof round !== 'number' || !Array.isArray(mostIds) || !Array.isArray(leastIds)) {
return cbFailure('Invalid argument provided to getWhyRanksAndPoints.')
}

try {
const userId = this.synuser.id

// Step 1: Fetch ranks
const ranks = await Ranks.find({
discussionId,
round,
userId,
stage: 'why',
}).toArray()

// If no ranks exist, fetch random whys directly
if (ranks.length === 0) {
console.log('Ranks are empty. Fetching random whys directly.')
const mostWhysPromises = mostIds.map(id => new Promise(resolve => getRandomWhys.call(this, id, 'most', WHY_FETCH_COUNT, resolve)))
const leastWhysPromises = leastIds.map(id => new Promise(resolve => getRandomWhys.call(this, id, 'least', WHY_FETCH_COUNT, resolve)))

const mostWhys = (await Promise.all(mostWhysPromises)).flat()
const leastWhys = (await Promise.all(leastWhysPromises)).flat()

return cb({ ranks: [], whys: [...mostWhys, ...leastWhys] })
}

// Step 2: Extract parentIds from ranks
const parentIds = ranks.map(rank => rank.parentId.toString())

// Step 3: Fetch points based on parentIds
const points = await Points.find({ _id: { $in: parentIds } }).toArray()

// Step 4: Check if all mostIds and leastIds have corresponding why-points
const pointsWithWhys = new Set(points.map(point => point.parentId.toString()))

const missingMostIds = mostIds.filter(id => !pointsWithWhys.has(id.toString()))
console.log('missingMostIds:', missingMostIds)
const missingLeastIds = leastIds.filter(id => !pointsWithWhys.has(id.toString()))
console.log('missingLeastIds:', missingLeastIds)

if (!missingMostIds.length && !missingLeastIds.length) {
console.log('All mostIds and leastIds have corresponding why-points.')
// If all parentIds have corresponding why-points, return ranks and points
return cb({ ranks, whys: points })
}

// Step 5: Fetch missing whys using getRandomWhys
const mostWhysPromises = missingMostIds.map(id => new Promise(resolve => getRandomWhys.call(this, id, 'most', WHY_FETCH_COUNT, resolve)))
const leastWhysPromises = missingLeastIds.map(id => new Promise(resolve => getRandomWhys.call(this, id, 'least', WHY_FETCH_COUNT, resolve)))

const mostWhys = (await Promise.all(mostWhysPromises)).flat()
const leastWhys = (await Promise.all(leastWhysPromises)).flat()

const allWhys = [...mostWhys, ...leastWhys]

// Combine existing and new points
return cb({ ranks, whys: allWhys })
} catch (error) {
return cbFailure('Failed to retrieve ranks and points.')
}
}

module.exports = getWhyRanksAndPoints