changeset 116:d7025649d85c

Server: add database account Implement accounts using a abstract factory mechanism, the database object creates abstract account which are implemented differently depending on the backend. See: - test_database, - test_account - broken_account Refs #687, #682
author David Demelier <markand@malikania.fr>
date Mon, 11 Sep 2017 13:18:43 +0200
parents 07cb0d9f92e9
children 8a5d022aa6d5
files libserver-test/CMakeLists.txt libserver-test/malikania/server/db/broken_account.cpp libserver-test/malikania/server/db/broken_account.hpp libserver-test/malikania/server/db/broken_character.cpp libserver-test/malikania/server/db/broken_character.hpp libserver-test/malikania/server/db/test_account.cpp libserver-test/malikania/server/db/test_account.hpp libserver-test/malikania/server/db/test_character.cpp libserver-test/malikania/server/db/test_character.hpp libserver-test/malikania/server/db/test_database.cpp libserver-test/malikania/server/db/test_database.hpp libserver-test/malikania/server/db/test_spell.cpp libserver-test/malikania/server/db/test_spell.hpp libserver/CMakeLists.txt libserver/malikania/server/db/account.cpp libserver/malikania/server/db/account.hpp libserver/malikania/server/db/account_dao.hpp libserver/malikania/server/db/character.cpp libserver/malikania/server/db/character.hpp libserver/malikania/server/db/character_dao.hpp libserver/malikania/server/db/dao.hpp libserver/malikania/server/db/database.hpp libserver/malikania/server/db/dynlib_database.cpp libserver/malikania/server/db/model.hpp libserver/malikania/server/db/spell.cpp libserver/malikania/server/db/spell.hpp libserver/malikania/server/db/spell_dao.hpp libserver/malikania/server/net/auth_handler.cpp tests/libserver/db/account/main.cpp
diffstat 29 files changed, 1285 insertions(+), 891 deletions(-) [+]
line wrap: on
line diff
--- a/libserver-test/CMakeLists.txt	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver-test/CMakeLists.txt	Mon Sep 11 13:18:43 2017 +0200
@@ -21,15 +21,21 @@
 set(
     HEADERS
     ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/broken_account.hpp
+    ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/broken_character.hpp
     ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_account.hpp
+    ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_character.hpp
     ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_database.hpp
+    ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_spell.hpp
 )
 
 set(
     SOURCES
     ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/broken_account.cpp
+    ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/broken_character.cpp
     ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_account.cpp
+    ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_character.cpp
     ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_database.cpp
+    ${libmlk-server-test_SOURCE_DIR}/malikania/server/db/test_spell.cpp
 )
 
 malikania_define_library(
--- a/libserver-test/malikania/server/db/broken_account.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver-test/malikania/server/db/broken_account.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -32,6 +32,12 @@
     id_ = 1;
 }
 
