diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d8158..bac8ca6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,3 +55,9 @@ add_custom_target(docs COMMAND doxygen WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} ) + +add_custom_target(test + COMMAND ./run_tests.sh ${CMAKE_BINARY_DIR}/core ${CMAKE_SOURCE_DIR}/core.ini + DEPENDS core + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests +) diff --git a/endpoints/api_v1_schedules.c b/endpoints/api_v1_schedules.c index 6fa797f..7773aca 100644 --- a/endpoints/api_v1_schedules.c +++ b/endpoints/api_v1_schedules.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -24,9 +25,9 @@ api_v1_schedules_POST(struct mg_connection *c, endpoint_args_t *args, struct htt cJSON *json_name = cJSON_GetObjectItemCaseSensitive(json, "name"); if(!cJSON_IsString(json_name) || (json_name->valuestring == NULL)) { - char *error_msg = "ERROR: no name for schedule provided"; - mg_send_head(c, 400, strlen(error_msg), "Content-Type: text/plain"); - mg_printf(c, "%s", error_msg); + LOG_DEBUG("no name for schedule provided\n"); + mg_send_head(c, 400, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); cJSON_Delete(json); return; @@ -112,12 +113,12 @@ api_v1_schedules_POST(struct mg_connection *c, endpoint_args_t *args, struct htt if (json_str == NULL) { LOG_ERROR("failed to print schedule json\n"); - mg_send_head(c, 201, 2, "Content-Type: application/json"); + mg_send_head(c, 201, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); mg_printf(c, "{}"); } else { - mg_send_head(c, 201, strlen(json_str), "Content-Type: application/json"); + mg_send_head(c, 201, strlen(json_str), "Content-Type: application/json\r\n" STANDARD_HEADERS); mg_printf(c, "%s", json_str); free(json_str); } @@ -145,12 +146,12 @@ api_v1_schedules_GET(struct mg_connection *c, endpoint_args_t *args, struct http if (json_str == NULL) { LOG_ERROR("failed to print schedules json\n"); - mg_send_head(c, 500, 2, "Content-Type: application/json"); + mg_send_head(c, 500, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); mg_printf(c, "[]"); } else { - mg_send_head(c, 200, strlen(json_str), "Content-Type: application/json"); + mg_send_head(c, 200, strlen(json_str), "Content-Type: application/json\r\n" STANDARD_HEADERS); mg_printf(c, "%s", json_str); free(json_str); } diff --git a/endpoints/api_v1_schedules_STR.c b/endpoints/api_v1_schedules_STR.c index 40797cf..e1c2652 100644 --- a/endpoints/api_v1_schedules_STR.c +++ b/endpoints/api_v1_schedules_STR.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -8,47 +9,227 @@ void api_v1_schedules_STR_GET(struct mg_connection *c, endpoint_args_t *args, struct http_message *hm) { - (void)args; (void)hm; uuid_t target_uid; if(schedule_uid_parse(args[0].value.v_str, target_uid)) { LOG_ERROR("failed to unparse uid\n"); - mg_send_head(c, 400, 2, "Content-Type: application/json"); + mg_send_head(c, 400, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); mg_printf(c, "{}"); return; } - char debug_str[40]; - uuid_unparse(target_uid, debug_str); - LOG_DEBUG("uid: %s\n", debug_str); + schedule_t* schedule = schedule_get_by_uid(target_uid); - schedule_t** all_schedules = schedule_get_all(); - - cJSON *json = cJSON_CreateArray(); - - for(int i = 0; all_schedules[i] != NULL; ++i) + if(!schedule) { - cJSON *json_schedule = schedule_to_json(all_schedules[i]); - - cJSON_AddItemToArray(json, json_schedule); + LOG_ERROR("could not find a schedule for uid '%s'\n", args[0].value.v_str); + mg_send_head(c, 404, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; } + cJSON *json = schedule_to_json(schedule); + char *json_str = cJSON_Print(json); if (json_str == NULL) { LOG_ERROR("failed to print schedules json\n"); - mg_send_head(c, 500, 2, "Content-Type: application/json"); + mg_send_head(c, 500, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); mg_printf(c, "[]"); } else { - mg_send_head(c, 200, strlen(json_str), "Content-Type: application/json"); + mg_send_head(c, 200, strlen(json_str), "Content-Type: application/json\r\n" STANDARD_HEADERS); mg_printf(c, "%s", json_str); free(json_str); } cJSON_Delete(json); - schedule_free_list(all_schedules); + schedule_free(schedule); } +void +api_v1_schedules_STR_PUT(struct mg_connection *c, endpoint_args_t *args, struct http_message *hm) +{ + (void)hm; + + uuid_t target_uid; + if(schedule_uid_parse(args[0].value.v_str, target_uid)) + { + LOG_ERROR("failed to unparse uid\n"); + mg_send_head(c, 400, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; + } + + schedule_t* schedule = schedule_get_by_uid(target_uid); + + if(!schedule) + { + LOG_ERROR("could not find a schedule for uid '%s'\n", args[0].value.v_str); + mg_send_head(c, 404, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; + } + + cJSON *json = cJSON_ParseWithLength(hm->body.p, hm->body.len); + + if(json == NULL) + { + const char *error_ptr = cJSON_GetErrorPtr(); + if (error_ptr != NULL) + { + LOG_ERROR("error before: %s\n", error_ptr); + } + cJSON_Delete(json); + } + + cJSON *json_name = cJSON_GetObjectItemCaseSensitive(json, "name"); + if(cJSON_IsString(json_name) && json_name->valuestring) + { + strncpy(schedule->name, json_name->valuestring, MAX_NAME_LENGTH); + schedule->name[MAX_NAME_LENGTH] = '\0'; + } + + if(!schedule_is_protected(schedule)) + { + cJSON *json_period; + cJSON *json_periods = cJSON_GetObjectItemCaseSensitive(json, "periods"); + + int periods_count = cJSON_GetArraySize(json_periods); + free(schedule->periods); + schedule->periods = malloc(sizeof(period_t) * periods_count); + + int periods_valid = 0; + + cJSON_ArrayForEach(json_period, json_periods) + { + cJSON *json_period_start = cJSON_GetObjectItemCaseSensitive(json_period, "start"); + cJSON *json_period_end = cJSON_GetObjectItemCaseSensitive(json_period, "end"); + + if(!cJSON_IsString(json_period_start) || (json_period_start->valuestring == NULL)) + { + LOG_DEBUG("period is missing start\n"); + continue; + } + if(!cJSON_IsString(json_period_end) || (json_period_end->valuestring == NULL)) + { + LOG_DEBUG("period is missing end\n"); + continue; + } + + uint16_t start; + uint16_t end; + if(period_helper_parse_hhmm(json_period_start->valuestring, &start)) + { + LOG_DEBUG("couldn't parse start '%s'\n", json_period_start->valuestring); + continue; + } + if(period_helper_parse_hhmm(json_period_end->valuestring, &end)) + { + LOG_DEBUG("couldn't parse end '%s'\n", json_period_end->valuestring); + continue; + } + + schedule->periods[periods_valid].start = start; + schedule->periods[periods_valid].end = end; + ++periods_valid; + } + + schedule->periods_count = periods_valid; + } + + if(schedule_save(schedule)) + { + LOG_ERROR("failed to save schedule\n"); + mg_send_head(c, 500, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + free(schedule); + cJSON_Delete(json); + return; + } + + junction_tag_remove_for_schedule(schedule->id); + cJSON *json_tag; + cJSON *json_tags = cJSON_GetObjectItemCaseSensitive(json, "tags"); + cJSON_ArrayForEach(json_tag, json_tags) + { + if(!cJSON_IsString(json_tag) || (json_tag->valuestring == NULL)) + { + LOG_DEBUG("invalid tag in tags\n"); + continue; + } + const char *tag = json_tag->valuestring; + int tag_id = tag_get_id(tag); + if(tag_id == 0) + { + tag_save(tag_id, tag); + tag_id = tag_get_id(tag); + } + junction_tag_insert(tag_id, 0, schedule->id); + } + + cJSON_Delete(json); + json = schedule_to_json(schedule); + + char *json_str = cJSON_Print(json); + if (json_str == NULL) + { + LOG_ERROR("failed to print schedule json\n"); + mg_send_head(c, 200, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + } + else + { + mg_send_head(c, 200, strlen(json_str), "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "%s", json_str); + free(json_str); + } + cJSON_Delete(json); + schedule_free(schedule); +} + +void +api_v1_schedules_STR_DELETE(struct mg_connection *c, endpoint_args_t *args, struct http_message *hm) +{ + (void)hm; + + char *target_uid_str = args[0].value.v_str; + + uuid_t target_uid; + if(schedule_uid_parse(target_uid_str, target_uid)) + { + LOG_ERROR("failed to unparse uid\n"); + mg_send_head(c, 400, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; + } + + schedule_t* schedule = schedule_get_by_uid(target_uid); + + if(schedule_is_protected(schedule)) + { + mg_send_head(c, 403, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; + } + + if(!schedule) + { + LOG_ERROR("could not find a schedule for uid '%s'\n", target_uid_str); + mg_send_head(c, 404, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; + } + + if(schedule_remove(schedule)) + { + mg_send_head(c, 500, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; + } + mg_send_head(c, 200, 2, "Content-Type: application/json\r\n" STANDARD_HEADERS); + mg_printf(c, "{}"); + return; +} diff --git a/include/constants.h b/include/constants.h index 329868c..ceb06db 100644 --- a/include/constants.h +++ b/include/constants.h @@ -28,4 +28,6 @@ #define PIFACE_GPIO_BASE 200 +#define STANDARD_HEADERS "Access-Control-Allow-Origin: *" + #endif /* CORE_CONTANTS_H */ diff --git a/include/endpoints/api_v1_schedules.h b/include/endpoints/api_v1_schedules.h index ca1e34b..85a2b10 100644 --- a/include/endpoints/api_v1_schedules.h +++ b/include/endpoints/api_v1_schedules.h @@ -12,4 +12,10 @@ api_v1_schedules_GET(struct mg_connection *c, endpoint_args_t *args, struct http void api_v1_schedules_STR_GET(struct mg_connection *c, endpoint_args_t *args, struct http_message *hm); +void +api_v1_schedules_STR_PUT(struct mg_connection *c, endpoint_args_t *args, struct http_message *hm); + +void +api_v1_schedules_STR_DELETE(struct mg_connection *c, endpoint_args_t *args, struct http_message *hm); + #endif /* CORE_ENDPOINTS_API_V1_SCHEDULES_H */ diff --git a/include/logger.h b/include/logger.h index 7ea5019..10a2553 100644 --- a/include/logger.h +++ b/include/logger.h @@ -6,7 +6,12 @@ #include #include -#include + +#ifndef SOURCE_PATH_SIZE + #define SOURCE_PATH_SIZE 0 +#endif + +#define __FILENAME__ (__FILE__ + SOURCE_PATH_SIZE) void logger_log(FILE *stream, log_level_t level, const char *filename, int line, const char *func, const char *msg, ...); diff --git a/include/macros.h b/include/macros.h deleted file mode 100644 index 5105451..0000000 --- a/include/macros.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef CORE_MACROS_H -#define CORE_MACROS_H - -#include -#include - -#ifndef SOURCE_PATH_SIZE - #define SOURCE_PATH_SIZE 0 -#endif - -#define __FILENAME__ (__FILE__ + SOURCE_PATH_SIZE) - -#endif //CORE_MACROS_H diff --git a/include/models/schedule.h b/include/models/schedule.h index 72a56e6..8fd1517 100644 --- a/include/models/schedule.h +++ b/include/models/schedule.h @@ -22,6 +22,9 @@ schedule_save(schedule_t *schedule); int schedule_remove(schedule_t *schedule); +int +schedule_is_protected(schedule_t *schedule); + void schedule_free(schedule_t *schedule); @@ -46,6 +49,9 @@ schedule_get_by_id_or_off(int id); schedule_t* schedule_get_by_id(int id); +schedule_t* +schedule_get_by_uid(uuid_t uid); + schedule_t** schedule_get_all(); diff --git a/models/schedule.c b/models/schedule.c index 17d6666..8cb1049 100644 --- a/models/schedule.c +++ b/models/schedule.c @@ -96,7 +96,7 @@ schedule_db_select(sqlite3_stmt *stmt) } else { - LOG_ERROR("srror selecting schedules from database: %s\n", sqlite3_errstr(s)); + LOG_ERROR("error selecting schedules from database: %s\n", sqlite3_errstr(s)); break; } } @@ -139,6 +139,47 @@ schedule_save(schedule_t *schedule) return result; } +int +schedule_remove(schedule_t *schedule) +{ + sqlite3_stmt *stmt; + if(!schedule->id) + { + return 0; + } + + sqlite3_prepare_v2(global_database, "DELETE FROM schedules WHERE id=?1;", -1, &stmt, NULL); + sqlite3_bind_int(stmt, 1, schedule->id); + + int rc = sqlite3_step(stmt); + + sqlite3_finalize(stmt); + + return rc != SQLITE_DONE; +} + +int +schedule_is_protected(schedule_t *schedule) +{ + uuid_t tmp_uuid; + + memset(tmp_uuid, 0, sizeof(uuid_t)); + memcpy(tmp_uuid, "off", 3); + if(uuid_compare(schedule->uid, tmp_uuid) == 0) + { + return 1; + } + + memset(tmp_uuid, 0, sizeof(uuid_t)); + memcpy(tmp_uuid, "on", 2); + if(uuid_compare(schedule->uid, tmp_uuid) == 0) + { + return 1; + } + + return 0; +} + void schedule_free(schedule_t *schedule) { @@ -262,10 +303,41 @@ schedule_to_json(schedule_t *schedule) } cJSON_AddItemToObject(json, "tags", json_tags); - return json; } +schedule_t* +schedule_get_by_id(int id) +{ + sqlite3_stmt *stmt; + + sqlite3_prepare_v2(global_database, "SELECT * FROM schedules WHERE id = ?1;", -1, &stmt, NULL); + sqlite3_bind_int(stmt, 1, id); + + schedule_t **sql_result = schedule_db_select(stmt); + + schedule_t *result = sql_result[0]; + free(sql_result); + + return result; +} + +schedule_t* +schedule_get_by_uid(uuid_t uid) +{ + sqlite3_stmt *stmt; + + sqlite3_prepare_v2(global_database, "SELECT * FROM schedules WHERE uid = ?1;", -1, &stmt, NULL); + sqlite3_bind_blob(stmt, 1, uid, sizeof(uuid_t), SQLITE_STATIC); + + schedule_t **sql_result = schedule_db_select(stmt); + + schedule_t *result = sql_result[0]; + free(sql_result); + + return result; +} + schedule_t** schedule_get_all() { diff --git a/router.c b/router.c index af3bc29..f9ab403 100644 --- a/router.c +++ b/router.c @@ -30,9 +30,9 @@ static void endpoint_not_found_func(struct mg_connection *c, endpoint_args_t *args, struct http_message *hm) { (void)args; - mg_send_head(c, 404, hm->body.len, "Content-Type: text/plain"); - //mg_printf(c, "%.*s", (int)hm->message.len, hm->message.p); - mg_printf(c, "%.*s", (int)hm->body.len, hm->body.p); + (void)hm; + mg_send_head(c, 404, 9, "Content-Type: text/plain"); + mg_printf(c, "not found"); } void @@ -55,6 +55,8 @@ router_init() router_register_endpoint("/api/v1/schedules/", HTTP_METHOD_GET, api_v1_schedules_GET); router_register_endpoint("/api/v1/schedules/", HTTP_METHOD_POST, api_v1_schedules_POST); router_register_endpoint("/api/v1/schedules/{str}/", HTTP_METHOD_GET, api_v1_schedules_STR_GET); + router_register_endpoint("/api/v1/schedules/{str}/", HTTP_METHOD_PUT, api_v1_schedules_STR_PUT); + router_register_endpoint("/api/v1/schedules/{str}/", HTTP_METHOD_DELETE, api_v1_schedules_STR_DELETE); } void diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..10919ae --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh + +mkdir ./testing_tmp +cd ./testing_tmp + +cp $1 ./core +cp $2 ./core.ini + +./core start >/dev/null 2>&1 & +core_id=$! + +sleep 2; + +tavern-ci --tavern-beta-new-traceback .. +kill $core_id +cd .. +rm -r ./testing_tmp diff --git a/tests/test_get_all.tavern.yaml b/tests/test_get_all.tavern.yaml new file mode 100644 index 0000000..5cf3a9d --- /dev/null +++ b/tests/test_get_all.tavern.yaml @@ -0,0 +1,25 @@ +test_name: Test basic get all requests + +stages: + - name: "[test_get_all] get all schedules" + request: + url: "http://localhost:5000/api/v1/schedules/" + method: GET + response: + status_code: 200 + + - name: "[test_get_all] get all relays" + skip: True + request: + url: "http://localhost:5000/api/v1/relays/" + method: GET + response: + status_code: 200 + + - name: "[test_get_all] get all controllers" + skip: True + request: + url: "http://localhost:5000/api/v1/controllers/" + method: GET + response: + status_code: 200 diff --git a/tests/test_schedules_basic.tavern.yaml b/tests/test_schedules_basic.tavern.yaml new file mode 100644 index 0000000..26234d9 --- /dev/null +++ b/tests/test_schedules_basic.tavern.yaml @@ -0,0 +1,50 @@ +test_name: Test basic requests + +stages: + - name: "[test_schedules_basic] Make sure we get any response" + request: + url: "http://localhost:5000/api/v1/schedules/" + method: GET + response: + status_code: 200 + - name: "[test_schedules_basic] post schedule, expect it to be echoed back" + request: + method: POST + url: "http://localhost:5000/api/v1/schedules/" + json: + name: "hello" + periods: + - start: '00:10' + end: '00:20' + - start: '00:30' + end: '00:40' + - start: '00:50' + end: '01:00' + response: + status_code: 201 + body: + name: "{tavern.request_vars.json.name}" + save: + body: + returned_name: name + returned_id: id + - name: "[test_schedules_basic] get schedule, check name and some periods" + request: + method: GET + url: "http://localhost:5000/api/v1/schedules/{returned_id}" + response: + status_code: 200 + body: + name: "{returned_name}" + - name: "[test_schedules_basic] delete schedule" + request: + method: DELETE + url: "http://localhost:5000/api/v1/schedules/{returned_id}" + response: + status_code: 200 + - name: "[test_schedules_basic] get deleted schedule, expect 404" + request: + method: GET + url: "http://localhost:5000/api/v1/schedules/{returned_id}" + response: + status_code: 404 diff --git a/tests/test_schedules_protected.tavern.yaml b/tests/test_schedules_protected.tavern.yaml new file mode 100644 index 0000000..0a1a88b --- /dev/null +++ b/tests/test_schedules_protected.tavern.yaml @@ -0,0 +1,66 @@ +test_name: Test basic requests + +stages: + - name: "[test_schedules_protected] delete protected off schedule; expect forbidden/fail" + request: + method: DELETE + url: "http://localhost:5000/api/v1/schedules/off" + response: + status_code: 403 + - name: "[test_schedules_protected] get protected off schedule" + request: + method: GET + url: "http://localhost:5000/api/v1/schedules/off" + response: + status_code: 200 + body: + name: "off" + periods: [] + - name: "[test_schedules_protected] overwrite protected off schedule" + request: + method: PUT + url: "http://localhost:5000/api/v1/schedules/off" + json: + name: "turned_off" + periods: + - start: "00:10" + end: "00:20" + response: + status_code: 200 + body: + name: "{tavern.request_vars.json.name}" + periods: [] + + - name: "[test_schedules_protected] delete protected on schedule; expect forbidden/fail" + request: + method: DELETE + url: "http://localhost:5000/api/v1/schedules/on" + response: + status_code: 403 + - name: get protected on schedule + request: + method: GET + url: "http://localhost:5000/api/v1/schedules/on" + response: + status_code: 200 + body: + name: "on" + periods: + - start: "00:00" + end: "23:59" + - name: "[test_schedules_protected] overwrite protected on schedule" + request: + method: PUT + url: "http://localhost:5000/api/v1/schedules/on" + json: + name: "turned_on" + periods: + - start: "16:10" + end: "17:20" + response: + status_code: 200 + body: + name: "{tavern.request_vars.json.name}" + periods: + - start: "00:00" + end: "23:59"