diff --git a/package.json b/package.json index 6db0440..d3fdb9e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "webjamsocketserver", "description": "Uses latest version of socketcluster-server", - "version": "3.0.11", + "version": "3.0.12", "license": "MIT", "type": "module", "main": "build/src/index.js", diff --git a/src/AgController/index.ts b/src/AgController/index.ts index 05a1bea..70d65e8 100644 --- a/src/AgController/index.ts +++ b/src/AgController/index.ts @@ -269,7 +269,11 @@ class AgController { try { const gig = receiver.value.gig ?? receiver.value.tour; await this.verifyAdminWrite(receiver.value.token); - if (gig && gig.datetime && gig.city && gig.usState && gig.venue) { + // A gig is identified by venueId (linked to a Venue doc) OR a non-empty + // free-text venue (one-off gig) — #256. city/usState are legacy + // display-only fields resolved from the linked venue and are no + // longer required here. + if (gig && gig.datetime && (gig.venueId || gig.venue)) { await utils.handleGig('createDocs', gig, 'gigCreated', this.gigController, this.server); } else throw new Error('Invalid create gig data'); } catch (e) { @@ -305,7 +309,10 @@ class AgController { try { const id = data.gigId ?? data.tourId; const gig = data.gig ?? data.tour ?? {}; - if (!gig.venue || !gig.datetime || !gig.city || !gig.usState) throw new Error('Invalid gig data'); + // Same rule as newGig (#256): a gig is identified by venueId OR a + // non-empty free-text venue; city/usState are legacy display-only + // fields resolved from the linked venue and are no longer required. + if (!gig.datetime || !(gig.venueId || gig.venue)) throw new Error('Invalid gig data'); r = await this.gigController.findByIdAndUpdate(id, gig); } catch (e) { // Rethrow (#253, same pattern as handleImage/JaMmusic#1199): swallowing diff --git a/src/model/gig/gig-schema.ts b/src/model/gig/gig-schema.ts index e60539a..9d58ad2 100644 --- a/src/model/gig/gig-schema.ts +++ b/src/model/gig/gig-schema.ts @@ -13,7 +13,10 @@ const gigSchema = new Schema({ location: { type: String, required: false }, city: { type: String, required: false }, usState: { type: String, required: false }, - venue: { type: String, required: true }, + // A gig is identified by venueId (linked) OR free-text venue (one-off); + // neither is mandatory at the schema layer — AgController's newGig/updateGig + // guards are the gate (#256). + venue: { type: String, required: false }, tickets: { type: String, required: false }, duration: { type: Number, required: false, default: 0 }, promoImageUrl: { type: String, required: false }, diff --git a/test/AgController/index.spec.ts b/test/AgController/index.spec.ts index 7da1385..db9e8b3 100644 --- a/test/AgController/index.spec.ts +++ b/test/AgController/index.spec.ts @@ -280,6 +280,40 @@ describe('AgControler', () => { gig: {}, })).rejects.toThrow('Invalid gig data'); }); + it('updates a gig when venueId is set and venue/city/usState are empty strings (#256)', async () => { + const agController = new AgController(aStub); + agController.gigController.findByIdAndUpdate = vi.fn(() => Promise.resolve(true)); + r = await agController.updateGig({ + gigId: testId, + gig: { + venueId: testId, datetime: new Date(), venue: '', city: '', usState: '', + }, + }); + expect(r).toBe('Gig updated'); + }); + it('updates a one-off gig with only free-text venue set and no venueId (#256)', async () => { + const agController = new AgController(aStub); + agController.gigController.findByIdAndUpdate = vi.fn(() => Promise.resolve(true)); + r = await agController.updateGig({ + gigId: testId, + gig: { venue: 'The Local Bar', datetime: new Date() }, + }); + expect(r).toBe('Gig updated'); + }); + it('rejects updateGig when neither venueId nor venue is set (#256)', async () => { + const agController = new AgController(aStub); + await expect(agController.updateGig({ + gigId: testId, + gig: { datetime: new Date() }, + })).rejects.toThrow('Invalid gig data'); + }); + it('rejects updateGig when datetime is missing even though venueId is set (#256)', async () => { + const agController = new AgController(aStub); + await expect(agController.updateGig({ + gigId: testId, + gig: { venueId: testId }, + })).rejects.toThrow('Invalid gig data'); + }); it('does not process the newTour message from client when token is not valid', async () => { const agController = new AgController(aStub); agController.clients = ['123']; @@ -494,6 +528,124 @@ describe('AgControler', () => { { newGig: 'Invalid create gig data' }, ); }); + it('creates a gig when venueId is set and venue/city/usState are empty strings (#256)', async () => { + const agController = new AgController(aStub); + agController.clients = ['123']; + agController.gigController.createDocs = vi.fn(() => Promise.resolve([])); + agController.verifyAdminWrite = vi.fn(() => Promise.resolve()); + const cStub:any = { + socket: { + id: '123', + listener: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ done: true, value: '1000' }) }) }), + transmit: () => { }, + receiver: () => ({ + createConsumer: () => ({ + next: () => Promise.resolve({ + value: { + token: 'token', + gig: { + venueId: testId, datetime: new Date(), venue: '', city: '', usState: '', + }, + }, + done: true, + }), + }), + }), + }, + }; + const setIntervalMock:any = vi.fn((cb:any) => cb()); + global.setInterval = setIntervalMock; + agController.newGig(cStub, 'newGig'); + await delay(1000); + expect(agController.gigController.createDocs).toHaveBeenCalled(); + }); + it('creates a one-off gig with only free-text venue set and no venueId (#256)', async () => { + const agController = new AgController(aStub); + agController.clients = ['123']; + agController.gigController.createDocs = vi.fn(() => Promise.resolve([])); + agController.verifyAdminWrite = vi.fn(() => Promise.resolve()); + const cStub:any = { + socket: { + id: '123', + listener: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ done: true, value: '1000' }) }) }), + transmit: () => { }, + receiver: () => ({ + createConsumer: () => ({ + next: () => Promise.resolve({ + value: { + token: 'token', + gig: { venue: 'The Local Bar', datetime: new Date() }, + }, + done: true, + }), + }), + }), + }, + }; + const setIntervalMock:any = vi.fn((cb:any) => cb()); + global.setInterval = setIntervalMock; + agController.newGig(cStub, 'newGig'); + await delay(1000); + expect(agController.gigController.createDocs).toHaveBeenCalled(); + }); + it('rejects newGig with neither venueId nor venue (#256)', async () => { + const agController = new AgController(aStub); + agController.clients = ['123']; + agController.gigController.createDocs = vi.fn(() => Promise.resolve([])); + agController.verifyAdminWrite = vi.fn(() => Promise.resolve()); + const eStub:any = { + socket: { + id: '123', + listener: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ done: true, value: '1000' }) }) }), + transmit: vi.fn(), + receiver: () => ({ + createConsumer: () => ({ + next: () => Promise.resolve({ + value: { + token: 'token', + gig: { datetime: new Date() }, + }, + done: true, + }), + }), + }), + }, + }; + const setIntervalMock:any = vi.fn((cb:any) => cb()); + global.setInterval = setIntervalMock; + agController.newGig(eStub, 'newGig'); + await delay(1000); + expect(eStub.socket.transmit).toHaveBeenCalledWith('socketError', { newGig: 'Invalid create gig data' }); + }); + it('rejects newGig when datetime is missing even though venueId is set (#256)', async () => { + const agController = new AgController(aStub); + agController.clients = ['123']; + agController.gigController.createDocs = vi.fn(() => Promise.resolve([])); + agController.verifyAdminWrite = vi.fn(() => Promise.resolve()); + const eStub:any = { + socket: { + id: '123', + listener: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ done: true, value: '1000' }) }) }), + transmit: vi.fn(), + receiver: () => ({ + createConsumer: () => ({ + next: () => Promise.resolve({ + value: { + token: 'token', + gig: { venueId: testId }, + }, + done: true, + }), + }), + }), + }, + }; + const setIntervalMock:any = vi.fn((cb:any) => cb()); + global.setInterval = setIntervalMock; + agController.newGig(eStub, 'newGig'); + await delay(1000); + expect(eStub.socket.transmit).toHaveBeenCalledWith('socketError', { newGig: 'Invalid create gig data' }); + }); it('handles missing receiver value when process the newTour message from client', () => { const agController = new AgController(aStub); agController.clients = ['123'];