/* * Test the storage API of the internal SMS Centre. * * (C) 2019 by Vadim Yanitskiy * * All Rights Reserved * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ #include #include #include #include #include #include #include #include #include #include #include /* Talloc context of this unit test */ static void *talloc_ctx = NULL; static const struct sms_tp_ud { /* Data Coding Scheme */ uint8_t dcs; /* TP User-Data-Length (depends on DCS) */ uint8_t length; /* Static TP User-Data filler (0 means disabled) */ uint8_t filler_byte; /* TP User-Data */ uint8_t data[GSM340_UDL_OCT_MAX]; /* Decoded text (for 7-bit default alphabet only) */ char dec_text[GSM340_UDL_SPT_MAX + 1]; } sms_tp_ud_set[] = { { .dcs = 0x00, /* Default GSM 7-bit alphabet */ .length = 9, /* in septets */ .dec_text = "Mahlzeit!", .data = { 0xcd, 0x30, 0x9a, 0xad, 0x2f, 0xa7, 0xe9, 0x21, }, }, { .dcs = 0x08, /* UCS-2 (16-bit) / UTF-16 */ .length = 120, /* in octets */ .data = { 0x04, 0x23, 0x04, 0x32, 0x04, 0x30, 0x04, 0x36, 0x04, 0x30, 0x04, 0x35, 0x04, 0x3c, 0x04, 0x4b, 0x04, 0x39, 0x00, 0x20, 0x04, 0x3a, 0x04, 0x3b, 0x04, 0x38, 0x04, 0x35, 0x04, 0x3d, 0x04, 0x42, 0x00, 0x21, 0x00, 0x20, 0x04, 0x1d, 0x04, 0x30, 0x04, 0x41, 0x04, 0x42, 0x04, 0x40, 0x04, 0x3e, 0x04, 0x39, 0x04, 0x3a, 0x04, 0x38, 0x00, 0x20, 0x00, 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x04, 0x38, 0x00, 0x20, 0x00, 0x4d, 0x00, 0x4d, 0x00, 0x53, 0x00, 0x20, 0x04, 0x31, 0x04, 0x43, 0x04, 0x34, 0x04, 0x43, 0x04, 0x42, 0x00, 0x20, 0x04, 0x34, 0x04, 0x3e, 0x04, 0x41, 0x04, 0x42, 0x04, 0x30, 0x04, 0x32, 0x04, 0x3b, 0x04, 0x35, 0x04, 0x3d, 0x04, 0x4b, 0x00, 0x2e, }, }, { .dcs = 0x04, /* 8-bit data */ .length = 12, /* in octets */ .data = { /* User-Data-Header */ 0x1e, /* Buffer-overflow! (should be 0x05) */ /* Concatenated SM, 8-bit reference number */ 0x00, 0x03, 0x5a, 0x05, 0x01, /* Dummy payload... */ 0x05, 0x04, 0x0b, 0x84, 0x0b, 0x84, }, }, { .dcs = 0x00, /* Default GSM 7-bit alphabet */ .length = 160, /* maximum, in septets */ .filler_byte = 0x41, }, { .dcs = 0x04, /* 8-bit data */ .length = 140, /* maximum, in octets */ .filler_byte = 0x42, }, { .dcs = 0x00, /* Default GSM 7-bit alphabet */ .length = 200, /* invalid, buffer overflow */ .filler_byte = 0x41, }, { .dcs = 0x04, /* 8-bit data */ .length = 0xff, /* invalid, buffer overflow */ .filler_byte = 0x42, }, }; #define SMS_ADDR(addr) \ { 0x00, 0x00, addr } static struct sms_test { /* Human-readable name of particular test message */ const char *name; /* Whether we expect db_sms_store() to fail */ bool exp_db_sms_store_fail; /* Whether we expect db_sms_get() to fail */ bool exp_db_sms_get_fail; /* SM TP-User-Data from sms_tp_ud_set[] */ const struct sms_tp_ud *ud; /* The message itself */ struct gsm_sms sms; } sms_test_set[] = { { .name = "Regular MO SMS", .sms = { .msg_ref = 0xde, .src = SMS_ADDR("123456"), .dst = SMS_ADDR("654321"), .validity_minutes = 10, .protocol_id = 0x00, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[0], }, { .name = "Regular MT SMS", .sms = { .msg_ref = 0xbe, .src = SMS_ADDR("654321"), .dst = SMS_ADDR("123456"), .validity_minutes = 180, .protocol_id = 0x00, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[1], }, { .name = "Complete TP-UD (160 septets, 7-bit encoding)", .sms = { .msg_ref = 0xee, .src = SMS_ADDR("266753837248772"), .dst = SMS_ADDR("266753837248378"), .validity_minutes = 360, .protocol_id = 0x00, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[3], }, { .name = "Complete TP-UD (140 octets, 8-bit encoding)", .sms = { .msg_ref = 0xee, .src = SMS_ADDR("266753838248772"), .dst = SMS_ADDR("266753838248378"), .validity_minutes = 360, .protocol_id = 0xaa, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[4], }, { .name = "TP-UD buffer overflow (UDH-Length > UD-Length)", .sms = { .msg_ref = 0x88, .src = SMS_ADDR("834568373569772"), .dst = SMS_ADDR("834568373569378"), .validity_minutes = 200, .protocol_id = 0xbb, .ud_hdr_ind = 0x01, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[2], }, { .name = "Truncated TP-UD (200 septets, 7-bit encoding)", .sms = { .msg_ref = 0xee, .src = { 0x01, 0x00, "8786228337248772" }, .dst = { 0x00, 0x01, "8786228337248378" }, .validity_minutes = 360, .protocol_id = 0xcc, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[5], }, { .name = "Truncated TP-UD (255 octets, 8-bit encoding)", .sms = { .msg_ref = 0xee, .src = { 0x01, 0x01, "8786228338248772" }, .dst = { 0xaa, 0xff, "8786228338248378" }, .validity_minutes = 360, .protocol_id = 0xbb, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[6], }, { .name = "Same MSISDN #1", .sms = { .msg_ref = 0x11, .src = SMS_ADDR("72631"), .dst = SMS_ADDR("72632"), .validity_minutes = 10, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[0], }, { .name = "Same MSISDN #2", .sms = { .msg_ref = 0x12, .src = SMS_ADDR("72632"), .dst = SMS_ADDR("72631"), .validity_minutes = 10, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[0], }, { .name = "Expired SMS", .sms = { .msg_ref = 0xde, .src = SMS_ADDR("3974733772"), .dst = SMS_ADDR("3974733378"), .validity_minutes = 0, /* SM TP-User-Data is taken from sms_tp_ud_set[] */ }, .ud = &sms_tp_ud_set[0], }, { .name = "Empty TP-UD", .sms = { .msg_ref = 0x38, .src = SMS_ADDR("3678983772"), .dst = SMS_ADDR("3678983378"), .validity_minutes = 450, .is_report = true, .reply_path_req = 0x01, .status_rep_req = 0x01, .protocol_id = 0x55, .data_coding_scheme = 0x08, .ud_hdr_ind = 0x00, .user_data_len = 0x00, /* No TP-User-Data */ }, .ud = NULL, }, }; static void prepare_sms_test_set(void) { int i; for (i = 0; i < ARRAY_SIZE(sms_test_set); i++) { struct sms_test *test = &sms_test_set[i]; const struct sms_tp_ud *ud = test->ud; /* ID auto-increment */ test->sms.id = i + 1; if (ud == NULL) continue; test->sms.data_coding_scheme = ud->dcs; test->sms.user_data_len = ud->length; if (ud->filler_byte) { memset(test->sms.user_data, ud->filler_byte, sizeof(test->sms.user_data)); } else { memcpy(test->sms.user_data, ud->data, sizeof(ud->data)); if (ud->dec_text[0] != '\0') strcpy(test->sms.text, ud->dec_text); } } } static void test_db_sms_store(void) { int rc, i; LOGP(DDB, LOGL_INFO, "Testing db_sms_store()...\n"); /* Store test SMS messages */ for (i = 0; i < ARRAY_SIZE(sms_test_set); i++) { struct sms_test *test = &sms_test_set[i]; LOGP(DDB, LOGL_NOTICE, "%s('%s'): ", __func__, test->name); rc = db_sms_store(&test->sms); if (!test->exp_db_sms_store_fail && rc == 0) LOGPC(DDB, LOGL_INFO, "success, as expected\n"); else if (test->exp_db_sms_store_fail && rc != 0) LOGPC(DDB, LOGL_INFO, "failure, as expected\n"); else LOGPC(DDB, LOGL_ERROR, "unexpected rc=%d\n", rc); } } static int verify_sms(const struct sms_test *test, const struct gsm_sms *sms) { int rc; LOGP(DDB, LOGL_NOTICE, "%s('%s'): ", __func__, test->name); #define MATCH_SMS_ADDR(ADDR) \ if (strcmp(sms->ADDR.addr, test->sms.ADDR.addr) \ || sms->ADDR.npi != test->sms.ADDR.npi \ || sms->ADDR.ton != test->sms.ADDR.ton) { \ LOGPC(DDB, LOGL_ERROR, #ADDR " address mismatch\n"); \ return -EINVAL; \ } MATCH_SMS_ADDR(src); MATCH_SMS_ADDR(dst); #define MATCH_SMS_PARAM(PARAM, FMT) \ if (sms->PARAM != test->sms.PARAM) { \ LOGPC(DDB, LOGL_ERROR, \ #PARAM " mismatch: E%" FMT " vs A%" FMT "\n", \ test->sms.PARAM, sms->PARAM); \ return -EINVAL; \ } MATCH_SMS_PARAM(id, "llu"); MATCH_SMS_PARAM(validity_minutes, "lu"); MATCH_SMS_PARAM(is_report, "i"); MATCH_SMS_PARAM(reply_path_req, PRIu8); MATCH_SMS_PARAM(status_rep_req, PRIu8); MATCH_SMS_PARAM(ud_hdr_ind, PRIu8); MATCH_SMS_PARAM(protocol_id, PRIu8); MATCH_SMS_PARAM(data_coding_scheme, PRIu8); MATCH_SMS_PARAM(msg_ref, PRIu8); MATCH_SMS_PARAM(user_data_len, PRIu8); /* Compare TP-User-Data */ rc = memcmp(sms->user_data, test->sms.user_data, sizeof(sms->user_data)); if (rc) { LOGPC(DDB, LOGL_ERROR, "TP-User-Data mismatch\n"); return -EINVAL; } /* Compare decoded text */ rc = strncmp(sms->text, test->sms.text, sizeof(sms->text)); if (rc) { LOGPC(DDB, LOGL_ERROR, "TP-User-Data (text) mismatch\n"); return -EINVAL; } LOGPC(DDB, LOGL_NOTICE, "match\n"); return 0; } static void test_db_sms_get(void) { struct gsm_sms *sms; int i; LOGP(DDB, LOGL_INFO, "Testing db_sms_get()...\n"); /* Retrieve stored SMS messages */ for (i = 0; i < ARRAY_SIZE(sms_test_set); i++) { const struct sms_test *test = &sms_test_set[i]; LOGP(DDB, LOGL_NOTICE, "%s('%s'): ", __func__, test->name); sms = db_sms_get(NULL, test->sms.id); if (!test->exp_db_sms_get_fail && sms != NULL) LOGPC(DDB, LOGL_INFO, "success, as expected\n"); else if (test->exp_db_sms_get_fail && sms == NULL) LOGPC(DDB, LOGL_INFO, "failure, as expected\n"); else LOGPC(DDB, LOGL_ERROR, "unexpected result\n"); if (sms) { verify_sms(test, sms); talloc_free(sms); } } } static void test_db_sms_delivery(void) { struct gsm_sms *sms1, *sms2; struct gsm_sms *sms; int rc; LOGP(DDB, LOGL_INFO, "Testing db_sms_get_next_unsent() " "and db_sms_mark_delivered()...\n"); /* Retrieve both #1 and #2 */ sms1 = db_sms_get_next_unsent(NULL, 1, 0); LOGP(DDB, LOGL_NOTICE, "db_sms_get_next_unsent(#1): %s\n", sms1 ? "found" : "not found"); if (sms1 != NULL) verify_sms(&sms_test_set[0], sms1); sms2 = db_sms_get_next_unsent(NULL, 2, 0); LOGP(DDB, LOGL_NOTICE, "db_sms_get_next_unsent(#2): %s\n", sms2 ? "found" : "not found"); if (sms2 != NULL) verify_sms(&sms_test_set[1], sms2); /* Mark both #1 and #2 and delivered, release memory */ if (sms1) { LOGP(DDB, LOGL_DEBUG, "Marking #%llu as delivered: ", sms1->id); rc = db_sms_mark_delivered(sms1); LOGPC(DDB, LOGL_DEBUG, "rc=%d\n", rc); talloc_free(sms1); } if (sms2) { LOGP(DDB, LOGL_DEBUG, "Marking #%llu as delivered: ", sms2->id); rc = db_sms_mark_delivered(sms2); LOGPC(DDB, LOGL_DEBUG, "rc=%d\n", rc); talloc_free(sms2); } /* Expect #3 as the next undelivered */ sms = db_sms_get_next_unsent(NULL, 1, 0); LOGP(DDB, LOGL_NOTICE, "db_sms_get_next_unsent(starting from #1): %s\n", sms ? "found" : "not found"); if (sms) { verify_sms(&sms_test_set[2], sms); talloc_free(sms); } } static void test_db_sms_delete(void) { int rc; LOGP(DDB, LOGL_INFO, "Testing db_sms_delete_sent_message_by_id()...\n"); /* Delete #1, which is marked as sent */ LOGP(DDB, LOGL_NOTICE, "db_sms_delete_sent_message_by_id(#1, sent): "); rc = db_sms_delete_sent_message_by_id(1); LOGPC(DDB, LOGL_NOTICE, "rc=%d\n", rc); /* Don't expect to retrieve this message anymore */ sms_test_set[0].exp_db_sms_get_fail = true; /* Try to delete #3, which is not marked as sent */ LOGP(DDB, LOGL_NOTICE, "db_sms_delete_sent_message_by_id(#3, not sent): "); rc = db_sms_delete_sent_message_by_id(3); LOGPC(DDB, LOGL_NOTICE, "rc=%d\n", rc); /* Do expect to retrieve this message anyway */ sms_test_set[2].exp_db_sms_get_fail = false; LOGP(DDB, LOGL_INFO, "Testing db_sms_delete_by_msisdn()...\n"); LOGP(DDB, LOGL_NOTICE, "db_sms_delete_by_msisdn('72631'): "); rc = db_sms_delete_by_msisdn("72631"); LOGPC(DDB, LOGL_NOTICE, "rc=%d\n", rc); /* Don't expect both #8 and #9 anymore */ sms_test_set[7].exp_db_sms_get_fail = true; sms_test_set[8].exp_db_sms_get_fail = true; LOGP(DDB, LOGL_INFO, "Testing db_sms_delete_oldest_expired_message()...\n"); LOGP(DDB, LOGL_NOTICE, "db_sms_delete_oldest_expired_message()\n"); db_sms_delete_oldest_expired_message(); /* Don't expect #10 anymore */ sms_test_set[9].exp_db_sms_get_fail = true; /* We need to make sure that we removed exactly what we expected to remove */ LOGP(DDB, LOGL_INFO, "Expectations updated, retrieving all messages again\n"); test_db_sms_get(); } static struct log_info_cat db_sms_test_categories[] = { [DDB] = { .name = "DDB", .description = "Database Layer", .enabled = 1, .loglevel = LOGL_DEBUG, }, }; static struct log_info info = { .cat = db_sms_test_categories, .num_cat = ARRAY_SIZE(db_sms_test_categories), }; int main(int argc, char **argv) { void *logging_ctx; int rc; /* Track the use of talloc NULL memory contexts */ talloc_enable_null_tracking(); talloc_ctx = talloc_named_const(NULL, 0, "db_sms_test"); logging_ctx = talloc_named_const(talloc_ctx, 0, "logging"); osmo_init_logging2(logging_ctx, &info); OSMO_ASSERT(osmo_stderr_target); log_set_use_color(osmo_stderr_target, 0); log_set_print_timestamp(osmo_stderr_target, 0); log_set_print_filename(osmo_stderr_target, 0); log_set_print_category(osmo_stderr_target, 1); log_set_print_level(osmo_stderr_target, 1); #if 0 /* Having the database stored in a regular file may be useful * for debugging, but this comes at the price of performance. */ FILE *dbf = fopen("db_sms_test.db", "wb"); OSMO_ASSERT(dbf != NULL); fclose(dbf); #endif /* Init a volatile database in RAM */ LOGP(DDB, LOGL_DEBUG, "Init a new database\n"); /* HACK: db_init() prints libdbi version using LOGL_NOTICE, so * the test output is not deterministic. Let's suppress this * message by increasing the log level to LOGL_ERROR. */ log_parse_category_mask(osmo_stderr_target, "DDB,7"); rc = db_init(":memory:"); OSMO_ASSERT(rc == 0); /* HACK: relax log level back to LOGL_DEBUG (see note above) */ log_parse_category_mask(osmo_stderr_target, "DDB,1"); /* Prepare some tables */ rc = db_prepare(); OSMO_ASSERT(rc == 0); LOGP(DDB, LOGL_DEBUG, "Init complete\n"); /* Prepare the test set */ prepare_sms_test_set(); test_db_sms_store(); test_db_sms_get(); test_db_sms_delivery(); test_db_sms_delete(); /* Close the database */ db_fini(); /* Deinit logging */ log_fini(); /* Check for memory leaks */ rc = talloc_total_blocks(talloc_ctx); OSMO_ASSERT(rc == 2); /* db_sms_test + logging */ talloc_free(talloc_ctx); talloc_report_full(NULL, stderr); talloc_disable_null_tracking(); return 0; }