diff --git a/n_const.c b/n_const.c index 3881d2f..41ce569 100644 --- a/n_const.c +++ b/n_const.c @@ -26,4 +26,5 @@ const char *c_cmd = "cmd"; const char *c_bad = "bad"; const char *c_iobad = "bad {io}"; const char *c_ioerr = "{io}"; +const char *c_unsupported = "{not-supported}"; const char *c_badbinerr = "{bad-bin}"; diff --git a/n_lib.h b/n_lib.h index 6551b70..60d59b7 100644 --- a/n_lib.h +++ b/n_lib.h @@ -189,6 +189,9 @@ extern const char *c_iobad; extern const char *c_ioerr; #define c_ioerr_len 4 +extern const char *c_unsupported; +#define c_unsupported_len 15 + extern const char *c_badbinerr; #define c_badbinerr_len 9 diff --git a/n_request.c b/n_request.c index 078bac9..9226d17 100644 --- a/n_request.c +++ b/n_request.c @@ -256,7 +256,7 @@ J *NoteRequestResponseWithRetry(J *req, uint32_t timeoutSeconds) rsp = NoteTransaction(req); // Loop if there is no response, or if there is an io error - if ( (rsp == NULL) || JContainsString(rsp, c_err, c_ioerr)) { + if ((rsp == NULL) || (JContainsString(rsp, c_err, c_ioerr) && !JContainsString(rsp, c_err, c_unsupported))) { // Free error response if (rsp != NULL) { @@ -278,10 +278,6 @@ J *NoteRequestResponseWithRetry(J *req, uint32_t timeoutSeconds) // Free the request JDelete(req); - if (rsp == NULL) { - return NULL; - } - // Return the response return rsp; } @@ -637,8 +633,8 @@ J *noteTransactionShouldLock(J *req, bool lockNotecard) bool isBadBin = false; bool isIoError = false; if (rsp != NULL) { - isBadBin = NoteErrorContains(JGetString(rsp, c_err), c_badbinerr); - isIoError = NoteErrorContains(JGetString(rsp, c_err), c_ioerr); + isBadBin = JContainsString(rsp, c_err, c_badbinerr); + isIoError = JContainsString(rsp, c_err, c_ioerr) && !JContainsString(rsp, c_err, c_unsupported); } else { // Failed to parse response as JSON if (responseJSON == NULL) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3caa539..9849431 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -59,6 +59,7 @@ endmacro(add_test) add_test(NoteTransaction_test) add_test(NoteRequest_test) add_test(NoteRequestResponse_test) +add_test(NoteRequestResponseWithRetry_test) add_test(NoteRequestResponseJSON_test) add_test(NoteRequestWithRetry_test) add_test(NoteReset_test) diff --git a/test/src/NoteRequestResponseWithRetry_test.cpp b/test/src/NoteRequestResponseWithRetry_test.cpp new file mode 100644 index 0000000..8a7c4af --- /dev/null +++ b/test/src/NoteRequestResponseWithRetry_test.cpp @@ -0,0 +1,229 @@ +/*! + * @file NoteRequestResponseWithRetry_test.cpp + * + * Written by the Blues Inc. team. + * + * Copyright (c) 2024 Blues Inc. MIT License. Use of this source code is + * governed by licenses granted by the copyright holder including that found in + * the + * LICENSE + * file. + * + */ + +#include +#include "fff.h" + +#include "n_lib.h" + +DEFINE_FFF_GLOBALS +FAKE_VALUE_FUNC(J *, NoteTransaction, J *) +FAKE_VALUE_FUNC(uint32_t, NoteGetMs) + +namespace +{ + +J *NoteTransactionIOError(J *) +{ + J *resp = JCreateObject(); + assert(resp != NULL); + JAddStringToObject(resp, c_err, c_ioerr); + + return resp; +} + +J *NoteTransactionNotSupportedError(J *) +{ + J *resp = JCreateObject(); + assert(resp != NULL); + JAddStringToObject(resp, c_err, c_unsupported); + + return resp; +} + +J *NoteTransactionIOAndNotSupportedErrors(J *) +{ + J *resp = JCreateObject(); + assert(resp != NULL); + JAddStringToObject(resp, c_err, "{io} {not-supported}"); + + return resp; +} + +J *NoteTransactionOtherError(J *) +{ + J *resp = JCreateObject(); + assert(resp != NULL); + JAddStringToObject(resp, c_err, c_bad); + + return resp; +} + +TEST_CASE("NoteRequestResponseWithRetry") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + J *rsp = NULL; + + GIVEN("A NULL request") { + J *req = NULL; + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, 0); + + THEN("The returned response is NULL") { + CHECK(rsp == NULL); + } + } + } + + GIVEN("A valid request") { + J *req = NoteNewRequest("note.add"); + REQUIRE(req != NULL); + + AND_GIVEN("A timeout of 5 seconds") { + const uint32_t timeoutSec = 5; + + AND_GIVEN("The timeout will trigger after 1 retry") { + uint32_t getMsReturnVals[3] = {0, 3000, 6000}; + SET_RETURN_SEQ(NoteGetMs, getMsReturnVals, 3); + + AND_GIVEN("The response to NoteTransaction is NULL") { + NoteTransaction_fake.return_val = NULL; + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, timeoutSec); + + THEN("The returned response is NULL") { + CHECK(rsp == NULL); + } + + THEN("NoteTransaction will be called twice (1 retry)") { + CHECK(NoteTransaction_fake.call_count == 2); + } + } + } + + AND_GIVEN("The response to NoteTransaction contains an I/O " + "error") { + NoteTransaction_fake.custom_fake = NoteTransactionIOError; + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, timeoutSec); + + THEN("The returned response is NULL") { + CHECK(rsp == NULL); + } + + THEN("NoteTransaction will be called twice (1 " + "retry)") { + CHECK(NoteTransaction_fake.call_count == 2); + } + } + } + + AND_GIVEN("The response to NoteTransaction contains a not " + "supported error") { + NoteTransaction_fake.custom_fake = NoteTransactionNotSupportedError; + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, timeoutSec); + + THEN("The returned response has a not-supported error") { + CHECK(JContainsString(rsp, c_err, c_unsupported)); + } + + THEN("NoteTransaction is only called once (no retries)") { + CHECK(NoteTransaction_fake.call_count == 1); + } + } + } + + AND_GIVEN("The response to NoteTransaction contains a not " + "supported error AND an I/O error") { + NoteTransaction_fake.custom_fake = NoteTransactionIOAndNotSupportedErrors; + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, timeoutSec); + + THEN("The returned response has a not-supported " + "error") { + CHECK(JContainsString(rsp, c_err, c_unsupported)); + } + + THEN("The returned response has an I/O error") { + CHECK(JContainsString(rsp, c_err, c_ioerr)); + } + + THEN("NoteTransaction is only called once (no " + "retries)") { + CHECK(NoteTransaction_fake.call_count == 1); + } + } + } + + AND_GIVEN("The response to NoteTransaction contains an error " + "that isn't I/O or \"not supported\"") { + NoteTransaction_fake.custom_fake = NoteTransactionOtherError; + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, timeoutSec); + + THEN("The returned response has the error") { + CHECK(JContainsString(rsp, c_err, c_bad)); + } + + THEN("NoteTransaction is only called once (no " + "retries)") { + CHECK(NoteTransaction_fake.call_count == 1); + } + } + } + + AND_GIVEN("There's a valid response on the first " + " NoteTransaction attempt") { + NoteTransaction_fake.return_val = JCreateObject(); + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, timeoutSec); + + THEN("The returned response is non-NULL") { + CHECK(rsp != NULL); + } + + THEN("NoteTranaction is only called once (no " + "retries)") { + CHECK(NoteTransaction_fake.call_count == 1); + } + } + } + + AND_GIVEN("There's a valid response on the second " + "NoteTransaction attempt") { + J *noteTransactionReturnVals[2] = {NULL, JCreateObject()}; + SET_RETURN_SEQ(NoteTransaction, noteTransactionReturnVals, + 2); + + WHEN("NoteRequestResponseWithRetry is called") { + rsp = NoteRequestResponseWithRetry(req, timeoutSec); + + THEN("The returned response is non-NULL") { + CHECK(rsp != NULL); + } + + THEN("NoteTranaction is only called twice (1 retry)") { + CHECK(NoteTransaction_fake.call_count == 2); + } + } + } + } + } + } + + JDelete(rsp); + + RESET_FAKE(NoteTransaction); + RESET_FAKE(NoteGetMs); +} + +} diff --git a/test/src/NoteTransaction_test.cpp b/test/src/NoteTransaction_test.cpp index a9e8794..9787c64 100644 --- a/test/src/NoteTransaction_test.cpp +++ b/test/src/NoteTransaction_test.cpp @@ -3,7 +3,7 @@ * * Written by the Blues Inc. team. * - * Copyright (c) 2023 Blues Inc. MIT License. Use of this source code is + * Copyright (c) 2024 Blues Inc. MIT License. Use of this source code is * governed by licenses granted by the copyright holder including that found in * the * LICENSE @@ -11,8 +11,6 @@ * */ - - #include #include "fff.h" @@ -68,6 +66,19 @@ const char *noteJSONTransactionIOError(const char *, size_t, char **resp, uint32 return NULL; } +const char *noteJSONTransactionNotSupportedError(const char *, size_t, char **resp, uint32_t) +{ + static char respString[] = "{\"err\": \"{not-supported}\"}"; + + if (resp) { + char* respBuf = reinterpret_cast(malloc(sizeof(respString))); + memcpy(respBuf, respString, sizeof(respString)); + *resp = respBuf; + } + + return NULL; +} + SCENARIO("NoteTransaction") { NoteSetFnDefault(malloc, free, NULL, NULL); @@ -214,6 +225,32 @@ SCENARIO("NoteTransaction") JDelete(resp); } + GIVEN("A valid request") { + J *req = NoteNewRequest("note.add"); + REQUIRE(req != NULL); + + AND_GIVEN("noteJSONTransaction returns a response with a \"not " + "supported\" error") { + noteJSONTransaction_fake.custom_fake = noteJSONTransactionNotSupportedError; + + WHEN("NoteTransaction is called") { + J *rsp = NoteTransaction(req); + + THEN("The response contains the \"not supported\" error") { + CHECK(JContainsString(rsp, c_err, c_unsupported)); + } + + THEN("noteJSONTransaction is only called once (no retries)") { + CHECK(noteJSONTransaction_fake.call_count == 1); + } + + JDelete(rsp); + } + } + + JDelete(req); + } + SECTION("A reset is required and it fails") { J *req = NoteNewRequest("note.add"); REQUIRE(req != NULL); @@ -407,5 +444,3 @@ SCENARIO("NoteTransaction") } } - -