diff --git a/.env.example b/.env.example index 60d4415..a1b7e5c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -MONGO_DB_URI=mongodb+srv://username:password@orgname-zmpox.mongodb.net/databasename -TEST_DB=mongodb+srv://username:password@orgname-zmpox.mongodb.net/databasename +MONGO_DB_URI=mongodb+srv://username:password@orgname-xxxxx.mongodb.net/databasename +TEST_DB=mongodb+srv://username:password@orgname-xxxxx.mongodb.net/databasename GoogleClientSecret= HashString= AllowUrl={"urls": ["http://localhost:9000", "https://localhost:9000", "http://localhost:7000", "https://localhost:7000"]} diff --git a/package-lock.json b/package-lock.json index 57fe906..51172bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webjamsocketserver", - "version": "3.0.8", + "version": "3.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webjamsocketserver", - "version": "3.0.8", + "version": "3.0.10", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1802,9 +1802,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" diff --git a/package.json b/package.json index fcfbff3..8c83c4d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "webjamsocketserver", "description": "Uses latest version of socketcluster-server", - "version": "3.0.8", + "version": "3.0.10", "license": "MIT", "type": "module", "main": "build/src/index.js", diff --git a/src/AgController/index.ts b/src/AgController/index.ts index 87ebdf0..690d893 100644 --- a/src/AgController/index.ts +++ b/src/AgController/index.ts @@ -272,13 +272,12 @@ class AgController { if (gig && gig.datetime && gig.city && gig.usState && gig.venue) { await utils.handleGig('createDocs', gig, 'gigCreated', this.gigController, this.server); } else throw new Error('Invalid create gig data'); - if (receiver.done) break; } catch (e) { const eMessage = (e as Error).message; debug(eMessage); client.socket.transmit('socketError', { newGig: eMessage });// send error back to client - break; } + /* istanbul ignore else */if (receiver.done) break; } })(); } diff --git a/src/model/gig/gig-facade.ts b/src/model/gig/gig-facade.ts index 6799728..914e6ca 100644 --- a/src/model/gig/gig-facade.ts +++ b/src/model/gig/gig-facade.ts @@ -1,8 +1,28 @@ import Model from '../../lib/facade.js'; import gigSchema from './gig-schema.js'; +const venuePopulateFields = 'name address city usState website'; + class GigModel extends Model { + async find(query: any): Promise { + let result; + try { + result = await this.Schema.find(query).populate('venueId', venuePopulateFields).lean().exec(); + } catch (e) { + return Promise.reject(e); + } + return Promise.resolve(result); + } + async findSort(query: any, sort: any): Promise { + let result; + try { + result = await this.Schema.find(query).sort(sort).populate('venueId', venuePopulateFields).lean().exec(); + } catch (e) { + return Promise.reject(e); + } + return Promise.resolve(result); + } } export default new GigModel(gigSchema); diff --git a/src/model/gig/gig-schema.ts b/src/model/gig/gig-schema.ts index 8925e71..e60539a 100644 --- a/src/model/gig/gig-schema.ts +++ b/src/model/gig/gig-schema.ts @@ -22,7 +22,12 @@ const gigSchema = new Schema({ // all pre-#237 records, which read as the default (josh) artist. Kept in // sync with web-jam-back's mirror of this schema. artist: { type: String, required: false }, + venueId: { + type: Schema.Types.ObjectId, ref: 'Venue', required: false, + }, }, options); +import '../venue/venue-schema.js'; + // Explicit collection name 'gigs' (a tours -> gigs migration moves the data). export default mongoose.models.Gig || mongoose.model('Gig', gigSchema, 'gigs'); diff --git a/src/model/venue/venue-schema.ts b/src/model/venue/venue-schema.ts new file mode 100644 index 0000000..32d6605 --- /dev/null +++ b/src/model/venue/venue-schema.ts @@ -0,0 +1,13 @@ +import mongoose from '../db.js'; + +const { Schema } = mongoose; + +const venueSchema = new Schema({ + name: { type: String, required: true, trim: true }, + address: { type: String, required: false, trim: true }, + city: { type: String, required: false, trim: true }, + usState: { type: String, required: false, trim: true }, + website: { type: String, required: false, trim: true }, +}); + +export default mongoose.models.Venue || mongoose.model('Venue', venueSchema, 'venues'); diff --git a/test/AgController/index.spec.ts b/test/AgController/index.spec.ts index f6a7b0d..33c4510 100644 --- a/test/AgController/index.spec.ts +++ b/test/AgController/index.spec.ts @@ -30,8 +30,19 @@ const aStub:any = { }), }; +const realHandleGig = utils.handleGig; +const realRemoveGig = utils.removeGig; + describe('AgControler', () => { - afterEach(() => { vi.unstubAllGlobals(); }); + afterEach(() => { + vi.unstubAllGlobals(); + // Several tests replace these shared utils exports with vi.fn() stubs + // (e.g. line ~483) without restoring them, which otherwise leaks into + // later tests (like the #246 regression test) that need the real + // implementation and silently breaks their assertions. + utils.handleGig = realHandleGig; + utils.removeGig = realRemoveGig; + }); let r, clientStub:any = { id: '123', listener: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ done: true, value: '1000' }) }) }), @@ -500,6 +511,49 @@ describe('AgControler', () => { agController.newGig(cStub, 'newGig'); expect(utils.handleGig).not.toHaveBeenCalled(); }); + it('keeps handling newGig on the same socket after an earlier newGig errored (#246)', async () => { + const agController = new AgController(aStub); + agController.clients = ['123']; + agController.gigController.createDocs = vi.fn(() => Promise.resolve([])); + agController.verifyAdminWrite = vi.fn(() => Promise.resolve()); + const transmit = vi.fn(); + let call = 0; + const cStub:any = { + socket: { + id: '123', + listener: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ done: true, value: '1000' }) }) }), + transmit, + receiver: () => ({ + createConsumer: () => ({ + next: () => { + call += 1; + // First newGig on this socket: invalid gig data -> throws, must NOT break the loop. + if (call === 1) { + return Promise.resolve({ + value: { token: 'token', gig: { venue: 'venue' } }, + done: false, + }); + } + // Second newGig on the SAME socket/consumer: valid data -> must still be handled. + return Promise.resolve({ + value: { + token: 'token', + gig: { + venue: 'venue', datetime: new Date(), city: 'city', usState: 'state', + }, + }, + done: true, + }); + }, + }), + }), + }, + }; + agController.newGig(cStub, 'newGig'); + await delay(1000); + expect(transmit).toHaveBeenCalledWith('socketError', { newGig: 'Invalid create gig data' }); + expect(agController.gigController.createDocs).toHaveBeenCalled(); + }); it('process the newImage message from client', async () => { const agController = new AgController(aStub); agController.clients = ['123']; diff --git a/test/gig/gig-facade.test.ts b/test/gig/gig-facade.test.ts new file mode 100644 index 0000000..ad5dbee --- /dev/null +++ b/test/gig/gig-facade.test.ts @@ -0,0 +1,47 @@ +import gigFacade from '../../src/model/gig/gig-facade.js'; + +describe('GigModel', () => { + const populatedVenue = { + name: 'The Venue', address: '123 Main St', city: 'Salem', usState: 'OR', website: 'https://venue.example', + }; + + describe('find', () => { + it('returns a gig with venueId populated as a subdocument carrying name, city, usState, website, and address', async () => { + const gig = { _id: 'gig1', venueId: populatedVenue }; + (gigFacade as any).Schema = { + find: () => ({ populate: () => ({ lean: () => ({ exec: () => Promise.resolve([gig]) }) }) }), + }; + const result = await gigFacade.find({}); + expect(result).toEqual([gig]); + expect(result[0].venueId).toEqual(populatedVenue); + expect(result[0].venueId.address).toBe('123 Main St'); + }); + + it('propagates a rejection', async () => { + (gigFacade as any).Schema = { + find: () => ({ populate: () => ({ lean: () => ({ exec: () => Promise.reject(new Error('bad')) }) }) }), + }; + await expect(gigFacade.find({})).rejects.toThrow('bad'); + }); + }); + + describe('findSort', () => { + it('returns a gig with venueId populated as a subdocument carrying name, city, usState, website, and address', async () => { + const gig = { _id: 'gig1', venueId: populatedVenue }; + (gigFacade as any).Schema = { + find: () => ({ sort: () => ({ populate: () => ({ lean: () => ({ exec: () => Promise.resolve([gig]) }) }) }) }), + }; + const result = await gigFacade.findSort({}, {}); + expect(result).toEqual([gig]); + expect(result[0].venueId).toEqual(populatedVenue); + expect(result[0].venueId.address).toBe('123 Main St'); + }); + + it('propagates a rejection', async () => { + (gigFacade as any).Schema = { + find: () => ({ sort: () => ({ populate: () => ({ lean: () => ({ exec: () => Promise.reject(new Error('bad')) }) }) }) }), + }; + await expect(gigFacade.findSort({}, {})).rejects.toThrow('bad'); + }); + }); +});