From 7d2d50e33aae7be340c931f72dcc3da032eff537 Mon Sep 17 00:00:00 2001 From: Andrew Lees <32634907+Andrew-Lees11@users.noreply.github.com> Date: Thu, 13 Dec 2018 13:08:57 +0000 Subject: [PATCH] Feat: Add/Retrieve Codable objects in a session (#54) --- README.md | 34 +++ Sources/KituraSession/SessionState.swift | 53 +++- .../TestCodableSession.swift | 252 ++++++++++++++++++ Tests/KituraSessionTests/TestSession.swift | 72 +++-- Tests/LinuxMain.swift | 2 + 5 files changed, 372 insertions(+), 41 deletions(-) create mode 100644 Tests/KituraSessionTests/TestCodableSession.swift diff --git a/README.md b/README.md index 4b30013..63a3d51 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,40 @@ router.all(middleware: session) ``` First an instance of `RedisStore` is created (see [`KituraSessionRedis`](https://github.com/IBM-Swift/Kitura-Session-Redis) for more information), then an instance of `Session` with the store as parameter is created, and finally it is connected to the desired path. +## Codable Session Example + +The example below defines a `User` struct and a `Router` with the sessions middleware. +The router has a POST route that decodes a `User` instance from the request body + and stores it in the request session using the user's id as the key. +The router has a GET route that reads a user id from the query parameters + and decodes the instance of `User` that is in the session for that id. + +``` +public struct User: Codable { + let id: String + let name: String +} +let router = Router() +router.all(middleware: Session(secret: "secret")) +router.post("/user") { request, response, next in + let user = try request.read(as: User.self) + request.session?[user.id] = user + response.status(.created) + response.send(user) + next() +} +router.get("/user") { request, response, next in + guard let userID = request.queryParameters["userid"] else { + return try response.status(.notFound).end() + } + guard let user: User = request.session?[userID] else { + return try response.status(.internalServerError).end() + } + response.status(.OK) + response.send(user) + next() +} +``` ## Plugins * [Redis store](https://github.com/IBM-Swift/Kitura-Session-Redis) diff --git a/Sources/KituraSession/SessionState.swift b/Sources/KituraSession/SessionState.swift index d64cc19..0ecc5b7 100644 --- a/Sources/KituraSession/SessionState.swift +++ b/Sources/KituraSession/SessionState.swift @@ -98,10 +98,12 @@ public class SessionState { isDirty = true } - /// Retrieve an entry from the session data. + /// Retrieve or store an entry from the session data. /// /// - Parameter key: The key of the entry to retrieve. public subscript(key: String) -> Any? { + // This function allows you to store values which will fail when you try to serialize them to JSON. + // This should be removed in the next major release of Kitura-Session in favour of Codable subscript. get { return state[key] } @@ -110,4 +112,53 @@ public class SessionState { isDirty = true } } + + /// Retrieve or store a Codable entry from the session data. + /// + /// - Parameter key: The Codable key of the entry to retrieve/save. + public subscript(key: String) -> T? { + get { + guard let value = state[key] else { + return nil + } + if let primitive = value as? T { + return primitive + } else { + guard let data = try? JSONSerialization.data(withJSONObject: value) else { + return nil + } + return try? JSONDecoder().decode(T.self, from: data) + } + } + set { + let json: Any + guard let value = newValue else { + state[key] = nil + isDirty = true + return + } + if let data = try? JSONEncoder().encode(value) { + let mirror = Mirror(reflecting: value) + if mirror.displayStyle == .collection { + guard let array = try? JSONSerialization.jsonObject(with: data) as? [Any] else { + return + } + json = array as Any + } else { + guard let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return + } + json = dict as Any + } + } else { + json = value + } + state[key] = json + isDirty = true + } + } } + + + + diff --git a/Tests/KituraSessionTests/TestCodableSession.swift b/Tests/KituraSessionTests/TestCodableSession.swift new file mode 100644 index 0000000..1692720 --- /dev/null +++ b/Tests/KituraSessionTests/TestCodableSession.swift @@ -0,0 +1,252 @@ +/** + * Copyright IBM Corporation 2018 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import Kitura +import KituraNet + +import Foundation +import XCTest + +@testable import KituraSession + +struct CodableSessionTest: Codable, Equatable { + let sessionKey: String + public static func == (lhs: CodableSessionTest, rhs: CodableSessionTest) -> Bool { + return lhs.sessionKey == rhs.sessionKey + } +} +enum SessionEnum: String, Codable { + case one, two +} +let CodableSessionTestArray = ["sessionValue1", "sessionValue2", "sessionValue3"] +let CodableSessionTestDict = ["sessionKey1": "sessionValue1", "sessionKey2": "sessionValue2", "sessionKey3": "sessionValue3"] +let CodableSessionTestCodableArray = [CodableSessionTest(sessionKey: "sessionValue1"), CodableSessionTest(sessionKey: "sessionValue2"), CodableSessionTest(sessionKey: "sessionValue3")] +let CodableSessionTestCodableDict = ["sessionKey1": CodableSessionTest(sessionKey: "sessionValue1"), "sessionKey2": CodableSessionTest(sessionKey: "sessionValue2"), "sessionKey3": CodableSessionTest(sessionKey: "sessionValue3")] + +class TestCodableSession: XCTestCase, KituraTest { + + static var allTests: [(String, (TestCodableSession) -> () throws -> Void)] { + return [ + ("testCodableSessionAddReadArray", testCodableSessionAddReadArray), + ("testCodableSessionAddReadCodable", testCodableSessionAddReadCodable), + ("testCodableSessionAddReadDict", testCodableSessionAddReadDict), + ("testCodableSessionAddReadCodableArray", testCodableSessionAddReadCodableArray), + ] + } + + func testCodableSessionAddReadArray() { + let router = Router() + router.all(middleware: Session(secret: "secret")) + + router.post("/codable") { request, response, next in + request.session?[sessionTestKey] = CodableSessionTestArray + response.status(.created) + next() + } + + router.get("/codable") { request, response, next in + guard let codable: [String] = request.session?[sessionTestKey] else { + return try response.send(status: .notFound).end() + } + response.status(.OK) + response.send(codable) + next() + } + performServerTest(router: router, asyncTasks: { + self.performRequest(method: "post", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) + XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.") + guard let cookieValue = cookie?.value else { + return XCTFail() + } + self.performRequest(method: "get", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)") + do { + guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else { + XCTFail("No response body") + return + } + let decoder = JSONDecoder() + let returnedSession = try decoder.decode([String].self, from: sessionData) + XCTAssertEqual(returnedSession, CodableSessionTestArray) + } catch { + XCTFail("No response body") + } + }, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"]) + }) + }) + } + + func testCodableSessionAddReadCodable() { + let router = Router() + router.all(middleware: Session(secret: "secret")) + + router.post("/codable") { request, response, next in + let codableSession = CodableSessionTest(sessionKey: sessionTestValue) + request.session?[sessionTestKey] = codableSession + response.status(.created) + next() + } + + router.get("/codable") { request, response, next in + let codable: CodableSessionTest? = request.session?[sessionTestKey] + response.status(.OK) + response.send(codable) + next() + } + performServerTest(router: router, asyncTasks: { + self.performRequest(method: "post", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) + XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.") + guard let cookieValue = cookie?.value else { + return XCTFail() + } + self.performRequest(method: "get", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)") + do { + guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else { + XCTFail("No response body") + return + } + let decoder = JSONDecoder() + let returnedSession = try decoder.decode(CodableSessionTest.self, from: sessionData) + XCTAssertEqual(returnedSession.sessionKey, sessionTestValue) + } catch { + XCTFail("No response body") + } + }, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"]) + }) + }) + } + + func testCodableSessionAddReadDict() { + let router = Router() + router.all(middleware: Session(secret: "secret")) + + router.post("/codable") { request, response, next in + request.session?[sessionTestKey] = CodableSessionTestDict + response.status(.created) + next() + } + + router.get("/codable") { request, response, next in + guard let codable: [String: String] = request.session?[sessionTestKey] else { + return try response.send(status: .notFound).end() + } + response.status(.OK) + response.send(codable) + next() + } + performServerTest(router: router, asyncTasks: { + self.performRequest(method: "post", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) + XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.") + guard let cookieValue = cookie?.value else { + return XCTFail() + } + self.performRequest(method: "get", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)") + do { + guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else { + XCTFail("No response body") + return + } + let decoder = JSONDecoder() + let returnedSession = try decoder.decode([String: String].self, from: sessionData) + XCTAssertEqual(returnedSession, CodableSessionTestDict) + } catch { + XCTFail("No response body") + } + }, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"]) + }) + }) + } + + func testCodableSessionAddReadCodableArray() { + let router = Router() + router.all(middleware: Session(secret: "secret")) + + router.post("/codable") { request, response, next in + request.session?[sessionTestKey] = CodableSessionTestCodableArray + response.status(.created) + next() + } + + router.get("/codable") { request, response, next in + guard let codable: [CodableSessionTest] = request.session?[sessionTestKey] else { + return try response.send(status: .notFound).end() + } + response.status(.OK) + response.send(codable) + next() + } + performServerTest(router: router, asyncTasks: { + self.performRequest(method: "post", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + let (cookie, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) + XCTAssertNotNil(cookie, "Cookie \(cookieDefaultName) wasn't found in the response.") + guard let cookieValue = cookie?.value else { + return XCTFail() + } + self.performRequest(method: "get", path: "/codable", callback: { response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + guard let response = response else { + return XCTFail() + } + XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)") + do { + guard let body = try response.readString(), let sessionData = body.data(using: .utf8) else { + XCTFail("No response body") + return + } + let decoder = JSONDecoder() + let returnedSession = try decoder.decode([CodableSessionTest].self, from: sessionData) + XCTAssertEqual(returnedSession.map({$0.sessionKey}), CodableSessionTestCodableArray.map({$0.sessionKey})) + } catch { + XCTFail("No response body") + } + }, headers: ["Cookie": "\(cookieDefaultName)=\(cookieValue)"]) + }) + }) + } +} diff --git a/Tests/KituraSessionTests/TestSession.swift b/Tests/KituraSessionTests/TestSession.swift index c8e5409..0430b4e 100644 --- a/Tests/KituraSessionTests/TestSession.swift +++ b/Tests/KituraSessionTests/TestSession.swift @@ -43,17 +43,15 @@ class TestSession: XCTestCase, KituraTest { let router = setupAdvancedSessionRouter() performServerTest(router: router, asyncTasks: { self.performRequest(method: "get", path: "/1/session", callback: {response in - XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { - return + guard let response = response else { + return XCTFail("ERROR!!! ClientRequest response object was nil") } - XCTAssertEqual(response!.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") - let (cookie1, cookie1Expire) = CookieUtils.cookieFrom(response: response!, named: cookie1Name) - XCTAssert(cookie1 != nil, "Cookie \(cookie1Name) wasn't found in the response.") - guard (cookie1 != nil) else { - return + XCTAssertEqual(response.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") + let (cookie1, cookie1Expire) = CookieUtils.cookieFrom(response: response, named: cookie1Name) + guard let cookie = cookie1 else { + return XCTFail("Cookie \(cookie1Name) wasn't found in the response.") } - XCTAssertEqual(cookie1!.path, "/1", "Path of Cookie \(cookie1Name) is not /1, was \(cookie1!.path)") + XCTAssertEqual(cookie.path, "/1", "Path of Cookie \(cookie1Name) is not /1, was \(cookie.path)") XCTAssertNotNil(cookie1Expire, "\(cookie1Name) had no expiration date. It should have had one") }) }) @@ -78,18 +76,16 @@ class TestSession: XCTestCase, KituraTest { let router = setupBasicSessionRouter() performServerTest(router: router, asyncTasks: { self.performRequest(method: "get", path: "/2/session", callback: {response in - XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { - return + guard let response = response else { + return XCTFail("ERROR!!! ClientRequest response object was nil") } - XCTAssertEqual(response!.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") - let (cookie2, cookie2Expire) = CookieUtils.cookieFrom(response: response!, named: cookieDefaultName) - XCTAssertNotNil(cookie2, "Cookie \(cookieDefaultName) wasn't found in the response.") - guard (cookie2 != nil) else { - return + XCTAssertEqual(response.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") + let (cookie2, cookie2Expire) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) + guard let cookie = cookie2 else { + return XCTFail("Cookie \(cookieDefaultName) wasn't found in the response.") } - XCTAssertNotNil(cookie2!.path, "ERROR!!! cookie2!.path is nil") - XCTAssertEqual(cookie2!.path, "/", "Path of Cookie \(cookieDefaultName) is not /, was \(cookie2!.path)") + XCTAssertNotNil(cookie.path, "ERROR!!! cookie2!.path is nil") + XCTAssertEqual(cookie.path, "/", "Path of Cookie \(cookieDefaultName) is not /, was \(cookie.path)") XCTAssertNil(cookie2Expire, "\(cookieDefaultName) has expiration date. It shouldn't have had one") }) }) @@ -99,24 +95,21 @@ class TestSession: XCTestCase, KituraTest { let router = setupBasicSessionRouter() performServerTest(router: router, asyncTasks: { self.performRequest(method: "post", path: "/3/session", callback: {response in - XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { - return + guard let response = response else { + return XCTFail("ERROR!!! ClientRequest response object was nil") } - let (cookie3, _) = CookieUtils.cookieFrom(response: response!, named: cookieDefaultName) - XCTAssertNotNil(cookie3, "Cookie \(cookieDefaultName) wasn't found in the response.") - guard (cookie3 != nil) else { - return + let (cookie3, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) + guard let cookie3value = cookie3?.value else { + return XCTFail("Cookie \(cookieDefaultName) wasn't found in the response.") } - let cookie3value = cookie3!.value self.performRequest(method: "get", path: "/3/session", callback: {response in XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { - return + guard let response = response else { + return XCTFail() } - XCTAssertEqual(response!.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response!.statusCode)") + XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(response.statusCode)") do { - guard let body = try response!.readString() else { + guard let body = try response.readString() else { XCTFail("No response body") return } @@ -134,10 +127,10 @@ class TestSession: XCTestCase, KituraTest { performServerTest(router: router, asyncTasks: { self.performRequest(method: "post", path: "/3/session", callback: {response in XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { + guard let response = response else { return } - let (cookie3, _) = CookieUtils.cookieFrom(response: response!, named: cookieDefaultName) + let (cookie3, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) XCTAssertNotNil(cookie3, "Cookie \(cookieDefaultName) wasn't found in the response.") guard (cookie3 != nil) else { return @@ -145,10 +138,10 @@ class TestSession: XCTestCase, KituraTest { let cookie3value = cookie3!.value self.performRequest(method: "get", path: "/3/session", callback: {response in XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { + guard let response = response else { return } - XCTAssertEqual(response!.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") + XCTAssertEqual(response.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") }, headers: ["Cookie": "\(cookie1Name)=\(cookie3value); Zxcv=tyuiop"]) }) }) @@ -160,20 +153,20 @@ class TestSession: XCTestCase, KituraTest { performServerTest(router: router, asyncTasks: { self.performRequest(method: "post", path: "/3/session", callback: {response in XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { + guard let response = response else { return } - let (cookie3, _) = CookieUtils.cookieFrom(response: response!, named: cookieDefaultName) + let (cookie3, _) = CookieUtils.cookieFrom(response: response, named: cookieDefaultName) XCTAssertNotNil(cookie3, "Cookie \(cookieDefaultName) wasn't found in the response.") guard (cookie3 != nil) else { return } self.performRequest(method: "get", path: "/3/session", callback: {response in XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") - guard (response != nil) else { + guard let response = response else { return } - XCTAssertEqual(response!.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") + XCTAssertEqual(response.statusCode, HTTPStatusCode.noContent, "Session route did not match single path request") }, headers: ["Cookie": "\(cookieDefaultName)=lalala; Zxcv=tyuiop"]) }) }) @@ -194,7 +187,6 @@ class TestSession: XCTestCase, KituraTest { router.post("/3/session") {request, response, next in request.session?[sessionTestKey] = sessionTestValue response.status(.noContent) - next() } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 1b266bf..7637cb4 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -21,4 +21,6 @@ import XCTest XCTMain([ testCase(TestSession.allTests), + testCase(TestCodableSession.allTests), + testCase(TestTypeSafeSession.allTests) ])