diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca6ec5f9..d27b0d04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,14 +4,14 @@ on: jobs: xenial: container: - image: vapor/swift:5.1-xenial + image: vapor/swift:5.2-xenial runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - run: swift test --enable-test-discovery --enable-code-coverage bionic: container: - image: vapor/swift:5.1-bionic + image: vapor/swift:5.2-bionic runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -20,7 +20,7 @@ jobs: - name: Setup container for codecov upload run: apt-get update && apt-get install curl - name: Process coverage file - run: llvm-cov show .build/x86_64-unknown-linux/debug/SteamPressPackageTests.xctest -instr-profile=.build/x86_64-unknown-linux/debug/codecov/default.profdata > coverage.txt + run: llvm-cov show .build/x86_64-unknown-linux-gnu/debug/SteamPressPackageTests.xctest -instr-profile=.build/debug/codecov/default.profdata > coverage.txt - name: Upload code coverage uses: codecov/codecov-action@v1 with: diff --git a/Package.swift b/Package.swift index 3a41bf30..0e0896f9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,20 +1,28 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.2 import PackageDescription let package = Package( name: "SteamPress", + platforms: [ + .macOS(.v10_15) + ], products: [ .library(name: "SteamPress", targets: ["SteamPress"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-rc"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.0.0"), - .package(url: "https://github.com/vapor-community/markdown.git", from: "0.4.0"), - .package(url: "https://github.com/vapor/auth.git", from: "2.0.0"), + .package(url: "https://github.com/vapor/leaf-kit.git", from: "1.0.0-rc.1"), + .package(name: "SwiftMarkdown", url: "https://github.com/vapor-community/markdown.git", from: "0.6.1"), ], targets: [ - .target(name: "SteamPress", dependencies: ["Vapor", "SwiftSoup", "SwiftMarkdown", "Authentication"]), + .target(name: "SteamPress", dependencies: [ + .product(name: "Vapor", package: "vapor"), + .product(name: "LeafKit", package: "leaf-kit"), + "SwiftSoup", + "SwiftMarkdown" + ]), .testTarget(name: "SteamPressTests", dependencies: ["SteamPress"]), ] ) diff --git a/README.md b/README.md index 294b93aa..44798305 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Language + Language Build Status @@ -16,7 +16,7 @@

-SteamPress is a Swift blogging engine for use with the Vapor Framework to deploy blogs to sites that run on top of Vapor. It uses [Fluent](https://github.com/vapor/fluent) so will work with any database that has a Fluent Driver. It also incorporates a [Markdown Provider](https://github.com/vapor-community/markdown-provider) allowing you to write your posts in Markdown and then use Leaf to render the markdown. +SteamPress is a Swift blogging engine for use with the Vapor Framework to deploy blogs to sites that run on top of Vapor. It uses protocols to define database storage, so will work with any database that has a `SteamPressRepository` implementation, or you can write your own! It also incorporates a [Markdown Provider](https://github.com/vapor-community/markdown-provider) allowing you to write your posts in Markdown and then use Leaf to render the markdown. The blog can either be used as the root of your website (i.e. appearing at https://www.acme.org) or in a subpath (i.e. https://www.acme.org/blog/). @@ -42,6 +42,8 @@ There is an example of how it can work in a site (and what it requires in terms ## Add as a dependency +**TODO** Update + SteamPress is easy to integrate with your application. There are two providers that provide implementations for [PostgreSQL](https://github.com/brokenhandsio/steampress-fluent-postgres) or [MySQL](https://github.com/brokenhandsio/steampress-fluent-mysql). You are also free to write your own integrations. Normally you'd choose one of the implementations as that provides repository integrations for the database. In this example, we're using Postgres. First, add the provider to your `Package.swift` dependencies: @@ -49,7 +51,7 @@ First, add the provider to your `Package.swift` dependencies: ```swift dependencies: [ // ... - .package(name: "SteampressFluentPostgres", url: "https://github.com/brokenhandsio/steampress-fluent-postgres.git", from: "1.0.0"), + .package(name: "SteampressFluentPostgres", url: "https://github.com/brokenhandsio/steampress-fluent-postgres.git", from: "2.0.0"), ], ``` @@ -98,7 +100,7 @@ First add SteamPress to your `Package.swift` dependencies: ```swift dependencies: [ // ..., - .package(name: "SteamPress", url: "https://github.com/brokenhandsio/SteamPress", from: "1.0.0") + .package(name: "SteamPress", url: "https://github.com/brokenhandsio/SteamPress", from: "2.0.0") ] ``` @@ -115,42 +117,29 @@ And then as a dependency to your target: This will register the routes for you. You must provide implementations for the different repository types to your services: ```swift -services.register(MyTagRepository(), as: BlogTagRepository.self) -services.register(MyUserRepository(), as: BlogUserRepository.self) -services.register(MyPostRepository(), as: BlogPostRepository.self) +app.steampress.blogRepositories.use { application in + MyRepository(application: application) +} ``` -You can then register the SteamPress provider with your services: - -```swift -let steampressProvider = SteamPress.Provider() -try services.register(steampressProvider) -``` +SteamPress will be automatically registered, depending on the configuration provided (see below). ## Integration SteamPress offers a 'Remember Me' functionality when logging in to extend the duration of the session. In order for this to work, you must register the middleware: ```swift -var middlewares = MiddlewareConfig() -// ... -middlewares.use(BlogRememberMeMiddleware.self) -middlewares.use(SessionsMiddleware.self) -services.register(middlewares) +application.middlewares.use(BlogRememberMeMiddleware()) ``` **Note:** This must be registered before you register the `SessionsMiddleware`. -SteamPress uses a `PasswordVerifier` protocol to check passwords. Vapor doesn't provide a default BCrypt implementation for this, so you must register this yourself: - -```swift -config.prefer(BCryptDigest.self, for: PasswordVerifier.self) -``` +**TODO: Update** Finally, if you wish to use the `#markdown()` tag with your blog Leaf templates, you must register this. There's also a paginator tag, to make pagination easy: ```swift - var tags = LeafTagConfig.default() +var tags = LeafTagConfig.default() tags.use(Markdown(), as: "markdown") let paginatorTag = PaginatorTag(paginationLabel: "Blog Posts") tags.use(paginatorTag, as: PaginatorTag.name) @@ -175,7 +164,7 @@ let feedInformation = FeedInformation( description: "SteamPress is an open-source blogging engine written for Vapor in Swift", copyright: "Released under the MIT licence", imageURL: "https://user-images.githubusercontent.com/9938337/29742058-ed41dcc0-8a6f-11e7-9cfc-680501cdfb97.png") -try services.register(SteamPressFluentPostgresProvider(blogPath: "blog", feedInformation: feedInformation, postsPerPage: 5)) +application.steampress.configuration = SteamPressConfiguration(blogPath: "blog", feedInformation: feedInformation, postsPerPage: 5) ``` Additionally, you should set the `WEBSITE_URL` environment variable to the root address of your website, e.g. `https://www.steampress.io`. This is used to set various parameters throughout SteamPress. diff --git a/Sources/SteamPress/Controllers/API/APIController.swift b/Sources/SteamPress/Controllers/API/APIController.swift index 4d2147d9..7d9f53b1 100644 --- a/Sources/SteamPress/Controllers/API/APIController.swift +++ b/Sources/SteamPress/Controllers/API/APIController.swift @@ -1,8 +1,8 @@ import Vapor struct APIController: RouteCollection { - func boot(router: Router) throws { - let apiRoutes = router.grouped("api") + func boot(routes: RoutesBuilder) throws { + let apiRoutes = routes.grouped("api") let apiTagController = APITagController() try apiRoutes.register(collection: apiTagController) diff --git a/Sources/SteamPress/Controllers/API/APITagController.swift b/Sources/SteamPress/Controllers/API/APITagController.swift index 9c39b813..7fa74e6e 100644 --- a/Sources/SteamPress/Controllers/API/APITagController.swift +++ b/Sources/SteamPress/Controllers/API/APITagController.swift @@ -1,13 +1,12 @@ import Vapor struct APITagController: RouteCollection { - func boot(router: Router) throws { - let tagsRoute = router.grouped("tags") + func boot(routes: RoutesBuilder) throws { + let tagsRoute = routes.grouped("tags") tagsRoute.get(use: allTagsHandler) } func allTagsHandler(_ req: Request) throws -> EventLoopFuture<[BlogTag]> { - let repository = try req.make(BlogTagRepository.self) - return repository.getAllTags(on: req) + req.blogTagRepository.getAllTags() } } diff --git a/Sources/SteamPress/Controllers/Admin/LoginController.swift b/Sources/SteamPress/Controllers/Admin/LoginController.swift index aa50c1d8..ae1ceb74 100644 --- a/Sources/SteamPress/Controllers/Admin/LoginController.swift +++ b/Sources/SteamPress/Controllers/Admin/LoginController.swift @@ -1,136 +1,141 @@ import Vapor -import Authentication struct LoginController: RouteCollection { - + // MARK: - Properties private let pathCreator: BlogPathCreator - + // MARK: - Initialiser init(pathCreator: BlogPathCreator) { self.pathCreator = pathCreator } - + // MARK: - Route setup - func boot(router: Router) throws { - router.get("login", use: loginHandler) - router.post("login", use: loginPostHandler) - + func boot(routes: RoutesBuilder) throws { + routes.get("login", use: loginHandler) + routes.post("login", use: loginPostHandler) + let redirectMiddleware = BlogLoginRedirectAuthMiddleware(pathCreator: pathCreator) - let protectedRoutes = router.grouped(redirectMiddleware) + let protectedRoutes = routes.grouped(redirectMiddleware) protectedRoutes.post("logout", use: logoutHandler) protectedRoutes.get("resetPassword", use: resetPasswordHandler) protectedRoutes.post("resetPassword", use: resetPasswordPostHandler) } - + // MARK: - Route handlers func loginHandler(_ req: Request) throws -> EventLoopFuture { let loginRequied = (try? req.query.get(Bool.self, at: "loginRequired")) != nil - let presenter = try req.make(BlogPresenter.self) - return try presenter.loginView(on: req, loginWarning: loginRequied, errors: nil, username: nil, usernameError: false, passwordError: false, rememberMe: false, pageInformation: req.pageInformation()) + return try req.blogPresenter.loginView(loginWarning: loginRequied, errors: nil, username: nil, usernameError: false, passwordError: false, rememberMe: false, pageInformation: req.pageInformation()) } - + func loginPostHandler(_ req: Request) throws -> EventLoopFuture { - let loginData = try req.content.syncDecode(LoginData.self) + let loginData = try req.content.decode(LoginData.self) var loginErrors = [String]() var usernameError = false var passwordError = false - + if loginData.username == nil { loginErrors.append("You must supply your username") usernameError = true } - + if loginData.password == nil { loginErrors.append("You must supply your password") passwordError = true } - + if !loginErrors.isEmpty { - let presenter = try req.make(BlogPresenter.self) - return try presenter.loginView(on: req, loginWarning: false, errors: loginErrors, username: loginData.username, usernameError: usernameError, passwordError: passwordError, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encode(for: req) + return try req.blogPresenter.loginView(loginWarning: false, errors: loginErrors, username: loginData.username, usernameError: usernameError, passwordError: passwordError, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encodeResponse(for: req) } - + guard let username = loginData.username, let password = loginData.password else { throw Abort(.internalServerError) } - + if let rememberMe = loginData.rememberMe, rememberMe { - try req.session()["SteamPressRememberMe"] = "YES" + req.session.data["SteamPressRememberMe"] = "YES" } else { - try req.session()["SteamPressRememberMe"] = nil + req.session.data["SteamPressRememberMe"] = nil } - - let userRepository = try req.make(BlogUserRepository.self) - return userRepository.getUser(username: username, on: req).flatMap { user in - let verifier = try req.make(PasswordVerifier.self) - guard let user = user, try verifier.verify(password, created: user.password) else { + + return req.blogUserRepository.getUser(username: username).flatMap { user -> EventLoopFuture in + guard let user = user else { let loginError = ["Your username or password is incorrect"] - let presenter = try req.make(BlogPresenter.self) - return try presenter.loginView(on: req, loginWarning: false, errors: loginError, username: loginData.username, usernameError: false, passwordError: false, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encode(for: req) + do { + return try req.blogPresenter.loginView(loginWarning: false, errors: loginError, username: loginData.username, usernameError: false, passwordError: false, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encodeResponse(for: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } + return req.password.async.verify(password, created: user.password).flatMap { userAuthenticated in + guard userAuthenticated else { + let loginError = ["Your username or password is incorrect"] + do { + return try req.blogPresenter.loginView(loginWarning: false, errors: loginError, username: loginData.username, usernameError: false, passwordError: false, rememberMe: loginData.rememberMe ?? false, pageInformation: req.pageInformation()).encodeResponse(for: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } + user.authenticateSession(on: req) + return req.eventLoop.future(req.redirect(to: self.pathCreator.createPath(for: "admin"))) } - try user.authenticateSession(on: req) - return req.future(req.redirect(to: self.pathCreator.createPath(for: "admin"))) } } - - func logoutHandler(_ request: Request) throws -> Response { - try request.unauthenticateBlogUserSession() + + func logoutHandler(_ request: Request) -> Response { + request.unauthenticateBlogUserSession() return request.redirect(to: pathCreator.createPath(for: pathCreator.blogPath)) } - + func resetPasswordHandler(_ req: Request) throws -> EventLoopFuture { - let presenter = try req.make(BlogAdminPresenter.self) - return try presenter.createResetPasswordView(on: req, errors: nil, passwordError: nil, confirmPasswordError: nil, pageInformation: req.adminPageInfomation()) + try req.adminPresenter.createResetPasswordView(errors: nil, passwordError: nil, confirmPasswordError: nil, pageInformation: req.adminPageInfomation()) } - + func resetPasswordPostHandler(_ req: Request) throws -> EventLoopFuture { - let data = try req.content.syncDecode(ResetPasswordData.self) - + let data = try req.content.decode(ResetPasswordData.self) + var resetPasswordErrors = [String]() var passwordError: Bool? var confirmPasswordError: Bool? - + guard let password = data.password, let confirmPassword = data.confirmPassword else { - + if data.password == nil { resetPasswordErrors.append("You must specify a password") passwordError = true } - + if data.confirmPassword == nil { resetPasswordErrors.append("You must confirm your password") confirmPasswordError = true } - - let presenter = try req.make(BlogAdminPresenter.self) - let view = try presenter.createResetPasswordView(on: req, errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation()) - return try view.encode(for: req) + + let view = try req.adminPresenter.createResetPasswordView(errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation()) + return view.encodeResponse(for: req) } - + if password != confirmPassword { resetPasswordErrors.append("Your passwords must match!") passwordError = true confirmPasswordError = true } - + if password.count < 10 { passwordError = true resetPasswordErrors.append("Your password must be at least 10 characters long") } - + guard resetPasswordErrors.isEmpty else { - let presenter = try req.make(BlogAdminPresenter.self) - let view = try presenter.createResetPasswordView(on: req, errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation()) - return try view.encode(for: req) + let view = try req.adminPresenter.createResetPasswordView(errors: resetPasswordErrors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: req.adminPageInfomation()) + return view.encodeResponse(for: req) + } + + let user = try req.auth.require(BlogUser.self) + return req.password.async.hash(password).flatMap { hashedPassword in + user.password = hashedPassword + user.resetPasswordRequired = false + let redirect = req.redirect(to: self.pathCreator.createPath(for: "admin")) + return req.blogUserRepository.save(user).transform(to: redirect) } - - let user = try req.requireAuthenticated(BlogUser.self) - let hasher = try req.make(PasswordHasher.self) - user.password = try hasher.hash(password) - user.resetPasswordRequired = false - let userRespository = try req.make(BlogUserRepository.self) - let redirect = req.redirect(to: pathCreator.createPath(for: "admin")) - return userRespository.save(user, on: req).transform(to: redirect) } } diff --git a/Sources/SteamPress/Controllers/Admin/PostAdminController.swift b/Sources/SteamPress/Controllers/Admin/PostAdminController.swift index 53d724e4..4f73d4a0 100644 --- a/Sources/SteamPress/Controllers/Admin/PostAdminController.swift +++ b/Sources/SteamPress/Controllers/Admin/PostAdminController.swift @@ -11,32 +11,30 @@ struct PostAdminController: RouteCollection { } // MARK: - Route setup - func boot(router: Router) throws { - router.get("createPost", use: createPostHandler) - router.post("createPost", use: createPostPostHandler) - router.get("posts", BlogPost.parameter, "edit", use: editPostHandler) - router.post("posts", BlogPost.parameter, "edit", use: editPostPostHandler) - router.post("posts", BlogPost.parameter, "delete", use: deletePostHandler) + func boot(routes: RoutesBuilder) throws { + routes.get("createPost", use: createPostHandler) + routes.post("createPost", use: createPostPostHandler) + routes.get("posts", BlogPost.parameter, "edit", use: editPostHandler) + routes.post("posts", BlogPost.parameter, "edit", use: editPostPostHandler) + routes.post("posts", BlogPost.parameter, "delete", use: deletePostHandler) } // MARK: - Route handlers func createPostHandler(_ req: Request) throws -> EventLoopFuture { - let presenter = try req.make(BlogAdminPresenter.self) - return try presenter.createPostView(on: req, errors: nil, title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: false, post: nil, isDraft: nil, titleError: false, contentsError: false, pageInformation: req.adminPageInfomation()) + return try req.adminPresenter.createPostView(errors: nil, title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: false, post: nil, isDraft: nil, titleError: false, contentsError: false, pageInformation: req.adminPageInfomation()) } func createPostPostHandler(_ req: Request) throws -> EventLoopFuture { - let data = try req.content.syncDecode(CreatePostData.self) - let author = try req.requireAuthenticated(BlogUser.self) + let data = try req.content.decode(CreatePostData.self) + let author = try req.auth.require(BlogUser.self) if data.draft == nil && data.publish == nil { throw Abort(.badRequest) } if let createPostErrors = validatePostCreation(data) { - let presenter = try req.make(BlogAdminPresenter.self) - let view = try presenter.createPostView(on: req, errors: createPostErrors.errors, title: data.title, contents: data.contents, slugURL: nil, tags: data.tags, isEditing: false, post: nil, isDraft: nil, titleError: createPostErrors.titleError, contentsError: createPostErrors.contentsError, pageInformation: req.adminPageInfomation()) - return try view.encode(for: req) + let view = try req.adminPresenter.createPostView(errors: createPostErrors.errors, title: data.title, contents: data.contents, slugURL: nil, tags: data.tags, isEditing: false, post: nil, isDraft: nil, titleError: createPostErrors.titleError, contentsError: createPostErrors.contentsError, pageInformation: req.adminPageInfomation()) + return view.encodeResponse(for: req) } guard let title = data.title, let contents = data.contents else { @@ -44,37 +42,39 @@ struct PostAdminController: RouteCollection { } return try BlogPost.generateUniqueSlugURL(from: title, on: req).flatMap { uniqueSlug in - let newPost = try BlogPost(title: title, contents: contents, author: author, creationDate: Date(), slugUrl: uniqueSlug, published: data.publish != nil) - - let postRepository = try req.make(BlogPostRepository.self) - return postRepository.save(newPost, on: req).flatMap { post in - let tagsRepository = try req.make(BlogTagRepository.self) + let newPost: BlogPost + do { + newPost = try BlogPost(title: title, contents: contents, author: author, creationDate: Date(), slugUrl: uniqueSlug, published: data.publish != nil) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + return req.blogPostRepository.save(newPost).flatMap { post in var existingTagsQuery = [EventLoopFuture]() for tagName in data.tags { - existingTagsQuery.append(tagsRepository.getTag(tagName, on: req)) + existingTagsQuery.append(req.blogTagRepository.getTag(tagName)) } - return existingTagsQuery.flatten(on: req).flatMap { existingTagsWithOptionals in + return existingTagsQuery.flatten(on: req.eventLoop).flatMap { existingTagsWithOptionals in let existingTags = existingTagsWithOptionals.compactMap { $0 } var tagsSaves = [EventLoopFuture]() for tagName in data.tags { if !existingTags.contains(where: { $0.name == tagName }) { let tag = BlogTag(name: tagName) - tagsSaves.append(tagsRepository.save(tag, on: req)) + tagsSaves.append(req.blogTagRepository.save(tag)) } } - return tagsSaves.flatten(on: req).flatMap { tags in + return tagsSaves.flatten(on: req.eventLoop).flatMap { tags in var tagLinks = [EventLoopFuture]() for tag in tags { - tagLinks.append(tagsRepository.add(tag, to: post, on: req)) + tagLinks.append(req.blogTagRepository.add(tag, to: post)) } for tag in existingTags { - tagLinks.append(tagsRepository.add(tag, to: post, on: req)) + tagLinks.append(req.blogTagRepository.add(tag, to: post)) } let redirect = req.redirect(to: self.pathCreator.createPath(for: "posts/\(post.slugUrl)")) - return tagLinks.flatten(on: req).transform(to: redirect) + return tagLinks.flatten(on: req.eventLoop).transform(to: redirect) } } } @@ -82,36 +82,39 @@ struct PostAdminController: RouteCollection { } func deletePostHandler(_ req: Request) throws -> EventLoopFuture { - return try req.parameters.next(BlogPost.self).flatMap { post in - let tagsRepository = try req.make(BlogTagRepository.self) - return tagsRepository.deleteTags(for: post, on: req).flatMap { + return req.parameters.findPost(on: req).flatMap { post in + return req.blogTagRepository.deleteTags(for: post).flatMap { let redirect = req.redirect(to: self.pathCreator.createPath(for: "admin")) - let postRepository = try req.make(BlogPostRepository.self) - return postRepository.delete(post, on: req).transform(to: redirect) + return req.blogPostRepository.delete(post).transform(to: redirect) } } } func editPostHandler(_ req: Request) throws -> EventLoopFuture { - return try req.parameters.next(BlogPost.self).flatMap { post in - let tagsRepository = try req.make(BlogTagRepository.self) - return tagsRepository.getTags(for: post, on: req).flatMap { tags in - let presenter = try req.make(BlogAdminPresenter.self) - return try presenter.createPostView(on: req, errors: nil, title: post.title, contents: post.contents, slugURL: post.slugUrl, tags: tags.map { $0.name }, isEditing: true, post: post, isDraft: !post.published, titleError: false, contentsError: false, pageInformation: req.adminPageInfomation()) + return req.parameters.findPost(on: req).flatMap { post in + return req.blogTagRepository.getTags(for: post).flatMap { tags in + do { + return try req.adminPresenter.createPostView(errors: nil, title: post.title, contents: post.contents, slugURL: post.slugUrl, tags: tags.map { $0.name }, isEditing: true, post: post, isDraft: !post.published, titleError: false, contentsError: false, pageInformation: req.adminPageInfomation()) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } } } func editPostPostHandler(_ req: Request) throws -> EventLoopFuture { - let data = try req.content.syncDecode(CreatePostData.self) - return try req.parameters.next(BlogPost.self).flatMap { post in + let data = try req.content.decode(CreatePostData.self) + return req.parameters.findPost(on: req).flatMap { post -> EventLoopFuture in if let errors = self.validatePostCreation(data) { - let presenter = try req.make(BlogAdminPresenter.self) - return try presenter.createPostView(on: req, errors: errors.errors, title: data.title, contents: data.contents, slugURL: post.slugUrl, tags: data.tags, isEditing: true, post: post, isDraft: !post.published, titleError: errors.titleError, contentsError: errors.contentsError, pageInformation: req.adminPageInfomation()).encode(for: req) + do { + return try req.adminPresenter.createPostView(errors: errors.errors, title: data.title, contents: data.contents, slugURL: post.slugUrl, tags: data.tags, isEditing: true, post: post, isDraft: !post.published, titleError: errors.titleError, contentsError: errors.contentsError, pageInformation: req.adminPageInfomation()).encodeResponse(for: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } guard let title = data.title, let contents = data.contents else { - throw Abort(.internalServerError) + return req.eventLoop.makeFailedFuture(Abort(.internalServerError)) } post.title = title @@ -119,9 +122,13 @@ struct PostAdminController: RouteCollection { let slugURLFuture: EventLoopFuture if let updateSlugURL = data.updateSlugURL, updateSlugURL { - slugURLFuture = try BlogPost.generateUniqueSlugURL(from: title, on: req) + do { + slugURLFuture = try BlogPost.generateUniqueSlugURL(from: title, on: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } else { - slugURLFuture = req.future(post.slugUrl) + slugURLFuture = req.eventLoop.future(post.slugUrl) } return slugURLFuture.flatMap { slugURL in @@ -135,8 +142,7 @@ struct PostAdminController: RouteCollection { } } - let tagsRepository = try req.make(BlogTagRepository.self) - return flatMap(tagsRepository.getTags(for: post, on: req), tagsRepository.getAllTags(on: req)) { existingTags, allTags in + return req.blogTagRepository.getTags(for: post).and(req.blogTagRepository.getAllTags()).flatMap { existingTags, allTags in let tagsToUnlink = existingTags.filter { (anExistingTag) -> Bool in for tagName in data.tags { if anExistingTag.name == tagName { @@ -147,7 +153,7 @@ struct PostAdminController: RouteCollection { } var removeTagLinkResults = [EventLoopFuture]() for tagToUnlink in tagsToUnlink { - removeTagLinkResults.append(tagsRepository.remove(tagToUnlink, from: post, on: req)) + removeTagLinkResults.append(req.blogTagRepository.remove(tagToUnlink, from: post)) } let newTagsNames = data.tags.filter { (tagName) -> Bool in @@ -160,22 +166,21 @@ struct PostAdminController: RouteCollection { for newTagName in newTagsNames { let foundInAllTags = allTags.filter { $0.name == newTagName }.first if let existingTag = foundInAllTags { - tagCreateSaves.append(req.future(existingTag)) + tagCreateSaves.append(req.eventLoop.future(existingTag)) } else { let newTag = BlogTag(name: newTagName) - tagCreateSaves.append(tagsRepository.save(newTag, on: req)) + tagCreateSaves.append(req.blogTagRepository.save(newTag)) } } - return removeTagLinkResults.flatten(on: req).and(tagCreateSaves.flatten(on: req)).flatMap { (_, newTags) in + return removeTagLinkResults.flatten(on: req.eventLoop).and(tagCreateSaves.flatten(on: req.eventLoop)).flatMap { (_, newTags) in var postTagLinkResults = [EventLoopFuture]() for tag in newTags { - postTagLinkResults.append(tagsRepository.add(tag, to: post, on: req)) + postTagLinkResults.append(req.blogTagRepository.add(tag, to: post)) } - return postTagLinkResults.flatten(on: req).flatMap { + return postTagLinkResults.flatten(on: req.eventLoop).flatMap { let redirect = req.redirect(to: self.pathCreator.createPath(for: "posts/\(post.slugUrl)")) - let postRepository = try req.make(BlogPostRepository.self) - return postRepository.save(post, on: req).transform(to: redirect) + return req.blogPostRepository.save(post).transform(to: redirect) } } } diff --git a/Sources/SteamPress/Controllers/Admin/UserAdminController.swift b/Sources/SteamPress/Controllers/Admin/UserAdminController.swift index 39601db4..208c1b1e 100644 --- a/Sources/SteamPress/Controllers/Admin/UserAdminController.swift +++ b/Sources/SteamPress/Controllers/Admin/UserAdminController.swift @@ -1,5 +1,4 @@ import Vapor -import Authentication struct UserAdminController: RouteCollection { @@ -12,127 +11,149 @@ struct UserAdminController: RouteCollection { } // MARK: - Route setup - func boot(router: Router) throws { - router.get("createUser", use: createUserHandler) - router.post("createUser", use: createUserPostHandler) - router.get("users", BlogUser.parameter, "edit", use: editUserHandler) - router.post("users", BlogUser.parameter, "edit", use: editUserPostHandler) - router.post("users", BlogUser.parameter, "delete", use: deleteUserPostHandler) + func boot(routes: RoutesBuilder) throws { + routes.get("createUser", use: createUserHandler) + routes.post("createUser", use: createUserPostHandler) + routes.get("users", BlogUser.parameter, "edit", use: editUserHandler) + routes.post("users", BlogUser.parameter, "edit", use: editUserPostHandler) + routes.post("users", BlogUser.parameter, "delete", use: deleteUserPostHandler) } // MARK: - Route handlers func createUserHandler(_ req: Request) throws -> EventLoopFuture { - let presenter = try req.make(BlogAdminPresenter.self) - return try presenter.createUserView(on: req, editing: false, errors: nil, name: nil, nameError: false, username: nil, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: nil, profilePicture: nil, twitterHandle: nil, biography: nil, tagline: nil, pageInformation: req.adminPageInfomation()) + return try req.adminPresenter.createUserView(editing: false, errors: nil, name: nil, nameError: false, username: nil, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: nil, profilePicture: nil, twitterHandle: nil, biography: nil, tagline: nil, pageInformation: req.adminPageInfomation()) } func createUserPostHandler(_ req: Request) throws -> EventLoopFuture { - let data = try req.content.syncDecode(CreateUserData.self) + let data = try req.content.decode(CreateUserData.self) return try validateUserCreation(data, on: req).flatMap { createUserErrors in if let errors = createUserErrors { - let presenter = try req.make(BlogAdminPresenter.self) - let view = try presenter.createUserView(on: req, editing: false, errors: errors.errors, name: data.name, nameError: errors.nameError, username: data.username, usernameErorr: errors.usernameError, passwordError: errors.passwordError, confirmPasswordError: errors.confirmPasswordError, resetPasswordOnLogin: data.resetPasswordOnLogin ?? false, userID: nil, profilePicture: data.profilePicture, twitterHandle: data.twitterHandle, biography: data.biography, tagline: data.tagline, pageInformation: req.adminPageInfomation()) - return try view.encode(for: req) + do { + let view = try req.adminPresenter.createUserView(editing: false, errors: errors.errors, name: data.name, nameError: errors.nameError, username: data.username, usernameErorr: errors.usernameError, passwordError: errors.passwordError, confirmPasswordError: errors.confirmPasswordError, resetPasswordOnLogin: data.resetPasswordOnLogin ?? false, userID: nil, profilePicture: data.profilePicture, twitterHandle: data.twitterHandle, biography: data.biography, tagline: data.tagline, pageInformation: req.adminPageInfomation()) + return view.encodeResponse(for: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } guard let name = data.name, let username = data.username, let password = data.password else { - throw Abort(.internalServerError) + return req.eventLoop.makeFailedFuture(Abort(.internalServerError)) } - let hasher = try req.make(PasswordHasher.self) - let hashedPassword = try hasher.hash(password) - let profilePicture = data.profilePicture.isEmptyOrWhitespace() ? nil : data.profilePicture - let twitterHandle = data.twitterHandle.isEmptyOrWhitespace() ? nil : data.twitterHandle - let biography = data.biography.isEmptyOrWhitespace() ? nil : data.biography - let tagline = data.tagline.isEmptyOrWhitespace() ? nil : data.tagline - let newUser = BlogUser(name: name, username: username.lowercased(), password: hashedPassword, profilePicture: profilePicture, twitterHandle: twitterHandle, biography: biography, tagline: tagline) - if let resetPasswordRequired = data.resetPasswordOnLogin, resetPasswordRequired { - newUser.resetPasswordRequired = true - } - let userRepository = try req.make(BlogUserRepository.self) - return userRepository.save(newUser, on: req).map { _ in - return req.redirect(to: self.pathCreator.createPath(for: "admin")) + return req.password.async.hash(password).flatMap { hashedPassword in + let profilePicture = data.profilePicture.isEmptyOrWhitespace() ? nil : data.profilePicture + let twitterHandle = data.twitterHandle.isEmptyOrWhitespace() ? nil : data.twitterHandle + let biography = data.biography.isEmptyOrWhitespace() ? nil : data.biography + let tagline = data.tagline.isEmptyOrWhitespace() ? nil : data.tagline + let newUser = BlogUser(name: name, username: username.lowercased(), password: hashedPassword, profilePicture: profilePicture, twitterHandle: twitterHandle, biography: biography, tagline: tagline) + if let resetPasswordRequired = data.resetPasswordOnLogin, resetPasswordRequired { + newUser.resetPasswordRequired = true + } + return req.blogUserRepository.save(newUser).map { _ in + return req.redirect(to: self.pathCreator.createPath(for: "admin")) + } } - } } func editUserHandler(_ req: Request) throws -> EventLoopFuture { - return try req.parameters.next(BlogUser.self).flatMap { user in - let presenter = try req.make(BlogAdminPresenter.self) - return try presenter.createUserView(on: req, editing: true, errors: nil, name: user.name, nameError: false, username: user.username, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: user.resetPasswordRequired, userID: user.userID, profilePicture: user.profilePicture, twitterHandle: user.twitterHandle, biography: user.biography, tagline: user.tagline, pageInformation: req.adminPageInfomation()) + req.parameters.findUser(on: req).flatMap { user in + do { + return try req.adminPresenter.createUserView(editing: true, errors: nil, name: user.name, nameError: false, username: user.username, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: user.resetPasswordRequired, userID: user.userID, profilePicture: user.profilePicture, twitterHandle: user.twitterHandle, biography: user.biography, tagline: user.tagline, pageInformation: req.adminPageInfomation()) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } } func editUserPostHandler(_ req: Request) throws -> EventLoopFuture { - return try req.parameters.next(BlogUser.self).flatMap { user in - let data = try req.content.syncDecode(CreateUserData.self) + req.parameters.findUser(on: req).flatMap { user in + do { + let data = try req.content.decode(CreateUserData.self) - guard let name = data.name, let username = data.username else { - throw Abort(.internalServerError) - } - - return try self.validateUserCreation(data, editing: true, existingUsername: user.username, on: req).flatMap { errors in - if let editUserErrors = errors { - let presenter = try req.make(BlogAdminPresenter.self) - let view = try presenter.createUserView(on: req, editing: true, errors: editUserErrors.errors, name: data.name, nameError: errors?.nameError ?? false, username: data.username, usernameErorr: errors?.usernameError ?? false, passwordError: editUserErrors.passwordError, confirmPasswordError: editUserErrors.confirmPasswordError, resetPasswordOnLogin: data.resetPasswordOnLogin ?? false, userID: user.userID, profilePicture: data.profilePicture, twitterHandle: data.twitterHandle, biography: data.biography, tagline: data.tagline, pageInformation: req.adminPageInfomation()) - return try view.encode(for: req) + guard let name = data.name, let username = data.username else { + throw Abort(.internalServerError) } - user.name = name - user.username = username.lowercased() - - let profilePicture = data.profilePicture.isEmptyOrWhitespace() ? nil : data.profilePicture - let twitterHandle = data.twitterHandle.isEmptyOrWhitespace() ? nil : data.twitterHandle - let biography = data.biography.isEmptyOrWhitespace() ? nil : data.biography - let tagline = data.tagline.isEmptyOrWhitespace() ? nil : data.tagline - - user.profilePicture = profilePicture - user.twitterHandle = twitterHandle - user.biography = biography - user.tagline = tagline - - if let resetPasswordOnLogin = data.resetPasswordOnLogin, resetPasswordOnLogin { - user.resetPasswordRequired = true - } + return try self.validateUserCreation(data, editing: true, existingUsername: user.username, on: req).flatMap { errors in + if let editUserErrors = errors { + do { + let view = try req.adminPresenter.createUserView(editing: true, errors: editUserErrors.errors, name: data.name, nameError: errors?.nameError ?? false, username: data.username, usernameErorr: errors?.usernameError ?? false, passwordError: editUserErrors.passwordError, confirmPasswordError: editUserErrors.confirmPasswordError, resetPasswordOnLogin: data.resetPasswordOnLogin ?? false, userID: user.userID, profilePicture: data.profilePicture, twitterHandle: data.twitterHandle, biography: data.biography, tagline: data.tagline, pageInformation: req.adminPageInfomation()) + return view.encodeResponse(for: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } - if let password = data.password, password != "" { - let hasher = try req.make(PasswordHasher.self) - user.password = try hasher.hash(password) - } + user.name = name + user.username = username.lowercased() + + let profilePicture = data.profilePicture.isEmptyOrWhitespace() ? nil : data.profilePicture + let twitterHandle = data.twitterHandle.isEmptyOrWhitespace() ? nil : data.twitterHandle + let biography = data.biography.isEmptyOrWhitespace() ? nil : data.biography + let tagline = data.tagline.isEmptyOrWhitespace() ? nil : data.tagline + + user.profilePicture = profilePicture + user.twitterHandle = twitterHandle + user.biography = biography + user.tagline = tagline + + if let resetPasswordOnLogin = data.resetPasswordOnLogin, resetPasswordOnLogin { + user.resetPasswordRequired = true + } - let redirect = req.redirect(to: self.pathCreator.createPath(for: "admin")) - let userRepository = try req.make(BlogUserRepository.self) - return userRepository.save(user, on: req).transform(to: redirect) + let updatePassword: EventLoopFuture + if let password = data.password, password != "" { + updatePassword = req.password.async.hash(password).map { hashedPassword in + user.password = hashedPassword + } + } else { + updatePassword = req.eventLoop.future() + } + return updatePassword.flatMap { + let redirect = req.redirect(to: self.pathCreator.createPath(for: "admin")) + return req.blogUserRepository.save(user).transform(to: redirect) + } + } + } catch { + return req.eventLoop.makeFailedFuture(error) } } } func deleteUserPostHandler(_ req: Request) throws -> EventLoopFuture { - let userRepository = try req.make(BlogUserRepository.self) - return try flatMap(req.parameters.next(BlogUser.self), userRepository.getUsersCount(on: req)) { user, userCount in + req.parameters.findUser(on: req).and(req.blogUserRepository.getUsersCount()).flatMap { user, userCount in guard userCount > 1 else { - let postRepository = try req.make(BlogPostRepository.self) - return flatMap(postRepository.getAllPostsSortedByPublishDate(includeDrafts: true, on: req), userRepository.getAllUsers(on: req)) { posts, users in - let presenter = try req.make(BlogAdminPresenter.self) - let view = try presenter.createIndexView(on: req, posts: posts, users: users, errors: ["You cannot delete the last user"], pageInformation: req.adminPageInfomation()) - return try view.encode(for: req) + return req.blogPostRepository.getAllPostsSortedByPublishDate(includeDrafts: true).and(req.blogUserRepository.getAllUsers()).flatMap { posts, users in + do { + let view = try req.adminPresenter.createIndexView(posts: posts, users: users, errors: ["You cannot delete the last user"], pageInformation: req.adminPageInfomation()) + return view.encodeResponse(for: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } } - let loggedInUser = try req.requireAuthenticated(BlogUser.self) + let loggedInUser: BlogUser + do { + loggedInUser = try req.auth.require(BlogUser.self) + } catch { + return req.eventLoop.makeFailedFuture(error) + } guard loggedInUser.userID != user.userID else { - let postRepository = try req.make(BlogPostRepository.self) - return flatMap(postRepository.getAllPostsSortedByPublishDate(includeDrafts: true, on: req), userRepository.getAllUsers(on: req)) { posts, users in - let presenter = try req.make(BlogAdminPresenter.self) - let view = try presenter.createIndexView(on: req, posts: posts, users: users, errors: ["You cannot delete yourself whilst logged in"], pageInformation: req.adminPageInfomation()) - return try view.encode(for: req) + return req.blogPostRepository.getAllPostsSortedByPublishDate(includeDrafts: true).and(req.blogUserRepository.getAllUsers()).flatMap { posts, users in + do { + let view = try req.adminPresenter.createIndexView(posts: posts, users: users, errors: ["You cannot delete yourself whilst logged in"], pageInformation: req.adminPageInfomation()) + return view.encodeResponse(for: req) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } } let redirect = req.redirect(to: self.pathCreator.createPath(for: "admin")) - return userRepository.delete(user, on: req).transform(to: redirect) + return req.blogUserRepository.delete(user).transform(to: redirect) } } @@ -180,19 +201,18 @@ struct UserAdminController: RouteCollection { } do { - try data.validate() + try CreateUserData.validate(req) } catch { createUserErrors.append("The username provided is not valid") usernameError = true } var usernameUniqueError: EventLoopFuture - let usersRepository = try req.make(BlogUserRepository.self) if let username = data.username { if editing && data.username == existingUsername { - usernameUniqueError = req.future(nil) + usernameUniqueError = req.eventLoop.future(nil) } else { - usernameUniqueError = usersRepository.getUser(username: username.lowercased(), on: req).map { user in + usernameUniqueError = req.blogUserRepository.getUser(username: username.lowercased()).map { user in if user != nil { return "Sorry that username has already been taken" } else { @@ -201,7 +221,7 @@ struct UserAdminController: RouteCollection { } } } else { - usernameUniqueError = req.future(nil) + usernameUniqueError = req.eventLoop.future(nil) } return usernameUniqueError.map { usernameErrorOccurred in diff --git a/Sources/SteamPress/Controllers/BlogAdminController.swift b/Sources/SteamPress/Controllers/BlogAdminController.swift index 805c617d..25a4fa68 100644 --- a/Sources/SteamPress/Controllers/BlogAdminController.swift +++ b/Sources/SteamPress/Controllers/BlogAdminController.swift @@ -1,5 +1,4 @@ import Vapor -import Authentication struct BlogAdminController: RouteCollection { @@ -12,8 +11,8 @@ struct BlogAdminController: RouteCollection { } // MARK: - Route setup - func boot(router: Router) throws { - let adminRoutes = router.grouped("admin") + func boot(routes: RoutesBuilder) throws { + let adminRoutes = routes.grouped("admin") let redirectMiddleware = BlogLoginRedirectAuthMiddleware(pathCreator: pathCreator) let adminProtectedRoutes = adminRoutes.grouped(redirectMiddleware) @@ -29,11 +28,12 @@ struct BlogAdminController: RouteCollection { // MARK: Admin Handler func adminHandler(_ req: Request) throws -> EventLoopFuture { - let usersRepository = try req.make(BlogUserRepository.self) - let postsRepository = try req.make(BlogPostRepository.self) - return flatMap(postsRepository.getAllPostsSortedByPublishDate(includeDrafts: true, on: req), usersRepository.getAllUsers(on: req)) { posts, users in - let presenter = try req.make(BlogAdminPresenter.self) - return try presenter.createIndexView(on: req, posts: posts, users: users, errors: nil, pageInformation: req.adminPageInfomation()) + return req.blogPostRepository.getAllPostsSortedByPublishDate(includeDrafts: true).and(req.blogUserRepository.getAllUsers()).flatMap { posts, users in + do { + return try req.adminPresenter.createIndexView(posts: posts, users: users, errors: nil, pageInformation: req.adminPageInfomation()) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } } diff --git a/Sources/SteamPress/Controllers/BlogController.swift b/Sources/SteamPress/Controllers/BlogController.swift index 19b54213..40904538 100644 --- a/Sources/SteamPress/Controllers/BlogController.swift +++ b/Sources/SteamPress/Controllers/BlogController.swift @@ -3,11 +3,11 @@ import Vapor struct BlogController: RouteCollection { // MARK: - Properties - fileprivate let blogPostsPath = "posts" - fileprivate let tagsPath = "tags" - fileprivate let authorsPath = "authors" - fileprivate let apiPath = "api" - fileprivate let searchPath = "search" + fileprivate let blogPostsPath = PathComponent(stringLiteral: "posts") + fileprivate let tagsPath = PathComponent(stringLiteral: "tags") + fileprivate let authorsPath = PathComponent(stringLiteral: "authors") + fileprivate let apiPath = PathComponent(stringLiteral: "api") + fileprivate let searchPath = PathComponent(stringLiteral: "search") fileprivate let pathCreator: BlogPathCreator fileprivate let enableAuthorPages: Bool fileprivate let enableTagsPages: Bool @@ -22,35 +22,35 @@ struct BlogController: RouteCollection { } // MARK: - Add routes - func boot(router: Router) throws { - router.get(use: indexHandler) - router.get(blogPostsPath, String.parameter, use: blogPostHandler) - router.get(blogPostsPath, use: blogPostIndexRedirectHandler) - router.get(searchPath, use: searchHandler) + func boot(routes: RoutesBuilder) throws { + routes.get(use: indexHandler) + routes.get(blogPostsPath, ":blogSlug", use: blogPostHandler) + routes.get(blogPostsPath, use: blogPostIndexRedirectHandler) + routes.get(searchPath, use: searchHandler) if enableAuthorPages { - router.get(authorsPath, use: allAuthorsViewHandler) - router.get(authorsPath, String.parameter, use: authorViewHandler) + routes.get(authorsPath, use: allAuthorsViewHandler) + routes.get(authorsPath, ":authorUsername", use: authorViewHandler) } if enableTagsPages { - router.get(tagsPath, BlogTag.parameter, use: tagViewHandler) - router.get(tagsPath, use: allTagsViewHandler) + routes.get(tagsPath, BlogTag.parameter, use: tagViewHandler) + routes.get(tagsPath, use: allTagsViewHandler) } } // MARK: - Route Handlers func indexHandler(_ req: Request) throws -> EventLoopFuture { - let postRepository = try req.make(BlogPostRepository.self) - let tagRepository = try req.make(BlogTagRepository.self) - let userRepository = try req.make(BlogUserRepository.self) let paginationInformation = req.getPaginationInformation(postsPerPage: postsPerPage) - return flatMap(postRepository.getAllPostsSortedByPublishDate(includeDrafts: false, on: req, count: postsPerPage, offset: paginationInformation.offset), - tagRepository.getAllTags(on: req), - userRepository.getAllUsers(on: req), - postRepository.getAllPostsCount(includeDrafts: false, on: req), - tagRepository.getTagsForAllPosts(on: req)) { posts, tags, users, totalPostCount, tagsForPosts in - let presenter = try req.make(BlogPresenter.self) - return presenter.indexView(on: req, posts: posts, tags: tags, authors: users, tagsForPosts: tagsForPosts, pageInformation: try req.pageInformation(), paginationTagInfo: self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: totalPostCount, currentQuery: req.http.url.query)) + return req.blogPostRepository.getAllPostsSortedByPublishDate(includeDrafts: false, count: postsPerPage, offset: paginationInformation.offset).and(req.blogTagRepository.getAllTags()).flatMap { posts, tags in + req.blogUserRepository.getAllUsers().and(req.blogPostRepository.getAllPostsCount(includeDrafts: false)).flatMap { users, totalPostCount in + req.blogTagRepository.getTagsForAllPosts().flatMap { tagsForPosts in + do { + return req.blogPresenter.indexView(posts: posts, tags: tags, authors: users, tagsForPosts: tagsForPosts, pageInformation: try req.pageInformation(), paginationTagInfo: self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: totalPostCount, currentQuery: req.url.query)) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } + } } } @@ -59,106 +59,121 @@ struct BlogController: RouteCollection { } func blogPostHandler(_ req: Request) throws -> EventLoopFuture { - let blogSlug = try req.parameters.next(String.self) - let blogRepository = try req.make(BlogPostRepository.self) - return blogRepository.getPost(slug: blogSlug, on: req).unwrap(or: Abort(.notFound)).flatMap { post in - let userRepository = try req.make(BlogUserRepository.self) - let tagsRepository = try req.make(BlogTagRepository.self) - let tagsQuery = tagsRepository.getTags(for: post, on: req) - let userQuery = userRepository.getUser(id: post.author, on: req).unwrap(or: Abort(.internalServerError)) - return flatMap(userQuery, tagsQuery) { user, tags in - let presenter = try req.make(BlogPresenter.self) - return presenter.postView(on: req, post: post, author: user, tags: tags, pageInformation: try req.pageInformation()) + guard let blogSlug: String = req.parameters.get("blogSlug") else { + throw Abort(.badRequest) + } + return req.blogPostRepository.getPost(slug: blogSlug).unwrap(or: Abort(.notFound)).flatMap { (post: BlogPost) -> EventLoopFuture in + let tagsQuery: EventLoopFuture<[BlogTag]> = req.blogTagRepository.getTags(for: post) + let userQuery: EventLoopFuture = req.blogUserRepository.getUser(id: post.author).unwrap(or: Abort(.internalServerError)) + return userQuery.and(tagsQuery).flatMap { (user: BlogUser, tags: [BlogTag]) -> EventLoopFuture in + do { + let pageInformation: BlogGlobalPageInformation = try req.pageInformation() + return req.blogPresenter.postView(post: post, author: user, tags: tags, pageInformation: pageInformation) + } catch { + return req.eventLoop.makeFailedFuture(error) + } } } } func tagViewHandler(_ req: Request) throws -> EventLoopFuture { - return try req.parameters.next(BlogTag.self).flatMap { tag in - let postRepository = try req.make(BlogPostRepository.self) - let usersRepository = try req.make(BlogUserRepository.self) + return req.parameters.findTag(on: req).flatMap { tag in let paginationInformation = req.getPaginationInformation(postsPerPage: self.postsPerPage) - let postsQuery = postRepository.getSortedPublishedPosts(for: tag, on: req, count: self.postsPerPage, offset: paginationInformation.offset) - let postCountQuery = postRepository.getPublishedPostCount(for: tag, on: req) - let usersQuery = usersRepository.getAllUsers(on: req) - return flatMap(postsQuery, postCountQuery, usersQuery) { posts, totalPosts, authors in - let presenter = try req.make(BlogPresenter.self) - let paginationTagInfo = self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: totalPosts, currentQuery: req.http.url.query) - return presenter.tagView(on: req, tag: tag, posts: posts, authors: authors, totalPosts: totalPosts, pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) + let postsQuery = req.blogPostRepository.getSortedPublishedPosts(for: tag, count: self.postsPerPage, offset: paginationInformation.offset) + let postCountQuery = req.blogPostRepository.getPublishedPostCount(for: tag) + let usersQuery = req.blogUserRepository.getAllUsers() + return postsQuery.and(postCountQuery).flatMap { posts, totalPosts in + usersQuery.flatMap { authors in + let paginationTagInfo = self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: totalPosts, currentQuery: req.url.query) + do { + return req.blogPresenter.tagView(tag: tag, posts: posts, authors: authors, totalPosts: totalPosts, pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } } } } func authorViewHandler(_ req: Request) throws -> EventLoopFuture { - let authorUsername = try req.parameters.next(String.self) - let userRepository = try req.make(BlogUserRepository.self) + guard let authorUsername = req.parameters.get("authorUsername") else { + throw Abort(.badRequest) + } let paginationInformation = req.getPaginationInformation(postsPerPage: postsPerPage) - return userRepository.getUser(username: authorUsername, on: req).flatMap { user in + return req.blogUserRepository.getUser(username: authorUsername).flatMap { user in guard let author = user else { - throw Abort(.notFound) + return req.eventLoop.makeFailedFuture(Abort(.notFound)) } - - let postRepository = try req.make(BlogPostRepository.self) - let tagsRepostiory = try req.make(BlogTagRepository.self) - let authorPostQuery = postRepository.getAllPostsSortedByPublishDate(for: author, includeDrafts: false, on: req, count: self.postsPerPage, offset: paginationInformation.offset) - let tagQuery = tagsRepostiory.getTagsForAllPosts(on: req) - let authorPostCountQuery = postRepository.getPostCount(for: author, on: req) - return flatMap(authorPostQuery, authorPostCountQuery, tagQuery) { posts, postCount, tagsForPosts in - let presenter = try req.make(BlogPresenter.self) - let paginationTagInfo = self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: postCount, currentQuery: req.http.url.query) - return presenter.authorView(on: req, author: author, posts: posts, postCount: postCount, tagsForPosts: tagsForPosts, pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) + let authorPostQuery = req.blogPostRepository.getAllPostsSortedByPublishDate(for: author, includeDrafts: false, count: self.postsPerPage, offset: paginationInformation.offset) + let tagQuery = req.blogTagRepository.getTagsForAllPosts() + let authorPostCountQuery = req.blogPostRepository.getPostCount(for: author) + return authorPostQuery.and(authorPostCountQuery).flatMap { posts, postCount in + tagQuery.flatMap { tagsForPosts in + let paginationTagInfo = self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: postCount, currentQuery: req.url.query) + do { + return req.blogPresenter.authorView(author: author, posts: posts, postCount: postCount, tagsForPosts: tagsForPosts, pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } } } } func allTagsViewHandler(_ req: Request) throws -> EventLoopFuture { - let tagRepository = try req.make(BlogTagRepository.self) - return tagRepository.getAllTagsWithPostCount(on: req).flatMap { tagswithCount in - let presenter = try req.make(BlogPresenter.self) + return req.blogTagRepository.getAllTagsWithPostCount().flatMap { tagswithCount in let allTags = tagswithCount.map { $0.0 } - let tagCounts = try tagswithCount.reduce(into: [Int: Int]()) { - guard let tagID = $1.0.tagID else { - throw SteamPressError(identifier: "BlogController", "Tag ID not set") + do { + let tagCounts = try tagswithCount.reduce(into: [Int: Int]()) { + guard let tagID = $1.0.tagID else { + throw SteamPressError(identifier: "BlogController", "Tag ID not set") + } + return $0[tagID] = $1.1 } - return $0[tagID] = $1.1 + return req.blogPresenter.allTagsView(tags: allTags, tagPostCounts: tagCounts, pageInformation: try req.pageInformation()) + } catch { + return req.eventLoop.makeFailedFuture(error) } - return presenter.allTagsView(on: req, tags: allTags, tagPostCounts: tagCounts, pageInformation: try req.pageInformation()) } } func allAuthorsViewHandler(_ req: Request) throws -> EventLoopFuture { - let presenter = try req.make(BlogPresenter.self) - let authorRepository = try req.make(BlogUserRepository.self) - return authorRepository.getAllUsersWithPostCount(on: req).flatMap { allUsersWithCount in + return req.blogUserRepository.getAllUsersWithPostCount().flatMap { allUsersWithCount in let allUsers = allUsersWithCount.map { $0.0 } - let authorCounts = try allUsersWithCount.reduce(into: [Int: Int]()) { - guard let userID = $1.0.userID else { - throw SteamPressError(identifier: "BlogController", "User ID not set") + do { + let authorCounts = try allUsersWithCount.reduce(into: [Int: Int]()) { + guard let userID = $1.0.userID else { + throw SteamPressError(identifier: "BlogController", "User ID not set") + } + return $0[userID] = $1.1 } - return $0[userID] = $1.1 + return req.blogPresenter.allAuthorsView(authors: allUsers, authorPostCounts: authorCounts, pageInformation: try req.pageInformation()) + } catch { + return req.eventLoop.makeFailedFuture(error) } - return presenter.allAuthorsView(on: req, authors: allUsers, authorPostCounts: authorCounts, pageInformation: try req.pageInformation()) } } func searchHandler(_ req: Request) throws -> EventLoopFuture { - let preseneter = try req.make(BlogPresenter.self) let paginationInformation = req.getPaginationInformation(postsPerPage: postsPerPage) guard let searchTerm = req.query[String.self, at: "term"], !searchTerm.isEmpty else { - let paginationTagInfo = getPaginationInformation(currentPage: paginationInformation.page, totalPosts: 0, currentQuery: req.http.url.query) - return preseneter.searchView(on: req, totalResults: 0, posts: [], authors: [], searchTerm: nil, tagsForPosts: [:], pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) + let paginationTagInfo = getPaginationInformation(currentPage: paginationInformation.page, totalPosts: 0, currentQuery: req.url.query) + return req.blogPresenter.searchView(totalResults: 0, posts: [], authors: [], searchTerm: nil, tagsForPosts: [:], pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) } - let postRepository = try req.make(BlogPostRepository.self) - let authorRepository = try req.make(BlogUserRepository.self) - let tagRepository = try req.make(BlogTagRepository.self) - let postsCountQuery = postRepository.getPublishedPostCount(for: searchTerm, on: req) - let postsQuery = postRepository.findPublishedPostsOrdered(for: searchTerm, on: req, count: self.postsPerPage, offset: paginationInformation.offset) - let tagsQuery = tagRepository.getTagsForAllPosts(on: req) - let userQuery = authorRepository.getAllUsers(on: req) - return flatMap(postsQuery, postsCountQuery, userQuery, tagsQuery) { posts, totalPosts, users, tagsForPosts in - let paginationTagInfo = self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: totalPosts, currentQuery: req.http.url.query) - return preseneter.searchView(on: req, totalResults: totalPosts, posts: posts, authors: users, searchTerm: searchTerm, tagsForPosts: tagsForPosts, pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) + let postsCountQuery = req.blogPostRepository.getPublishedPostCount(for: searchTerm) + let postsQuery = req.blogPostRepository.findPublishedPostsOrdered(for: searchTerm, count: self.postsPerPage, offset: paginationInformation.offset) + let tagsQuery = req.blogTagRepository.getTagsForAllPosts() + let userQuery = req.blogUserRepository.getAllUsers() + return postsQuery.and(postsCountQuery).flatMap { posts, totalPosts in + userQuery.and(tagsQuery).flatMap { users, tagsForPosts in + let paginationTagInfo = self.getPaginationInformation(currentPage: paginationInformation.page, totalPosts: totalPosts, currentQuery: req.url.query) + do { + return req.blogPresenter.searchView(totalResults: totalPosts, posts: posts, authors: users, searchTerm: searchTerm, tagsForPosts: tagsForPosts, pageInformation: try req.pageInformation(), paginationTagInfo: paginationTagInfo) + } catch { + return req.eventLoop.makeFailedFuture(error) + } + } } } diff --git a/Sources/SteamPress/Controllers/FeedController.swift b/Sources/SteamPress/Controllers/FeedController.swift index b51b03de..a3ee6248 100644 --- a/Sources/SteamPress/Controllers/FeedController.swift +++ b/Sources/SteamPress/Controllers/FeedController.swift @@ -28,8 +28,8 @@ struct FeedController: RouteCollection { } // MARK: - Route Collection - func boot(router: Router) throws { - router.get("atom.xml", use: atomGenerator.feedHandler) - router.get("rss.xml", use: rssGenerator.feedHandler) + func boot(routes: RoutesBuilder) throws { + routes.get("atom.xml", use: atomGenerator.feedHandler) + routes.get("rss.xml", use: rssGenerator.feedHandler) } } diff --git a/Sources/SteamPress/Extensions/BCrypt+PasswordHasher.swift b/Sources/SteamPress/Extensions/BCrypt+PasswordHasher.swift deleted file mode 100644 index f9b94eed..00000000 --- a/Sources/SteamPress/Extensions/BCrypt+PasswordHasher.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Vapor -import Crypto - -public protocol PasswordHasher: Service { - func hash(_ plaintext: LosslessDataConvertible) throws -> String -} - -extension BCryptDigest: PasswordHasher { - public func hash(_ plaintext: LosslessDataConvertible) throws -> String { - return try self.hash(plaintext, salt: nil) - } -} diff --git a/Sources/SteamPress/Extensions/Models+Parameters.swift b/Sources/SteamPress/Extensions/Models+Parameters.swift new file mode 100644 index 00000000..d83ab769 --- /dev/null +++ b/Sources/SteamPress/Extensions/Models+Parameters.swift @@ -0,0 +1,81 @@ +import Vapor + +extension BlogUser: ParameterModel { +// typealias Repository = BlogUserRepository + public static let parameterKey = "blogUserID" + public static let parameter = PathComponent(stringLiteral: ":\(BlogUser.parameterKey)") + +// public typealias ResolvedParameter = EventLoopFuture +// public static func resolveParameter(_ parameter: String, on container: Container) throws -> BlogUser.ResolvedParameter { +// let userRepository = try container.make(BlogUserRepository.self) +// guard let userID = Int(parameter) else { +// throw SteamPressError(identifier: "Invalid-ID-Type", "Unable to convert \(parameter) to a User ID") +// } +// return userRepository.getUser(id: userID, on: container).unwrap(or: Abort(.notFound)) +// } +} + +extension BlogPost: ParameterModel { +// typealias Repository = BlogPostRepository + public static let parameterKey = "blogPostID" + public static let parameter = PathComponent(stringLiteral: ":\(BlogPost.parameterKey)") + +// public typealias ResolvedParameter = EventLoopFuture +// public static func resolveParameter(_ parameter: String, on container: Container) throws -> EventLoopFuture { +// let postRepository = try container.make(BlogPostRepository.self) +// guard let postID = Int(parameter) else { +// throw SteamPressError(identifier: "Invalid-ID-Type", "Unable to convert \(parameter) to a Post ID") +// } +// return postRepository.getPost(id: postID, on: container).unwrap(or: Abort(.notFound)) +// } +} + +extension BlogTag: ParameterModel { +// typealias Repository = BlogTagRepository + public static let parameterKey = "blogTagName" + public static let parameter = PathComponent(stringLiteral: ":\(BlogTag.parameterKey)") + +// public typealias ResolvedParameter = EventLoopFuture +// public static func resolveParameter(_ parameter: String, on container: Container) throws -> EventLoopFuture { +// let tagRepository = try container.make(BlogTagRepository.self) +// return tagRepository.getTag(parameter, on: container).unwrap(or: Abort(.notFound)) +// } +} + +protocol ParameterModel { + static var parameterKey: String { get } + static var parameter: PathComponent { get } +// associatedtype Repository: SteamPressRepository +} +// +//extension Parameters { +// func find(on req: Request, repository: SteamPressRepository) -> EventLoopFuture where T: ParameterModel { +// guard let idString = req.parameters.get(T.parameterKey), let id = Int(idString) else { +// return req.eventLoop.makeFailedFuture(Abort(.badRequest)) +// } +// return repository.get(id, on: req.eventLoop) +// } +//} + +extension Parameters { + func findUser(on req: Request) -> EventLoopFuture { + guard let idString = req.parameters.get(BlogUser.parameterKey), let id = Int(idString) else { + return req.eventLoop.makeFailedFuture(Abort(.badRequest)) + } + return req.blogUserRepository.getUser(id: id).unwrap(or: Abort(.notFound)) + } + + func findPost(on req: Request) -> EventLoopFuture { + guard let idString = req.parameters.get(BlogPost.parameterKey), let id = Int(idString) else { + return req.eventLoop.makeFailedFuture(Abort(.badRequest)) + } + return req.blogPostRepository.getPost(id: id).unwrap(or: Abort(.notFound)) + } + + func findTag(on req: Request) -> EventLoopFuture { + guard let tagName = req.parameters.get(BlogTag.parameterKey) else { + return req.eventLoop.makeFailedFuture(Abort(.notFound)) + } + return req.blogTagRepository.getTag(tagName).unwrap(or: Abort(.notFound)) + } +} diff --git a/Sources/SteamPress/Extensions/Request+PageInformation.swift b/Sources/SteamPress/Extensions/Request+PageInformation.swift index 038397c0..2408fffa 100644 --- a/Sources/SteamPress/Extensions/Request+PageInformation.swift +++ b/Sources/SteamPress/Extensions/Request+PageInformation.swift @@ -6,10 +6,10 @@ extension Request { guard let currentEncodedURL = currentURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { throw SteamPressError(identifier: "STEAMPRESS", "Failed to convert page url to URL encoded") } - return try BlogGlobalPageInformation(disqusName: Environment.get("BLOG_DISQUS_NAME"), siteTwitterHandle: Environment.get("BLOG_SITE_TWITTER_HANDLE"), googleAnalyticsIdentifier: Environment.get("BLOG_GOOGLE_ANALYTICS_IDENTIFIER"), loggedInUser: authenticated(BlogUser.self), websiteURL: self.rootUrl(), currentPageURL: currentURL, currentPageEncodedURL: currentEncodedURL) + return try BlogGlobalPageInformation(disqusName: Environment.get("BLOG_DISQUS_NAME"), siteTwitterHandle: Environment.get("BLOG_SITE_TWITTER_HANDLE"), googleAnalyticsIdentifier: Environment.get("BLOG_GOOGLE_ANALYTICS_IDENTIFIER"), loggedInUser: self.auth.get(BlogUser.self), websiteURL: self.rootUrl(), currentPageURL: currentURL, currentPageEncodedURL: currentEncodedURL) } func adminPageInfomation() throws -> BlogAdminPageInformation { - return try BlogAdminPageInformation(loggedInUser: requireAuthenticated(BlogUser.self), websiteURL: self.rootUrl(), currentPageURL: self.url()) + return try BlogAdminPageInformation(loggedInUser: self.auth.require(BlogUser.self), websiteURL: self.rootUrl(), currentPageURL: self.url()) } } diff --git a/Sources/SteamPress/Extensions/String+Random.swift b/Sources/SteamPress/Extensions/String+Random.swift index 71095523..f7e25d6a 100644 --- a/Sources/SteamPress/Extensions/String+Random.swift +++ b/Sources/SteamPress/Extensions/String+Random.swift @@ -1,9 +1,8 @@ import Crypto extension String { - public static func random(length: Int = 12) throws -> String { - let randomData = try CryptoRandom().generateData(count: length) - let randomString = randomData.base64EncodedString() + public static func random(length: Int = 24) throws -> String { + let randomString = [UInt8].random(count: length).base64 return randomString } } diff --git a/Sources/SteamPress/Extensions/URL+Converters.swift b/Sources/SteamPress/Extensions/URL+Converters.swift index 889960ef..c82774d5 100644 --- a/Sources/SteamPress/Extensions/URL+Converters.swift +++ b/Sources/SteamPress/Extensions/URL+Converters.swift @@ -3,46 +3,26 @@ import Vapor extension Request { func url() throws -> URL { - let path = self.http.url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" - - let hostname: String - if let envURL = Environment.get("WEBSITE_URL") { - hostname = envURL + let path = self.url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let rootURL = try self.rootUrl() + if rootURL.absoluteString == "/" { + guard let pathURL = URL(string: path) else { + throw SteamPressError(identifier: "SteamPressError", "Failed to convert path to URL") + } + return pathURL } else { - hostname = self.http.remotePeer.description - } - - let urlString = "\(hostname)\(path)" - guard let url = URL(string: urlString) else { - throw SteamPressError(identifier: "SteamPressError", "Failed to convert url path to URL") + return rootURL.appendingPathComponent(path) } - return url } func rootUrl() throws -> URL { - if let envURL = Environment.get("WEBSITE_URL") { - guard let url = URL(string: envURL) else { - throw SteamPressError(identifier: "SteamPressError", "Failed to convert url hostname to URL") - } - return url + guard let hostname = Environment.get("WEBSITE_URL") else { + throw SteamPressError(identifier: "SteamPressError", "WEBSITE_URL not set") } - var hostname = self.http.remotePeer.description - if hostname == "" { - hostname = "/" - } guard let url = URL(string: hostname) else { throw SteamPressError(identifier: "SteamPressError", "Failed to convert url hostname to URL") } return url } } - -private extension String { - func replacingFirstOccurrence(of target: String, with replaceString: String) -> String { - if let range = self.range(of: target) { - return self.replacingCharacters(in: range, with: replaceString) - } - return self - } -} diff --git a/Sources/SteamPress/Feed Generators/AtomFeedGenerator.swift b/Sources/SteamPress/Feed Generators/AtomFeedGenerator.swift index 4d9e369c..f92c68a4 100644 --- a/Sources/SteamPress/Feed Generators/AtomFeedGenerator.swift +++ b/Sources/SteamPress/Feed Generators/AtomFeedGenerator.swift @@ -28,56 +28,61 @@ struct AtomFeedGenerator { // MARK: - Route Handler - func feedHandler(_ request: Request) throws -> EventLoopFuture { + func feedHandler(_ request: Request) throws -> EventLoopFuture { - let blogRepository = try request.make(BlogPostRepository.self) - return blogRepository.getAllPostsSortedByPublishDate(includeDrafts: false, on: request).flatMap { posts in - var feed = self.getFeedStart(for: request) + return request.blogPostRepository.getAllPostsSortedByPublishDate(includeDrafts: false).flatMap { posts in + do { + var feed = try self.getFeedStart(for: request) - if !posts.isEmpty { - let postDate = posts[0].lastEdited ?? posts[0].created - feed += "\(self.iso8601Formatter.string(from: postDate))\n" - } else { - feed += "\(self.iso8601Formatter.string(from: Date()))\n" - } - - if let copyright = self.copyright { - feed += "\(copyright)\n" - } + if !posts.isEmpty { + let postDate = posts[0].lastEdited ?? posts[0].created + feed += "\(self.iso8601Formatter.string(from: postDate))\n" + } else { + feed += "\(self.iso8601Formatter.string(from: Date()))\n" + } - if let imageURL = self.imageURL { - feed += "\(imageURL)\n" - } + if let copyright = self.copyright { + feed += "\(copyright)\n" + } - var postData: [EventLoopFuture] = [] - for post in posts { - try postData.append(post.getPostAtomFeed(blogPath: self.getRootPath(for: request), dateFormatter: self.iso8601Formatter, for: request)) - } + if let imageURL = self.imageURL { + feed += "\(imageURL)\n" + } - return postData.flatten(on: request).map { postsInformation in - for postInformation in postsInformation { - feed += postInformation + var postData: [EventLoopFuture] = [] + for post in posts { + try postData.append(post.getPostAtomFeed(blogPath: self.getRootPath(for: request), dateFormatter: self.iso8601Formatter, for: request)) } - feed += self.feedEnd - var httpResponse = HTTPResponse(body: feed) - httpResponse.headers.add(name: .contentType, value: "application/atom+xml") - return httpResponse + return postData.flatten(on: request.eventLoop).map { postsInformation in + for postInformation in postsInformation { + feed += postInformation + } + + feed += self.feedEnd + let httpResponse = Response(body: .init(stringLiteral: feed)) + httpResponse.headers.add(name: .contentType, value: "application/atom+xml") + return httpResponse + } + } catch { + return request.eventLoop.makeFailedFuture(error) } } } // MARK: - Private functions - private func getFeedStart(for request: Request) -> String { - let blogLink = getRootPath(for: request) + "/" + private func getFeedStart(for request: Request) throws -> String { + let blogLink = try getRootPath(for: request) + "/" let feedLink = blogLink + "atom.xml" return "\(xmlDeclaration)\n\(feedStart)\n\n\(title)\n\(description)\n\(blogLink)\n\n\nSteamPress\n" } - private func getRootPath(for request: Request) -> String { - let hostname = request.http.remotePeer.description - let path = request.http.url.path + private func getRootPath(for request: Request) throws -> String { + guard let hostname = Environment.get("WEBSITE_URL") else { + throw SteamPressError(identifier: "SteamPressError", "WEBSITE_URL not set") + } + let path = request.url.path return "\(hostname)\(path.replacingOccurrences(of: "/atom.xml", with: ""))" } } @@ -85,26 +90,28 @@ struct AtomFeedGenerator { fileprivate extension BlogPost { func getPostAtomFeed(blogPath: String, dateFormatter: DateFormatter, for request: Request) throws -> EventLoopFuture { let updatedTime = lastEdited ?? created - let authorRepository = try request.make(BlogUserRepository.self) - return authorRepository.getUser(id: author, on: request).flatMap { user in - guard let user = user else { - throw SteamPressError(identifier: "Invalid-relationship", "Blog user with ID \(self.author) not found") - } - guard let postID = self.blogID else { - throw SteamPressError(identifier: "ID-required", "Blog Post has no ID") - } - var postEntry = "\n\(blogPath)/posts-id/\(postID)/\n\(self.title)\n\(dateFormatter.string(from: updatedTime))\n\(dateFormatter.string(from: self.created))\n\n\(user.name)\n\(blogPath)/authors/\(user.username)/\n\n\(try self.description())\n\n" + return request.blogUserRepository.getUser(id: author).flatMap { user in + do { + guard let user = user else { + throw SteamPressError(identifier: "Invalid-relationship", "Blog user with ID \(self.author) not found") + } + guard let postID = self.blogID else { + throw SteamPressError(identifier: "ID-required", "Blog Post has no ID") + } + var postEntry = "\n\(blogPath)/posts-id/\(postID)/\n\(self.title)\n\(dateFormatter.string(from: updatedTime))\n\(dateFormatter.string(from: self.created))\n\n\(user.name)\n\(blogPath)/authors/\(user.username)/\n\n\(try self.description())\n\n" - let tagRepository = try request.make(BlogTagRepository.self) - return tagRepository.getTags(for: self, on: request).map { tags in - for tag in tags { - if let percentDecodedTag = tag.name.removingPercentEncoding { - postEntry += "\n" + return request.blogTagRepository.getTags(for: self).map { tags in + for tag in tags { + if let percentDecodedTag = tag.name.removingPercentEncoding { + postEntry += "\n" + } } - } - postEntry += "\n" - return postEntry + postEntry += "\n" + return postEntry + } + } catch { + return request.eventLoop.makeFailedFuture(error) } } } diff --git a/Sources/SteamPress/Feed Generators/RSSFeedGenerator.swift b/Sources/SteamPress/Feed Generators/RSSFeedGenerator.swift index 8919ecc6..16fdc7b2 100644 --- a/Sources/SteamPress/Feed Generators/RSSFeedGenerator.swift +++ b/Sources/SteamPress/Feed Generators/RSSFeedGenerator.swift @@ -28,42 +28,44 @@ struct RSSFeedGenerator { // MARK: - Route Handler - func feedHandler(_ request: Request) throws -> EventLoopFuture { + func feedHandler(_ request: Request) throws -> EventLoopFuture { + request.blogPostRepository.getAllPostsSortedByPublishDate(includeDrafts: false).flatMap { posts in + do { + var xmlFeed = try self.getXMLStart(for: request) + + if !posts.isEmpty { + let postDate = posts[0].lastEdited ?? posts[0].created + xmlFeed += "\(self.rfc822DateFormatter.string(from: postDate))\n" + } - let blogRepository = try request.make(BlogPostRepository.self) - return blogRepository.getAllPostsSortedByPublishDate(includeDrafts: false, on: request).flatMap { posts in - var xmlFeed = self.getXMLStart(for: request) + xmlFeed += try "\nSearch \(self.title)\nSearch\n\(self.getRootPath(for: request))/search?\nterm\n\n" - if !posts.isEmpty { - let postDate = posts[0].lastEdited ?? posts[0].created - xmlFeed += "\(self.rfc822DateFormatter.string(from: postDate))\n" - } + var postData: [EventLoopFuture] = [] + for post in posts { + try postData.append(post.getPostRSSFeed(rootPath: self.getRootPath(for: request), dateFormatter: self.rfc822DateFormatter, for: request)) + } - xmlFeed += "\nSearch \(self.title)\nSearch\n\(self.getRootPath(for: request))/search?\nterm\n\n" + return postData.flatten(on: request.eventLoop).map { postInformation in + for post in postInformation { + xmlFeed += post + } - var postData: [EventLoopFuture] = [] - for post in posts { - try postData.append(post.getPostRSSFeed(rootPath: self.getRootPath(for: request), dateFormatter: self.rfc822DateFormatter, for: request)) - } - - return postData.flatten(on: request).map { postInformation in - for post in postInformation { - xmlFeed += post + xmlFeed += self.xmlEnd + let httpResponse = Response(body: .init(stringLiteral: xmlFeed)) + httpResponse.headers.add(name: .contentType, value: "application/rss+xml") + return httpResponse } - - xmlFeed += self.xmlEnd - var httpResponse = HTTPResponse(body: xmlFeed) - httpResponse.headers.add(name: .contentType, value: "application/rss+xml") - return httpResponse + } catch { + return request.eventLoop.makeFailedFuture(error) } } } // MARK: - Private functions - private func getXMLStart(for request: Request) -> String { + private func getXMLStart(for request: Request) throws -> String { - let link = getRootPath(for: request) + "/" + let link = try getRootPath(for: request) + "/" var start = "\n\n\n\n\(title)\n\(link)\n\(description)\nSteamPress\n60\n" @@ -78,9 +80,11 @@ struct RSSFeedGenerator { return start } - private func getRootPath(for request: Request) -> String { - let hostname = request.http.remotePeer.description - let path = request.http.url.path + private func getRootPath(for request: Request) throws -> String { + guard let hostname = Environment.get("WEBSITE_URL") else { + throw SteamPressError(identifier: "SteamPressError", "WEBSITE_URL not set") + } + let path = request.url.path return "\(hostname)\(path.replacingOccurrences(of: "/rss.xml", with: ""))" } } @@ -90,8 +94,7 @@ fileprivate extension BlogPost { let link = rootPath + "/posts/\(slugUrl)/" var postEntry = "\n\n\(title)\n\n\n\(try description())\n\n\n\(link)\n\n" - let tagRepository = try request.make(BlogTagRepository.self) - return tagRepository.getTags(for: self, on: request).map { tags in + return request.blogTagRepository.getTags(for: self).map { tags in for tag in tags { if let percentDecodedTag = tag.name.removingPercentEncoding { postEntry += "\(percentDecodedTag)\n" diff --git a/Sources/SteamPress/Middleware/BlogAuthSessionsMiddleware.swift b/Sources/SteamPress/Middleware/BlogAuthSessionsMiddleware.swift index b739b45e..16c64e4c 100644 --- a/Sources/SteamPress/Middleware/BlogAuthSessionsMiddleware.swift +++ b/Sources/SteamPress/Middleware/BlogAuthSessionsMiddleware.swift @@ -4,26 +4,25 @@ public final class BlogAuthSessionsMiddleware: Middleware { public init() {} - public func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture { + public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { let future: EventLoopFuture - if let userIDString = try request.session()["_BlogUserSession"], let userID = Int(userIDString) { - let userRepository = try request.make(BlogUserRepository.self) - future = userRepository.getUser(id: userID, on: request).flatMap { user in + if let userIDString = request.session.data["_BlogUserSession"], let userID = Int(userIDString) { + future = request.blogUserRepository.getUser(id: userID).flatMap { user in if let user = user { - try request.authenticate(user) + request.auth.login(user) } - return .done(on: request) + return request.eventLoop.future() } } else { - future = .done(on: request) + future = request.eventLoop.future() } return future.flatMap { - return try next.respond(to: request).map { response in - if let user = try request.authenticated(BlogUser.self) { - try user.authenticateSession(on: request) + return next.respond(to: request).map { response in + if let user = request.auth.get(BlogUser.self) { + user.authenticateSession(on: request) } else { - try request.unauthenticateBlogUserSession() + request.unauthenticateBlogUserSession() } return response } diff --git a/Sources/SteamPress/Middleware/BlogLoginRedirectAuthMiddleware.swift b/Sources/SteamPress/Middleware/BlogLoginRedirectAuthMiddleware.swift index eb78d457..b5c1ab8e 100644 --- a/Sources/SteamPress/Middleware/BlogLoginRedirectAuthMiddleware.swift +++ b/Sources/SteamPress/Middleware/BlogLoginRedirectAuthMiddleware.swift @@ -3,22 +3,22 @@ import Vapor struct BlogLoginRedirectAuthMiddleware: Middleware { let pathCreator: BlogPathCreator - - func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture { + + func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { do { - let user = try request.requireAuthenticated(BlogUser.self) + let user = try request.auth.require(BlogUser.self) let resetPasswordPath = pathCreator.createPath(for: "admin/resetPassword") - var requestPath = request.http.urlString + var requestPath = request.url.string if !requestPath.hasSuffix("/") { requestPath = requestPath + "/" } if user.resetPasswordRequired && requestPath != resetPasswordPath { let redirect = request.redirect(to: resetPasswordPath) - return request.future(redirect) + return request.eventLoop.future(redirect) } } catch { - return request.future(request.redirect(to: pathCreator.createPath(for: "admin/login", query: "loginRequired"))) + return request.eventLoop.future(request.redirect(to: pathCreator.createPath(for: "admin/login", query: "loginRequired"))) } - return try next.respond(to: request) + return next.respond(to: request) } } diff --git a/Sources/SteamPress/Middleware/BlogRememberMeMiddleware.swift b/Sources/SteamPress/Middleware/BlogRememberMeMiddleware.swift index 8ee207d7..bcfb7d6d 100644 --- a/Sources/SteamPress/Middleware/BlogRememberMeMiddleware.swift +++ b/Sources/SteamPress/Middleware/BlogRememberMeMiddleware.swift @@ -1,19 +1,17 @@ import Vapor -public struct BlogRememberMeMiddleware: Middleware, ServiceType { +public struct BlogRememberMeMiddleware: Middleware { - public static func makeService(for container: Container) throws -> BlogRememberMeMiddleware { - return .init() - } - - public func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture { - return try next.respond(to: request).map { response in - if let rememberMe = try request.session()["SteamPressRememberMe"], rememberMe == "YES" { - if var steampressCookie = response.http.cookies["steampress-session"] { + public init() {} + + public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { + return next.respond(to: request).map { response in + if let rememberMe = request.session.data["SteamPressRememberMe"], rememberMe == "YES" { + if var steampressCookie = response.cookies["steampress-session"] { let oneYear: TimeInterval = 60 * 60 * 24 * 365 steampressCookie.expires = Date().addingTimeInterval(oneYear) - response.http.cookies["steampress-session"] = steampressCookie - try request.session()["SteamPressRememberMe"] = nil + response.cookies["steampress-session"] = steampressCookie + request.session.data["SteamPressRememberMe"] = nil } } return response diff --git a/Sources/SteamPress/Models/BlogPost.swift b/Sources/SteamPress/Models/BlogPost.swift index f6a9e39b..ca9cf3d5 100644 --- a/Sources/SteamPress/Models/BlogPost.swift +++ b/Sources/SteamPress/Models/BlogPost.swift @@ -29,6 +29,18 @@ public final class BlogPost: Codable { self.lastEdited = nil self.published = published } + + public init(blogID: Int? = nil, title: String, contents: String, authorID: Int, creationDate: Date, slugUrl: String, + published: Bool) { + self.blogID = blogID + self.title = title + self.contents = contents + self.author = authorID + self.created = creationDate + self.slugUrl = slugUrl + self.lastEdited = nil + self.published = published + } } // MARK: - BlogPost Utilities @@ -61,17 +73,15 @@ extension BlogPost { } static func generateUniqueSlugURL(from title: String, on req: Request) throws -> EventLoopFuture { - let postRepository = try req.make(BlogPostRepository.self) let alphanumericsWithHyphenAndSpace = CharacterSet(charactersIn: " -0123456789abcdefghijklmnopqrstuvwxyz") let initialSlug = title.lowercased() .trimmingCharacters(in: .whitespacesAndNewlines) .components(separatedBy: alphanumericsWithHyphenAndSpace.inverted).joined() .components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }.joined(separator: " ") .replacingOccurrences(of: " ", with: "-", options: .regularExpression) - return postRepository.getPost(slug: initialSlug, on: req).map { postWithSameSlug in + return req.blogPostRepository.getPost(slug: initialSlug).map { postWithSameSlug in if postWithSameSlug != nil { - let randomNumberGenerator = try req.make(SteamPressRandomNumberGenerator.self) - let randomNumber = randomNumberGenerator.getNumber() + let randomNumber = req.randomNumberGenerator.getNumber() return "\(initialSlug)-\(randomNumber)" } else { return initialSlug diff --git a/Sources/SteamPress/Models/BlogUser.swift b/Sources/SteamPress/Models/BlogUser.swift index 3d9cb11d..cb11c9f9 100644 --- a/Sources/SteamPress/Models/BlogUser.swift +++ b/Sources/SteamPress/Models/BlogUser.swift @@ -1,5 +1,4 @@ import Vapor -import Authentication // MARK: - Model @@ -9,17 +8,18 @@ public final class BlogUser: Codable { public var name: String public var username: String public var password: String - public var resetPasswordRequired: Bool = false + public var resetPasswordRequired: Bool public var profilePicture: String? public var twitterHandle: String? public var biography: String? public var tagline: String? - public init(userID: Int? = nil, name: String, username: String, password: String, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?) { + public init(userID: Int? = nil, name: String, username: String, password: String, resetPasswordRequired: Bool = false, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?) { self.userID = userID self.name = name self.username = username.lowercased() self.password = password + self.resetPasswordRequired = resetPasswordRequired self.profilePicture = profilePicture self.twitterHandle = twitterHandle self.biography = biography @@ -31,18 +31,18 @@ public final class BlogUser: Codable { // MARK: - Authentication extension BlogUser: Authenticatable { - func authenticateSession(on req: Request) throws { - try req.session()["_BlogUserSession"] = self.userID?.description - try req.authenticate(self) + func authenticateSession(on req: Request) { + req.session.data["_BlogUserSession"] = self.userID?.description + req.auth.login(self) } } extension Request { - func unauthenticateBlogUserSession() throws { - guard try self.hasSession() else { + func unauthenticateBlogUserSession() { + guard self.hasSession else { return } - try session()["_BlogUserSession"] = nil - try unauthenticate(BlogUser.self) + session.data["_BlogUserSession"] = nil + self.auth.logout(BlogUser.self) } } diff --git a/Sources/SteamPress/Models/Contexts/ContextViews/ViewBlogPost.swift b/Sources/SteamPress/Models/Contexts/ContextViews/ViewBlogPost.swift index 9081cb23..7c11a29c 100644 --- a/Sources/SteamPress/Models/Contexts/ContextViews/ViewBlogPost.swift +++ b/Sources/SteamPress/Models/Contexts/ContextViews/ViewBlogPost.swift @@ -94,9 +94,7 @@ extension BlogPost { } extension Array where Element: BlogPost { - func convertToViewBlogPosts(authors: [BlogUser], tagsForPosts: [Int: [BlogTag]], on container: Container) throws -> [ViewBlogPost] { - let longDateFormatter = try container.make(LongPostDateFormatter.self) - let numericDateFormatter = try container.make(NumericPostDateFormatter.self) + func convertToViewBlogPosts(authors: [BlogUser], tagsForPosts: [Int: [BlogTag]], longDateFormatter: LongPostDateFormatter, numericDateFormatter: NumericPostDateFormatter) throws -> [ViewBlogPost] { let viewPosts = try self.map { post -> ViewBlogPost in guard let blogID = post.blogID else { throw SteamPressError(identifier: "ViewBlogPost", "Post has no ID set") @@ -106,9 +104,7 @@ extension Array where Element: BlogPost { return viewPosts } - func convertToViewBlogPostsWithoutTags(authors: [BlogUser], on container: Container) throws -> [ViewBlogPostWithoutTags] { - let longDateFormatter = try container.make(LongPostDateFormatter.self) - let numericDateFormatter = try container.make(NumericPostDateFormatter.self) + func convertToViewBlogPostsWithoutTags(authors: [BlogUser], longDateFormatter: LongPostDateFormatter, numericDateFormatter: NumericPostDateFormatter) throws -> [ViewBlogPostWithoutTags] { let viewPosts = try self.map { post -> ViewBlogPostWithoutTags in return try post.toViewPostWithoutTags(authorName: authors.getAuthorName(id: post.author), authorUsername: authors.getAuthorUsername(id: post.author), longFormatter: longDateFormatter, numericFormatter: numericDateFormatter) } diff --git a/Sources/SteamPress/Models/FormData/CreateUserData.swift b/Sources/SteamPress/Models/FormData/CreateUserData.swift index e8705ac0..86a6084c 100644 --- a/Sources/SteamPress/Models/FormData/CreateUserData.swift +++ b/Sources/SteamPress/Models/FormData/CreateUserData.swift @@ -12,12 +12,11 @@ struct CreateUserData: Content { let resetPasswordOnLogin: Bool? } -extension CreateUserData: Validatable, Reflectable { - static func validations() throws -> Validations { - var validations = Validations(CreateUserData.self) +extension CreateUserData: Validatable { + + static func validations(_ validations: inout Validations) { let usernameCharacterSet = CharacterSet(charactersIn: "-_") let usernameValidationCharacters = Validator.characterSet(.alphanumerics + usernameCharacterSet) - try validations.add(\.username, usernameValidationCharacters || .nil) - return validations + validations.add("username", as: String.self, is: usernameValidationCharacters) } } diff --git a/Sources/SteamPress/Presenters/Application+SteamPress+BlogAdminPresenter.swift b/Sources/SteamPress/Presenters/Application+SteamPress+BlogAdminPresenter.swift new file mode 100644 index 00000000..6ed7c8c9 --- /dev/null +++ b/Sources/SteamPress/Presenters/Application+SteamPress+BlogAdminPresenter.swift @@ -0,0 +1,75 @@ +import Vapor + +extension Request { + var adminPresenter: BlogAdminPresenter { + self.application.steampress.adminPresenters.adminPresenter.for(self, pathCreator: self.application.steampress.adminPresenters.storage.pathCreator) + } +} + +extension Application.SteamPress { + struct BlogAdminPresenters { + struct Provider { + static var view: Self { + .init { + $0.steampress.adminPresenters.use { $0.steampress.adminPresenters.view } + } + } + + let run: (Application) -> () + + init(_ run: @escaping (Application) -> ()) { + self.run = run + } + } + + final class Storage { + let pathCreator: BlogPathCreator + var makePresenter: ((Application) -> BlogAdminPresenter)? + init(pathCreator: BlogPathCreator) { + self.pathCreator = pathCreator + } + } + + struct Key: StorageKey { + typealias Value = Storage + } + + let application: Application + + var view: ViewBlogAdminPresenter { + return .init(pathCreator: self.storage.pathCreator, viewRenderer: self.application.view, eventLoopGroup: self.application.eventLoopGroup, longDateFormatter: LongPostDateFormatter(), numericDateFormatter: NumericPostDateFormatter()) + } + + var adminPresenter: BlogAdminPresenter { + guard let makePresenter = self.storage.makePresenter else { + fatalError("No blog admin presenter configured. Configure with app.adminPresenters.use(...)") + } + return makePresenter(self.application) + } + + func use(_ provider: Provider) { + provider.run(self.application) + } + + func use(_ makePresenter: @escaping (Application) -> (BlogAdminPresenter)) { + self.storage.makePresenter = makePresenter + } + + func initialize(pathCreator: BlogPathCreator) { + self.application.storage[Key.self] = .init(pathCreator: pathCreator) + self.use(.view) + } + + var storage: Storage { + if self.application.storage[Key.self] == nil { + let pathCreator = BlogPathCreator(blogPath: self.application.steampress.configuration.blogPath) + initialize(pathCreator: pathCreator) + } + return self.application.storage[Key.self]! + } + } + + var adminPresenters: BlogAdminPresenters { + .init(application: self.application) + } +} diff --git a/Sources/SteamPress/Presenters/Application+SteamPress+BlogPresenter.swift b/Sources/SteamPress/Presenters/Application+SteamPress+BlogPresenter.swift new file mode 100644 index 00000000..b6ade71b --- /dev/null +++ b/Sources/SteamPress/Presenters/Application+SteamPress+BlogPresenter.swift @@ -0,0 +1,71 @@ +import Vapor + +extension Request { + var blogPresenter: BlogPresenter { + self.application.steampress.blogPresenters.blogPresenter.for(self) + } +} + +extension Application.SteamPress { + struct BlogPresenters { + struct Provider { + static var view: Self { + .init { + $0.steampress.blogPresenters.use { $0.steampress.blogPresenters.view } + } + } + + let run: (Application) -> () + + public init(_ run: @escaping (Application) -> ()) { + self.run = run + } + } + + final class Storage { + var makePresenter: ((Application) -> BlogPresenter)? + init() { } + } + + struct Key: StorageKey { + typealias Value = Storage + } + + let application: Application + + var view: ViewBlogPresenter { + return .init(viewRenderer: self.application.view, longDateFormatter: LongPostDateFormatter(), numericDateFormatter: NumericPostDateFormatter(), eventLoopGroup: self.application.eventLoopGroup) + } + + var blogPresenter: BlogPresenter { + guard let makePresenter = self.storage.makePresenter else { + fatalError("No blog presenter configured. Configure with app.blogPresenters.use(...)") + } + return makePresenter(self.application) + } + + func use(_ provider: Provider) { + provider.run(self.application) + } + + func use(_ makePresenter: @escaping (Application) -> (BlogPresenter)) { + self.storage.makePresenter = makePresenter + } + + func initialize() { + self.application.storage[Key.self] = .init() + self.use(.view) + } + + private var storage: Storage { + if self.application.storage[Key.self] == nil { + self.initialize() + } + return self.application.storage[Key.self]! + } + } + + var blogPresenters: BlogPresenters { + .init(application: self.application) + } +} diff --git a/Sources/SteamPress/Presenters/BlogAdminPresenter.swift b/Sources/SteamPress/Presenters/BlogAdminPresenter.swift index 47a1792d..40edbc5f 100644 --- a/Sources/SteamPress/Presenters/BlogAdminPresenter.swift +++ b/Sources/SteamPress/Presenters/BlogAdminPresenter.swift @@ -1,8 +1,15 @@ import Vapor -public protocol BlogAdminPresenter: Service { - func createIndexView(on container: Container, posts: [BlogPost], users: [BlogUser], errors: [String]?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture - func createPostView(on container: Container, errors: [String]?, title: String?, contents: String?, slugURL: String?, tags: [String]?, isEditing: Bool, post: BlogPost?, isDraft: Bool?, titleError: Bool, contentsError: Bool, pageInformation: BlogAdminPageInformation) -> EventLoopFuture - func createUserView(on container: Container, editing: Bool, errors: [String]?, name: String?, nameError: Bool, username: String?, usernameErorr: Bool, passwordError: Bool, confirmPasswordError: Bool, resetPasswordOnLogin: Bool, userID: Int?, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture - func createResetPasswordView(on container: Container, errors: [String]?, passwordError: Bool?, confirmPasswordError: Bool?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture +protocol BlogAdminPresenter { + func `for`(_ request: Request, pathCreator: BlogPathCreator) -> BlogAdminPresenter + func createIndexView(posts: [BlogPost], users: [BlogUser], errors: [String]?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture + func createPostView(errors: [String]?, title: String?, contents: String?, slugURL: String?, tags: [String]?, isEditing: Bool, post: BlogPost?, isDraft: Bool?, titleError: Bool, contentsError: Bool, pageInformation: BlogAdminPageInformation) -> EventLoopFuture + func createUserView(editing: Bool, errors: [String]?, name: String?, nameError: Bool, username: String?, usernameErorr: Bool, passwordError: Bool, confirmPasswordError: Bool, resetPasswordOnLogin: Bool, userID: Int?, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture + func createResetPasswordView(errors: [String]?, passwordError: Bool?, confirmPasswordError: Bool?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture +} + +extension ViewBlogAdminPresenter { + func `for`(_ request: Request, pathCreator: BlogPathCreator) -> BlogAdminPresenter { + return ViewBlogAdminPresenter(pathCreator: pathCreator, viewRenderer: request.view, eventLoopGroup: request.eventLoop, longDateFormatter: LongPostDateFormatter(), numericDateFormatter: NumericPostDateFormatter()) + } } diff --git a/Sources/SteamPress/Presenters/BlogPresenter.swift b/Sources/SteamPress/Presenters/BlogPresenter.swift index b9d48058..9f3b6a75 100644 --- a/Sources/SteamPress/Presenters/BlogPresenter.swift +++ b/Sources/SteamPress/Presenters/BlogPresenter.swift @@ -1,12 +1,19 @@ import Vapor -public protocol BlogPresenter: Service { - func indexView(on container: Container, posts: [BlogPost], tags: [BlogTag], authors: [BlogUser], tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture - func postView(on container: Container, post: BlogPost, author: BlogUser, tags: [BlogTag], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture - func allAuthorsView(on container: Container, authors: [BlogUser], authorPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture - func authorView(on container: Container, author: BlogUser, posts: [BlogPost], postCount: Int, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture - func allTagsView(on container: Container, tags: [BlogTag], tagPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture - func tagView(on container: Container, tag: BlogTag, posts: [BlogPost], authors: [BlogUser], totalPosts: Int, pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture - func searchView(on container: Container, totalResults: Int, posts: [BlogPost], authors: [BlogUser], searchTerm: String?, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture - func loginView(on container: Container, loginWarning: Bool, errors: [String]?, username: String?, usernameError: Bool, passwordError: Bool, rememberMe: Bool, pageInformation: BlogGlobalPageInformation) -> EventLoopFuture +protocol BlogPresenter { + func `for`(_ request: Request) -> BlogPresenter + func indexView(posts: [BlogPost], tags: [BlogTag], authors: [BlogUser], tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture + func postView(post: BlogPost, author: BlogUser, tags: [BlogTag], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture + func allAuthorsView(authors: [BlogUser], authorPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture + func authorView(author: BlogUser, posts: [BlogPost], postCount: Int, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture + func allTagsView(tags: [BlogTag], tagPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture + func tagView(tag: BlogTag, posts: [BlogPost], authors: [BlogUser], totalPosts: Int, pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture + func searchView(totalResults: Int, posts: [BlogPost], authors: [BlogUser], searchTerm: String?, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture + func loginView(loginWarning: Bool, errors: [String]?, username: String?, usernameError: Bool, passwordError: Bool, rememberMe: Bool, pageInformation: BlogGlobalPageInformation) -> EventLoopFuture +} + +extension ViewBlogPresenter { + func `for`(_ request: Request) -> BlogPresenter { + return ViewBlogPresenter(viewRenderer: request.view, longDateFormatter: LongPostDateFormatter(), numericDateFormatter: NumericPostDateFormatter(), eventLoopGroup: request.eventLoop) + } } diff --git a/Sources/SteamPress/Presenters/ViewBlogAdminPresenter.swift b/Sources/SteamPress/Presenters/ViewBlogAdminPresenter.swift index 1853c899..300ba2e5 100644 --- a/Sources/SteamPress/Presenters/ViewBlogAdminPresenter.swift +++ b/Sources/SteamPress/Presenters/ViewBlogAdminPresenter.swift @@ -1,63 +1,51 @@ import Vapor public struct ViewBlogAdminPresenter: BlogAdminPresenter { - + let pathCreator: BlogPathCreator - - public func createIndexView(on container: Container, posts: [BlogPost], users: [BlogUser], errors: [String]?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + let viewRenderer: ViewRenderer + let eventLoopGroup: EventLoopGroup + let longDateFormatter: LongPostDateFormatter + let numericDateFormatter: NumericPostDateFormatter + + public func createIndexView(posts: [BlogPost], users: [BlogUser], errors: [String]?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) - let publishedPosts = try posts.filter { $0.published }.convertToViewBlogPostsWithoutTags(authors: users, on: container) - let draftPosts = try posts.filter { !$0.published }.convertToViewBlogPostsWithoutTags(authors: users, on: container) + let publishedPosts = try posts.filter { $0.published }.convertToViewBlogPostsWithoutTags(authors: users, longDateFormatter: longDateFormatter, numericDateFormatter: numericDateFormatter) + let draftPosts = try posts.filter { !$0.published }.convertToViewBlogPostsWithoutTags(authors: users, longDateFormatter: longDateFormatter, numericDateFormatter: numericDateFormatter) let context = AdminPageContext(errors: errors, publishedPosts: publishedPosts, draftPosts: draftPosts, users: users, pageInformation: pageInformation) return viewRenderer.render("blog/admin/index", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - - public func createPostView(on container: Container, errors: [String]?, title: String?, contents: String?, slugURL: String?, tags: [String]?, isEditing: Bool, post: BlogPost?, isDraft: Bool?, titleError: Bool, contentsError: Bool, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { - do { - if isEditing { - guard post != nil else { - return container.future(error: SteamPressError(identifier: "ViewBlogAdminPresenter", "Blog Post is empty whilst editing")) - } + + public func createPostView(errors: [String]?, title: String?, contents: String?, slugURL: String?, tags: [String]?, isEditing: Bool, post: BlogPost?, isDraft: Bool?, titleError: Bool, contentsError: Bool, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + if isEditing { + guard post != nil else { + return eventLoopGroup.future(error: SteamPressError(identifier: "ViewBlogAdminPresenter", "Blog Post is empty whilst editing")) } - let viewRenderer = try container.make(ViewRenderer.self) - let postPathSuffix = pathCreator.createPath(for: "posts") - let postPathPrefix = pageInformation.websiteURL.appendingPathComponent(postPathSuffix) - let pageTitle = isEditing ? "Edit Blog Post" : "Create Blog Post" - let context = CreatePostPageContext(title: pageTitle, editing: isEditing, post: post, draft: isDraft ?? false, errors: errors, titleSupplied: title, contentsSupplied: contents, tagsSupplied: tags, slugURLSupplied: slugURL, titleError: titleError, contentsError: contentsError, postPathPrefix: postPathPrefix.absoluteString, pageInformation: pageInformation) - return viewRenderer.render("blog/admin/createPost", context) - } catch { - return container.future(error: error) } + let postPathSuffix = pathCreator.createPath(for: "posts") + let postPathPrefix = pageInformation.websiteURL.appendingPathComponent(postPathSuffix) + let pageTitle = isEditing ? "Edit Blog Post" : "Create Blog Post" + let context = CreatePostPageContext(title: pageTitle, editing: isEditing, post: post, draft: isDraft ?? false, errors: errors, titleSupplied: title, contentsSupplied: contents, tagsSupplied: tags, slugURLSupplied: slugURL, titleError: titleError, contentsError: contentsError, postPathPrefix: postPathPrefix.absoluteString, pageInformation: pageInformation) + return viewRenderer.render("blog/admin/createPost", context) } - - public func createUserView(on container: Container, editing: Bool, errors: [String]?, name: String?, nameError: Bool, username: String?, usernameErorr: Bool, passwordError: Bool, confirmPasswordError: Bool, resetPasswordOnLogin: Bool, userID: Int?, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { - do { - if editing { - guard userID != nil else { - return container.future(error: SteamPressError(identifier: "ViewBlogAdminPresenter", "User ID is nil whilst editing")) - } + + public func createUserView(editing: Bool, errors: [String]?, name: String?, nameError: Bool, username: String?, usernameErorr: Bool, passwordError: Bool, confirmPasswordError: Bool, resetPasswordOnLogin: Bool, userID: Int?, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + if editing { + guard userID != nil else { + return eventLoopGroup.future(error: SteamPressError(identifier: "ViewBlogAdminPresenter", "User ID is nil whilst editing")) } - - let viewRenderer = try container.make(ViewRenderer.self) - let context = CreateUserPageContext(editing: editing, errors: errors, nameSupplied: name, nameError: nameError, usernameSupplied: username, usernameError: usernameErorr, passwordError: passwordError, confirmPasswordError: confirmPasswordError, resetPasswordOnLoginSupplied: resetPasswordOnLogin, userID: userID, twitterHandleSupplied: twitterHandle, profilePictureSupplied: profilePicture, biographySupplied: biography, taglineSupplied: tagline, pageInformation: pageInformation) - return viewRenderer.render("blog/admin/createUser", context) - } catch { - return container.future(error: error) } + + let context = CreateUserPageContext(editing: editing, errors: errors, nameSupplied: name, nameError: nameError, usernameSupplied: username, usernameError: usernameErorr, passwordError: passwordError, confirmPasswordError: confirmPasswordError, resetPasswordOnLoginSupplied: resetPasswordOnLogin, userID: userID, twitterHandleSupplied: twitterHandle, profilePictureSupplied: profilePicture, biographySupplied: biography, taglineSupplied: tagline, pageInformation: pageInformation) + return viewRenderer.render("blog/admin/createUser", context) } - - public func createResetPasswordView(on container: Container, errors: [String]?, passwordError: Bool?, confirmPasswordError: Bool?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { - do { - let viewRenderer = try container.make(ViewRenderer.self) - let context = ResetPasswordPageContext(errors: errors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: pageInformation) - return viewRenderer.render("blog/admin/resetPassword", context) - } catch { - return container.future(error: error) - } + + public func createResetPasswordView(errors: [String]?, passwordError: Bool?, confirmPasswordError: Bool?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + let context = ResetPasswordPageContext(errors: errors, passwordError: passwordError, confirmPasswordError: confirmPasswordError, pageInformation: pageInformation) + return viewRenderer.render("blog/admin/resetPassword", context) } - + } diff --git a/Sources/SteamPress/Presenters/ViewBlogPresenter.swift b/Sources/SteamPress/Presenters/ViewBlogPresenter.swift index 033b757a..fef8a8c7 100644 --- a/Sources/SteamPress/Presenters/ViewBlogPresenter.swift +++ b/Sources/SteamPress/Presenters/ViewBlogPresenter.swift @@ -3,23 +3,25 @@ import SwiftSoup import SwiftMarkdown public struct ViewBlogPresenter: BlogPresenter { + + let viewRenderer: ViewRenderer + let longDateFormatter: LongPostDateFormatter + let numericDateFormatter: NumericPostDateFormatter + let eventLoopGroup: EventLoopGroup - public func indexView(on container: Container, posts: [BlogPost], tags: [BlogTag], authors: [BlogUser], tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + public func indexView(posts: [BlogPost], tags: [BlogTag], authors: [BlogUser], tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) - let viewPosts = try posts.convertToViewBlogPosts(authors: authors, tagsForPosts: tagsForPosts, on: container) + let viewPosts = try posts.convertToViewBlogPosts(authors: authors, tagsForPosts: tagsForPosts, longDateFormatter: longDateFormatter, numericDateFormatter: numericDateFormatter) let viewTags = try tags.map { try $0.toViewBlogTag() } let context = BlogIndexPageContext(posts: viewPosts, tags: viewTags, authors: authors, pageInformation: pageInformation, paginationTagInformation: paginationTagInfo) return viewRenderer.render("blog/blog", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - public func postView(on container: Container, post: BlogPost, author: BlogUser, tags: [BlogTag], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + public func postView(post: BlogPost, author: BlogUser, tags: [BlogTag], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) - var postImage: String? var postImageAlt: String? if let image = try SwiftSoup.parse(markdownToHTML(post.contents)).select("img").first() { @@ -30,21 +32,18 @@ public struct ViewBlogPresenter: BlogPresenter { } } let shortSnippet = post.shortSnippet() - let longFormatter = try container.make(LongPostDateFormatter.self) - let numericFormatter = try container.make(NumericPostDateFormatter.self) - let viewPost = try post.toViewPost(authorName: author.name, authorUsername: author.username, longFormatter: longFormatter, numericFormatter: numericFormatter, tags: tags) + let viewPost = try post.toViewPost(authorName: author.name, authorUsername: author.username, longFormatter: longDateFormatter, numericFormatter: numericDateFormatter, tags: tags) let context = BlogPostPageContext(title: post.title, post: viewPost, author: author, pageInformation: pageInformation, postImage: postImage, postImageAlt: postImageAlt, shortSnippet: shortSnippet) return viewRenderer.render("blog/post", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - public func allAuthorsView(on container: Container, authors: [BlogUser], authorPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + public func allAuthorsView(authors: [BlogUser], authorPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) var viewAuthors = try authors.map { user -> ViewBlogAuthor in guard let userID = user.userID else { throw SteamPressError(identifier: "ViewBlogPresenter", "User ID Was Not Set") @@ -56,30 +55,28 @@ public struct ViewBlogPresenter: BlogPresenter { let context = AllAuthorsPageContext(pageInformation: pageInformation, authors: viewAuthors) return viewRenderer.render("blog/authors", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - public func authorView(on container: Container, author: BlogUser, posts: [BlogPost], postCount: Int, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + public func authorView(author: BlogUser, posts: [BlogPost], postCount: Int, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) let myProfile: Bool if let loggedInUser = pageInformation.loggedInUser { myProfile = loggedInUser.userID == author.userID } else { myProfile = false } - let viewPosts = try posts.convertToViewBlogPosts(authors: [author], tagsForPosts: tagsForPosts, on: container) + let viewPosts = try posts.convertToViewBlogPosts(authors: [author], tagsForPosts: tagsForPosts, longDateFormatter: longDateFormatter, numericDateFormatter: numericDateFormatter) let context = AuthorPageContext(author: author, posts: viewPosts, pageInformation: pageInformation, myProfile: myProfile, postCount: postCount, paginationTagInformation: paginationTagInfo) return viewRenderer.render("blog/profile", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - public func allTagsView(on container: Container, tags: [BlogTag], tagPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + public func allTagsView(tags: [BlogTag], tagPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) var viewTags = try tags.map { tag -> BlogTagWithPostCount in guard let tagID = tag.tagID else { throw SteamPressError(identifier: "ViewBlogPresenter", "Tag ID Was Not Set") @@ -93,13 +90,12 @@ public struct ViewBlogPresenter: BlogPresenter { let context = AllTagsPageContext(title: "All Tags", tags: viewTags, pageInformation: pageInformation) return viewRenderer.render("blog/tags", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - public func tagView(on container: Container, tag: BlogTag, posts: [BlogPost], authors: [BlogUser], totalPosts: Int, pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + public func tagView(tag: BlogTag, posts: [BlogPost], authors: [BlogUser], totalPosts: Int, pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) let tagsForPosts = try posts.reduce(into: [Int: [BlogTag]]()) { dict, blog in guard let blogID = blog.blogID else { throw SteamPressError(identifier: "ViewBlogPresenter", "Blog has no ID set") @@ -107,33 +103,27 @@ public struct ViewBlogPresenter: BlogPresenter { dict[blogID] = [tag] } - let viewPosts = try posts.convertToViewBlogPosts(authors: authors, tagsForPosts: tagsForPosts, on: container) + let viewPosts = try posts.convertToViewBlogPosts(authors: authors, tagsForPosts: tagsForPosts, longDateFormatter: longDateFormatter, numericDateFormatter: numericDateFormatter) let context = TagPageContext(tag: tag, pageInformation: pageInformation, posts: viewPosts, postCount: totalPosts, paginationTagInformation: paginationTagInfo) return viewRenderer.render("blog/tag", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - public func searchView(on container: Container, totalResults: Int, posts: [BlogPost], authors: [BlogUser], searchTerm: String?, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + public func searchView(totalResults: Int, posts: [BlogPost], authors: [BlogUser], searchTerm: String?, tagsForPosts: [Int: [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { do { - let viewRenderer = try container.make(ViewRenderer.self) - let viewPosts = try posts.convertToViewBlogPosts(authors: authors, tagsForPosts: tagsForPosts, on: container) + let viewPosts = try posts.convertToViewBlogPosts(authors: authors, tagsForPosts: tagsForPosts, longDateFormatter: longDateFormatter, numericDateFormatter: numericDateFormatter) let context = SearchPageContext(searchTerm: searchTerm, posts: viewPosts, totalResults: totalResults, pageInformation: pageInformation, paginationTagInformation: paginationTagInfo) return viewRenderer.render("blog/search", context) } catch { - return container.future(error: error) + return eventLoopGroup.future(error: error) } } - public func loginView(on container: Container, loginWarning: Bool, errors: [String]?, username: String?, usernameError: Bool, passwordError: Bool, rememberMe: Bool, pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { - do { - let viewRenderer = try container.make(ViewRenderer.self) - let context = LoginPageContext(errors: errors, loginWarning: loginWarning, username: username, usernameError: usernameError, passwordError: passwordError, rememberMe: rememberMe, pageInformation: pageInformation) - return viewRenderer.render("blog/admin/login", context) - } catch { - return container.future(error: error) - } + public func loginView(loginWarning: Bool, errors: [String]?, username: String?, usernameError: Bool, passwordError: Bool, rememberMe: Bool, pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + let context = LoginPageContext(errors: errors, loginWarning: loginWarning, username: username, usernameError: usernameError, passwordError: passwordError, rememberMe: rememberMe, pageInformation: pageInformation) + return viewRenderer.render("blog/admin/login", context) } } diff --git a/Sources/SteamPress/Provider.swift b/Sources/SteamPress/Provider.swift deleted file mode 100644 index 0fd9c9e8..00000000 --- a/Sources/SteamPress/Provider.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Vapor -import Authentication - -public struct Provider: Vapor.Provider { - - let blogPath: String? - let feedInformation: FeedInformation - let postsPerPage: Int - let enableAuthorPages: Bool - let enableTagPages: Bool - let pathCreator: BlogPathCreator - - /** - Initialiser for SteamPress' Provider to add a blog to your Vapor App. You can pass it an optional - `blogPath` to add the blog to. For instance, if you pass in "blog", your blog will be accessible - at http://mysite.com/blog/, or if you pass in `nil` your blog will be added to the root of your - site (i.e. http://mysite.com/) - - Parameter blogPath: The path to add the blog to (see above). - - Parameter feedInformation: Information to vend to the RSS and Atom feeds. Defaults to empty information. - - Parameter postsPerPage: The number of posts to show per page on the main index page of the blog. Defaults to 10. - - Parameter enableAuthorsPages: Flag used to determine whether to publicly expose the authors endpoints - or not. Defaults to true. - - Parameter enableTagsPages: Flag used to determine whether to publicy expose the tags endpoints or not. - Defaults to true. - */ - public init( - blogPath: String? = nil, - feedInformation: FeedInformation = FeedInformation(), - postsPerPage: Int = 10, - enableAuthorPages: Bool = true, - enableTagPages: Bool = true) { - self.blogPath = blogPath - self.feedInformation = feedInformation - self.postsPerPage = postsPerPage - self.enableAuthorPages = enableAuthorPages - self.enableTagPages = enableTagPages - self.pathCreator = BlogPathCreator(blogPath: self.blogPath) - } - - public func register(_ services: inout Services) throws { - services.register(BlogPresenter.self) { _ in - return ViewBlogPresenter() - } - - services.register(BlogAdminPresenter.self) { _ in - return ViewBlogAdminPresenter(pathCreator: self.pathCreator) - } - - try services.register(AuthenticationProvider()) - services.register([PasswordHasher.self, PasswordVerifier.self]) { _ in - return BCryptDigest() - } - services.register(SteamPressRandomNumberGenerator.self) { _ in - return RealRandomNumberGenerator() - } - - services.register(BlogRememberMeMiddleware.self) - services.register(LongPostDateFormatter.self) - services.register(NumericPostDateFormatter.self) - } - - public func willBoot(_ container: Container) throws -> EventLoopFuture { - let router = try container.make(Router.self) - - let feedController = FeedController(pathCreator: self.pathCreator, feedInformation: self.feedInformation) - let apiController = APIController() - let blogController = BlogController(pathCreator: self.pathCreator, enableAuthorPages: self.enableAuthorPages, enableTagPages: self.enableTagPages, postsPerPage: self.postsPerPage) - let blogAdminController = BlogAdminController(pathCreator: self.pathCreator) - - let blogRoutes: Router - if let blogPath = blogPath { - blogRoutes = router.grouped(blogPath) - } else { - blogRoutes = router.grouped("") - } - let steampressSessionsConfig = SessionsConfig(cookieName: "steampress-session") { value in - return HTTPCookieValue(string: value) - } - let steampressSessions = try SessionsMiddleware(sessions: container.make(), config: steampressSessionsConfig) - let steampressAuthSessions = BlogAuthSessionsMiddleware() - let sessionedRoutes = blogRoutes.grouped(steampressSessions, steampressAuthSessions) - - try sessionedRoutes.register(collection: feedController) - try sessionedRoutes.register(collection: apiController) - try sessionedRoutes.register(collection: blogController) - try sessionedRoutes.register(collection: blogAdminController) - return .done(on: container) - } - - public func didBoot(_ container: Container) throws -> EventLoopFuture { - return .done(on: container) - } - -} diff --git a/Sources/SteamPress/Repositories/Application+SteamPress+Repositories.swift b/Sources/SteamPress/Repositories/Application+SteamPress+Repositories.swift new file mode 100644 index 00000000..907b4260 --- /dev/null +++ b/Sources/SteamPress/Repositories/Application+SteamPress+Repositories.swift @@ -0,0 +1,84 @@ +import Vapor + +public extension Application.SteamPress { + struct BlogRepositories { + public struct Provider { + let run: (Application) -> () + + public init(_ run: @escaping (Application) -> ()) { + self.run = run + } + } + + final class Storage { + var makePostRepository: ((Application) -> BlogPostRepository)? + var makeTagRepository: ((Application) -> BlogTagRepository)? + var makeUserRepository: ((Application) -> BlogUserRepository)? + init() { } + } + + struct Key: StorageKey { + typealias Value = Storage + } + + let application: Application + + public var userRepository: BlogUserRepository { + guard let makeRepository = self.storage.makeUserRepository else { + fatalError("No user repository configured. Configure with app.blogRepositories.use(...)") + } + return makeRepository(self.application) + } + + public var postRepository: BlogPostRepository { + guard let makeRepository = self.storage.makePostRepository else { + fatalError("No post repository configured. Configure with app.blogRepositories.use(...)") + } + return makeRepository(self.application) + } + + public var tagRepository: BlogTagRepository { + guard let makeRepository = self.storage.makeTagRepository else { + fatalError("No tag repository configured. Configure with app.blogRepositories.use(...)") + } + return makeRepository(self.application) + } + + public func use(_ provider: Provider) { + provider.run(self.application) + } + + public func use(_ makeRespository: @escaping (Application) -> (BlogUserRepository & BlogTagRepository & BlogPostRepository)) { + self.storage.makeUserRepository = makeRespository + self.storage.makeTagRepository = makeRespository + self.storage.makePostRepository = makeRespository + } + + public func use(_ makeRepository: @escaping (Application) -> BlogUserRepository) { + self.storage.makeUserRepository = makeRepository + } + + public func use(_ makeRepository: @escaping (Application) -> BlogTagRepository) { + self.storage.makeTagRepository = makeRepository + } + + public func use(_ makeRepository: @escaping (Application) -> BlogPostRepository) { + self.storage.makePostRepository = makeRepository + } + + public func initialize() { + self.application.storage[Key.self] = .init() + } + + private var storage: Storage { + if self.application.storage[Key.self] == nil { + self.initialize() + } + return self.application.storage[Key.self]! + } + } + + var blogRepositories: BlogRepositories { + .init(application: self.application) + } +} diff --git a/Sources/SteamPress/Repositories/Request+Repositories.swift b/Sources/SteamPress/Repositories/Request+Repositories.swift new file mode 100644 index 00000000..b92feae0 --- /dev/null +++ b/Sources/SteamPress/Repositories/Request+Repositories.swift @@ -0,0 +1,15 @@ +import Vapor + +public extension Request { + var blogUserRepository: BlogUserRepository { + self.application.steampress.blogRepositories.userRepository.for(self) + } + + var blogPostRepository: BlogPostRepository { + self.application.steampress.blogRepositories.postRepository.for(self) + } + + var blogTagRepository: BlogTagRepository { + self.application.steampress.blogRepositories.tagRepository.for(self) + } +} diff --git a/Sources/SteamPress/Repositories/SteamPressRepository.swift b/Sources/SteamPress/Repositories/SteamPressRepository.swift index fd398a51..436c1262 100644 --- a/Sources/SteamPress/Repositories/SteamPressRepository.swift +++ b/Sources/SteamPress/Repositories/SteamPressRepository.swift @@ -1,71 +1,49 @@ import Vapor -public protocol BlogTagRepository { - func getAllTags(on container: Container) -> EventLoopFuture<[BlogTag]> - func getAllTagsWithPostCount(on container: Container) -> EventLoopFuture<[(BlogTag, Int)]> - func getTags(for post: BlogPost, on container: Container) -> EventLoopFuture<[BlogTag]> - func getTagsForAllPosts(on container: Container) -> EventLoopFuture<[Int: [BlogTag]]> - func getTag(_ name: String, on container: Container) -> EventLoopFuture - func save(_ tag: BlogTag, on container: Container) -> EventLoopFuture - // Delete all the pivots between a post and collection of tags - you should probably delete the - // tags that have no posts associated with a tag - func deleteTags(for post: BlogPost, on container: Container) -> EventLoopFuture - func remove(_ tag: BlogTag, from post: BlogPost, on container: Container) -> EventLoopFuture - func add(_ tag: BlogTag, to post: BlogPost, on container: Container) -> EventLoopFuture -} - -public protocol BlogPostRepository { - func getAllPostsSortedByPublishDate(includeDrafts: Bool, on container: Container) -> EventLoopFuture<[BlogPost]> - func getAllPostsCount(includeDrafts: Bool, on container: Container) -> EventLoopFuture - func getAllPostsSortedByPublishDate(includeDrafts: Bool, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> - func getAllPostsSortedByPublishDate(for user: BlogUser, includeDrafts: Bool, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> - func getPostCount(for user: BlogUser, on container: Container) -> EventLoopFuture - func getPost(slug: String, on container: Container) -> EventLoopFuture - func getPost(id: Int, on container: Container) -> EventLoopFuture - func getSortedPublishedPosts(for tag: BlogTag, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> - func getPublishedPostCount(for tag: BlogTag, on container: Container) -> EventLoopFuture - func findPublishedPostsOrdered(for searchTerm: String, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> - func getPublishedPostCount(for searchTerm: String, on container: Container) -> EventLoopFuture - func save(_ post: BlogPost, on container: Container) -> EventLoopFuture - func delete(_ post: BlogPost, on container: Container) -> EventLoopFuture +public protocol SteamPressRepository { + // associatedtype ModelType + // func get(_ id: Int, on eventLoop: EventLoop) -> EventLoopFuture } -public protocol BlogUserRepository { - func getAllUsers(on container: Container) -> EventLoopFuture<[BlogUser]> - func getAllUsersWithPostCount(on container: Container) -> EventLoopFuture<[(BlogUser, Int)]> - func getUser(id: Int, on container: Container) -> EventLoopFuture - func getUser(username: String, on container: Container) -> EventLoopFuture - func save(_ user: BlogUser, on container: Container) -> EventLoopFuture - func delete(_ user: BlogUser, on container: Container) -> EventLoopFuture - func getUsersCount(on container: Container) -> EventLoopFuture -} - -extension BlogUser: Parameter { - public typealias ResolvedParameter = EventLoopFuture - public static func resolveParameter(_ parameter: String, on container: Container) throws -> BlogUser.ResolvedParameter { - let userRepository = try container.make(BlogUserRepository.self) - guard let userID = Int(parameter) else { - throw SteamPressError(identifier: "Invalid-ID-Type", "Unable to convert \(parameter) to a User ID") - } - return userRepository.getUser(id: userID, on: container).unwrap(or: Abort(.notFound)) - } +public protocol BlogTagRepository: SteamPressRepository { + func `for`(_ request: Request) -> BlogTagRepository + func getAllTags() -> EventLoopFuture<[BlogTag]> + func getAllTagsWithPostCount() -> EventLoopFuture<[(BlogTag, Int)]> + func getTags(for post: BlogPost) -> EventLoopFuture<[BlogTag]> + func getTagsForAllPosts() -> EventLoopFuture<[Int: [BlogTag]]> + func getTag(_ name: String) -> EventLoopFuture + func save(_ tag: BlogTag) -> EventLoopFuture + // Delete all the pivots between a post and collection of tags - you should probably delete the + // tags that have no posts associated with a tag + func deleteTags(for post: BlogPost) -> EventLoopFuture + func remove(_ tag: BlogTag, from post: BlogPost) -> EventLoopFuture + func add(_ tag: BlogTag, to post: BlogPost) -> EventLoopFuture } -extension BlogPost: Parameter { - public typealias ResolvedParameter = EventLoopFuture - public static func resolveParameter(_ parameter: String, on container: Container) throws -> EventLoopFuture { - let postRepository = try container.make(BlogPostRepository.self) - guard let postID = Int(parameter) else { - throw SteamPressError(identifier: "Invalid-ID-Type", "Unable to convert \(parameter) to a Post ID") - } - return postRepository.getPost(id: postID, on: container).unwrap(or: Abort(.notFound)) - } +public protocol BlogPostRepository: SteamPressRepository { + func `for`(_ request: Request) -> BlogPostRepository + func getAllPostsSortedByPublishDate(includeDrafts: Bool) -> EventLoopFuture<[BlogPost]> + func getAllPostsCount(includeDrafts: Bool) -> EventLoopFuture + func getAllPostsSortedByPublishDate(includeDrafts: Bool, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> + func getAllPostsSortedByPublishDate(for user: BlogUser, includeDrafts: Bool, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> + func getPostCount(for user: BlogUser) -> EventLoopFuture + func getPost(slug: String) -> EventLoopFuture + func getPost(id: Int) -> EventLoopFuture + func getSortedPublishedPosts(for tag: BlogTag, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> + func getPublishedPostCount(for tag: BlogTag) -> EventLoopFuture + func findPublishedPostsOrdered(for searchTerm: String, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> + func getPublishedPostCount(for searchTerm: String) -> EventLoopFuture + func save(_ post: BlogPost) -> EventLoopFuture + func delete(_ post: BlogPost) -> EventLoopFuture } -extension BlogTag: Parameter { - public typealias ResolvedParameter = EventLoopFuture - public static func resolveParameter(_ parameter: String, on container: Container) throws -> EventLoopFuture { - let tagRepository = try container.make(BlogTagRepository.self) - return tagRepository.getTag(parameter, on: container).unwrap(or: Abort(.notFound)) - } +public protocol BlogUserRepository: SteamPressRepository { + func `for`(_ request: Request) -> BlogUserRepository + func getAllUsers() -> EventLoopFuture<[BlogUser]> + func getAllUsersWithPostCount() -> EventLoopFuture<[(BlogUser, Int)]> + func getUser(id: Int) -> EventLoopFuture + func getUser(username: String) -> EventLoopFuture + func save(_ user: BlogUser) -> EventLoopFuture + func delete(_ user: BlogUser) -> EventLoopFuture + func getUsersCount() -> EventLoopFuture } diff --git a/Sources/SteamPress/Services/LongPostDateFormatter.swift b/Sources/SteamPress/Services/LongPostDateFormatter.swift index 1092462b..04e8131f 100644 --- a/Sources/SteamPress/Services/LongPostDateFormatter.swift +++ b/Sources/SteamPress/Services/LongPostDateFormatter.swift @@ -1,11 +1,7 @@ import Foundation import Vapor -struct LongPostDateFormatter: ServiceType { - static func makeService(for container: Container) throws -> LongPostDateFormatter { - return .init() - } - +struct LongPostDateFormatter { let formatter: DateFormatter init() { diff --git a/Sources/SteamPress/Services/NumericPostFormatter.swift b/Sources/SteamPress/Services/NumericPostFormatter.swift index 578a5d2b..0271dec7 100644 --- a/Sources/SteamPress/Services/NumericPostFormatter.swift +++ b/Sources/SteamPress/Services/NumericPostFormatter.swift @@ -1,10 +1,7 @@ import Foundation import Vapor -struct NumericPostDateFormatter: ServiceType { - static func makeService(for container: Container) throws -> NumericPostDateFormatter { - return .init() - } +struct NumericPostDateFormatter { let formatter: DateFormatter diff --git a/Sources/SteamPress/Services/SteamPressRandomNumberGenerator.swift b/Sources/SteamPress/Services/SteamPressRandomNumberGenerator.swift index d1875e80..ce1753db 100644 --- a/Sources/SteamPress/Services/SteamPressRandomNumberGenerator.swift +++ b/Sources/SteamPress/Services/SteamPressRandomNumberGenerator.swift @@ -1,5 +1,82 @@ import Vapor -public protocol SteamPressRandomNumberGenerator: Service { +public protocol SteamPressRandomNumberGenerator { + func `for`(_ request: Request) -> SteamPressRandomNumberGenerator func getNumber() -> Int } + +extension RealRandomNumberGenerator { + public func `for`(_ request: Request) -> SteamPressRandomNumberGenerator { + RealRandomNumberGenerator() + } +} + +public extension Request { + var randomNumberGenerator: SteamPressRandomNumberGenerator { + self.application.steampress.randomNumberGenerators.generator.for(self) + } +} + +public extension Application.SteamPress { + struct RandomNumberGenerators { + public struct Provider { + static var real: Self { + .init { + $0.steampress.randomNumberGenerators.use { $0.steampress.randomNumberGenerators.real } + } + } + + let run: (Application) -> () + + init(_ run: @escaping (Application) -> ()) { + self.run = run + } + } + + final class Storage { + var makeGenerator: ((Application) -> SteamPressRandomNumberGenerator)? + init() { } + } + + struct Key: StorageKey { + typealias Value = Storage + } + + let application: Application + + var real: RealRandomNumberGenerator { + return .init() + } + + var generator: SteamPressRandomNumberGenerator { + guard let makeGenerator = self.storage.makeGenerator else { + fatalError("No random number generator configured. Configure with app.randomNumberGenerators.use(...)") + } + return makeGenerator(self.application) + } + + public func use(_ provider: Provider) { + provider.run(self.application) + } + + public func use(_ makeGenerator: @escaping (Application) -> (SteamPressRandomNumberGenerator)) { + self.storage.makeGenerator = makeGenerator + } + + func initialize() { + self.application.storage[Key.self] = .init() + self.use(.real) + } + + private var storage: Storage { + if self.application.storage[Key.self] == nil { + self.initialize() + } + return self.application.storage[Key.self]! + } + } + + var randomNumberGenerators: RandomNumberGenerators { + .init(application: self.application) + } +} diff --git a/Sources/SteamPress/SteamPress+Application.swift b/Sources/SteamPress/SteamPress+Application.swift new file mode 100644 index 00000000..2a09e626 --- /dev/null +++ b/Sources/SteamPress/SteamPress+Application.swift @@ -0,0 +1,72 @@ +import Vapor + +extension Application { + public class SteamPress { + public let application: Application + let lifecycleHandler: SteamPressRoutesLifecycleHandler + + init(application: Application, lifecycleHandler: SteamPressRoutesLifecycleHandler) { + self.application = application + self.lifecycleHandler = lifecycleHandler + } + + final class Storage { + var configuration: SteamPressConfiguration + + init() { + configuration = SteamPressConfiguration() + } + } + + struct Key: StorageKey { + typealias Value = Storage + } + + var storage: Storage { + if self.application.storage[Key.self] == nil { + self.initialize() + } + return self.application.storage[Key.self]! + } + + func initialize() { + self.application.storage[Key.self] = .init() + self.application.lifecycle.use(lifecycleHandler) + } + + public var configuration: SteamPressConfiguration { + get { + self.storage.configuration + } + set { + self.storage.configuration = newValue + self.lifecycleHandler.configuration = newValue + } + } + } + + public var steampress: SteamPress { + .init(application: self, lifecycleHandler: SteamPressRoutesLifecycleHandler()) + } +} + +public class SteamPressConfiguration { + let blogPath: String? + let feedInformation: FeedInformation + let postsPerPage: Int + let enableAuthorPages: Bool + let enableTagPages: Bool + + public init( + blogPath: String? = nil, + feedInformation: FeedInformation = FeedInformation(), + postsPerPage: Int = 10, + enableAuthorPages: Bool = true, + enableTagPages: Bool = true) { + self.blogPath = blogPath + self.feedInformation = feedInformation + self.postsPerPage = postsPerPage + self.enableAuthorPages = enableAuthorPages + self.enableTagPages = enableTagPages + } +} diff --git a/Sources/SteamPress/SteamPressError.swift b/Sources/SteamPress/SteamPressError.swift index ff396d6d..7e1a4712 100644 --- a/Sources/SteamPress/SteamPressError.swift +++ b/Sources/SteamPress/SteamPressError.swift @@ -1,6 +1,6 @@ import Vapor -struct SteamPressError: AbortError, Debuggable { +struct SteamPressError: AbortError, DebuggableError { let identifier: String let reason: String diff --git a/Sources/SteamPress/SteamPressRoutesLifecycleHandler.swift b/Sources/SteamPress/SteamPressRoutesLifecycleHandler.swift new file mode 100644 index 00000000..7f664f9d --- /dev/null +++ b/Sources/SteamPress/SteamPressRoutesLifecycleHandler.swift @@ -0,0 +1,39 @@ +import Vapor + +public class SteamPressRoutesLifecycleHandler: LifecycleHandler { + + var configuration: SteamPressConfiguration + + public init(configuration: SteamPressConfiguration = SteamPressConfiguration()) { + self.configuration = configuration + } + + public func willBoot(_ application: Application) throws { + let router = application.routes + let pathCreator = BlogPathCreator(blogPath: self.configuration.blogPath) + + let feedController = FeedController(pathCreator: pathCreator, feedInformation: self.configuration.feedInformation) + let apiController = APIController() + let blogController = BlogController(pathCreator: pathCreator, enableAuthorPages: self.configuration.enableAuthorPages, enableTagPages: self.configuration.enableTagPages, postsPerPage: self.configuration.postsPerPage) + let blogAdminController = BlogAdminController(pathCreator: pathCreator) + + let blogRoutes: RoutesBuilder + if let blogPath = self.configuration.blogPath { + blogRoutes = router.grouped(PathComponent(stringLiteral: blogPath)) + } else { + blogRoutes = router.grouped("") + } + let steampressSessionsConfig = SessionsConfiguration(cookieName: "steampress-session") { value in + HTTPCookies.Value(string: value.string) + } + let steampressSessions = SessionsMiddleware(session: application.sessions.driver, configuration: steampressSessionsConfig) + let steampressAuthSessions = BlogAuthSessionsMiddleware() + let sessionedRoutes = blogRoutes.grouped(steampressSessions, steampressAuthSessions) + + try sessionedRoutes.register(collection: feedController) + try sessionedRoutes.register(collection: apiController) + try sessionedRoutes.register(collection: blogController) + try sessionedRoutes.register(collection: blogAdminController) + } +} + diff --git a/Sources/SteamPress/Views/PaginatorTag.swift b/Sources/SteamPress/Views/PaginatorTag.swift index a9ae6d2e..fdc575f4 100644 --- a/Sources/SteamPress/Views/PaginatorTag.swift +++ b/Sources/SteamPress/Views/PaginatorTag.swift @@ -1,6 +1,7 @@ -import TemplateKit +import LeafKit +import Foundation -public final class PaginatorTag: TagRenderer { +public final class PaginatorTag: LeafTag { public enum Error: Swift.Error { case expectedPaginationInformation } @@ -13,19 +14,18 @@ public final class PaginatorTag: TagRenderer { public static let name = "paginator" - public func render(tag: TagContext) throws -> EventLoopFuture { - try tag.requireNoBody() + public func render(_ ctx: LeafContext) throws -> LeafData { + try ctx.requireNoBody() - guard let paginationInformaton = tag.context.data.dictionary?["paginationTagInformation"] else { + guard let paginationInformation = ctx.data["paginationTagInformation"]?.dictionary else { throw Error.expectedPaginationInformation } - guard let currentPage = paginationInformaton.dictionary?["currentPage"]?.int, - let totalPages = paginationInformaton.dictionary?["totalPages"]?.int else { + guard let currentPage = paginationInformation["currentPage"]?.int, let totalPages = paginationInformation["totalPages"]?.int else { throw Error.expectedPaginationInformation } - let currentQuery = paginationInformaton.dictionary?["currentQuery"]?.string + let currentQuery = paginationInformation["currentQuery"]?.string let previousPage: String? let nextPage: String? @@ -45,7 +45,7 @@ public final class PaginatorTag: TagRenderer { } let data = buildNavigation(currentPage: currentPage, totalPages: totalPages, previousPage: previousPage, nextPage: nextPage, currentQuery: currentQuery) - return tag.eventLoop.future(data) + return data } } @@ -102,7 +102,7 @@ extension PaginatorTag { return links } - func buildNavigation(currentPage: Int, totalPages: Int, previousPage: String?, nextPage: String?, currentQuery: String?) -> TemplateData { + func buildNavigation(currentPage: Int, totalPages: Int, previousPage: String?, nextPage: String?, currentQuery: String?) -> LeafData { var result = "" @@ -126,7 +126,7 @@ extension PaginatorTag { result += footer - return TemplateData.string(result) + return LeafData.string(result) } func buildLink(title: String, active: Bool, link: String?, disabled: Bool) -> String { diff --git a/Tests/SteamPressTests/APITests/APITagControllerTests.swift b/Tests/SteamPressTests/APITests/APITagControllerTests.swift index c0376136..253c31f3 100644 --- a/Tests/SteamPressTests/APITests/APITagControllerTests.swift +++ b/Tests/SteamPressTests/APITests/APITagControllerTests.swift @@ -4,11 +4,22 @@ import SteamPress class APITagControllerTests: XCTestCase { + // MARK: - Properties + var testWorld: TestWorld! + + // MARK: - Overrides + + override func setUpWithError() throws { + testWorld = try TestWorld.create() + } + + override func tearDownWithError() throws { + try testWorld.shutdown() + } + // MARK: - Tests func testThatAllTagsAreReturnedFromAPI() throws { - var testWorld = try TestWorld.create() - let tag1 = try testWorld.context.repository.addTag(name: "Vapor3") let tag2 = try testWorld.context.repository.addTag(name: "Engineering") @@ -16,8 +27,6 @@ class APITagControllerTests: XCTestCase { XCTAssertEqual(tags[0].name, tag1.name) XCTAssertEqual(tags[1].name, tag2.name) - - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) } } diff --git a/Tests/SteamPressTests/AdminTests/AccessControlTests.swift b/Tests/SteamPressTests/AdminTests/AccessControlTests.swift index e3e58131..fb547657 100644 --- a/Tests/SteamPressTests/AdminTests/AccessControlTests.swift +++ b/Tests/SteamPressTests/AdminTests/AccessControlTests.swift @@ -11,13 +11,13 @@ class AccessControlTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create(path: "blog") + override func setUpWithError() throws { + testWorld = try TestWorld.create(path: "blog") user = testWorld.createUser() } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests @@ -82,33 +82,33 @@ class AccessControlTests: XCTestCase { func testCanAccessAdminPageWhenLoggedIn() throws { let response = try testWorld.getResponse(to: "/blog/admin/", loggedInUser: user) - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testCanAccessCreatePostPageWhenLoggedIn() throws { let response = try testWorld.getResponse(to: "/blog/admin/createPost", loggedInUser: user) - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testCanAccessEditPostPageWhenLoggedIn() throws { let post = try testWorld.createPost() let response = try testWorld.getResponse(to: "/blog/admin/posts/\(post.post.blogID!)/edit", loggedInUser: user) - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testCanAccessCreateUserPageWhenLoggedIn() throws { let response = try testWorld.getResponse(to: "/blog/admin/createUser", loggedInUser: user) - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testCanAccessEditUserPageWhenLoggedIn() throws { let response = try testWorld.getResponse(to: "/blog/admin/users/1/edit", loggedInUser: user) - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testCanAccessResetPasswordPage() throws { let response = try testWorld.getResponse(to: "/blog/admin/resetPassword", loggedInUser: user) - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } // MARK: - Helpers @@ -116,8 +116,8 @@ class AccessControlTests: XCTestCase { private func assertLoginRequired(method: HTTPMethod, path: String) throws { let response = try testWorld.getResponse(to: "/blog/admin/\(path)", method: method) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/blog/admin/login/?loginRequired") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/blog/admin/login/?loginRequired") } } diff --git a/Tests/SteamPressTests/AdminTests/AdminPageTests.swift b/Tests/SteamPressTests/AdminTests/AdminPageTests.swift index 29d069ed..050cfcb5 100644 --- a/Tests/SteamPressTests/AdminTests/AdminPageTests.swift +++ b/Tests/SteamPressTests/AdminTests/AdminPageTests.swift @@ -5,7 +5,7 @@ import SteamPress class AdminPageTests: XCTestCase { func testAdminPagePassesCorrectInformationToPresenter() throws { - var testWorld = try TestWorld.create() + let testWorld = try TestWorld.create(websiteURL: "/") let user = testWorld.createUser(username: "leia") let testData1 = try testWorld.createPost(author: user) let testData2 = try testWorld.createPost(title: "A second post", author: user) @@ -22,8 +22,8 @@ class AdminPageTests: XCTestCase { XCTAssertEqual(presenter.adminViewPageInformation?.loggedInUser.username, user.username) XCTAssertEqual(presenter.adminViewPageInformation?.websiteURL.absoluteString, "/") - XCTAssertEqual(presenter.adminViewPageInformation?.currentPageURL.absoluteString, "/admin") + XCTAssertEqual(presenter.adminViewPageInformation?.currentPageURL.absoluteString, "/admin/") - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + try testWorld.shutdown() } } diff --git a/Tests/SteamPressTests/AdminTests/AdminPostTests.swift b/Tests/SteamPressTests/AdminTests/AdminPostTests.swift index 1471ba60..bbbb0424 100644 --- a/Tests/SteamPressTests/AdminTests/AdminPostTests.swift +++ b/Tests/SteamPressTests/AdminTests/AdminPostTests.swift @@ -15,20 +15,20 @@ class AdminPostTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create() + override func setUpWithError() throws { + testWorld = try TestWorld.create(websiteURL: "/") user = testWorld.createUser(username: "leia") } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Post Creation func testPostCanBeCreated() throws { struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -53,17 +53,18 @@ class AdminPostTests: XCTestCase { XCTAssertTrue(testWorld.context.repository.postTagLinks .contains { $0.postID == post.blogID! && $0.tagID == secondTagID }) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/posts/post-title/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/posts/post-title/") } func testCreatingPostWithNonUniqueSlugFromSameTitle() throws { let randomNumber = 345 + try testWorld.shutdown() testWorld = try TestWorld.create(randomNumberGenerator: StubbedRandomNumberGenerator(numberToReturn: randomNumber)) let initialPostData = try testWorld.createPost(title: "Post Title", slugUrl: "post-title") struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -75,7 +76,7 @@ class AdminPostTests: XCTestCase { XCTAssertEqual(testWorld.context.repository.posts.count, 2) let post = try XCTUnwrap(testWorld.context.repository.posts.last) XCTAssertEqual(post.slugUrl, "post-title-\(randomNumber)") - XCTAssertEqual(response.http.headers[.location].first, "/posts/post-title-\(randomNumber)/") + XCTAssertEqual(response.headers[.location].first, "/posts/post-title-\(randomNumber)/") } func testPostCreationPageGetsBasicInfo() throws { @@ -95,13 +96,13 @@ class AdminPostTests: XCTestCase { XCTAssertFalse(titleError) XCTAssertFalse(contentsError) XCTAssertEqual(presenter.createPostPageInformation?.loggedInUser.username, user.username) - XCTAssertEqual(presenter.createPostPageInformation?.currentPageURL.absoluteString, "/admin/createPost") + XCTAssertEqual(presenter.createPostPageInformation?.currentPageURL.absoluteString, "/admin/createPost/") XCTAssertEqual(presenter.createPostPageInformation?.websiteURL.absoluteString, "/") } func testPostCannotBeCreatedIfDraftAndPublishNotSet() throws { struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -110,12 +111,12 @@ class AdminPostTests: XCTestCase { let response = try testWorld.getResponse(to: createPostPath, body: createData, loggedInUser: user) - XCTAssertEqual(response.http.status, .badRequest) + XCTAssertEqual(response.status, .badRequest) } func testCreatePostMustIncludeTitle() throws { struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] let publish = true @@ -130,13 +131,13 @@ class AdminPostTests: XCTestCase { XCTAssertTrue(titleError) XCTAssertFalse(contentsError) XCTAssertEqual(presenter.createPostPageInformation?.loggedInUser.username, user.username) - XCTAssertEqual(presenter.createPostPageInformation?.currentPageURL.absoluteString, "/admin/createPost") + XCTAssertEqual(presenter.createPostPageInformation?.currentPageURL.absoluteString, "/admin/createPost/") XCTAssertEqual(presenter.createPostPageInformation?.websiteURL.absoluteString, "/") } func testCreatePostMustIncludeContents() throws { struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let tags = ["First Tag", "Second Tag"] let publish = true @@ -154,7 +155,7 @@ class AdminPostTests: XCTestCase { func testPresenterGetsDataIfValidationOfDataFails() throws { struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let tags = ["First Tag", "Second Tag"] let publish = true @@ -176,7 +177,7 @@ class AdminPostTests: XCTestCase { func testCreatePostWithDraftDoesNotPublishPost() throws { struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -197,7 +198,7 @@ class AdminPostTests: XCTestCase { let existingTag = try testWorld.createTag(existingTagName, on: existingPost.post) struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -218,7 +219,7 @@ class AdminPostTests: XCTestCase { func testPostCanBeUpdated() throws { struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -241,7 +242,7 @@ class AdminPostTests: XCTestCase { func testPostCanBeUpdatedAndUpdateSlugURL() throws { struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -298,8 +299,8 @@ class AdminPostTests: XCTestCase { let updateData = UpdateData(title: testData.post.title) let response = try testWorld.getResponse(to: "/admin/posts/\(testData.post.blogID!)/edit", body: updateData, loggedInUser: user) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/posts/\(testData.post.slugUrl)/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/posts/\(testData.post.slugUrl)/") } func testThatEditingPostGetsRedirectToPostPageWithNewSlugURL() throws { @@ -315,8 +316,8 @@ class AdminPostTests: XCTestCase { let updateData = UpdateData(title: "Some New Title") let response = try testWorld.getResponse(to: "/admin/posts/\(testData.post.blogID!)/edit", body: updateData, loggedInUser: user) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/posts/some-new-title/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/posts/some-new-title/") } func testEditingPostWithNewTagsRemovesOldLinksAndAddsNewLinks() throws { @@ -329,7 +330,7 @@ class AdminPostTests: XCTestCase { let newTagName = "A New Tag" struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags: [String] @@ -352,7 +353,7 @@ class AdminPostTests: XCTestCase { func testLastUpdatedTimeGetsChangedWhenEditingAPost() throws { struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -373,7 +374,7 @@ class AdminPostTests: XCTestCase { func testCreatedTimeSetWhenPublishingADraft() throws { struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -395,7 +396,7 @@ class AdminPostTests: XCTestCase { func testCreatedTimeSetAndMarkedAsDraftWhenSavingADraft() throws { struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -417,7 +418,7 @@ class AdminPostTests: XCTestCase { func testEditingPageWithInvalidDataPassesExistingDataToPresenter() throws { struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "" let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] @@ -449,7 +450,7 @@ class AdminPostTests: XCTestCase { func testEditingPageWithInvalidContentsDataPassesExistingDataToPresenter() throws { struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "A new title" let contents = "" let tags = ["First Tag", "Second Tag"] @@ -475,8 +476,8 @@ class AdminPostTests: XCTestCase { let testData = try testWorld.createPost() let response = try testWorld.getResponse(to: "/admin/posts/\(testData.post.blogID!)/delete", method: .POST, body: EmptyContent(), loggedInUser: user) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") XCTAssertEqual(testWorld.context.repository.posts.count, 0) } @@ -548,7 +549,7 @@ class AdminPostTests: XCTestCase { let existingTag = try testWorld.createTag(existingTagName) struct UpdatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title = "Post Title" let contents = "# Post Title\n\nWe have a post" let tags: [String] @@ -578,14 +579,14 @@ class AdminPostTests: XCTestCase { let website = "" setenv("WEBSITE_URL", website, 1) let response = try testWorld.getResponse(to: createPostPath, loggedInUser: user) - XCTAssertEqual(response.http.status, .internalServerError) + XCTAssertEqual(response.status, .internalServerError) } // MARK: - Helpers private func createPostViaRequest(title: String) throws -> BlogPost { struct CreatePostData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let title: String let contents = "# Post Title\n\nWe have a post" let tags = ["First Tag", "Second Tag"] diff --git a/Tests/SteamPressTests/AdminTests/AdminUserTests.swift b/Tests/SteamPressTests/AdminTests/AdminUserTests.swift index 04c65127..3aeb8ea0 100644 --- a/Tests/SteamPressTests/AdminTests/AdminUserTests.swift +++ b/Tests/SteamPressTests/AdminTests/AdminUserTests.swift @@ -15,13 +15,13 @@ class AdminUserTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create() + override func setUpWithError() throws { + testWorld = try TestWorld.create() user = testWorld.createUser(name: "Leia", username: "leia") } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - User Creation @@ -53,7 +53,7 @@ class AdminUserTests: XCTestCase { func testUserCanBeCreatedSuccessfully() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "somepassword" @@ -76,13 +76,13 @@ class AdminUserTests: XCTestCase { XCTAssertEqual(user.tagline, createData.tagline) XCTAssertEqual(user.biography, createData.biography) XCTAssertEqual(user.twitterHandle, createData.twitterHandle) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testUserHasNoAdditionalInfoIfEmptyStringsSent() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "somepassword" @@ -107,7 +107,7 @@ class AdminUserTests: XCTestCase { func testUserMustResetPasswordIfSetToWhenCreatingUser() throws { struct CreateUserResetData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "somepassword" @@ -128,7 +128,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithoutName() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let username = "lukes" let password = "password" let confirmPassword = "password" @@ -145,7 +145,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithoutUsername() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let password = "password" let confirmPassword = "password" @@ -162,7 +162,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithUsernameThatAlreadyExists() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let password = "password" let confirmPassword = "password" @@ -182,7 +182,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithUsernameThatAlreadyExistsIgnoringCase() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let password = "password" let confirmPassword = "password" @@ -202,7 +202,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithoutPassword() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let confirmPassword = "password" @@ -219,7 +219,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithEmptyPassword() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "" @@ -237,7 +237,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithoutSpecifyingAConfirmPassword() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "password" @@ -254,7 +254,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithPasswordsThatDontMatch() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "astrongpassword" @@ -275,7 +275,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithSimplePassword() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "password" @@ -295,7 +295,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithEmptyName() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let mame = "" let username = "lukes" let password = "password" @@ -313,7 +313,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithEmptyUsername() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "" let password = "password" @@ -331,7 +331,7 @@ class AdminUserTests: XCTestCase { func testUserCannotBeCreatedWithInvalidUsername() throws { struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes!" let password = "password" @@ -348,13 +348,14 @@ class AdminUserTests: XCTestCase { } func testPasswordIsActuallyHashedWhenCreatingAUser() throws { - testWorld = try! TestWorld.create(passwordHasherToUse: .reversed) + try testWorld.shutdown() + testWorld = try TestWorld.create(passwordHasherToUse: .reversed) let usersPassword = "password" let hashedPassword = String(usersPassword.reversed()) user = testWorld.createUser(name: "Leia", username: "leia", password: hashedPassword) struct CreateUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "somepassword" @@ -396,7 +397,7 @@ class AdminUserTests: XCTestCase { func testUserCanBeUpdated() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Darth Vader" let username = "darth_vader" } @@ -409,13 +410,13 @@ class AdminUserTests: XCTestCase { XCTAssertEqual(updatedUser.username, editData.username) XCTAssertEqual(updatedUser.name, editData.name) XCTAssertEqual(updatedUser.userID, user.userID) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testUserCanBeUpdatedWithSameUsername() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Leia Organa" let username = "leia" } @@ -428,13 +429,13 @@ class AdminUserTests: XCTestCase { XCTAssertEqual(updatedUser.username, editData.username) XCTAssertEqual(updatedUser.name, editData.name) XCTAssertEqual(updatedUser.userID, user.userID) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testUserCanBeUpdatedWithAllInformation() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Darth Vader" let username = "darth_vader" let twitterHandle = "darthVader" @@ -455,13 +456,13 @@ class AdminUserTests: XCTestCase { XCTAssertEqual(updatedUser.tagline, editData.tagline) XCTAssertEqual(updatedUser.biography, editData.biography) XCTAssertEqual(updatedUser.userID, user.userID) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testOptionalInfoDoesntGetUpdatedWhenEditingUsernameAndSendingEmptyValuesIfSomeAlreadySet() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Darth Vader" let username = "darth_vader" let twitterHandle = "" @@ -491,7 +492,7 @@ class AdminUserTests: XCTestCase { func testUpdatingOptionalInfoToEmptyValuesWhenValueOriginallySetSetsItToNil() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Darth Vader" let username = "darth_vader" let twitterHandle = "" @@ -520,7 +521,7 @@ class AdminUserTests: XCTestCase { func testWhenEditingUserResetPasswordFlagSetIfRequired() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let resetPasswordOnLogin = true @@ -533,13 +534,13 @@ class AdminUserTests: XCTestCase { let updatedUser = try XCTUnwrap(testWorld.context.repository.users.last) XCTAssertTrue(updatedUser.resetPasswordRequired) XCTAssertEqual(updatedUser.userID, user.userID) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testWhenEditingUserResetPasswordFlagNotSetIfSetToFalse() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let resetPasswordOnLogin = false @@ -552,13 +553,13 @@ class AdminUserTests: XCTestCase { let updatedUser = try XCTUnwrap(testWorld.context.repository.users.last) XCTAssertFalse(updatedUser.resetPasswordRequired) XCTAssertEqual(updatedUser.userID, user.userID) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testPasswordIsUpdatedWhenNewPasswordProvidedWhenEditingUser() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "anewpassword" @@ -572,13 +573,13 @@ class AdminUserTests: XCTestCase { let updatedUser = try XCTUnwrap(testWorld.context.repository.users.last) XCTAssertEqual(updatedUser.password, editData.password) XCTAssertEqual(updatedUser.userID, user.userID) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testPasswordIsNotUpdatedWhenEmptyPasswordProvidedWhenEditingUser() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "" @@ -593,13 +594,13 @@ class AdminUserTests: XCTestCase { let updatedUser = try XCTUnwrap(testWorld.context.repository.users.last) XCTAssertEqual(updatedUser.password, oldPassword) XCTAssertEqual(updatedUser.userID, user.userID) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") } func testErrorShownWhenUpdatingUsersPasswordWithNonMatchingPasswords() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "anewpassword" @@ -620,7 +621,7 @@ class AdminUserTests: XCTestCase { func testErrorShownWhenChangingUsersPasswordWithShortPassword() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Luke" let username = "lukes" let password = "a" @@ -637,13 +638,14 @@ class AdminUserTests: XCTestCase { } func testPasswordIsActuallyHashedWhenEditingAUser() throws { - testWorld = try! TestWorld.create(passwordHasherToUse: .reversed) + try testWorld.shutdown() + testWorld = try TestWorld.create(passwordHasherToUse: .reversed) let usersPassword = "password" let hashedPassword = String(usersPassword.reversed()) user = testWorld.createUser(name: "Leia", username: "leia", password: hashedPassword) struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Darth Vader" let username = "darth_vader" let password = "somenewpassword" @@ -659,7 +661,7 @@ class AdminUserTests: XCTestCase { func testNameMustBeSetWhenEditingAUser() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "" let username = "darth_vader" let password = "somenewpassword" @@ -677,7 +679,7 @@ class AdminUserTests: XCTestCase { func testUsernameMustBeSetWhenEditingAUser() throws { struct EditUserData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let name = "Darth Vader" let username = "" let password = "somenewpassword" @@ -700,8 +702,8 @@ class AdminUserTests: XCTestCase { let response = try testWorld.getResponse(to: "/admin/users/\(user2.userID!)/delete", body: EmptyContent(), loggedInUser: user) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/admin/") XCTAssertEqual(testWorld.context.repository.users.count, 1) XCTAssertNotEqual(testWorld.context.repository.users.last?.name, "Han") } @@ -723,6 +725,7 @@ class AdminUserTests: XCTestCase { } func testCannotDeleteLastUser() throws { + try testWorld.shutdown() testWorld = try TestWorld.create() let adminUser = testWorld.createUser(name: "Admin", username: "admin") let testData = try testWorld.createPost(author: adminUser) diff --git a/Tests/SteamPressTests/AdminTests/LoginTests.swift b/Tests/SteamPressTests/AdminTests/LoginTests.swift index 191ae0a4..ef5fd6f8 100644 --- a/Tests/SteamPressTests/AdminTests/LoginTests.swift +++ b/Tests/SteamPressTests/AdminTests/LoginTests.swift @@ -2,7 +2,6 @@ import XCTest import Vapor import SteamPress import Foundation -import Authentication class LoginTests: XCTestCase { @@ -21,58 +20,56 @@ class LoginTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create(path: "blog") + override func setUpWithError() throws { + testWorld = try TestWorld.create(path: "blog", websiteURL: "/") user = testWorld.createUser() } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests func testLogin() throws { + try testWorld.shutdown() testWorld = try TestWorld.create(path: "blog", passwordHasherToUse: .real) - let hashedPassword = try BCrypt.hash("password") + let hashedPassword = try BCryptDigest().hash("password") user = testWorld.createUser(password: hashedPassword) let loginData = LoginData(username: user.username, password: "password") let loginResponse = try testWorld.getResponse(to: "/blog/admin/login", method: .POST, body: loginData) - XCTAssertEqual(loginResponse.http.status, .seeOther) - XCTAssertEqual(loginResponse.http.headers[.location].first, "/blog/admin/") - XCTAssertNotNil(loginResponse.http.headers[.setCookie].first) - XCTAssertNotNil(loginResponse.http.cookies["steampress-session"]) + XCTAssertEqual(loginResponse.status, .seeOther) + XCTAssertEqual(loginResponse.headers[.location].first, "/blog/admin/") + XCTAssertNotNil(loginResponse.headers[.setCookie].first) + XCTAssertNotNil(loginResponse.cookies["steampress-session"]) - let sessionCookie = loginResponse.http.cookies["steampress-session"] - var adminRequest = HTTPRequest(method: .GET, url: URL(string: "/blog/admin")!) + let sessionCookie = loginResponse.cookies["steampress-session"] + let adminRequest = Request(application: testWorld.context.app, method: .GET, url: URI(path: "/blog/admin"), on: testWorld.context.app.eventLoopGroup.next()) adminRequest.cookies["steampress-session"] = sessionCookie - let wrappedAdminRequest = Request(http: adminRequest, using: testWorld.context.app!) - let adminResponse = try testWorld.getResponse(to: wrappedAdminRequest) + let adminResponse = try testWorld.getResponse(to: adminRequest) - XCTAssertEqual(adminResponse.http.status, .ok) + XCTAssertEqual(adminResponse.status, .ok) - var logoutRequest = HTTPRequest(method: .POST, url: URL(string: "/blog/admin/logout")!) + let logoutRequest = Request(application: testWorld.context.app, method: .POST, url: URI(path: "/blog/admin/logout"), on: testWorld.context.app.eventLoopGroup.next()) logoutRequest.cookies["steampress-session"] = sessionCookie - let wrappedLogoutRequest = Request(http: logoutRequest, using: testWorld.context.app!) - let logoutResponse = try testWorld.getResponse(to: wrappedLogoutRequest) + let logoutResponse = try testWorld.getResponse(to: logoutRequest) - XCTAssertEqual(logoutResponse.http.status, .seeOther) - XCTAssertEqual(logoutResponse.http.headers[.location].first, "/blog/") + XCTAssertEqual(logoutResponse.status, .seeOther) + XCTAssertEqual(logoutResponse.headers[.location].first, "/blog/") - var secondAdminRequest = HTTPRequest(method: .GET, url: URL(string: "/blog/admin")!) + let secondAdminRequest = Request(application: testWorld.context.app, method: .GET, url: URI(path: "/blog/admin"), on: testWorld.context.app.eventLoopGroup.next()) secondAdminRequest.cookies["steampress-session"] = sessionCookie - let wrappedSecondRequest = Request(http: secondAdminRequest, using: testWorld.context.app!) - let loggedOutAdminResponse = try testWorld.getResponse(to: wrappedSecondRequest) + let loggedOutAdminResponse = try testWorld.getResponse(to: secondAdminRequest) - XCTAssertEqual(loggedOutAdminResponse.http.status, .seeOther) - XCTAssertEqual(loggedOutAdminResponse.http.headers[.location].first, "/blog/admin/login/?loginRequired") + XCTAssertEqual(loggedOutAdminResponse.status, .seeOther) + XCTAssertEqual(loggedOutAdminResponse.headers[.location].first, "/blog/admin/login/?loginRequired") } func testLoginPageCanBeAccessed() throws { let response = try testWorld.getResponse(to: "/blog/admin/login") - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testLoginWarningShownIfRedirecting() throws { @@ -93,7 +90,7 @@ class LoginTests: XCTestCase { func testUserCanResetPassword() throws { struct ResetPasswordData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let password = "Th3S@m3password" let confirmPassword = "Th3S@m3password" } @@ -102,14 +99,14 @@ class LoginTests: XCTestCase { let response = try testWorld.getResponse(to: "/blog/admin/resetPassword", body: data, loggedInUser: user) XCTAssertEqual(user.password, data.password) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/blog/admin/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/blog/admin/") XCTAssertTrue(testWorld.context.repository.userUpdated) } func testUserCannotResetPasswordWithMismatchingPasswords() throws { struct ResetPasswordData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let password = "Th3S@m3password" let confirmPassword = "An0th3rPass!" } @@ -131,7 +128,7 @@ class LoginTests: XCTestCase { func testUserCannotResetPasswordWithoutPassword() throws { struct ResetPasswordData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let confirmPassword = "Th3S@m3password" } @@ -148,7 +145,7 @@ class LoginTests: XCTestCase { func testUserCannotResetPasswordWithoutConfirmPassword() throws { struct ResetPasswordData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let password = "Th3S@m3password" } @@ -164,7 +161,7 @@ class LoginTests: XCTestCase { func testUserCannotResetPasswordWithShortPassword() throws { struct ResetPasswordData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let password = "apassword" let confirmPassword = "apassword" } @@ -179,7 +176,7 @@ class LoginTests: XCTestCase { func testThatAfterResettingPasswordUserIsNotAskedToResetPassword() throws { let user2 = testWorld.createUser(name: "Han", username: "hans", resetPasswordRequired: true) struct ResetPasswordData: Content { - static let defaultContentType = MediaType.urlEncodedForm + static let defaultContentType = HTTPMediaType.urlEncodedForm let password = "alongpassword" let confirmPassword = "alongpassword" } @@ -189,7 +186,7 @@ class LoginTests: XCTestCase { let response = try testWorld.getResponse(to: "/blog/admin", method: .GET, body: EmptyContent(), loggedInUser: user2) - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testUserIsRedirectedWhenLoggingInAndPasswordResetRequired() throws { @@ -197,8 +194,8 @@ class LoginTests: XCTestCase { let response = try testWorld.getResponse(to: "/blog/admin/", method: .GET, body: EmptyContent(), loggedInUser: user2) - XCTAssertEqual(response.http.status, .seeOther) - XCTAssertEqual(response.http.headers[.location].first, "/blog/admin/resetPassword/") + XCTAssertEqual(response.status, .seeOther) + XCTAssertEqual(response.headers[.location].first, "/blog/admin/resetPassword/") } func testErrorShownWhenTryingToLoginWithoutUsername() throws { @@ -233,7 +230,7 @@ class LoginTests: XCTestCase { let loginData = LoginData(username: "luke", password: "password", rememberMe: true) let response = try testWorld.getResponse(to: "/blog/admin/login", method: .POST, body: loginData) - let cookieExpiry = try XCTUnwrap(response.http.cookies["steampress-session"]?.expires) + let cookieExpiry = try XCTUnwrap(response.cookies["steampress-session"]?.expires) let oneYear: TimeInterval = 60 * 60 * 24 * 365 XCTAssertEqual(cookieExpiry.timeIntervalSince1970, Date().addingTimeInterval(oneYear).timeIntervalSince1970, accuracy: 1) } @@ -242,7 +239,7 @@ class LoginTests: XCTestCase { let loginData = LoginData(username: "luke", password: "password", rememberMe: nil) let response = try testWorld.getResponse(to: "/blog/admin/login", method: .POST, body: loginData) - let cookie = try XCTUnwrap(response.http.cookies["steampress-session"]) + let cookie = try XCTUnwrap(response.cookies["steampress-session"]) XCTAssertNil(cookie.expires) } @@ -250,7 +247,7 @@ class LoginTests: XCTestCase { let loginData = LoginData(username: "luke", password: "password", rememberMe: false) let response = try testWorld.getResponse(to: "/blog/admin/login", method: .POST, body: loginData) - let cookie = try XCTUnwrap(response.http.cookies["steampress-session"]) + let cookie = try XCTUnwrap(response.cookies["steampress-session"]) XCTAssertNil(cookie.expires) } @@ -261,7 +258,7 @@ class LoginTests: XCTestCase { loginData = LoginData(username: "luke", password: "password", rememberMe: false) let response = try testWorld.getResponse(to: "/blog/admin/login", method: .POST, body: loginData) - let cookie = try XCTUnwrap(response.http.cookies["steampress-session"]) + let cookie = try XCTUnwrap(response.cookies["steampress-session"]) XCTAssertNil(cookie.expires) } @@ -269,13 +266,12 @@ class LoginTests: XCTestCase { let loginData = LoginData(username: "luke", password: "password", rememberMe: true) let loginResponse = try testWorld.getResponse(to: "/blog/admin/login", method: .POST, body: loginData) - let cookie = loginResponse.http.cookies["steampress-session"] - var adminRequest = HTTPRequest(method: .GET, url: URL(string: "/blog/admin")!) + let cookie = loginResponse.cookies["steampress-session"] + let adminRequest = Request(application: testWorld.context.app, method: .GET, url: URI(path: "/blog/admin"), on: testWorld.context.app.eventLoopGroup.next()) adminRequest.cookies["steampress-session"] = cookie - let wrappedAdminRequest = Request(http: adminRequest, using: testWorld.context.app!) - let response = try testWorld.getResponse(to: wrappedAdminRequest) + let response = try testWorld.getResponse(to: adminRequest) - XCTAssertEqual(loginResponse.http.cookies["steampress-session"]?.expires, response.http.cookies["steampress-session"]?.expires) + XCTAssertEqual(loginResponse.cookies["steampress-session"]?.expires, response.cookies["steampress-session"]?.expires) } func testCorrectPageInformationForLogin() throws { diff --git a/Tests/SteamPressTests/BlogTests/AuthorTests.swift b/Tests/SteamPressTests/BlogTests/AuthorTests.swift index b6ae451d..ac8cdc0a 100644 --- a/Tests/SteamPressTests/BlogTests/AuthorTests.swift +++ b/Tests/SteamPressTests/BlogTests/AuthorTests.swift @@ -17,15 +17,15 @@ class AuthorTests: XCTestCase { private var postsPerPage = 7 // MARK: - Overrides - - override func setUp() { - testWorld = try! TestWorld.create(postsPerPage: postsPerPage) + + override func setUpWithError() throws { + testWorld = try TestWorld.create(postsPerPage: postsPerPage, websiteURL: "/") user = testWorld.createUser(username: "leia") - postData = try! testWorld.createPost(author: user) + postData = try testWorld.createPost(author: user) } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests @@ -53,14 +53,15 @@ class AuthorTests: XCTestCase { } func testDisabledBlogAuthorsPath() throws { + try testWorld.shutdown() testWorld = try TestWorld.create(enableAuthorPages: false) _ = testWorld.createUser(username: "leia") let authorResponse = try testWorld.getResponse(to: authorsRequestPath) let allAuthorsResponse = try testWorld.getResponse(to: allAuthorsRequestPath) - XCTAssertEqual(authorResponse.http.status, .notFound) - XCTAssertEqual(allAuthorsResponse.http.status, .notFound) + XCTAssertEqual(authorResponse.status, .notFound) + XCTAssertEqual(allAuthorsResponse.status, .notFound) } func testAuthorView() throws { @@ -163,7 +164,7 @@ class AuthorTests: XCTestCase { let tag2Name = "Search" let tag1 = try testWorld.createTag(tag1Name, on: post2.post) _ = try testWorld.createTag(tag2Name, on: postData.post) - try testWorld.context.repository.add(tag1, to: postData.post) + try testWorld.context.repository.internalAdd(tag1, to: postData.post) _ = try testWorld.getResponse(to: "/authors/leia") let tagsForPosts = try XCTUnwrap(presenter.authorPageTagsForPost) diff --git a/Tests/SteamPressTests/BlogTests/DisabledBlogTagTests.swift b/Tests/SteamPressTests/BlogTests/DisabledBlogTagTests.swift index e09ac400..49117f42 100644 --- a/Tests/SteamPressTests/BlogTests/DisabledBlogTagTests.swift +++ b/Tests/SteamPressTests/BlogTests/DisabledBlogTagTests.swift @@ -3,17 +3,17 @@ import Vapor class DisabledBlogTagTests: XCTestCase { func testDisabledBlogTagsPath() throws { - var testWorld = try TestWorld.create(enableTagPages: false) + let testWorld = try TestWorld.create(enableTagPages: false) _ = try testWorld.createTag("Engineering") var tagResponse: Response? = try testWorld.getResponse(to: "/tags/Engineering") var allTagsResponse: Response? = try testWorld.getResponse(to: "/tags") - XCTAssertEqual(.notFound, tagResponse?.http.status) - XCTAssertEqual(.notFound, allTagsResponse?.http.status) + XCTAssertEqual(.notFound, tagResponse?.status) + XCTAssertEqual(.notFound, allTagsResponse?.status) tagResponse = nil allTagsResponse = nil - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + try testWorld.shutdown() } } diff --git a/Tests/SteamPressTests/BlogTests/IndexTests.swift b/Tests/SteamPressTests/BlogTests/IndexTests.swift index 1794b8ec..4a65a6b3 100644 --- a/Tests/SteamPressTests/BlogTests/IndexTests.swift +++ b/Tests/SteamPressTests/BlogTests/IndexTests.swift @@ -16,13 +16,13 @@ class IndexTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create(postsPerPage: postsPerPage) - firstData = try! testWorld.createPost(title: "Test Path", slugUrl: "test-path") + override func setUpWithError() throws { + testWorld = try TestWorld.create(postsPerPage: postsPerPage, websiteURL: "/") + firstData = try testWorld.createPost(title: "Test Path", slugUrl: "test-path") } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests @@ -71,15 +71,16 @@ class IndexTests: XCTestCase { func testThatAccessingPathsRouteRedirectsToBlogIndex() throws { let response = try testWorld.getResponse(to: "/posts/") - XCTAssertEqual(response.http.status, .movedPermanently) - XCTAssertEqual(response.http.headers[.location].first, "/") + XCTAssertEqual(response.status, .movedPermanently) + XCTAssertEqual(response.headers[.location].first, "/") } func testThatAccessingPathsRouteRedirectsToBlogIndexWithCustomPath() throws { - testWorld = try! TestWorld.create(path: "blog") + try testWorld.shutdown() + testWorld = try TestWorld.create(path: "blog") let response = try testWorld.getResponse(to: "/blog/posts/") - XCTAssertEqual(response.http.status, .movedPermanently) - XCTAssertEqual(response.http.headers[.location].first, "/blog/") + XCTAssertEqual(response.status, .movedPermanently) + XCTAssertEqual(response.headers[.location].first, "/blog/") } // MARK: - Pagination Tests @@ -135,7 +136,8 @@ class IndexTests: XCTestCase { } func testIndexPageCurrentPageWhenAtSubPath() throws { - testWorld = try TestWorld.create(path: "blog") + try testWorld.shutdown() + testWorld = try TestWorld.create(path: "blog", websiteURL: "/") _ = try testWorld.getResponse(to: "/blog") XCTAssertEqual(presenter.indexPageInformation?.currentPageURL.absoluteString, "/blog") XCTAssertEqual(presenter.indexPageInformation?.websiteURL.absoluteString, "/") diff --git a/Tests/SteamPressTests/BlogTests/PostTests.swift b/Tests/SteamPressTests/BlogTests/PostTests.swift index 8f213f90..0fda83a9 100644 --- a/Tests/SteamPressTests/BlogTests/PostTests.swift +++ b/Tests/SteamPressTests/BlogTests/PostTests.swift @@ -16,13 +16,13 @@ class PostTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create() - firstData = try! testWorld.createPost(title: "Test Path", slugUrl: "test-path") + override func setUpWithError() throws { + testWorld = try TestWorld.create(websiteURL: "/") + firstData = try testWorld.createPost(title: "Test Path", slugUrl: "test-path") } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests @@ -77,4 +77,9 @@ class PostTests: XCTestCase { XCTAssertEqual(tags.first?.name, tag1Name) XCTAssertEqual(tags.last?.name, tag2Name) } + + func testExtraInitialiserWorks() throws { + let post = BlogPost(blogID: 1, title: "title", contents: "contents", authorID: 1, creationDate: Date(), slugUrl: "slug-url", published: true) + XCTAssertEqual(post.blogID, 1) + } } diff --git a/Tests/SteamPressTests/BlogTests/SearchTests.swift b/Tests/SteamPressTests/BlogTests/SearchTests.swift index d8f642f8..daa1abbd 100644 --- a/Tests/SteamPressTests/BlogTests/SearchTests.swift +++ b/Tests/SteamPressTests/BlogTests/SearchTests.swift @@ -15,13 +15,13 @@ class SearchTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create() - firstData = try! testWorld.createPost(title: "Test Path", slugUrl: "test-path") + override func setUpWithError() throws { + testWorld = try TestWorld.create(websiteURL: "/") + firstData = try testWorld.createPost(title: "Test Path", slugUrl: "test-path") } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests @@ -29,7 +29,7 @@ class SearchTests: XCTestCase { func testBlogPassedToSearchPageCorrectly() throws { let response = try testWorld.getResponse(to: "/search?term=Test") - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) XCTAssertEqual(presenter.searchTerm, "Test") XCTAssertEqual(presenter.searchTotalResults, 1) XCTAssertEqual(presenter.searchPosts?.first?.title, firstData.post.title) @@ -38,7 +38,7 @@ class SearchTests: XCTestCase { func testThatSearchTermNilIfEmptySearch() throws { let response = try testWorld.getResponse(to: "/search?term=") - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) XCTAssertEqual(presenter.searchPosts?.count, 0) XCTAssertNil(presenter.searchTerm) } @@ -46,7 +46,7 @@ class SearchTests: XCTestCase { func testThatSearchTermNilIfNoSearchTerm() throws { let response = try testWorld.getResponse(to: "/search") - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) XCTAssertEqual(presenter.searchPosts?.count, 0) XCTAssertNil(presenter.searchTerm) } @@ -94,7 +94,7 @@ class SearchTests: XCTestCase { let tag2Name = "Search" let tag1 = try testWorld.createTag(tag1Name, on: post2.post) _ = try testWorld.createTag(tag2Name, on: firstData.post) - try testWorld.context.repository.add(tag1, to: firstData.post) + try testWorld.context.repository.internalAdd(tag1, to: firstData.post) _ = try testWorld.getResponse(to: "/search?term=Test") let tagsForPosts = try XCTUnwrap(presenter.searchPageTagsForPost) diff --git a/Tests/SteamPressTests/BlogTests/TagTests.swift b/Tests/SteamPressTests/BlogTests/TagTests.swift index 1cef5079..850682a5 100644 --- a/Tests/SteamPressTests/BlogTests/TagTests.swift +++ b/Tests/SteamPressTests/BlogTests/TagTests.swift @@ -19,23 +19,23 @@ class TagTests: XCTestCase { // MARK: - Overrides - override func setUp() { - testWorld = try! TestWorld.create(postsPerPage: postsPerPage) - postData = try! testWorld.createPost() - tag = try! testWorld.createTag(tagName, on: postData.post) + override func setUpWithError() throws { + testWorld = try TestWorld.create(postsPerPage: postsPerPage, websiteURL: "/") + postData = try testWorld.createPost() + tag = try testWorld.createTag(tagName, on: postData.post) } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests func testAllTagsPageGetsAllTags() throws { - let secondPost = try! testWorld.createPost() - let thirdPost = try! testWorld.createPost() + let secondPost = try testWorld.createPost() + let thirdPost = try testWorld.createPost() let secondTag = try testWorld.createTag("AnotherTag", on: secondPost.post) - try testWorld.context.repository.add(secondTag, to: thirdPost.post) + try testWorld.context.repository.internalAdd(secondTag, to: thirdPost.post) _ = try testWorld.getResponse(to: allTagsRequestPath) XCTAssertEqual(presenter.allTagsPageTags?.count, 2) @@ -77,7 +77,7 @@ class TagTests: XCTestCase { func testRequestToURLEncodedTag() throws { _ = try testWorld.createTag("Some tag") let response = try testWorld.getResponse(to: "/tags/Some%20tag") - XCTAssertEqual(response.http.status, .ok) + XCTAssertEqual(response.status, .ok) } func testTagPageInformationGetsLoggedInUser() throws { diff --git a/Tests/SteamPressTests/Fakes/CapturingViewRenderer.swift b/Tests/SteamPressTests/Fakes/CapturingViewRenderer.swift index c44aa725..38eed9a0 100644 --- a/Tests/SteamPressTests/Fakes/CapturingViewRenderer.swift +++ b/Tests/SteamPressTests/Fakes/CapturingViewRenderer.swift @@ -1,19 +1,23 @@ import Vapor -class CapturingViewRenderer: ViewRenderer, Service { +class CapturingViewRenderer: ViewRenderer { var shouldCache = false - var worker: Worker + var eventLoop: EventLoop - init(worker: Worker) { - self.worker = worker + init(eventLoop: EventLoop) { + self.eventLoop = eventLoop + } + + func `for`(_ request: Request) -> ViewRenderer { + return self } private(set) var capturedContext: Encodable? private(set) var templatePath: String? - func render(_ path: String, _ context: E, userInfo: [AnyHashable: Any]) -> EventLoopFuture where E: Encodable { + func render(_ name: String, _ context: E) -> EventLoopFuture where E : Encodable { self.capturedContext = context - self.templatePath = path - return Future.map(on: worker) { return View(data: "Test".convertToData()) } + self.templatePath = name + return TestDataBuilder.createFutureView(on: eventLoop) } } diff --git a/Tests/SteamPressTests/Fakes/PlaintextHasher.swift b/Tests/SteamPressTests/Fakes/PlaintextHasher.swift deleted file mode 100644 index 50e4afc9..00000000 --- a/Tests/SteamPressTests/Fakes/PlaintextHasher.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Vapor -import SteamPress - -struct PlaintextHasher: PasswordHasher { - func hash(_ plaintext: LosslessDataConvertible) throws -> String { - return String.convertFromData(plaintext.convertToData()) - } -} diff --git a/Tests/SteamPressTests/Fakes/Presenters/CapturingAdminPresenter.swift b/Tests/SteamPressTests/Fakes/Presenters/CapturingAdminPresenter.swift index 2abbb3dd..c63b66d8 100644 --- a/Tests/SteamPressTests/Fakes/Presenters/CapturingAdminPresenter.swift +++ b/Tests/SteamPressTests/Fakes/Presenters/CapturingAdminPresenter.swift @@ -1,19 +1,28 @@ -import SteamPress +@testable import SteamPress import Vapor class CapturingAdminPresenter: BlogAdminPresenter { + + let eventLoop: EventLoop + init(eventLoop: EventLoop) { + self.eventLoop = eventLoop + } + + func `for`(_ request: Request, pathCreator: BlogPathCreator) -> BlogAdminPresenter { + return self + } // MARK: - BlogPresenter private(set) var adminViewErrors: [String]? private(set) var adminViewPosts: [BlogPost]? private(set) var adminViewUsers: [BlogUser]? private(set) var adminViewPageInformation: BlogAdminPageInformation? - func createIndexView(on container: Container, posts: [BlogPost], users: [BlogUser], errors: [String]?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + func createIndexView(posts: [BlogPost], users: [BlogUser], errors: [String]?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { self.adminViewErrors = errors self.adminViewPosts = posts self.adminViewUsers = users self.adminViewPageInformation = pageInformation - return createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var createPostErrors: [String]? @@ -27,7 +36,7 @@ class CapturingAdminPresenter: BlogAdminPresenter { private(set) var createPostTitleError: Bool? private(set) var createPostContentsError: Bool? private(set) var createPostPageInformation: BlogAdminPageInformation? - func createPostView(on container: Container, errors: [String]?, title: String?, contents: String?, slugURL: String?, tags: [String]?, isEditing: Bool, post: BlogPost?, isDraft: Bool?, titleError: Bool, contentsError: Bool, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + func createPostView(errors: [String]?, title: String?, contents: String?, slugURL: String?, tags: [String]?, isEditing: Bool, post: BlogPost?, isDraft: Bool?, titleError: Bool, contentsError: Bool, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { self.createPostErrors = errors self.createPostTitle = title self.createPostContents = contents @@ -39,7 +48,7 @@ class CapturingAdminPresenter: BlogAdminPresenter { self.createPostTitleError = titleError self.createPostContentsError = contentsError self.createPostPageInformation = pageInformation - return createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var createUserErrors: [String]? @@ -56,7 +65,7 @@ class CapturingAdminPresenter: BlogAdminPresenter { private(set) var createUserEditing: Bool? private(set) var createUserNameError: Bool? private(set) var createUserUsernameError: Bool? - func createUserView(on container: Container, editing: Bool, errors: [String]?, name: String?, nameError: Bool, username: String?, usernameErorr: Bool, passwordError: Bool, confirmPasswordError: Bool, resetPasswordOnLogin: Bool, userID: Int?, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + func createUserView(editing: Bool, errors: [String]?, name: String?, nameError: Bool, username: String?, usernameErorr: Bool, passwordError: Bool, confirmPasswordError: Bool, resetPasswordOnLogin: Bool, userID: Int?, profilePicture: String?, twitterHandle: String?, biography: String?, tagline: String?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { self.createUserEditing = editing self.createUserErrors = errors self.createUserName = name @@ -71,26 +80,18 @@ class CapturingAdminPresenter: BlogAdminPresenter { self.createUserNameError = nameError self.createUserUsernameError = usernameErorr self.createUserResetPasswordRequired = resetPasswordOnLogin - return createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var resetPasswordErrors: [String]? private(set) var resetPasswordError: Bool? private(set) var resetPasswordConfirmError: Bool? private(set) var resetPasswordPageInformation: BlogAdminPageInformation? - func createResetPasswordView(on container: Container, errors: [String]?, passwordError: Bool?, confirmPasswordError: Bool?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { + func createResetPasswordView(errors: [String]?, passwordError: Bool?, confirmPasswordError: Bool?, pageInformation: BlogAdminPageInformation) -> EventLoopFuture { self.resetPasswordErrors = errors self.resetPasswordError = passwordError self.resetPasswordConfirmError = confirmPasswordError self.resetPasswordPageInformation = pageInformation - return createFutureView(on: container) - } - - // MARK: - Helpers - - func createFutureView(on container: Container) -> EventLoopFuture { - let data = "some HTML".convertToData() - let view = View(data: data) - return container.future(view) + return TestDataBuilder.createFutureView(on: eventLoop) } } diff --git a/Tests/SteamPressTests/Fakes/Presenters/CapturingBlogPresenter.swift b/Tests/SteamPressTests/Fakes/Presenters/CapturingBlogPresenter.swift index 5a88da34..cc36e8fb 100644 --- a/Tests/SteamPressTests/Fakes/Presenters/CapturingBlogPresenter.swift +++ b/Tests/SteamPressTests/Fakes/Presenters/CapturingBlogPresenter.swift @@ -1,9 +1,18 @@ -import SteamPress +@testable import SteamPress import Vapor import Foundation class CapturingBlogPresenter: BlogPresenter { + + let eventLoop: EventLoop + init(eventLoop: EventLoop) { + self.eventLoop = eventLoop + } + + func `for`(_ request: Request) -> BlogPresenter { + return self + } // MARK: - BlogPresenter private(set) var indexPosts: [BlogPost]? @@ -12,36 +21,36 @@ class CapturingBlogPresenter: BlogPresenter { private(set) var indexPageInformation: BlogGlobalPageInformation? private(set) var indexPaginationTagInfo: PaginationTagInformation? private(set) var indexTagsForPosts: [Int: [BlogTag]]? - func indexView(on container: Container, posts: [BlogPost], tags: [BlogTag], authors: [BlogUser], tagsForPosts: [Int : [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + func indexView(posts: [BlogPost], tags: [BlogTag], authors: [BlogUser], tagsForPosts: [Int : [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { self.indexPosts = posts self.indexTags = tags self.indexAuthors = authors self.indexPageInformation = pageInformation self.indexPaginationTagInfo = paginationTagInfo self.indexTagsForPosts = tagsForPosts - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var post: BlogPost? private(set) var postAuthor: BlogUser? private(set) var postPageInformation: BlogGlobalPageInformation? private(set) var postPageTags: [BlogTag]? - func postView(on container: Container, post: BlogPost, author: BlogUser, tags: [BlogTag], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + func postView(post: BlogPost, author: BlogUser, tags: [BlogTag], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { self.post = post self.postAuthor = author self.postPageInformation = pageInformation self.postPageTags = tags - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var allAuthors: [BlogUser]? private(set) var allAuthorsPostCount: [Int: Int]? private(set) var allAuthorsPageInformation: BlogGlobalPageInformation? - func allAuthorsView(on container: Container, authors: [BlogUser], authorPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + func allAuthorsView(authors: [BlogUser], authorPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { self.allAuthors = authors self.allAuthorsPostCount = authorPostCounts self.allAuthorsPageInformation = pageInformation - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var author: BlogUser? @@ -50,24 +59,24 @@ class CapturingBlogPresenter: BlogPresenter { private(set) var authorPageInformation: BlogGlobalPageInformation? private(set) var authorPaginationTagInfo: PaginationTagInformation? private(set) var authorPageTagsForPost: [Int: [BlogTag]]? - func authorView(on container: Container, author: BlogUser, posts: [BlogPost], postCount: Int, tagsForPosts: [Int : [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + func authorView(author: BlogUser, posts: [BlogPost], postCount: Int, tagsForPosts: [Int : [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { self.author = author self.authorPosts = posts self.authorPostCount = postCount self.authorPageInformation = pageInformation self.authorPaginationTagInfo = paginationTagInfo self.authorPageTagsForPost = tagsForPosts - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var allTagsPageTags: [BlogTag]? private(set) var allTagsPagePostCount: [Int: Int]? private(set) var allTagsPageInformation: BlogGlobalPageInformation? - func allTagsView(on container: Container, tags: [BlogTag], tagPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + func allTagsView(tags: [BlogTag], tagPostCounts: [Int: Int], pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { self.allTagsPageTags = tags self.allTagsPagePostCount = tagPostCounts self.allTagsPageInformation = pageInformation - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var tag: BlogTag? @@ -76,14 +85,14 @@ class CapturingBlogPresenter: BlogPresenter { private(set) var tagPaginationTagInfo: PaginationTagInformation? private(set) var tagPageTotalPosts: Int? private(set) var tagPageAuthors: [BlogUser]? - func tagView(on container: Container, tag: BlogTag, posts: [BlogPost], authors: [BlogUser], totalPosts: Int, pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + func tagView(tag: BlogTag, posts: [BlogPost], authors: [BlogUser], totalPosts: Int, pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { self.tag = tag self.tagPosts = posts self.tagPageInformation = pageInformation self.tagPaginationTagInfo = paginationTagInfo self.tagPageTotalPosts = totalPosts self.tagPageAuthors = authors - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var searchPosts: [BlogPost]? @@ -93,7 +102,7 @@ class CapturingBlogPresenter: BlogPresenter { private(set) var searchPageInformation: BlogGlobalPageInformation? private(set) var searchPaginationTagInfo: PaginationTagInformation? private(set) var searchPageTagsForPost: [Int: [BlogTag]]? - func searchView(on container: Container, totalResults: Int, posts: [BlogPost], authors: [BlogUser], searchTerm: String?, tagsForPosts: [Int : [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { + func searchView(totalResults: Int, posts: [BlogPost], authors: [BlogUser], searchTerm: String?, tagsForPosts: [Int : [BlogTag]], pageInformation: BlogGlobalPageInformation, paginationTagInfo: PaginationTagInformation) -> EventLoopFuture { self.searchPosts = posts self.searchTerm = searchTerm self.searchPageInformation = pageInformation @@ -101,7 +110,7 @@ class CapturingBlogPresenter: BlogPresenter { self.searchAuthors = authors self.searchPaginationTagInfo = paginationTagInfo self.searchPageTagsForPost = tagsForPosts - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } private(set) var loginWarning: Bool? @@ -111,7 +120,7 @@ class CapturingBlogPresenter: BlogPresenter { private(set) var loginPasswordError: Bool? private(set) var loginPageInformation: BlogGlobalPageInformation? private(set) var loginPageRememberMe: Bool? - func loginView(on container: Container, loginWarning: Bool, errors: [String]?, username: String?, usernameError: Bool, passwordError: Bool, rememberMe: Bool, pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { + func loginView(loginWarning: Bool, errors: [String]?, username: String?, usernameError: Bool, passwordError: Bool, rememberMe: Bool, pageInformation: BlogGlobalPageInformation) -> EventLoopFuture { self.loginWarning = loginWarning self.loginErrors = errors self.loginUsername = username @@ -119,6 +128,6 @@ class CapturingBlogPresenter: BlogPresenter { self.loginPasswordError = passwordError self.loginPageInformation = pageInformation self.loginPageRememberMe = rememberMe - return TestDataBuilder.createFutureView(on: container) + return TestDataBuilder.createFutureView(on: eventLoop) } } diff --git a/Tests/SteamPressTests/Fakes/ReversedPasswordHasher.swift b/Tests/SteamPressTests/Fakes/ReversedPasswordHasher.swift index f774f73a..14d360b9 100644 --- a/Tests/SteamPressTests/Fakes/ReversedPasswordHasher.swift +++ b/Tests/SteamPressTests/Fakes/ReversedPasswordHasher.swift @@ -1,15 +1,23 @@ import Vapor -import Authentication import SteamPress -struct ReversedPasswordHasher: PasswordHasher, PasswordVerifier { - func hash(_ plaintext: LosslessDataConvertible) throws -> String { - return String(String.convertFromData(plaintext.convertToData()).reversed()) +struct ReversedPasswordHasher: PasswordHasher { + + func verify(_ password: Password, created digest: Digest) throws -> Bool where Password : DataProtocol, Digest : DataProtocol { + return password.reversed() == Array(digest) } + + func hash(_ password: Password) throws -> [UInt8] where Password : DataProtocol { + return password.reversed() + } +} - func verify(_ password: LosslessDataConvertible, created hash: LosslessDataConvertible) throws -> Bool { - let passwordString = String.convertFromData(password.convertToData()) - let passwordHash = String.convertFromData(hash.convertToData()) - return passwordString == String(passwordHash.reversed()) +extension Application.Passwords.Provider { + public static var reversed: Self { + .init { + $0.passwords.use { _ in + ReversedPasswordHasher() + } + } } } diff --git a/Tests/SteamPressTests/Fakes/StubbedRandomNumberGenerator.swift b/Tests/SteamPressTests/Fakes/StubbedRandomNumberGenerator.swift index 1b3fb471..65de6ff9 100644 --- a/Tests/SteamPressTests/Fakes/StubbedRandomNumberGenerator.swift +++ b/Tests/SteamPressTests/Fakes/StubbedRandomNumberGenerator.swift @@ -1,7 +1,16 @@ import SteamPress +import Vapor struct StubbedRandomNumberGenerator: SteamPressRandomNumberGenerator { + func `for`(_ request: Request) -> SteamPressRandomNumberGenerator { + return StubbedRandomNumberGenerator(numberToReturn: self.numberToReturn) + } + let numberToReturn: Int + + init(numberToReturn: Int) { + self.numberToReturn = numberToReturn + } func getNumber() -> Int { return numberToReturn diff --git a/Tests/SteamPressTests/Feed Tests/AtomFeedTests.swift b/Tests/SteamPressTests/Feed Tests/AtomFeedTests.swift index 40822ea6..e0a13b74 100644 --- a/Tests/SteamPressTests/Feed Tests/AtomFeedTests.swift +++ b/Tests/SteamPressTests/Feed Tests/AtomFeedTests.swift @@ -18,8 +18,8 @@ class AtomFeedTests: XCTestCase { dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests @@ -27,7 +27,7 @@ class AtomFeedTests: XCTestCase { func testNoPostsReturnsCorrectAtomFeed() throws { testWorld = try TestWorld.create() - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) @@ -39,7 +39,7 @@ class AtomFeedTests: XCTestCase { let feedInformation = FeedInformation(title: title) testWorld = try TestWorld.create(feedInformation: feedInformation) - let expectedXML = "\n\n\n\(title)\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" + let expectedXML = "\n\n\n\(title)\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -50,7 +50,7 @@ class AtomFeedTests: XCTestCase { let feedInformation = FeedInformation(description: description) testWorld = try TestWorld.create(feedInformation: feedInformation) - let expectedXML = "\n\n\nSteamPress Blog\n\(description)\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" + let expectedXML = "\n\n\nSteamPress Blog\n\(description)\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -61,41 +61,17 @@ class AtomFeedTests: XCTestCase { let feedInformation = FeedInformation(copyright: copyright) testWorld = try TestWorld.create(feedInformation: feedInformation) - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\(copyright)\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\(copyright)\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) } - func testThatLinksAreCorrectForFullURI() throws { - testWorld = try TestWorld.create(path: "blog") - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://geeks.brokenhands.io/blog/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" - - let fullPath = "/blog/atom.xml" - let actualXmlResponse = try testWorld.getResponseString(to: fullPath, headers: [ - "X-Forwarded-Proto": "https", - "X-Forwarded-For": "geeks.brokenhands.io" - ]) - XCTAssertEqual(actualXmlResponse, expectedXML) - } - - func testThatHTTPSLinksWorkWhenBehindReverseProxy() throws { - testWorld = try TestWorld.create() - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://geeks.brokenhands.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n" - - let fullPath = "/atom.xml" - let actualXmlResponse = try testWorld.getResponseString(to: fullPath, headers: [ - "X-Forwarded-Proto": "https", - "X-Forwarded-For": "geeks.brokenhands.io" - ]) - XCTAssertEqual(actualXmlResponse, expectedXML) - } - func testThatLogoCanBeConfigured() throws { let imageURL = "https://static.brokenhands.io/images/feeds/atom.png" let feedInformation = FeedInformation(imageURL: imageURL) testWorld = try TestWorld.create(feedInformation: feedInformation) - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\(imageURL)\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\(imageURL)\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -108,7 +84,7 @@ class AtomFeedTests: XCTestCase { let post = testData.post let author = testData.author - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(self.dateFormatter.string(from: Date()))\n\n/posts-id/1/\n\(post.title)\n\(self.dateFormatter.string(from: post.created))\n\(self.dateFormatter.string(from: post.created))\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post.description())\n\n\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(self.dateFormatter.string(from: Date()))\n\nhttps://www.steampress.io/posts-id/1/\n\(post.title)\n\(self.dateFormatter.string(from: post.created))\n\(self.dateFormatter.string(from: post.created))\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post.description())\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -121,7 +97,7 @@ class AtomFeedTests: XCTestCase { let post = testData.post let author = testData.author - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/blog/\n\n\nSteamPress\n\(self.dateFormatter.string(from: Date()))\n\n/blog/posts-id/1/\n\(post.title)\n\(self.dateFormatter.string(from: post.created))\n\(self.dateFormatter.string(from: post.created))\n\n\(author.name)\n/blog/authors/\(author.username)/\n\n\(try post.description())\n\n\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/blog/\n\n\nSteamPress\n\(self.dateFormatter.string(from: Date()))\n\nhttps://www.steampress.io/blog/posts-id/1/\n\(post.title)\n\(self.dateFormatter.string(from: post.created))\n\(self.dateFormatter.string(from: post.created))\n\n\(author.name)\nhttps://www.steampress.io/blog/authors/\(author.username)/\n\n\(try post.description())\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: blogAtomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -138,7 +114,7 @@ class AtomFeedTests: XCTestCase { let secondPostDate = Date() let post2 = try testWorld.createPost(createdDate: secondPostDate, title: secondTitle, author: author).post - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\n/posts-id/2/\n\(secondTitle)\n\(dateFormatter.string(from: secondPostDate))\n\(dateFormatter.string(from: secondPostDate))\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post2.description())\n\n\n\n/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: post.created))\n\(dateFormatter.string(from: post.created))\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post.description())\n\n\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\nhttps://www.steampress.io/posts-id/2/\n\(secondTitle)\n\(dateFormatter.string(from: secondPostDate))\n\(dateFormatter.string(from: secondPostDate))\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post2.description())\n\n\n\nhttps://www.steampress.io/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: post.created))\n\(dateFormatter.string(from: post.created))\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post.description())\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -153,7 +129,7 @@ class AtomFeedTests: XCTestCase { _ = try testWorld.createPost(title: "A Draft Post", published: false) - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\n/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: post.created))\n\(dateFormatter.string(from: post.created))\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post.description())\n\n\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\nhttps://www.steampress.io/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: post.created))\n\(dateFormatter.string(from: post.created))\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post.description())\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -173,7 +149,7 @@ class AtomFeedTests: XCTestCase { let post2 = try testWorld.createPost(createdDate: secondPostDate, title: secondTitle, author: author).post post2.lastEdited = newEditDate - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: newEditDate))\n\n/posts-id/2/\n\(secondTitle)\n\(dateFormatter.string(from: newEditDate))\n\(dateFormatter.string(from: secondPostDate))\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post2.description())\n\n\n\n/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: firstPostDate))\n\(dateFormatter.string(from: firstPostDate))\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post.description())\n\n\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: newEditDate))\n\nhttps://www.steampress.io/posts-id/2/\n\(secondTitle)\n\(dateFormatter.string(from: newEditDate))\n\(dateFormatter.string(from: secondPostDate))\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post2.description())\n\n\n\nhttps://www.steampress.io/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: firstPostDate))\n\(dateFormatter.string(from: firstPostDate))\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post.description())\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -189,34 +165,16 @@ class AtomFeedTests: XCTestCase { let post = testData.post let author = testData.author - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\n/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: post.created))\n\(dateFormatter.string(from: post.created))\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post.description())\n\n\n\n\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\nhttps://www.steampress.io/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: post.created))\n\(dateFormatter.string(from: post.created))\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post.description())\n\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) } - func testThatFullLinksWorksForPosts() throws { - testWorld = try TestWorld.create(path: "blog") - - let testData = try testWorld.createPost() - - let post = testData.post - let author = testData.author - - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://geeks.brokenhands.io/blog/\n\n\nSteamPress\n\(dateFormatter.string(from: Date()))\n\nhttps://geeks.brokenhands.io/blog/posts-id/1/\n\(post.title)\n\(dateFormatter.string(from: post.created))\n\(dateFormatter.string(from: post.created))\n\n\(author.name)\nhttps://geeks.brokenhands.io/blog/authors/\(author.username)/\n\n\(try post.description())\n\n\n" - - let fullPath = "/blog/atom.xml" - let actualXmlResponse = try testWorld.getResponseString(to: fullPath, headers: [ - "X-Forwarded-Proto": "https", - "X-Forwarded-For": "geeks.brokenhands.io" - ]) - XCTAssertEqual(actualXmlResponse, expectedXML) - } - func testCorrectHeaderSetForAtomFeed() throws { testWorld = try TestWorld.create() let actualXmlResponse = try testWorld.getResponse(to: atomPath) - XCTAssertEqual(actualXmlResponse.http.headers.firstValue(name: .contentType), "application/atom+xml") + XCTAssertEqual(actualXmlResponse.headers.first(name: .contentType), "application/atom+xml") } func testThatDateFormatterIsCorrect() throws { @@ -228,7 +186,7 @@ class AtomFeedTests: XCTestCase { let post = testData.post let author = testData.author - let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\n/\n\n\nSteamPress\n2017-09-20T00:25:08Z\n\n/posts-id/1/\n\(post.title)\n2017-09-20T00:25:08Z\n2017-09-20T00:25:08Z\n\n\(author.name)\n/authors/\(author.username)/\n\n\(try post.description())\n\n\n" + let expectedXML = "\n\n\nSteamPress Blog\nSteamPress is an open-source blogging engine written for Vapor in Swift\nhttps://www.steampress.io/\n\n\nSteamPress\n2017-09-20T00:25:08Z\n\nhttps://www.steampress.io/posts-id/1/\n\(post.title)\n2017-09-20T00:25:08Z\n2017-09-20T00:25:08Z\n\n\(author.name)\nhttps://www.steampress.io/authors/\(author.username)/\n\n\(try post.description())\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: atomPath) XCTAssertEqual(actualXmlResponse, expectedXML) diff --git a/Tests/SteamPressTests/Feed Tests/RSSFeedTests.swift b/Tests/SteamPressTests/Feed Tests/RSSFeedTests.swift index ef031031..1a9ee98a 100644 --- a/Tests/SteamPressTests/Feed Tests/RSSFeedTests.swift +++ b/Tests/SteamPressTests/Feed Tests/RSSFeedTests.swift @@ -18,8 +18,8 @@ class RSSFeedTests: XCTestCase { dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) } - override func tearDown() { - XCTAssertNoThrow(try testWorld.tryAsHardAsWeCanToShutdownApplication()) + override func tearDownWithError() throws { + try testWorld.shutdown() } // MARK: - Tests @@ -27,7 +27,7 @@ class RSSFeedTests: XCTestCase { func testNoPostsReturnsCorrectRSSFeed() throws { testWorld = try TestWorld.create() - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -37,7 +37,7 @@ class RSSFeedTests: XCTestCase { testWorld = try TestWorld.create() let testData = try testWorld.createPost() let post = testData.post - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -53,7 +53,7 @@ class RSSFeedTests: XCTestCase { let contents = "This is some short contents" let post2 = try testWorld.createPost(title: anotherTitle, contents: contents, slugUrl: "another-title", author: author).post - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post2.created))\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n\(anotherTitle)\n\n\n\(contents)\n\n\n/posts/another-title/\n\n\(dateFormatter.string(from: post2.created))\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post2.created))\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(anotherTitle)\n\n\n\(contents)\n\n\nhttps://www.steampress.io/posts/another-title/\n\n\(dateFormatter.string(from: post2.created))\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -66,7 +66,7 @@ class RSSFeedTests: XCTestCase { _ = try testWorld.createPost(title: "A Draft Post", published: false) - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -80,7 +80,7 @@ class RSSFeedTests: XCTestCase { let testData = try testWorld.createPost() let post = testData.post - let expectedXML = "\n\n\n\n\(title)\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch \(title)\nSearch\n/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\n\(title)\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch \(title)\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -94,7 +94,7 @@ class RSSFeedTests: XCTestCase { let testData = try testWorld.createPost() let post = testData.post - let expectedXML = "\n\n\n\nSteamPress Blog\n/\n\(description)\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\n\(description)\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -103,7 +103,7 @@ class RSSFeedTests: XCTestCase { func testRSSFeedEndpointAddedToCorrectEndpointWhenBlogInSubPath() throws { testWorld = try TestWorld.create(path: "blog-path") - let expectedXML = "\n\n\n\nSteamPress Blog\n/blog-path/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\nSearch SteamPress Blog\nSearch\n/blog-path/search?\nterm\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/blog-path/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/blog-path/search?\nterm\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: blogRSSPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -114,7 +114,7 @@ class RSSFeedTests: XCTestCase { let testData = try testWorld.createPost() let post = testData.post - let expectedXML = "\n\n\n\nSteamPress Blog\n/blog-path/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\n/blog-path/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/blog-path/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/blog-path/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/blog-path/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/blog-path/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: blogRSSPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -125,7 +125,7 @@ class RSSFeedTests: XCTestCase { let feedInformation = FeedInformation(copyright: copyright) testWorld = try TestWorld.create(feedInformation: feedInformation) - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(copyright)\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(copyright)\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -136,48 +136,18 @@ class RSSFeedTests: XCTestCase { let testData = try testWorld.createPost(tags: ["Vapor 2", "Engineering"]) let post = testData.post - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/posts/\(post.slugUrl)/\n\nVapor 2\nEngineering\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\nVapor 2\nEngineering\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) } - func testThatLinksComesFromRequestCorrectly() throws { - testWorld = try TestWorld.create(path: "blog-path") - let testData = try testWorld.createPost() - let post = testData.post - - let expectedXML = "\n\n\n\nSteamPress Blog\nhttp://geeks.brokenhands.io/blog-path/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttp://geeks.brokenhands.io/blog-path/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttp://geeks.brokenhands.io/blog-path/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" - - let fullPath = "/blog-path/rss.xml" - let actualXmlResponse = try testWorld.getResponseString(to: fullPath, headers: [ - "X-Forwarded-Proto": "http", - "X-Forwarded-For": "geeks.brokenhands.io" - ]) - XCTAssertEqual(actualXmlResponse, expectedXML) - } - - func testThatLinksSpecifyHTTPSWhenComingFromReverseProxy() throws { - testWorld = try TestWorld.create(path: "blog-path") - let testData = try testWorld.createPost() - let post = testData.post - - let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://geeks.brokenhands.io/blog-path/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttps://geeks.brokenhands.io/blog-path/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://geeks.brokenhands.io/blog-path/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" - - let fullPath = "/blog-path/rss.xml" - let actualXmlResponse = try testWorld.getResponseString(to: fullPath, headers: [ - "X-Forwarded-Proto": "https", - "X-Forwarded-For": "geeks.brokenhands.io" - ]) - XCTAssertEqual(actualXmlResponse, expectedXML) - } - func testImageIsProvidedIfSupplied() throws { let image = "https://static.brokenhands.io/images/brokenhands.png" let feedInformation = FeedInformation(imageURL: image) testWorld = try TestWorld.create(feedInformation: feedInformation) - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\n\(image)\nSteamPress Blog\n/\n\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\n\(image)\nSteamPress Blog\nhttps://www.steampress.io/\n\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -187,7 +157,7 @@ class RSSFeedTests: XCTestCase { testWorld = try TestWorld.create() let actualXmlResponse = try testWorld.getResponse(to: rssPath) - XCTAssertEqual(actualXmlResponse.http.headers.firstValue(name: .contentType), "application/rss+xml") + XCTAssertEqual(actualXmlResponse.headers.first(name: .contentType), "application/rss+xml") } func testThatDateFormatterIsCorrect() throws { @@ -196,7 +166,7 @@ class RSSFeedTests: XCTestCase { let testData = try testWorld.createPost(createdDate: createDate) let post = testData.post - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\nWed, 20 Sep 2017 00:25:08 GMT\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\n/posts/\(post.slugUrl)/\n\nWed, 20 Sep 2017 00:25:08 GMT\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\nWed, 20 Sep 2017 00:25:08 GMT\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(post.title)\n\n\n\(try post.description())\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\nWed, 20 Sep 2017 00:25:08 GMT\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) @@ -208,7 +178,7 @@ class RSSFeedTests: XCTestCase { let testData = try testWorld.createPost(contents: contents) let post = testData.post - let expectedXML = "\n\n\n\nSteamPress Blog\n/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\n/search?\nterm\n\n\n\n\(post.title)\n\n\nThis is a post that contains some text. Formatting should be removed\n\n\n/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" + let expectedXML = "\n\n\n\nSteamPress Blog\nhttps://www.steampress.io/\nSteamPress is an open-source blogging engine written for Vapor in Swift\nSteamPress\n60\n\(dateFormatter.string(from: post.created))\n\nSearch SteamPress Blog\nSearch\nhttps://www.steampress.io/search?\nterm\n\n\n\n\(post.title)\n\n\nThis is a post that contains some text. Formatting should be removed\n\n\nhttps://www.steampress.io/posts/\(post.slugUrl)/\n\n\(dateFormatter.string(from: post.created))\n\n\n\n" let actualXmlResponse = try testWorld.getResponseString(to: rssPath) XCTAssertEqual(actualXmlResponse, expectedXML) diff --git a/Tests/SteamPressTests/Helpers/InMemoryRepository.swift b/Tests/SteamPressTests/Helpers/InMemoryRepository.swift index c4f71154..7382f463 100644 --- a/Tests/SteamPressTests/Helpers/InMemoryRepository.swift +++ b/Tests/SteamPressTests/Helpers/InMemoryRepository.swift @@ -1,35 +1,41 @@ import Vapor import SteamPress -class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserRepository, Service { +class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserRepository { private(set) var tags: [BlogTag] private(set) var posts: [BlogPost] private(set) var users: [BlogUser] private(set) var postTagLinks: [BlogPostTagLink] + private(set) var eventLoop: EventLoop - init() { + init(eventLoop: EventLoop) { tags = [] posts = [] users = [] postTagLinks = [] + self.eventLoop = eventLoop } // MARK: - BlogTagRepository + + func `for`(_ request: Request) -> BlogTagRepository { + return self + } - func getAllTags(on container: Container) -> EventLoopFuture<[BlogTag]> { - return container.future(tags) + func getAllTags() -> EventLoopFuture<[BlogTag]> { + return eventLoop.future(tags) } - func getAllTagsWithPostCount(on container: Container) -> EventLoopFuture<[(BlogTag, Int)]> { + func getAllTagsWithPostCount() -> EventLoopFuture<[(BlogTag, Int)]> { let tagsWithCount = tags.map { tag -> (BlogTag, Int) in let postCount = postTagLinks.filter { $0.tagID == tag.tagID }.count return (tag, postCount) } - return container.future(tagsWithCount) + return eventLoop.future(tagsWithCount) } - func getTagsForAllPosts(on container: Container) -> EventLoopFuture<[Int : [BlogTag]]> { + func getTagsForAllPosts() -> EventLoopFuture<[Int : [BlogTag]]> { var dict = [Int: [BlogTag]]() for tag in tags { postTagLinks.filter { $0.tagID == tag.tagID }.forEach { link in @@ -41,10 +47,10 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit } } } - return container.future(dict) + return eventLoop.future(dict) } - func getTags(for post: BlogPost, on container: Container) -> EventLoopFuture<[BlogTag]> { + func getTags(for post: BlogPost) -> EventLoopFuture<[BlogTag]> { var results = [BlogTag]() guard let postID = post.blogID else { fatalError("Post doesn't exist when it should") @@ -56,15 +62,15 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit } results.append(tag) } - return container.future(results) + return eventLoop.future(results) } - func save(_ tag: BlogTag, on container: Container) -> EventLoopFuture { + func save(_ tag: BlogTag) -> EventLoopFuture { if tag.tagID == nil { tag.tagID = tags.count + 1 } tags.append(tag) - return container.future(tag) + return eventLoop.future(tag) } func addTag(name: String) throws -> BlogTag { @@ -73,16 +79,16 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit return newTag } - func add(_ tag: BlogTag, to post: BlogPost, on container: Container) -> EventLoopFuture { + func add(_ tag: BlogTag, to post: BlogPost) -> EventLoopFuture { do { - try add(tag, to: post) - return container.future() + try internalAdd(tag, to: post) + return eventLoop.future() } catch { - return container.future(error: SteamPressTestError(name: "Failed to add tag to post")) + return eventLoop.future(error: SteamPressTestError(name: "Failed to add tag to post")) } } - func add(_ tag: BlogTag, to post: BlogPost) throws { + func internalAdd(_ tag: BlogTag, to post: BlogPost) throws { guard let postID = post.blogID else { fatalError("Blog doesn't exist when it should") } @@ -95,12 +101,12 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit func addTag(name: String, for post: BlogPost) throws -> BlogTag { let newTag = try addTag(name: name) - try add(newTag, to: post) + try internalAdd(newTag, to: post) return newTag } - func getTag(_ name: String, on container: Container) -> EventLoopFuture { - return container.future(tags.first { $0.name == name }) + func getTag(_ name: String) -> EventLoopFuture { + return eventLoop.future(tags.first { $0.name == name }) } func addTag(_ tag: BlogTag, to post: BlogPost) { @@ -114,48 +120,52 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit postTagLinks.append(newLink) } - func deleteTags(for post: BlogPost, on container: Container) -> EventLoopFuture { - return getTags(for: post, on: container).map { tags in + func deleteTags(for post: BlogPost) -> EventLoopFuture { + return getTags(for: post).map { tags in for tag in tags { self.postTagLinks.removeAll { $0.tagID == tag.tagID! && $0.postID == post.blogID! } } } } - func remove(_ tag: BlogTag, from post: BlogPost, on container: Container) -> EventLoopFuture { + func remove(_ tag: BlogTag, from post: BlogPost) -> EventLoopFuture { self.postTagLinks.removeAll { $0.tagID == tag.tagID! && $0.postID == post.blogID! } - return container.future() + return eventLoop.future() } // MARK: - BlogPostRepository + + func `for`(_ request: Request) -> BlogPostRepository { + return self + } - func getAllPostsSortedByPublishDate(includeDrafts: Bool, on container: Container) -> EventLoopFuture<[BlogPost]> { + func getAllPostsSortedByPublishDate(includeDrafts: Bool) -> EventLoopFuture<[BlogPost]> { var sortedPosts = posts.sorted { $0.created > $1.created } if !includeDrafts { sortedPosts = sortedPosts.filter { $0.published } } - return container.future(sortedPosts) + return eventLoop.future(sortedPosts) } - func getAllPostsSortedByPublishDate(includeDrafts: Bool, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { + func getAllPostsSortedByPublishDate(includeDrafts: Bool, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { var sortedPosts = posts.sorted { $0.created > $1.created } if !includeDrafts { sortedPosts = sortedPosts.filter { $0.published } } let startIndex = min(offset, sortedPosts.count) let endIndex = min(offset + count, sortedPosts.count) - return container.future(Array(sortedPosts[startIndex.. EventLoopFuture { + func getAllPostsCount(includeDrafts: Bool) -> EventLoopFuture { var sortedPosts = posts.sorted { $0.created > $1.created } if !includeDrafts { sortedPosts = sortedPosts.filter { $0.published } } - return container.future(sortedPosts.count) + return eventLoop.future(sortedPosts.count) } - func getAllPostsSortedByPublishDate(for user: BlogUser, includeDrafts: Bool, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { + func getAllPostsSortedByPublishDate(for user: BlogUser, includeDrafts: Bool, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { let authorsPosts = posts.filter { $0.author == user.userID } var sortedPosts = authorsPosts.sorted { $0.created > $1.created } if !includeDrafts { @@ -163,22 +173,22 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit } let startIndex = min(offset, sortedPosts.count) let endIndex = min(offset + count, sortedPosts.count) - return container.future(Array(sortedPosts[startIndex.. EventLoopFuture { - return container.future(posts.filter { $0.author == user.userID }.count) + func getPostCount(for user: BlogUser) -> EventLoopFuture { + return eventLoop.future(posts.filter { $0.author == user.userID }.count) } - func getPost(slug: String, on container: Container) -> EventLoopFuture { - return container.future(posts.first { $0.slugUrl == slug }) + func getPost(slug: String) -> EventLoopFuture { + return eventLoop.future(posts.first { $0.slugUrl == slug }) } - func getPost(id: Int, on container: Container) -> EventLoopFuture { - return container.future(posts.first { $0.blogID == id }) + func getPost(id: Int) -> EventLoopFuture { + return eventLoop.future(posts.first { $0.blogID == id }) } - func getSortedPublishedPosts(for tag: BlogTag, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { + func getSortedPublishedPosts(for tag: BlogTag, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { var results = [BlogPost]() guard let tagID = tag.tagID else { fatalError("Tag doesn't exist when it should") @@ -193,10 +203,10 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit let sortedPosts = results.sorted { $0.created > $1.created }.filter { $0.published } let startIndex = min(offset, sortedPosts.count) let endIndex = min(offset + count, sortedPosts.count) - return container.future(Array(sortedPosts[startIndex.. EventLoopFuture { + func getPublishedPostCount(for tag: BlogTag) -> EventLoopFuture { var results = [BlogPost]() guard let tagID = tag.tagID else { fatalError("Tag doesn't exist when it should") @@ -209,26 +219,26 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit results.append(post) } let sortedPosts = results.sorted { $0.created > $1.created }.filter { $0.published } - return container.future(sortedPosts.count) + return eventLoop.future(sortedPosts.count) } - func getPublishedPostCount(for searchTerm: String, on container: Container) -> EventLoopFuture { + func getPublishedPostCount(for searchTerm: String) -> EventLoopFuture { let titleResults = posts.filter { $0.title.contains(searchTerm) } let results = titleResults.sorted { $0.created > $1.created }.filter { $0.published } - return container.future(results.count) + return eventLoop.future(results.count) } - func findPublishedPostsOrdered(for searchTerm: String, on container: Container, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { + func findPublishedPostsOrdered(for searchTerm: String, count: Int, offset: Int) -> EventLoopFuture<[BlogPost]> { let titleResults = posts.filter { $0.title.contains(searchTerm) } let results = titleResults.sorted { $0.created > $1.created }.filter { $0.published } let startIndex = min(offset, results.count) let endIndex = min(offset + count, results.count) - return container.future(Array(results[startIndex.. EventLoopFuture { + func save(_ post: BlogPost) -> EventLoopFuture { self.add(post) - return container.future(post) + return eventLoop.future(post) } func add(_ post: BlogPost) { @@ -238,12 +248,16 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit } } - func delete(_ post: BlogPost, on container: Container) -> EventLoopFuture { + func delete(_ post: BlogPost) -> EventLoopFuture { posts.removeAll { $0.blogID == post.blogID } - return container.future() + return eventLoop.future() } // MARK: - BlogUserRepository + + func `for`(_ request: Request) -> BlogUserRepository { + return self + } func add(_ user: BlogUser) { if (users.first { $0.userID == user.userID } == nil) { @@ -255,40 +269,40 @@ class InMemoryRepository: BlogTagRepository, BlogPostRepository, BlogUserReposit } } - func getUser(id: Int, on container: Container) -> EventLoopFuture { - return container.future(users.first { $0.userID == id }) + func getUser(id: Int) -> EventLoopFuture { + return eventLoop.future(users.first { $0.userID == id }) } - func getAllUsers(on container: Container) -> EventLoopFuture<[BlogUser]> { - return container.future(users) + func getAllUsers() -> EventLoopFuture<[BlogUser]> { + return eventLoop.future(users) } - func getAllUsersWithPostCount(on container: Container) -> EventLoopFuture<[(BlogUser, Int)]> { + func getAllUsersWithPostCount() -> EventLoopFuture<[(BlogUser, Int)]> { let usersWithCount = users.map { user -> (BlogUser, Int) in let postCount = posts.filter { $0.author == user.userID }.count return (user, postCount) } - return container.future(usersWithCount) + return eventLoop.future(usersWithCount) } - func getUser(username: String, on container: Container) -> EventLoopFuture { - return container.future(users.first { $0.username == username }) + func getUser(username: String) -> EventLoopFuture { + return eventLoop.future(users.first { $0.username == username }) } private(set) var userUpdated = false - func save(_ user: BlogUser, on container: Container) -> EventLoopFuture { + func save(_ user: BlogUser) -> EventLoopFuture { self.add(user) userUpdated = true - return container.future(user) + return eventLoop.future(user) } - func delete(_ user: BlogUser, on container: Container) -> EventLoopFuture { + func delete(_ user: BlogUser) -> EventLoopFuture { users.removeAll { $0.userID == user.userID } - return container.future() + return eventLoop.future() } - func getUsersCount(on container: Container) -> EventLoopFuture { - return container.future(users.count) + func getUsersCount() -> EventLoopFuture { + return eventLoop.future(users.count) } } diff --git a/Tests/SteamPressTests/Helpers/TestDataBuilder.swift b/Tests/SteamPressTests/Helpers/TestDataBuilder.swift index 03a2e40b..1dba196b 100644 --- a/Tests/SteamPressTests/Helpers/TestDataBuilder.swift +++ b/Tests/SteamPressTests/Helpers/TestDataBuilder.swift @@ -1,7 +1,6 @@ import Foundation @testable import SteamPress import Vapor -import Authentication struct TestDataBuilder { @@ -51,11 +50,13 @@ struct TestDataBuilder { repository.add(user) return user } - - static func createFutureView(on container: Container) -> EventLoopFuture { - let data = "some HTML".convertToData() - let view = View(data: data) - return container.future(view) + + static func createFutureView(on eventLoop: EventLoop) -> EventLoopFuture { + let string = "Some HTML" + var byteBuffer = ByteBufferAllocator().buffer(capacity: string.count) + byteBuffer.writeString("Some HTML") + let view = View(data: byteBuffer) + return eventLoop.future(view) } } diff --git a/Tests/SteamPressTests/Helpers/TestWorld+Application.swift b/Tests/SteamPressTests/Helpers/TestWorld+Application.swift index 8a42c63c..2c967974 100644 --- a/Tests/SteamPressTests/Helpers/TestWorld+Application.swift +++ b/Tests/SteamPressTests/Helpers/TestWorld+Application.swift @@ -1,9 +1,9 @@ -import SteamPress +@testable import SteamPress import Vapor -import Authentication extension TestWorld { - static func getSteamPressApp(repository: InMemoryRepository, + static func getSteamPressApp(eventLoopGroup: EventLoopGroup, + repository: InMemoryRepository, path: String?, postsPerPage: Int, feedInformation: FeedInformation, @@ -12,62 +12,37 @@ extension TestWorld { enableAuthorPages: Bool, enableTagPages: Bool, passwordHasherToUse: PasswordHasherChoice, - randomNumberGenerator: StubbedRandomNumberGenerator) throws -> Application { - var services = Services.default() - let steampress = SteamPress.Provider( - blogPath: path, - feedInformation: feedInformation, - postsPerPage: postsPerPage, - enableAuthorPages: enableAuthorPages, - enableTagPages: enableTagPages) - try services.register(steampress) - - services.register([BlogTagRepository.self, BlogPostRepository.self, BlogUserRepository.self]) { _ in + randomNumberGenerator: StubbedRandomNumberGenerator) -> Application { + + let application = Application(.testing, .shared(eventLoopGroup)) + + application.steampress.configuration = SteamPressConfiguration(blogPath: path, feedInformation: feedInformation, postsPerPage: postsPerPage, enableAuthorPages: enableAuthorPages, enableTagPages: enableTagPages) + + application.steampress.blogRepositories.use { _ in return repository } - services.register(SteamPressRandomNumberGenerator.self) { _ in - return randomNumberGenerator - } - - services.register(BlogPresenter.self) { _ in + application.steampress.randomNumberGenerators.use { _ in randomNumberGenerator } + + application.middleware.use(BlogRememberMeMiddleware()) + application.middleware.use(SessionsMiddleware(session: application.sessions.driver)) + + application.steampress.blogPresenters.use { _ in return blogPresenter } - - services.register(BlogAdminPresenter.self) { _ in + application.steampress.adminPresenters.use { _ in return adminPresenter } - var middlewareConfig = MiddlewareConfig() - middlewareConfig.use(ErrorMiddleware.self) - middlewareConfig.use(BlogRememberMeMiddleware.self) - middlewareConfig.use(SessionsMiddleware.self) - services.register(middlewareConfig) - - var config = Config.default() - - config.prefer(CapturingBlogPresenter.self, for: BlogPresenter.self) - config.prefer(CapturingAdminPresenter.self, for: BlogAdminPresenter.self) - config.prefer(StubbedRandomNumberGenerator.self, for: SteamPressRandomNumberGenerator.self) - switch passwordHasherToUse { case .real: - config.prefer(BCryptDigest.self, for: PasswordVerifier.self) - config.prefer(BCryptDigest.self, for: PasswordHasher.self) + application.passwords.use(.bcrypt) case .plaintext: - services.register(PasswordHasher.self) { _ in - return PlaintextHasher() - } - config.prefer(PlaintextVerifier.self, for: PasswordVerifier.self) - config.prefer(PlaintextHasher.self, for: PasswordHasher.self) + application.passwords.use(.plaintext) case .reversed: - services.register([PasswordHasher.self, PasswordVerifier.self]) { _ in - return ReversedPasswordHasher() - } - config.prefer(ReversedPasswordHasher.self, for: PasswordVerifier.self) - config.prefer(ReversedPasswordHasher.self, for: PasswordHasher.self) + application.passwords.use(.reversed) } - return try Application(config: config, services: services) + return application } } diff --git a/Tests/SteamPressTests/Helpers/TestWorld+Responses.swift b/Tests/SteamPressTests/Helpers/TestWorld+Responses.swift index 4cb3deab..2c2de8db 100644 --- a/Tests/SteamPressTests/Helpers/TestWorld+Responses.swift +++ b/Tests/SteamPressTests/Helpers/TestWorld+Responses.swift @@ -4,12 +4,11 @@ import Vapor extension TestWorld { func getResponse(to path: String, method: HTTPMethod = .GET, headers: HTTPHeaders = .init(), decodeTo type: T.Type) throws -> T where T: Content { let response = try getResponse(to: path, method: method, headers: headers) - return try response.content.decode(type).wait() + return try response.content.decode(type) } func getResponseString(to path: String, headers: HTTPHeaders = .init()) throws -> String { - let data = try getResponse(to: path, headers: headers).http.body.convertToHTTPBody().data - return String(data: data!, encoding: .utf8)! + return try getResponse(to: path, headers: headers).body.string! } func getResponse(to path: String, method: HTTPMethod = .POST, body: T, loggedInUser: BlogUser? = nil, passwordToLoginWith: String? = nil, headers: HTTPHeaders = .init()) throws -> Response { @@ -24,16 +23,12 @@ extension TestWorld { } func setupRequest(to path: String, method: HTTPMethod = .POST, loggedInUser: BlogUser? = nil, passwordToLoginWith: String? = nil, headers: HTTPHeaders = .init()) throws -> Request { - var request = HTTPRequest(method: method, url: URL(string: path)!, headers: headers) + let request = Request(application: context.app, method: method, url: URI(path: path), headers: headers, on: context.eventLoopGroup.next()) request.cookies["steampress-session"] = try setLoginCookie(for: loggedInUser, password: passwordToLoginWith) - - guard let app = context.app else { - fatalError("App has already been deinitiliased") - } - return Request(http: request, using: app) + return request } - func setLoginCookie(for user: BlogUser?, password: String? = nil) throws -> HTTPCookieValue? { + func setLoginCookie(for user: BlogUser?, password: String? = nil) throws -> HTTPCookies.Value? { if let user = user { let loginData = LoginData(username: user.username, password: password ?? user.password) var loginPath = "/admin/login" @@ -41,7 +36,7 @@ extension TestWorld { loginPath = "/\(path)\(loginPath)" } let loginResponse = try getResponse(to: loginPath, method: .POST, body: loginData) - let sessionCookie = loginResponse.http.cookies["steampress-session"] + let sessionCookie = loginResponse.cookies["steampress-session"] return sessionCookie } else { return nil @@ -49,10 +44,6 @@ extension TestWorld { } func getResponse(to request: Request) throws -> Response { - guard let app = context.app else { - fatalError("App has already been deinitiliased") - } - let responder = try app.make(Responder.self) - return try responder.respond(to: request).wait() + return try context.app.responder.respond(to: request).wait() } } diff --git a/Tests/SteamPressTests/Helpers/TestWorld+TestDataBuilder.swift b/Tests/SteamPressTests/Helpers/TestWorld+TestDataBuilder.swift index e435f528..45a44228 100644 --- a/Tests/SteamPressTests/Helpers/TestWorld+TestDataBuilder.swift +++ b/Tests/SteamPressTests/Helpers/TestWorld+TestDataBuilder.swift @@ -10,7 +10,7 @@ extension TestWorld { for index in 1...count { let data = try createPost(title: "Post \(index)", slugUrl: "post-\(index)", author: author) if let tag = tag { - try context.repository.add(tag, to: data.post) + try context.repository.internalAdd(tag, to: data.post) } } } diff --git a/Tests/SteamPressTests/Helpers/TestWorld.swift b/Tests/SteamPressTests/Helpers/TestWorld.swift index 8bd614a6..ab0f69bc 100644 --- a/Tests/SteamPressTests/Helpers/TestWorld.swift +++ b/Tests/SteamPressTests/Helpers/TestWorld.swift @@ -3,47 +3,39 @@ import Vapor struct TestWorld { - static func create(path: String? = nil, postsPerPage: Int = 10, feedInformation: FeedInformation = FeedInformation(), enableAuthorPages: Bool = true, enableTagPages: Bool = true, passwordHasherToUse: PasswordHasherChoice = .plaintext, randomNumberGenerator: StubbedRandomNumberGenerator = StubbedRandomNumberGenerator(numberToReturn: 666)) throws -> TestWorld { - let repository = InMemoryRepository() - let blogPresenter = CapturingBlogPresenter() - let blogAdminPresenter = CapturingAdminPresenter() - let application = try TestWorld.getSteamPressApp(repository: repository, path: path, postsPerPage: postsPerPage, feedInformation: feedInformation, blogPresenter: blogPresenter, adminPresenter: blogAdminPresenter, enableAuthorPages: enableAuthorPages, enableTagPages: enableTagPages, passwordHasherToUse: passwordHasherToUse, randomNumberGenerator: randomNumberGenerator) - let context = Context(app: application, repository: repository, blogPresenter: blogPresenter, blogAdminPresenter: blogAdminPresenter, path: path) + static func create(path: String? = nil, postsPerPage: Int = 10, feedInformation: FeedInformation = FeedInformation(), enableAuthorPages: Bool = true, enableTagPages: Bool = true, passwordHasherToUse: PasswordHasherChoice = .plaintext, randomNumberGenerator: StubbedRandomNumberGenerator = StubbedRandomNumberGenerator(numberToReturn: 666), websiteURL: String = "https://www.steampress.io") throws -> TestWorld { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + let repository = InMemoryRepository(eventLoop: eventLoopGroup.next()) + let blogPresenter = CapturingBlogPresenter(eventLoop: eventLoopGroup.next()) + let blogAdminPresenter = CapturingAdminPresenter(eventLoop: eventLoopGroup.next()) + let application = TestWorld.getSteamPressApp(eventLoopGroup: eventLoopGroup, repository: repository, path: path, postsPerPage: postsPerPage, feedInformation: feedInformation, blogPresenter: blogPresenter, adminPresenter: blogAdminPresenter, enableAuthorPages: enableAuthorPages, enableTagPages: enableTagPages, passwordHasherToUse: passwordHasherToUse, randomNumberGenerator: randomNumberGenerator) + let context = Context(app: application, repository: repository, blogPresenter: blogPresenter, blogAdminPresenter: blogAdminPresenter, path: path, eventLoopGroup: eventLoopGroup) unsetenv("BLOG_GOOGLE_ANALYTICS_IDENTIFIER") unsetenv("BLOG_SITE_TWITTER_HANDLE") unsetenv("BLOG_DISQUS_NAME") unsetenv("WEBSITE_URL") + setenv("WEBSITE_URL", websiteURL, 1) + try application.boot() return TestWorld(context: context) } - var context: Context + let context: Context init(context: Context) { self.context = context } struct Context { - var app: Application? + let app: Application let repository: InMemoryRepository let blogPresenter: CapturingBlogPresenter let blogAdminPresenter: CapturingAdminPresenter let path: String? + let eventLoopGroup: EventLoopGroup } - // To work around Vapor 3 dodgy lifecycle mess - mutating func tryAsHardAsWeCanToShutdownApplication() throws { - struct ApplicationDidNotGoAway: Error { - var description: String - } - weak var weakApp: Application? = context.app - context.app = nil - var tries = 0 - while weakApp != nil && tries < 10 { - Thread.sleep(forTimeInterval: 0.1) - tries += 1 - } - if weakApp != nil { - throw ApplicationDidNotGoAway(description: "application leak: \(weakApp.debugDescription)") - } + func shutdown() throws { + context.app.shutdown() + try context.eventLoopGroup.syncShutdownGracefully() } } diff --git a/Tests/SteamPressTests/ProviderTests.swift b/Tests/SteamPressTests/ProviderTests.swift index 05d7555a..877ea828 100644 --- a/Tests/SteamPressTests/ProviderTests.swift +++ b/Tests/SteamPressTests/ProviderTests.swift @@ -1,42 +1,26 @@ import XCTest -import SteamPress +@testable import SteamPress import Vapor class ProviderTests: XCTestCase { - func testUsingProviderSetsCorrectServices() throws { - var services = Services.default() - let steampress = SteamPress.Provider() - try services.register(steampress) - - var middlewareConfig = MiddlewareConfig() - middlewareConfig.use(ErrorMiddleware.self) - middlewareConfig.use(BlogRememberMeMiddleware.self) - middlewareConfig.use(SessionsMiddleware.self) - services.register(middlewareConfig) + func testSteamPressSetsCorrectServices() throws { + let app = Application() + app.middleware.use(BlogRememberMeMiddleware()) + app.middleware.use(SessionsMiddleware(session: app.sessions.driver)) - services.register([BlogTagRepository.self, BlogPostRepository.self, BlogUserRepository.self]) { _ in - return InMemoryRepository() + app.steampress.blogRepositories.use { application in + return InMemoryRepository(eventLoop: application.eventLoopGroup.next()) } - let app: Application? = try Application(services: services) - - let numberGenerator = try app!.make(SteamPressRandomNumberGenerator.self) + let numberGenerator = app.steampress.randomNumberGenerators.generator XCTAssertTrue(type(of: numberGenerator) == RealRandomNumberGenerator.self) - let blogPresenter = try app!.make(BlogPresenter.self) + let blogPresenter = app.steampress.blogPresenters.blogPresenter XCTAssertTrue(type(of: blogPresenter) == ViewBlogPresenter.self) - let blogAdminPresenter = try app!.make(BlogAdminPresenter.self) + let blogAdminPresenter = app.steampress.adminPresenters.adminPresenter XCTAssertTrue(type(of: blogAdminPresenter) == ViewBlogAdminPresenter.self) - // Work around Vapor 3 lifecycle mess - weak var weakApp: Application? = app - weakApp = nil - var tries = 0 - while weakApp != nil && tries < 10 { - Thread.sleep(forTimeInterval: 0.1) - tries += 1 - } - XCTAssertNil(weakApp, "application leak: \(weakApp.debugDescription)") + app.shutdown() } } diff --git a/Tests/SteamPressTests/ViewTests/BlogAdminPresenterTests.swift b/Tests/SteamPressTests/ViewTests/BlogAdminPresenterTests.swift index 60d6e3af..8b1c54e0 100644 --- a/Tests/SteamPressTests/ViewTests/BlogAdminPresenterTests.swift +++ b/Tests/SteamPressTests/ViewTests/BlogAdminPresenterTests.swift @@ -5,7 +5,7 @@ import Vapor class BlogAdminPresenterTests: XCTestCase { // MARK: - Properties - var basicContainer: BasicContainer! + var eventLoopGroup: MultiThreadedEventLoopGroup! var presenter: ViewBlogAdminPresenter! var viewRenderer: CapturingViewRenderer! @@ -21,22 +21,16 @@ class BlogAdminPresenterTests: XCTestCase { private static let siteTwitterHandle = "brokenhandsio" private static let disqusName = "steampress" private static let googleAnalyticsIdentifier = "UA-12345678-1" - // MARK: - Overrides override func setUp() { - presenter = ViewBlogAdminPresenter(pathCreator: BlogPathCreator(blogPath: "blog")) - basicContainer = BasicContainer(config: Config.default(), environment: Environment.testing, services: .init(), on: EmbeddedEventLoop()) - basicContainer.services.register(ViewRenderer.self) { _ in - return self.viewRenderer - } - basicContainer.services.register(LongPostDateFormatter.self) - basicContainer.services.register(NumericPostDateFormatter.self) - viewRenderer = CapturingViewRenderer(worker: basicContainer) + eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + viewRenderer = CapturingViewRenderer(eventLoop: eventLoopGroup.next()) + presenter = ViewBlogAdminPresenter(pathCreator: BlogPathCreator(blogPath: "blog"), viewRenderer: viewRenderer, eventLoopGroup: eventLoopGroup, longDateFormatter: LongPostDateFormatter(), numericDateFormatter: NumericPostDateFormatter()) } - override func tearDown() { - try! basicContainer.syncShutdownGracefully() + override func tearDownWithError() throws { + try eventLoopGroup.syncShutdownGracefully() } // MARK: - Tests @@ -45,7 +39,7 @@ class BlogAdminPresenterTests: XCTestCase { func testPasswordViewGivenCorrectParameters() throws { let pageInformation = buildPageInformation(currentPageURL: resetPasswordURL) - _ = presenter.createResetPasswordView(on: basicContainer, errors: nil, passwordError: nil, confirmPasswordError: nil, pageInformation: pageInformation) + _ = presenter.createResetPasswordView(errors: nil, passwordError: nil, confirmPasswordError: nil, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? ResetPasswordPageContext) XCTAssertNil(context.errors) @@ -60,7 +54,7 @@ class BlogAdminPresenterTests: XCTestCase { func testPasswordViewHasCorrectParametersWhenError() throws { let expectedError = "Passwords do not match" let pageInformation = buildPageInformation(currentPageURL: resetPasswordURL) - _ = presenter.createResetPasswordView(on: basicContainer, errors: [expectedError], passwordError: true, confirmPasswordError: true, pageInformation: pageInformation) + _ = presenter.createResetPasswordView(errors: [expectedError], passwordError: true, confirmPasswordError: true, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? ResetPasswordPageContext) XCTAssertEqual(context.errors?.count, 1) @@ -78,7 +72,7 @@ class BlogAdminPresenterTests: XCTestCase { let post = try TestDataBuilder.anyPost(author: currentUser) let pageInformation = buildPageInformation(currentPageURL: adminPageURL) - _ = presenter.createIndexView(on: basicContainer, posts: [draftPost, post], users: [currentUser], errors: nil, pageInformation: pageInformation) + _ = presenter.createIndexView(posts: [draftPost, post], users: [currentUser], errors: nil, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AdminPageContext) @@ -101,7 +95,7 @@ class BlogAdminPresenterTests: XCTestCase { func testAdminPageWithErrors() throws { let expectedError = "You cannot delete yourself!" let pageInformation = buildPageInformation(currentPageURL: adminPageURL) - _ = presenter.createIndexView(on: basicContainer, posts: [], users: [], errors: [expectedError], pageInformation: pageInformation) + _ = presenter.createIndexView(posts: [], users: [], errors: [expectedError], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AdminPageContext) XCTAssertEqual(context.errors?.first, expectedError) @@ -111,7 +105,7 @@ class BlogAdminPresenterTests: XCTestCase { func testCreateUserViewGetsCorrectParameters() throws { let pageInformation = buildPageInformation(currentPageURL: createUserPageURL) - _ = presenter.createUserView(on: basicContainer, editing: false, errors: nil, name: nil, nameError: false, username: nil, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: nil, profilePicture: nil, twitterHandle: nil, biography: nil, tagline: nil, pageInformation: pageInformation) + _ = presenter.createUserView(editing: false, errors: nil, name: nil, nameError: false, username: nil, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: nil, profilePicture: nil, twitterHandle: nil, biography: nil, tagline: nil, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreateUserPageContext) @@ -147,7 +141,7 @@ class BlogAdminPresenterTests: XCTestCase { let expectedTagline = "A son without a father" let pageInformation = buildPageInformation(currentPageURL: createUserPageURL) - _ = presenter.createUserView(on: basicContainer, editing: false, errors: [expectedError], name: expectedName, nameError: false, username: expectedUsername, usernameErorr: false, passwordError: true, confirmPasswordError: true, resetPasswordOnLogin: true, userID: nil, profilePicture: expectedProfilePicture, twitterHandle: expectedTwitterHandler, biography: expectedBiography, tagline: expectedTagline, pageInformation: pageInformation) + _ = presenter.createUserView(editing: false, errors: [expectedError], name: expectedName, nameError: false, username: expectedUsername, usernameErorr: false, passwordError: true, confirmPasswordError: true, resetPasswordOnLogin: true, userID: nil, profilePicture: expectedProfilePicture, twitterHandle: expectedTwitterHandler, biography: expectedBiography, tagline: expectedTagline, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreateUserPageContext) XCTAssertEqual(context.errors?.count, 1) @@ -169,7 +163,7 @@ class BlogAdminPresenterTests: XCTestCase { let expectedError = "No name supplied" let pageInformation = buildPageInformation(currentPageURL: createUserPageURL) - _ = presenter.createUserView(on: basicContainer, editing: false, errors: [expectedError], name: nil, nameError: true, username: nil, usernameErorr: true, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: true, userID: nil, profilePicture: nil, twitterHandle: nil, biography: nil, tagline: nil, pageInformation: pageInformation) + _ = presenter.createUserView(editing: false, errors: [expectedError], name: nil, nameError: true, username: nil, usernameErorr: true, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: true, userID: nil, profilePicture: nil, twitterHandle: nil, biography: nil, tagline: nil, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreateUserPageContext) XCTAssertNil(context.nameSupplied) @@ -180,7 +174,7 @@ class BlogAdminPresenterTests: XCTestCase { func testCreateUserViewForEditing() throws { let pageInformation = buildPageInformation(currentPageURL: editUserPageURL) - _ = presenter.createUserView(on: basicContainer, editing: true, errors: nil, name: currentUser.name, nameError: false, username: currentUser.username, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: currentUser.userID, profilePicture: currentUser.profilePicture, twitterHandle: currentUser.twitterHandle, biography: currentUser.biography, tagline: currentUser.tagline, pageInformation: pageInformation) + _ = presenter.createUserView(editing: true, errors: nil, name: currentUser.name, nameError: false, username: currentUser.username, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: currentUser.userID, profilePicture: currentUser.profilePicture, twitterHandle: currentUser.twitterHandle, biography: currentUser.biography, tagline: currentUser.tagline, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreateUserPageContext) XCTAssertEqual(context.nameSupplied, currentUser.name) XCTAssertFalse(context.nameError) @@ -207,7 +201,7 @@ class BlogAdminPresenterTests: XCTestCase { var errored = false do { - _ = try presenter.createUserView(on: basicContainer, editing: true, errors: [], name: currentUser.name, nameError: false, username: currentUser.username, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: nil, profilePicture: currentUser.profilePicture, twitterHandle: currentUser.twitterHandle, biography: currentUser.biography, tagline: currentUser.tagline, pageInformation: pageInformation).wait() + _ = try presenter.createUserView(editing: true, errors: [], name: currentUser.name, nameError: false, username: currentUser.username, usernameErorr: false, passwordError: false, confirmPasswordError: false, resetPasswordOnLogin: false, userID: nil, profilePicture: currentUser.profilePicture, twitterHandle: currentUser.twitterHandle, biography: currentUser.biography, tagline: currentUser.tagline, pageInformation: pageInformation).wait() } catch { errored = true } @@ -218,7 +212,7 @@ class BlogAdminPresenterTests: XCTestCase { func testCreateBlogPostViewGetsCorrectParameters() throws { let pageInformation = buildPageInformation(currentPageURL: createBlogPageURL) - _ = presenter.createPostView(on: basicContainer, errors: nil, title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: false, post: nil, isDraft: nil, titleError: false, contentsError: false, pageInformation: pageInformation) + _ = presenter.createPostView(errors: nil, title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: false, post: nil, isDraft: nil, titleError: false, contentsError: false, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreatePostPageContext) @@ -246,7 +240,7 @@ class BlogAdminPresenterTests: XCTestCase { let expectedError = "Please enter a title" let pageInformation = buildPageInformation(currentPageURL: createBlogPageURL) - _ = presenter.createPostView(on: basicContainer, errors: [expectedError], title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: false, post: nil, isDraft: nil, titleError: true, contentsError: true, pageInformation: pageInformation) + _ = presenter.createPostView(errors: [expectedError], title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: false, post: nil, isDraft: nil, titleError: true, contentsError: true, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreatePostPageContext) @@ -265,7 +259,7 @@ class BlogAdminPresenterTests: XCTestCase { let tag = "Engineering" let pageInformation = buildPageInformation(currentPageURL: editPostPageURL) - _ = presenter.createPostView(on: basicContainer, errors: nil, title: postToEdit.title, contents: postToEdit.contents, slugURL: postToEdit.slugUrl, tags: [tag], isEditing: true, post: postToEdit, isDraft: false, titleError: false, contentsError: false, pageInformation: pageInformation) + _ = presenter.createPostView(errors: nil, title: postToEdit.title, contents: postToEdit.contents, slugURL: postToEdit.slugUrl, tags: [tag], isEditing: true, post: postToEdit, isDraft: false, titleError: false, contentsError: false, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreatePostPageContext) @@ -295,7 +289,7 @@ class BlogAdminPresenterTests: XCTestCase { var errored = false do { let pageInformation = buildPageInformation(currentPageURL: editPostPageURL) - _ = try presenter.createPostView(on: basicContainer, errors: nil, title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: true, post: nil, isDraft: nil, titleError: false, contentsError: false, pageInformation: pageInformation).wait() + _ = try presenter.createPostView(errors: nil, title: nil, contents: nil, slugURL: nil, tags: nil, isEditing: true, post: nil, isDraft: nil, titleError: false, contentsError: false, pageInformation: pageInformation).wait() } catch { errored = true } @@ -307,7 +301,7 @@ class BlogAdminPresenterTests: XCTestCase { let draftPost = try TestDataBuilder.anyPost(author: currentUser, published: false) let pageInformation = buildPageInformation(currentPageURL: editPostPageURL) - _ = presenter.createPostView(on: basicContainer, errors: nil, title: draftPost.title, contents: draftPost.contents, slugURL: draftPost.slugUrl, tags: nil, isEditing: true, post: draftPost, isDraft: true, titleError: false, contentsError: false, pageInformation: pageInformation) + _ = presenter.createPostView(errors: nil, title: draftPost.title, contents: draftPost.contents, slugURL: draftPost.slugUrl, tags: nil, isEditing: true, post: draftPost, isDraft: true, titleError: false, contentsError: false, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? CreatePostPageContext) XCTAssertTrue(context.draft) diff --git a/Tests/SteamPressTests/ViewTests/BlogPresenterTests.swift b/Tests/SteamPressTests/ViewTests/BlogPresenterTests.swift index 1e0b3a9b..29462b43 100644 --- a/Tests/SteamPressTests/ViewTests/BlogPresenterTests.swift +++ b/Tests/SteamPressTests/ViewTests/BlogPresenterTests.swift @@ -5,7 +5,7 @@ import Vapor class BlogPresenterTests: XCTestCase { // MARK: - Properties - var basicContainer: BasicContainer! + var eventLoopGroup: MultiThreadedEventLoopGroup! var presenter: ViewBlogPresenter! var viewRenderer: CapturingViewRenderer! var testTag: BlogTag! @@ -22,23 +22,17 @@ class BlogPresenterTests: XCTestCase { private static let siteTwitterHandle = "brokenhandsio" private static let disqusName = "steampress" private static let googleAnalyticsIdentifier = "UA-12345678-1" - // MARK: - Overrides override func setUp() { - presenter = ViewBlogPresenter() - basicContainer = BasicContainer(config: Config.default(), environment: Environment.testing, services: .init(), on: EmbeddedEventLoop()) - basicContainer.services.register(ViewRenderer.self) { _ in - return self.viewRenderer - } - basicContainer.services.register(LongPostDateFormatter.self) - basicContainer.services.register(NumericPostDateFormatter.self) - viewRenderer = CapturingViewRenderer(worker: basicContainer) + eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + viewRenderer = CapturingViewRenderer(eventLoop: eventLoopGroup.next()) + presenter = ViewBlogPresenter(viewRenderer: viewRenderer, longDateFormatter: LongPostDateFormatter(), numericDateFormatter: NumericPostDateFormatter(), eventLoopGroup: eventLoopGroup) testTag = BlogTag(id: 1, name: "Tattoine") } - override func tearDown() { - try! basicContainer.syncShutdownGracefully() + override func tearDownWithError() throws { + try eventLoopGroup.syncShutdownGracefully() } // MARK: - Tests @@ -49,7 +43,7 @@ class BlogPresenterTests: XCTestCase { let tags = [BlogTag(id: 0, name: "tag1"), BlogTag(id: 1, name: "tag2")] let pageInformation = buildPageInformation(currentPageURL: allTagsURL) - _ = presenter.allTagsView(on: basicContainer, tags: tags, tagPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allTagsView(tags: tags, tagPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllTagsPageContext) @@ -71,7 +65,7 @@ class BlogPresenterTests: XCTestCase { let tags = [tag1, tag2] let tagPostCount = [0: 5, 1: 20] let pageInformation = buildPageInformation(currentPageURL: allTagsURL) - _ = presenter.allTagsView(on: basicContainer, tags: tags, tagPostCounts: tagPostCount, pageInformation: pageInformation) + _ = presenter.allTagsView(tags: tags, tagPostCounts: tagPostCount, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllTagsPageContext) XCTAssertEqual(context.tags.first?.postCount, 20) @@ -86,7 +80,7 @@ class BlogPresenterTests: XCTestCase { let tags = [tag1, tag2] let tagPostCount = [0: 0, 1: 20] let pageInformation = buildPageInformation(currentPageURL: allTagsURL) - _ = presenter.allTagsView(on: basicContainer, tags: tags, tagPostCounts: tagPostCount, pageInformation: pageInformation) + _ = presenter.allTagsView(tags: tags, tagPostCounts: tagPostCount, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllTagsPageContext) XCTAssertEqual(context.tags[1].tagID, 0) @@ -96,7 +90,7 @@ class BlogPresenterTests: XCTestCase { func testTwitterHandleNotSetOnAllTagsPageIfNotGiven() throws { let tags = [BlogTag(id: 0, name: "tag1"), BlogTag(id: 1, name: "tag2")] let pageInformation = buildPageInformation(currentPageURL: allTagsURL, siteTwitterHandle: nil) - _ = presenter.allTagsView(on: basicContainer, tags: tags, tagPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allTagsView(tags: tags, tagPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllTagsPageContext) XCTAssertNil(context.pageInformation.siteTwitterHandle) @@ -105,7 +99,7 @@ class BlogPresenterTests: XCTestCase { func testDisqusNameNotSetOnAllTagsPageIfNotGiven() throws { let tags = [BlogTag(id: 0, name: "tag1"), BlogTag(id: 1, name: "tag2")] let pageInformation = buildPageInformation(currentPageURL: allTagsURL, disqusName: nil) - _ = presenter.allTagsView(on: basicContainer, tags: tags, tagPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allTagsView(tags: tags, tagPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllTagsPageContext) XCTAssertNil(context.pageInformation.disqusName) @@ -114,7 +108,7 @@ class BlogPresenterTests: XCTestCase { func testGAIdentifierNotSetOnAllTagsPageIfNotGiven() throws { let tags = [BlogTag(id: 0, name: "tag1"), BlogTag(id: 1, name: "tag2")] let pageInformation = buildPageInformation(currentPageURL: allTagsURL, googleAnalyticsIdentifier: nil) - _ = presenter.allTagsView(on: basicContainer, tags: tags, tagPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allTagsView(tags: tags, tagPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllTagsPageContext) XCTAssertNil(context.pageInformation.googleAnalyticsIdentifier) @@ -124,7 +118,7 @@ class BlogPresenterTests: XCTestCase { let tags = [BlogTag(id: 0, name: "tag1"), BlogTag(id: 1, name: "tag2")] let user = TestDataBuilder.anyUser() let pageInformation = buildPageInformation(currentPageURL: allTagsURL, user: user) - _ = presenter.allTagsView(on: basicContainer, tags: tags, tagPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allTagsView(tags: tags, tagPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllTagsPageContext) XCTAssertEqual(context.pageInformation.loggedInUser?.name, user.name) @@ -138,7 +132,7 @@ class BlogPresenterTests: XCTestCase { let user2 = TestDataBuilder.anyUser(id: 1, name: "Han", username: "han") let authors = [user1, user2] let pageInformation = buildPageInformation(currentPageURL: allAuthorsURL) - _ = presenter.allAuthorsView(on: basicContainer, authors: authors, authorPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allAuthorsView(authors: authors, authorPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllAuthorsPageContext) XCTAssertEqual(context.authors.count, 2) @@ -158,7 +152,7 @@ class BlogPresenterTests: XCTestCase { let authors = [user1, user2] let authorPostCount = [0: 1, 1: 20] let pageInformation = buildPageInformation(currentPageURL: allAuthorsURL) - _ = presenter.allAuthorsView(on: basicContainer, authors: authors, authorPostCounts: authorPostCount, pageInformation: pageInformation) + _ = presenter.allAuthorsView(authors: authors, authorPostCounts: authorPostCount, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllAuthorsPageContext) XCTAssertEqual(context.authors.first?.postCount, 20) @@ -173,7 +167,7 @@ class BlogPresenterTests: XCTestCase { let authors = [user1, user2] let authorPostCount = [0: 0, 1: 20] let pageInformation = buildPageInformation(currentPageURL: allAuthorsURL) - _ = presenter.allAuthorsView(on: basicContainer, authors: authors, authorPostCounts: authorPostCount, pageInformation: pageInformation) + _ = presenter.allAuthorsView(authors: authors, authorPostCounts: authorPostCount, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllAuthorsPageContext) XCTAssertEqual(context.authors[1].userID, 0) @@ -182,7 +176,7 @@ class BlogPresenterTests: XCTestCase { func testTwitterHandleNotSetOnAllAuthorsPageIfNotProvided() throws { let pageInformation = buildPageInformation(currentPageURL: allAuthorsURL, siteTwitterHandle: nil) - _ = presenter.allAuthorsView(on: basicContainer, authors: [], authorPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allAuthorsView(authors: [], authorPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllAuthorsPageContext) XCTAssertNil(context.pageInformation.siteTwitterHandle) @@ -190,7 +184,7 @@ class BlogPresenterTests: XCTestCase { func testDisqusNameNotSetOnAllAuthorsPageIfNotProvided() throws { let pageInformation = buildPageInformation(currentPageURL: allAuthorsURL, disqusName: nil) - _ = presenter.allAuthorsView(on: basicContainer, authors: [], authorPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allAuthorsView(authors: [], authorPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllAuthorsPageContext) XCTAssertNil(context.pageInformation.disqusName) @@ -198,7 +192,7 @@ class BlogPresenterTests: XCTestCase { func testGAIdentifierNotSetOnAllAuthorsPageIfNotProvided() throws { let pageInformation = buildPageInformation(currentPageURL: allAuthorsURL, googleAnalyticsIdentifier: nil) - _ = presenter.allAuthorsView(on: basicContainer, authors: [], authorPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allAuthorsView(authors: [], authorPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllAuthorsPageContext) XCTAssertNil(context.pageInformation.googleAnalyticsIdentifier) @@ -207,7 +201,7 @@ class BlogPresenterTests: XCTestCase { func testLoggedInUserPassedToAllAuthorsPageIfProvided() throws { let user = TestDataBuilder.anyUser() let pageInformation = buildPageInformation(currentPageURL: allAuthorsURL, user: user) - _ = presenter.allAuthorsView(on: basicContainer, authors: [], authorPostCounts: [:], pageInformation: pageInformation) + _ = presenter.allAuthorsView(authors: [], authorPostCounts: [:], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? AllAuthorsPageContext) XCTAssertEqual(context.pageInformation.loggedInUser?.name, user.name) @@ -228,7 +222,7 @@ class BlogPresenterTests: XCTestCase { let totalPages = 10 let currentQuery = "?page=2" - _ = presenter.tagView(on: basicContainer, tag: testTag, posts: posts, authors: [user], totalPosts: 3, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation(currentPage: currentPage, totalPages: totalPages, currentQuery: currentQuery)) + _ = presenter.tagView(tag: testTag, posts: posts, authors: [user], totalPosts: 3, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation(currentPage: currentPage, totalPages: totalPages, currentQuery: currentQuery)) let context = try XCTUnwrap(viewRenderer.capturedContext as? TagPageContext) XCTAssertEqual(context.tag.name, testTag.name) @@ -256,7 +250,7 @@ class BlogPresenterTests: XCTestCase { func testNoLoggedInUserPassedToTagPageIfNoneProvided() throws { let pageInformation = buildPageInformation(currentPageURL: tagURL) - _ = presenter.tagView(on: basicContainer, tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.tagView(tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? TagPageContext) XCTAssertNil(context.pageInformation.loggedInUser) @@ -264,7 +258,7 @@ class BlogPresenterTests: XCTestCase { func testDisqusNameNotPassedToTagPageIfNotSet() throws { let pageInformation = buildPageInformation(currentPageURL: tagURL, disqusName: nil) - _ = presenter.tagView(on: basicContainer, tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.tagView(tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? TagPageContext) XCTAssertNil(context.pageInformation.disqusName) @@ -272,7 +266,7 @@ class BlogPresenterTests: XCTestCase { func testTwitterHandleNotPassedToTagPageIfNotSet() throws { let pageInformation = buildPageInformation(currentPageURL: tagURL, siteTwitterHandle: nil) - _ = presenter.tagView(on: basicContainer, tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.tagView(tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? TagPageContext) XCTAssertNil(context.pageInformation.siteTwitterHandle) @@ -280,7 +274,7 @@ class BlogPresenterTests: XCTestCase { func testGAIdentifierNotPassedToTagPageIfNotSet() throws { let pageInformation = buildPageInformation(currentPageURL: tagURL, googleAnalyticsIdentifier: nil) - _ = presenter.tagView(on: basicContainer, tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.tagView(tag: testTag, posts: [], authors: [], totalPosts: 0, pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? TagPageContext) XCTAssertNil(context.pageInformation.googleAnalyticsIdentifier) @@ -306,7 +300,7 @@ class BlogPresenterTests: XCTestCase { let currentQuery = "?page=2" let pageInformation = buildPageInformation(currentPageURL: blogIndexURL) - _ = presenter.indexView(on: basicContainer, posts: [post, post2], tags: tags, authors: [author1, author2], tagsForPosts: [1: [tag1, tag2], 2: [tag1]], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation(currentPage: currentPage, totalPages: totalPages, currentQuery: currentQuery)) + _ = presenter.indexView(posts: [post, post2], tags: tags, authors: [author1, author2], tagsForPosts: [1: [tag1, tag2], 2: [tag1]], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation(currentPage: currentPage, totalPages: totalPages, currentQuery: currentQuery)) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogIndexPageContext) XCTAssertEqual(context.title, "Blog") @@ -353,7 +347,7 @@ class BlogPresenterTests: XCTestCase { func testUserPassedToBlogIndexIfUserPassedIn() throws { let user = TestDataBuilder.anyUser() let pageInformation = buildPageInformation(currentPageURL: blogIndexURL, user: user) - _ = presenter.indexView(on: basicContainer, posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.indexView(posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogIndexPageContext) XCTAssertEqual(context.pageInformation.loggedInUser?.name, user.name) @@ -362,7 +356,7 @@ class BlogPresenterTests: XCTestCase { func testDisqusNameNotPassedToBlogIndexIfNotPassedIn() throws { let pageInformation = buildPageInformation(currentPageURL: blogIndexURL, disqusName: nil) - _ = presenter.indexView(on: basicContainer, posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.indexView(posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogIndexPageContext) XCTAssertNil(context.pageInformation.disqusName) @@ -370,7 +364,7 @@ class BlogPresenterTests: XCTestCase { func testTwitterHandleNotPassedToBlogIndexIfNotPassedIn() throws { let pageInformation = buildPageInformation(currentPageURL: blogIndexURL, siteTwitterHandle: nil) - _ = presenter.indexView(on: basicContainer, posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.indexView(posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogIndexPageContext) XCTAssertNil(context.pageInformation.siteTwitterHandle) @@ -378,7 +372,7 @@ class BlogPresenterTests: XCTestCase { func testGAIdentifierNotPassedToBlogIndexIfNotPassedIn() throws { let pageInformation = buildPageInformation(currentPageURL: blogIndexURL, googleAnalyticsIdentifier: nil) - _ = presenter.indexView(on: basicContainer, posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.indexView(posts: [], tags: [], authors: [], tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogIndexPageContext) XCTAssertNil(context.pageInformation.googleAnalyticsIdentifier) @@ -397,7 +391,7 @@ class BlogPresenterTests: XCTestCase { let query = "page=2" let pageInformation = buildPageInformation(currentPageURL: authorURL) - _ = presenter.authorView(on: basicContainer, author: author, posts: [post1, post2], postCount: 2, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation(currentPage: page, totalPages: totalPages, currentQuery: query)) + _ = presenter.authorView(author: author, posts: [post1, post2], postCount: 2, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation(currentPage: page, totalPages: totalPages, currentQuery: query)) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) XCTAssertEqual(context.author.name, author.name) @@ -426,7 +420,7 @@ class BlogPresenterTests: XCTestCase { let author = TestDataBuilder.anyUser(id: 0) let user = TestDataBuilder.anyUser(id: 1, username: "hans") let pageInformation = buildPageInformation(currentPageURL: authorURL, user: user) - _ = presenter.authorView(on: basicContainer, author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.authorView(author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) XCTAssertEqual(context.pageInformation.loggedInUser?.userID, user.userID) @@ -436,7 +430,7 @@ class BlogPresenterTests: XCTestCase { func testMyProfileFlagSetIfLoggedInUserIsTheSameAsAuthorOnAuthorView() throws { let author = TestDataBuilder.anyUser(id: 0) let pageInformation = buildPageInformation(currentPageURL: authorURL, user: author) - _ = presenter.authorView(on: basicContainer, author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.authorView(author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) XCTAssertTrue(context.myProfile) @@ -445,7 +439,7 @@ class BlogPresenterTests: XCTestCase { func testAuthorViewDoesNotGetDisqusNameIfNotProvided() throws { let author = TestDataBuilder.anyUser() let pageInformation = buildPageInformation(currentPageURL: authorURL, disqusName: nil) - _ = presenter.authorView(on: basicContainer, author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.authorView(author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) XCTAssertNil(context.pageInformation.disqusName) @@ -454,7 +448,7 @@ class BlogPresenterTests: XCTestCase { func testAuthorViewDoesNotGetTwitterHandleIfNotProvided() throws { let author = TestDataBuilder.anyUser() let pageInformation = buildPageInformation(currentPageURL: authorURL, siteTwitterHandle: nil) - _ = presenter.authorView(on: basicContainer, author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.authorView(author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) XCTAssertNil(context.pageInformation.siteTwitterHandle) @@ -463,7 +457,7 @@ class BlogPresenterTests: XCTestCase { func testAuthorViewDoesNotGetGAIdentifierIfNotProvided() throws { let author = TestDataBuilder.anyUser() let pageInformation = buildPageInformation(currentPageURL: authorURL, googleAnalyticsIdentifier: nil) - _ = presenter.authorView(on: basicContainer, author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.authorView(author: author, posts: [], postCount: 0, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) XCTAssertNil(context.pageInformation.googleAnalyticsIdentifier) @@ -478,7 +472,7 @@ class BlogPresenterTests: XCTestCase { let post3 = try TestDataBuilder.anyPost(author: author) post3.blogID = 3 let pageInformation = buildPageInformation(currentPageURL: authorURL) - _ = presenter.authorView(on: basicContainer, author: author, posts: [post1, post2, post3], postCount: 3, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.authorView(author: author, posts: [post1, post2, post3], postCount: 3, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) XCTAssertEqual(context.postCount, 3) @@ -489,7 +483,7 @@ class BlogPresenterTests: XCTestCase { let post1 = try TestDataBuilder.anyPost(author: author, contents: TestDataBuilder.longContents) post1.blogID = 1 let pageInformation = buildPageInformation(currentPageURL: authorURL) - _ = presenter.authorView(on: basicContainer, author: author, posts: [post1], postCount: 1, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) + _ = presenter.authorView(author: author, posts: [post1], postCount: 1, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: buildPaginationInformation()) let context = try XCTUnwrap(viewRenderer.capturedContext as? AuthorPageContext) let characterCount = try XCTUnwrap(context.posts.first?.longSnippet.count) @@ -498,7 +492,7 @@ class BlogPresenterTests: XCTestCase { func testLoginViewGetsCorrectParameters() throws { let pageInformation = buildPageInformation(currentPageURL: loginURL) - _ = presenter.loginView(on: basicContainer, loginWarning: false, errors: nil, username: nil, usernameError: false, passwordError: false, rememberMe: false, pageInformation: pageInformation) + _ = presenter.loginView(loginWarning: false, errors: nil, username: nil, usernameError: false, passwordError: false, rememberMe: false, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? LoginPageContext) XCTAssertNil(context.errors) @@ -515,7 +509,7 @@ class BlogPresenterTests: XCTestCase { func testLoginViewWhenErrored() throws { let expectedError = "Username/password incorrect" let pageInformation = buildPageInformation(currentPageURL: loginURL) - _ = presenter.loginView(on: basicContainer, loginWarning: true, errors: [expectedError], username: "tim", usernameError: true, passwordError: true, rememberMe: true, pageInformation: pageInformation) + _ = presenter.loginView(loginWarning: true, errors: [expectedError], username: "tim", usernameError: true, passwordError: true, rememberMe: true, pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? LoginPageContext) XCTAssertEqual(context.errors?.count, 1) @@ -536,7 +530,7 @@ class BlogPresenterTests: XCTestCase { let pageInformation = buildPageInformation(currentPageURL: searchURL) let paginationInformation = PaginationTagInformation(currentPage: 1, totalPages: 3, currentQuery: "?term=vapor") - _ = presenter.searchView(on: basicContainer, totalResults: 2, posts: [post1, post2], authors: [author], searchTerm: "vapor", tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: paginationInformation) + _ = presenter.searchView(totalResults: 2, posts: [post1, post2], authors: [author], searchTerm: "vapor", tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: paginationInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? SearchPageContext) XCTAssertEqual(context.title, "Search Blog") @@ -562,7 +556,7 @@ class BlogPresenterTests: XCTestCase { func testSearchPageGetsNilIfNoSearchTermProvided() throws { let pageInformation = buildPageInformation(currentPageURL: searchURL) let paginationInformation = PaginationTagInformation(currentPage: 0, totalPages: 0, currentQuery: nil) - _ = presenter.searchView(on: basicContainer, totalResults: 0, posts: [], authors: [], searchTerm: nil, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: paginationInformation) + _ = presenter.searchView(totalResults: 0, posts: [], authors: [], searchTerm: nil, tagsForPosts: [:], pageInformation: pageInformation, paginationTagInfo: paginationInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? SearchPageContext) XCTAssertNil(context.searchTerm) diff --git a/Tests/SteamPressTests/ViewTests/BlogViewTests.swift b/Tests/SteamPressTests/ViewTests/BlogViewTests.swift index e80f0d90..b1a45f04 100644 --- a/Tests/SteamPressTests/ViewTests/BlogViewTests.swift +++ b/Tests/SteamPressTests/ViewTests/BlogViewTests.swift @@ -5,7 +5,7 @@ import Vapor class BlogViewTests: XCTestCase { // MARK: - Properties - var basicContainer: BasicContainer! + var eventLoopGroup: MultiThreadedEventLoopGroup! var presenter: ViewBlogPresenter! var author: BlogUser! var post: BlogPost! @@ -13,36 +13,30 @@ class BlogViewTests: XCTestCase { var pageInformation: BlogGlobalPageInformation! var websiteURL: URL! var currentPageURL: URL! - + // MARK: - Overrides - override func setUp() { - presenter = ViewBlogPresenter() - basicContainer = BasicContainer(config: Config.default(), environment: Environment.testing, services: .init(), on: EmbeddedEventLoop()) - basicContainer.services.register(ViewRenderer.self) { _ in - return self.viewRenderer - } - basicContainer.services.register(LongPostDateFormatter.self) - basicContainer.services.register(NumericPostDateFormatter.self) - viewRenderer = CapturingViewRenderer(worker: basicContainer) - author = TestDataBuilder.anyUser() - author.userID = 1 + override func setUpWithError() throws { + eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + viewRenderer = CapturingViewRenderer(eventLoop: eventLoopGroup.next()) + presenter = ViewBlogPresenter(viewRenderer: viewRenderer, longDateFormatter: LongPostDateFormatter(), numericDateFormatter: NumericPostDateFormatter(), eventLoopGroup: eventLoopGroup) + author = TestDataBuilder.anyUser(id: 1) let createdDate = Date(timeIntervalSince1970: 1584714638) let lastEditedDate = Date(timeIntervalSince1970: 1584981458) - post = try! TestDataBuilder.anyPost(author: author, contents: TestDataBuilder.longContents, creationDate: createdDate, lastEditedDate: lastEditedDate) + post = try TestDataBuilder.anyPost(author: author, contents: TestDataBuilder.longContents, creationDate: createdDate, lastEditedDate: lastEditedDate) websiteURL = URL(string: "https://www.brokenhands.io")! currentPageURL = websiteURL.appendingPathComponent("blog").appendingPathComponent("posts").appendingPathComponent("test-post") pageInformation = BlogGlobalPageInformation(disqusName: "disqusName", siteTwitterHandle: "twitterHandleSomething", googleAnalyticsIdentifier: "GAString....", loggedInUser: author, websiteURL: websiteURL, currentPageURL: currentPageURL, currentPageEncodedURL: currentPageURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) } - override func tearDown() { - try! basicContainer.syncShutdownGracefully() + override func tearDownWithError() throws { + try eventLoopGroup.syncShutdownGracefully() } // MARK: - Tests func testDescriptionOnBlogPostPageIsShortSnippetTextCleaned() throws { - _ = presenter.postView(on: basicContainer, post: post, author: author, tags: [], pageInformation: pageInformation) + _ = presenter.postView(post: post, author: author, tags: [], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogPostPageContext) let expectedDescription = "Welcome to SteamPress!\nSteamPress started out as an idea - after all, I was porting sites and backends over to Swift and would like to have a blog as well. Being early days for Server-Side Swift, and embracing Vapor, there wasn't anything available to put a blog on my site, so I did what any self-respecting engineer would do - I made one! Besides, what better way to learn a framework than build a blog!" @@ -51,7 +45,7 @@ class BlogViewTests: XCTestCase { func testBlogPostPageGetsCorrectParameters() throws { let tag = BlogTag(id: 1, name: "Engineering") - _ = presenter.postView(on: basicContainer, post: post, author: author, tags: [tag], pageInformation: pageInformation) + _ = presenter.postView(post: post, author: author, tags: [tag], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogPostPageContext) @@ -89,7 +83,7 @@ class BlogViewTests: XCTestCase { func testDisqusNameNotPassedToBlogPostPageIfNotPassedIn() throws { let pageInformationWithoutDisqus = BlogGlobalPageInformation(disqusName: nil, siteTwitterHandle: "twitter", googleAnalyticsIdentifier: "google", loggedInUser: author, websiteURL: websiteURL, currentPageURL: currentPageURL, currentPageEncodedURL: currentPageURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) - _ = presenter.postView(on: basicContainer, post: post, author: author, tags: [], pageInformation: pageInformationWithoutDisqus) + _ = presenter.postView(post: post, author: author, tags: [], pageInformation: pageInformationWithoutDisqus) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogPostPageContext) XCTAssertNil(context.pageInformation.disqusName) @@ -97,7 +91,7 @@ class BlogViewTests: XCTestCase { func testTwitterHandleNotPassedToBlogPostPageIfNotPassedIn() throws { let pageInformationWithoutTwitterHandle = BlogGlobalPageInformation(disqusName: "disqus", siteTwitterHandle: nil, googleAnalyticsIdentifier: "google", loggedInUser: author, websiteURL: websiteURL, currentPageURL: currentPageURL, currentPageEncodedURL: currentPageURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) - _ = presenter.postView(on: basicContainer, post: post, author: author, tags: [], pageInformation: pageInformationWithoutTwitterHandle) + _ = presenter.postView(post: post, author: author, tags: [], pageInformation: pageInformationWithoutTwitterHandle) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogPostPageContext) XCTAssertNil(context.pageInformation.siteTwitterHandle) @@ -105,7 +99,7 @@ class BlogViewTests: XCTestCase { func testGAIdentifierNotPassedToBlogPostPageIfNotPassedIn() throws { let pageInformationWithoutGAIdentifier = BlogGlobalPageInformation(disqusName: "disqus", siteTwitterHandle: "twitter", googleAnalyticsIdentifier: nil, loggedInUser: author, websiteURL: websiteURL, currentPageURL: currentPageURL, currentPageEncodedURL: currentPageURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) - _ = presenter.postView(on: basicContainer, post: post, author: author, tags: [], pageInformation: pageInformationWithoutGAIdentifier) + _ = presenter.postView(post: post, author: author, tags: [], pageInformation: pageInformationWithoutGAIdentifier) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogPostPageContext) XCTAssertNil(context.pageInformation.googleAnalyticsIdentifier) @@ -116,7 +110,7 @@ class BlogViewTests: XCTestCase { let urlEncodedName = try XCTUnwrap(tagName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)) let tag = BlogTag(id: 1, name: tagName) - _ = presenter.postView(on: basicContainer, post: post, author: author, tags: [tag], pageInformation: pageInformation) + _ = presenter.postView(post: post, author: author, tags: [tag], pageInformation: pageInformation) let context = try XCTUnwrap(viewRenderer.capturedContext as? BlogPostPageContext) XCTAssertEqual(context.post.tags.first?.urlEncodedName, urlEncodedName)