From 0ed41eb5fd0e4192e1b7dc374f819d17aef3e805 Mon Sep 17 00:00:00 2001
From: George Joseph <gtjoseph@users.noreply.github.com>
Date: Tue, 21 Dec 2021 19:32:22 -0700
Subject: [PATCH] sip_inv:  Additional multipart support (#2919) (#2920)

---
 pjsip/include/pjsip-ua/sip_inv.h       | 108 ++++++++++-
 pjsip/src/pjsip-ua/sip_inv.c           | 240 ++++++++++++++++++++-----
 pjsip/src/test/inv_offer_answer_test.c | 103 ++++++++++-
 3 files changed, 394 insertions(+), 57 deletions(-)

--- a/pjsip/include/pjsip-ua/sip_inv.h
+++ b/pjsip/include/pjsip-ua/sip_inv.h
@@ -451,11 +451,11 @@ struct pjsip_inv_session
 
 
 /**
- * This structure represents SDP information in a pjsip_rx_data. Application
- * retrieve this information by calling #pjsip_rdata_get_sdp_info(). This
+ * This structure represents SDP information in a pjsip_(rx|tx)_data. Application
+ * retrieve this information by calling #pjsip_get_sdp_info(). This
  * mechanism supports multipart message body.
  */
-typedef struct pjsip_rdata_sdp_info
+typedef struct pjsip_sdp_info
 {
     /**
      * Pointer and length of the text body in the incoming message. If
@@ -475,7 +475,15 @@ typedef struct pjsip_rdata_sdp_info
      */
     pjmedia_sdp_session *sdp;
 
-} pjsip_rdata_sdp_info;
+} pjsip_sdp_info;
+
+/**
+ * For backwards compatibility and completeness,
+ * pjsip_rdata_sdp_info and pjsip_tdata_sdp_info
+ * are typedef'd to pjsip_sdp_info.
+ */
+typedef pjsip_sdp_info pjsip_rdata_sdp_info;
+typedef pjsip_sdp_info pjsip_tdata_sdp_info;
 
 
 /**
@@ -1046,6 +1054,44 @@ PJ_DECL(pj_status_t) pjsip_create_sdp_bo
 					   pjsip_msg_body **p_body);
 
 /**
+ * This is a utility function to create a multipart body with the
+ * SIP body as the first part.
+ *
+ * @param pool		Pool to allocate memory.
+ * @param sdp		SDP session to be put in the SIP message body.
+ * @param p_body	Pointer to receive SIP message body containing
+ *			the SDP session.
+ *
+ * @return		PJ_SUCCESS on success.
+ */
+PJ_DECL(pj_status_t) pjsip_create_multipart_sdp_body( pj_pool_t *pool,
+                                           pjmedia_sdp_session *sdp,
+                                           pjsip_msg_body **p_body);
+
+/**
+ * Retrieve SDP information from a message body. Application should
+ * prefer to use this function rather than parsing the SDP manually since
+ * this function supports multipart message body.
+ *
+ * This function will only parse the SDP once, the first time it is called
+ * on the same message. Subsequent call on the same message will just pick
+ * up the already parsed SDP from the message.
+ *
+ * @param pool               Pool to allocate memory.
+ * @param body               The message body.
+ * @param msg_media_type     From the rdata or tdata Content-Type header, if available.
+ *                           If NULL, the content_type from the body will be used.
+ * @param search_media_type  The media type to search for.
+ *                           If NULL, "application/sdp" will be used.
+ *
+ * @return                   The SDP info.
+ */
+PJ_DECL(pjsip_sdp_info*) pjsip_get_sdp_info(pj_pool_t *pool,
+					   pjsip_msg_body *body,
+					   pjsip_media_type *msg_media_type,
+					   const pjsip_media_type *search_media_type);
+
+/**
  * Retrieve SDP information from an incoming message. Application should
  * prefer to use this function rather than parsing the SDP manually since
  * this function supports multipart message body.
@@ -1061,6 +1107,60 @@ PJ_DECL(pj_status_t) pjsip_create_sdp_bo
 PJ_DECL(pjsip_rdata_sdp_info*) pjsip_rdata_get_sdp_info(pjsip_rx_data *rdata);
 
 
+/**
+ * Retrieve SDP information from an incoming message. Application should
+ * prefer to use this function rather than parsing the SDP manually since
+ * this function supports multipart message body.
+ *
+ * This function will only parse the SDP once, the first time it is called
+ * on the same message. Subsequent call on the same message will just pick
+ * up the already parsed SDP from the message.
+ *
+ * @param rdata               The incoming message.
+ * @param search_media_type   The SDP media type to search for.
+ *                            If NULL, "application/sdp" will be used.
+ *
+ * @return                    The SDP info.
+ */
+PJ_DECL(pjsip_rdata_sdp_info*) pjsip_rdata_get_sdp_info2(
+					    pjsip_rx_data *rdata,
+					    const pjsip_media_type *search_media_type);
+
+/**
+ * Retrieve SDP information from an outgoing message. Application should
+ * prefer to use this function rather than parsing the SDP manually since
+ * this function supports multipart message body.
+ *
+ * This function will only parse the SDP once, the first time it is called
+ * on the same message. Subsequent call on the same message will just pick
+ * up the already parsed SDP from the message.
+ *
+ * @param tdata    The outgoing message.
+ *
+ * @return         The SDP info.
+ */
+PJ_DECL(pjsip_tdata_sdp_info*) pjsip_tdata_get_sdp_info(pjsip_tx_data *tdata);
+
+/**
+ * Retrieve SDP information from an outgoing message. Application should
+ * prefer to use this function rather than parsing the SDP manually since
+ * this function supports multipart message body.
+ *
+ * This function will only parse the SDP once, the first time it is called
+ * on the same message. Subsequent call on the same message will just pick
+ * up the already parsed SDP from the message.
+ *
+ * @param tdata               The outgoing message.
+ * @param search_media_type   The SDP media type to search for.
+ *                            If NULL, "application/sdp" will be used.
+ *
+ * @return                    The SDP info.
+ */
+PJ_DECL(pjsip_tdata_sdp_info*) pjsip_tdata_get_sdp_info2(
+					    pjsip_tx_data *tdata,
+					    const pjsip_media_type *search_media_type);
+
+
 PJ_END_DECL
 
 /**
--- a/pjsip/src/pjsip-ua/sip_inv.c
+++ b/pjsip/src/pjsip-ua/sip_inv.c
@@ -118,6 +118,8 @@ static pj_status_t handle_timer_response
 static pj_bool_t inv_check_secure_dlg(pjsip_inv_session *inv,
 				      pjsip_event *e);
 
+static int print_sdp(pjsip_msg_body *body, char *buf, pj_size_t len);
+
 static void (*inv_state_handler[])( pjsip_inv_session *inv, pjsip_event *e) = 
 {
     &inv_on_state_null,
@@ -956,66 +958,170 @@ PJ_DEF(pj_status_t) pjsip_inv_create_uac
     return PJ_SUCCESS;
 }
 
-PJ_DEF(pjsip_rdata_sdp_info*) pjsip_rdata_get_sdp_info(pjsip_rx_data *rdata)
-{
-    pjsip_rdata_sdp_info *sdp_info;
-    pjsip_msg_body *body = rdata->msg_info.msg->body;
-    pjsip_ctype_hdr *ctype_hdr = rdata->msg_info.ctype;
-    pjsip_media_type app_sdp;
+PJ_DEF(pjsip_sdp_info*) pjsip_get_sdp_info(pj_pool_t *pool,
+                                           pjsip_msg_body *body,
+                                           pjsip_media_type *msg_media_type,
+                                           const pjsip_media_type *search_media_type)
+{
+    pjsip_sdp_info *sdp_info;
+    pjsip_media_type search_type;
+    pjsip_media_type multipart_mixed;
+    pjsip_media_type multipart_alternative;
+    pjsip_media_type *msg_type;
+    pj_status_t status;
 
-    sdp_info = (pjsip_rdata_sdp_info*)
-	       rdata->endpt_info.mod_data[mod_inv.mod.id];
-    if (sdp_info)
-	return sdp_info;
+    sdp_info = PJ_POOL_ZALLOC_T(pool,
+                                pjsip_sdp_info);
 
-    sdp_info = PJ_POOL_ZALLOC_T(rdata->tp_info.pool,
-				pjsip_rdata_sdp_info);
     PJ_ASSERT_RETURN(mod_inv.mod.id >= 0, sdp_info);
-    rdata->endpt_info.mod_data[mod_inv.mod.id] = sdp_info;
 
-    pjsip_media_type_init2(&app_sdp, "application", "sdp");
+    if (!body) {
+        return sdp_info;
+    }
 
-    if (body && ctype_hdr &&
-	pj_stricmp(&ctype_hdr->media.type, &app_sdp.type)==0 &&
-	pj_stricmp(&ctype_hdr->media.subtype, &app_sdp.subtype)==0)
+    if (msg_media_type) {
+	msg_type = msg_media_type;
+    } else {
+	if (body->content_type.type.slen == 0) {
+	    return sdp_info;
+	}
+	msg_type = &body->content_type;
+    }
+
+    if (!search_media_type) {
+        pjsip_media_type_init2(&search_type, "application", "sdp");
+    } else {
+        pj_memcpy(&search_type, search_media_type, sizeof(search_type));
+    }
+
+    pjsip_media_type_init2(&multipart_mixed, "multipart", "mixed");
+    pjsip_media_type_init2(&multipart_alternative, "multipart", "alternative");
+
+    if (pjsip_media_type_cmp(msg_type, &search_type, PJ_FALSE) == 0)
     {
-	sdp_info->body.ptr = (char*)body->data;
-	sdp_info->body.slen = body->len;
-    } else if  (body && ctype_hdr &&
-	    	pj_stricmp2(&ctype_hdr->media.type, "multipart")==0 &&
-	    	(pj_stricmp2(&ctype_hdr->media.subtype, "mixed")==0 ||
-	    	 pj_stricmp2(&ctype_hdr->media.subtype, "alternative")==0))
+	/*
+	 * If the print_body function is print_sdp, we know that
+	 * body->data is a pjmedia_sdp_session object and came from
+	 * a tx_data.  If not, it's the text representation of the
+	 * sdp from an rx_data.
+	 */
+        if (body->print_body == print_sdp) {
+            sdp_info->sdp = body->data;
+        } else {
+            sdp_info->body.ptr = (char*)body->data;
+            sdp_info->body.slen = body->len;
+        }
+    } else if (pjsip_media_type_cmp(&multipart_mixed, msg_type, PJ_FALSE) == 0 ||
+	pjsip_media_type_cmp(&multipart_alternative, msg_type, PJ_FALSE) == 0)
     {
-	pjsip_multipart_part *part;
+        pjsip_multipart_part *part;
+        part = pjsip_multipart_find_part(body, &search_type, NULL);
+        if (part) {
+            if (part->body->print_body == print_sdp) {
+                sdp_info->sdp = part->body->data;
+            } else {
+                sdp_info->body.ptr = (char*)part->body->data;
+                sdp_info->body.slen = part->body->len;
+            }
+        }
+    }
 
