From 40f438852f41161db8ce38a1ef89af9c5d1db43c Mon Sep 17 00:00:00 2001
From: Johannes Loher <johannes.loher@fg4f.de>
Date: Sun, 17 Sep 2017 17:52:41 +0200
Subject: [PATCH] initial addition of unittests

---
 dub.json                                |  25 +++++-
 source/calendarwebapp/app.d             |  31 +++++++
 source/calendarwebapp/authenticator.d   |  30 +++++++
 source/calendarwebapp/calendarwebapp.d  |  98 +++++++++++++++++++++
 source/calendarwebapp/configuration.d   |  61 +++++++++++++
 source/calendarwebapp/event.d           |  79 +++++++++++++++++
 test/calendarwebapp/testauthenticator.d |  53 ++++++++++++
 test/calendarwebapp/testevent.d         | 109 ++++++++++++++++++++++++
 8 files changed, 483 insertions(+), 3 deletions(-)
 create mode 100644 source/calendarwebapp/app.d
 create mode 100644 source/calendarwebapp/authenticator.d
 create mode 100644 source/calendarwebapp/calendarwebapp.d
 create mode 100644 source/calendarwebapp/configuration.d
 create mode 100644 source/calendarwebapp/event.d
 create mode 100644 test/calendarwebapp/testauthenticator.d
 create mode 100644 test/calendarwebapp/testevent.d

diff --git a/dub.json b/dub.json
index effc53a..c8225ba 100644
--- a/dub.json
+++ b/dub.json
@@ -4,14 +4,33 @@
 		"Johannes Loher"
 	],
 	"dependencies": {
-		"vibe-d": "0.8.1-rc.2",
-		"poodinis": "~>8.0.1"
+		"vibe-d": "0.8.1",
+		"poodinis": "8.0.1"
 	},
 	"description": "A simple webapplication to edit and view calendar entries",
 	"copyright": "Copyright © 2017, Johannes Loher",
 	"license": "MIT",
+	"targetType": "executable",
+    "targetPath": "generated",
+    "configurations": [
+		{
+			"name": "executable",
+			"versions": [
+				"VibeDefaultMain"
+			]
+		},
+        {
+            "name": "unittest",
+            "targetType": "executable",
+            "preBuildCommands": ["dub run unit-threaded -c gen_ut_main -- -f generated/ut.d test"],
+			"mainSourceFile": "generated/ut.d",
+			"sourcePaths": ["test"],
+            "dependencies": {
+                "unit-threaded": "0.7.30"
+            }
+        }
+    ],
 	"versions": [
-		"VibeDefaultMain",
 		"VibeUseOpenSSL11"
 	]
 }
