diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..361a3c85 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "test/googletest"] + path = test/googletest + url = https://github.com/google/googletest.git +[submodule "test/arduino-mock"] + path = test/arduino-mock + url = https://github.com/ed7coyne/arduino-mock.git diff --git a/src/Firebase.cpp b/src/Firebase.cpp index 701fa0dd..08c2ebbe 100644 --- a/src/Firebase.cpp +++ b/src/Firebase.cpp @@ -15,17 +15,9 @@ // #include "Firebase.h" -// Detect whether stable version of HTTP library is installed instead of -// master branch and patch in missing status and methods. -#ifndef HTTP_CODE_TEMPORARY_REDIRECT -#define HTTP_CODE_TEMPORARY_REDIRECT 307 -#define USE_ESP_ARDUINO_CORE_2_0_0 -#endif +using std::unique_ptr; namespace { -const char* kFirebaseFingerprint = "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A"; -const uint16_t kFirebasePort = 443; - String makeFirebaseURL(const String& path, const String& auth) { String url; if (path[0] != '/') { @@ -41,7 +33,8 @@ String makeFirebaseURL(const String& path, const String& auth) { } // namespace Firebase::Firebase(const String& host) : host_(host) { - http_.setReuse(true); + http_.reset(FirebaseHttpClient::create()); + http_->setReuseConnection(true); } Firebase& Firebase::auth(const String& auth) { @@ -49,37 +42,66 @@ Firebase& Firebase::auth(const String& auth) { return *this; } +const String& Firebase::auth() { + return auth_; +} + FirebaseGet Firebase::get(const String& path) { - return FirebaseGet(host_, auth_, path, &http_); + return FirebaseGet(host_, auth_, path, http_.get()); +} + +unique_ptr Firebase::getPtr(const String& path) { + return unique_ptr(new FirebaseGet(host_, auth_, path, http_.get())); } FirebaseSet Firebase::set(const String& path, const String& value) { - return FirebaseSet(host_, auth_, path, value, &http_); + return FirebaseSet(host_, auth_, path, value, http_.get()); +} + +unique_ptr Firebase::setPtr(const String& path, + const String& value) { + return unique_ptr( + new FirebaseSet(host_, auth_, path, value, http_.get())); } FirebasePush Firebase::push(const String& path, const String& value) { - return FirebasePush(host_, auth_, path, value, &http_); + return FirebasePush(host_, auth_, path, value, http_.get()); +} +unique_ptr Firebase::pushPtr(const String& path, const String& value) { + return unique_ptr( + new FirebasePush(host_, auth_, path, value, http_.get())); } FirebaseRemove Firebase::remove(const String& path) { - return FirebaseRemove(host_, auth_, path, &http_); + return FirebaseRemove(host_, auth_, path, http_.get()); +} + +unique_ptr Firebase::removePtr(const String& path) { + return unique_ptr( + new FirebaseRemove(host_, auth_, path, http_.get())); } FirebaseStream Firebase::stream(const String& path) { // TODO: create new client dedicated to stream. - return FirebaseStream(host_, auth_, path, &http_); + return FirebaseStream(host_, auth_, path, http_.get()); +} + +unique_ptr Firebase::streamPtr(const String& path) { + // TODO: create new client dedicated to stream. + return unique_ptr( + new FirebaseStream(host_, auth_, path, http_.get())); } // FirebaseCall FirebaseCall::FirebaseCall(const String& host, const String& auth, const char* method, const String& path, - const String& data, HTTPClient* http) : http_(http) { - String url = makeFirebaseURL(path, auth); - http_->setReuse(true); - http_->begin(host, kFirebasePort, url, true, kFirebaseFingerprint); + const String& data, FirebaseHttpClient* http) : http_(http) { + String path_with_auth = makeFirebaseURL(path, auth); + http_->setReuseConnection(true); + http_->begin(host, path_with_auth); bool followRedirect = false; - if (method == "STREAM") { + if (String(method) == "STREAM") { method = "GET"; http_->addHeader("Accept", "text/event-stream"); followRedirect = true; @@ -90,26 +112,24 @@ FirebaseCall::FirebaseCall(const String& host, const String& auth, http_->collectHeaders(headers, 1); } - int status = http_->sendRequest(method, (uint8_t*)data.c_str(), data.length()); + int status = http_->sendRequest(method, data); // TODO: Add a max redirect check if (followRedirect) { - while (status == HTTP_CODE_TEMPORARY_REDIRECT) { + while (status == HttpStatus::TEMPORARY_REDIRECT) { String location = http_->header("Location"); - http_->setReuse(false); + http_->setReuseConnection(false); http_->end(); - http_->setReuse(true); - http_->begin(location, kFirebaseFingerprint); - status = http_->sendRequest("GET", (uint8_t*)NULL, 0); + http_->setReuseConnection(true); + http_->begin(location); + status = http_->sendRequest("GET", String()); } } if (status != 200) { -#ifdef USE_ESP_ARDUINO_CORE_2_0_0 - error_ = FirebaseError(status, String(method) + " " + url + ": " + status); -#else - error_ = FirebaseError(status, String(method) + " " + url + ": " + HTTPClient::errorToString(status)); -#endif + error_ = FirebaseError(status, + String(method) + " " + path_with_auth + + ": " + http_->errorToString(status)); } // if not streaming. @@ -128,14 +148,14 @@ const JsonObject& FirebaseCall::json() { // FirebaseGet FirebaseGet::FirebaseGet(const String& host, const String& auth, const String& path, - HTTPClient* http) + FirebaseHttpClient* http) : FirebaseCall(host, auth, "GET", path, "", http) { } // FirebaseSet FirebaseSet::FirebaseSet(const String& host, const String& auth, const String& path, const String& value, - HTTPClient* http) + FirebaseHttpClient* http) : FirebaseCall(host, auth, "PUT", path, value, http) { if (!error()) { // TODO: parse json @@ -145,24 +165,24 @@ FirebaseSet::FirebaseSet(const String& host, const String& auth, // FirebasePush FirebasePush::FirebasePush(const String& host, const String& auth, const String& path, const String& value, - HTTPClient* http) + FirebaseHttpClient* http) : FirebaseCall(host, auth, "POST", path, value, http) { if (!error()) { - name_ = json()["name"].as(); + //name_ = json()["name"].as(); } } // FirebasePush FirebaseRemove::FirebaseRemove(const String& host, const String& auth, const String& path, - HTTPClient* http) + FirebaseHttpClient* http) : FirebaseCall(host, auth, "DELETE", path, "", http) { } // FirebaseStream FirebaseStream::FirebaseStream(const String& host, const String& auth, const String& path, - HTTPClient* http) + FirebaseHttpClient* http) : FirebaseCall(host, auth, "STREAM", path, "", http) { } diff --git a/src/Firebase.h b/src/Firebase.h index 92ddea86..b511914c 100644 --- a/src/Firebase.h +++ b/src/Firebase.h @@ -21,9 +21,11 @@ #define firebase_h #include -#include -#include -#include +#include +#include "FirebaseHttpClient.h" +// TODO(edcoyne): move this into our mock_arduino fork where we actually do the +// override. +#define ARDUINO_STRING_OVERRIDE #include "third-party/arduino-json-5.1.1/include/ArduinoJson.h" class FirebaseGet; @@ -35,26 +37,41 @@ class FirebaseStream; // Firebase REST API client. class Firebase { public: - Firebase(const String& host); + explicit Firebase(const String& host); Firebase& auth(const String& auth); + virtual ~Firebase() = default; + + Firebase(const Firebase&) = delete; + + // Fetch auth string back. + const String& auth(); // Fetch json encoded `value` at `path`. FirebaseGet get(const String& path); + virtual std::unique_ptr getPtr(const String& path); // Set json encoded `value` at `path`. FirebaseSet set(const String& path, const String& json); + virtual std::unique_ptr setPtr(const String& path, const String& json); // Add new json encoded `value` to list at `path`. FirebasePush push(const String& path, const String& json); + virtual std::unique_ptr pushPtr(const String& path, const String& json); // Delete value at `path`. FirebaseRemove remove(const String& path); + virtual std::unique_ptr removePtr(const String& path); // Start a stream of events that affect value at `path`. FirebaseStream stream(const String& path); + virtual std::unique_ptr streamPtr(const String& path); + + protected: + // Used for testing. + Firebase() {} private: - HTTPClient http_; + std::unique_ptr http_; String host_; String auth_; }; @@ -77,20 +94,20 @@ class FirebaseCall { FirebaseCall() {} FirebaseCall(const String& host, const String& auth, const char* method, const String& path, - const String& data = "", - HTTPClient* http = NULL); - const FirebaseError& error() const { + const String& data = "", + FirebaseHttpClient* http = NULL); + virtual const FirebaseError& error() const { return error_; } - const String& response() { + virtual const String& response() { return response_; } const JsonObject& json(); protected: - HTTPClient* http_; + FirebaseHttpClient* http_; FirebaseError error_; String response_; DynamicJsonBuffer buffer_; @@ -100,7 +117,7 @@ class FirebaseGet : public FirebaseCall { public: FirebaseGet() {} FirebaseGet(const String& host, const String& auth, - const String& path, HTTPClient* http = NULL); + const String& path, FirebaseHttpClient* http = NULL); private: String json_; @@ -110,7 +127,8 @@ class FirebaseSet: public FirebaseCall { public: FirebaseSet() {} FirebaseSet(const String& host, const String& auth, - const String& path, const String& value, HTTPClient* http = NULL); + const String& path, const String& value, FirebaseHttpClient* http = NULL); + private: String json_; @@ -120,9 +138,9 @@ class FirebasePush : public FirebaseCall { public: FirebasePush() {} FirebasePush(const String& host, const String& auth, - const String& path, const String& value, HTTPClient* http = NULL); + const String& path, const String& value, FirebaseHttpClient* http = NULL); - const String& name() const { + virtual const String& name() const { return name_; } @@ -134,7 +152,7 @@ class FirebaseRemove : public FirebaseCall { public: FirebaseRemove() {} FirebaseRemove(const String& host, const String& auth, - const String& path, HTTPClient* http = NULL); + const String& path, FirebaseHttpClient* http = NULL); }; @@ -142,10 +160,10 @@ class FirebaseStream : public FirebaseCall { public: FirebaseStream() {} FirebaseStream(const String& host, const String& auth, - const String& path, HTTPClient* http = NULL); + const String& path, FirebaseHttpClient* http = NULL); // Return if there is any event available to read. - bool available(); + virtual bool available(); // Event type. enum Event { @@ -154,8 +172,21 @@ class FirebaseStream : public FirebaseCall { PATCH }; + static inline String EventToName(Event event) { + switch(event) { + case UNKNOWN: + return "UNKNOWN"; + case PUT: + return "PUT"; + case PATCH: + return "PATCH"; + default: + return "INVALID_EVENT_" + event; + } + } + // Read next json encoded `event` from stream. - Event read(String& event); + virtual Event read(String& event); const FirebaseError& error() const { return _error; diff --git a/src/FirebaseHttpClient.h b/src/FirebaseHttpClient.h new file mode 100644 index 00000000..326c7410 --- /dev/null +++ b/src/FirebaseHttpClient.h @@ -0,0 +1,41 @@ +#ifndef FIREBASE_HTTP_CLIENT_H +#define FIREBASE_HTTP_CLIENT_H + +#include "Arduino.h" +#include "Stream.h" + +struct HttpStatus { + static const int TEMPORARY_REDIRECT = 307; +}; + +class FirebaseHttpClient { + public: + static FirebaseHttpClient* create(); + + virtual void setReuseConnection(bool reuse) = 0; + virtual void begin(const String& url) = 0; + virtual void begin(const String& host, const String& path) = 0; + + virtual void end() = 0; + + virtual void addHeader(const String& name, const String& value) = 0; + virtual void collectHeaders(const char* header_keys[], + const int header_key_count) = 0; + virtual String header(const String& name) = 0; + + virtual int sendRequest(const String& method, const String& data) = 0; + + virtual String getString() = 0; + + virtual Stream* getStreamPtr() = 0; + + virtual String errorToString(int error_code) = 0; + + protected: + static const uint16_t kFirebasePort = 443; +}; + +static const String kFirebaseFingerprint = + "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A"; + +#endif // FIREBASE_HTTP_CLIENT_H diff --git a/src/FirebaseHttpClient_Esp8266.cpp b/src/FirebaseHttpClient_Esp8266.cpp new file mode 100644 index 00000000..145afa9a --- /dev/null +++ b/src/FirebaseHttpClient_Esp8266.cpp @@ -0,0 +1,75 @@ + +#include "FirebaseHttpClient.h" + +// The ordering of these includes matters greatly. +#include +#include +#include + +// Detect whether stable version of HTTP library is installed instead of +// master branch and patch in missing status and methods. +#ifndef HTTP_CODE_TEMPORARY_REDIRECT +#define HTTP_CODE_TEMPORARY_REDIRECT 307 +#define USE_ESP_ARDUINO_CORE_2_0_0 +#endif + +class FirebaseHttpClientEsp8266 : public FirebaseHttpClient { + public: + FirebaseHttpClientEsp8266() {} + + void setReuseConnection(bool reuse) override { + http_.setReuse(reuse); + } + + void begin(const String& url) override { + http_.begin(url, kFirebaseFingerprint); + } + + void begin(const String& host, const String& path) override { + http_.begin(host, kFirebasePort, path, true, kFirebaseFingerprint); + } + + void end() override { + http_.end(); + } + + void addHeader(const String& name, const String& value) override { + http_.addHeader(name, value); + } + + void collectHeaders(const char* header_keys[], const int count) override { + http_.collectHeaders(header_keys, count); + } + + String header(const String& name) override { + return http_.header(name.c_str()); + } + + int sendRequest(const String& method, const String& data) override { + return http_.sendRequest(method.c_str(), (uint8_t*)data.c_str(), data.length()); + } + + String getString() override { + return http_.getString(); + } + + Stream* getStreamPtr() override { + return http_.getStreamPtr(); + } + + String errorToString(int error_code) override { +#ifdef USE_ESP_ARDUINO_CORE_2_0_0 + return String(error_code); +#else + return HTTPClient::errorToString(error_code); +#endif + } + + private: + HTTPClient http_; +}; + +FirebaseHttpClient* FirebaseHttpClient::create() { + return new FirebaseHttpClientEsp8266(); +} + diff --git a/src/SerialTransceiver.h b/src/SerialTransceiver.h new file mode 100644 index 00000000..45ae91cf --- /dev/null +++ b/src/SerialTransceiver.h @@ -0,0 +1,3 @@ +#include "modem/SerialTransceiver.h" +// Bring them into the base namespace for easier use in arduino ide. +using firebase::modem::SerialTransceiver; diff --git a/src/modem/SerialTransceiver.cpp b/src/modem/SerialTransceiver.cpp new file mode 100644 index 00000000..a5947d46 --- /dev/null +++ b/src/modem/SerialTransceiver.cpp @@ -0,0 +1,61 @@ +#include "SerialTransceiver.h" + +namespace firebase { +namespace modem { + +void SerialTransceiver::begin(Stream* serial) { + std::unique_ptr fbase; + + in_.reset(new ArduinoInputStream(serial)); + out_.reset(new ArduinoOutputStream(serial)); +} + +void SerialTransceiver::loop() { + String command_name = in_->readStringUntil(' '); + + if (command_name.length() == 0 // Generally means a timeout has occured. + || command_name == "\n") { + return; + } + + if (command_name == "BEGIN") { + BeginCommand command; + if (command.execute(command_name, in_.get(), out_.get())) { + fbase_ = std::move(command.firebase()); + } + return; + } else if (!fbase_) { + in_->drain(); + out_->println("-FAIL Must call BEGIN before anything else."); + return; + } + + std::unique_ptr command = CreateCommand(command_name, fbase_.get()); + if (!command) { + in_->drain(); + out_->println(String("-FAIL Invalid command '") + command_name + "'." ); + return; + } + + command->execute(command_name, in_.get(), out_.get()); +} + +std::unique_ptr SerialTransceiver::CreateCommand(const String& text, + Firebase* fbase) { + std::unique_ptr command; + if (text == "GET") { + command.reset(new GetCommand(fbase)); + } else if (text == "SET") { + command.reset(new SetCommand(fbase)); + } else if (text == "PUSH") { + command.reset(new PushCommand(fbase)); + } else if (text == "REMOVE") { + command.reset(new RemoveCommand(fbase)); + } else if (text == "BEGIN_STREAM") { + command.reset(new StreamCommand(fbase)); + } + return command; +} + +} // modem +} // firebase diff --git a/src/modem/SerialTransceiver.h b/src/modem/SerialTransceiver.h new file mode 100644 index 00000000..f2b3f671 --- /dev/null +++ b/src/modem/SerialTransceiver.h @@ -0,0 +1,28 @@ +#ifndef MODEM_SERIAL_TRANSCIEVER_H +#define MODEM_SERIAL_TRANSCIEVER_H + +#include + +#include "Firebase.h" +#include "modem/commands.h" + +namespace firebase { +namespace modem { + +class SerialTransceiver { + public: + void begin(Stream* serial); + void loop(); + + private: + std::unique_ptr CreateCommand(const String& name, Firebase* fbase); + + std::unique_ptr fbase_; + std::unique_ptr in_; + std::unique_ptr out_; +}; + +} // modem +} // firebase + +#endif // MODEM_SERIAL_TRANSCIEVER_H diff --git a/src/modem/begin-command.cpp b/src/modem/begin-command.cpp new file mode 100644 index 00000000..4f7cb34d --- /dev/null +++ b/src/modem/begin-command.cpp @@ -0,0 +1,50 @@ +#include "modem/commands.h" + +namespace firebase { +namespace modem { + +bool BeginCommand::execute(const String& command, + InputStream* in, OutputStream* out) { + if (in == nullptr || out == nullptr) { + return false; + } + + if (command != "BEGIN") { + return false; + } + + String host; + String auth; + + String data(in->readLine()); + + int space_index = data.indexOf(' '); + if (space_index == -1) { + // host only. + host = data; + } else { + // host and auth. + host = data.substring(0, space_index); + auth = data.substring(space_index + 1); + } + + if (host.length() == 0) { + out->println("-FAIL Missing host"); + return false; + } + + new_firebase_.reset(new Firebase(host)); + if (auth.length() != 0) { + new_firebase_->auth(auth); + } + + out->println("+OK"); + return true; +} + +std::unique_ptr BeginCommand::firebase() { + return std::move(new_firebase_); +} + +} // modem +} // firebase diff --git a/src/modem/commands.h b/src/modem/commands.h new file mode 100644 index 00000000..77ff1053 --- /dev/null +++ b/src/modem/commands.h @@ -0,0 +1,79 @@ +#ifndef MODEM_COMMAND_H +#define MODEM_COMMAND_H + +#include "Firebase.h" +#include "modem/output-stream.h" +#include "modem/input-stream.h" + +namespace firebase { +namespace modem { + +class Command { + public: + Command(Firebase* fbase) : fbase_(fbase) {} + + // Execute command, reading any additional data needed from stream. + // Return false if execution failed. + virtual bool execute(const String& command, + InputStream* in, OutputStream* out) = 0; + protected: + Firebase& fbase() { + return *fbase_; + } + + private: + Firebase* fbase_; +}; + +class GetCommand : public Command { + public: + GetCommand(Firebase* fbase) : Command(fbase) {} + + bool execute(const String& command, InputStream* in, OutputStream* out); +}; + +class SetCommand : public Command { + public: + SetCommand(Firebase* fbase) : Command(fbase) {} + + bool execute(const String& command, InputStream* in, OutputStream* out); +}; + +class RemoveCommand : public Command { + public: + RemoveCommand(Firebase* fbase) : Command(fbase) {} + + bool execute(const String& command, InputStream* in, OutputStream* out); +}; + +class PushCommand : public Command { + public: + PushCommand(Firebase* fbase) : Command(fbase) {} + + bool execute(const String& command, InputStream* in, OutputStream* out); +}; + +class BeginCommand : public Command { + public: + BeginCommand() : Command(nullptr) {} + + bool execute(const String& command, InputStream* in, OutputStream* out); + + // This can only be called once. + std::unique_ptr firebase(); + + private: + std::unique_ptr new_firebase_; +}; + +class StreamCommand : public Command { + public: + StreamCommand(Firebase* fbase) : Command(fbase) {} + + bool execute(const String& command, InputStream* in, OutputStream* out); +}; + +} // modem +} // firebase + +#endif //MODEM_COMMAND_H diff --git a/src/modem/design.md b/src/modem/design.md new file mode 100644 index 00000000..fea7487b --- /dev/null +++ b/src/modem/design.md @@ -0,0 +1,10 @@ +Overview: + +![Design diagram](diagram.png) + +We create a SerialTransceiver object that will manage the serial connection. It will read the first word on a line to determine which command is being called and construct the appropriate Command subclass to handle the interaction. Commands will be short lived objects with no state between calls, they will own the serial connection until they hand it back and are responsible for handling all necessary input and leaving the input line in a clean state for the next command. + +We will aim to keep the Transceiver/Command interaction generic enough to enable creating additional Protocol Transceivers in the future. + +The Transceiver object will also own the Firebase object and present it to each command during construction. The Firebase object is our link to the firebase backend. + diff --git a/src/modem/diagram.png b/src/modem/diagram.png new file mode 100644 index 00000000..ea5407a9 Binary files /dev/null and b/src/modem/diagram.png differ diff --git a/src/modem/get-command.cpp b/src/modem/get-command.cpp new file mode 100644 index 00000000..0ee19299 --- /dev/null +++ b/src/modem/get-command.cpp @@ -0,0 +1,33 @@ +#include "modem/commands.h" + +namespace firebase { +namespace modem { + +bool GetCommand::execute(const String& command, + InputStream* in, OutputStream* out) { + if (in == nullptr || out == nullptr) { + return false; + } + + if (command != "GET") { + return false; + } + + String path = in->readLine(); + std::unique_ptr get(fbase().getPtr(path)); + + if (get->error()) { + out->print("-FAIL "); + out->println(get->error().message()); + return false; + } + + String value(get->response()); + // TODO implement json parsing to pull and process value. + out->print("+"); + out->println(value); + return true; +} + +} // modem +} // firebase diff --git a/src/modem/input-stream.h b/src/modem/input-stream.h new file mode 100644 index 00000000..9340552d --- /dev/null +++ b/src/modem/input-stream.h @@ -0,0 +1,51 @@ +#ifndef MODEM_INPUT_STREAM_H +#define MODEM_INPUT_STREAM_H + +#include + +namespace firebase { +namespace modem { + +class InputStream { + public: + virtual String readLine() = 0; + // This call consumes the terminator. + virtual String readStringUntil(const char terminator) = 0; + virtual void drain() = 0; + virtual bool available() = 0; +}; + +class ArduinoInputStream : public InputStream { + public: + ArduinoInputStream(Stream* stream) : stream_(stream) {} + String readLine() { + String out = stream_->readStringUntil('\n'); + if (out[out.length()-1] == '\r') { + out.remove(out.length()-1); + } + return out; + } + + String readStringUntil(const char terminator) { + return stream_->readStringUntil(terminator); + } + + void drain() { + while(stream_->available()) { + stream_->read(); + } + } + + bool available() { + return stream_->available(); + } + + private: + Stream* stream_; +}; + +} // modem +} // firebase + +#endif // MODEM_INPUT_STREAM_H + diff --git a/src/modem/json_util.h b/src/modem/json_util.h new file mode 100644 index 00000000..544120a1 --- /dev/null +++ b/src/modem/json_util.h @@ -0,0 +1,17 @@ +#ifndef MODEM_JSON_UTIL_H +#define MODEM_JSON_UTIL_H + +namespace firebase { +namespace modem { + +// TODO(edcoyne): We should use a json library to escape. +inline String EncodeForJson(String input) { + input.replace("\\", "\\\\"); + input.replace("\"", "\\\""); + return "\"" + input + "\""; +} + +} // modem +} // firebase + +#endif // MODEM_JSON_UTIL_H diff --git a/src/modem/output-stream.h b/src/modem/output-stream.h new file mode 100644 index 00000000..010adb6b --- /dev/null +++ b/src/modem/output-stream.h @@ -0,0 +1,39 @@ +#ifndef MODEM_OUTPUT_STREAM_H +#define MODEM_OUTPUT_STREAM_H + +#include +#include + +namespace firebase { +namespace modem { + +class OutputStream { + public: + virtual int println(const String& string) = 0; + virtual int println(const int value) = 0; + virtual int print(const String& string) = 0; +}; + +class ArduinoOutputStream : public OutputStream { + public: + ArduinoOutputStream(Stream* stream) : stream_(stream) {} + + int println(const String& string) override { + return stream_->println(string.c_str()); + } + + int println(const int value) override { + return stream_->println(value); + } + + int print(const String& string) override { + return stream_->print(string.c_str()); + } + private: + Stream* stream_; +}; + +} // modem +} // firebase + +#endif // MODEM_OUTPUT_STREAM diff --git a/src/modem/push-command.cpp b/src/modem/push-command.cpp new file mode 100644 index 00000000..73d0f7ef --- /dev/null +++ b/src/modem/push-command.cpp @@ -0,0 +1,34 @@ +#include "modem/commands.h" +#include "modem/json_util.h" + +namespace firebase { +namespace modem { + +bool PushCommand::execute(const String& command, + InputStream* in, OutputStream* out) { + if (in == nullptr || out == nullptr) { + return false; + } + + if (command != "PUSH") { + return false; + } + + String path(in->readStringUntil(' ')); + String data(in->readLine()); + + std::unique_ptr push( + fbase().pushPtr(path, EncodeForJson(data))); + + if (push->error()) { + out->print("-FAIL "); + out->println(push->error().message()); + return false; + } else { + out->println("+OK"); + return true; + } +} + +} // modem +} // firebase diff --git a/src/modem/remove-command.cpp b/src/modem/remove-command.cpp new file mode 100644 index 00000000..5026d25b --- /dev/null +++ b/src/modem/remove-command.cpp @@ -0,0 +1,30 @@ +#include "modem/commands.h" + +namespace firebase { +namespace modem { + +bool RemoveCommand::execute(const String& command, + InputStream* in, OutputStream* out) { + if (in == nullptr || out == nullptr) { + return false; + } + + if (command != "REMOVE") { + return false; + } + + String path = in->readLine(); + std::unique_ptr get(fbase().removePtr(path)); + + if (get->error()) { + out->print("-FAIL "); + out->println(get->error().message()); + return false; + } + + out->println("+OK"); + return true; +} + +} // modem +} // firebase diff --git a/src/modem/set-command.cpp b/src/modem/set-command.cpp new file mode 100644 index 00000000..35b53040 --- /dev/null +++ b/src/modem/set-command.cpp @@ -0,0 +1,34 @@ +#include "modem/commands.h" +#include "modem/json_util.h" + +namespace firebase { +namespace modem { + +bool SetCommand::execute(const String& command, + InputStream* in, OutputStream* out) { + if (in == nullptr || out == nullptr) { + return false; + } + + if (command != "SET") { + return false; + } + + String path(in->readStringUntil(' ')); + String data(in->readLine()); + + std::unique_ptr set(fbase().setPtr(path, + EncodeForJson(data))); + + if (set->error()) { + out->print("-FAIL "); + out->println(set->error().message()); + return false; + } else { + out->println("+OK"); + return true; + } +} + +} // modem +} // firebase diff --git a/src/modem/stream-command.cpp b/src/modem/stream-command.cpp new file mode 100644 index 00000000..b3b1c649 --- /dev/null +++ b/src/modem/stream-command.cpp @@ -0,0 +1,51 @@ +#include "modem/commands.h" + +namespace firebase { +namespace modem { + +bool StreamCommand::execute(const String& command, + InputStream* in, OutputStream* out) { + if (in == nullptr || out == nullptr) { + return false; + } + + if (command != "BEGIN_STREAM") { + return false; + } + + String path = in->readLine(); + std::unique_ptr stream(fbase().streamPtr(path)); + + if (stream->error()) { + out->print("-FAIL "); + out->println(stream->error().message()); + return false; + } + + bool running = true; + while(running) { + if (stream->available()) { + String json; + FirebaseStream::Event event = stream->read(json); + out->print("+"); + out->print(FirebaseStream::EventToName(event) + " "); + // TODO(edcoyne): add json parsing and get real path. + out->println("/dummy/path"); + out->println(json.length()); + out->println(json); + } else if (in->available()) { + String command = in->readLine(); + if (command == "END_STREAM") { + out->println("+OK"); + running = false; + } else { + out->println("-FAIL Cannot call any command but END_STREAM."); + } + } + } + + return true; +} + +} // modem +} // firebase diff --git a/src/third-party/arduino-json-5.1.1/include/ArduinoJson/Arduino/String.hpp b/src/third-party/arduino-json-5.1.1/include/ArduinoJson/Arduino/String.hpp index a11a6511..83cf6500 100644 --- a/src/third-party/arduino-json-5.1.1/include/ArduinoJson/Arduino/String.hpp +++ b/src/third-party/arduino-json-5.1.1/include/ArduinoJson/Arduino/String.hpp @@ -9,8 +9,11 @@ #ifndef ARDUINO +#ifndef ARDUINO_STRING_OVERRIDE +#define ARDUINO_STRING_OVERRIDE #include typedef std::string String; +#endif #else diff --git a/test/arduino-mock b/test/arduino-mock new file mode 160000 index 00000000..ff0f896a --- /dev/null +++ b/test/arduino-mock @@ -0,0 +1 @@ +Subproject commit ff0f896a815df02c08bcd7d99168cd3bcbf05b1a diff --git a/test/dummies/ESP8266HTTPClient.h b/test/dummies/ESP8266HTTPClient.h new file mode 100644 index 00000000..8533d9a6 --- /dev/null +++ b/test/dummies/ESP8266HTTPClient.h @@ -0,0 +1,2 @@ +// Need a placeholder for complilation for now. +class HTTPClient {}; diff --git a/test/dummies/FirebaseHttpClient_dummy.cpp b/test/dummies/FirebaseHttpClient_dummy.cpp new file mode 100644 index 00000000..e8a746e9 --- /dev/null +++ b/test/dummies/FirebaseHttpClient_dummy.cpp @@ -0,0 +1,53 @@ +#ifdef __GNUC__ +# define UNUSED_ARG(x) UNUSED_ ## x __attribute__((__unused__)) +#else +# define UNUSED_ARG(x) UNUSED_ ## x +#endif + +#include "FirebaseHttpClient.h" + +class FirebaseHttpClientDummy : public FirebaseHttpClient { + public: + void setReuseConnection(bool UNUSED_ARG(reuse)) override { + } + + void begin(const String& UNUSED_ARG(url)) override { + } + + void begin(const String& UNUSED_ARG(host), const String& UNUSED_ARG(path)) override { + } + + void end() override { + } + + void addHeader(const String& UNUSED_ARG(name), const String& UNUSED_ARG(value)) override { + } + + void collectHeaders(const char* UNUSED_ARG(header_keys[]), const int UNUSED_ARG(count)) override { + } + + String header(const String& UNUSED_ARG(name)) override { + return ""; + } + + int sendRequest(const String& UNUSED_ARG(method), const String& UNUSED_ARG(data)) override { + return 0; + } + + String getString() override { + return ""; + } + + Stream* getStreamPtr() override { + return nullptr; + } + + String errorToString(int UNUSED_ARG(error_code)) override { + return String(); + } +}; + +FirebaseHttpClient* FirebaseHttpClient::create() { + return new FirebaseHttpClientDummy(); +} + diff --git a/test/dummies/Stream.h b/test/dummies/Stream.h new file mode 100644 index 00000000..8c5f427e --- /dev/null +++ b/test/dummies/Stream.h @@ -0,0 +1,34 @@ +#ifndef TEST_DUMMIES_STREAM_H +#define TEST_DUMMIES_STREAM_H + +#include "Arduino.h" + +class Stream { + public: + int available() { + return 0; + } + String readStringUntil(const char term __attribute__((unused))) { + return String(); + } + int println(const String&) { + return 0; + } + int println(const char*) { + return 0; + } + int println(int) { + return 0; + } + int print(const char*) { + return 0; + } + char peek() { + return '\0'; + } + char read() { + return '\0'; + } +}; + +#endif // TEST_DUMMIES_STREAM_H diff --git a/test/googletest b/test/googletest new file mode 160000 index 00000000..ff07a5de --- /dev/null +++ b/test/googletest @@ -0,0 +1 @@ +Subproject commit ff07a5de0e81580547f1685e101194ed1a4fcd56 diff --git a/test/mock-firebase.h b/test/mock-firebase.h new file mode 100644 index 00000000..3d788e69 --- /dev/null +++ b/test/mock-firebase.h @@ -0,0 +1,52 @@ +#ifndef TEST_MOCK_FIREBASE_H +#define TEST_MOCK_FIREBASE_H + +#include +#include "gtest/gtest.h" +#include "Firebase.h" + +namespace firebase { +namespace modem { + +class MockFirebase : public Firebase { + public: + MOCK_METHOD1(getPtr, std::unique_ptr(const String&)); + MOCK_METHOD2(setPtr, std::unique_ptr(const String&, const String&)); + MOCK_METHOD2(pushPtr, std::unique_ptr(const String&, const String&)); + MOCK_METHOD1(removePtr, std::unique_ptr(const String&)); + MOCK_METHOD1(streamPtr, std::unique_ptr(const String&)); +}; + +class MockFirebaseGet : public FirebaseGet { + public: + MOCK_CONST_METHOD0(response, const String&()); + MOCK_CONST_METHOD0(error, const FirebaseError&()); +}; + +class MockFirebaseSet : public FirebaseSet { + public: + MOCK_CONST_METHOD0(json, const String&()); + MOCK_CONST_METHOD0(error, const FirebaseError&()); +}; + +class MockFirebasePush : public FirebasePush { + public: + MOCK_CONST_METHOD0(name, const String&()); + MOCK_CONST_METHOD0(error, const FirebaseError&()); +}; + +class MockFirebaseRemove : public FirebaseRemove { + public: + MOCK_CONST_METHOD0(error, const FirebaseError&()); +}; + +class MockFirebaseStream : public FirebaseStream { + public: + MOCK_METHOD0(available, bool()); + MOCK_METHOD1(read, Event(String& event)); + MOCK_CONST_METHOD0(error, const FirebaseError&()); +}; + +} // modem +} // firebase +#endif // TEST_MOCK_FIREBASE_H diff --git a/test/modem/Makefile b/test/modem/Makefile new file mode 100644 index 00000000..88e8caa4 --- /dev/null +++ b/test/modem/Makefile @@ -0,0 +1,236 @@ +# A sample Makefile for building both Google Mock and Google Test and +# using them in user tests. This file is self-contained, so you don't +# need to use the Makefile in Google Test's source tree. Please tweak +# it to suit your environment and project. You may want to move it to +# your project's root directory. +# +# SYNOPSIS: +# +# make [all] - makes everything. +# make TARGET - makes the given target. +# make clean - removes all files generated by make. + +# Please tweak the following variable definitions as needed by your +# project, except GMOCK_HEADERS and GTEST_HEADERS, which you can use +# in your own targets but shouldn't modify. + +# Points to the root of Google Test, relative to where this file is. +# Remember to tweak this if you move this file, or if you want to use +# a copy of Google Test at a different location. +GTEST_DIR = ../googletest/googletest/ + +# Points to the root of Google Mock, relative to where this file is. +# Remember to tweak this if you move this file. +GMOCK_DIR = ../googletest/googlemock/ + +# Points to the root of Arduino mock, relative to where this file is. +# Remember to tweak this if you move this file. +ARDUINO_MOCK_DIR = ../arduino-mock/ + +# Where to find user code. +TEST_DIR = . + +PROJECT_ROOT = ../../ +SRC_ROOT = $(PROJECT_ROOT)/src + +# Flags passed to the preprocessor. +# Set Google Test and Google Mock's header directories as system +# directories, such that the compiler doesn't generate warnings in +# these headers. +CPPFLAGS += -isystem $(GTEST_DIR)/include -isystem $(GMOCK_DIR)/include \ + -I$(ARDUINO_MOCK_DIR)/include/arduino-mock/ \ + -I$(ARDUINO_MOCK_DIR)/include/ \ + -I$(PROJECT_ROOT)/test/dummies \ + -I$(PROJECT_ROOT)/src \ + -I$(PROJECT_ROOT) + +# Flags passed to the C++ compiler. +CXXFLAGS += -g -Wall -Wextra -pthread -std=c++11 + +# All tests produced by this Makefile. Remember to add new tests you +# created to the list. +TESTS = get-command_test set-command_test remove-command_test \ + push-command_test begin-command_test stream-command_test + +# All Google Test headers. Usually you shouldn't change this +# definition. +GTEST_HEADERS = $(GTEST_DIR)/include/gtest/*.h \ + $(GTEST_DIR)/include/gtest/internal/*.h + +# All Google Mock headers. Note that all Google Test headers are +# included here too, as they are #included by Google Mock headers. +# Usually you shouldn't change this definition. +GMOCK_HEADERS = $(GMOCK_DIR)/include/gmock/*.h \ + $(GMOCK_DIR)/include/gmock/internal/*.h \ + $(GTEST_HEADERS) + +# Arduino Mock headers. +# Usually you shouldn't change this definition. +ARDUINO_MOCK_HEADERS = $(ARDUINO_MOCK_DIR)/include/arduino-mock/*.h + +# House-keeping build targets. + +all : $(TESTS) + +test : $(TESTS) + for t in $(TESTS); do ./$$t; done; + +clean : + rm -f $(TESTS) gmock.a gmock_main.a arduino_mock_all.a arduino_json.a *.o + +# Builds gmock.a and gmock_main.a. These libraries contain both +# Google Mock and Google Test. A test should link with either gmock.a +# or gmock_main.a, depending on whether it defines its own main() +# function. It's fine if your test only uses features from Google +# Test (and not Google Mock). + +# Usually you shouldn't tweak such internal variables, indicated by a +# trailing _. +GTEST_SRCS_ = $(GTEST_DIR)/src/*.cc $(GTEST_DIR)/src/*.h $(GTEST_HEADERS) +GMOCK_SRCS_ = $(GMOCK_DIR)/src/*.cc $(GMOCK_HEADERS) +ARDUINO_MOCK_SRCS_ = $(ARDUINO_MOCK_DIR)/src/*.cc $(ARDUINO_MOCK_HEADERS) + +# For simplicity and to avoid depending on implementation details of +# Google Mock and Google Test, the dependencies specified below are +# conservative and not optimized. This is fine as Google Mock and +# Google Test compile fast and for ordinary users their source rarely +# changes. +gtest-all.o : $(GTEST_SRCS_) + $(CXX) $(CPPFLAGS) -I$(GTEST_DIR) -I$(GMOCK_DIR) $(CXXFLAGS) \ + -c $(GTEST_DIR)/src/gtest-all.cc + +gmock-all.o : $(GMOCK_SRCS_) + $(CXX) $(CPPFLAGS) -I$(GTEST_DIR) -I$(GMOCK_DIR) $(CXXFLAGS) \ + -c $(GMOCK_DIR)/src/gmock-all.cc + +gmock_main.o : $(GMOCK_SRCS_) + $(CXX) $(CPPFLAGS) -I$(GTEST_DIR) -I$(GMOCK_DIR) $(CXXFLAGS) \ + -c $(GMOCK_DIR)/src/gmock_main.cc + +gmock.a : gmock-all.o gtest-all.o + $(AR) $(ARFLAGS) $@ $^ + +gmock_main.a : gmock-all.o gtest-all.o gmock_main.o + $(AR) $(ARFLAGS) $@ $^ + + +# Builds Arduino mocks. +ArduinoMockAll.o : $(ARDUINO_MOCK_SRCS_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_MOCK_DIR) $(CXXFLAGS) -c \ + $(ARDUINO_MOCK_DIR)/src/ArduinoMockAll.cc + +arduino_mock_all.a : ArduinoMockAll.o + $(AR) $(ARFLAGS) $@ $^ + +# Builds ArduinoJson +ARDUINO_JSON_SRC_ = ../../src/third-party/arduino-json-5.1.1/src +JsonObject.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/JsonObject.cpp + +JsonBuffer.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/JsonBuffer.cpp + +JsonVariant.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/JsonVariant.cpp + +JsonArray.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/JsonArray.cpp + +JsonParser.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/Internals/JsonParser.cpp + +Comments.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/Internals/Comments.cpp + +List.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/Internals/List.cpp + +Encoding.o : $(ARDUINO_JSON_SRC_) + $(CXX) $(CPPFLAGS) -I$(ARDUINO_JSON_SRC_) $(CXXFLAGS) -c \ + $(ARDUINO_JSON_SRC_)/Internals/Encoding.cpp + +arduino_json.a : JsonObject.o JsonBuffer.o JsonVariant.o JsonArray.o \ + JsonParser.o Comments.o List.o Encoding.o + $(AR) $(ARFLAGS) $@ $^ + + +# Builds shared objects. + +Firebase.o : $(SRC_ROOT)/Firebase.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(SRC_ROOT)/Firebase.cpp + +FirebaseHttpClient_dummy.o : $(PROJECT_ROOT)/test/dummies/FirebaseHttpClient_dummy.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(PROJECT_ROOT)/test/dummies/FirebaseHttpClient_dummy.cpp + +# Builds tests. + +get-command.o : $(SRC_ROOT)/modem/get-command.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(SRC_ROOT)/modem/get-command.cpp + +get-command_test.o : $(TEST_DIR)/get-command_test.cpp $(GMOCK_HEADERS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(TEST_DIR)/get-command_test.cpp + +get-command_test : get-command_test.o Firebase.o FirebaseHttpClient_dummy.o get-command.o gmock_main.a \ + arduino_mock_all.a arduino_json.a + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -lpthread $^ -o $@ + + +set-command.o : $(SRC_ROOT)/modem/set-command.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(SRC_ROOT)/modem/set-command.cpp + +set-command_test.o : $(TEST_DIR)/set-command_test.cpp $(GMOCK_HEADERS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(TEST_DIR)/set-command_test.cpp + +set-command_test : set-command.o set-command_test.o Firebase.o FirebaseHttpClient_dummy.o gmock_main.a \ + arduino_mock_all.a arduino_json.a + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -lpthread $^ -o $@ + + +remove-command.o : $(SRC_ROOT)/modem/remove-command.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(SRC_ROOT)/modem/remove-command.cpp + +remove-command_test.o : $(TEST_DIR)/remove-command_test.cpp $(GMOCK_HEADERS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(TEST_DIR)/remove-command_test.cpp + +remove-command_test : remove-command.o remove-command_test.o Firebase.o FirebaseHttpClient_dummy.o gmock_main.a \ + arduino_mock_all.a arduino_json.a + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -lpthread $^ -o $@ + + +push-command.o : $(SRC_ROOT)/modem/push-command.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(SRC_ROOT)/modem/push-command.cpp + +push-command_test.o : $(TEST_DIR)/push-command_test.cpp $(GMOCK_HEADERS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(TEST_DIR)/push-command_test.cpp + +push-command_test : push-command.o push-command_test.o Firebase.o FirebaseHttpClient_dummy.o gmock_main.a \ + arduino_mock_all.a arduino_json.a + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -lpthread $^ -o $@ + +begin-command.o : $(SRC_ROOT)/modem/begin-command.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(SRC_ROOT)/modem/begin-command.cpp + +begin-command_test.o : $(TEST_DIR)/begin-command_test.cpp $(GMOCK_HEADERS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(TEST_DIR)/begin-command_test.cpp + +begin-command_test : begin-command.o begin-command_test.o Firebase.o FirebaseHttpClient_dummy.o gmock_main.a \ + arduino_mock_all.a arduino_json.a + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -lpthread $^ -o $@ + +stream-command.o : $(SRC_ROOT)/modem/stream-command.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(SRC_ROOT)/modem/stream-command.cpp + +stream-command_test.o : $(TEST_DIR)/stream-command_test.cpp $(GMOCK_HEADERS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $(TEST_DIR)/stream-command_test.cpp + +stream-command_test : stream-command.o stream-command_test.o Firebase.o FirebaseHttpClient_dummy.o gmock_main.a \ + arduino_mock_all.a arduino_json.a + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -lpthread $^ -o $@ + diff --git a/test/modem/begin-command_test.cpp b/test/modem/begin-command_test.cpp new file mode 100644 index 00000000..804b699f --- /dev/null +++ b/test/modem/begin-command_test.cpp @@ -0,0 +1,84 @@ +#include "Firebase.h" +#include "gtest/gtest.h" +#include "modem/commands.h" +#include "test/modem/mock-input-stream.h" +#include "test/modem/mock-output-stream.h" +#include "test/mock-firebase.h" + +namespace firebase { +namespace modem { + +using ::testing::Return; +using ::testing::ByMove; +using ::testing::ReturnRef; +using ::testing::StartsWith; +using ::testing::Matcher; +using ::testing::_; + +class BeginCommandTest : public ::testing::Test { + protected: + void FeedCommand(const String& host, const String& auth = "") { + String command_fragment(host); + if (!auth.empty()) { + command_fragment += String(" ") + auth; + } + + EXPECT_CALL(in_, readLine()) + .WillOnce(Return(command_fragment)); + } + + void ExpectOutput(const String& output) { + EXPECT_CALL(out_, println(output)) + .WillOnce(Return(3)); + } + + void ExpectOutputStartsWith(const String& output) { + // We have to be explicit here due to the polymorphic nature of println(). + const Matcher matcher = StartsWith(output); + EXPECT_CALL(out_, println(matcher)) + .WillOnce(Return(3)); + } + + MockInputStream in_; + MockOutputStream out_; +}; + +TEST_F(BeginCommandTest, hostOnly) { + const String host("http://test.firebase.com"); + + FeedCommand(host); + ExpectOutput("+OK"); + + BeginCommand command; + ASSERT_TRUE(command.execute("BEGIN", &in_, &out_)); + + std::unique_ptr firebase(command.firebase()); + EXPECT_EQ("", firebase->auth()); +} + +TEST_F(BeginCommandTest, hostAndAuth) { + const String host("http://test.firebase.com"); + const String auth("afasdfsadfasdssfadsfsd"); + + FeedCommand(host, auth); + ExpectOutput("+OK"); + + BeginCommand command; + ASSERT_TRUE(command.execute("BEGIN", &in_, &out_)); + + std::unique_ptr firebase(command.firebase()); + EXPECT_EQ(auth, firebase->auth()); +} + +TEST_F(BeginCommandTest, neitherHostNorAuth) { + FeedCommand(""); + ExpectOutputStartsWith("-FAIL"); + + BeginCommand command; + ASSERT_FALSE(command.execute("BEGIN", &in_, &out_)); + + std::unique_ptr firebase(command.firebase()); + EXPECT_FALSE(firebase); +} +} // modem +} // firebase diff --git a/test/modem/get-command_test.cpp b/test/modem/get-command_test.cpp new file mode 100644 index 00000000..6c1981bb --- /dev/null +++ b/test/modem/get-command_test.cpp @@ -0,0 +1,77 @@ +#include "Firebase.h" +#include "gtest/gtest.h" +#include "modem/commands.h" +#include "test/modem/mock-input-stream.h" +#include "test/modem/mock-output-stream.h" +#include "test/mock-firebase.h" + +namespace firebase { +namespace modem { + +using ::testing::Return; +using ::testing::ByMove; +using ::testing::ReturnRef; +using ::testing::_; + +class GetCommandTest : public ::testing::Test { + protected: + void SetUp() override { + get_.reset(new MockFirebaseGet()); + } + + void FeedCommand(const String& path) { + const String command_fragment(String(" ") + path); + EXPECT_CALL(in_, readLine()) + .WillOnce(Return(command_fragment)); + } + + bool RunCommand(const FirebaseError& error) { + EXPECT_CALL(*get_, error()) + .WillRepeatedly(ReturnRef(error)); + + EXPECT_CALL(fbase_, getPtr(_)) + .WillOnce(Return(ByMove(std::move(get_)))); + + GetCommand getCmd(&fbase_); + return getCmd.execute("GET", &in_, &out_); + } + + MockInputStream in_; + MockOutputStream out_; + MockFirebase fbase_; + std::unique_ptr get_; +}; + +TEST_F(GetCommandTest, gets) { + const String path("/test/path"); + FeedCommand(path); + + const String value("Test value"); + EXPECT_CALL(*get_, response()) + .WillOnce(ReturnRef(value)); + + EXPECT_CALL(out_, print(String("+"))) + .WillOnce(Return(1)); + + EXPECT_CALL(out_, println(value)) + .WillOnce(Return(1)); + + ASSERT_TRUE(RunCommand(FirebaseError())); +} + +TEST_F(GetCommandTest, handlesError) { + FirebaseError error(-200, "Test Error."); + const String path("/test/path"); + FeedCommand(path); + + EXPECT_CALL(out_, print(String("-FAIL "))) + .WillOnce(Return(1)); + + EXPECT_CALL(out_, println(error.message())) + .WillOnce(Return(1)); + ASSERT_FALSE(RunCommand(error)); + +} + +} // modem +} // firebase diff --git a/test/modem/mock-input-stream.h b/test/modem/mock-input-stream.h new file mode 100644 index 00000000..d0f7e9ae --- /dev/null +++ b/test/modem/mock-input-stream.h @@ -0,0 +1,21 @@ +#ifndef MODEM_TEST_MOCK_INPUT_STREAM_H +#define MODEM_TEST_MOCK_INPUT_STREAM_H + +#include "gtest/gtest.h" +#include "modem/input-stream.h" + +namespace firebase { +namespace modem { + +class MockInputStream : public InputStream { + public: + MOCK_METHOD0(readLine, String ()); + MOCK_METHOD1(readStringUntil, String (const char)); + MOCK_METHOD0(drain, void ()); + MOCK_METHOD0(available, bool ()); +}; + +} // modem +} // firebase + +#endif //MODEM_TEST_MOCK_INPUT_STREAM_H diff --git a/test/modem/mock-output-stream.h b/test/modem/mock-output-stream.h new file mode 100644 index 00000000..ef15fadf --- /dev/null +++ b/test/modem/mock-output-stream.h @@ -0,0 +1,21 @@ +#ifndef MODEM_TEST_MOCK_OUTPUT_STREAM_H +#define MODEM_TEST_MOCK_OUTPUT_STREAM_H + +#include "gtest/gtest.h" +#include "modem/output-stream.h" + +namespace firebase { +namespace modem { + +class MockOutputStream : public OutputStream { + public: + MOCK_METHOD1(println, int (const String&)); + MOCK_METHOD1(println, int (const int)); + MOCK_METHOD1(print, int (const String&)); +}; + +} // modem +} // firebase + +#endif //MODEM_TEST_MOCK_OUTPUT_STREAM_H + diff --git a/test/modem/push-command_test.cpp b/test/modem/push-command_test.cpp new file mode 100644 index 00000000..7e4d17bc --- /dev/null +++ b/test/modem/push-command_test.cpp @@ -0,0 +1,83 @@ +#include "Firebase.h" +#include "gtest/gtest.h" +#include "modem/commands.h" +#include "modem/json_util.h" +#include "test/modem/mock-input-stream.h" +#include "test/modem/mock-output-stream.h" +#include "test/mock-firebase.h" + +namespace firebase { +namespace modem { + +using ::testing::Return; +using ::testing::ByMove; +using ::testing::ReturnRef; +using ::testing::_; + +class PushCommandTest : public ::testing::Test { + protected: + void SetUp() override { + push_.reset(new MockFirebasePush()); + } + + void FeedCommand(const String& path, const String& data) { + const String data_fragment(data); + EXPECT_CALL(in_, readStringUntil(' ')) + .WillOnce(Return(path)); + EXPECT_CALL(in_, readLine()) + .WillOnce(Return(data_fragment)); + } + + void ExpectOutput(const String& output) { + EXPECT_CALL(out_, println(output)) + .WillOnce(Return(output.length())); + } + + void ExpectErrorOutput(const String& error_message) { + EXPECT_CALL(out_, print(String("-FAIL "))) + .WillOnce(Return(5)); + EXPECT_CALL(out_, println(error_message)) + .WillOnce(Return(error_message.length())); + } + + bool RunExpectingData(const String& data, const FirebaseError& error) { + EXPECT_CALL(*push_, error()) + .WillRepeatedly(ReturnRef(error)); + + EXPECT_CALL(fbase_, pushPtr(_, EncodeForJson(data))) + .WillOnce(Return(ByMove(std::move(push_)))); + + PushCommand pushCmd(&fbase_); + return pushCmd.execute("PUSH", &in_, &out_); + } + + MockInputStream in_; + MockOutputStream out_; + MockFirebase fbase_; + std::unique_ptr push_; +}; + +TEST_F(PushCommandTest, sendsData) { + const String path("/test/path"); + const String data("This is a test payload."); + + FeedCommand(path, data); + ExpectOutput("+OK"); + + ASSERT_TRUE(RunExpectingData(data, FirebaseError())); +} + +TEST_F(PushCommandTest, HandlesError) { + const String path("/test/path"); + const String data("This is a test payload."); + FirebaseError error(-200, "Test error."); + + FeedCommand(path, data); + ExpectErrorOutput(error.message()); + + ASSERT_FALSE(RunExpectingData(data, error)); +} + +} // modem +} // firebase + diff --git a/test/modem/remove-command_test.cpp b/test/modem/remove-command_test.cpp new file mode 100644 index 00000000..5c361e7e --- /dev/null +++ b/test/modem/remove-command_test.cpp @@ -0,0 +1,69 @@ +#include "gtest/gtest.h" +#include "test/modem/mock-output-stream.h" +#include "test/modem/mock-input-stream.h" +#include "test/mock-firebase.h" +#include "Firebase.h" +#include "modem/commands.h" + +namespace firebase { +namespace modem { + +using ::testing::Return; +using ::testing::ByMove; +using ::testing::ReturnRef; +using ::testing::_; + +class RemoveCommandTest : public ::testing::Test { + protected: + void SetUp() override { + remove_.reset(new MockFirebaseRemove()); + } + + void FeedCommand(const String& path) { + const String command_fragment(String(" ") + path); + EXPECT_CALL(in_, readLine()) + .WillOnce(Return(command_fragment)); + } + + bool RunCommand(const FirebaseError& error) { + EXPECT_CALL(*remove_, error()) + .WillRepeatedly(ReturnRef(error)); + + EXPECT_CALL(fbase_, removePtr(_)) + .WillOnce(Return(ByMove(std::move(remove_)))); + + RemoveCommand command(&fbase_); + return command.execute("REMOVE", &in_, &out_); + } + + MockInputStream in_; + MockOutputStream out_; + MockFirebase fbase_; + std::unique_ptr remove_; +}; + +TEST_F(RemoveCommandTest, success) { + const String path("/test/path"); + FeedCommand(path); + + EXPECT_CALL(out_, println(String("+OK"))) + .WillOnce(Return(3)); + + ASSERT_TRUE(RunCommand(FirebaseError())); +} + +TEST_F(RemoveCommandTest, handlesError) { + FirebaseError error(-200, "Test Error."); + const String path("/test/path"); + FeedCommand(path); + + EXPECT_CALL(out_, print(String("-FAIL "))) + .WillOnce(Return(1)); + + EXPECT_CALL(out_, println(error.message())) + .WillOnce(Return(1)); + ASSERT_FALSE(RunCommand(error)); +} + +} // modem +} // firebase diff --git a/test/modem/set-command_test.cpp b/test/modem/set-command_test.cpp new file mode 100644 index 00000000..729a85d7 --- /dev/null +++ b/test/modem/set-command_test.cpp @@ -0,0 +1,81 @@ +#include "Firebase.h" +#include "gtest/gtest.h" +#include "modem/commands.h" +#include "modem/json_util.h" +#include "test/modem/mock-input-stream.h" +#include "test/modem/mock-output-stream.h" +#include "test/mock-firebase.h" + +namespace firebase { +namespace modem { + +using ::testing::Return; +using ::testing::ByMove; +using ::testing::ReturnRef; +using ::testing::_; + +class SetCommandTest : public ::testing::Test { + protected: + void SetUp() override { + set_.reset(new MockFirebaseSet()); + } + + void FeedCommand(const String& path, const String& data) { + const String data_fragment(data); + EXPECT_CALL(in_, readStringUntil(' ')) + .WillOnce(Return(path)); + EXPECT_CALL(in_, readLine()) + .WillOnce(Return(data_fragment)); + } + + void ExpectOutput(const String& output) { + EXPECT_CALL(out_, println(output)) + .WillOnce(Return(3)); + } + + void ExpectErrorOutput(const String& error_message) { + EXPECT_CALL(out_, print(String("-FAIL "))) + .WillOnce(Return(5)); + EXPECT_CALL(out_, println(error_message)) + .WillOnce(Return(error_message.length())); + } + + bool RunExpectingData(const String& data, const FirebaseError& error) { + EXPECT_CALL(*set_, error()) + .WillRepeatedly(ReturnRef(error)); + + EXPECT_CALL(fbase_, setPtr(_, EncodeForJson(data))) + .WillOnce(Return(ByMove(std::move(set_)))); + + SetCommand setCmd(&fbase_); + return setCmd.execute("SET", &in_, &out_); + } + + MockInputStream in_; + MockOutputStream out_; + MockFirebase fbase_; + std::unique_ptr set_; +}; + +TEST_F(SetCommandTest, sendsData) { + const String path("/test/path"); + const String data("This is a test payload."); + + FeedCommand(path, data); + ExpectOutput("+OK"); + + ASSERT_TRUE(RunExpectingData(data, FirebaseError())); +} + +TEST_F(SetCommandTest, HandlesError) { + const String path("/test/path"); + const String data("This is a test payload."); + FirebaseError error(-200, "Test error."); + + FeedCommand(path, data); + ExpectErrorOutput(error.message()); + + ASSERT_FALSE(RunExpectingData(data, error)); +} +} // modem +} // firebase diff --git a/test/modem/stream-command_test.cpp b/test/modem/stream-command_test.cpp new file mode 100644 index 00000000..4cfc17be --- /dev/null +++ b/test/modem/stream-command_test.cpp @@ -0,0 +1,93 @@ +#include "Firebase.h" +#include "gtest/gtest.h" +#include "modem/commands.h" +#include "test/modem/mock-input-stream.h" +#include "test/modem/mock-output-stream.h" +#include "test/mock-firebase.h" + +namespace firebase { +namespace modem { + +using ::testing::Return; +using ::testing::Invoke; +using ::testing::ByMove; +using ::testing::ReturnRef; +using ::testing::_; + +class StreamCommandTest : public ::testing::Test { + protected: + void SetUp() override { + stream_.reset(new MockFirebaseStream()); + } + + bool RunCommand(const FirebaseError& error) { + EXPECT_CALL(*stream_, error()) + .WillRepeatedly(ReturnRef(error)); + + EXPECT_CALL(fbase_, streamPtr(_)) + .WillOnce(Return(ByMove(std::move(stream_)))); + + StreamCommand cmd(&fbase_); + return cmd.execute("BEGIN_STREAM", &in_, &out_); + } + + MockInputStream in_; + MockOutputStream out_; + MockFirebase fbase_; + std::unique_ptr stream_; +}; + +TEST_F(StreamCommandTest, streams) { + const String path("/test/path"); + EXPECT_CALL(in_, available()) + .WillRepeatedly(Return(true)); + + EXPECT_CALL(in_, readLine()) + .WillOnce(Return(path)) + .WillOnce(Return("END_STREAM")); + + const String value("Test value"); + EXPECT_CALL(*stream_, available()) + .WillOnce(Return(true)) + .WillRepeatedly(Return(false)); + + EXPECT_CALL(*stream_, read(_)) + .WillOnce(Invoke([&value](String& json) { + json = value; + return FirebaseStream::PUT; + })); + + EXPECT_CALL(out_, print(String("+"))) + .WillOnce(Return(1)); + EXPECT_CALL(out_, print(String("PUT "))) + .WillOnce(Return(1)); + EXPECT_CALL(out_, println(String("/dummy/path"))) + .WillOnce(Return(1)); + + EXPECT_CALL(out_, println(value.length())) + .WillOnce(Return(1)); + EXPECT_CALL(out_, println(value)) + .WillOnce(Return(1)); + + EXPECT_CALL(out_, println(String("+OK"))) + .WillOnce(Return(1)); + + ASSERT_TRUE(RunCommand(FirebaseError())); +} + +TEST_F(StreamCommandTest, handlesError) { + FirebaseError error(-200, "Test Error."); + const String path("/test/path"); + EXPECT_CALL(in_, readLine()) + .WillOnce(Return(path)); + + EXPECT_CALL(out_, print(String("-FAIL "))) + .WillOnce(Return(1)); + + EXPECT_CALL(out_, println(error.message())) + .WillOnce(Return(1)); + ASSERT_FALSE(RunCommand(error)); +} + +} // modem +} // firebase