node-mongo-demo

node.js and mongodb demo

git clone https://9o.is/git/node-mongo-demo.git

commit 8ee73c0411858812679c0ee9f8d15f60596c4d97
parent aa0761227301108a9a275db58cf84f9420d89c4c
Author: Jul <jul@9o.is>
Date:   Tue, 28 Jan 2025 21:29:54 -0500

move tests to separate directory

Diffstat:
Dbackend/src/api/lucky7.int.test.js | 126-------------------------------------------------------------------------------
Dbackend/src/events/lucky7-bets.unit.test.js | 257-------------------------------------------------------------------------------
Dbackend/src/models/lucky7-bet.unit.test.js | 118-------------------------------------------------------------------------------
Dbackend/src/models/lucky7-session.unit.test.js | 56--------------------------------------------------------
Dbackend/src/utils/timer.unit.test.js | 83-------------------------------------------------------------------------------
Abackend/tests/lucky7-bet-events.unit.test.js | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abackend/tests/lucky7-bet-model.unit.test.js | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abackend/tests/lucky7-session-model.unit.test.js | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abackend/tests/lucky7.int.test.js | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abackend/tests/timer.unit.test.js | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 640 insertions(+), 640 deletions(-)

diff --git a/backend/src/api/lucky7.int.test.js b/backend/src/api/lucky7.int.test.js @@ -1,126 +0,0 @@ -import express from 'express'; -import bodyParser from "body-parser"; -import jwt from 'jsonwebtoken'; -import request from 'supertest'; -import { jest } from '@jest/globals'; -import lucky7 from './lucky7.js'; -import { changeStream } from './lucky7-bets.js'; - -describe('Lucky7', () => { - let app; - - beforeAll(() => { - app = express(); - app.use(bodyParser.json()); - app.use(lucky7); - }); - - describe('POST /sessions', () => { - let sessionId; - - const post = () => - auth(request(app).post('/sessions')); - - it('creates', async () => { - const res = await post(); - - expect(res.status).toBe(201); - expect(res.body).toHaveProperty('id'); - expect(res.body).toHaveProperty('createdAt'); - - sessionId = res.body.id; - }); - - it('fetches existing session', async () => { - const res = await post(); - - expect(res.status).toBe(201); - expect(res.body.id).toBe(sessionId); - }); - }); - - describe('POST /bets', () => { - let betId; - - const post = body => - auth(request(app).post('/bets').send(body)); - - beforeAll(() => { - jest.useFakeTimers({ doNotFake: ['nextTick'] }); - }); - - it('creates', async () => { - const res = await post({ lucky: true }); - - expect(res.status).toBe(201); - expect(res.body).toHaveProperty('id'); - expect(res.body).toHaveProperty('state'); - expect(res.body).toHaveProperty('rollAt'); - expect(res.body).toHaveProperty('lucky'); - expect(res.body).not.toHaveProperty('roll'); - expect(res.body).not.toHaveProperty('win'); - expect(res.body.lucky).toBe(true); - - betId = res.body.id; - }); - - it('updates existing bet', async () => { - const res = await post({ lucky: false }); - - expect(res.status).toBe(201); - expect(res.body.id).toBe(betId); - expect(res.body.lucky).toBe(false); - }); - - it('errors on invalid input', async () => { - const res = await post({}); - - expect(res.status).toBe(400); - expect(res.body.error.details[0].message).toBe( - '"lucky" is required', - ); - }); - - it('errors on unplayable session', async () => { - // advance 10 seconds - jest.advanceTimersByTime(10 * 1000); - - const res = await post({ lucky: true }); - - expect(res.status).toBe(409); - expect(res.body.code).toBe('SESSION_TIMER'); - }); - - it('errors on expiring session', async () => { - // advance almost 1 day - jest.advanceTimersByTime(86390 * 1000); - - const res = await post({ lucky: true }); - - expect(res.status).toBe(409); - expect(res.body.code).toBe('SESSION_EXPIRING'); - }); - - }); - - it('GET /leaderboard/streaks', async () => { - const res = await request(app).get('/leaderboard/streaks'); - - expect(res.status).toBe(200); - expect(Array.isArray(res.body)).toBe(true); - }); - - afterAll(async () => { - await changeStream.close(); - }); -}); - -function auth(request) { - const token = jwt.sign( - { _id: '6799219573e6c41d3aaece71' }, - 'test', - { expiresIn: '1h' } - ); - - return request.set('Authorization', `Bearer ${token}`); -} diff --git a/backend/src/events/lucky7-bets.unit.test.js b/backend/src/events/lucky7-bets.unit.test.js @@ -1,257 +0,0 @@ -import { subscribe, onChange } from './lucky7-bets.js'; -import mongoose from 'mongoose'; -import { jest } from '@jest/globals'; - -describe('Bet Events', () => { - it('insert', () => { - setTime('2024-01-01T00:00:00Z'); - - const stream = createStream(); - const bet = { - _id: id(), - userId: id(), - lucky: true, - roll: [3,4], - rollAt: new Date('2024-01-01T00:00:15Z'), - }; - - subscribe(bet.userId.toString(), stream); - onChange(createInsertEvent(bet)); - - expect(stream.writeJson).toHaveBeenCalledTimes(1); - expect(stream.writeJson).toHaveBeenCalledWith({ - operationType: 'insert', - bet: { - id: bet._id.toString(), - state: 'pending', - lucky: bet.lucky, - rollAt: bet.rollAt, - roll: undefined, - win: undefined, - }, - }); - }); - - it('update', () => { - setTime('2024-01-01T00:00:00Z'); - - const stream = createStream(); - const bet = { - _id: id(), - userId: id(), - lucky: true, - rollAt: new Date('2024-01-01T00:00:15Z'), - }; - - subscribe(bet.userId.toString(), stream); - onChange(createInsertEvent(bet)); - onChange(createUpdateEvent(bet._id, { lucky: false })); - - expect(stream.writeJson).toHaveBeenCalledTimes(2); - expect(stream.writeJson).toHaveBeenNthCalledWith(2, { - operationType: 'update', - bet: { - id: bet._id.toString(), - state: 'pending', - lucky: false, - }, - }); - }); - - it('finished event', () => { - setTime('2024-01-01T00:00:00Z'); - - const stream = createStream(); - const bet = { - _id: id(), - userId: id(), - lucky: true, - roll: [3,4], - rollAt: new Date('2024-01-01T00:00:15Z'), - }; - - subscribe(bet.userId.toString(), stream); - onChange(createInsertEvent(bet)); - - jest.advanceTimersByTime(14999); - expect(stream.writeJson).toHaveBeenCalledTimes(1); - - jest.advanceTimersByTime(1); - expect(stream.writeJson).toHaveBeenCalledTimes(2); - - expect(stream.writeJson).toHaveBeenNthCalledWith(2, { - operationType: 'update', - bet: { - id: bet._id.toString(), - state: 'finished', - lucky: bet.lucky, - rollAt: bet.rollAt, - roll: bet.roll, - win: true, - }, - }); - }); - - it('ignores other user\' events', () => { - setTime('2024-01-01T00:00:00Z'); - - const stream = createStream(); - const userId = id(); - const bet = { - _id: id(), - userId: id(), - lucky: true, - roll: [3,4], - rollAt: new Date('2024-01-01T00:00:15Z'), - }; - - subscribe(userId.toString(), stream); - onChange(createInsertEvent(bet)); - - expect(stream.writeJson).toHaveBeenCalledTimes(0); - }); - - it('multiple bets', () => { - setTime('2024-01-01T00:00:00Z'); - - const stream = createStream(); - const userId = id(); - - const bet1 = { - _id: id(), - userId, - lucky: true, - roll: [3,4], - rollAt: new Date('2024-01-01T00:00:15Z'), - }; - - const bet2 = { - _id: id(), - userId, - lucky: false, - roll: [5,2], - rollAt: new Date('2024-01-01T00:00:10Z'), - }; - - subscribe(userId.toString(), stream); - onChange(createInsertEvent(bet1)); - onChange(createInsertEvent(bet2)); - - expect(stream.writeJson).toHaveBeenCalledTimes(2); - - expect(stream.writeJson).toHaveBeenCalledWith({ - operationType: 'insert', - bet: { - id: bet1._id.toString(), - state: 'pending', - lucky: bet1.lucky, - rollAt: bet1.rollAt, - roll: undefined, - win: undefined, - }, - }); - - expect(stream.writeJson).toHaveBeenCalledWith({ - operationType: 'insert', - bet: { - id: bet2._id.toString(), - state: 'pending', - lucky: bet2.lucky, - rollAt: bet2.rollAt, - roll: undefined, - win: undefined, - }, - }); - }); - - it('multiple users', () => { - setTime('2024-01-01T00:00:00Z'); - - const user1Id = id(); - const user2Id = id(); - const stream1 = createStream(); - const stream2 = createStream(); - - const bet1 = { - _id: id(), - userId: user1Id, - lucky: true, - roll: [3,4], - rollAt: new Date('2024-01-01T00:00:15Z'), - }; - - const bet2 = { - _id: id(), - userId: user2Id, - lucky: false, - roll: [5,2], - rollAt: new Date('2024-01-01T00:00:10Z'), - }; - - subscribe(user1Id.toString(), stream1); - subscribe(user2Id.toString(), stream2); - - onChange(createInsertEvent(bet2)); - onChange(createInsertEvent(bet1)); - - expect(stream1.writeJson).toHaveBeenCalledTimes(1); - expect(stream1.writeJson).toHaveBeenCalledWith({ - operationType: 'insert', - bet: { - id: bet1._id.toString(), - state: 'pending', - lucky: bet1.lucky, - rollAt: bet1.rollAt, - roll: undefined, - win: undefined, - }, - }); - - expect(stream2.writeJson).toHaveBeenCalledTimes(1); - expect(stream2.writeJson).toHaveBeenCalledWith({ - operationType: 'insert', - bet: { - id: bet2._id.toString(), - state: 'pending', - lucky: bet2.lucky, - rollAt: bet2.rollAt, - roll: undefined, - win: undefined, - }, - }); - }); -}); - -function createInsertEvent(fullDocument) { - return { - operationType: 'insert', - fullDocument, - }; -} - -function createUpdateEvent(_id, updatedFields) { - return { - operationType: 'update', - documentKey: { - _id, - }, - updateDescription: { - updatedFields, - }, - }; -} - -function createStream() { - return { - writeJson: jest.fn(), - onClose: () => {}, - }; -} - -function id(x) { - return new mongoose.Types.ObjectId(x); -} - -function setTime(dt) { - jest.useFakeTimers().setSystemTime(new Date(dt)); -} diff --git a/backend/src/models/lucky7-bet.unit.test.js b/backend/src/models/lucky7-bet.unit.test.js @@ -1,118 +0,0 @@ -import Lucky7Bet from './lucky7-bet.js'; -import { jest } from '@jest/globals'; - -describe('Lucky7Bet Model', () => { - it('state pending', () => { - setTime('2024-01-01T00:00:00Z'); - - const bet = new Lucky7Bet({ - rollAt: new Date('2024-01-01T00:00:01Z'), - }); - - expect(bet.state).toBe('pending'); - }); - - it('state finished', () => { - setTime('2024-01-01T00:00:01Z'); - - const bet = new Lucky7Bet({ - rollAt: new Date('2024-01-01T00:00:01Z'), - }); - - expect(bet.state).toBe('finished'); - }); - - it('won lucky', () => { - const bet = new Lucky7Bet({ - roll: [3,4], - lucky: true, - }); - - expect(bet.win).toBe(true); - }); - - it('won unlucky', () => { - const bet = new Lucky7Bet({ - roll: [1,1], - lucky: false, - }); - - expect(bet.win).toBe(true); - }); - - it('lost lucky', () => { - const bet = new Lucky7Bet({ - roll: [1,1], - lucky: true, - }); - - expect(bet.win).toBe(false); - }); - - it('lost unlucky', () => { - const bet = new Lucky7Bet({ - roll: [3,4], - lucky: false, - }); - - expect(bet.win).toBe(false); - }); - - it('played() false', () => { - const bet = new Lucky7Bet(); - expect(bet.played()).toBe(false); - }); - - it('played() true', () => { - const bet = new Lucky7Bet(); - bet.play(); - expect(bet.played()).toBe(true); - }); - - it('toRedactedObject() pending', () => { - setTime('2024-01-01T00:00:00Z'); - - const bet = new Lucky7Bet(createProps({ - rollAt: new Date('2024-01-01T01:00:00Z'), - })); - const obj = bet.toRedactedObject(); - - expect(Object.keys(obj).length).toBe(6); - expect(obj.id).toBeDefined(); - expect(obj.rollAt).toBeDefined(); - expect(obj.lucky).toBeDefined(); - expect(obj.state).toBeDefined(); - expect(obj.roll).toBeUndefined(); - expect(obj.win).toBeUndefined(); - }); - - it('toRedactedObject() finished', () => { - setTime('2024-01-01T00:00:00Z'); - - const bet = new Lucky7Bet(createProps({ - rollAt: new Date('2024-01-01T00:00:00Z'), - })); - const obj = bet.toRedactedObject(); - - expect(Object.keys(obj)).toHaveLength(6); - expect(obj.id).toBeDefined(); - expect(obj.rollAt).toBeDefined(); - expect(obj.lucky).toBeDefined(); - expect(obj.state).toBeDefined(); - expect(obj.roll).toBeDefined(); - expect(obj.win).toBeDefined(); - }); -}); - -function createProps(props) { - return { - rollAt: new Date('2024-01-01T00:00:00Z'), - lucky: true, - roll: [3,4], - ...props, - }; -} - -function setTime(dt) { - jest.useFakeTimers().setSystemTime(new Date(dt)); -} diff --git a/backend/src/models/lucky7-session.unit.test.js b/backend/src/models/lucky7-session.unit.test.js @@ -1,56 +0,0 @@ -import Lucky7Session from './lucky7-session.js'; -import { jest } from '@jest/globals'; - -describe('Lucky7Session Model', () => { - it('game intervals are set to 15 seconds', () => { - const session = new Lucky7Session(); - expect(session.gameIntervalInSeconds).toBe(15); - }); - - it('expiration set to one day', () => { - const date1 = new Date('2024-01-01T00:00:00Z'); - const date2 = new Date('2024-01-02T00:00:00Z'); - const session = new Lucky7Session({ - createdAt: date1, - }); - - const expiration = String(session.expiresAt); - expect(expiration).toBe(String(date2)); - }); - - it('nextIntervalAt()', () => { - setTime('2024-01-01T00:00:01Z'); - - const session = new Lucky7Session({ - createdAt: '2024-01-01T00:00:00Z', - }); - - const actual = session.nextIntervalAt().toISOString(); - expect(actual).toBe('2024-01-01T00:00:15.000Z'); - }); - - it('isPlayable() false', () => { - setTime('2024-01-01T00:00:10Z'); - - const session = new Lucky7Session({ - createdAt: '2024-01-01T00:00:00Z', - }); - - expect(session.isPlayable()).toBe(false); - }); - - it('isPlayable() true', () => { - setTime('2024-01-01T00:00:09.999Z'); - - const session = new Lucky7Session({ - createdAt: '2024-01-01T00:00:00Z', - }); - - expect(session.isPlayable()).toBe(true); - }); -}); - -function setTime(dt) { - jest.useFakeTimers().setSystemTime(new Date(dt)); -} - diff --git a/backend/src/utils/timer.unit.test.js b/backend/src/utils/timer.unit.test.js @@ -1,83 +0,0 @@ -import timer from './timer.js'; -import { jest } from '@jest/globals'; - -describe('Timer', () => { - it('nextIntervalAt before interval', () => { - setTime('2024-01-01T00:00:14.999Z'); - - const { nextIntervalAt } = timer({ - createdAt: new Date('2024-01-01T00:00:00Z'), - expiresAt: new Date('2024-01-01T01:00:00Z'), - gameIntervalInSeconds: 15, - }); - - const actual = nextIntervalAt.toISOString(); - expect(actual).toBe('2024-01-01T00:00:15.000Z'); - }); - - it('nextIntervalAt at interval', () => { - setTime('2024-01-01T00:00:15.000Z'); - - const { nextIntervalAt } = timer({ - createdAt: new Date('2024-01-01T00:00:00Z'), - expiresAt: new Date('2024-01-01T01:00:00Z'), - gameIntervalInSeconds: 15, - }); - - const actual = nextIntervalAt.toISOString(); - expect(actual).toBe('2024-01-01T00:00:15.000Z'); - }); - - it('nextIntervalAt after interval', () => { - setTime('2024-01-01T00:00:15.001Z'); - - const { nextIntervalAt } = timer({ - createdAt: new Date('2024-01-01T00:00:00Z'), - expiresAt: new Date('2024-01-01T01:00:00Z'), - gameIntervalInSeconds: 15, - }); - - const actual = nextIntervalAt.toISOString(); - expect(actual).toBe('2024-01-01T00:00:30.000Z'); - }); - - it('nextIntervalInMillis', () => { - setTime('2024-01-01T00:00:14.997Z'); - - const { nextIntervalInMillis } = timer({ - createdAt: new Date('2024-01-01T00:00:00Z'), - expiresAt: new Date('2024-01-01T01:00:00Z'), - gameIntervalInSeconds: 15, - }); - - expect(nextIntervalInMillis).toBe(3); - }); - - it('non-expiring session', () => { - setTime('2024-01-01T00:59:45.000Z'); - - const { nextIntervalAt } = timer({ - createdAt: new Date('2024-01-01T00:00:00Z'), - expiresAt: new Date('2024-01-01T01:00:00Z'), - gameIntervalInSeconds: 15, - }); - - expect(nextIntervalAt).toBeDefined(); - }); - - it('expiring session', () => { - setTime('2024-01-01T00:59:45.001Z'); - - const { nextIntervalAt } = timer({ - createdAt: new Date('2024-01-01T00:00:00Z'), - expiresAt: new Date('2024-01-01T01:00:00Z'), - gameIntervalInSeconds: 15, - }); - - expect(nextIntervalAt).toBeUndefined(); - }); -}); - -function setTime(dt) { - jest.useFakeTimers().setSystemTime(new Date(dt)); -} diff --git a/backend/tests/lucky7-bet-events.unit.test.js b/backend/tests/lucky7-bet-events.unit.test.js @@ -0,0 +1,257 @@ +import { subscribe, onChange } from '../src/events/lucky7-bets.js'; +import mongoose from 'mongoose'; +import { jest } from '@jest/globals'; + +describe('Bet Events', () => { + it('insert', () => { + setTime('2024-01-01T00:00:00Z'); + + const stream = createStream(); + const bet = { + _id: id(), + userId: id(), + lucky: true, + roll: [3,4], + rollAt: new Date('2024-01-01T00:00:15Z'), + }; + + subscribe(bet.userId.toString(), stream); + onChange(createInsertEvent(bet)); + + expect(stream.writeJson).toHaveBeenCalledTimes(1); + expect(stream.writeJson).toHaveBeenCalledWith({ + operationType: 'insert', + bet: { + id: bet._id.toString(), + state: 'pending', + lucky: bet.lucky, + rollAt: bet.rollAt, + roll: undefined, + win: undefined, + }, + }); + }); + + it('update', () => { + setTime('2024-01-01T00:00:00Z'); + + const stream = createStream(); + const bet = { + _id: id(), + userId: id(), + lucky: true, + rollAt: new Date('2024-01-01T00:00:15Z'), + }; + + subscribe(bet.userId.toString(), stream); + onChange(createInsertEvent(bet)); + onChange(createUpdateEvent(bet._id, { lucky: false })); + + expect(stream.writeJson).toHaveBeenCalledTimes(2); + expect(stream.writeJson).toHaveBeenNthCalledWith(2, { + operationType: 'update', + bet: { + id: bet._id.toString(), + state: 'pending', + lucky: false, + }, + }); + }); + + it('finished event', () => { + setTime('2024-01-01T00:00:00Z'); + + const stream = createStream(); + const bet = { + _id: id(), + userId: id(), + lucky: true, + roll: [3,4], + rollAt: new Date('2024-01-01T00:00:15Z'), + }; + + subscribe(bet.userId.toString(), stream); + onChange(createInsertEvent(bet)); + + jest.advanceTimersByTime(14999); + expect(stream.writeJson).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(1); + expect(stream.writeJson).toHaveBeenCalledTimes(2); + + expect(stream.writeJson).toHaveBeenNthCalledWith(2, { + operationType: 'update', + bet: { + id: bet._id.toString(), + state: 'finished', + lucky: bet.lucky, + rollAt: bet.rollAt, + roll: bet.roll, + win: true, + }, + }); + }); + + it('ignores other user\' events', () => { + setTime('2024-01-01T00:00:00Z'); + + const stream = createStream(); + const userId = id(); + const bet = { + _id: id(), + userId: id(), + lucky: true, + roll: [3,4], + rollAt: new Date('2024-01-01T00:00:15Z'), + }; + + subscribe(userId.toString(), stream); + onChange(createInsertEvent(bet)); + + expect(stream.writeJson).toHaveBeenCalledTimes(0); + }); + + it('multiple bets', () => { + setTime('2024-01-01T00:00:00Z'); + + const stream = createStream(); + const userId = id(); + + const bet1 = { + _id: id(), + userId, + lucky: true, + roll: [3,4], + rollAt: new Date('2024-01-01T00:00:15Z'), + }; + + const bet2 = { + _id: id(), + userId, + lucky: false, + roll: [5,2], + rollAt: new Date('2024-01-01T00:00:10Z'), + }; + + subscribe(userId.toString(), stream); + onChange(createInsertEvent(bet1)); + onChange(createInsertEvent(bet2)); + + expect(stream.writeJson).toHaveBeenCalledTimes(2); + + expect(stream.writeJson).toHaveBeenCalledWith({ + operationType: 'insert', + bet: { + id: bet1._id.toString(), + state: 'pending', + lucky: bet1.lucky, + rollAt: bet1.rollAt, + roll: undefined, + win: undefined, + }, + }); + + expect(stream.writeJson).toHaveBeenCalledWith({ + operationType: 'insert', + bet: { + id: bet2._id.toString(), + state: 'pending', + lucky: bet2.lucky, + rollAt: bet2.rollAt, + roll: undefined, + win: undefined, + }, + }); + }); + + it('multiple users', () => { + setTime('2024-01-01T00:00:00Z'); + + const user1Id = id(); + const user2Id = id(); + const stream1 = createStream(); + const stream2 = createStream(); + + const bet1 = { + _id: id(), + userId: user1Id, + lucky: true, + roll: [3,4], + rollAt: new Date('2024-01-01T00:00:15Z'), + }; + + const bet2 = { + _id: id(), + userId: user2Id, + lucky: false, + roll: [5,2], + rollAt: new Date('2024-01-01T00:00:10Z'), + }; + + subscribe(user1Id.toString(), stream1); + subscribe(user2Id.toString(), stream2); + + onChange(createInsertEvent(bet2)); + onChange(createInsertEvent(bet1)); + + expect(stream1.writeJson).toHaveBeenCalledTimes(1); + expect(stream1.writeJson).toHaveBeenCalledWith({ + operationType: 'insert', + bet: { + id: bet1._id.toString(), + state: 'pending', + lucky: bet1.lucky, + rollAt: bet1.rollAt, + roll: undefined, + win: undefined, + }, + }); + + expect(stream2.writeJson).toHaveBeenCalledTimes(1); + expect(stream2.writeJson).toHaveBeenCalledWith({ + operationType: 'insert', + bet: { + id: bet2._id.toString(), + state: 'pending', + lucky: bet2.lucky, + rollAt: bet2.rollAt, + roll: undefined, + win: undefined, + }, + }); + }); +}); + +function createInsertEvent(fullDocument) { + return { + operationType: 'insert', + fullDocument, + }; +} + +function createUpdateEvent(_id, updatedFields) { + return { + operationType: 'update', + documentKey: { + _id, + }, + updateDescription: { + updatedFields, + }, + }; +} + +function createStream() { + return { + writeJson: jest.fn(), + onClose: () => {}, + }; +} + +function id(x) { + return new mongoose.Types.ObjectId(x); +} + +function setTime(dt) { + jest.useFakeTimers().setSystemTime(new Date(dt)); +} diff --git a/backend/tests/lucky7-bet-model.unit.test.js b/backend/tests/lucky7-bet-model.unit.test.js @@ -0,0 +1,118 @@ +import Lucky7Bet from '../src/models/lucky7-bet.js'; +import { jest } from '@jest/globals'; + +describe('Lucky7Bet Model', () => { + it('state pending', () => { + setTime('2024-01-01T00:00:00Z'); + + const bet = new Lucky7Bet({ + rollAt: new Date('2024-01-01T00:00:01Z'), + }); + + expect(bet.state).toBe('pending'); + }); + + it('state finished', () => { + setTime('2024-01-01T00:00:01Z'); + + const bet = new Lucky7Bet({ + rollAt: new Date('2024-01-01T00:00:01Z'), + }); + + expect(bet.state).toBe('finished'); + }); + + it('won lucky', () => { + const bet = new Lucky7Bet({ + roll: [3,4], + lucky: true, + }); + + expect(bet.win).toBe(true); + }); + + it('won unlucky', () => { + const bet = new Lucky7Bet({ + roll: [1,1], + lucky: false, + }); + + expect(bet.win).toBe(true); + }); + + it('lost lucky', () => { + const bet = new Lucky7Bet({ + roll: [1,1], + lucky: true, + }); + + expect(bet.win).toBe(false); + }); + + it('lost unlucky', () => { + const bet = new Lucky7Bet({ + roll: [3,4], + lucky: false, + }); + + expect(bet.win).toBe(false); + }); + + it('played() false', () => { + const bet = new Lucky7Bet(); + expect(bet.played()).toBe(false); + }); + + it('played() true', () => { + const bet = new Lucky7Bet(); + bet.play(); + expect(bet.played()).toBe(true); + }); + + it('toRedactedObject() pending', () => { + setTime('2024-01-01T00:00:00Z'); + + const bet = new Lucky7Bet(createProps({ + rollAt: new Date('2024-01-01T01:00:00Z'), + })); + const obj = bet.toRedactedObject(); + + expect(Object.keys(obj).length).toBe(6); + expect(obj.id).toBeDefined(); + expect(obj.rollAt).toBeDefined(); + expect(obj.lucky).toBeDefined(); + expect(obj.state).toBeDefined(); + expect(obj.roll).toBeUndefined(); + expect(obj.win).toBeUndefined(); + }); + + it('toRedactedObject() finished', () => { + setTime('2024-01-01T00:00:00Z'); + + const bet = new Lucky7Bet(createProps({ + rollAt: new Date('2024-01-01T00:00:00Z'), + })); + const obj = bet.toRedactedObject(); + + expect(Object.keys(obj)).toHaveLength(6); + expect(obj.id).toBeDefined(); + expect(obj.rollAt).toBeDefined(); + expect(obj.lucky).toBeDefined(); + expect(obj.state).toBeDefined(); + expect(obj.roll).toBeDefined(); + expect(obj.win).toBeDefined(); + }); +}); + +function createProps(props) { + return { + rollAt: new Date('2024-01-01T00:00:00Z'), + lucky: true, + roll: [3,4], + ...props, + }; +} + +function setTime(dt) { + jest.useFakeTimers().setSystemTime(new Date(dt)); +} diff --git a/backend/tests/lucky7-session-model.unit.test.js b/backend/tests/lucky7-session-model.unit.test.js @@ -0,0 +1,56 @@ +import Lucky7Session from '../src/models/lucky7-session.js'; +import { jest } from '@jest/globals'; + +describe('Lucky7Session Model', () => { + it('game intervals are set to 15 seconds', () => { + const session = new Lucky7Session(); + expect(session.gameIntervalInSeconds).toBe(15); + }); + + it('expiration set to one day', () => { + const date1 = new Date('2024-01-01T00:00:00Z'); + const date2 = new Date('2024-01-02T00:00:00Z'); + const session = new Lucky7Session({ + createdAt: date1, + }); + + const expiration = String(session.expiresAt); + expect(expiration).toBe(String(date2)); + }); + + it('nextIntervalAt()', () => { + setTime('2024-01-01T00:00:01Z'); + + const session = new Lucky7Session({ + createdAt: '2024-01-01T00:00:00Z', + }); + + const actual = session.nextIntervalAt().toISOString(); + expect(actual).toBe('2024-01-01T00:00:15.000Z'); + }); + + it('isPlayable() false', () => { + setTime('2024-01-01T00:00:10Z'); + + const session = new Lucky7Session({ + createdAt: '2024-01-01T00:00:00Z', + }); + + expect(session.isPlayable()).toBe(false); + }); + + it('isPlayable() true', () => { + setTime('2024-01-01T00:00:09.999Z'); + + const session = new Lucky7Session({ + createdAt: '2024-01-01T00:00:00Z', + }); + + expect(session.isPlayable()).toBe(true); + }); +}); + +function setTime(dt) { + jest.useFakeTimers().setSystemTime(new Date(dt)); +} + diff --git a/backend/tests/lucky7.int.test.js b/backend/tests/lucky7.int.test.js @@ -0,0 +1,126 @@ +import lucky7 from '../src/api/lucky7.js'; +import { changeStream } from '../src/api/lucky7-bets.js'; +import { jest } from '@jest/globals'; +import express from 'express'; +import bodyParser from "body-parser"; +import jwt from 'jsonwebtoken'; +import request from 'supertest'; + +describe('Lucky7', () => { + let app; + + beforeAll(() => { + app = express(); + app.use(bodyParser.json()); + app.use(lucky7); + }); + + describe('POST /sessions', () => { + let sessionId; + + const post = () => + auth(request(app).post('/sessions')); + + it('creates', async () => { + const res = await post(); + + expect(res.status).toBe(201); + expect(res.body).toHaveProperty('id'); + expect(res.body).toHaveProperty('createdAt'); + + sessionId = res.body.id; + }); + + it('fetches existing session', async () => { + const res = await post(); + + expect(res.status).toBe(201); + expect(res.body.id).toBe(sessionId); + }); + }); + + describe('POST /bets', () => { + let betId; + + const post = body => + auth(request(app).post('/bets').send(body)); + + beforeAll(() => { + jest.useFakeTimers({ doNotFake: ['nextTick'] }); + }); + + it('creates', async () => { + const res = await post({ lucky: true }); + + expect(res.status).toBe(201); + expect(res.body).toHaveProperty('id'); + expect(res.body).toHaveProperty('state'); + expect(res.body).toHaveProperty('rollAt'); + expect(res.body).toHaveProperty('lucky'); + expect(res.body).not.toHaveProperty('roll'); + expect(res.body).not.toHaveProperty('win'); + expect(res.body.lucky).toBe(true); + + betId = res.body.id; + }); + + it('updates existing bet', async () => { + const res = await post({ lucky: false }); + + expect(res.status).toBe(201); + expect(res.body.id).toBe(betId); + expect(res.body.lucky).toBe(false); + }); + + it('errors on invalid input', async () => { + const res = await post({}); + + expect(res.status).toBe(400); + expect(res.body.error.details[0].message).toBe( + '"lucky" is required', + ); + }); + + it('errors on unplayable session', async () => { + // advance 10 seconds + jest.advanceTimersByTime(10 * 1000); + + const res = await post({ lucky: true }); + + expect(res.status).toBe(409); + expect(res.body.code).toBe('SESSION_TIMER'); + }); + + it('errors on expiring session', async () => { + // advance almost 1 day + jest.advanceTimersByTime(86390 * 1000); + + const res = await post({ lucky: true }); + + expect(res.status).toBe(409); + expect(res.body.code).toBe('SESSION_EXPIRING'); + }); + + }); + + it('GET /leaderboard/streaks', async () => { + const res = await request(app).get('/leaderboard/streaks'); + + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + afterAll(async () => { + await changeStream.close(); + }); +}); + +function auth(request) { + const token = jwt.sign( + { _id: '6799219573e6c41d3aaece71' }, + 'test', + { expiresIn: '1h' } + ); + + return request.set('Authorization', `Bearer ${token}`); +} diff --git a/backend/tests/timer.unit.test.js b/backend/tests/timer.unit.test.js @@ -0,0 +1,83 @@ +import timer from '../src/utils/timer.js'; +import { jest } from '@jest/globals'; + +describe('Timer', () => { + it('nextIntervalAt before interval', () => { + setTime('2024-01-01T00:00:14.999Z'); + + const { nextIntervalAt } = timer({ + createdAt: new Date('2024-01-01T00:00:00Z'), + expiresAt: new Date('2024-01-01T01:00:00Z'), + gameIntervalInSeconds: 15, + }); + + const actual = nextIntervalAt.toISOString(); + expect(actual).toBe('2024-01-01T00:00:15.000Z'); + }); + + it('nextIntervalAt at interval', () => { + setTime('2024-01-01T00:00:15.000Z'); + + const { nextIntervalAt } = timer({ + createdAt: new Date('2024-01-01T00:00:00Z'), + expiresAt: new Date('2024-01-01T01:00:00Z'), + gameIntervalInSeconds: 15, + }); + + const actual = nextIntervalAt.toISOString(); + expect(actual).toBe('2024-01-01T00:00:15.000Z'); + }); + + it('nextIntervalAt after interval', () => { + setTime('2024-01-01T00:00:15.001Z'); + + const { nextIntervalAt } = timer({ + createdAt: new Date('2024-01-01T00:00:00Z'), + expiresAt: new Date('2024-01-01T01:00:00Z'), + gameIntervalInSeconds: 15, + }); + + const actual = nextIntervalAt.toISOString(); + expect(actual).toBe('2024-01-01T00:00:30.000Z'); + }); + + it('nextIntervalInMillis', () => { + setTime('2024-01-01T00:00:14.997Z'); + + const { nextIntervalInMillis } = timer({ + createdAt: new Date('2024-01-01T00:00:00Z'), + expiresAt: new Date('2024-01-01T01:00:00Z'), + gameIntervalInSeconds: 15, + }); + + expect(nextIntervalInMillis).toBe(3); + }); + + it('non-expiring session', () => { + setTime('2024-01-01T00:59:45.000Z'); + + const { nextIntervalAt } = timer({ + createdAt: new Date('2024-01-01T00:00:00Z'), + expiresAt: new Date('2024-01-01T01:00:00Z'), + gameIntervalInSeconds: 15, + }); + + expect(nextIntervalAt).toBeDefined(); + }); + + it('expiring session', () => { + setTime('2024-01-01T00:59:45.001Z'); + + const { nextIntervalAt } = timer({ + createdAt: new Date('2024-01-01T00:00:00Z'), + expiresAt: new Date('2024-01-01T01:00:00Z'), + gameIntervalInSeconds: 15, + }); + + expect(nextIntervalAt).toBeUndefined(); + }); +}); + +function setTime(dt) { + jest.useFakeTimers().setSystemTime(new Date(dt)); +}