-	part = pjsip_multipart_find_part(body, &app_sdp, NULL);
-	if (part) {
-	    sdp_info->body.ptr = (char*)part->body->data;
-	    sdp_info->body.slen = part->body->len;
-	}
+    /*
+     * If the body was already a pjmedia_sdp_session, we can just
+     * return it.  If not and there wasn't a text representation
+     * of the sdp either, we can also just return.
+     */
+    if (sdp_info->sdp || !sdp_info->body.ptr) {
+	return sdp_info;
     }
 
-    if (sdp_info->body.ptr) {
-	pj_status_t status;
-	status = pjmedia_sdp_parse(rdata->tp_info.pool,
-				   sdp_info->body.ptr,
-				   sdp_info->body.slen,
-				   &sdp_info->sdp);
-	if (status == PJ_SUCCESS)
-	    status = pjmedia_sdp_validate2(sdp_info->sdp, PJ_FALSE);
+    /*
+     * If the body was the text representation of teh SDP, we need
+     * to parse it to create a pjmedia_sdp_session object.
+     */
+    status = pjmedia_sdp_parse(pool,
+				sdp_info->body.ptr,
+				sdp_info->body.slen,
+				&sdp_info->sdp);
+    if (status == PJ_SUCCESS)
+	status = pjmedia_sdp_validate2(sdp_info->sdp, PJ_FALSE);
 
-	if (status != PJ_SUCCESS) {
-	    sdp_info->sdp = NULL;
-	    PJ_PERROR(1,(THIS_FILE, status,
-			 "Error parsing/validating SDP body"));
-	}
+    if (status != PJ_SUCCESS) {
+	sdp_info->sdp = NULL;
+	PJ_PERROR(1, (THIS_FILE, status,
+	    "Error parsing/validating SDP body"));
+    }
+
+    sdp_info->sdp_err = status;
+
+    return sdp_info;
+}
+
+PJ_DEF(pjsip_rdata_sdp_info*) pjsip_rdata_get_sdp_info2(
+                                            pjsip_rx_data *rdata,
+                                            const pjsip_media_type *search_media_type)
+{
+    pjsip_media_type *msg_media_type = NULL;
+    pjsip_rdata_sdp_info *sdp_info;
 
-	sdp_info->sdp_err = status;
+    if (rdata->endpt_info.mod_data[mod_inv.mod.id]) {
+	return (pjsip_rdata_sdp_info *)rdata->endpt_info.mod_data[mod_inv.mod.id];
+    }
+
+    /*
+     * rdata should have a Content-Type header at this point but we'll
+     * make sure.
+     */
+    if (rdata->msg_info.ctype) {
+	msg_media_type = &rdata->msg_info.ctype->media;
     }
+    sdp_info = pjsip_get_sdp_info(rdata->tp_info.pool,
+				   rdata->msg_info.msg->body,
+				   msg_media_type,
+				   search_media_type);
+    rdata->endpt_info.mod_data[mod_inv.mod.id] = sdp_info;
 
     return sdp_info;
 }
 
