Skip to content

Commit

Permalink
Response.cookie
Browse files Browse the repository at this point in the history
  • Loading branch information
hnry committed Aug 12, 2016
1 parent 2a4b8c1 commit 9301172
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 9 deletions.
34 changes: 30 additions & 4 deletions docs/api/request-response-map.md
Expand Up @@ -27,17 +27,21 @@ These properties should not be modified except under very rare circumstances whe

Additional properties can be added. For example it is common to have the request body be parsed and included as a body property, (this is provided via middleware).

`headers` is as node.js delivers it, therefore all the header names are lower cased.

# response map

A response map is a map (object literal) that has __at least__ two properties and corresponding types, `status` (number), `headers` (object).

The `headers` property is a object literal representing key, value of HTTP headers.

It can _optionally_also have a `body` property which can be of type undefined, string, Buffer, or a readable stream.
It can _optionally_ also have a `body` property which can be of type undefined, string, Buffer, or stream (readable).

A response map is considered a simplified representation of what is intended to be written back for a http request.

A response map is considered a representation of what is intended to be written back for a http request.
Because of it's simple form, it provides an easy outlet for testing the proper response of functions.

##### Common example:
#### Example:
```js
{
status: 200,
Expand All @@ -56,4 +60,26 @@ This would write back a 200 response with the Content-Type set with the body `"<
headers: {}
}
```
This is a valid response, as only a `status` and `headers` are needed, and `headers` can be empty object literal too.
This is a valid response, as only a `status` and `headers` are needed, and `headers` can be an empty object (which just means no headers).


### Note about headers

Headers share exactly the same concept as headers in Node.js. That is, the header names should be unique regardless of case. And to set multiple values of the same header, use an array.

Example:
```js
{
status: 200,
headers: {
"Set-Cookie": ["type=ninja", "language=javascript"]
}
}
```

Header names __should__ (meaning strongly recommended) be capitalized per word. For example `"Content-Type"` and not `"Content-type"`.

There is a [Response](https://github.com/spirit-js/spirit/blob/master/docs/api/Response.md) object class that helps when working with response maps. It provides chainable helper functions to make it easier to work with headers. (You don't normally use it directly, but instead it's returned from `spirit.node.response`, `spirit.node.file_response`, `spirit.node.redirect`, `spirit.node.err_response`)

A Response is a valid response map, as it has `status`, `headers`, the optional `body` property.

120 changes: 119 additions & 1 deletion spec/http/response_class-spec.js
Expand Up @@ -214,7 +214,125 @@ describe("response-class", () => {
})

describe("cookie", () => {
it("")
it("sets a cookie in headers", () => {
const r = new Response()
r.cookie("test", "123")
expect(r.headers["Set-Cookie"]).toEqual(["test=123"])

const t = r.cookie("another", "hi")
expect(r.headers["Set-Cookie"]).toEqual([
"test=123",
"another=hi"
])

expect(t).toBe(r) // returns this
})

it("non-string values are converted to string", () => {
const r = new Response()
r.cookie("test", 123)
expect(r.headers["Set-Cookie"]).toEqual(["test=123"])
})

it("duplicates are not handled in any way", () => {
const r = new Response()
r.cookie("test", 123)
expect(r.headers["Set-Cookie"]).toEqual(["test=123"])
r.cookie("test", 123)
expect(r.headers["Set-Cookie"]).toEqual(["test=123", "test=123"])
})

it("path option", () => {
const r = new Response()
r.cookie("test", "123", { path: "/" })
expect(r.headers["Set-Cookie"]).toEqual(["test=123; Path=/"])
r.cookie("aBc", "123", { path: "/test" })
expect(r.headers["Set-Cookie"]).toEqual([
"test=123; Path=/",
"aBc=123; Path=/test"
])
})

it("domain, httponly, maxage, secure options", () => {
const r = new Response()
// domain
r.cookie("test", "123", { domain: "test.com" })
expect(r.headers["Set-Cookie"]).toEqual(["test=123; Domain=test.com"])
// httponly
r.cookie("a", "b", { httponly: true })
expect(r.headers["Set-Cookie"][1]).toBe("a=b; HttpOnly")
// maxage
r.cookie("a", "b", { maxage: "2000" })
expect(r.headers["Set-Cookie"][2]).toBe("a=b; Max-Age=2000")
// secure
r.cookie("a", "b", { secure: true })
expect(r.headers["Set-Cookie"][3]).toBe("a=b; Secure")

// all together
r.cookie("a", "b", {
httponly: true,
secure: true,
maxage: 4000,
domain: "test.com"
})
expect(r.headers["Set-Cookie"][4]).toBe("a=b; Domain=test.com; Max-Age=4000; Secure; HttpOnly")
})

it("expires option", () => {
// date object ok
const date = new Date()
const r = new Response()
r.cookie("c", "d", { expires: date })
expect(r.headers["Set-Cookie"][0]).toBe("c=d; Expires=" + date.toUTCString())

// string ok
r.cookie("c", "d", { expires: "hihihi" })
expect(r.headers["Set-Cookie"][1]).toBe("c=d; Expires=hihihi")
})

describe("deleting cookies", () => {
it("setting an undefined value means to delete any previous cookie matching the same name & path", () => {
const r = new Response()
r.cookie("test", "123", { path: "/" })
expect(r.headers["Set-Cookie"]).toEqual(["test=123; Path=/"])
r.cookie("test", undefined, { path: "/" })
expect(r.headers["Set-Cookie"]).toEqual([])

r.cookie("test", "123", { path: "/" })
r.cookie("test", "123", { path: "/" })
r.cookie("test", "123", { path: "/" })
expect(r.headers["Set-Cookie"].length).toBe(3)
const t = r.cookie("test", { path: "/" })
expect(r.headers["Set-Cookie"]).toEqual([])

expect(t).toBe(r) // returns this
})
})

it("path always defaults to '/' and options do not affect matching", () => {
const r = new Response()
r.cookie("test", "123", { path: "/", httponly: true })
r.cookie("test", "123")
r.cookie("test", "123", { path: "/", domain: "hi.com" })
expect(r.headers["Set-Cookie"].length).toBe(3)
r.cookie("test")
expect(r.headers["Set-Cookie"]).toEqual([])
})

it("doesn't touch non-matching cookies", () => {
const r = new Response()
r.cookie("test", "123", { path: "/" })
r.cookie("test", "123", { path: "/test" })
r.cookie("tesT", "123", { path: "/" })
expect(r.headers["Set-Cookie"].length).toBe(3)
r.cookie("test", undefined)
expect(r.headers["Set-Cookie"].length).toBe(2)
expect(r.headers["Set-Cookie"]).toEqual([
"test=123; Path=/test",
"tesT=123; Path=/"
])
})

})

describe("charset", () => {
Expand Down
105 changes: 101 additions & 4 deletions src/http/response-class.js
Expand Up @@ -101,12 +101,109 @@ class Response {
return this.set("Content-Type", t + charset)
}

cookie() {
return this
_clear_cookies(cookies, name, path) {
path = path || "/"
return cookies.filter((ck) => {
// get cookie name
const _name = ck.slice(0, ck.indexOf("="))
let _path = "/"

if (_name === name) {
// if name matches, check path
const ck_lower = ck.toLowerCase()
const _begin = ck_lower.indexOf("path=")

if (_begin !== -1) {
ck = ck.slice(_begin + 5)
const _end = ck.indexOf(";")

if (_end === -1) {
_path = ck
} else {
_path = ck.slice(0, _end)
}
}

return _path !== path
}

return true
})
}

charset() {
return this
/**
* Sets a cookie to headers, if the header already exists
* It will append to the array (and be converted to one, if
* it isn't already one)
*
* No encoding is done, if needed, encode the value before
* calling this method
*
* If value is undefined, then the cookie will not be set
* And if it already exists, then all instances of it
* will be removed
*
* Possible duplicate cookies of the same name & path
* are not handled
* NOTE: cookies are considered unique to it's name & path
*
* Options: {
* path {string}
* domain {string}
* httponly {boolean}
* maxage {string}
* secure {boolean}
* expires {Date}
* }
*
* @param {string} name - cookie name
* @param {string} value - cookie value
* @param {object} opts - an object of options
* @return {this}
*/
cookie(name, value, opts) {
// get current cookies (as an array)
let curr_cookies = this.get("Set-Cookie")
if (curr_cookies === undefined) {
curr_cookies = []
} else {
if (Array.isArray(curr_cookies) === false) {
curr_cookies = [curr_cookies]
}
}

// optional arguments & default values
if (typeof value === "object") {
opts = value
value = undefined
} else if (opts === undefined) {
opts = {}
}

// is this for deletion?
if (value === undefined) {
const _filtered_cookies = this._clear_cookies(curr_cookies, name, opts.path)
return this.set("Set-Cookie", _filtered_cookies)
}

// begin constructing cookie string
value = [value]
// * set optional values *
if (opts.path) value.push("Path=" + opts.path)
if (opts.domain) value.push("Domain=" + opts.domain)
if (opts.maxage) value.push("Max-Age=" + opts.maxage)
if (opts.secure === true) value.push("Secure")
if (opts.expires) {
if (typeof opts.expires.toUTCString === "function") {
value.push("Expires=" + opts.expires.toUTCString())
} else {
value.push("Expires=" + opts.expires)
}
}
if (opts.httponly === true) value.push("HttpOnly")

curr_cookies.push(name + "=" + value.join("; "))
return this.set("Set-Cookie", curr_cookies)
}

location(url) {
Expand Down

0 comments on commit 9301172

Please sign in to comment.