+void broken_account::do_remove()
+{
+    if (!bool(allow_& allow_flags::remove))
+        throw std::runtime_error("broken do_remove");
+}
+
 void broken_account::do_set_password(const std::string&)
 {
     if (!bool(allow_ & allow_flags::set_password))
--- a/libserver-test/malikania/server/db/broken_account.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver-test/malikania/server/db/broken_account.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -41,10 +41,11 @@
     enum class allow_flags {
         none                = 0,            //!< everything is broken
         save                = (1 << 0),     //!< allow do_save
-        set_password        = (1 << 1),     //!< allow do_set_password
-        set_email           = (1 << 2),     //!< allow do_set_email
-        set_firstname       = (1 << 3),     //!< allow do_set_firstname
-        set_lastname        = (1 << 4)      //!< allow do_set_lastname
+        remove              = (1 << 1),     //!< allow do_remove
+        set_password        = (1 << 2),     //!< allow do_set_password
+        set_email           = (1 << 3),     //!< allow do_set_email
+        set_firstname       = (1 << 4),     //!< allow do_set_firstname
+        set_lastname        = (1 << 5)      //!< allow do_set_lastname
     };
 
 private:
@@ -57,6 +58,11 @@
     void do_save() override;
 
     /**
+     * \copydoc account::do_remove
+     */
+    void do_remove() override;
+
+    /**
      * \copydoc account::do_set_password
      */
     void do_set_password(const std::string& password) override;
@@ -82,7 +88,7 @@
      *
      * \param login the login
      * \param password the password
-     * \param flags the flags
+     * \param flags the flags for a less broken account
      */
     inline broken_account(std::string login,
                           std::string password,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libserver-test/malikania/server/db/broken_character.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -0,0 +1,49 @@
+/*
+ * broken_character.cpp -- database character object (broken implementation)
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "broken_character.hpp"
+
+namespace mlk {
+
+namespace server {
+
+void broken_character::do_save(std::int64_t account_id)
+{
+    if (!bool(allow_ & allow_flags::save))
+        throw std::runtime_error("broken do_save");
+
+    id_ = 1;
+}
+
+void broken_character::do_remove()
+{
+    if (!bool(allow_ & allow_flags::remove))
+        throw std::runtime_error("broken do_remove");
+
+    id_ = -1;
+}
+
+void broken_character::do_set_level(std::uint16_t)
+{
+    if (!bool(allow_ & allow_flags::set_level))
+        throw std::runtime_error("broken do_set_level");
+}
+
+} // !server
+
+} // !mlk
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libserver-test/malikania/server/db/broken_character.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -0,0 +1,131 @@
+/*
+ * broken_character.hpp -- database character object (broken implementation)
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MALIKANIA_SERVER_BROKEN_CHARACTER_HPP
+#define MALIKANIA_SERVER_BROKEN_CHARACTER_HPP
+
+/**
+ * \file broken_character.hpp
+ * \brief Database character object (broken implementation).
+ */
+
+#include <malikania/server/db/character.hpp>
+
+namespace mlk {
+
+namespace server {
+
+/**
+ * \brief Database character object (broken implementation).
+ */
+class broken_character : public character {
+public:
+    /**
+     * \brief Define which function should not be broken.
+     */
+    enum class allow_flags : unsigned {
+        none                = 0,            //!< everything is broken
+        save                = (1 << 0),     //!< allow do_save
+        remove              = (1 << 1),     //!< allow do_remove
+        set_level           = (1 << 2),     //!< allow do_set_level
+    };
+
+private:
+    allow_flags allow_;
+
+protected:
+    /**
+     * Called by account::add_character.
+     */
+    void do_save(std::int64_t account_id) override;
+
+    /**
+     * Called by account::remove_character.
+     */
+    void do_remove() override;
+
+    /**
+     * Set the character level.
+     */
+    void do_set_level(std::uint16_t level) override;
+
+public:
+    /**
+     * Construct a broken character.
+     *
+     * \param login the login
+     * \param password the password
+     * \param flags the flags for a less broken account
+     */
+    inline broken_character(std::string nickname,
+                            std::string classname,
+                            allow_flags flags = allow_flags::none) noexcept
+        : character(std::move(nickname), std::move(classname))
+        , allow_(flags)
+    {
+    }
+};
+
+/**
+ * \cond ENUM_HIDDEN_SYMBOLS
+ */
+
+inline broken_character::allow_flags operator^(broken_character::allow_flags v1, broken_character::allow_flags v2) noexcept
+{
+    return static_cast<broken_character::allow_flags>(static_cast<unsigned>(v1) ^ static_cast<unsigned>(v2));
+}
+
+inline broken_character::allow_flags operator&(broken_character::allow_flags v1, broken_character::allow_flags v2) noexcept
+{
+    return static_cast<broken_character::allow_flags>(static_cast<unsigned>(v1) & static_cast<unsigned>(v2));
+}
+
+inline broken_character::allow_flags operator|(broken_character::allow_flags v1, broken_character::allow_flags v2) noexcept
+{
+    return static_cast<broken_character::allow_flags>(static_cast<unsigned>(v1) | static_cast<unsigned>(v2));
+}
+
+inline broken_character::allow_flags operator~(broken_character::allow_flags v) noexcept
+{
+    return static_cast<broken_character::allow_flags>(~static_cast<unsigned>(v));
+}
+
+inline broken_character::allow_flags& operator|=(broken_character::allow_flags& v1, broken_character::allow_flags v2) noexcept
+{
+    return v1 = v1 | v2;
+}
+
+inline broken_character::allow_flags& operator&=(broken_character::allow_flags& v1, broken_character::allow_flags v2) noexcept
+{
+    return v1 = v1 & v2;
+}
+
+inline broken_character::allow_flags& operator^=(broken_character::allow_flags& v1, broken_character::allow_flags v2) noexcept
+{
+    return v1 = v1 ^ v2;
+}
+
+/**
+ * \endcond
+ */
+
+} // !server
+
+} // !mlk
+
+#endif // !MALIKANIA_SERVER_BROKEN_CHARACTER_HPP
--- a/libserver-test/malikania/server/db/test_account.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver-test/malikania/server/db/test_account.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -16,7 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include "test_account.hpp"
+#include "test_database.hpp"
 
 namespace mlk {
 
@@ -24,59 +24,81 @@
 
 using test_dao = test_account::test_dao;
 
-test_account::test_account(std::shared_ptr<test_dao> dao,
-                           std::string login,
-                           std::string password) noexcept
-    : account(std::move(login), std::move(password))
-    , dao_(std::move(dao))
+void test_account::do_save()
 {
+    assert(db_.test_account_dao().accounts().count(id_) == 0);
+
+    id_ = db_.test_account_dao().next_id();
+    db_.test_account_dao().accounts().emplace(id_, serialize());
 }
 
-void test_account::do_save()
+void test_account::do_remove()
 {
-    id_ = dao_->next_id();
-    dao_->accounts().emplace(id_, *this);
+    assert(db_.test_account_dao().accounts().count(id_));
+
+    // Do cascade deletion.
+    db_.test_account_dao().accounts().erase(id_);
+    db_.test_character_dao().remove_all(id_);
 }
 
 void test_account::do_set_password(const std::string& password)
 {
-    assert(dao_->accounts().count(id_));
+    assert(db_.test_account_dao().accounts().count(id_));
 
-    dao_->accounts().at(id_).password_ = password;
+    db_.test_account_dao().accounts().at(id_)["password"] = password;
 }
 
 void test_account::do_set_email(const std::string& email)
 {
-    assert(dao_->accounts().count(id_));
+    assert(db_.test_account_dao().accounts().count(id_));
 
-    dao_->accounts().at(id_).email_ = email;
+    db_.test_account_dao().accounts().at(id_)["email"] = email;
 }
 
 void test_account::do_set_firstname(const std::string& name)
 {
-    assert(dao_->accounts().count(id_));
+    assert(db_.test_account_dao().accounts().count(id_));
 
-    dao_->accounts().at(id_).firstname_ = name;
+    db_.test_account_dao().accounts().at(id_)["firstname"] = name;
 }
 
 void test_account::do_set_lastname(const std::string& name)
 {
-    assert(dao_->accounts().count(id_));
+    assert(db_.test_account_dao().accounts().count(id_));
 
-    dao_->accounts().at(id_).lastname_ = name;
+    db_.test_account_dao().accounts().at(id_)["lastname"] = name;
 }
 
-std::shared_ptr<account> test_dao::do_find_by_login(const std::string& login)
+nlohmann::json test_account::serialize() const
+{
+    return {
+        { "id", id_ },
+        { "login", login_ },
+        { "password", password_ },
+        { "email", email_ },
+        { "firstname", firstname_ },
+        { "lastname", lastname_ }
+    };
+}
+
+void test_account::unserialize(const nlohmann::json& input)
+{
+    id_ = input["id"];
+    login_ = input["login"];
+    password_ = input["password"];
+    email_ = input["email"];
+    firstname_ = input["firstname"];
+    lastname_ = input["lastname"];
+}
+
+std::unique_ptr<account> test_dao::find_by_login(const std::string& login)
 {
     for (const auto& pair : accounts_) {
-        if (pair.second.login() == login) {
-            auto a = std::make_shared<test_account>(shared_from_this(),
-                pair.second.login(), pair.second.password());
+        if (pair.second["login"] == login) {
+            auto a = std::make_unique<test_account>(pair.second["login"], pair.second["password"], db_);
 
-            a->id_ = pair.first;
-            a->email_ = pair.second.email_;
-            a->firstname_ = pair.second.firstname_;
-            a->lastname_ = pair.second.lastname_;
+            a->unserialize(pair.second);
+            a->characters_ = db_.test_character_dao().characters_for_account(a->id_);
 
             return a;
         }
--- a/libserver-test/malikania/server/db/test_account.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver-test/malikania/server/db/test_account.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -26,12 +26,18 @@
 
 #include <unordered_map>
 
+#include <json.hpp>
+
 #include <malikania/server/db/account.hpp>
 
+#include "test_character.hpp"
+
 namespace mlk {
 
 namespace server {
 
+class test_database;
+
 /**
  * \brief Database account object (memory implementation)
  */
@@ -40,19 +46,36 @@
     class test_dao;
 
 private:
-    std::shared_ptr<test_dao> dao_;
+    test_database& db_;
 
 public:
     /**
      * Construct a test_account.
      *
-     * \param dao the dao owner
      * \param login the login
      * \param password the password
+     * \param db the test_database
      */
-    test_account(std::shared_ptr<test_dao> dao,
-                 std::string login,
-                 std::string password) noexcept;
+    inline test_account(std::string login, std::string password, test_database& db) noexcept
+        : account(std::move(login), std::move(password))
+        , db_(db)
+    {
+    }
+
+    /**
+     * Dump this account as json.
+     *
+     * \return the json
+     */
+    nlohmann::json serialize() const;
+
+    /**
+     * Fill this account with json input.
+     *
+     * \warning json input must be valid, no checks are performed
+     * \param input the json input
+     */
+    void unserialize(const nlohmann::json& json);
 
     /**
      * \copydoc account::do_save
@@ -60,6 +83,11 @@
     void do_save() override;
 
     /**
+     * \copydoc account::do_remove
+     */
+    void do_remove() override;
+
+    /**
      * \copydoc account::do_set_password
      */
     void do_set_password(const std::string& password) override;
@@ -86,20 +114,29 @@
  * This class saves accounts in memory, thus once deleted the no accounts are
  * available anymore.
  */
-class test_account::test_dao
-    : public account::dao
-    , public std::enable_shared_from_this<test_account::test_dao> {
+class test_account::test_dao : public account::dao {
 private:
-    std::unordered_map<std::int64_t, test_account> accounts_;
+    std::unordered_map<std::int64_t, nlohmann::json> accounts_;
     std::int64_t sequence_{0};
+    test_database& db_;
 
 public:
     /**
+     * Construct the dao, requires character_dao to load characters.
+     *
+     * \param db the test databasse
+     */
+    inline test_dao(test_database& db) noexcept
+        : db_(db)
+    {
+    }
+
+    /**
      * Get the in memory accounts.
      *
      * \return the account map
      */
-    inline const std::unordered_map<std::int64_t, test_account>& accounts() const noexcept
+    inline const std::unordered_map<std::int64_t, nlohmann::json>& accounts() const noexcept
     {
         return accounts_;
     }
@@ -109,7 +146,7 @@
      *
      * \return the account map
      */
-    inline std::unordered_map<std::int64_t, test_account>& accounts() noexcept
+    inline std::unordered_map<std::int64_t, nlohmann::json>& accounts() noexcept
     {
         return accounts_;
     }
@@ -125,9 +162,9 @@
     }
 
     /**
-     * \copydoc dao::do_find_by_login
+     * \copydoc dao::find_by_login
      */
-    std::shared_ptr<account> do_find_by_login(const std::string& login) override;
+    std::unique_ptr<account> find_by_login(const std::string& login) override;
 };
 
 } // !server
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libserver-test/malikania/server/db/test_character.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -0,0 +1,99 @@
+/*
+ * test_character.cpp -- database account object (memory implementation)
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "test_database.hpp"
+
+namespace mlk {
+
+namespace server {
+
+using test_dao = test_character::test_dao;
+
+void test_character::do_save(std::int64_t account_id)
+{
+    id_ = db_.test_character_dao().next_id();
+    account_id_ = account_id;
+    db_.test_character_dao().characters().emplace(id_, serialize());
+}
+
+void test_character::do_remove()
+{
+    db_.test_character_dao().characters().erase(id_);
+}
+
+void test_character::do_set_level(std::uint16_t level)
+{
+    assert(db_.test_character_dao().characters().count(id_));
+
+    db_.test_character_dao().characters().at(id_)["level"] = level;
+}
+
+nlohmann::json test_character::serialize() const
+{
+    return {
+        { "id", id_ },
+        { "account_id", account_id_ },
+        { "nickname", nickname_ },
+        { "classname", classname_ },
+        { "level", level_ }
+    };
+}
+
+void test_character::unserialize(const nlohmann::json& json)
+{
+    id_ = json["id"];
+    account_id_ = json["account_id"];
+    nickname_ = json["nickname"];
+    classname_ = json["classname"];
+    level_ = json["level"];
+}
+
+character_set test_dao::characters_for_account(std::int64_t account_id)
+{
+    character_set set;
+
+    for (const auto& pair : characters_) {
+        if (pair.second["account_id"] == account_id) {
+            auto c = std::make_unique<test_character>(
+                pair.second["nickname"],
+                pair.second["classname"],
+                db_
+            );
+
+            c->unserialize(pair.second);
+            set.insert(std::move(c));
+        }
+    }
+
+    return set;
+}
+
+void test_dao::remove_all(std::int64_t account_id)
+{
+    for (auto it = characters_.begin(); it != characters_.end(); ) {
+        if (it->second["account_id"] == account_id) {
+            db_.test_character_dao().remove_all(it->second["id"]);
+            it = characters_.erase(it);
+        } else
+            it++;
+    }
+}
+
+} // !server
+
+} // !mlk
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libserver-test/malikania/server/db/test_character.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -0,0 +1,160 @@
+/*
+ * test_character.hpp -- database account object (memory implementation)
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MALIKANIA_SERVER_TEST_CHARACTER_HPP
+#define MALIKANIA_SERVER_TEST_CHARACTER_HPP
+
+/**
+ * \file test_account.hpp
+ * \brief Database account object (memory implementation)
+ */
+
+#include <unordered_map>
+
+#include <json.hpp>
+
+#include <malikania/server/db/character.hpp>
+
+namespace mlk {
+
+namespace server {
+
+class test_database;
+
+/**
+ * \brief Database account object (memory implementation)
+ */
+class test_character : public character {
+public:
+    class test_dao;
+
+private:
+    test_database& db_;
+
+protected:
+    /**
+     * \copydoc character::do_save
+     */
+    void do_save(std::int64_t account_id) override;
+
+    /**
+     * \copydoc character::do_remove
+     */
+    void do_remove() override;
+
+    /**
+     * \copydoc character::do_set_level
+     */
+    void do_set_level(std::uint16_t level) override;
+
+public:
+    /**
+     * Construct a test account.
+     *
+     * \param nickname the nickname
+     * \param classname the classname
+     * \param db reference to test_database
+     */
+    inline test_character(std::string nickname, std::string classname, test_database& db) noexcept
+        : character(std::move(nickname), std::move(classname))
+        , db_(db)
+    {
+    }
+
+    /**
+     * Dump this account as json.
+     *
+     * \return the json
+     */
+    nlohmann::json serialize() const;
+
+    /**
+     * Fill this account with json input.
+     *
+     * \warning json input must be valid, no checks are performed
+     * \param input the json input
+     */
+    void unserialize(const nlohmann::json& json);
+};
+
+/**
+ * \brief Character dao for test_character.
+ *
+ * This class saves characters in memory, thus once deleted the no characters
+ * are available anymore.
+ */
+class test_character::test_dao {
+private:
+    std::unordered_map<std::int64_t, nlohmann::json> characters_;
+    std::int64_t sequence_{0};
+    test_database& db_;
+
+public:
+    inline test_dao(test_database& db) noexcept
+        : db_(db)
+    {
+    }
+
+    /**
+     * Get the in memory characters.
+     *
+     * \return the character map
+     */
+    inline const std::unordered_map<std::int64_t, nlohmann::json>& characters() const noexcept
+    {
+        return characters_;
+    }
+
+    /**
+     * Overloaded function.
+     *
+     * \return the character map
+     */
+    inline std::unordered_map<std::int64_t, nlohmann::json>& characters() noexcept
+    {
+        return characters_;
+    }
+
+    /**
+     * Compute the next id from the internal sequence.
+     *
+     * \return the next id
+     */
+    inline std::int64_t next_id() noexcept
+    {
+        return ++sequence_;
+    }
+
+    /**
+     * Get the set of characters for the given account.
+     */
+    character_set characters_for_account(std::int64_t account_id);
+
+    /**
+     * Remove all characters for the specified account.
+     *
+     * \param account_id the account id
+     */
+    void remove_all(std::int64_t account_id);
+};
+
+} // !server
+
+} // !mlk
+
+#endif // !MALIKANIA_SERVER_TEST_CHARACTER_HPP
--- a/libserver-test/malikania/server/db/test_database.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver-test/malikania/server/db/test_database.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -23,19 +23,19 @@
 
 namespace server {
 
-std::shared_ptr<account> test_database::account_draft(std::string login, std::string password)
+std::unique_ptr<account> test_database::account_draft(std::string login, std::string password)
 {
-    return std::make_shared<test_account>(account_dao_, std::move(login), std::move(password));
+    return std::make_unique<test_account>(std::move(login), std::move(password), *this);
 }
 
-std::shared_ptr<account::dao> test_database::account_dao()
+account::dao& test_database::account_dao()
 {
     return account_dao_;
 }
 
-std::shared_ptr<character> test_database::character_draft()
+std::unique_ptr<character> test_database::character_draft(std::string nickname, std::string classname)
 {
-    return nullptr;
+    return std::make_unique<test_character>(std::move(nickname), std::move(classname), *this);
 }
 
 } // !server
--- a/libserver-test/malikania/server/db/test_database.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver-test/malikania/server/db/test_database.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -28,6 +28,8 @@
 #include <malikania/server/db/database.hpp>
 
 #include "test_account.hpp"
+#include "test_character.hpp"
+#include "test_spell.hpp"
 
 namespace mlk {
 
@@ -38,33 +40,55 @@
  */
 class test_database : public database {
 private:
-    std::shared_ptr<test_account::test_dao> account_dao_{new test_account::test_dao};
+    test_spell::test_dao spell_dao_{*this};
+    test_character::test_dao character_dao_{*this};
+    test_account::test_dao account_dao_{*this};
 
 public:
     /**
-     * Get the real underlying test_dao type.
+     * Get the real underlying test_dao type for accounts.
      *
      * \return the test_dao instance
      */
-    inline std::shared_ptr<test_account::test_dao> test_account_dao() noexcept
+    inline test_account::test_dao& test_account_dao() noexcept
     {
         return account_dao_;
     }
 
     /**
+     * Get the real underlying test_dao type for characters.
+     *
+     * \return the test_character::test_dao instance
+     */
+    inline test_character::test_dao& test_character_dao() noexcept
+    {
+        return character_dao_;
+    }
+
+    /**
+     * Get the real underlying test_dao type for spells.
+     *
+     * \return the test_spell::test_dao instance
+     */
+    inline test_spell::test_dao& test_spell_dao() noexcept
+    {
+        return spell_dao_;
+    }
+
+    /**
      * \copydoc database::account_draft
      */
-    std::shared_ptr<account> account_draft(std::string login, std::string password) override;
+    std::unique_ptr<account> account_draft(std::string login, std::string password) override;
 
     /**
      * \copydoc database::account_dao
      */
-    std::shared_ptr<account::dao> account_dao() override;
+    account::dao& account_dao() override;
 
     /**
      * \copydoc database::character_draft
      */
-    std::shared_ptr<character> character_draft() override;
+    std::unique_ptr<character> character_draft(std::string nickname, std::string classname) override;
 };
 
 } // !server
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libserver-test/malikania/server/db/test_spell.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -0,0 +1,74 @@
+#include "test_database.hpp"
+
+namespace mlk {
+
+namespace server {
+
+using test_dao = test_spell::test_dao;
+
+void test_spell::do_save(std::int64_t character_id)
+{
+    id_ = db_.test_spell_dao().next_id();
+    character_id_ = character_id;
+    db_.test_spell_dao().spells().emplace(id_, serialize());
+}
+
+void test_spell::do_remove()
+{
+    db_.test_spell_dao().spells().erase(id_);
+}
+
+void test_spell::do_set_level(std::uint8_t level)
+{
+    assert(db_.test_spell_dao().spells().count(id_));
+
+    db_.test_spell_dao().spells().at(id_)["level"] = level;
+}
+
+nlohmann::json test_spell::serialize() const
+{
+    return {
+        { "id", id_ },
+        { "character_id", character_id_ },
+        { "classname", classname_ },
+        { "level", level_ }
+    };
+}
+
+void test_spell::unserialize(const nlohmann::json& json)
+{
+    id_ = json["id"];
+    character_id_ = json["character_id"];
+    classname_ = json["classname"];
+    level_ = json["level"];
+}
+
+spell_set test_dao::spells_for_charater(std::int64_t character_id)
+{
+    spell_set set;
+
+    for (const auto& pair : spells_) {
+        if (pair.second["character_id"] == character_id) {
+            auto s = std::make_unique<test_spell>(pair.second["classname"], db_);
+
+            s->unserialize(pair.second);
+            set.insert(std::move(s));
+        }
+    }
+
+    return set;
+}
+
+void test_dao::remove_all(std::int64_t character_id)
+{
+    for (auto it = spells_.begin(); it != spells_.end(); ) {
+        if (it->second["character_id"] == character_id)
+            it = spells_.erase(it);
+        else
+            it++;
+    }
+}
+
+} // !server
+
+} // !mlk
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libserver-test/malikania/server/db/test_spell.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -0,0 +1,136 @@
+/*
+ * test_spell.hpp -- database spell object (memory implementation)
+ *
+ * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MALIKANIA_SERVER_TEST_SPELL_HPP
+#define MALIKANIA_SERVER_TEST_SPELL_HPP
+
+/**
+ * \file test_spell.hpp
+ * \brief Database spell object (memory implementation).
+ */
+
+#include <unordered_map>
+
+#include <malikania/server/db/spell.hpp>
+
+#include <json.hpp>
+
+namespace mlk {
+
+namespace server {
+
+class test_database;
+
+/**
+ * \brief Database spell object (memory implementation).
+ */
+class test_spell : public spell {
+public:
+    class test_dao;
+
+private:
+    test_database& db_;
+
+protected:
+    void do_save(std::int64_t character_id) override;
+    void do_remove() override;
+    void do_set_level(std::uint8_t level) override;
+
+public:
+    inline test_spell(std::string classname, test_database& db) noexcept
+        : spell(std::move(classname))
+        , db_(db)
+    {
+    }
+
+    /**
+     * Dump this character as json.
+     *
+     * \return the json
+     */
+    nlohmann::json serialize() const;
+
+    /**
+     * Fill this character with json input.
+     *
+     * \warning json input must be valid, no checks are performed
+     * \param input the json input
+     */
+    void unserialize(const nlohmann::json& json);
+};
+
+class test_spell::test_dao {
+private:
+    std::unordered_map<std::int64_t, nlohmann::json> spells_;
+    std::int64_t sequence_{0};
+    test_database& db_;
+
+public:
+    inline test_dao(test_database& db) noexcept
+        : db_(db)
+    {
+    }
+
+    /**
+     * Get the in memory spells.
+     *
+     * \return the spell map
+     */
+    inline const std::unordered_map<std::int64_t, nlohmann::json>& spells() const noexcept
+    {
+        return spells_;
+    }
+
+    /**
+     * Overloaded function.
+     *
+     * \return the spell map
+     */
+    inline std::unordered_map<std::int64_t, nlohmann::json>& spells() noexcept
+    {
+        return spells_;
+    }
+
+    /**
+     * Compute the next id from the internal sequence.
+     *
+     * \return the next id
+     */
+    inline std::int64_t next_id() noexcept
+    {
+        return ++sequence_;
+    }
+
+    /**
+     * Get the set of spells for the given character.
+     */
+    spell_set spells_for_charater(std::int64_t character_id);
+
+    /**
+     * Remove all characters for the specified character.
+     *
+     * \param character_id the character id
+     */
+    void remove_all(std::int64_t character_id);
+};
+
+} // !server
+
+} // !mlk
+
+#endif // !MALIKANIA_SERVER_TEST_SPELL_HPP
--- a/libserver/CMakeLists.txt	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/CMakeLists.txt	Mon Sep 11 13:18:43 2017 +0200
@@ -21,10 +21,8 @@
 set(
     HEADERS
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/account.hpp
-    ${libmlk-server_SOURCE_DIR}/malikania/server/db/account_dao.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/character.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/database.hpp
-    ${libmlk-server_SOURCE_DIR}/malikania/server/db/dynlib_database.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/spell.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/model.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.hpp
@@ -36,8 +34,6 @@
     SOURCES
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/account.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/character.cpp
-    ${libmlk-server_SOURCE_DIR}/malikania/server/db/dynlib_database.cpp
-    ${libmlk-server_SOURCE_DIR}/malikania/server/db/spell.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/client.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/server.cpp
--- a/libserver/malikania/server/db/account.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/account.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -17,8 +17,6 @@
  */
 
 #include "account.hpp"
-#include "account_dao.hpp"
-#include "character.hpp"
 #include "database.hpp"
 
 namespace mlk {
@@ -29,62 +27,39 @@
  * account::add_character
  * ------------------------------------------------------------------
  */
-
-void account::add_character(std::shared_ptr<character> ch)
+const std::unique_ptr<character>& account::add_character(std::unique_ptr<character> ch)
 {
     assert(is_published());
     assert(ch && ch->is_draft());
 
-    ch->do_link(id_);
-    characters_.insert(std::move(ch));
+    ch->do_save(id_);
+
+    return *characters_.insert(std::move(ch)).first;
 }
 
 /*
  * account::remove_character
  * ------------------------------------------------------------------
  */
-
-void account::remove_character(std::shared_ptr<character> ch)
+void account::remove_character(const std::unique_ptr<character>& ch)
 {
-    assert(ch);
-    assert(ch->is_published());
+    assert(is_published());
+    assert(ch && ch->is_published());
 
     auto it = characters_.find(ch);
 
     if (it == characters_.end())
         return;
 
-    ch->do_unlink();
+    ch->do_remove();
     characters_.erase(it);
 }
 
 /*
- * account::dao::find_by_login
- * ------------------------------------------------------------------
- */
-
-std::shared_ptr<account> account::dao::find_by_login(const std::string& login)
-{
-    auto ac = accounts_.find_if([&] (const auto& a) {
-        return a->login() == login;
-    });
-
-    if (!ac) {
-        ac = do_find_by_login(login);
-
-        if (ac)
-            accounts_.push_back(ac);
-    }
-
-    return ac;
-}
-
-/*
  * account::dao::authenticate
  * ------------------------------------------------------------------
  */
-
-std::shared_ptr<account> account::dao::authenticate(const std::string& login,
+std::unique_ptr<account> account::dao::authenticate(const std::string& login,
                                                     const std::string& password)
 {
     auto ac = find_by_login(login);
--- a/libserver/malikania/server/db/account.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/account.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -19,42 +19,103 @@
 #ifndef MALIKANIA_SERVER_DB_ACCOUNT_HPP
 #define MALIKANIA_SERVER_DB_ACCOUNT_HPP
 
-#include <string>
-#include <unordered_set>
-
-#include <malikania/weak_array.hpp>
+/**
+ * \file account.hpp
+ * \brief Database account object.
+ */
 
 #include "model.hpp"
+#include "character.hpp"
 
 namespace mlk {
 
 namespace server {
 
-class account : public model<account> {
+/**
+ * \brief Database account object.
+ */
+class account : public model {
 public:
     class dao;
 
-    using character_set_t = std::unordered_set<std::shared_ptr<class character>>;
+protected:
+    std::string login_;                 //!< unique login (not null)
+    std::string password_;              //!< password stored as-is
+    std::string email_;                 //!< raw email
+    std::string firstname_;             //!< optional first name
+    std::string lastname_;              //!< optional last name
+    character_set characters_;          //!< set of characters
 
-protected:
-    std::string login_;
-    std::string password_;
-    std::string email_;
-    std::string firstname_;
-    std::string lastname_;
-    character_set_t characters_;
+    /**
+     * Save the account.
+     *
+     * The implementation will be called only if the account is draft and must
+     * do the following:
+     *
+     * 1. Register the account to its database,
+     * 2. Create a unique and global id and set it as id_ member variable.
+     *
+     * \note called from account::save
+     * \throw std::exception if the operation could not succeed
+     */
+    virtual void do_save() = 0;
+
+    /**
+     * Remove the account.
+     *
+     * The implementation will be called only if the account is public and must
+     * do remove from the database all the associated characters along with
+     * their spells.
+     *
+     * \note called from account::remove
+     * \throw std::exception if the operation could not succeed
+     */
+    virtual void do_remove() = 0;
 
-    virtual void do_save() = 0;
+    /**
+     * Update the account password in database.
+     *
+     * Only called when the password needs to be changed, the implementation
+     * does not need to update password_ field.
+     *
+     * \note called from set_password helper
+     * \throw std::exception if the operation could not succeed
+     */
     virtual void do_set_password(const std::string& password) = 0;
+
+    /**
+     * Update the account email in database.
+     *
+     * Only called when the email needs to be changed, the implementation does
+     * not need to update email_ field.
+     *
+     * \note called from set_email helper
+     * \throw std::exception if the operation could not succeed
+     */
     virtual void do_set_email(const std::string& email) = 0;
+
+    /**
+     * Update the account firstname in database.
+     *
+     * Only called when the first name needs to be changed, the implementation
+     * does not need to update firstname_ field.
+     *
+     * \note called from set_firstname helper
+     * \throw std::exception if the operation could not succeed
+     */
     virtual void do_set_firstname(const std::string& name) = 0;
+
+    /**
+     * Update the account lastname in database.
+     *
+     * Only called when the last name needs to be changed, the implementation
+     * does not need to update lastname_ field.
+     *
+     * \note called from set_lastname helper
+     * \throw std::exception if the operation could not succeed
+     */
     virtual void do_set_lastname(const std::string& name) = 0;
 
-#if 0
-    virtual void do_add_character(const std::shared_ptr<class character>& ch);
-    virtual void do_remove_character(const std::shared_ptr<class character>& ch);
-#endif
-
     /**
      * Create a draft account.
      *
@@ -130,7 +191,7 @@
     {
         assert(!email.empty());
 
-        if (is_published())
+        if (is_published() && email_ != email)
             do_set_email(email);
 
         email_ = std::move(email);
@@ -153,7 +214,7 @@
      */
     inline void set_firstname(std::string name)
     {
-        if (is_published())
+        if (is_published() && firstname_ != name)
             do_set_firstname(name);
 
         firstname_ = std::move(name);
@@ -176,7 +237,7 @@
      */
     inline void set_lastname(std::string name)
     {
-        if (is_published())
+        if (is_published() && lastname_ != name)
             do_set_lastname(name);
 
         lastname_ = std::move(name);
@@ -187,7 +248,7 @@
      *
      * \return the associated characters.
      */
-    inline const character_set_t& characters() const noexcept
+    inline const character_set& characters() const noexcept
     {
         return characters_;
     }
@@ -195,14 +256,22 @@
     /**
      * Add the character to the account.
      *
-     * If the character is draft, it is first saved.
+     * Account takes ownership of character.
      *
-     * \pre the character must be draft or parented to this account
+     * \pre ch->is_draft()
      * \param ch the character
+     * \return the attached character
+     * \post ch->is_published()
      */
-    void add_character(std::shared_ptr<class character> ch);
+    const std::unique_ptr<character>& add_character(std::unique_ptr<character> ch);
 
-    void remove_character(std::shared_ptr<class character>);
+    /**
+     * Remove the character from the account.
+     *
+     * \param ch the character
+     * \post ch->is_draft() and empty
+     */
+    void remove_character(const std::unique_ptr<character>& ch);
 
     /**
      * Save the account, does nothing if is_published().
@@ -217,37 +286,40 @@
 
         assert(is_published());
     }
+
+    /**
+     * Destroy the account.
+     *
+     * The account will contains no characters anymore.
+     *
+     * \throw std::exception on errors
+     * \post is_draft()
+     */
+    inline void remove()
+    {
+        if (is_published())
+            do_remove();
+
+        id_ = -1;
+        characters_.clear();
+
+        assert(is_draft());
+    }
 };
 
+/**
+ * \brief DAO for accounts.
+ */
 class account::dao {
-private:
-    weak_array<account> accounts_;
-
-protected:
+public:
     /**
-     * Find an account by login
-     *
-     * This function will be called only if no instance of that account is
-     * already loaded.
+     * Find an account by login.
      *
      * \param login the login name
      * \return the account or null if not found
      * \throw exception on other errors
      */
-    virtual std::shared_ptr<account> do_find_by_login(const std::string& login) = 0;
-
-public:
-    /**
-     * Find an acocunt by login.
-     *
-     * If a previous login was found, return that instance, othwerise, search
-     * the database to load it.
-     *
-     * \param login the login name
-     * \return the account or nullptr if not found
-     * \throw std::exception on any other errors
-     */
-    std::shared_ptr<account> find_by_login(const std::string& login);
+    virtual std::unique_ptr<account> find_by_login(const std::string& login) = 0;
 
     /**
      * Authenticate the user.
@@ -256,7 +328,7 @@
      * \param password the password
      * \return the account or null if does not exist
      */
-    std::shared_ptr<account> authenticate(const std::string& login,
+    std::unique_ptr<account> authenticate(const std::string& login,
                                           const std::string& password);
 };
 
--- a/libserver/malikania/server/db/account_dao.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/*
- * account_dao.hpp -- database account interface
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef MALIKANIA_SERVER_DB_ACCOUNT_DAO_HPP
-#define MALIKANIA_SERVER_DB_ACCOUNT_DAO_HPP
-
-/**
- * \file account_dao.hpp
- * \brief Database account interface.
- */
-
-#include <cstdint>
-#include <string>
-
-#include "dao.hpp"
-
-namespace mlk {
-
-namespace server {
-
-class account;
-
-/**
- * \file account_dao.hpp
- * \brief Database account interface.
- */
-class account_dao : public dao {
-public:
-    /**
-     * Inherited constructors.
-     */
-    using dao::dao;
-
-    /**
-     * Virtual destructor defaulted.
-     */
-    virtual ~account_dao() noexcept = default;
-
-    /**
-     * Save the account and return its new id.
-     *
-     * \param ac the account
-     * \return the newly created id
-     * \post returned it >= 0
-     */
-    virtual std::int64_t save(std::shared_ptr<account> ac) = 0;
-
-    /**
-     * Find an account, the implementation must return the same instance of the
-     * account.
-     *
-     * \param login the account id
-     * \return the account or null if does not exist
-     */
-    virtual std::shared_ptr<account> find(const std::string& login) = 0;
-
-    /**
-     * Authenticate the user.
-     *
-     * \param login the login
-     * \param password the password
-     * \return the account or null if does not exist
-     */
-    virtual std::shared_ptr<account> authenticate(const std::string& login,
-                                                  const std::string& password) = 0;
-
-    /**
-     * Update the account email.
-     *
-     * \param ac the account owner
-     * \param email the new email
-     */
-    virtual void set_email(std::shared_ptr<account> ac, const std::string& email) = 0;
-
-    /**
-     * Update the account first name.
-     *
-     * \param ac the account owner
-     * \param name the first name
-     */
-    virtual void set_firstname(std::shared_ptr<account> ac, const std::string& name) = 0;
-
-    /**
-     * Update the account last name.
-     *
-     * \param ac the account owner
-     * \param name the new name
-     */
-    virtual void set_lastname(std::shared_ptr<account> ac, const std::string& name) = 0;
-};
-
-} // !server
-
-} // !mlk
-
-#endif // !MALIKANIA_SERVER_DB_ACCOUNT_DAO_HPP
--- a/libserver/malikania/server/db/character.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/character.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -16,103 +16,44 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <algorithm>
-#include <cassert>
-#include <stdexcept>
-#include <string>
-
-#include "account.hpp"
 #include "character.hpp"
-#include "character_dao.hpp"
-#include "database.hpp"
-#include "spell.hpp"
-#include "spell_dao.hpp"
 
 namespace mlk {
 
 namespace server {
 
-#if 0
-
-void character::set_name(std::string name)
+/*
+ * character::add_spell
+ * ------------------------------------------------------------------
+ */
+const std::unique_ptr<spell>& character::add_spell(std::unique_ptr<spell> sp)
 {
-    if (is_published())
-        db_->character_dao()->set_name(shared_from_this(), name);
+    assert(is_published());
+    assert(sp && sp->is_draft());
 
-    name_ = std::move(name);
+    sp->do_save(id_);
+
+    return *spells_.insert(std::move(sp)).first;
 }
 
-void character::add_spell(std::shared_ptr<spell>)
+/*
+ * character::remove_spell
+ * ------------------------------------------------------------------
+ */
+void character::remove_spell(const std::unique_ptr<spell>& sp)
 {
-#if 0
-    assert(spell);
-
-    if (std::find(spells_.begin(), spells_.end(), spell) != spells_.end())
-        return;
-    if (spell->is_published())
-        throw referenced_error(shared_from_this(), spell);
+    assert(is_published());
+    assert(sp && sp->is_published());
 
-    db_->spell_dao()->save(*spell, *this);
-    spells_.push_back(std::move(spell));
-#endif
-}
+    auto it = spells_.find(sp);
 
-void character::remove_spell(std::shared_ptr<spell>)
-{
-#if 0
-    assert(spell);
-
-    if (std::find(spells_.begin(), spells_.end(), spell) == spells_.end())
+    if (it == spells_.end())
         return;
 
-    db_->spell_dao()->remove(*spell);
-#if 0
-    spell->set_id(-1);
-    spell->set_character(nullptr);
-    spells_.erase(std::remove(spells_.begin(), spells_.end(), spell), spells_.end());
-#endif
-#endif
-}
-
-void character::save(std::shared_ptr<class account> parent)
-{
-    if (is_published())
-        return;
-
-    assert(parent);
-    assert(parent->is_published());
-
-    auto self = shared_from_this();
-
-    id_ = db_->character_dao()->save(self, parent);
-    account_ = parent;
-
-    parent->add_character(self);
+    sp->do_remove();
+    spells_.erase(it);
 }
 
-void character::remove()
-{
-    if (is_draft())
-        return;
-
-    auto self = shared_from_this();
-
-    if (is_published())
-        db_->character_dao()->remove(self);
-
-    id_ = -1;
-
-    auto parent = account_.lock();
-
-    if (parent)
-        parent->remove_character(self);
-
-    account_.reset();
-    spells_.clear();
-}
-
-#endif
-
 } // !server
 
 } // !mlk
--- a/libserver/malikania/server/db/character.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/character.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -19,37 +19,66 @@
 #ifndef MALIKANIA_SERVER_DB_CHARACTER_HPP
 #define MALIKANIA_SERVER_DB_CHARACTER_HPP
 
-#include <cstdint>
-#include <memory>
-#include <string>
-#include <vector>
+/*
+ * \file character.hpp
+ * \brief Database character object.
+ */
 
 #include "model.hpp"
+#include "spell.hpp"
 
 namespace mlk {
 
 namespace server {
 
-class spell;
-
-/**
- * \brief Describe an account character.
+/*
+ * \brief Database character object.
  */
-class character : public model<character> {
+class character : public model {
 private:
     friend class account;
 
-public:
-    class dao;
+protected:
+    std::int64_t account_id_{-1};       //!< parent acocunt
+    std::string nickname_;              //!< nickname (non null)
+    std::string classname_;             //!< class type to instanciate
+    std::uint16_t level_{1};            //!< character level
+    spell_set spells_;                  //!< set of spells
+
+    /**
+     * Save this character.
+     *
+     * The implementation must save the character and update the id, account_id
+     * member variables.
+     *
+     * Then it will be added into account.characters_ variable.
+     *
+     * \param account_id the parent account
+     * \note called by account::add_character.
+     * \throw std::exception if the operation could not succeed
+     */
+    virtual void do_save(std::int64_t account_id) = 0;
 
-protected:
-    std::string nickname_;
-    std::string classname_;
-    std::uint16_t level_{1};
+    /**
+     * Remove this character.
+     *
+     * The implementation must remove the character from the database and update
+     * id, account_id member variables.
+     *
+     * \note called by account::remove_character.
+     */
+    virtual void do_remove() = 0;
 
+    /**
+     * Update the character level in database.
+     *
+     * Only called when the level needs to be changed, the implementation does
+     * not need to update level_ field.
+     *
+     * \note called from set_level helper
+     * \throw std::exception if the operation could not succeed
+     */
     virtual void do_set_level(std::uint16_t level) = 0;
-    virtual void do_link(std::int64_t account_id) = 0;
-    virtual void do_unlink() = 0;
 
     /**
      * Construct a character, no database is modified yet.
@@ -99,56 +128,52 @@
     }
 
     /**
+     * Get the set of spells.
+     *
+     * \return the spells
+     */
+    inline const spell_set& spells() const noexcept
+    {
+        return spells_;
+    }
+
+    /**
+     * Add a spell.
+     *
+     * Character takes ownership of spell.
+     *
+     * \pre sp->is_draft()
+     * \param sp the spell
+     * \return the attached spell
+     */
+    const std::unique_ptr<spell>& add_spell(std::unique_ptr<spell> sp);
+
+    /**
+     * Remove the spell from the character.
+     *
+     * \param sp the spell
+     * \post sp->is_draft() and empty
+     */
+    void remove_spell(const std::unique_ptr<spell>& sp);
+
+    /**
      * Set the account level.
      *
      * \param level the level
      */
     inline void set_level(std::uint16_t level)
     {
-        if (is_published())
+        if (is_published() && level_ != level)
             do_set_level(level);
-    }
-
-#if 0
-
-    /**
-     * Add a spell into the database and the object.
-     *
-     * \param spell the spell
-     * \throw referenced_error if spell is public
-     * \throw phase_error if character is not public
-     */
-    void add_spell(std::shared_ptr<spell> spell);
 
-    /**
-     * Remove the specified spell.
-     *
-     * \param spell the spell to remove
-     */
-    void remove_spell(std::shared_ptr<spell> spell);
-
-    /**
-     * Save the character and update its parent.
-     *
-     * No-op if already published.
-     *
-     * \pre parent is not null and parent->is_published()
-     * \param parent the parent account
-     */
-    void save(std::shared_ptr<class account> parent);
-
-    /**
-     * Remove the character and update its parent.
-     */
-    void remove();
-#endif
+        level_ = level;
+    }
 };
 
-class character::dao {
-public:
-    dao() noexcept = default;
-    virtual ~dao() noexcept = default;
-};
+/**
+ * Type for storing characters.
+ */
+using character_set = std::unordered_set<std::unique_ptr<character>>;
 
 } // !server
 
--- a/libserver/malikania/server/db/character_dao.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-/*
- * character_dao.hpp -- database character interface
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef MALIKANIA_SERVER_DB_CHARACTER_DAO_HPP
-#define MALIKANIA_SERVER_DB_CHARACTER_DAO_HPP
-
-#include <cstdint>
-#include <string>
-
-#include "dao.hpp"
-
-namespace mlk {
-
-namespace server {
-
-class account;
-class character;
-
-class character_dao : public dao {
-public:
-    /**
-     * Inherited constructors.
-     */
-    using dao::dao;
-
-    virtual std::int64_t save(std::shared_ptr<character>, std::shared_ptr<account>) = 0;
-
-    virtual void set_name(std::shared_ptr<character>, const std::string&) = 0;
-
-    virtual void remove(std::shared_ptr<character>) = 0;
-};
-
-} // !server
-
-} // !mlk
-
-#endif // !MALIKANIA_SERVER_DB_CHARACTER_DAO_HPP
--- a/libserver/malikania/server/db/dao.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-#ifndef MALIKANIA_SERVER_DB_DAO_HPP
-#define MALIKANIA_SERVER_DB_DAO_HPP
-
-#include <cassert>
-#include <memory>
-#include <utility>
-
-namespace mlk {
-
-namespace server {
-
-class database;
-
-class dao {
-protected:
-    std::shared_ptr<database> db_;
-
-public:
-    /**
-     * Construct the dao object.
-     *
-     * \pre db != nullptr
-     * \param db the database object
-     */
-    inline dao(std::shared_ptr<database> db) noexcept
-        : db_(std::move(db))
-    {
-        assert(db_);
-    }
-
-    /**
-     * Virtual destructor defaulted.
-     */
-    virtual ~dao() noexcept = default;
-};
-
-} // !server
-
-} // !mlk
-
-#endif // !MALIKANIA_SERVER_DB_DAO_HPP
--- a/libserver/malikania/server/db/database.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/database.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -19,7 +19,10 @@
 #ifndef MALIKANIA_SERVER_DB_DATABASE_HPP
 #define MALIKANIA_SERVER_DB_DATABASE_HPP
 
-#include <memory>
+/**
+ * \file database.hpp
+ * \brief Abstract database interface.
+ */
 
 #include "account.hpp"
 #include "character.hpp"
@@ -29,6 +32,9 @@
 
 namespace server {
 
+/**
+ * \brief Abstract database interface.
+ */
 class database {
 public:
     /**
@@ -57,22 +63,24 @@
      * \post value->is_draft()
      * \see account::account
      */
-    virtual std::shared_ptr<account> account_draft(std::string login, std::string password) = 0;
+    virtual std::unique_ptr<account> account_draft(std::string login, std::string password) = 0;
 
     /**
      * Create the account dao.
      *
      * \return the account dao
      */
-    virtual std::shared_ptr<account::dao> account_dao() = 0;
+    virtual account::dao& account_dao() = 0;
 
     /**
      * Create a draft character.
      *
+     * \param nickname the character nickname
+     * \param classname the character class type
      * \return the draft character
      * \post value->is_draft()
      */
-    virtual std::shared_ptr<character> character_draft() = 0;
+    virtual std::unique_ptr<character> character_draft(std::string nickname, std::string classname) = 0;
 };
 
 } // !server
--- a/libserver/malikania/server/db/dynlib_database.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/dynlib_database.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -16,9 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include "account_dao.hpp"
-#include "character_dao.hpp"
-#include "spell_dao.hpp"
 #include "dynlib_database.hpp"
 
 namespace mlk {
--- a/libserver/malikania/server/db/model.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/model.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -19,63 +19,42 @@
 #ifndef MALIKANIA_SERVER_DB_MODEL_HPP
 #define MALIKANIA_SERVER_DB_MODEL_HPP
 
+/**
+ * \file model.hpp
+ * \brief Abstract database object.
+ */
+
 #include <cassert>
 #include <cstdint>
-#include <exception>
-#include <memory>
-#include <ostream>
-#include <sstream>
-#include <string>
 
 namespace mlk {
 
 namespace server {
 
 /**
- * \brief Describe availability of an object.
- */
-enum class model_phase {
-    draft,                  //!< object is not saved
-    published               //!< object is saved
-};
-
-/**
- * Show the model_phase.
- *
- * \param out the output stream
- * \param phase the object phase
- * \return out
+ * \brief Abstract database object.
  */
-inline std::ostream& operator<<(std::ostream& out, model_phase phase)
-{
-    out << (phase == model_phase::draft ? "draft" : "published");
-
-    return out;
-}
+class model {
+private:
+    model(const model&) = delete;
+    model& operator=(const model&) = delete;
 
-/**
- * \brief Base class for database object.
- */
-class basic_model {
+    model(model&&) = delete;
+    model& operator=(model&&) = delete;
+
 protected:
-    std::int64_t id_{-1};                   //!< object id
-#if 0
-    std::shared_ptr<database> db_;          //!< database object
-#endif
+    std::int64_t id_{-1};               //!< object id
 
 public:
-#if 0
-    inline basic_model(std::shared_ptr<database> db) noexcept
-        : db_(std::move(db))
-    {
-    }
-#endif
-    basic_model() noexcept = default;
+    /**
+     * Default constructor.
+     */
+    model() noexcept = default;
 
     /**
      * Virtual destructor defaulted.
      */
-    virtual ~basic_model() noexcept = default;
+    virtual ~model() noexcept = default;
 
     /**
      * Get the id.
@@ -87,32 +66,6 @@
         return id_;
     }
 
-#if 0
-
-    /**
-     * Set the model id.
-     *
-     * \pre is_draft()
-     */
-    inline void set_id(std::int64_t id) noexcept
-    {
-        assert(is_draft());
-
-        id_ = id;
-    }
-
-    inline const std::shared_ptr<database>& db() const noexcept
-    {
-        return db_;
-    }
-
-    inline std::shared_ptr<database>& db() noexcept
-    {
-        return db_;
-    }
-
-#endif
-
     /**
      * Tells if the object is not persistent.
      *
@@ -132,174 +85,8 @@
     {
         return id_ >= 0;
     }
-
-    /**
-     * Get the current phase.
-     *
-     * \return the phase
-     */
-    inline model_phase phase() const noexcept
-    {
-        return is_draft() ? model_phase::draft : model_phase::published;
-    }
 };
 
-/**
- * \brief Class helper for std::enabled_shared_from_this.
- */
-template <typename T>
-class model
-    : public basic_model
-    , public std::enable_shared_from_this<T>
-{
-public:
-    /**
-     * Inherited constructors.
-     */
-    using basic_model::basic_model;
-};
-
-#if 0
-
-/**
- * \brief Already referenced error.
- *
- * This class is used when an operation attempted to add a child object that
- * already have a parent.
- */
-class referenced_error : public std::exception {
-private:
-    std::string message_;
-    std::shared_ptr<basic_model> parent_;
-    std::shared_ptr<basic_model> object_;
-
-public:
-    /**
-     * Construct a referenced_error.
-     *
-     * \pre parent != nullptr && object != nullptr
-     * \param parent the parent that already have object
-     * \param object the child object of parent
-     */
-    inline referenced_error(std::shared_ptr<basic_model> parent,
-                            std::shared_ptr<basic_model> object)
-        : parent_(std::move(parent))
-        , object_(std::move(object))
-    {
-        assert(parent_ && object_);
-
-        std::ostringstream oss;
-
-        oss << "object " << object_->table() << "(" << object_->id() << ") is ";
-        oss << "already referenced in " << parent_->table() << "(" << parent_->id() << ")";
-
-        message_ = oss.str();
-    }
-
-    /**
-     * Get the parent object
-     *
-     * \return the parent
-     */
-    inline const std::shared_ptr<basic_model>& parent() const noexcept
-    {
-        return parent_;
-    }
-
-    /**
-     * Get the child object that already have a parent.
-     *
-     * \return the object
-     */
-    inline const std::shared_ptr<basic_model>& object() const noexcept
-    {
-        return object_;
-    }
-
-    /**
-     * Get the error message.
-     *
-     * \return the message
-     */
-    const char* what() const noexcept
-    {
-        return message_.c_str();
-    }
-};
-
-/**
- * \brief Error when an operation required a different phase.
- */
-class phase_error : public std::exception {
-private:
-    std::string message_;
-    std::shared_ptr<basic_model> object_;
-    model_phase expected_phase_;
-
-public:
-    /**
-     * Create a phase_error
-     *
-     * \pre object != nullptr
-     * \param object the object model
-     * \param expected_phase the required phase
-     */
-    inline phase_error(std::shared_ptr<basic_model> object, model_phase expected_phase) noexcept
-        : object_(std::move(object))
-        , expected_phase_(expected_phase)
-    {
-        assert(object_);
-
-        std::ostringstream oss;
-
-        oss << "object '" << object_->table() << "' has invalid phase ";
-        oss << "(phase " << expected_phase_ << " expected, ";
-        oss << "got " << object_->phase() << ")";
-    }
-
-    /**
-     * Get the object.
-     *
-     * \return the object
-     */
-    inline const std::shared_ptr<basic_model>& object() const noexcept
-    {
-        return object_;
-    }
-
-    /**
-     * Get the expected phase.
-     *
-     * \return the expected phase
-     */
-    inline model_phase expected_phase() const noexcept
-    {
-        return expected_phase_;
-    }
-
-    /**
-     * Get the current object phase.
-     *
-     * \return the object phase
-     */
-    inline model_phase acquired_phase() const noexcept
-    {
-        return object_->phase();
-    }
-
-    /**
-     * Get the error message.
-     *
-     * \return the message
-     */
-    const char* what() const noexcept
-    {
-        return message_.c_str();
-    }
-};
-
-#endif
-
 } // !server
 
 } // !mlk
--- a/libserver/malikania/server/db/spell.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/spell.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -19,38 +19,11 @@
 #include "database.hpp"
 #include "character.hpp"
 #include "spell.hpp"
-#include "spell_dao.hpp"
 
 namespace mlk {
 
 namespace server {
 
-#if 0
-
-void spell::set_level(std::uint8_t level)
-{
-#if 0
-    if (is_published())
-        db_->spell_dao()->set_level(shared_from_this(), level);
-
-    level_ = level;
-#endif
-}
-
-#if 0
-
-void spell::set_character(std::shared_ptr<class character> ch)
-{
-    if (is_published())
-        throw std::runtime_error("attempt to reparent spell");
-
-    character_ = std::move(ch);
-}
-
-#endif
-
-#endif
-
 } // !server
 
 } // !mlk
--- a/libserver/malikania/server/db/spell.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/db/spell.hpp	Mon Sep 11 13:18:43 2017 +0200
@@ -19,26 +19,66 @@
 #ifndef MALIKANIA_SERVER_DB_SPELL_HPP
 #define MALIKANIA_SERVER_DB_SPELL_HPP
 
-#include <cstdint>
+/**
+ * \file spell.hpp
+ * \brief Database spell object.
+ */
+
 #include <memory>
+#include <unordered_set>
 
 #include "model.hpp"
 
 namespace mlk {
 
+/**
+ * \brief Database spell object.
+ */
 namespace server {
 
-class character;
-
 /**
  * \brief Describe a spell.
  */
-class spell : public model<spell> {
+class spell : public model {
+private:
+    friend class character;
+
 protected:
-    std::string classname_;
-    std::uint8_t level_{1};
-    std::weak_ptr<class character> character_;
+    std::int64_t character_id_{-1};     //!< parent character
+    std::string classname_;             //!< class type to instanciate
+    std::uint8_t level_{1};             //!< spell level
+
+    /**
+     * Save this spell.
+     *
+     * The implementation must save the spell and update id, character_id
+     * member variables.
+     *
+     * Then it will be added into character.spells_ variable.
+     *
+     * \note called by character::add_spell.
+     */
+    virtual void do_save(std::int64_t character_id) = 0;
 
+    /**
+     * Remove this spell.
+     *
+     * The implementation must remove the spell from the database and update
+     * id, character_id member variables.
+     *
+     * \note called by character::remove_spell
+     */
+    virtual void do_remove() = 0;
+
+    /**
+     * Update the spell level in database.
+     *
+     * Only called when the level needs to be changed, the implementation does
+     * not need to update level_ field.
+     *
+     * \note called from set_level helper
+     * \throw std::exception if the operation could not succeed
+     */
     virtual void do_set_level(std::uint8_t level) = 0;
 
     /**
@@ -76,38 +116,12 @@
 
         level_ = level;
     }
-
-    /**
-     * Get the owner.
-     *
-     * \return the owner or null if is_draft() or if character does not exist
-     */
-    inline std::shared_ptr<class character> character() const noexcept
-    {
-        return character_.lock();
-    }
-
-#if 0
-    /**
-     * Set the character owner.
-     *
-     * \param ch the character (may be null)
-     * \throw referenced_error if is_public()
-     */
-    void set_character(std::shared_ptr<class character> ch);
-#endif
 };
 
-#if 0
-
-class spell::dao {
-public:
-    virtual void save(std::shared_ptr<spell>&, std::shared_ptr<class character>&) = 0;
-    virtual void set_level(std::shared_ptr<spell>&, std::uint8_t) = 0;
-    virtual void remove(std::shared_ptr<spell>&) = 0;
-};
-
-#endif
+/**
+ * Type for storing spells.
+ */
+using spell_set = std::unordered_set<std::unique_ptr<spell>>;
 
 } // !server
 
--- a/libserver/malikania/server/db/spell_dao.hpp	Mon Sep 11 16:26:35 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-/*
- * spell_dao.hpp -- database spell interface
- *
- * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef MALIKANIA_SERVER_DB_SPELL_DAO_HPP
-#define MALIKANIA_SERVER_DB_SPELL_DAO_HPP
-
-#include <cstdint>
-
-#include "dao.hpp"
-
-namespace mlk {
-
-namespace server {
-
-class character;
-class spell;
-
-class spell_dao : public dao {
-public:
-    /**
-     * Inherited constructors.
-     */
-    using dao::dao;
-
-    virtual ~spell_dao() noexcept = default;
-    virtual std::int64_t save(std::shared_ptr<spell>, std::shared_ptr<character>) = 0;
-    virtual void set_level(std::shared_ptr<spell>, std::uint8_t) = 0;
-    virtual void remove(std::shared_ptr<spell>) = 0;
-};
-
-} // !server
-
-} // !mlk
-
-#endif // !MALIKANIA_SERVER_DB_SPELL_DAO_HPP
--- a/libserver/malikania/server/net/auth_handler.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/libserver/malikania/server/net/auth_handler.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -19,7 +19,7 @@
 #include <util.hpp>
 
 #include "db/database.hpp"
-#include "db/account_dao.hpp"
+#include "db/account.hpp"
 
 #include "auth_handler.hpp"
 #include "client.hpp"
@@ -32,8 +32,8 @@
 void auth_handler::exec(server& server, std::shared_ptr<client> clt, nlohmann::json object) noexcept
 {
     try {
-        auto dao = server.database().account_dao();
-        auto ac = dao->authenticate(
+        // TODO: VERIFY ALREADY LOGGED IN!
+        auto ac = server.database().account_dao().authenticate(
             util::json::require_string(object, "/login"_json_pointer),
             util::json::require_string(object, "/password"_json_pointer)
         );
@@ -41,7 +41,6 @@
         if (ac)
             clt->error("auth", "invalid credentials");
         else {
-            clt->set_account(ac);
             clt->set_state(client::state::ready);
             clt->ok("auth");
         }
--- a/tests/libserver/db/account/main.cpp	Mon Sep 11 16:26:35 2017 +0200
+++ b/tests/libserver/db/account/main.cpp	Mon Sep 11 13:18:43 2017 +0200
@@ -21,6 +21,7 @@
 
 #include <malikania/server/db/test_database.hpp>
 #include <malikania/server/db/broken_account.hpp>
+#include <malikania/server/db/broken_character.hpp>
 
 namespace mlk {
 
@@ -29,10 +30,46 @@
 class account_fixture {
 protected:
     std::shared_ptr<test_database> db_{new test_database};
+
+    inline const std::unordered_map<std::int64_t, nlohmann::json> accounts() const noexcept
+    {
+        return db_->test_account_dao().accounts();
+    }
+
+    inline std::unordered_map<std::int64_t, nlohmann::json> accounts() noexcept
+    {
+        return db_->test_account_dao().accounts();
+    }
+
+    inline const std::unordered_map<std::int64_t, nlohmann::json> characters() const noexcept
+    {
+        return db_->test_character_dao().characters();
+    }
+
+    inline std::unordered_map<std::int64_t, nlohmann::json> characters() noexcept
+    {
+        return db_->test_character_dao().characters();
+    }
+
+    const nlohmann::json& get(std::int64_t id) const noexcept
+    {
+        return db_->test_account_dao().accounts().at(id);
+    }
 };
 
+/*
+ * Basic suite using test database.
+ * ------------------------------------------------------------------
+ */
+
 BOOST_FIXTURE_TEST_SUITE(basic_suite, account_fixture)
 
+/*
+ * account::save.
+ * ------------------------------------------------------------------
+ *
+ * Verify that database is untouched during all operation on a draft account.
+ */
 BOOST_AUTO_TEST_CASE(save)
 {
     auto acc = db_->account_draft("nanahara", "dummypassword");
@@ -53,7 +90,7 @@
     BOOST_TEST(acc->email() == "nanahara@malikania.fr");
     BOOST_TEST(acc->firstname() == "Alexis");
     BOOST_TEST(acc->lastname() == "Dörr");
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 0U);
+    BOOST_TEST(accounts().size() == 0U);
 
     // Do save, database must be updated and account published.
     acc->save();
@@ -63,119 +100,143 @@
     BOOST_TEST(acc->email() == "nanahara@malikania.fr");
     BOOST_TEST(acc->firstname() == "Alexis");
     BOOST_TEST(acc->lastname() == "Dörr");
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 1U);
+    BOOST_TEST(accounts().size() == 1U);
 }
 
+/*
+ * account::set_password.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_password)
 {
     auto ac = db_->account_draft("markand", "nopassword");
 
     ac->set_password("temporarypassword");
-
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 0U);
-
     ac->save();
     ac->set_password("newpassword");
 
-    BOOST_TEST(db_->test_account_dao()->accounts().at(ac->id()).password() == "newpassword");
+    BOOST_TEST(get(ac->id())["password"].get<std::string>() == "newpassword");
 }
 
+/*
+ * account::set_email.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_email)
 {
     auto ac = db_->account_draft("markand", "nopassword");
 
     ac->set_email("fake@malikania.fr");
-
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 0U);
-
     ac->save();
     ac->set_email("markand@malikania.fr");
 
-    BOOST_TEST(db_->test_account_dao()->accounts().at(ac->id()).email() == "markand@malikania.fr");
+    BOOST_TEST(get(ac->id())["email"].get<std::string>() == "markand@malikania.fr");
 }
 
+/*
+ * account::set_firstname.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_firstname)
 {
     auto ac = db_->account_draft("markand", "nopassword");
 
     ac->set_firstname("Jean");
-
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 0U);
-
     ac->save();
     ac->set_firstname("David");
 
-    BOOST_TEST(db_->test_account_dao()->accounts().at(ac->id()).firstname() == "David");
+    BOOST_TEST(get(ac->id())["firstname"].get<std::string>() == "David");
 }
 
+/*
+ * account::set_lastname.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_lastname)
 {
     auto ac = db_->account_draft("markand", "nopassword");
 
     ac->set_lastname("Bertrand");
-
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 0U);
-
     ac->save();
     ac->set_lastname("Demelier");
 
-    BOOST_TEST(db_->test_account_dao()->accounts().at(ac->id()).lastname() == "Demelier");
+    BOOST_TEST(get(ac->id())["lastname"].get<std::string>() == "Demelier");
 }
 
-BOOST_AUTO_TEST_SUITE(find_by_login)
-
-BOOST_AUTO_TEST_CASE(simple)
+/*
+ * account::add_character.
+ * ------------------------------------------------------------------
+ */
+BOOST_AUTO_TEST_CASE(add_character)
 {
-    db_->account_draft("markand", "nopassword")->save();
-
-    auto ac = db_->account_dao()->find_by_login("markand");
+    auto ac = db_->account_draft("markand", "nopassword");
 
-    BOOST_TEST(ac);
-    BOOST_TEST(ac->is_published());
-    BOOST_TEST(ac->login() == "markand");
-}
+    ac->save();
 
-BOOST_AUTO_TEST_CASE(not_found)
-{
-    auto ac = db_->account_dao()->find_by_login("doesnotexist");
+    auto& ch = ac->add_character(db_->character_draft("erekin", "fire"));
 
-    BOOST_TEST(!ac);
+    BOOST_TEST(ch->is_published());
+    BOOST_TEST(characters().size() == 1U);
 }
 
-BOOST_AUTO_TEST_CASE(same_instance)
+/*
+ * account::dao::find_by_login suite.
+ * ------------------------------------------------------------------
+ */
+BOOST_AUTO_TEST_SUITE(find_by_login)
+
+/*
+ * Find existing account.
+ * ------------------------------------------------------------------
+ */
+BOOST_AUTO_TEST_CASE(simple)
 {
-    db_->account_draft("markand", "nopassword")->save();
+    {
+        auto ac = db_->account_draft("markand", "nopassword");
 
-    auto a1 = db_->account_dao()->find_by_login("markand");
-    auto a2 = db_->account_dao()->find_by_login("markand");
+        ac->set_email("markand@malikania.fr");
+        ac->set_firstname("David");
+        ac->set_lastname("Demelier");
+        ac->save();
+        ac->add_character(db_->character_draft("erekin", "mage"));
+        ac->add_character(db_->character_draft("irina", "fairy"));
+    }
 
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 1U);
-    BOOST_TEST(a1 == a2);
+    auto ac = db_->account_dao().find_by_login("markand");
+
+    BOOST_TEST(ac->is_published());
+    BOOST_TEST(ac->login() == "markand");
+    BOOST_TEST(ac->email() == "markand@malikania.fr");
+    BOOST_TEST(ac->firstname() == "David");
+    BOOST_TEST(ac->lastname() == "Demelier");
+    BOOST_TEST(ac->characters().size() == 2U);
 }
 
-BOOST_AUTO_TEST_CASE(new_instance)
+/*
+ * Not existing account.
+ * ------------------------------------------------------------------
+ */
+BOOST_AUTO_TEST_CASE(not_found)
 {
-    db_->account_draft("markand", "nopassword")->save();
+    auto ac = db_->account_dao().find_by_login("doesnotexist");
 
-    /*
-     * Get rid of a1 reference, account::dao is supposed to call
-     * do_find_by_login again, creating a new pointer to the account.
-     */
-    auto a1 = db_->account_dao()->find_by_login("markand");
-    a1 = nullptr;
-
-    auto a2 = db_->account_dao()->find_by_login("markand");
-
-    BOOST_TEST(db_->test_account_dao()->accounts().size() == 1U);
-    BOOST_TEST(a1 != a2);
+    BOOST_TEST(!ac);
 }
 
 BOOST_AUTO_TEST_SUITE_END()
 
 BOOST_AUTO_TEST_SUITE_END()
 
+/*
+ * Test behaviour with broken implementation.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_SUITE(broken)
 
+/*
+ * account::save.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(save)
 {
     broken_account ac("markand", "nopassword");
@@ -187,6 +248,27 @@
     BOOST_TEST(ac.is_draft());
 }
 
+/*
+ * account::remove.
+ * ------------------------------------------------------------------
+ */
+BOOST_AUTO_TEST_CASE(remove)
+{
+    broken_account ac("markand", "nopassword", broken_account::allow_flags::save);
+
+    ac.save();
+
+    try {
+        ac.remove();
+    } catch (...) {}
+
+    BOOST_TEST(ac.is_published());
+}
+
+/*
+ * account::set_password.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_password)
 {
     broken_account ac("markand", "nopassword", broken_account::allow_flags::save);
@@ -200,6 +282,10 @@
     BOOST_TEST(ac.password() == "nopassword");
 }
 
+/*
+ * account::set_email.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_email)
 {
     broken_account ac("markand", "nopassword", broken_account::allow_flags::save);
@@ -213,6 +299,10 @@
     BOOST_TEST(ac.email() == "");
 }
 
+/*
+ * account::set_firstname.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_firstname)
 {
     broken_account ac("markand", "nopassword", broken_account::allow_flags::save);
@@ -226,6 +316,10 @@
     BOOST_TEST(ac.firstname() == "");
 }
 
+/*
+ * account::set_lastname.
+ * ------------------------------------------------------------------
+ */
 BOOST_AUTO_TEST_CASE(set_lastname)
 {
     broken_account ac("markand", "nopassword", broken_account::allow_flags::save);
@@ -239,6 +333,23 @@
     BOOST_TEST(ac.lastname() == "");
 }
 
+/*
+ * account::add_character.
+ * ------------------------------------------------------------------
+ */
+BOOST_AUTO_TEST_CASE(add_character)
+{
+    broken_account ac("markand", "nopassword", broken_account::allow_flags::save);
+
+    ac.save();
+
+    try {
+        ac.add_character(std::make_unique<broken_character>("erekin", "fire"));
+    } catch (...) {}
+
+    BOOST_TEST(ac.characters().size() == 0U);
+}
+
 BOOST_AUTO_TEST_SUITE_END()
 
 } // !server