New HTTP RESTful requests: MeshMS read message(s)

This commit is contained in:
Andrew Bettison 2014-06-24 12:11:58 +09:30
parent eba7f6555f
commit 7736a4ceb1
7 changed files with 385 additions and 87 deletions

View File

@ -97,6 +97,7 @@ static int http_request_parse_http_version(struct http_request *r);
static int http_request_start_parsing_headers(struct http_request *r);
static int http_request_parse_header(struct http_request *r);
static int http_request_start_body(struct http_request *r);
static int http_request_reject_content(struct http_request *r);
static int http_request_parse_body_form_data(struct http_request *r);
static void http_request_start_response(struct http_request *r);
@ -1025,6 +1026,9 @@ static int http_request_start_body(struct http_request *r)
DEBUGF("Malformed HTTP %s request: missing Content-Length header", r->verb);
return 411;
}
if (r->request_header.content_length == 0) {
r->parser = http_request_reject_content;
} else {
if (r->request_header.content_type.type[0] == '\0') {
if (r->debug_flag && *r->debug_flag)
DEBUGF("Malformed HTTP %s request: missing Content-Type header", r->verb);
@ -1050,6 +1054,7 @@ static int http_request_start_body(struct http_request *r)
return 415;
}
}
}
else {
if (r->debug_flag && *r->debug_flag)
DEBUGF("Unsupported HTTP %s request", r->verb);
@ -1061,6 +1066,22 @@ static int http_request_start_body(struct http_request *r)
return 0;
}
/* A special content parser that rejects any content, used when a Content-Type: 0 header was
* received.
*
* @author Andrew Bettison <andrew@servalproject.com>
*/
static int http_request_reject_content(struct http_request *r)
{
if (r->debug_flag && *r->debug_flag) {
if (r->request_header.content_length != CONTENT_LENGTH_UNKNOWN)
DEBUGF("Malformed HTTP %s request (Content-Length %"PRIhttp_size_t"): spurious content", r->verb, r->request_header.content_length);
else
DEBUGF("Malformed HTTP %s request: spurious content", r->verb);
}
return 400;
}
/* Returns 1 if a MIME delimiter is skipped, 2 if a MIME close-delimiter is skipped.
*/
static int _skip_mime_boundary(struct http_request *r)
@ -1208,7 +1229,7 @@ static int http_request_form_data_start_part(struct http_request *r, int b)
*
* NOTE: No support for nested/mixed parts, as that would considerably complicate the parser. If
* the need arises in future, we will deal with it then. In the meantime, we will have something
* that meets our immediate needs for Rhizome Direct and a variety of use cases.
* that meets our immediate needs for Rhizome Direct and the RESTful API.
*
* @author Andrew Bettison <andrew@servalproject.com>
*/

143
meshms.c
View File