\ No newline at end of file
diff --git a/source/calendarwebapp/app.d b/source/calendarwebapp/app.d
new file mode 100644
index 0000000..d6c35c9
--- /dev/null
+++ b/source/calendarwebapp/app.d
@@ -0,0 +1,31 @@
+module calendarwebapp.app;
+
+import calendarwebapp.calendarwebapp : CalendarWebapp;
+import calendarwebapp.configuration : Context;
+
+import poodinis;
+
+import vibe.core.log : logInfo;
+
+import vibe.http.fileserver : serveStaticFiles;
+import vibe.http.router : URLRouter;
+import vibe.http.server : HTTPServerSettings, listenHTTP, MemorySessionStore;
+import vibe.web.web : registerWebInterface;
+
+shared static this()
+{
+    auto container = new shared DependencyContainer();
+    container.registerContext!Context;
+
+    auto router = new URLRouter;
+    router.registerWebInterface(container.resolve!CalendarWebapp);
+    router.get("*", serveStaticFiles("public"));
+
+    auto settings = new HTTPServerSettings;
+    settings.port = 8080;
+    settings.bindAddresses = ["::1", "127.0.0.1"];
+    settings.sessionStore = new MemorySessionStore;
+    listenHTTP(settings, router);
+
+    logInfo("Please open http://127.0.0.1:8080/ in your browser.");
+}
diff --git a/source/calendarwebapp/authenticator.d b/source/calendarwebapp/authenticator.d
new file mode 100644
index 0000000..89c36b5
--- /dev/null
+++ b/source/calendarwebapp/authenticator.d
@@ -0,0 +1,30 @@
+module calendarwebapp.authenticator;
+
+import poodinis;
+
+import vibe.data.bson : Bson;
+import vibe.db.mongo.collection : MongoCollection;
+
+interface Authenticator
+{
+    bool checkUser(string username, string password) @safe;
+}
+
+class MongoDBAuthenticator(Collection = MongoCollection) : Authenticator
+{
+private:
+    @Value("users")
+    Collection users;
+
+public:
+    bool checkUser(string username, string password) @safe
+    {
+        auto result = users.findOne(["username" : username, "password" : password]);
+        return result != Bson(null);
+    }
+}
+
+struct AuthInfo
+{
+    string userName;
+}
diff --git a/source/calendarwebapp/calendarwebapp.d b/source/calendarwebapp/calendarwebapp.d
new file mode 100644
index 0000000..21a652e
--- /dev/null
+++ b/source/calendarwebapp/calendarwebapp.d
@@ -0,0 +1,98 @@
+module calendarwebapp.calendarwebapp;
+
+import calendarwebapp.authenticator : Authenticator, AuthInfo;
+import calendarwebapp.event;
+
+import core.time : days;
+
+import poodinis;
+
+import std.datetime : Date;
+import std.exception : enforce;
+import std.typecons : Nullable;
+
+import vibe.data.bson : BsonObjectID;
+import vibe.http.common : HTTPStatusException;
+import vibe.http.server : HTTPServerRequest, HTTPServerResponse;
+import vibe.http.status : HTTPStatus;
+import vibe.web.auth;
+import vibe.web.web : errorDisplay, noRoute, redirect, render, SessionVar,
+    terminateSession;
+
+@requiresAuth class CalendarWebapp
+{
+    @noRoute AuthInfo authenticate(scope HTTPServerRequest req, scope HTTPServerResponse) @safe
+    {
+        if (!req.session || !req.session.isKeySet("auth"))
+        {
+            redirect("/login");
+            return AuthInfo.init;
+        }
+        return req.session.get!AuthInfo("auth");
+    }
+
+public:
+    @anyAuth void index()
+    {
+        auto events = eventStore.getAllEvents();
+        render!("showevents.dt", events);
+    }
+
+    @noAuth void getLogin(string _error = null)
+    {
+        render!("login.dt", _error);
+    }
+
+    @noAuth @errorDisplay!getLogin void postLogin(string username, string password) @safe
+    {
+        enforce(authenticator.checkUser(username, password), "Benutzername oder Passwort ungültig");
+        immutable AuthInfo authInfo = {username};
+        auth = authInfo;
+        redirect("/");
+    }
+
+    @anyAuth void getLogout() @safe
+    {
+        terminateSession();
+        redirect("/");
+    }
+
+    @anyAuth void getCreate(ValidationErrorData _error = ValidationErrorData.init)
+    {
+        render!("create.dt", _error);
+    }
+
+    @anyAuth @errorDisplay!getCreate void postCreate(Date begin,
+            Nullable!Date end, string description, string name, EventType type, bool shout) @safe
+    {
+        import std.array : replace, split;
+
+        if (!end.isNull)
+            enforce(end - begin >= 1.days,
+                    "Mehrtägige Ereignisse müssen mindestens einen Tag dauern");
+        auto event = Event(BsonObjectID.generate, begin, end, name,
+                description.replace("\r", ""), type, shout);
+
+        eventStore.addEvent(event);
+
+        redirect("/");
+    }
+
+    @anyAuth void postRemove(BsonObjectID id) @safe
+    {
+        eventStore.removeEvent(id);
+        redirect("/");
+    }
+
+private:
+    struct ValidationErrorData
+    {
+        string msg;
+        string field;
+    }
+
+    SessionVar!(AuthInfo, "auth") auth;
+
+    @Autowire EventStore eventStore;
+    @Autowire Authenticator authenticator;
+}
diff --git a/source/calendarwebapp/configuration.d b/source/calendarwebapp/configuration.d
new file mode 100644
index 0000000..c2c0959
--- /dev/null
+++ b/source/calendarwebapp/configuration.d
@@ -0,0 +1,61 @@
+module calendarwebapp.configuration;
+
+import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator;
+import calendarwebapp.calendarwebapp : CalendarWebapp;
+import calendarwebapp.event : EventStore, MongoDBEventStore;
+
+import poodinis;
+
+import vibe.db.mongo.client : MongoClient;
+import vibe.db.mongo.collection : MongoCollection;
+import vibe.db.mongo.mongo : connectMongoDB;
+
+class Context : ApplicationContext
+{
+public:
+    override void registerDependencies(shared(DependencyContainer) container)
+    {
+        auto mongoClient = connectMongoDB("localhost");
+        container.register!MongoClient.existingInstance(mongoClient);
+        container.register!(EventStore, MongoDBEventStore!());
+        container.register!(Authenticator, MongoDBAuthenticator!());
+        container.register!CalendarWebapp;
+        container.register!(ValueInjector!string, StringInjector);
+        container.register!(ValueInjector!MongoCollection, MongoCollectionInjector);
+    }
+}
+
+class StringInjector : ValueInjector!string
+{
+private:
+    string[string] config;
+
+public:
+    this() const @safe pure nothrow
+    {
+        // dfmt off
+        config = ["Database name" :           "CalendarWebapp",
+                  "Users collection name":    "users",
+                  "Events collection name" :  "events"];
+        // dfmt on
+    }
+
+    override string get(string key) const @safe pure nothrow
+    {
+        return config[key];
+    }
+}
+
+class MongoCollectionInjector : ValueInjector!MongoCollection
+{
+private:
+    @Autowire MongoClient mongoClient;
+    @Value("Database name")
+    string databaseName;
+
+public:
+    override MongoCollection get(string key) @safe
+    {
+        return mongoClient.getCollection(databaseName ~ "." ~ key);
+    }
+}
diff --git a/source/calendarwebapp/event.d b/source/calendarwebapp/event.d
new file mode 100644
index 0000000..e03ee43
--- /dev/null
+++ b/source/calendarwebapp/event.d
@@ -0,0 +1,79 @@
+module calendarwebapp.event;
+
+import poodinis;
+
+import std.algorithm : map;
+import std.datetime : Date;
+import std.range.interfaces : InputRange, inputRangeObject;
+import std.typecons : Nullable;
+
+import vibe.data.bson : Bson, BsonObjectID, deserializeBson, serializeToBson;
+import vibe.data.serialization : serializationName = name;
+import vibe.db.mongo.collection : MongoCollection;
+
+interface EventStore
+{
+    Event getEvent(BsonObjectID id) @safe;
+    InputRange!Event getAllEvents() @safe;
+    void addEvent(Event) @safe;
+    InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe;
+    void removeEvent(BsonObjectID id) @safe;
+}
+
+class MongoDBEventStore(Collection = MongoCollection) : EventStore
+{
+public:
+    Event getEvent(BsonObjectID id) @safe
+    {
+        return events.findOne(["_id" : id]).deserializeBson!Event;
+    }
+
+    InputRange!Event getAllEvents() @safe
+    {
+        return events.find().map!(deserializeBson!Event).inputRangeObject;
+    }
+
+    void addEvent(Event event) @safe
+    {
+        if (!event.id.valid)
+            event.id = BsonObjectID.generate;
+
+        events.insert(event.serializeToBson);
+    }
+
+    InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe
+    {
+        return events.find(["$and" : [["date" : ["$gte" : begin.serializeToBson]], ["date"
+                    : ["$lte" : end.serializeToBson]]]]).map!(deserializeBson!Event)
+            .inputRangeObject;
+    }
+
+    void removeEvent(BsonObjectID id) @safe
+    {
+        events.remove(["_id" : id]);
+    }
+
+private:
+    @Value("events")
+    Collection events;
+}
+
+enum EventType
+{
+    Holiday,
+    Birthday,
+    FSI_Event,
+    General_University_Event,
+    Any
+}
+
+struct Event
+{
+    @serializationName("_id") BsonObjectID id;
+    @serializationName("date") Date begin;
+    @serializationName("end_date") Nullable!Date end;
+    string name;
+    @serializationName("desc") string description;
+    @serializationName("etype") EventType type;
+    bool shout;
+}
diff --git a/test/calendarwebapp/testauthenticator.d b/test/calendarwebapp/testauthenticator.d
new file mode 100644
index 0000000..65f6542
--- /dev/null
+++ b/test/calendarwebapp/testauthenticator.d
@@ -0,0 +1,53 @@
+module test.calendarwebapp.testauthenticator;
+
+import calendarwebapp.authenticator;
+
+import poodinis;
+
+import unit_threaded.mock;
+import unit_threaded.should;
+
+import vibe.data.bson : Bson;
+
+interface Collection
+{
+    Bson findOne(string[string] query) @safe;
+}
+
+class CollectionInjector : ValueInjector!Collection
+{
+private:
+    Collection[string] collections;
+
+public:
+    void add(string key, Collection collection)
+    {
+        collections[key] = collection;
+    }
+
+    override Collection get(string key) @safe
+    {
+        return collections[key];
+    }
+}
+
+@("Test MongoDBAuthenticator")
+@system unittest
+{
+    auto collection = mock!Collection;
+    auto container = new shared DependencyContainer;
+    container.register!(ValueInjector!Collection, CollectionInjector);
+    container.resolve!CollectionInjector.add("users", collection);
+    container.register!(Authenticator, MongoDBAuthenticator!(Collection))(
+            RegistrationOption.doNotAddConcreteTypeRegistration);
+
+    collection.returnValue!"findOne"(Bson(true), Bson(null));
+    collection.expect!"findOne"(["username" : "", "password" : ""]);
+    collection.expect!"findOne"(["username" : "foo", "password" : "bar"]);
+
+    auto authenticator = container.resolve!(Authenticator);
+    authenticator.checkUser("", "").shouldBeTrue;
+    authenticator.checkUser("foo", "bar").shouldBeFalse;
+
+    collection.verify;
+}
diff --git a/test/calendarwebapp/testevent.d b/test/calendarwebapp/testevent.d
new file mode 100644
index 0000000..2e4122c
--- /dev/null
+++ b/test/calendarwebapp/testevent.d
@@ -0,0 +1,109 @@
+module test.calendarwebapp.testevent;
+
+import calendarwebapp.event;
+
+import poodinis;
+
+import unit_threaded.mock;
+import unit_threaded.should;
+
+import vibe.data.bson : Bson, BsonObjectID, serializeToBson;
+
+interface Collection
+{
+    Bson findOne(BsonObjectID[string] query) @safe;
+    Bson[] find() @safe;
+    Bson[] find(Bson[string][string][][string] query) @safe;
+    void insert(Bson document) @safe;
+    void remove(BsonObjectID[string] selector) @safe;
+}
+
+class CollectionInjector : ValueInjector!Collection
+{
+private:
+    Collection[string] collections;
+
+public:
+    void add(string key, Collection collection)
+    {
+        collections[key] = collection;
+    }
+
+    override Collection get(string key) @safe
+    {
+        return collections[key];
+    }
+}
+
+@("Test failing getEventMongoDBEventStore.getEvent")
+@system unittest
+{
+    auto collection = mock!Collection;
+    auto container = new shared DependencyContainer;
+    container.register!(ValueInjector!Collection, CollectionInjector);
+    container.resolve!CollectionInjector.add("events", collection);
+    container.register!(EventStore, MongoDBEventStore!(Collection))(
+            RegistrationOption.doNotAddConcreteTypeRegistration);
+
+    collection.returnValue!"findOne"(Bson(null));
+
+    auto id = BsonObjectID.fromString("599090de97355141140fc698");
+    collection.expect!"findOne"(["_id" : id]);
+
+    auto eventStore = container.resolve!(EventStore);
+    eventStore.getEvent(id).shouldThrowWithMessage!Exception("Expected object instead of null_");
+    collection.verify;
+}
+
+@("Test successful MongoDBEventStore.getEvent")
+@system unittest
+{
+    auto collection = mock!Collection;
+    auto container = new shared DependencyContainer;
+    container.register!(ValueInjector!Collection, CollectionInjector);
+    container.resolve!CollectionInjector.add("events", collection);
+    container.register!(EventStore, MongoDBEventStore!(Collection))(
+            RegistrationOption.doNotAddConcreteTypeRegistration);
+
+    auto id = BsonObjectID.fromString("599090de97355141140fc698");
+    Event event;
+    event.id = id;
+
+    collection.returnValue!"findOne"(event.serializeToBson);
+
+    collection.expect!"findOne"(["_id" : id]);
+
+    auto eventStore = container.resolve!(EventStore);
+    eventStore.getEvent(id).shouldEqual(event);
+    collection.verify;
+}
+
+@("Test MongoDBEventStore.addEvent")
+@system unittest
+{
+    auto collection = mock!Collection;
+    auto container = new shared DependencyContainer;
+    container.register!(ValueInjector!Collection, CollectionInjector);
+    container.resolve!CollectionInjector.add("events", collection);
+    container.register!(EventStore, MongoDBEventStore!(Collection))(
+            RegistrationOption.doNotAddConcreteTypeRegistration);
+
+    auto id = BsonObjectID.fromString("599090de97355141140fc698");
+    Event event;
+    event.id = id;
+    auto serializedEvent = event.serializeToBson;
+
+    collection.returnValue!"findOne"(Bson(null), event.serializeToBson);
+
+    collection.expect!"findOne"(["_id" : id]);
+    collection.expect!"insert"(serializedEvent);
+    collection.expect!"findOne"(["_id" : id]);
+
+    auto eventStore = container.resolve!(EventStore);
+
+    eventStore.getEvent(id).shouldThrowWithMessage!Exception("Expected object instead of null_");
+    eventStore.addEvent(event);
+    eventStore.getEvent(id).shouldEqual(event);
+
+    collection.verify;
+}