Add GTEST cases

Fixes openbmc/openbmc#1714

Change-Id: I51964f16fc2ea733ee3b3ae822f72ac7b431189a
Signed-off-by: Vishwanatha Subbanna <vishwa@linux.vnet.ibm.com>
diff --git a/Makefile.am b/Makefile.am
index 1e1db3e..5d5e14f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -15,3 +15,4 @@
 phosphor_user_manager_CXXFLAGS = $(SYSTEMD_CFLAGS) \
                                  $(PHOSPHOR_DBUS_INTERFACES_CFLAGS) \
                                  $(PHOSPHOR_LOGGING_CFLAGS)
+SUBDIRS = . test
diff --git a/configure.ac b/configure.ac
index c6bb3fe..6d41a5d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -26,6 +26,29 @@
 AS_IF([test "x$DEFAULT_CRYPT_ALGO" == "x"], [DEFAULT_CRYPT_ALGO="1"])
 AC_DEFINE_UNQUOTED([DEFAULT_CRYPT_ALGO], ["$DEFAULT_CRYPT_ALGO"], [The default crypt algorithm if one not found in shadow])
 
+# Check/set gtest specific functions.
+AX_PTHREAD([GTEST_CPPFLAGS="-DGTEST_HAS_PTHREAD=1"],[GTEST_CPPFLAGS="-DGTEST_HAS_PTHREAD=0"])
+AC_SUBST(GTEST_CPPFLAGS)
+
+# Test cases require SDK so only build if we're told to (and SDK is available)
+AC_ARG_ENABLE([oe-sdk],
+    AS_HELP_STRING([--enable-oe-sdk], [Link testcases absolutely against OE SDK so they can be ran within it.])
+)
+AC_ARG_VAR(OECORE_TARGET_SYSROOT,
+    [Path to the OE SDK SYSROOT])
+AS_IF([test "x$enable_oe_sdk" == "xyes"],
+    AS_IF([test "x$OECORE_TARGET_SYSROOT" == "x"],
+          AC_MSG_ERROR([OECORE_TARGET_SYSROOT must be set with --enable-oe-sdk])
+    )
+    AC_MSG_NOTICE([Enabling OE-SDK at $OECORE_TARGET_SYSROOT])
+    [
+        testcase_flags="-Wl,-rpath,\${OECORE_TARGET_SYSROOT}/lib"
+        testcase_flags="${testcase_flags} -Wl,-rpath,\${OECORE_TARGET_SYSROOT}/usr/lib"
+        testcase_flags="${testcase_flags} -Wl,-dynamic-linker,`find \${OECORE_TARGET_SYSROOT}/lib/ld-*.so | sort -r -n | head -n1`"
+    ]
+    AC_SUBST([OESDK_TESTCASE_FLAGS], [$testcase_flags])
+)
+
 # Checks for typedefs, structures, and compiler characteristics.
 AX_CXX_COMPILE_STDCXX_14([noext])
 AX_APPEND_COMPILE_FLAGS([-Wall -Werror], [CXXFLAGS])
@@ -34,5 +57,5 @@
 LT_INIT
 
 # Create configured output
