From e4784fd533e0fb3f18b6fc341a5218935ee0d839 Mon Sep 17 00:00:00 2001 From: Jason LaPier Date: Mon, 14 Aug 2023 07:52:55 -0700 Subject: [PATCH] CUTYPE and X-NUM-GUESTS support (#244) * Add participation type for CUtype fields + support for x-num-guests x-num-guests is a string for now to prevent its unnecessary inclusion * Add xnum guests + cutype to schema * Add additional fields to formatted attendee + format so that semicolon always ends first part and mailto always ends * restrict sequence and make x-num-guests a number * RSVP is not a required field * typeof not needed for undefined check --------- Co-authored-by: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> --- index.d.ts | 15 +++++++++--- src/schema/index.js | 6 +++-- src/utils/set-contact.js | 33 ++++++++++++++++++------- test/pipeline/format.spec.js | 2 +- test/pipeline/validate.spec.js | 9 +++++++ test/utils/set-contact.spec.js | 44 +++++++++++++++++++++------------- 6 files changed, 78 insertions(+), 31 deletions(-) diff --git a/index.d.ts b/index.d.ts index 809a995..572ab67 100644 --- a/index.d.ts +++ b/index.d.ts @@ -34,6 +34,13 @@ export type ParticipationRole = | 'OPT-PARTICIPANT' | 'NON-PARTICIPANT'; +export type ParticipationType = + | 'INDIVIDUAL' + | 'GROUP' + | 'RESOURCE' + | 'ROOM' + | 'UNKNOWN'; + export type Person = { name?: string; email?: string; @@ -44,6 +51,8 @@ export type Attendee = Person & { rsvp?: boolean; partstat?: ParticipationStatus; role?: ParticipationRole; + cutype?: ParticipationType; + xNumGuests?: number; }; export type ActionType = 'audio' | 'display' | 'email' | 'procedure'; @@ -81,15 +90,15 @@ export type EventAttributes = { url?: string; status?: EventStatus; busyStatus?: 'FREE' | 'BUSY' | 'TENTATIVE' | 'OOF'; - + organizer?: Person & { sentBy?: string; }; attendees?: Attendee[]; - + categories?: string[]; alarms?: Alarm[]; - + productId?: string; uid?: string; method?: string; diff --git a/src/schema/index.js b/src/schema/index.js index eb35227..52f26a1 100644 --- a/src/schema/index.js +++ b/src/schema/index.js @@ -34,7 +34,9 @@ const contactSchema = yup.object().shape({ rsvp: yup.boolean(), dir: yup.string().matches(urlRegex), partstat: yup.string(), - role: yup.string() + role: yup.string(), + cutype: yup.string(), + xNumGuests: yup.number() }).noUnknown() const organizerSchema = yup.object().shape({ @@ -65,7 +67,7 @@ const schema = yup.object().shape({ productId: yup.string(), method: yup.string(), uid: yup.string().required(), - sequence: yup.number(), + sequence: yup.number().integer().max(2_147_483_647), start: dateTimeSchema.required(), duration: durationSchema, startType: yup.string().matches(/utc|local/), diff --git a/src/utils/set-contact.js b/src/utils/set-contact.js index 07d8073..cee2e09 100644 --- a/src/utils/set-contact.js +++ b/src/utils/set-contact.js @@ -1,12 +1,27 @@ -export default function setContact({ name, email, rsvp, dir, partstat, role }) { - let formattedAttendee = '' - formattedAttendee += rsvp ? 'RSVP=TRUE;' : 'RSVP=FALSE;' - formattedAttendee += role ? `ROLE=${role};` : '' - formattedAttendee += partstat ? `PARTSTAT=${partstat};` : '' - formattedAttendee += dir ? `DIR=${dir};` : '' - formattedAttendee += 'CN=' - formattedAttendee += name || 'Unnamed attendee' - formattedAttendee += email ? `:mailto:${email}` : '' +export default function setContact({ name, email, rsvp, dir, partstat, role, cutype, xNumGuests }) { + let formattedParts = []; + + if(rsvp !== undefined){ + formattedParts.push(rsvp ? 'RSVP=TRUE' : 'RSVP=FALSE'); + } + if(cutype){ + formattedParts.push("CUTYPE=".concat(cutype)); + } + if(xNumGuests !== undefined){ + formattedParts.push(`X-NUM-GUESTS=${xNumGuests}`); + } + if(role){ + formattedParts.push("ROLE=".concat(role)); + } + if(partstat){ + formattedParts.push("PARTSTAT=".concat(partstat)); + } + if(dir){ + formattedParts.push("DIR=".concat(dir)); + } + formattedParts.push('CN='.concat((name || 'Unnamed attendee'))); + + var formattedAttendee = formattedParts.join(';').concat(email ? ":mailto:".concat(email) : ''); return formattedAttendee } diff --git a/test/pipeline/format.spec.js b/test/pipeline/format.spec.js index 7aebc41..d8cc7ae 100644 --- a/test/pipeline/format.spec.js +++ b/test/pipeline/format.spec.js @@ -175,7 +175,7 @@ describe('pipeline.formatEvent', () => { {name: 'Brittany Seaton', email: 'brittany@example.com', rsvp: true } ]}) const formattedEvent = formatEvent(event) - expect(formattedEvent).to.contain('ATTENDEE;RSVP=FALSE;CN=Adam Gibbons:mailto:adam@example.com') + expect(formattedEvent).to.contain('ATTENDEE;CN=Adam Gibbons:mailto:adam@example.com') expect(formattedEvent).to.contain('ATTENDEE;RSVP=TRUE;CN=Brittany Seaton:mailto:brittany@example.com') }) it('writes a busystatus', () => { diff --git a/test/pipeline/validate.spec.js b/test/pipeline/validate.spec.js index 9085687..6820428 100644 --- a/test/pipeline/validate.spec.js +++ b/test/pipeline/validate.spec.js @@ -11,6 +11,15 @@ describe('pipeline.validate', () => { expect(error).not.to.exist expect(value.uid).to.equal('1') }) + it('returns an error if the sequence number is too long', () => { + const { error, value } = validateEvent({ + uid: '1', + start: [1997, 10, 1, 22, 30], + duration: { hours: 1 }, + sequence: 3_456_789_123, // bigger than 2,147,483,647 + }) + expect(error).to.exist + }) it('returns undefined when passed no event', () => { const { error, value } = validateEvent() expect(value).to.be.undefined diff --git a/test/utils/set-contact.spec.js b/test/utils/set-contact.spec.js index ca99a9a..3e9fb8d 100644 --- a/test/utils/set-contact.spec.js +++ b/test/utils/set-contact.spec.js @@ -4,20 +4,24 @@ import { setContact } from '../../src/utils' describe('utils.setContact', () => { it('set a contact with role', () => { const contact = { name: 'm-vinc', email: 'vinc@example.com' } + expect(setContact(contact)) + .to.equal(`CN=m-vinc:mailto:vinc@example.com`) + const contactChair = Object.assign({role: 'CHAIR'}, contact) - const contactRequired = Object.assign({role: 'REQ-PARTICIPANT' }, contact) - const contactOptional = Object.assign({role: 'OPT-PARTICIPANT' }, contact) - const contactNon = Object.assign({role: 'NON-PARTICIPANT' }, contact) expect(setContact(contactChair)) - .to.equal(`RSVP=FALSE;ROLE=CHAIR;CN=m-vinc:mailto:vinc@example.com`) + .to.equal(`ROLE=CHAIR;CN=m-vinc:mailto:vinc@example.com`) + + const contactRequired = Object.assign({role: 'REQ-PARTICIPANT', rsvp: true }, contact) expect(setContact(contactRequired)) - .to.equal(`RSVP=FALSE;ROLE=REQ-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`) + .to.equal(`RSVP=TRUE;ROLE=REQ-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`) + + const contactOptional = Object.assign({role: 'OPT-PARTICIPANT', rsvp: false }, contact) expect(setContact(contactOptional)) .to.equal(`RSVP=FALSE;ROLE=OPT-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`) + + const contactNon = Object.assign({role: 'NON-PARTICIPANT' }, contact) expect(setContact(contactNon)) - .to.equal(`RSVP=FALSE;ROLE=NON-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`) - expect(setContact(contact)) - .to.equal(`RSVP=FALSE;CN=m-vinc:mailto:vinc@example.com`) + .to.equal(`ROLE=NON-PARTICIPANT;CN=m-vinc:mailto:vinc@example.com`) }) it('set a contact with partstat', () => { const contact = { name: 'm-vinc', email: 'vinc@example.com' } @@ -28,21 +32,21 @@ describe('utils.setContact', () => { const contactTentative = Object.assign({contact, partstat: 'TENTATIVE'}, contact) expect(setContact(contactUndefined)) - .to.equal('RSVP=FALSE;CN=m-vinc:mailto:vinc@example.com') + .to.equal('CN=m-vinc:mailto:vinc@example.com') expect(setContact(contactNeedsAction)) - .to.equal('RSVP=FALSE;PARTSTAT=NEEDS-ACTION;CN=m-vinc:mailto:vinc@example.com') + .to.equal('PARTSTAT=NEEDS-ACTION;CN=m-vinc:mailto:vinc@example.com') expect(setContact(contactDeclined)) - .to.equal('RSVP=FALSE;PARTSTAT=DECLINED;CN=m-vinc:mailto:vinc@example.com') + .to.equal('PARTSTAT=DECLINED;CN=m-vinc:mailto:vinc@example.com') expect(setContact(contactTentative)) - .to.equal('RSVP=FALSE;PARTSTAT=TENTATIVE;CN=m-vinc:mailto:vinc@example.com') - + .to.equal('PARTSTAT=TENTATIVE;CN=m-vinc:mailto:vinc@example.com') + expect(setContact(contactAccepted)) - .to.equal('RSVP=FALSE;PARTSTAT=ACCEPTED;CN=m-vinc:mailto:vinc@example.com') + .to.equal('PARTSTAT=ACCEPTED;CN=m-vinc:mailto:vinc@example.com') }) - it('sets a contact and defaults RSVP to false', () => { + it('sets a contact and only sets RSVP if specified', () => { const contact1 = { name: 'Adam Gibbons', email: 'adam@example.com' @@ -56,9 +60,17 @@ describe('utils.setContact', () => { } expect(setContact(contact1)) - .to.equal('RSVP=FALSE;CN=Adam Gibbons:mailto:adam@example.com') + .to.equal('CN=Adam Gibbons:mailto:adam@example.com') expect(setContact(contact2)) .to.equal('RSVP=TRUE;DIR=https://example.com/contacts/adam;CN=Adam Gibbons:mailto:adam@example.com') }) + it('set a contact with cutype and guests', () => { + const contact = { name: 'm-vinc', email: 'vinc@example.com' } + const contactCuGuests = Object.assign({ cutype: 'INDIVIDUAL', xNumGuests: 0 }, contact) + const contactString = setContact(contactCuGuests) + + expect(contactString).to.contain('CUTYPE=INDIVIDUAL') + expect(contactString).to.contain('X-NUM-GUESTS=0') + }) })