From 40f438852f41161db8ce38a1ef89af9c5d1db43c Mon Sep 17 00:00:00 2001 From: Johannes Loher 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; +}