@ -32,6 +32,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#define MESHMS_BLOCK_TYPE_ACK 0x01
#define MESHMS_BLOCK_TYPE_MESSAGE 0x02 // NUL-terminated UTF8 string
static unsigned mark_read(struct meshms_conversations *conv, const sid_t *their_sid, const uint64_t offset);
void meshms_free_conversations(struct meshms_conversations *conv)
{
if (conv) {
@ -681,7 +683,7 @@ static enum meshms_status write_known_conversations(rhizome_manifest *m, struct
// error is already logged
break;
case RHIZOME_BUNDLE_STATUS_NEW:
status = MESHMS_STATUS_OK;
status = MESHMS_STATUS_UPDATED;
break;
case RHIZOME_BUNDLE_STATUS_SAME:
case RHIZOME_BUNDLE_STATUS_DUPLICATE:
@ -953,6 +955,51 @@ enum meshms_status meshms_send_message(const sid_t *sender, const sid_t *recipie
return status;
}
enum meshms_status meshms_mark_read(const sid_t *sender, const sid_t *recipient, uint64_t offset)
{
if (config.debug.meshms)
DEBUGF("sender=%s recipient=%s offset=%"PRIu64,
alloca_tohex_sid_t(*sender),
recipient ? alloca_tohex_sid_t(*recipient) : "NULL",
offset
);
enum meshms_status status = MESHMS_STATUS_ERROR;
struct meshms_conversations *conv = NULL;
rhizome_manifest *m = rhizome_new_manifest();
if (!m)
goto end;
if (meshms_failed(status = get_my_conversation_bundle(sender, m)))
goto end;
// read all conversations, so we can write them again
if (meshms_failed(status = read_known_conversations(m, NULL, &conv)))
goto end;
// read the full list of conversations from the database too
if (meshms_failed(status = get_database_conversations(sender, NULL, &conv)))
goto end;
// check if any incoming conversations need to be acked or have new messages and update the read offset
unsigned changed = 0;
if (meshms_failed(status = update_conversations(sender, conv)))
goto end;
if (status == MESHMS_STATUS_UPDATED)
changed = 1;
changed += mark_read(conv, recipient, offset);
if (config.debug.meshms)
DEBUGF("changed=%u", changed);
if (changed) {
if (meshms_failed(status = write_known_conversations(m, conv)))
goto end;
if (status != MESHMS_STATUS_UPDATED) {
WHYF("expecting %d (MESHMS_STATUS_UPDATED), got %s", MESHMS_STATUS_UPDATED, status);
status = MESHMS_STATUS_ERROR;
}
}
end:
if (m)
rhizome_manifest_free(m);
meshms_free_conversations(conv);
return status;
}
// output the list of existing conversations for a given local identity
int app_meshms_conversations(const struct cli_parsed *parsed, struct cli_context *context)
{
@ -1127,35 +1174,30 @@ int app_meshms_list_messages(const struct cli_parsed *parsed, struct cli_context
}
// Returns the number of read markers moved.
static unsigned mark_read(struct meshms_conversations *conv, const sid_t *their_sid, const char *offset_str)
static unsigned mark_read(struct meshms_conversations *conv, const sid_t *their_sid, const uint64_t offset)
{
unsigned ret=0;
if (conv){
int cmp = their_sid ? cmp_sid_t(&conv->them, their_sid) : 0;
if (!their_sid || cmp<0){
ret+=mark_read(conv->_left, their_sid, offset_str);
}
if (!their_sid || cmp<0)
ret += mark_read(conv->_left, their_sid, offset);
if (!their_sid || cmp==0){
// update read offset
// - never rewind
// - never past their last message
uint64_t offset = conv->their_last_message;
if (offset_str){
uint64_t x = atol(offset_str);
if (x<offset)
offset=x;
}
if (offset > conv->read_offset){
// - never rewind, only advance
uint64_t new_offset = offset;
if (new_offset > conv->their_last_message)
new_offset = conv->their_last_message;
if (new_offset > conv->read_offset) {
if (config.debug.meshms)
DEBUGF("Moving read marker for %s, from %"PRId64" to %"PRId64,
alloca_tohex_sid_t(conv->them), conv->read_offset, offset);
conv->read_offset = offset;
alloca_tohex_sid_t(conv->them), conv->read_offset, new_offset);
conv->read_offset = new_offset;
ret++;
}
}
if (!their_sid || cmp>0){
ret+=mark_read(conv->_right, their_sid, offset_str);
}
if (!their_sid || cmp>0)
ret += mark_read(conv->_right, their_sid, offset);
}
return ret;
}
@ -1165,51 +1207,40 @@ int app_meshms_mark_read(const struct cli_parsed *parsed, struct cli_context *UN
const char *my_sidhex, *their_sidhex, *offset_str;
if (cli_arg(parsed, "sender_sid", &my_sidhex, str_is_subscriber_id, "") == -1
|| cli_arg(parsed, "recipient_sid", &their_sidhex, str_is_subscriber_id, NULL) == -1
|| cli_arg(parsed, "offset", &offset_str, NULL, NULL)==-1)
|| cli_arg(parsed, "offset", &offset_str, str_is_uint64_decimal, NULL)==-1)
return -1;
if (create_serval_instance_dir() == -1)
return -1;
if (!(keyring = keyring_open_instance_cli(parsed)))
return -1;
if (rhizome_opendb() == -1){
keyring_free(keyring);
keyring = NULL;
return -1;
}
int ret = -1;
if (rhizome_opendb() == -1)
goto done;
sid_t my_sid, their_sid;
fromhex(my_sid.binary, my_sidhex, sizeof(my_sid.binary));
if (their_sidhex)
fromhex(their_sid.binary, their_sidhex, sizeof(their_sid.binary));
enum meshms_status status = MESHMS_STATUS_ERROR;
struct meshms_conversations *conv = NULL;
rhizome_manifest *m = rhizome_new_manifest();
if (!m)
goto end;
if (meshms_failed(status = get_my_conversation_bundle(&my_sid, m)))
goto end;
// read all conversations, so we can write them again
if (meshms_failed(status = read_known_conversations(m, NULL, &conv)))
goto end;
// read the full list of conversations from the database too
if (meshms_failed(status = get_database_conversations(&my_sid, NULL, &conv)))
goto end;
// check if any incoming conversations need to be acked or have new messages and update the read offset
int changed = 0;
if (meshms_failed(status = update_conversations(&my_sid, conv)))
goto end;
if (status == MESHMS_STATUS_UPDATED)
changed = 1;
if (mark_read(conv, their_sidhex?&their_sid:NULL, offset_str))
changed =1;
if (changed)
status = write_known_conversations(m, conv);
end:
if (m)
rhizome_manifest_free(m);
meshms_free_conversations(conv);
if (str_to_sid_t(&my_sid, my_sidhex) == -1) {
ret = WHYF("my_sidhex=%s", my_sidhex);
goto done;
}
if (their_sidhex && str_to_sid_t(&their_sid, their_sidhex) == -1) {
ret = WHYF("their_sidhex=%s", their_sidhex);
goto done;
}
uint64_t offset = UINT64_MAX;
if (offset_str) {
if (!their_sidhex) {
ret = WHY("missing recipient_sid");
goto done;
}
if (!str_to_uint64(offset_str, 10, &offset, NULL)) {
ret = WHYF("offset_str=%s", offset_str);
goto done;
}
}
enum meshms_status status = meshms_mark_read(&my_sid, their_sidhex ? &their_sid : NULL, offset);
ret = (status == MESHMS_STATUS_UPDATED) ? MESHMS_STATUS_OK : status;
done:
keyring_free(keyring);
keyring = NULL;
return status;
return ret;
}

View File

@ -175,9 +175,25 @@ enum meshms_status meshms_message_iterator_prev(struct meshms_message_iterator *
/* Append a message ('message_len' bytes of UTF8 at 'message') to the sender's
* ply in the conversation between 'sender' and 'recipient'. If no
* conversation (ply bundle) exists, then create it. Returns 0 on success, -1
* on error (already logged).
* conversation (ply bundle) exists, then create it. Returns
* MESHMS_STATUS_UPDATED on success, any other value indicates a failure or
* error (which is already logged).
*/
enum meshms_status meshms_send_message(const sid_t *sender, const sid_t *recipient, const char *message, size_t message_len);
/* Update the read offset for one or more conversations. Returns
* MESHMS_STATUS_UPDATED on success, any other value indicates a failure or
* error (which is already logged).
*
* If 'offset' is greater than a conversation's last-received offset, then it
* is clamped to the last-received offset. This means that passing an offset
* of UINT64_MAX will mark the conversation as fully read, and an offset of
* zero will have no effect.
*
* If 'recipient' is NULL then all of the sender's conversations are marked
* with the given read offset. In this case it only makes sense to pass an
* offest of UINT64_MAX.
*/
enum meshms_status meshms_mark_read(const sid_t *sender, const sid_t *recipient, uint64_t offset);
#endif // __SERVAL_DNA__MESHMS_H

View File

@ -112,6 +112,9 @@ static HTTP_HANDLER restful_meshms_conversationlist_json;
static HTTP_HANDLER restful_meshms_messagelist_json;
static HTTP_HANDLER restful_meshms_newsince_messagelist_json;
static HTTP_HANDLER restful_meshms_sendmessage;
static HTTP_HANDLER restful_meshms_read_all_conversations;
static HTTP_HANDLER restful_meshms_read_all_messages;
static HTTP_HANDLER restful_meshms_read_to_offset;
int restful_meshms_(httpd_request *r, const char *remainder)
{
@ -119,6 +122,7 @@ int restful_meshms_(httpd_request *r, const char *remainder)
if (!is_rhizome_http_enabled())
return 403;
const char *verb = HTTP_VERB_GET;
http_size_t content_length = CONTENT_LENGTH_UNKNOWN;
HTTP_HANDLER *handler = NULL;
const char *end;
if (strn_to_sid_t(&r->sid1, remainder, SIZE_MAX, &end) != -1) {
@ -126,7 +130,14 @@ int restful_meshms_(httpd_request *r, const char *remainder)
if (strcmp(remainder, "/conversationlist.json") == 0) {
handler = restful_meshms_conversationlist_json;
remainder = "";
} else if (*remainder == '/' && strn_to_sid_t(&r->sid2, remainder + 1, SIZE_MAX, &end) != -1) {
}
else if (strcmp(remainder, "/readall") == 0) {
handler = restful_meshms_read_all_conversations;
verb = HTTP_VERB_POST;
content_length = 0;
remainder = "";
}
else if (*remainder == '/' && strn_to_sid_t(&r->sid2, remainder + 1, SIZE_MAX, &end) != -1) {
remainder = end;
if (strcmp(remainder, "/messagelist.json") == 0) {
handler = restful_meshms_messagelist_json;
@ -144,12 +155,36 @@ int restful_meshms_(httpd_request *r, const char *remainder)
verb = HTTP_VERB_POST;
remainder = "";
}
else if (strcmp(remainder, "/readall") == 0) {
handler = restful_meshms_read_all_messages;
verb = HTTP_VERB_POST;
content_length = 0;
remainder = "";
}
else if (str_startswith(remainder, "/recv/", &end)) {
remainder = end;
if (str_to_uint64(remainder, 10, &r->ui64, &end)) {
remainder = end;
if (strcmp(remainder, "/read") == 0) {
handler = restful_meshms_read_to_offset;
verb = HTTP_VERB_POST;
content_length = 0;
remainder = "";
}
}
}
}
}
if (handler == NULL)
return 404;
if (r->http.verb != verb)
return 405;
if ( content_length != CONTENT_LENGTH_UNKNOWN
&& r->http.request_header.content_length != CONTENT_LENGTH_UNKNOWN
&& r->http.request_header.content_length != content_length) {
http_request_simple_response(&r->http, 400, "Bad content length");
return 400;
}
int ret = authorize(&r->http);
if (ret)
return ret;
@ -566,3 +601,42 @@ static int restful_meshms_sendmessage_end(struct http_request *hr)
return http_request_meshms_response(r, 0, NULL, status);
return http_request_meshms_response(r, 201, "Message sent", status);
}
static int restful_meshms_read_all_conversations(httpd_request *r, const char *remainder)
{
if (*remainder)
return 404;
assert(r->finalise_union == NULL);
enum meshms_status status;
if (meshms_failed(status = meshms_mark_read(&r->sid1, NULL, UINT64_MAX)))
return http_request_meshms_response(r, 0, NULL, status);
if (status == MESHMS_STATUS_UPDATED)
return http_request_meshms_response(r, 201, "Read offsets updated", status);
return http_request_meshms_response(r, 200, "Read offsets unchanged", status);
}
static int restful_meshms_read_all_messages(httpd_request *r, const char *remainder)
{
if (*remainder)
return 404;
assert(r->finalise_union == NULL);
enum meshms_status status;
if (meshms_failed(status = meshms_mark_read(&r->sid1, &r->sid2, UINT64_MAX)))
return http_request_meshms_response(r, 0, NULL, status);
if (status == MESHMS_STATUS_UPDATED)
return http_request_meshms_response(r, 201, "Read offset updated", status);
return http_request_meshms_response(r, 200, "Read offset unchanged", status);
}
static int restful_meshms_read_to_offset(httpd_request *r, const char *remainder)
{
if (*remainder)
return 404;
assert(r->finalise_union == NULL);
enum meshms_status status;
if (meshms_failed(status = meshms_mark_read(&r->sid1, &r->sid2, r->ui64)))
return http_request_meshms_response(r, 0, NULL, status);
if (status == MESHMS_STATUS_UPDATED)
return http_request_meshms_response(r, 201, "Read offset updated", status);
return http_request_meshms_response(r, 200, "Read offset unchanged", status);
}

34
str.c
View File

@ -645,6 +645,11 @@ char *str_str(char *haystack, const char *needle, size_t haystack_len)
return NULL;
}
int str_is_uint64_decimal(const char *str)
{
return str_to_uint64(str, 10, NULL, NULL);
}
int str_to_int32(const char *str, unsigned base, int32_t *result, const char **afterp)
{
if (isspace(*str))
@ -710,14 +715,29 @@ int str_to_int64(const char *str, unsigned base, int64_t *result, const char **a
int str_to_uint64(const char *str, unsigned base, uint64_t *result, const char **afterp)
{
if (isspace(*str))
return 0;
const char *end = str;
errno = 0;
unsigned long long value = strtoull(str, (char**)&end, base);
return strn_to_uint64(str, 0, base, result, afterp);
}
int strn_to_uint64(const char *str, size_t strlen, unsigned base, uint64_t *result, const char **afterp)
{
assert(base > 0);
assert(base <= 16);
uint64_t value = 0;
uint64_t newvalue = 0;
const char *const end = str + strlen;
const char *s;
for (s = str; strlen ? s < end : *s; ++s) {
int digit = hexvalue(*s);
if (digit < 0 || (unsigned)digit >= base)
break;
newvalue = value * base + digit;
if (newvalue < value) // overflow
break;
value = newvalue;
}
if (afterp)
*afterp = end;
if (errno == ERANGE || end == str || isdigit(*end) || (!afterp && *end))
*afterp = s;
if (s == str || value > UINT64_MAX || value != newvalue || (!afterp && (strlen ? s != end : *s)))
return 0;
if (result)
*result = value;

9
str.h
View File

@ -380,6 +380,14 @@ int strn_str_casecmp(const char *str1, size_t len1, const char *str2);
*/
char *str_str(char *haystack, const char *needle, size_t haystack_len);
/* Returns 1 if the given nul-terminated string parses successfully as an unsigned 64-bit integer.
* Returns 0 if not. This is simply a shortcut for str_to_uint32(str, 10, NULL, NULL), which is
* convenient for when a pointer to a predicate function is needed.
*
* @author Andrew Bettison <andrew@servalproject.com>
*/
int str_is_uint64_decimal(const char *str);
/* Parse a NUL-terminated string as an integer in ASCII radix notation in the given 'base' (eg,
* base=10 means decimal).
*
@ -408,6 +416,7 @@ int str_to_uint64(const char *str, unsigned base, uint64_t *result, const char *
* @author Andrew Bettison <andrew@servalproject.com>
*/
int strn_to_uint32(const char *str, size_t strlen, unsigned base, uint32_t *result, const char **afterp);
int strn_to_uint64(const char *str, size_t strlen, unsigned base, uint64_t *result, const char **afterp);
/* Parse a string as an integer in ASCII radix notation in the given 'base' (eg, base=10 means
* decimal) and scale the result by a factor given by an optional suffix "scaling" character in the

View File

@ -126,6 +126,7 @@ set_rhizome_config() {
set debug.rhizome_manifest on \
set debug.rhizome_store on \
set debug.rhizome on \
set debug.meshms on \
set debug.verbose on \
set log.console.level debug
}
@ -1359,4 +1360,130 @@ test_MeshmsSendNoIdentity() {
assertJqGrep --ignore-case http.body '.http_status_message' 'identity.*unknown'
}
doc_MeshmsReadAllConversations="HTTP RESTful MeshMS mark all conversations read"
setup_MeshmsReadAllConversations() {
IDENTITY_COUNT=5
setup
# create 3 threads, with all permutations of incoming and outgoing messages
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message1"
executeOk_servald meshms send message $SIDA3 $SIDA1 "Message2"
executeOk_servald meshms send message $SIDA1 $SIDA4 "Message3"
executeOk_servald meshms send message $SIDA4 $SIDA1 "Message4"
executeOk_servald meshms list conversations $SIDA1
assertStdoutGrep --stderr --matches=1 ":$SIDA2::0:0\$"
assertStdoutGrep --stderr --matches=1 ":$SIDA3:unread:11:0\$"
assertStdoutGrep --stderr --matches=1 ":$SIDA4:unread:14:0\$"
}
test_MeshmsReadAllConversations() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output http_body \
--basic --user harry:potter \
--request POST \
"http://$addr_localhost:$PORTA/restful/meshms/$SIDA1/readall"
tfw_cat http_body
assertExitStatus == 0
assertStdoutIs 201
executeOk_servald meshms list conversations $SIDA1
assertStdoutGrep --stderr --matches=1 ":$SIDA2::0:0\$"
assertStdoutGrep --stderr --matches=1 ":$SIDA3::11:11\$"
assertStdoutGrep --stderr --matches=1 ":$SIDA4::14:14\$"
}
doc_MeshmsPostSpuriousContent="HTTP RESTful MeshMS rejects unwanted content in POST request"
setup_MeshmsPostSpuriousContent() {
IDENTITY_COUNT=2
setup
# create 3 threads, with all permutations of incoming and outgoing messages
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message1"
executeOk_servald meshms send message $SIDA2 $SIDA1 "Message2"
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message3"
executeOk_servald meshms send message $SIDA2 $SIDA1 "Message4"
executeOk_servald meshms list conversations $SIDA1
assertStdoutGrep --stderr --matches=1 ":$SIDA2:unread:29:0\$"
}
test_MeshmsPostSpuriousContent() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output http_body \
--basic --user harry:potter \
--request POST \
--form "offset=0" \
"http://$addr_localhost:$PORTA/restful/meshms/$SIDA1/readall"
tfw_cat http_body
assertExitStatus == 0
assertStdoutIs 400
assertJq http_body 'contains({"http_status_code": 400})'
assertJqGrep --ignore-case http_body '.http_status_message' 'content length'
executeOk_servald meshms list conversations $SIDA1
assertStdoutGrep --stderr --matches=1 ":$SIDA2:unread:29:0\$"
}
doc_MeshmsReadAllMessages="HTTP RESTful MeshMS mark all conversations read"
setup_MeshmsReadAllMessages() {
IDENTITY_COUNT=5
setup
# create 3 threads, with all permutations of incoming and outgoing messages
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message1"
executeOk_servald meshms send message $SIDA3 $SIDA1 "Message2"
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message3"
executeOk_servald meshms send message $SIDA1 $SIDA4 "Message4"
executeOk_servald meshms send message $SIDA4 $SIDA1 "Message5"
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message6"
executeOk_servald meshms list conversations $SIDA2
assertStdoutGrep --stderr --matches=1 ":$SIDA1:unread:33:0\$"
}
test_MeshmsReadAllMessages() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output http_body \
--basic --user harry:potter \
--request POST \
"http://$addr_localhost:$PORTA/restful/meshms/$SIDA2/$SIDA1/readall"
tfw_cat http_body
assertExitStatus == 0
assertStdoutIs 201
executeOk_servald meshms list conversations $SIDA2
assertStdoutGrep --stderr --matches=1 ":$SIDA1::33:33\$"
}
doc_MeshmsReadMessage="HTTP RESTful MeshMS mark a message as read"
setup_MeshmsReadMessage() {
IDENTITY_COUNT=5
setup
# create 3 threads, with all permutations of incoming and outgoing messages
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message1"
executeOk_servald meshms send message $SIDA3 $SIDA1 "Message2"
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message3"
executeOk_servald meshms send message $SIDA1 $SIDA4 "Message4"
executeOk_servald meshms send message $SIDA4 $SIDA1 "Message5"
executeOk_servald meshms send message $SIDA1 $SIDA2 "Message6"
executeOk_servald meshms list conversations $SIDA2
assertStdoutGrep --stderr --matches=1 ":$SIDA1:unread:33:0\$"
}
test_MeshmsReadMessage() {
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output http_body \
--basic --user harry:potter \
--request POST \
"http://$addr_localhost:$PORTA/restful/meshms/$SIDA2/$SIDA1/recv/22/read"
tfw_cat http_body
assertExitStatus == 0
assertStdoutIs 201
executeOk_servald meshms list conversations $SIDA2
assertStdoutGrep --stderr --matches=1 ":$SIDA1:unread:33:22\$"
executeOk curl \
--silent --show-error --write-out '%{http_code}' \
--output read.json \
--basic --user harry:potter \
--request POST \
"http://$addr_localhost:$PORTA/restful/meshms/$SIDA2/$SIDA1/recv/11/read"
tfw_cat read.json
assertExitStatus == 0
assertStdoutIs 200
executeOk_servald meshms list conversations $SIDA2
assertStdoutGrep --stderr --matches=1 ":$SIDA1:unread:33:22\$"
}
runTests "$@"