diff --git a/dub.json b/dub.json index effc53a..3db2ce0 100644 --- a/dub.json +++ b/dub.json @@ -4,14 +4,33 @@ "Johannes Loher" ], "dependencies": { - "vibe-d": "0.8.1-rc.2", + "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.31" + } + } + ], "versions": [ - "VibeDefaultMain", "VibeUseOpenSSL11" ] -} \ No newline at end of file +} diff --git a/source/app.d b/source/calendarwebapp/app.d similarity index 86% rename from source/app.d rename to source/calendarwebapp/app.d index ca618d1..d6c35c9 100644 --- a/source/app.d +++ b/source/calendarwebapp/app.d @@ -1,7 +1,7 @@ -module app; +module calendarwebapp.app; -import calendarwebapp : CalendarWebapp; -import configuration : Context; +import calendarwebapp.calendarwebapp : CalendarWebapp; +import calendarwebapp.configuration : Context; import poodinis; diff --git a/source/authenticator.d b/source/calendarwebapp/authenticator.d similarity index 77% rename from source/authenticator.d rename to source/calendarwebapp/authenticator.d index 2e53f91..89c36b5 100644 --- a/source/authenticator.d +++ b/source/calendarwebapp/authenticator.d @@ -1,4 +1,4 @@ -module authenticator; +module calendarwebapp.authenticator; import poodinis; @@ -10,11 +10,11 @@ interface Authenticator bool checkUser(string username, string password) @safe; } -class MongoDBAuthenticator : Authenticator +class MongoDBAuthenticator(Collection = MongoCollection) : Authenticator { private: @Value("users") - MongoCollection users; + Collection users; public: bool checkUser(string username, string password) @safe diff --git a/source/calendarwebapp.d b/source/calendarwebapp/calendarwebapp.d similarity index 92% rename from source/calendarwebapp.d rename to source/calendarwebapp/calendarwebapp.d index 651f5b6..21a652e 100644 --- a/source/calendarwebapp.d +++ b/source/calendarwebapp/calendarwebapp.d @@ -1,14 +1,13 @@ -module calendarwebapp; +module calendarwebapp.calendarwebapp; -import authenticator : Authenticator, AuthInfo; +import calendarwebapp.authenticator : Authenticator, AuthInfo; +import calendarwebapp.event; import core.time : days; -import event; - import poodinis; -import std.datetime.date : Date; +import std.datetime : Date; import std.exception : enforce; import std.typecons : Nullable; @@ -22,7 +21,7 @@ import vibe.web.web : errorDisplay, noRoute, redirect, render, SessionVar, @requiresAuth class CalendarWebapp { - @noRoute AuthInfo authenticate(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe + @noRoute AuthInfo authenticate(scope HTTPServerRequest req, scope HTTPServerResponse) @safe { if (!req.session || !req.session.isKeySet("auth")) { diff --git a/source/configuration.d b/source/calendarwebapp/configuration.d similarity index 79% rename from source/configuration.d rename to source/calendarwebapp/configuration.d index fce925d..c2c0959 100644 --- a/source/configuration.d +++ b/source/calendarwebapp/configuration.d @@ -1,8 +1,8 @@ -module configuration; +module calendarwebapp.configuration; -import authenticator : Authenticator, MongoDBAuthenticator; -import calendarwebapp : CalendarWebapp; -import event : EventStore, MongoDBEventStore; +import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator; +import calendarwebapp.calendarwebapp : CalendarWebapp; +import calendarwebapp.event : EventStore, MongoDBEventStore; import poodinis; @@ -17,8 +17,8 @@ public: { auto mongoClient = connectMongoDB("localhost"); container.register!MongoClient.existingInstance(mongoClient); - container.register!(EventStore, MongoDBEventStore); - container.register!(Authenticator, MongoDBAuthenticator); + container.register!(EventStore, MongoDBEventStore!()); + container.register!(Authenticator, MongoDBAuthenticator!()); container.register!CalendarWebapp; container.register!(ValueInjector!string, StringInjector); container.register!(ValueInjector!MongoCollection, MongoCollectionInjector); diff --git a/source/event.d b/source/calendarwebapp/event.d similarity index 92% rename from source/event.d rename to source/calendarwebapp/event.d index 5655cb7..e03ee43 100644 --- a/source/event.d +++ b/source/calendarwebapp/event.d @@ -1,9 +1,9 @@ -module event; +module calendarwebapp.event; import poodinis; import std.algorithm : map; -import std.datetime.date : Date; +import std.datetime : Date; import std.range.interfaces : InputRange, inputRangeObject; import std.typecons : Nullable; @@ -20,7 +20,7 @@ interface EventStore void removeEvent(BsonObjectID id) @safe; } -class MongoDBEventStore : EventStore +class MongoDBEventStore(Collection = MongoCollection) : EventStore { public: Event getEvent(BsonObjectID id) @safe @@ -55,7 +55,7 @@ public: private: @Value("events") - MongoCollection events; + Collection events; } enum EventType 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..3a1daf6 --- /dev/null +++ b/test/calendarwebapp/testevent.d @@ -0,0 +1,168 @@ +module test.calendarwebapp.testevent; + +import calendarwebapp.event; + +import poodinis; + +import std.array; +import std.algorithm : map; + +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]; + } +} + +@("getEventMongoDBEventStore.getEvent failure") +@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; +} + +@("MongoDBEventStore.getEvent success") +@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; +} + +@("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), serializedEvent); + + 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; +} + +@("MongoDBEventStore.removeEvent") +@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, Bson(null)); + + collection.expect!"findOne"(["_id" : id]); + collection.expect!"remove"(["_id" : id]); + collection.expect!"findOne"(["_id" : id]); + + auto eventStore = container.resolve!(EventStore); + + eventStore.getEvent(id).shouldEqual(event); + eventStore.removeEvent(event.id); + eventStore.getEvent(id).shouldThrowWithMessage!Exception("Expected object instead of null_"); + + collection.verify; +} + +@("MongoDBEventStore.getAllEvents") +@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); + + immutable ids = [ + BsonObjectID.fromString("599090de97355141140fc698"), BsonObjectID.fromString("599090de97355141140fc698"), + BsonObjectID.fromString("59cb9ad8fc0ba5751c0df02b") + ]; + auto events = ids.map!(id => Event(id)).array; + + collection.returnValue!"find"(events.map!serializeToBson.array); + + collection.expect!"find"(); + + auto eventStore = container.resolve!(EventStore); + + eventStore.getAllEvents.array.shouldEqual(events); + + collection.verify; +}