+PJ_DEF(pjsip_rdata_sdp_info*) pjsip_rdata_get_sdp_info(pjsip_rx_data *rdata)
+{
+    return pjsip_rdata_get_sdp_info2(rdata, NULL);
+}
+
+PJ_DEF(pjsip_tdata_sdp_info*) pjsip_tdata_get_sdp_info2(
+                                            pjsip_tx_data *tdata,
+                                            const pjsip_media_type *search_media_type)
+{
+    pjsip_ctype_hdr *ctype_hdr = NULL;
+    pjsip_media_type *msg_media_type = NULL;
+    pjsip_tdata_sdp_info *sdp_info;
+
+    if (tdata->mod_data[mod_inv.mod.id]) {
+	return (pjsip_tdata_sdp_info *)tdata->mod_data[mod_inv.mod.id];
+    }
+    /*
+     * tdata won't usually have a Content-Type header at this point
+     * but we'll check just the same,
+     */
+    ctype_hdr = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_CONTENT_TYPE, NULL);
+    if (ctype_hdr) {
+	msg_media_type = &ctype_hdr->media;
+    }
+
+    sdp_info = pjsip_get_sdp_info(tdata->pool,
+				   tdata->msg->body,
+				   msg_media_type,
+				   search_media_type);
+    tdata->mod_data[mod_inv.mod.id] = sdp_info;
+
+    return sdp_info;
+}
+
+PJ_DEF(pjsip_tdata_sdp_info*) pjsip_tdata_get_sdp_info(pjsip_tx_data *tdata)
+{
+    return pjsip_tdata_get_sdp_info2(tdata, NULL);
+}
 
 /*
  * Verify incoming INVITE request.
@@ -1740,13 +1846,55 @@ PJ_DEF(pj_status_t) pjsip_create_sdp_bod
     return PJ_SUCCESS;
 }
 
+static pjsip_multipart_part* create_sdp_part(pj_pool_t *pool, pjmedia_sdp_session *sdp)
+{
+    pjsip_multipart_part *sdp_part;
+    pjsip_media_type media_type;
+
+    pjsip_media_type_init2(&media_type, "application", "sdp");
+
+    sdp_part = pjsip_multipart_create_part(pool);
+    PJ_ASSERT_RETURN(sdp_part != NULL, NULL);
+
+    sdp_part->body = PJ_POOL_ZALLOC_T(pool, pjsip_msg_body);
+    PJ_ASSERT_RETURN(sdp_part->body != NULL, NULL);
+
+    pjsip_media_type_cp(pool, &sdp_part->body->content_type, &media_type);
+
+    sdp_part->body->data = sdp;
+    sdp_part->body->clone_data = clone_sdp;
+    sdp_part->body->print_body = print_sdp;
+
+    return sdp_part;
+}
+
+PJ_DEF(pj_status_t) pjsip_create_multipart_sdp_body(pj_pool_t *pool,
+						     pjmedia_sdp_session *sdp,
+						     pjsip_msg_body **p_body)
+{
+    pjsip_media_type media_type;
+    pjsip_msg_body *multipart;
+    pjsip_multipart_part *sdp_part;
+
+    pjsip_media_type_init2(&media_type, "multipart", "mixed");
+    multipart = pjsip_multipart_create(pool, &media_type, NULL);
+    PJ_ASSERT_RETURN(multipart != NULL, PJ_ENOMEM);
+
+    sdp_part = create_sdp_part(pool, sdp);
+    PJ_ASSERT_RETURN(sdp_part != NULL, PJ_ENOMEM);
+    pjsip_multipart_add_part(pool, multipart, sdp_part);
+    *p_body = multipart;
+
+    return PJ_SUCCESS;
+}
+
 static pjsip_msg_body *create_sdp_body(pj_pool_t *pool,
 				       const pjmedia_sdp_session *c_sdp)
 {
     pjsip_msg_body *body;
     pj_status_t status;
 
-    status = pjsip_create_sdp_body(pool, 
+    status = pjsip_create_sdp_body(pool,
 				   pjmedia_sdp_session_clone(pool, c_sdp),
 				   &body);
 
@@ -2069,6 +2217,7 @@ static pj_status_t inv_check_sdp_in_inco
 	       )
 	   )
 	{
+	    pjsip_sdp_info *tdata_sdp_info;
 	    const pjmedia_sdp_session *reoffer_sdp = NULL;
 
 	    PJ_LOG(4,(inv->obj_name, "Received %s response "
@@ -2077,14 +2226,15 @@ static pj_status_t inv_check_sdp_in_inco
 		      (st_code/10==18? "early" : "final" )));
 
 	    /* Retrieve original SDP offer from INVITE request */
