Skip to content

Commit 293854c

Browse files
committed
Chapter 11 is ready
1 parent c97d02b commit 293854c

File tree

10 files changed

+282
-16
lines changed

10 files changed

+282
-16
lines changed

Chapter 11/myProject/.env.testing

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DB_URL=postgres://myuser:mypass@localhost:5432/mydb
2+
APP_URL=http://localhost:8080
3+
AWS_KEY=<key>
4+
AWS_SECRET=<secret>

Chapter 11/myProject/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Chapter 11/myProject/Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ let package = Package(
1919
.package(url: "https://github.com/binarybirds/view-kit.git", from: "1.0.0"),
2020
.package(url: "https://github.com/binarybirds/content-api.git", from: "1.0.0"),
2121
.package(url: "https://github.com/binarybirds/viper-kit.git", from: "1.0.0"),
22+
//...
23+
.package(url: "https://github.com/binarybirds/spec.git", from: "1.0.0"),
2224
],
2325
targets: [
2426
.target(name: "App", dependencies: [
@@ -37,7 +39,7 @@ let package = Package(
3739
.target(name: "Run", dependencies: ["App"]),
3840
.testTarget(name: "AppTests", dependencies: [
3941
.target(name: "App"),
40-
.product(name: "XCTVapor", package: "vapor"),
42+
.product(name: "Spec", package: "spec"),
4143
])
4244
]
4345
)

Chapter 11/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,24 @@ import ContentApi
44

55
struct BlogPostApiController: ApiController {
66
typealias Model = BlogPostModel
7+
8+
func setValidCategory(req: Request, model: Model, categoryId: String) -> EventLoopFuture<Model> {
9+
guard let uuid = UUID(uuidString: categoryId) else {
10+
return req.eventLoop.future(error: Abort(.badRequest))
11+
}
12+
return BlogCategoryModel.find(uuid, on: req.db)
13+
.unwrap(or: Abort(.badRequest))
14+
.map { category in
15+
model.$category.id = category.id!
16+
return model
17+
}
18+
}
19+
20+
func beforeCreate(req: Request, model: Model, content: Model.CreateContent) -> EventLoopFuture<Model> {
21+
self.setValidCategory(req: req, model: model, categoryId: content.categoryId)
22+
}
23+
24+
func beforeUpdate(req: Request, model: Model, content: Model.UpdateContent) -> EventLoopFuture<Model> {
25+
self.setValidCategory(req: req, model: model, categoryId: content.categoryId)
26+
}
727
}

Chapter 11/myProject/Sources/App/Modules/Blog/Models/BlogPostModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ extension BlogPostModel: ApiRepresentable {
107107
var excerpt: String
108108
var date: Date
109109
var content: String
110+
var categoryId: String
110111
}
111112

112113
struct PatchContent: ValidatableContent {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
@testable import App
2+
import XCTVapor
3+
import Fluent
4+
import FluentSQLiteDriver
5+
6+
extension XCTApplicationTester {
7+
@discardableResult public func test<T>(
8+
_ method: HTTPMethod,
9+
_ path: String,
10+
headers: HTTPHeaders = [:],
11+
content: T,
12+
afterResponse: (XCTHTTPResponse) throws -> () = { _ in }
13+
) throws -> XCTApplicationTester where T: Content {
14+
try test(method, path, headers: headers, beforeRequest: { req in
15+
try req.content.encode(content)
16+
}, afterResponse: afterResponse)
17+
}
18+
}
19+
20+
open class AppTestCase: XCTestCase {
21+
22+
func createTestApp() throws -> Application {
23+
let app = Application(.testing)
24+
try configure(app)
25+
app.databases.use(.sqlite(.memory), as: .sqlite)
26+
app.databases.default(to: .sqlite)
27+
try app.autoMigrate().wait()
28+
return app
29+
}
30+
31+
func getApiToken(_ app: Application) throws -> String {
32+
struct UserLoginRequest: Content {
33+
let email: String
34+
let password: String
35+
}
36+
struct UserTokenResponse: Content {
37+
let id: String
38+
let value: String
39+
}
40+
41+
let userBody = UserLoginRequest(email: "[email protected]",
42+
password: "ChangeMe1")
43+
44+
var token: String?
45+
46+
try app.test(.POST, "/api/user/login", beforeRequest: { req in
47+
try req.content.encode(userBody)
48+
}, afterResponse: { res in
49+
XCTAssertContent(UserTokenResponse.self, res) { content in
50+
token = content.value
51+
}
52+
})
53+
guard let t = token else {
54+
XCTFail("Login failed")
55+
throw Abort(.unauthorized)
56+
}
57+
return t
58+
}
59+
}

Chapter 11/myProject/Tests/AppTests/AppTests.swift

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
@testable import App
2+
import XCTVapor
3+
import Fluent
4+
5+
final class BlogCategoryApiTests: AppTestCase {
6+
7+
func testGetCategories() throws {
8+
let app = try self.createTestApp()
9+
let token = try self.getApiToken(app)
10+
defer { app.shutdown() }
11+
12+
let headers = HTTPHeaders([("Authorization", "Bearer \(token)")])
13+
14+
try app
15+
//.testable(method: .inMemory)
16+
.testable(method: .running(port: 8081))
17+
.test(.GET, "/api/blog/categories", headers: headers) { res in
18+
XCTAssertEqual(res.status, .ok)
19+
let contentType = try XCTUnwrap(res.headers.contentType)
20+
XCTAssertEqual(contentType, .json)
21+
XCTAssertContent(Page<BlogCategoryModel.ListItem>.self, res) { content in
22+
XCTAssertEqual(content.metadata.total, 2)
23+
}
24+
}
25+
}
26+
27+
func testCreateCategory() throws {
28+
let app = try self.createTestApp()
29+
let token = try self.getApiToken(app)
30+
defer { app.shutdown() }
31+
32+
let headers = HTTPHeaders([("Authorization", "Bearer \(token)")])
33+
34+
let newCategory = BlogCategoryModel.CreateContent(title: "Test category")
35+
36+
try app.test(.POST, "/api/blog/categories",
37+
headers: headers,
38+
content: newCategory) { res in
39+
XCTAssertEqual(res.status, .ok)
40+
let contentType = try XCTUnwrap(res.headers.contentType)
41+
XCTAssertEqual(contentType, .json)
42+
XCTAssertContent(BlogCategoryModel.CreateContent.self, res) { content in
43+
XCTAssertEqual(content.title, newCategory.title)
44+
}
45+
}
46+
}
47+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
@testable import App
2+
import Spec
3+
import Fluent
4+
5+
final class BlogPostApiTests: AppTestCase {
6+
//...
7+
8+
func testGetPosts() throws {
9+
let app = try self.createTestApp()
10+
let token = try self.getApiToken(app)
11+
defer { app.shutdown() }
12+
13+
try app
14+
.describe("Blog posts should return ok")
15+
.get("/api/blog/posts")
16+
.bearerToken(token)
17+
.expect(.ok)
18+
.expect(.json)
19+
.expect(Page<BlogPostModel.ListItem>.self) { content in
20+
print(content)
21+
}
22+
.test()
23+
}
24+
25+
func testCreatePost() throws {
26+
let app = try self.createTestApp()
27+
let token = try self.getApiToken(app)
28+
defer { app.shutdown() }
29+
30+
let category = try BlogCategoryModel.query(on: app.db).first().wait()
31+
guard let c = category else {
32+
XCTFail("Missing default category")
33+
throw Abort(.notFound)
34+
}
35+
36+
let newPost = BlogPostModel.CreateContent(title: "Dummy post",
37+
slug: "dummy-slug",
38+
image: "/dummy/image.jpg",
39+
excerpt: "Lorem ipsum",
40+
date: Date(),
41+
content: "Lorem ipsum",
42+
categoryId: c.id!.uuidString)
43+
44+
//...
45+
var total = 1
46+
47+
try app
48+
.describe("Get original posts count")
49+
.get("/api/blog/posts")
50+
.bearerToken(token)
51+
.expect(.ok)
52+
.expect(.json)
53+
.expect(Page<BlogPostModel.ListItem>.self) { content in
54+
total += content.metadata.total
55+
}
56+
.test()
57+
58+
try app
59+
.describe("Create post should return ok")
60+
.post("/api/blog/posts")
61+
.body(newPost)
62+
.bearerToken(token)
63+
.expect(.ok)
64+
.expect(.json)
65+
.expect(BlogPostModel.GetContent.self) { content in
66+
XCTAssertEqual(content.title, newPost.title)
67+
}
68+
.test()
69+
70+
try app
71+
.describe("Blog post count should be correct")
72+
.get("/api/blog/posts")
73+
.bearerToken(token)
74+
.expect(.ok)
75+
.expect(.json)
76+
.expect(Page<BlogPostModel.ListItem>.self) { content in
77+
XCTAssertEqual(content.metadata.total, total)
78+
}
79+
.test()
80+
}
81+
82+
func testUpdatePost() throws {
83+
let app = try self.createTestApp()
84+
let token = try self.getApiToken(app)
85+
defer { app.shutdown() }
86+
87+
let post = try BlogPostModel
88+
.query(on: app.db)
89+
.with(\.$category)
90+
.first()
91+
.unwrap(or: Abort(.notFound))
92+
.wait()
93+
94+
let suffix = " updated"
95+
96+
let newPost = BlogPostModel.UpdateContent(title: post.title + suffix,
97+
slug: post.slug + suffix,
98+
image: post.image + suffix,
99+
excerpt: post.excerpt + suffix,
100+
date: post.date,
101+
content: post.content + suffix,
102+
categoryId: post.category.id!.uuidString)
103+
104+
try app
105+
.describe("Create post should return ok")
106+
.put("/api/blog/posts/\(post.id!.uuidString)")
107+
.body(newPost)
108+
.bearerToken(token)
109+
.expect(.ok)
110+
.expect(.json)
111+
.expect(BlogPostModel.GetContent.self) { content in
112+
XCTAssertEqual(content.id, post.id)
113+
XCTAssertEqual(content.title, newPost.title)
114+
XCTAssertEqual(content.slug, newPost.slug)
115+
XCTAssertEqual(content.image, newPost.image)
116+
XCTAssertEqual(content.excerpt, newPost.excerpt)
117+
XCTAssertEqual(content.content, newPost.content)
118+
}
119+
.test()
120+
}
121+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@testable import App
2+
import XCTVapor
3+
4+
final class FrontendTests: XCTestCase {
5+
6+
func testHomePage() throws {
7+
let app = Application(.testing)
8+
defer { app.shutdown() }
9+
try configure(app)
10+
11+
try app.testable(method: .inMemory).test(.GET, "") { res in
12+
XCTAssertEqual(res.status, .ok)
13+
let contentType = try XCTUnwrap(res.headers.contentType)
14+
XCTAssertEqual(contentType, .html)
15+
XCTAssertTrue(res.body.string.contains("myPage - Home"))
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)