From 7dee85f011b50783da4aff0e96b33893c1600e3a Mon Sep 17 00:00:00 2001 From: Tomas Rogala Date: Fri, 4 Dec 2020 07:33:54 +0100 Subject: [PATCH] [EGD-4711] Change calendar events endpoints -Create providers object -Change endpoints data message structure to json object -Extend ical format validation -Extend service desktop tests for calendar endpoints --- .../calendarEvents/CalendarEventsHelper.cpp | 238 +++++++++++---- .../calendarEvents/CalendarEventsHelper.hpp | 4 + module-utils/ical/ParserICS.cpp | 249 +++++++++++++--- module-utils/ical/ParserICS.hpp | 31 +- test/pytest/service-desktop/test_calendar.py | 274 ++++++++++++++++-- test/pytest/service-desktop/test_templates.py | 2 +- 6 files changed, 670 insertions(+), 128 deletions(-) diff --git a/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.cpp b/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.cpp index c8d0d282655d1d56d424152509e2e24eb72ef317..74c542c37b9e39270514f39399f5ada1f4e98f98 100644 --- a/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.cpp +++ b/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.cpp @@ -34,29 +34,74 @@ namespace parserFSM const inline auto two_days_before = Duration(0, 2, 0, 0); const inline auto one_week_before = Duration(1, 0, 0, 0); const inline auto event_time = Duration(0, 0, 0, 0); + const inline auto never = Duration(0, 0, 0, 0xFFFF); } // namespace duration } // namespace ical - namespace json::calendar::events + namespace json::calendar { - constexpr inline auto UID = "UID"; - constexpr inline auto data = "data"; + constexpr inline auto events = "calendar_events"; + namespace event + { + constexpr inline auto vevent = "VEVENT"; + + constexpr inline auto uid = "UID"; + constexpr inline auto summary = "SUMMARY"; + constexpr inline auto dtstart = "DTSTART"; + constexpr inline auto dtend = "DTEND"; + + namespace recurrence_rule + { + constexpr inline auto rrule = "RRULE"; + + constexpr inline auto frequency = "FREQ"; + constexpr inline auto count = "COUNT"; + constexpr inline auto interval = "INTERVAL"; + } // namespace recurrence_rule + + namespace alarm + { + constexpr inline auto valarm = "VALARM"; + + constexpr inline auto trigger = "TRIGGER"; + constexpr inline auto action = "ACTION"; + } // namespace alarm + + namespace provider + { + constexpr inline auto provider = "provider"; + + constexpr inline auto type = "type"; + constexpr inline auto id = "id"; + constexpr inline auto iCalUid = "iCalUid"; + } // namespace provider + } // namespace event constexpr inline auto offset = "offset"; constexpr inline auto limit = "limit"; constexpr inline auto count = "count"; constexpr inline auto total_count = "total_count"; - constexpr inline auto providers = "providers"; - namespace provider - { - constexpr inline auto type = "provider_type"; - constexpr inline auto id = "provider_id"; - constexpr inline auto iCalUid = "provider_iCalUid"; - } // namespace provider - } // namespace json::calendar::events + } // namespace json::calendar } // namespace parserFSM using namespace parserFSM; +auto CalendarEventsHelper::isICalEventValid(ICalEvent icalEvent) const -> bool +{ + if (!icalEvent.event.isValid) { + LOG_ERROR("Ical event invalid!"); + return false; + } + if (!icalEvent.alarm.isValid) { + LOG_ERROR("Ical alarm invalid!"); + return false; + } + if (!icalEvent.rrule.isValid) { + LOG_ERROR("Ical recurrence rule invalid!"); + return false; + } + return true; +} + auto CalendarEventsHelper::frequencyFromCustomRepeat(Repeat repeat) const -> Frequency { return Frequency(static_cast(repeat)); @@ -101,43 +146,44 @@ auto CalendarEventsHelper::alarmFrom(Reminder reminder) const -> Alarm { switch (reminder) { case Reminder::never: { - return Alarm(); + auto beforeEvent = ical::duration::never; + return Alarm(beforeEvent, Action::none); } case Reminder::five_min_before: { auto beforeEvent = ical::duration::five_minutes_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::fifteen_min_before: { auto beforeEvent = ical::duration::fifteen_minutes_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::thirty_min_before: { auto beforeEvent = ical::duration::thirty_minutes_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::one_hour_before: { auto beforeEvent = ical::duration::one_hour_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::two_hour_before: { auto beforeEvent = ical::duration::two_hours_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::one_day_before: { auto beforeEvent = ical::duration::one_day_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::two_days_before: { auto beforeEvent = ical::duration::two_days_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::one_week_before: { auto beforeEvent = ical::duration::one_week_before; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } case Reminder::event_time: { auto beforeEvent = ical::duration::event_time; - return Alarm(beforeEvent, Action::display); + return Alarm(beforeEvent, Action::none); } } return Alarm(); @@ -152,11 +198,43 @@ auto CalendarEventsHelper::icalEventFrom(const EventsRecord &record) const -> IC return ICalEvent{event, alarm, rrule}; } +auto CalendarEventsHelper::eventJsonObjectFrom(EventsRecord record) const -> json11::Json +{ + auto icalEvent = icalEventFrom(record); + if (!isICalEventValid(icalEvent)) { + LOG_ERROR("Bad event record formatting (Event UID: %s)", icalEvent.event.getUID().c_str()); + } + + auto rruleObj = json11::Json::object{ + {json::calendar::event::recurrence_rule::frequency, icalEvent.rrule.getFrequencyString().c_str()}, + {json::calendar::event::recurrence_rule::count, icalEvent.rrule.getCountString().c_str()}, + {json::calendar::event::recurrence_rule::interval, icalEvent.rrule.getIntervalString().c_str()}}; + + auto alarmObj = + json11::Json::object{{json::calendar::event::alarm::trigger, icalEvent.alarm.getTriggerString().c_str()}, + {json::calendar::event::alarm::action, icalEvent.alarm.getActionString().c_str()}}; + + auto providerObj = + json11::Json::object{{json::calendar::event::provider::type, record.provider_type.c_str()}, + {json::calendar::event::provider::id, record.provider_id.c_str()}, + {json::calendar::event::provider::iCalUid, record.provider_iCalUid.c_str()}}; + + auto eventObj = json11::Json::object{{json::calendar::event::dtend, icalEvent.event.getDTEndString().c_str()}, + {json::calendar::event::dtstart, icalEvent.event.getDTStartString().c_str()}, + {json::calendar::event::summary, icalEvent.event.getSummary().c_str()}, + {json::calendar::event::uid, icalEvent.event.getUID().c_str()}, + {json::calendar::event::recurrence_rule::rrule, rruleObj}, + {json::calendar::event::alarm::valarm, alarmObj}, + {json::calendar::event::provider::provider, providerObj}}; + + return eventObj; +} + auto CalendarEventsHelper::requestDataFromDB(Context &context) -> sys::ReturnCodes { - auto obj = context.getBody(); - uint32_t offset = obj[json::calendar::events::offset].int_value(); - uint32_t limit = obj[json::calendar::events::limit].int_value(); + const auto obj = context.getBody(); + uint32_t offset = obj[json::calendar::offset].int_value(); + uint32_t limit = obj[json::calendar::limit].int_value(); auto query = std::make_unique(offset, limit); auto listener = std::make_unique( @@ -166,17 +244,22 @@ auto CalendarEventsHelper::requestDataFromDB(Context &context) -> sys::ReturnCod uint32_t totalCount = EventsResult->getCountResult(); auto parser = std::make_unique(); std::vector icalEvents; + + auto eventsArray = json11::Json::array(); + for (auto rec : records) { - icalEvents.push_back(icalEventFrom(rec)); + auto eventObject = eventJsonObjectFrom(rec); + + eventsArray.emplace_back(eventObject); } - parser->importEvents(icalEvents); - auto jsonObj = - json11::Json::object({{json::calendar::events::data, parser->getIcsData()}, - {json::calendar::events::count, std::to_string(records.size())}, - {json::calendar::events::total_count, std::to_string(totalCount)}}); + + auto jsonObj = json11::Json::object({{json::calendar::events, eventsArray}, + {json::calendar::count, std::to_string(records.size())}, + {json::calendar::total_count, std::to_string(totalCount)}}); context.setResponseBody(jsonObj); MessageHandler::putToSendQueue(context.createSimpleResponse()); + return true; } return false; @@ -224,8 +307,7 @@ auto CalendarEventsHelper::repeatFrom(RecurrenceRule &rrule) const -> Repeat case Frequency::yearly: { return Repeat::yearly; } - case Frequency::invalid: { - LOG_ERROR("Frequency invalid"); + case Frequency::never: { return Repeat::never; } } @@ -249,26 +331,67 @@ auto CalendarEventsHelper::eventsRecordFrom(ICalEvent &icalEvent) const -> Event return record; } +auto CalendarEventsHelper::ICalEventFromJson(json11::Json eventObj) const -> ICalEvent +{ + ICalEvent icalEvent; + icalEvent.event.setUID(eventObj[json::calendar::event::uid].string_value()); + icalEvent.event.setSummary(eventObj[json::calendar::event::summary].string_value()); + icalEvent.event.setDTStart(eventObj[json::calendar::event::dtstart].string_value()); + icalEvent.event.setDTEnd(eventObj[json::calendar::event::dtend].string_value()); + + icalEvent.rrule.setFrequency( + eventObj[json::calendar::event::recurrence_rule::rrule][json::calendar::event::recurrence_rule::frequency] + .string_value()); + icalEvent.rrule.setCount( + eventObj[json::calendar::event::recurrence_rule::rrule][json::calendar::event::recurrence_rule::count] + .string_value()); + icalEvent.rrule.setInterval( + eventObj[json::calendar::event::recurrence_rule::rrule][json::calendar::event::recurrence_rule::interval] + .string_value()); + + icalEvent.alarm.setTrigger( + eventObj[json::calendar::event::alarm::valarm][json::calendar::event::alarm::trigger].string_value()); + icalEvent.alarm.setAction( + eventObj[json::calendar::event::alarm::valarm][json::calendar::event::alarm::action].string_value()); + + auto record = eventsRecordFrom(icalEvent); + + return icalEvent; +} + auto CalendarEventsHelper::createDBEntry(Context &context) -> sys::ReturnCodes { - auto parser = std::make_shared(); - parser->loadData(context.getBody()[json::calendar::events::data].string_value()); - auto icalEvents = parser->exportEvents(); + auto eventsJsonObj = context.getBody(); + auto eventsJsonArray = eventsJsonObj[json::calendar::events].array_items(); + bool ret = true; + for (auto event : eventsJsonArray) { - bool ret = true; - for (auto event : icalEvents) { - auto UID = event.event.getUID(); - auto record = eventsRecordFrom(event); - auto query = std::make_unique(record); + auto icalEvent = ICalEventFromJson(event); + if (!isICalEventValid(icalEvent)) { + context.setResponseStatus(http::Code::BadRequest); + MessageHandler::putToSendQueue(context.createSimpleResponse()); + return sys::ReturnCodes::Failure; + } + + auto record = eventsRecordFrom(icalEvent); + if (record.UID.empty()) { + record.UID = createUID(); + auto jsonObj = json11::Json::object({{json::calendar::event::uid, record.UID}}); + context.setResponseBody(jsonObj); + } + else { + LOG_ERROR("UID should not be recieved in put event endpoint. Recieved UID: %s", record.UID.c_str()); + context.setResponseStatus(http::Code::BadRequest); + MessageHandler::putToSendQueue(context.createSimpleResponse()); + return sys::ReturnCodes::Failure; + } + + auto query = std::make_unique(record); auto listener = std::make_unique( - [=](db::QueryResult *result, Context context) { + [&](db::QueryResult *result, Context context) { if (auto EventResult = dynamic_cast(result)) { - auto jsonObj = json11::Json::object( - {{json::calendar::events::data, parser->getIcsData()}, {json::calendar::events::UID, UID}}); - - context.setResponseBody(jsonObj); context.setResponseStatus(EventResult->getResult() ? http::Code::OK : http::Code::InternalServerError); @@ -295,14 +418,19 @@ auto CalendarEventsHelper::createDBEntry(Context &context) -> sys::ReturnCodes auto CalendarEventsHelper::updateDBEntry(Context &context) -> sys::ReturnCodes { - auto parser = std::make_unique(); - parser->loadData(context.getBody()[json::calendar::events::data].string_value()); - auto icalEvents = parser->exportEvents(); + auto eventsJsonObj = context.getBody(); bool ret = true; - for (auto event : icalEvents) { + for (auto event : eventsJsonObj[json::calendar::events].array_items()) { - auto record = eventsRecordFrom(event); + auto icalEvent = ICalEventFromJson(event); + if (!isICalEventValid(icalEvent) || icalEvent.event.getUID().empty()) { + context.setResponseStatus(http::Code::BadRequest); + MessageHandler::putToSendQueue(context.createSimpleResponse()); + return sys::ReturnCodes::Failure; + } + + auto record = eventsRecordFrom(icalEvent); auto query = std::make_unique(record); auto listener = std::make_unique( [](db::QueryResult *result, Context context) { @@ -331,7 +459,15 @@ auto CalendarEventsHelper::updateDBEntry(Context &context) -> sys::ReturnCodes auto CalendarEventsHelper::deleteDBEntry(Context &context) -> sys::ReturnCodes { - auto UID = context.getBody()[json::calendar::events::UID].string_value(); + auto UID = context.getBody()[json::calendar::event::uid].string_value(); + auto checkUID = Event(); + checkUID.setUID(UID); + if (!checkUID.isValid) { + LOG_ERROR("Wrong UID format. Provided UID: %s", UID.c_str()); + context.setResponseStatus(http::Code::BadRequest); + MessageHandler::putToSendQueue(context.createSimpleResponse()); + return sys::ReturnCodes::Failure; + } auto query = std::make_unique(UID); auto listener = std::make_unique( [=](db::QueryResult *result, Context context) { diff --git a/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.hpp b/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.hpp index 37eeb47eae2e7d8926cc9c4b218a4cee59b0174b..1a9716c9e5e1c1469730c976c2df89a21db793bd 100644 --- a/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.hpp +++ b/module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.hpp @@ -29,6 +29,10 @@ namespace parserFSM [[nodiscard]] auto repeatFrom(RecurrenceRule &rrule) const -> Repeat; [[nodiscard]] auto eventsRecordFrom(ICalEvent &icalEvent) const -> EventsRecord; + [[nodiscard]] auto eventJsonObjectFrom(EventsRecord record) const -> json11::Json; + [[nodiscard]] auto ICalEventFromJson(json11::Json eventObj) const -> ICalEvent; + [[nodiscard]] auto isICalEventValid(ICalEvent event) const -> bool; + public: CalendarEventsHelper(sys::Service *_ownerServicePtr) : DBHelper(_ownerServicePtr) {} diff --git a/module-utils/ical/ParserICS.cpp b/module-utils/ical/ParserICS.cpp index 9c96d3f99cd26a6f51dace879896abee2aed6e34..54bfe3d1361dbf10276826adedf45d721bfad23c 100644 --- a/module-utils/ical/ParserICS.cpp +++ b/module-utils/ical/ParserICS.cpp @@ -2,7 +2,8 @@ // For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md #include "ParserICS.hpp" -//#include +#include +#include namespace ical { @@ -72,6 +73,8 @@ namespace ical } // namespace frequency constexpr inline auto count = "COUNT="; constexpr inline auto interval = "INTERVAL="; + + constexpr inline auto max_interval = 2; } // namespace rrule namespace duration @@ -79,22 +82,44 @@ namespace ical constexpr inline auto minutesInHour = 60; constexpr inline auto minutesInDay = 24 * minutesInHour; constexpr inline auto minutesInWeek = 7 * minutesInDay; + + namespace minutes + { + constexpr inline auto never_happens_value = 0xFFFF; + } } // namespace duration + constexpr inline auto date_char_length = yearDigitsNumb + monthDigitsNumb + dayDigitsNumb; + constexpr inline auto time_char_length = HourDigitsNumb + MinDigitsNumb + SecDigitsNumb; + constexpr inline auto dt_char_length = date_char_length + 1 + time_char_length; } // namespace ical Duration::Duration(uint32_t week, uint32_t day, uint32_t hour, uint32_t minute) { + if (minute == ical::duration::minutes::never_happens_value) { + this->minute = ical::duration::minutes::never_happens_value; + return; + } this->week = week; this->day = day; this->hour = hour; this->minute = minute; + + if (!utils::time::validateTime(hour, minute, false)) { + isValid = false; + } } Duration::Duration(const std::string &property) { + if (property.empty()) { + LOG_DEBUG("Duration is empty. Event with no Alarm!"); + this->minute = ical::duration::minutes::never_happens_value; + return; + } uint32_t i = 0; - if (property.empty() || (property[0] != 'P' && property[1] != 'P')) { + if ((property[0] != 'P' && property[1] != 'P')) { LOG_ERROR("Duration constructor: Invalid format provided: %s", property.c_str()); + isValid = false; return; } while (property[i] != '\0' && i < property.length()) { @@ -118,14 +143,24 @@ Duration::Duration(const std::string &property) case 'M': this->minute = stoi(value); break; + default: + LOG_ERROR("Wrong duration unit value format"); + isValid = false; + break; } } catch (std::exception &e) { LOG_DEBUG("Duration conversion from string to int failed with exception:%s", e.what()); + isValid = false; } } ++i; } + + if (!utils::time::validateTime(hour, minute, false)) { + LOG_ERROR("Duration time value is invalid"); + isValid = false; + } } auto Duration::getDurationInMinutes() const -> uint32_t @@ -140,20 +175,23 @@ auto Duration::getDurationInMinutes() const -> uint32_t auto Duration::getDurationString() const -> std::string { std::string durationString; + if (minute == ical::duration::minutes::never_happens_value) { + return durationString; + } if (week) { - durationString += std::to_string(week) + 'W'; + durationString += utils::to_string(week) + 'W'; } if (day) { - durationString += std::to_string(day) + 'D'; + durationString += utils::to_string(day) + 'D'; } if (hour || minute) { durationString += 'T'; } if (hour) { - durationString += std::to_string(hour) + 'H'; + durationString += utils::to_string(hour) + 'H'; } if (minute) { - durationString += std::to_string(minute) + 'M'; + durationString += utils::to_string(minute) + 'M'; } if (week == 0 && day == 0 && hour == 0 && minute == 0) { durationString += "T0M"; @@ -161,13 +199,14 @@ auto Duration::getDurationString() const -> std::string return durationString; } -Alarm::Alarm(Duration &beforeEvent, Action action) +Alarm::Alarm(Duration &timeBeforeEvent, Action action) { - if (beforeEvent.getDurationString().empty()) { - LOG_ERROR("Got empty duration value"); + if (!timeBeforeEvent.isValid) { + LOG_ERROR("Duration format is invalid"); + isValid = false; return; } - this->trigger = beforeEvent; + this->trigger = timeBeforeEvent; this->action = action; } @@ -182,14 +221,25 @@ void Alarm::setAction(const std::string &action) else if (action == ical::alarm::value::action::audio) { this->action = Action::audio; } + else if (action.empty()) { + LOG_DEBUG("Alarm with no action"); + this->action = Action::none; + } else { - this->action = Action::invalid; + LOG_ERROR("Alarm action invalid"); + isValid = false; } } void Alarm::setTrigger(const std::string &duration) { - this->trigger = Duration(duration); + auto timeBeforeEvent = Duration(duration); + if (!timeBeforeEvent.isValid) { + LOG_ERROR("Duration format is invalid"); + isValid = false; + return; + } + this->trigger = timeBeforeEvent; } auto Alarm::getTriggerValue() const -> Duration @@ -204,7 +254,11 @@ auto Alarm::getActionValue() const -> Action auto Alarm::getTriggerString() const -> std::string { - return ical::alarm::value::before + trigger.getDurationString(); + auto durationString = trigger.getDurationString(); + if (durationString.empty()) { + return durationString; + } + return ical::alarm::value::before + durationString; } auto Alarm::getActionString() const -> std::string @@ -220,8 +274,8 @@ auto Alarm::getActionString() const -> std::string case Action::procedure: { return ical::alarm::value::action::procedure; } - case Action::invalid: { - LOG_ERROR("Alarm with no action"); + case Action::none: { + LOG_DEBUG("Alarm with no action"); return ""; } } @@ -230,11 +284,15 @@ auto Alarm::getActionString() const -> std::string RecurrenceRule::RecurrenceRule(Frequency freq, uint32_t count, uint32_t interval) { - if (count == 0 || interval == 0) { - LOG_ERROR("Invalid count or interval value!"); + if ((count == 0 || interval == 0) && freq != Frequency::never) { + LOG_ERROR("Invalid rrule format!"); + isValid = false; return; } - + if (interval > ical::rrule::max_interval) { + LOG_ERROR("Invalid interval value!"); + isValid = false; + } this->frequency = freq; this->count = count; this->interval = interval; @@ -254,9 +312,12 @@ void RecurrenceRule::setFrequency(const std::string &property) else if (property == ical::rrule::frequency::yearly) { this->frequency = Frequency::yearly; } + else if (property.empty()) { + this->frequency = Frequency::never; + } else { LOG_ERROR("Invalid frequency!"); - this->frequency = Frequency::invalid; + isValid = false; } } void RecurrenceRule::setCount(const std::string &property) @@ -265,7 +326,8 @@ void RecurrenceRule::setCount(const std::string &property) this->count = stoi(property); } catch (...) { - LOG_ERROR("Count value not conversionable to int!"); + LOG_ERROR("Count value is not an integer!"); + isValid = false; } } @@ -276,6 +338,7 @@ void RecurrenceRule::setInterval(const std::string &property) } catch (...) { LOG_ERROR("Interval value not conversionable to int!"); + isValid = false; } } @@ -314,8 +377,8 @@ auto RecurrenceRule::getFrequencyString() const -> std::string frequencyStr = ical::rrule::frequency::yearly; break; } - case Frequency::invalid: { - LOG_ERROR("Frequency invalid"); + case Frequency::never: { + LOG_DEBUG("Frequency never"); return ""; } } @@ -324,21 +387,12 @@ auto RecurrenceRule::getFrequencyString() const -> std::string auto RecurrenceRule::getCountString() const -> std::string { - if (this->frequency == Frequency::invalid) { - LOG_ERROR("Frequency value is invalid!"); - return ""; - } - return std::to_string(this->count); + return utils::to_string(this->count); } auto RecurrenceRule::getIntervalString() const -> std::string { - if (this->frequency == Frequency::invalid) { - LOG_ERROR("Frequency value is invalid!"); - return ""; - } - - return std::to_string(this->interval); + return utils::to_string(this->interval); } auto Event::getDateFromIcalFormat(const std::string &icalDateTime) const -> std::string @@ -346,21 +400,50 @@ auto Event::getDateFromIcalFormat(const std::string &icalDateTime) const -> std: return icalDateTime.substr(0, icalDateTime.find_first_of('T')); } +auto Event::getYearFromIcalDate(const std::string &icalDate) const -> std::string +{ + return icalDate.substr(0, yearDigitsNumb); +} + +auto Event::getMonthFromIcalDate(const std::string &icalDate) const -> std::string +{ + return icalDate.substr(yearDigitsNumb, monthDigitsNumb); +} + +auto Event::getDayFromIcalDate(const std::string &icalDate) const -> std::string +{ + return icalDate.substr(yearDigitsNumb + monthDigitsNumb, dayDigitsNumb); +} + auto Event::getTimeFromIcalFormat(const std::string &icalDateTime) const -> std::string { return icalDateTime.substr(icalDateTime.find_first_of('T') + 1); } +auto Event::getHourFromIcalTime(const std::string &icalTime) const -> std::string +{ + return icalTime.substr(0, HourDigitsNumb); +} + +auto Event::getMinutesFromIcalTime(const std::string &icalTime) const -> std::string +{ + return icalTime.substr(HourDigitsNumb, MinDigitsNumb); +} + +auto Event::getSecondsFromIcalTime(const std::string &icalTime) const -> std::string +{ + return icalTime.substr(HourDigitsNumb + MinDigitsNumb, SecDigitsNumb); +} + auto Event::dateStringFrom(const std::string &icalDate) const -> std::string { - return icalDate.substr(0, yearDigitsNumb) + "-" + icalDate.substr(yearDigitsNumb, monthDigitsNumb) + "-" + - icalDate.substr(yearDigitsNumb + monthDigitsNumb, dayDigitsNumb); + return getYearFromIcalDate(icalDate) + "-" + getMonthFromIcalDate(icalDate) + "-" + getDayFromIcalDate(icalDate); } auto Event::timeStringFrom(const std::string &icalTime) const -> std::string { - return icalTime.substr(0, HourDigitsNumb) + ":" + icalTime.substr(HourDigitsNumb, MinDigitsNumb) + ":" + - icalTime.substr(HourDigitsNumb + MinDigitsNumb, SecDigitsNumb); + return getHourFromIcalTime(icalTime) + ":" + getMinutesFromIcalTime(icalTime) + ":" + + getSecondsFromIcalTime(icalTime); } auto Event::TimePointFromIcalDate(const std::string &icalDateTime) const -> TimePoint @@ -387,28 +470,118 @@ auto Event::TimePointToIcalDate(const TimePoint &tp) const -> std::string return IcalDate; } +auto Event::isDate(const std::string &date) -> bool +{ + return utils::time::validateDate(getDayFromIcalDate(date), getMonthFromIcalDate(date), getYearFromIcalDate(date)); +} + +auto Event::isTime(const std::string &time) -> bool +{ + [[maybe_unused]] uint32_t seconds; + try { + seconds = stoi(getSecondsFromIcalTime(time)); + } + catch (...) { + LOG_ERROR("Seconds value is not an integer!"); + return false; + } + return utils::time::validateTime(getHourFromIcalTime(time), getMinutesFromIcalTime(time), false); +} + +auto Event::validateDT(const std::string &dt) -> bool +{ + uint32_t dateTimeSize; + if (dt.find_first_of('Z') == ical::date_char_length + ical::time_char_length) { + dateTimeSize = ical::dt_char_length + 1; + } + else { + dateTimeSize = ical::dt_char_length; + } + if (dt.size() != dateTimeSize) { + LOG_ERROR("Date time length is invalid"); + return false; + } + + auto separatorIndex = dt.find_first_of('T'); + LOG_DEBUG("Separator index = %d", (int)separatorIndex); + if (separatorIndex != (yearDigitsNumb + monthDigitsNumb + dayDigitsNumb)) { + LOG_ERROR("Date time separator is invalid"); + return false; + } + + auto date = getDateFromIcalFormat(dt); + auto time = getTimeFromIcalFormat(dt); + + if (!isDate(date)) { + LOG_ERROR("Date is invalid"); + return false; + } + + if (!isTime(time)) { + LOG_ERROR("Time is invalid"); + return false; + } + + return true; +} + +auto Event::validateUID(const std::string &UID) -> bool +{ + auto DTimestamp = UID.substr(0, UID.find_first_of('-')); + auto id = UID.substr(UID.find_first_of('-') + 1); + + try { + stoi(id); + } + catch (...) { + LOG_ERROR("UID value is not an integer!"); + return false; + } + + return validateDT(DTimestamp); +} + Event::Event(const std::string &summary, const TimePoint from, TimePoint till, const std::string &uid) { + if (summary.empty()) { + isValid = false; + } + if (!validateUID(uid) && !uid.empty()) { + isValid = false; + } + this->uid = uid; this->summary = summary; this->dtstart = from; this->dtend = till; - this->uid = uid; } void Event::setUID(const std::string &property) { + if (!validateUID(property) && !property.empty()) { + LOG_ERROR("UID invalid format"); + isValid = false; + } this->uid = property; } void Event::setSummary(const std::string &property) { + if (property.empty()) { + isValid = false; + } this->summary = property; } void Event::setDTStart(const std::string &property) { + if (!validateDT(property)) { + isValid = false; + } this->dtstart = TimePointFromIcalDate(property); } void Event::setDTEnd(const std::string &property) { + if (!validateDT(property)) { + isValid = false; + } this->dtend = TimePointFromIcalDate(property); } diff --git a/module-utils/ical/ParserICS.hpp b/module-utils/ical/ParserICS.hpp index 35aa47fc59fab43ce86e464765624b55d43457b8..0397fa4febe8f25c1d2911fb2bc395e1bc8521bf 100644 --- a/module-utils/ical/ParserICS.hpp +++ b/module-utils/ical/ParserICS.hpp @@ -17,6 +17,7 @@ class Duration uint32_t week = 0, day = 0, hour = 0, minute = 0; public: + bool isValid = true; Duration(uint32_t week = 0, uint32_t day = 0, uint32_t hour = 0, uint32_t minute = 0); explicit Duration(const std::string &property); @@ -26,7 +27,7 @@ class Duration enum class Action { - invalid, + none, audio, display, procedure @@ -35,11 +36,12 @@ enum class Action class Alarm { Duration trigger; - Action action; + Action action = Action::none; public: - Alarm() = default; - explicit Alarm(Duration &beforeEvent, Action action = Action::invalid); + bool isValid = true; + Alarm() = default; + explicit Alarm(Duration &beforeEvent, Action action = Action::none); void setAction(const std::string &action); void setTrigger(const std::string &duration); @@ -53,7 +55,7 @@ class Alarm enum class Frequency { - invalid, + never, daily, weekly, monthly, @@ -63,11 +65,12 @@ enum class Frequency class RecurrenceRule { - Frequency frequency = Frequency::invalid; + Frequency frequency = Frequency::never; uint32_t count = 0; uint32_t interval = 0; public: + bool isValid = true; RecurrenceRule() = default; RecurrenceRule(Frequency freq, uint32_t count, uint32_t interval); @@ -91,8 +94,21 @@ class Event TimePoint dtstart; TimePoint dtend; + auto isDate(const std::string &dt) -> bool; + auto isTime(const std::string &dt) -> bool; + auto validateDT(const std::string &dt) -> bool; + auto validateUID(const std::string &uid) -> bool; + [[nodiscard]] auto getDateFromIcalFormat(const std::string &icalDateTime) const -> std::string; + [[nodiscard]] auto getYearFromIcalDate(const std::string &icalDate) const -> std::string; + [[nodiscard]] auto getMonthFromIcalDate(const std::string &icalDate) const -> std::string; + [[nodiscard]] auto getDayFromIcalDate(const std::string &icalDate) const -> std::string; + [[nodiscard]] auto getTimeFromIcalFormat(const std::string &icalDateTime) const -> std::string; + [[nodiscard]] auto getHourFromIcalTime(const std::string &icalTime) const -> std::string; + [[nodiscard]] auto getMinutesFromIcalTime(const std::string &icalTime) const -> std::string; + [[nodiscard]] auto getSecondsFromIcalTime(const std::string &icalTime) const -> std::string; + [[nodiscard]] auto dateStringFrom(const std::string &icalDate) const -> std::string; [[nodiscard]] auto timeStringFrom(const std::string &icalTime) const -> std::string; @@ -100,7 +116,8 @@ class Event [[nodiscard]] auto TimePointToIcalDate(const TimePoint &tp) const -> std::string; public: - Event() = default; + bool isValid = true; + Event() = default; Event(const std::string &summary, TimePoint from, TimePoint till, const std::string &uid); void setUID(const std::string &property); diff --git a/test/pytest/service-desktop/test_calendar.py b/test/pytest/service-desktop/test_calendar.py index 0ea1249f91397dae2f6eb95838b236afb7bab22c..3d20f64e6dd5e44538772384a79386c00bc61226 100644 --- a/test/pytest/service-desktop/test_calendar.py +++ b/test/pytest/service-desktop/test_calendar.py @@ -2,41 +2,103 @@ # For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md import pytest from harness.interface.defs import status +import copy - -@pytest.mark.skip("ICS format bug") @pytest.mark.service_desktop_test def test_calendar(harness): - # add event - event = "BEGIN:VCALENDAR\nBEGIN:VEVENT\nSUMMARY:Testowy\nDTSTART:20200929T123611\nDTEND:20200929T124611\nBEGIN" \ - ":VALARM\nTRIGGER:-P5M\nEND:VALARM\nEND:VEVENT\nEND:VCALENDAR\n " + # add events + add_body = { + "calendar_events":[ + { + "UID":"", + "SUMMARY":"Testowy", + "DTSTART":"20200129T123600", + "DTEND":"20200129T124600", + "RRULE":{ + "COUNT":"0", + "FREQ":"", + "INTERVAL":"0" + }, + "VALARM":{ + "ACTION":"", + "TRIGGER":"" + }, + "provider":{ + "iCalUid":"", + "id":"", + "type":"" + } + } + ] + } + + ret = harness.endpoint_request("events", "put", add_body) + assert ret["status"] == status["OK"] - body = {"data": event} - ret = harness.endpoint_request("events", "put", body) + #add second event + second_event = copy.deepcopy(add_body) + second_event["calendar_events"][0]["SUMMARY"] = "Testowy2" + ret = harness.endpoint_request("events", "put", second_event) assert ret["status"] == status["OK"] # get all limited events - body = {"offset": 0, "limit": 1} - ret = harness.endpoint_request("events", "get", body) + get_body = {"offset": 0, "limit": 1} + ret = harness.endpoint_request("events", "get", get_body) + assert ret["status"] == status["OK"] - assert ret["body"]["data"] == event - assert ret["body"]["count"] == 1 + assert ret["body"]["count"] == '1' + assert ret["body"]["total_count"] == '2' + - uid_index = ret["body"]["data"].index('UID:') - summary_index = ret["body"]["data"].index('SUMMARY:') - UID = ret["body"]["data"][uid_index + 4:summary_index] + for event in ret["body"]["calendar_events"]: + assert event["SUMMARY"] == add_body["calendar_events"][0]["SUMMARY"] + assert event["RRULE"]["COUNT"] == add_body["calendar_events"][0]["RRULE"]["COUNT"] + assert event["RRULE"]["FREQ"] == add_body["calendar_events"][0]["RRULE"]["FREQ"] + assert event["RRULE"]["INTERVAL"] == add_body["calendar_events"][0]["RRULE"]["INTERVAL"] + assert event["VALARM"]["ACTION"] == add_body["calendar_events"][0]["VALARM"]["ACTION"] + assert event["VALARM"]["TRIGGER"] == add_body["calendar_events"][0]["VALARM"]["TRIGGER"] + assert event["provider"]["iCalUid"] == add_body["calendar_events"][0]["provider"]["iCalUid"] + assert event["provider"]["id"] == add_body["calendar_events"][0]["provider"]["id"] + assert event["provider"]["type"] == add_body["calendar_events"][0]["provider"]["type"] - assert "SUMMARY:Testowy" in ret["body"]["data"] - assert "DTSTART:20200929T123611" in ret["body"]["data"] - assert "DTEND:20200929T124611" in ret["body"]["data"] + # remove event + del_body = {"UID": ret["body"]["calendar_events"][0]["UID"]} + ret = harness.endpoint_request("events", "del", del_body) + assert ret["status"] == status["OK"] + + # check events after remove + body = {"offset": 0, "limit": 1} + ret = harness.endpoint_request("events", "get", body) + assert ret["status"] == status["OK"] assert ret["body"]["count"] == "1" # update event - event_update = "BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:" + UID + "\n" + "SUMMARY:Update\nDTSTART:20200928T123611" \ - "\nDTEND:20200928T124611\nEND:VEVENT\nEND" \ - ":VCALENDAR\n " - body = {"data": event_update} - ret = harness.endpoint_request("events", "post", body) + update_body = { + "calendar_events":[ + { + "UID":ret["body"]["calendar_events"][0]["UID"], + "SUMMARY":"TestowyUpdate", + "DTSTART":"20200929T123600", + "DTEND":"20200929T124600", + "RRULE":{ + "COUNT":"0", + "FREQ":"", + "INTERVAL":"0" + }, + "VALARM":{ + "ACTION":"", + "TRIGGER":"-PT5M" + }, + "provider":{ + "iCalUid":"", + "id":"", + "type":"" + } + } + ] + } + + ret = harness.endpoint_request("events", "post", update_body) assert ret["status"] == status["OK"] # get updated event @@ -44,20 +106,170 @@ def test_calendar(harness): ret = harness.endpoint_request("events", "get", body) assert ret["status"] == status["OK"] - assert ret["body"]["data"] == event_update - assert ret["body"]["count"] == 1 + for event in ret["body"]["calendar_events"]: + assert event["SUMMARY"] == update_body["calendar_events"][0]["SUMMARY"] + assert event["RRULE"]["COUNT"] == update_body["calendar_events"][0]["RRULE"]["COUNT"] + assert event["RRULE"]["FREQ"] == update_body["calendar_events"][0]["RRULE"]["FREQ"] + assert event["RRULE"]["INTERVAL"] == update_body["calendar_events"][0]["RRULE"]["INTERVAL"] + assert event["VALARM"]["ACTION"] == update_body["calendar_events"][0]["VALARM"]["ACTION"] + assert event["VALARM"]["TRIGGER"] == update_body["calendar_events"][0]["VALARM"]["TRIGGER"] + assert event["provider"]["iCalUid"] == update_body["calendar_events"][0]["provider"]["iCalUid"] + assert event["provider"]["id"] == update_body["calendar_events"][0]["provider"]["id"] + assert event["provider"]["type"] == update_body["calendar_events"][0]["provider"]["type"] - assert "SUMMARY:Update" in ret["body"]["data"] - assert "DTSTART:20200928T123611" in ret["body"]["data"] - assert "DTEND:20200928T124611" in ret["body"]["data"] assert ret["body"]["count"] == "1" - # remove event - body = {"UID": UID} - ret = harness.endpoint_request("events", "del", body) + + + # remove second event + body = {"offset": 0, "limit": 1} + ret = harness.endpoint_request("events", "get", body) + assert ret["status"] == status["OK"] + + del_body = {"UID": ret["body"]["calendar_events"][0]["UID"]} + ret = harness.endpoint_request("events", "del", del_body) assert ret["status"] == status["OK"] # check events after remove - body = {"data": "", "count": "1"} + body = {"offset": 0, "limit": 1} ret = harness.endpoint_request("events", "get", body) assert ret["status"] == status["OK"] + assert ret["body"]["count"] == "0" + + + + # INVALID FORMAT + add_body_invalid = copy.deepcopy(add_body) + update_body_invalid = copy.deepcopy(update_body) + + # add events invalid date and time formats + # wrong month + add_body_invalid["calendar_events"][0]["DTSTART"] = "20201329T123600" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # wrong day + add_body_invalid["calendar_events"][0]["DTSTART"] = "20201132T123600" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # wrong year + add_body_invalid["calendar_events"][0]["DTEND"] = "50201120T123600" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # wrong hour + add_body_invalid["calendar_events"][0]["DTSTART"] = "20201120T253600" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # wrong minute + add_body_invalid["calendar_events"][0]["DTEND"] = "20201122T126300" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # wrong second + add_body_invalid["calendar_events"][0]["DTSTART"] = "20201122T1200OO" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + #wrong separator + add_body_invalid["calendar_events"][0]["DTSTART"] = "20201122R120059" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # wrong length + add_body_invalid["calendar_events"][0]["DTSTART"] = "20201122T12550000" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + add_body_invalid["calendar_events"][0]["DTEND"] = "201122T125500" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # Summary invalid + add_body_invalid["calendar_events"][0]["SUMMARY"] = "" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + + # Alarm invalid + #trigger + add_body_invalid["calendar_events"][0]["VALARM"]["TRIGGER"] = "-PTM" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + add_body_invalid["calendar_events"][0]["VALARM"]["TRIGGER"] = "-PT4T" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + add_body_invalid["calendar_events"][0]["VALARM"]["TRIGGER"] = "-PT63M" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # Recurrency Rule invalid + # Freq + add_body_invalid["calendar_events"][0]["RRULE"]["FREQ"] = "DAIL" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # Count + add_body_invalid["calendar_events"][0]["RRULE"]["COUNT"] = "" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # interval + add_body_invalid["calendar_events"][0]["RRULE"]["COUNT"] = "" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + # UID invalid (UID should be empty in add event request) + add_body_invalid["calendar_events"][0]["UID"] = "20201122T125500-63" + ret = harness.endpoint_request("events", "put", add_body_invalid) + assert ret["status"] == status["BadRequest"] + add_body_invalid = copy.deepcopy(add_body) + + + # UPDATE INVALID + # UID invalid + update_body_invalid["calendar_events"][0]["UID"] = "" + ret = harness.endpoint_request("events", "post", update_body_invalid) + assert ret["status"] == status["BadRequest"] + update_body_invalid = copy.deepcopy(update_body_invalid) + + update_body_invalid["calendar_events"][0]["UID"] = "43526gsdgsa322" + ret = harness.endpoint_request("events", "post", update_body_invalid) + assert ret["status"] == status["BadRequest"] + update_body_invalid = copy.deepcopy(update_body_invalid) + + # REMOVE INVALID + # It is a valid UID, but not exist in any events in DB + # (handle it in db) + # del_body = {"UID": "20151122T125500-63"} + # + # ret = harness.endpoint_request("events", "post", del_body) + # assert ret["status"] == status["InternalServerError"] + + # UID invalid + del_body = {"UID": "20201122T125500-dadsadsada"} + ret = harness.endpoint_request("events", "del", del_body) + assert ret["status"] == status["BadRequest"] + + del_body = {"UID": "201122T125500-63"} + ret = harness.endpoint_request("events", "del", del_body) + assert ret["status"] == status["BadRequest"] + diff --git a/test/pytest/service-desktop/test_templates.py b/test/pytest/service-desktop/test_templates.py index 9b2d23ae461e81891c579e4c285f7a8638e5b369..ef29883a41c5e656a98a4a573a3a872f643164bb 100644 --- a/test/pytest/service-desktop/test_templates.py +++ b/test/pytest/service-desktop/test_templates.py @@ -59,4 +59,4 @@ def test_messages(harness): body = {"template": True, "count": True} ret = harness.endpoint_request("messages", "get", body) assert ret["status"] == status["OK"] - assert ret["body"]["count"] == count \ No newline at end of file + assert ret["body"]["count"] == count