-	    reoffer_sdp = (const pjmedia_sdp_session*) 
-			  tsx->last_tx->msg->body->data;
+	    tdata_sdp_info = pjsip_tdata_get_sdp_info(tsx->last_tx);
+	    reoffer_sdp = tdata_sdp_info->sdp;
 
 	    /* Feed the original offer to negotiator */
 	    status = pjmedia_sdp_neg_modify_local_offer2(inv->pool_prov, 
 							 inv->neg,
                                                          inv->sdp_neg_flags,
 						         reoffer_sdp);
+
 	    if (status != PJ_SUCCESS) {
 		PJ_LOG(1,(inv->obj_name, "Error updating local offer for "
 			  "forked 2xx/18x response (err=%d)", status));
--- a/pjsip/src/test/inv_offer_answer_test.c
+++ b/pjsip/src/test/inv_offer_answer_test.c
@@ -137,6 +137,7 @@ typedef struct inv_test_param_t
     pj_bool_t	need_established;
     unsigned	count;
     oa_t	oa[4];
+    pj_bool_t	multipart_body;
 } inv_test_param_t;
 
 typedef struct inv_test_t
@@ -257,6 +258,17 @@ static void on_media_update(pjsip_inv_se
 	    }
 	}
 
+	/* Special handling for standard offer/answer */
+	if (inv_test.param.count == 1 &&
+	    inv_test.param.oa[0] == OFFERER_UAC &&
+	    inv_test.param.need_established)
+	{
+	    jobs[job_cnt].type = ESTABLISH_CALL;
+	    jobs[job_cnt].who = PJSIP_ROLE_UAS;
+	    job_cnt++;
+	    TRACE_((THIS_FILE, "      C+++"));
+	}
+
 	pj_assert(job_cnt <= PJ_ARRAY_SIZE(jobs));
     }
 }
