/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "services/normal/timeline/item.h" #include "util/size.h" #include "clar.h" // Stubs //////////////////////////////////// #include "fake_rtc.h" #include "stubs_fonts.h" #include "stubs_layout_layer.h" #include "stubs_passert.h" #include "stubs_logging.h" #include "stubs_pbl_malloc.h" #include "stubs_pebble_tasks.h" #include "stubs_rand_ptr.h" static uint8_t s_payload_complete[] = { // Attribute 1 0x01, // Attribute ID - Title 0x11, 0x00, // Attribute Length // Attribute text: "Test Notification" 0x54, 0x65, 0x73, 0x74, 0x20, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, // Attribute 2 0x02, // Attribute ID - Subtitle 0x08, 0x00, // Attribute Length // Attribute text: "Subtitle" 'S', 'u', 'b', 't', 'i', 't', 'l', 'e', // Attribute 3 0x03, // Attribute ID - Body 0x3f, 0x00, // Attribute Length // Attribute text: "This is a test notification. Look at it and behold the awesome." 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x20, 0x4c, 0x6f, 0x6f, 0x6b, 0x20, 0x61, 0x74, 0x20, 0x69, 0x74, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x62, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x61, 0x77, 0x65, 0x73, 0x6f, 0x6d, 0x65, 0x2e, // Action 1 0x00, // Action ID 0x02, // Action Type - Pebble Protocol 0x01, // Number of action attributes // Action Attributes 0x01, // Attribute ID - Title 0x07, 0x00, // Attribute Length // Attribute text: 'D', 'i', 's', 'm', 'i', 's', 's', // Action 2 0x01, // Action ID 0x02, // Action Type - Pebble Protocol 0x02, // Number of action attributes // Action Attributes 0x01, // Attribute 1 ID - Title 0x04, 0x00, // Attribute 1 Length // Attribute text: 'L', 'i', 'k', 'e', 0x07, // Attribute 2 ID - ANCS UID 0x01, 0x00, // Attribute 2 Length // Attribute text: "Test" 0x01 }; void test_timeline_item__initialize(void) { } void test_timeline_item__cleanup(void) { } static const uint8_t s_serialized_attribute_list[] = { 0x01, // Attribute 1 ID - Title 0x04, 0x00, // Attribute 1 Length // Attribute text: 'L', 'i', 'k', 'e', 0x02, // Attribute 1 ID - Title 0x03, 0x00, // Attribute 1 Length // Attribute text: 'e', 'y', 'e', }; static const uint8_t s_invalid_serialized_attribute_list[] = { 0x01, // Attribute 1 ID - Title 0x04, 0x00, // Attribute 1 Length // Attribute text: 'L', 'i', 'k', 'e', 0x08, // Attribute 2 ID - String list 0x4e, 0x00, // Attribute 2 length // Attribute content 0x74, 0x65, 0x73, 0x74, 0x00, 0xd0, 0x94, 0xd0, 0xb0, 0x00, 0xd0, 0x9d, 0xd0, 0xb5, 0xd1, 0x82, 0x00, 0xd0, 0x9e, 0xd0, 0x9a, 0x00, 0xd0, 0xa5, 0xd0, 0xb0, 0x2d, 0xd1, 0x85, 0xd0, 0xb0, 0x00, 0xd0, 0xa1, 0xd0, 0xbf, 0xd0, 0xb0, 0xd1, 0x81, 0xd0, 0xb8, 0xd0, 0xb1, 0xd0, 0xbe, 0x00, 0xd0, 0xa5, 0xd0, 0xbe, 0xd1, 0x80, 0xd0, 0xbe, 0xd1, 0x88, 0xd0, 0xbe, 0x00, 0xd0, 0x9e, 0xd1, 0x82, 0xd0, 0xbb, 0xd0, 0xb8, 0xd1, 0x87, 0xd0, 0xbd, 0xd0, 0xbe, 0x00, 0xd0, 0xa1, 0xd0, 0xba, 0xd0, 0xbe, 0xd1, 0x80, 0xd0, 0xbe, 0x20, 0xd0, 0xb1, 0xd1, 0x83, 0xd0, 0xb4, 0xd1, }; void test_timeline_item__get_serialized_attributes_length(void) { const uint8_t *cursor = s_serialized_attribute_list; int32_t result = attribute_get_buffer_size_for_serialized_attributes(2, &cursor, s_serialized_attribute_list + sizeof(s_serialized_attribute_list)); cl_assert(result == 9); cursor = s_invalid_serialized_attribute_list; result = attribute_get_buffer_size_for_serialized_attributes(3, &cursor, (uint8_t *)s_invalid_serialized_attribute_list + sizeof(s_invalid_serialized_attribute_list)); cl_assert(result < 0); } void test_timeline_item__deserialize_payload(void) { size_t buf_size = 18 + 9 + 64 + 8 + 5 + 5; char *buffer; TimelineItem *item = timeline_item_create(3, 2, (uint8_t[]) {1, 2}, buf_size, (uint8_t **)&buffer); timeline_item_deserialize_payload(item, buffer, buf_size, s_payload_complete, sizeof(s_payload_complete)); cl_assert_equal_i(item->attr_list.num_attributes, 3); cl_assert_equal_i(item->attr_list.attributes[0].id, AttributeIdTitle); cl_assert_equal_s(item->attr_list.attributes[0].cstring, "Test Notification"); cl_assert_equal_i(item->attr_list.attributes[1].id, AttributeIdSubtitle); cl_assert_equal_s(item->attr_list.attributes[1].cstring, "Subtitle"); cl_assert_equal_i(item->attr_list.attributes[2].id, AttributeIdBody); cl_assert_equal_s(item->attr_list.attributes[2].cstring, "This is a test notification. " "Look at it and behold the awesome."); cl_assert_equal_i(item->action_group.num_actions, 2); cl_assert_equal_i(item->action_group.actions[0].id, 0); cl_assert_equal_i(item->action_group.actions[0].type, TimelineItemActionTypeGeneric); cl_assert_equal_i(item->action_group.actions[0].attr_list.num_attributes, 1); cl_assert_equal_i(item->action_group.actions[0].attr_list.attributes[0].id, AttributeIdTitle); cl_assert_equal_s(item->action_group.actions[0].attr_list.attributes[0].cstring, "Dismiss"); cl_assert_equal_i(item->action_group.actions[1].id, 1); cl_assert_equal_i(item->action_group.actions[1].type, TimelineItemActionTypeGeneric); cl_assert_equal_i(item->action_group.actions[1].attr_list.num_attributes, 2); cl_assert_equal_i(item->action_group.actions[1].attr_list.attributes[0].id, AttributeIdTitle); cl_assert_equal_s(item->action_group.actions[1].attr_list.attributes[0].cstring, "Like"); cl_assert_equal_i(item->action_group.actions[1].attr_list.attributes[1].id, AttributeIdAncsAction); cl_assert_equal_i(item->action_group.actions[1].attr_list.attributes[1].uint8, 1); timeline_item_destroy(item); } static Attribute action1_attributes[] = { {.id = AttributeIdTitle, .cstring = "Dismiss"}, }; static Attribute action2_attributes[] = { {.id = AttributeIdTitle, .cstring = "Like"}, {.id = AttributeIdAncsAction, .int8 = 1} }; static Attribute attributes[] = { {.id = AttributeIdTitle, .cstring = "Test Notification"}, {.id = AttributeIdSubtitle, .cstring = "Subtitle"}, {.id = AttributeIdBody, .cstring = "This is a test notification. " "Look at it and behold the awesome."}, }; static TimelineItemAction actions[] = { {.id = 0, .type = TimelineItemActionTypeGeneric, .attr_list = {.num_attributes = ARRAY_LENGTH(action1_attributes), .attributes = action1_attributes}}, {.id = 1, .type = TimelineItemActionTypeGeneric, .attr_list = {.num_attributes = ARRAY_LENGTH(action2_attributes), .attributes = action2_attributes}}, }; void test_timeline_item__serialize_payload(void) { TimelineItem item = { .attr_list.num_attributes = ARRAY_LENGTH(attributes), .attr_list.attributes = attributes, .action_group.num_actions = ARRAY_LENGTH(actions), .action_group.actions = actions, }; uint8_t *buffer = malloc(sizeof(s_payload_complete)); timeline_item_serialize_payload(&item, buffer, sizeof(s_payload_complete)); cl_assert(memcmp(buffer, s_payload_complete, sizeof(s_payload_complete)) == 0); } void test_timeline_item__string_list(void) { StringList *list = malloc(20); memset(list, 0, 20); // no data list->serialized_byte_length = 0; cl_assert_equal_i(0, string_list_count(list)); list->serialized_byte_length = 3; // 4 empty strings cl_assert_equal_i(4, string_list_count(list)); cl_assert_equal_s("", string_list_get_at(list, 0)); cl_assert_equal_s("", string_list_get_at(list, 1)); cl_assert_equal_s("", string_list_get_at(list, 2)); cl_assert_equal_s("", string_list_get_at(list, 3)); // non-null-terminated string is treated as one string - this is the standard case // please note that the string will only be terminated if there's another \0 following // when deserializing the data, the deserializer will append the needed \0 list->serialized_byte_length = 3; list->data[0] = 'a'; list->data[1] = 'b'; list->data[2] = 'c'; // end of data list->data[3] = 'd'; list->data[4] = '\0'; cl_assert_equal_i(1, string_list_count(list)); cl_assert_equal_s("abcd", string_list_get_at(list, 0)); // 1 string (null terminated) => 2 strings, last is empty list->serialized_byte_length = 3; list->data[0] = 'a'; list->data[1] = 'b'; list->data[2] = '\0'; // end of data list->data[3] = '\0'; cl_assert_equal_i(2, string_list_count(list)); cl_assert_equal_s("ab", string_list_get_at(list, 0)); cl_assert_equal_s("", string_list_get_at(list, 1)); // 2 strings (non-null terminated) - this is the standard case list->serialized_byte_length = 4; list->data[0] = 'a'; list->data[1] = 'b'; list->data[2] = '\0'; list->data[3] = 'c'; // end of data list->data[4] = '\0'; cl_assert_equal_i(2, string_list_count(list)); cl_assert_equal_s("ab", string_list_get_at(list, 0)); cl_assert_equal_s("c", string_list_get_at(list, 1)); // 3 strings (last two are is empty) list->serialized_byte_length = 4; list->data[0] = 'a'; list->data[1] = 'b'; list->data[2] = '\0'; list->data[3] = '\0'; // end of data list->data[4] = '\0'; cl_assert_equal_i(3, string_list_count(list)); cl_assert_equal_s("ab", string_list_get_at(list, 0)); cl_assert_equal_s("", string_list_get_at(list, 1)); cl_assert_equal_s("", string_list_get_at(list, 2)); cl_assert_equal_s(NULL, string_list_get_at(list, 3)); // 4 strings (first and last two are empty) list->serialized_byte_length = 4; list->data[0] = '\0'; list->data[1] = 'b'; list->data[2] = '\0'; list->data[3] = '\0'; // end of data list->data[4] = '\0'; cl_assert_equal_i(4, string_list_count(list)); cl_assert_equal_s("", string_list_get_at(list, 0)); cl_assert_equal_s("b", string_list_get_at(list, 1)); cl_assert_equal_s("", string_list_get_at(list, 2)); cl_assert_equal_s("", string_list_get_at(list, 3)); // 2 strings (last is not terminated and will fall through) will return 2 strings // when deserializing, the deserializer puts a \0 at the end // this case demonstrates the problem with incorrectly initialized data list->serialized_byte_length = 3; list->data[0] = 'a'; list->data[1] = '\0'; list->data[2] = 'b'; // end of data list->data[3] = 'c'; list->data[4] = '\0'; cl_assert_equal_i(2, string_list_count(list)); cl_assert_equal_s("a", string_list_get_at(list, 0)); cl_assert_equal_s("bc", string_list_get_at(list, 1)); } static TimelineItemAction s_basic_action_list[] = { { .id = 0, .type = TimelineItemActionTypeGeneric }, { .id = 1, .type = TimelineItemActionTypeHttp }, { .id = 2, .type = TimelineItemActionTypeOpenPin }, }; void test_timeline_item__find_action_with_id(void) { // Make sure we're resilient to NULL items cl_assert_equal_p(timeline_item_find_action_with_id(NULL, 0), NULL); // Make sure we can handle timeline items with no actions TimelineItem item = {}; cl_assert_equal_p(timeline_item_find_action_with_id(&item, 0), NULL); // Make sure we actually find the items we're looking for item.action_group.num_actions = ARRAY_LENGTH(s_basic_action_list); item.action_group.actions = s_basic_action_list; cl_assert_equal_p(timeline_item_find_action_with_id(&item, 0), &s_basic_action_list[0]); cl_assert_equal_p(timeline_item_find_action_with_id(&item, 1), &s_basic_action_list[1]); cl_assert_equal_p(timeline_item_find_action_with_id(&item, 2), &s_basic_action_list[2]); cl_assert_equal_p(timeline_item_find_action_with_id(&item, 3), NULL); item.header.id = UUID_INVALID; cl_assert_equal_p(timeline_item_find_action_with_id(&item, 0), NULL); } void test_timeline_item__find_action_by_type(void) { // Make sure we're resilient to NULL items cl_assert_equal_p(timeline_item_find_action_by_type(NULL, TimelineItemActionTypeGeneric), NULL); // Make sure we can handle timeline items with no actions TimelineItem item = {}; cl_assert_equal_p(timeline_item_find_action_by_type(&item, TimelineItemActionTypeGeneric), NULL); // Make sure we actually find the items we're looking for item.action_group.num_actions = ARRAY_LENGTH(s_basic_action_list); item.action_group.actions = s_basic_action_list; cl_assert_equal_p(timeline_item_find_action_by_type(&item, TimelineItemActionTypeGeneric), &s_basic_action_list[0]); cl_assert_equal_p(timeline_item_find_action_by_type(&item, TimelineItemActionTypeHttp), &s_basic_action_list[1]); cl_assert_equal_p(timeline_item_find_action_by_type(&item, TimelineItemActionTypeOpenPin), &s_basic_action_list[2]); cl_assert_equal_p(timeline_item_find_action_by_type(&item, TimelineItemActionTypeRemove), NULL); item.header.id = UUID_INVALID; cl_assert_equal_p(timeline_item_find_action_by_type(&item, 0), NULL); } void test_timeline_item__find_dismiss_action(void) { // Make sure we're resilient to NULL items cl_assert_equal_p(timeline_item_find_dismiss_action(NULL), NULL); // Make sure we can handle timeline items with no actions TimelineItem item = {}; cl_assert_equal_p(timeline_item_find_dismiss_action(&item), NULL); // Copy the action list since it's easiest to just modify it TimelineItemAction *action_list = malloc(sizeof(s_basic_action_list)); memcpy(action_list, s_basic_action_list, sizeof(s_basic_action_list)); item.action_group.num_actions = ARRAY_LENGTH(s_basic_action_list); item.action_group.actions = action_list; // Make sure we don't return anything if the action doesn't exist cl_assert_equal_p(timeline_item_find_dismiss_action(&item), NULL); // Make sure we find both dismiss and ancs negative actions action_list[1].type = TimelineItemActionTypeDismiss; cl_assert_equal_p(timeline_item_find_dismiss_action(&item), &action_list[1]); action_list[1].type = TimelineItemActionTypeAncsNegative; cl_assert_equal_p(timeline_item_find_dismiss_action(&item), &action_list[1]); item.header.id = UUID_INVALID; cl_assert_equal_p(timeline_item_find_dismiss_action(&item), NULL); free(action_list); } void test_timeline_item__find_reply_action(void) { // Make sure we're resilient to NULL items cl_assert_equal_p(timeline_item_find_reply_action(NULL), NULL); // Make sure we can handle timeline items with no actions TimelineItem item = {}; cl_assert_equal_p(timeline_item_find_reply_action(&item), NULL); // Copy the action list since it's easiest to just modify it TimelineItemAction *action_list = malloc(sizeof(s_basic_action_list)); memcpy(action_list, s_basic_action_list, sizeof(s_basic_action_list)); item.action_group.num_actions = ARRAY_LENGTH(s_basic_action_list); item.action_group.actions = action_list; // Make sure we don't return anything if the action doesn't exist cl_assert_equal_p(timeline_item_find_reply_action(&item), NULL); // Make sure we find both response and ancs response actions action_list[1].type = TimelineItemActionTypeResponse; cl_assert_equal_p(timeline_item_find_reply_action(&item), &action_list[1]); action_list[1].type = TimelineItemActionTypeAncsResponse; cl_assert_equal_p(timeline_item_find_reply_action(&item), &action_list[1]); item.header.id = UUID_INVALID; cl_assert_equal_p(timeline_item_find_reply_action(&item), NULL); free(action_list); }