-AC_CONFIG_FILES([Makefile])
+AC_CONFIG_FILES([Makefile test/Makefile])
 AC_OUTPUT
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..3acdaab
--- /dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,27 @@
+AM_CPPFLAGS = -I$(top_srcdir)
+
+# Run all 'check' test programs
+TESTS = $(check_PROGRAMS)
+
+# Build/add utest to test suite
+check_PROGRAMS = utest
+utest_CPPFLAGS = -Igtest \
+                 $(GTEST_CPPFLAGS) \
+                 $(AM_CPPFLAGS) \
+                 $(PHOSPHOR_LOGGING_CFLAGS) \
+                 $(SDBUSPLUS_CFLAGS)
+
+utest_CXXFLAGS = $(PTHREAD_CFLAGS)
+
+utest_LDFLAGS = -lgtest_main \
+                -lgtest \
+                $(PTHREAD_LIBS) \
+                $(OESDK_TESTCASE_FLAGS) \
+                $(PHOSPHOR_DBUS_INTERFACES_LIBS) \
+                $(PHOSPHOR_LOGGING_LIBS) \
+                $(SDBUSPLUS_LIBS) \
+                -lcrypt \
+                -lstdc++fs
+
+utest_SOURCES = utest.cpp
+utest_LDADD = $(top_builddir)/user.o
diff --git a/test/utest.cpp b/test/utest.cpp
new file mode 100644
index 0000000..bdca968
--- /dev/null
+++ b/test/utest.cpp
@@ -0,0 +1,210 @@
+#include <sys/stat.h>
+#include <string>
+#include <fstream>
+#include <experimental/filesystem>
+#include <gtest/gtest.h>
+#include <sdbusplus/bus.hpp>
+#include "user.hpp"
+namespace phosphor
+{
+namespace user
+{
+
+namespace fs = std::experimental::filesystem;
+
+constexpr auto path = "/dummy/user";
+constexpr auto testShadow = "/tmp/__tshadow__";
+constexpr auto shadowCopy = "/tmp/__tshadowCopy__";
+constexpr auto shadowCompare = "/tmp/__tshadowCompare__";
+
+// New password
+constexpr auto password = "passw0rd";
+
+constexpr auto MD5 = "1";
+constexpr auto SHA512 = "6";
+constexpr auto salt = "1G.cK/YP";
+
+// Example entry matching /etc/shadow structure
+constexpr auto spPwdp = "$1$1G.cK/YP$JI5t0oliPxZveXOvLcZ/H.:17344:1:90:7:::";
+
+class UserTest : public ::testing::Test
+{
+    public:
+        const std::string md5Salt = '$' + std::string(MD5) + '$'
+                                    + std::string(salt) + '$';
+        const std::string shaSalt = '$' + std::string(SHA512) + '$'
+                                    + std::string(salt) + '$';
+
+        const std::string entry = fs::path(path).filename().string() +
+                                  ':' + std::string(spPwdp);
+        sdbusplus::bus::bus bus;
+        phosphor::user::User user;
+
+        // Gets called as part of each TEST_F construction
+        UserTest()
+            : bus(sdbusplus::bus::new_default()),
+              user(bus, path)
+        {
+            // Create a shadow file entry
+            std::ofstream file(testShadow);
+            file << entry;
+            file.close();
+
+            // File to compare against
+            std::ofstream compare(shadowCompare);
+            compare << entry;
+            compare.close();
+        }
+
+        // Gets called as part of each TEST_F destruction
+        ~UserTest()
+        {
+            if (fs::exists(testShadow))
+            {
+                fs::remove(testShadow);
+            }
+
+            if (fs::exists(shadowCopy))
+            {
+                fs::remove(shadowCopy);
+            }
+
+            if (fs::exists(shadowCompare))
+            {
+                fs::remove(shadowCompare);
+            }
+        }
+
+        /** @brief wrapper for get crypt field */
+        auto getCryptField(char* data)
+        {
+            return User::getCryptField(
+                            std::forward<decltype(data)>(data));
+        }
+
+        /** @brief wrapper for getSaltString */
+        auto getSaltString(const std::string& crypt,
+                           const std::string& salt)
+        {
+            return User::getSaltString(
+                            std::forward<decltype(crypt)>(crypt),
+                                std::forward<decltype(salt)>(salt));
+        }
+
+        /** @brief wrapper for generateHash */
+        auto generateHash(const std::string& password,
+                          const std::string& salt)
+        {
+            return User::generateHash(
+                            std::forward<decltype(password)>(password),
+                                std::forward<decltype(salt)>(salt));
+        }
+
+        /** @brief Applies the new password */
+        auto applyPassword()
+        {
+            return user.applyPassword(testShadow, shadowCopy,
+                                      password, salt);
+        }
+};
+
+/** @brief Makes sure that SHA512 crypt field is extracted
+ */
+TEST_F(UserTest, sha512GetCryptField)
+{
+    auto salt = const_cast<char*>(shaSalt.c_str());
+    EXPECT_EQ(SHA512, this->getCryptField(salt));
+}
+
+/** @brief Makes sure that MD5 crypt field is extracted as default
+ */
+TEST_F(UserTest, md55GetCryptFieldDefault)
+{
+    auto salt = const_cast<char*>("hello");
+    EXPECT_EQ(MD5, this->getCryptField(salt));
+}
+
+/** @brief Makes sure that MD5 crypt field is extracted
+ */
+TEST_F(UserTest, md55GetCryptField)
+{
+    auto salt = const_cast<char*>(md5Salt.c_str());
+    EXPECT_EQ(MD5, this->getCryptField(salt));
+}
+
+/** @brief Makes sure that salt string is put within $$
+ */
+TEST_F(UserTest, getSaltString)
+{
+    EXPECT_EQ(md5Salt, this->getSaltString(MD5, salt));
+}
+
+/** @brief Makes sure hash is generated correctly
+ */
+TEST_F(UserTest, generateHash)
+{
+    std::string sample = crypt(password, md5Salt.c_str());
+    std::string actual = generateHash(password, md5Salt);
+    EXPECT_EQ(sample, actual);
+}
+
+/** @brief Verifies that the correct password is written to file
+ */
+TEST_F(UserTest, applyPassword)
+{
+    // Update the password
+    applyPassword();
+
+    // Read files and compare
+    std::ifstream shadow(testShadow);
+    std::ifstream copy(shadowCompare);
+
+    std::string shadowEntry;
+    shadow >> shadowEntry;
+
+    std::string shadowCompareEntry;
+    copy >> shadowCompareEntry;
+
+    EXPECT_EQ(shadowEntry, shadowCompareEntry);
+}
+
+/** @brief Verifies the shadow copy file is removed
+ */
+TEST_F(UserTest, checkShadowCopyRemove)
+{
+    // Update the password so that the temp file is in action
+    applyPassword();
+
+    // Compare the permission of 2 files
+    struct stat shadow{};
+    struct stat temp{};
+
+    stat(testShadow, &shadow);
+    stat(shadowCopy, &temp);
+    EXPECT_EQ(false, fs::exists(shadowCopy));
+}
+
+/** @brief Verifies the permissions are correct
+ */
+TEST_F(UserTest, verifyShadowPermission)
+{
+    // Change the permission to 400-> -r--------
+    chmod(testShadow, S_IRUSR);
+    chmod(shadowCompare, S_IRUSR);
+
+    // Update the password so that the temp file is in action
+    applyPassword();
+
+    // Compare the permission of 2 files.
+    // file rename would make sure that the permissions
+    // of old are moved to new
+    struct stat shadow{};
+    struct stat compare{};
+
+    stat(testShadow, &shadow);
+    stat(shadowCompare, &compare);
+    EXPECT_EQ(shadow.st_mode, compare.st_mode);
+}
+
+} // namespace user
+} // namespace phosphor
diff --git a/user.hpp b/user.hpp
index 44dd3a3..c05cacc 100644
--- a/user.hpp
+++ b/user.hpp
@@ -55,6 +55,7 @@
          */
         void setPassword(std::string newPassword) override;
 
+
     private:
         /** @brief sdbusplus handler */
         sdbusplus::bus::bus& bus;
@@ -128,6 +129,9 @@
          */
         void raiseException(int errNo,
                             const std::string& errMsg);
+
+        /** @brief For enabling test cases */
+        friend class UserTest;
 };
 
 } // namespace user