@@ -333,6 +345,15 @@ static pj_bool_t on_rx_request(pjsip_rx_
 					  NULL, &tdata);
 	pj_assert(status == PJ_SUCCESS);
 
+	/* Use multipart body, if configured */
+	if (sdp && inv_test.param.multipart_body) {
+	     status = pjsip_create_multipart_sdp_body(
+				tdata->pool,
+				pjmedia_sdp_session_clone(tdata->pool, sdp),
+				&tdata->msg->body);
+	}
+	pj_assert(status == PJ_SUCCESS);
+
 	status = pjsip_inv_send_msg(inv_test.uas, tdata);
 	pj_assert(status == PJ_SUCCESS);
 
@@ -426,6 +447,7 @@ static int perform_test(inv_test_param_t
 	sdp = NULL;
 
     status = pjsip_inv_create_uac(dlg, sdp, inv_test.param.inv_option, &inv_test.uac);
+    //inv_test.uac->create_multipart = param->multipart_body;
     PJ_ASSERT_RETURN(status==PJ_SUCCESS, -20);
 
     TRACE_((THIS_FILE, "    Sending INVITE %s offer", (sdp ? "with" : "without")));
@@ -436,8 +458,17 @@ static int perform_test(inv_test_param_t
     status = pjsip_inv_invite(inv_test.uac, &tdata);
     PJ_ASSERT_RETURN(status==PJ_SUCCESS, -30);
 
+    /* Use multipart body, if configured */
+    if (sdp && param->multipart_body) {
+	 status = pjsip_create_multipart_sdp_body(
+			    tdata->pool,
+			    pjmedia_sdp_session_clone(tdata->pool, sdp),
+			    &tdata->msg->body);
+    }
+    PJ_ASSERT_RETURN(status==PJ_SUCCESS, -40);
+
     status = pjsip_inv_send_msg(inv_test.uac, tdata);
-    PJ_ASSERT_RETURN(status==PJ_SUCCESS, -30);
+    PJ_ASSERT_RETURN(status==PJ_SUCCESS, -50);
 
     /*
      * Wait until test completes
@@ -525,13 +556,14 @@ static inv_test_param_t test_params[] =
     200/INVITE (answer)	<--
     ACK    		-->
  */
-#if 0
+#if 1
     {
 	"Standard INVITE with offer",
 	0,
 	PJ_TRUE,
 	1,
-	{ OFFERER_UAC }
+	{ OFFERER_UAC },
+	PJ_FALSE
     },
 
     {
@@ -539,7 +571,25 @@ static inv_test_param_t test_params[] =
 	PJSIP_INV_REQUIRE_100REL,
 	PJ_TRUE,
 	1,
-	{ OFFERER_UAC }
+	{ OFFERER_UAC },
+	PJ_FALSE
+    },
+    {
+	"Standard INVITE with offer, with Multipart",
+	0,
+	PJ_TRUE,
+	1,
+	{ OFFERER_UAC },
+	PJ_TRUE
+    },
+
+    {
+	"Standard INVITE with offer, with 100rel, with Multipart",
+	PJSIP_INV_REQUIRE_100REL,
+	PJ_TRUE,
+	1,
+	{ OFFERER_UAC },
+	PJ_TRUE
     },
 #endif
 
@@ -555,7 +605,8 @@ static inv_test_param_t test_params[] =
 	0,
 	PJ_TRUE,
 	1,
-	{ OFFERER_UAS }
+	{ OFFERER_UAS },
+	PJ_FALSE
     },
 
     {
@@ -563,7 +614,25 @@ static inv_test_param_t test_params[] =
 	PJSIP_INV_REQUIRE_100REL,
 	PJ_TRUE,
 	1,
-	{ OFFERER_UAS }
+	{ OFFERER_UAS },
+	PJ_FALSE
+    },
+    {
+	"INVITE with no offer, with Multipart",
+	0,
+	PJ_TRUE,
+	1,
+	{ OFFERER_UAS },
+	PJ_TRUE
+    },
+
+    {
+	"INVITE with no offer, with 100rel, with Multipart",
+	PJSIP_INV_REQUIRE_100REL,
+	PJ_TRUE,
+	1,
+	{ OFFERER_UAS },
+	PJ_TRUE
     },
 #endif
 
@@ -584,14 +653,24 @@ static inv_test_param_t test_params[] =
 	0,
 	PJ_TRUE,
 	2,
-	{ OFFERER_UAC, OFFERER_UAC }
+	{ OFFERER_UAC, OFFERER_UAC },
+	PJ_FALSE
+    },
+    {
+	"INVITE and UPDATE by UAC, with Multipart",
+	0,
+	PJ_TRUE,
+	2,
+	{ OFFERER_UAC, OFFERER_UAC },
+	PJ_TRUE
     },
     {
 	"INVITE and UPDATE by UAC, with 100rel",
 	PJSIP_INV_REQUIRE_100REL,
 	PJ_TRUE,
 	2,
-	{ OFFERER_UAC, OFFERER_UAC }
+	{ OFFERER_UAC, OFFERER_UAC },
+	PJ_FALSE
     },
 #endif
 
@@ -617,6 +696,14 @@ static inv_test_param_t test_params[] =
 	4,
 	{ OFFERER_UAC, OFFERER_UAS, OFFERER_UAC, OFFERER_UAS }
     },
+    {
+	"INVITE and many UPDATE by UAC and UAS, with Multipart",
+	0,
+	PJ_TRUE,
+	4,
+	{ OFFERER_UAC, OFFERER_UAS, OFFERER_UAC, OFFERER_UAS },
+	PJ_TRUE
+    },
 
 };
 
