tynn 2.0.0.alpha → 2.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,10 +9,7 @@ class Tynn
9
9
  #
10
10
  # [X-Frame-Options]
11
11
  # Provides {Clickjacking}[https://www.owasp.org/index.php/Clickjacking]
12
- # protection. Defaults to <tt>"SAMEORIGIN"</tt>.
13
- #
14
- # [X-Permitted-Cross-Domain-Policies]
15
- # Restricts Adobe Flash Player's access to data. Defaults to <tt>"none"</tt>.
12
+ # protection. Defaults to <tt>"deny"</tt>.
16
13
  #
17
14
  # [X-XSS-Protection]
18
15
  # Enables the XSS protection filter built into IE, Chrome and Safari.
@@ -29,8 +26,7 @@ class Tynn
29
26
  module SecureHeaders
30
27
  HEADERS = {
31
28
  "X-Content-Type-Options" => "nosniff",
32
- "X-Frame-Options" => "SAMEORIGIN",
33
- "X-Permitted-Cross-Domain-Policies" => "none",
29
+ "X-Frame-Options" => "deny",
34
30
  "X-XSS-Protection" => "1; mode=block"
35
31
  }.freeze # :nodoc:
36
32
 
@@ -10,8 +10,8 @@ class Tynn
10
10
  # Tynn.plugin(Tynn::Session, secret: "__change_me_not_secure__")
11
11
  #
12
12
  # Tynn.define do
13
- # on("login") do
14
- # post do
13
+ # on "login" do
14
+ # on post do
15
15
  # # ...
16
16
  #
17
17
  # session[:user_id] = user.id
@@ -46,6 +46,14 @@ class Tynn
46
46
  # If <tt>true</tt>, sets the <tt>Secure</tt> flag. This tells the browser
47
47
  # to only transmit the cookie over HTTPS. Defaults to <tt>false</tt>.
48
48
  #
49
+ # [same_site]
50
+ # Disables third-party usage for cookies. There are two possible values
51
+ # <tt>:Lax</tt> and <tt>:Strict</tt>. In <tt>Strict</tt> mode, the cookie
52
+ # is restrain to any cross-site usage; in <tt>Lax</tt> mode, some cross-site
53
+ # usage is allowed. Defaults to <tt>:Lax</tt>. If <tt>nil</tt> is passed,
54
+ # the flag is not included. Check this article[http://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/]
55
+ # for more information.
56
+ #
49
57
  # [expire_after]
50
58
  # The lifespan of the cookie. If <tt>nil</tt>, the session cookie is temporary
51
59
  # and is no retained after the browser is closed. Defaults to <tt>nil</tt>.
@@ -58,7 +66,8 @@ class Tynn
58
66
  # secret: ENV["SESSION_SECRET"],
59
67
  # expire_after: 36_000, # seconds
60
68
  # httponly: true,
61
- # secure: true
69
+ # secure: true,
70
+ # same_site: :Strict
62
71
  # )
63
72
  #
64
73
  module Session
@@ -68,7 +77,7 @@ class Tynn
68
77
  secret = options[:secret]
69
78
 
70
79
  if secret.nil?
71
- raise <<~MSG
80
+ raise Tynn::Error, <<~MSG
72
81
  No secret option provided to Tynn::Session.
73
82
 
74
83
  Tynn::Session uses a secret token to sign the cookie data, thus
@@ -86,7 +95,7 @@ class Tynn
86
95
  end
87
96
 
88
97
  if secret.length < SECRET_MIN_LENGTH
89
- raise <<~MSG
98
+ raise Tynn::Error, <<~MSG
90
99
  The secret provided is shorter than the minimum length.
91
100
 
92
101
  Make sure the secret is long and all random. You can generate a
@@ -13,21 +13,24 @@ class Tynn
13
13
  # 3. Setting the <tt>secure</tt> flag on cookies. This tells the browser to
14
14
  # only transmit them over HTTPS.
15
15
  #
16
- # You can configure HSTS passing a <tt>:hsts</tt> option. The following options
17
- # are supported:
18
- #
19
- # - *:expires* - The time, in seconds, that the browser access the site only
20
- # by HTTPS. Defaults to 180 days.
21
- #
22
- # - *:subdomains* - If this is <tt>true</tt>, the rule applies to all the
23
- # site's subdomains as well. Defaults to <tt>true</tt>.
24
- #
25
- # - *:preload* - A limitation of HSTS is that the initial request remains
26
- # unprotected if it uses HTTP. The same applies to the first request after
27
- # the activity period specified by <tt>max-age</tt>. Modern browsers implements
28
- # a "HSTS preload list", which contains known sites supporting HSTS. If you
29
- # would like to include your website into the list, set this option to
30
- # <tt>true</tt> and submit your domain to this form[https://hstspreload.appspot.com/].
16
+ # You can configure HSTS passing through the <tt>:hsts</tt> option.
17
+ # The following options are supported:
18
+ #
19
+ # [expires]
20
+ # The time, in seconds, that the browser access the site only by HTTPS.
21
+ # Defaults to 180 days.
22
+ #
23
+ # [subdomains]
24
+ # If this is <tt>true</tt>, the rule applies to all the site's subdomains as
25
+ # well. Defaults to <tt>true</tt>.
26
+ #
27
+ # [preload]
28
+ # A limitation of HSTS is that the initial request remains unprotected if it
29
+ # uses HTTP. The same applies to the first request after the activity period
30
+ # specified by <tt>max-age</tt>. Modern browsers implements a "HSTS preload
31
+ # list", which contains known sites supporting HSTS. If you would like to
32
+ # include your website into the list, set this option to <tt>true</tt> and
33
+ # submit your domain to this form[https://hstspreload.appspot.com/].
31
34
  # Supported by Chrome, Firefox, IE11+ and IE Edge.
32
35
  #
33
36
  # To disable HSTS, you will need to tell the browser to expire it immediately.
@@ -74,7 +77,7 @@ class Tynn
74
77
  end
75
78
 
76
79
  def call(env)
77
- request = Rack::Request.new(env)
80
+ request = Tynn::Request.new(env)
78
81
 
79
82
  return redirect_to_https(request) unless request.ssl?
80
83
 
@@ -7,16 +7,16 @@ class Tynn
7
7
  # require "tynn/test"
8
8
  #
9
9
  # Tynn.define do
10
- # root do
10
+ # on get do
11
11
  # res.write("Hei!")
12
12
  # end
13
13
  # end
14
14
  #
15
- # app = Tynn::Test.new
16
- # app.get("/")
15
+ # ts = Tynn::Test.new
16
+ # ts.get("/")
17
17
  #
18
- # app.res.status # => 200
19
- # app.res.body # => "Hei!"
18
+ # ts.res.status # => 200
19
+ # ts.res.body.join # => "Hei!"
20
20
  #
21
21
  class Test
22
22
  attr_reader :app # :nodoc:
@@ -26,8 +26,8 @@ class Tynn
26
26
  # class API < Tynn
27
27
  # end
28
28
  #
29
- # app = Tynn::Test.new(API)
30
- # app.get("/user.json")
29
+ # ts = Tynn::Test.new(API)
30
+ # ts.get("/user.json")
31
31
  #
32
32
  def initialize(app = Tynn)
33
33
  @app = app
@@ -52,40 +52,38 @@ class Tynn
52
52
  # end
53
53
  #
54
54
  module Methods
55
- # If a request has been issued, returns an instance of
56
- # Rack::Request[http://www.rubydoc.info/gems/rack/Rack/Request].
55
+ # If a request has been issued, returns an instance of Tynn::Request.
57
56
  # Otherwise, returns <tt>nil</tt>.
58
57
  #
59
- # app = Tynn::Test.new
60
- # app.get("/", { foo: "foo" }, { "HTTP_USER_AGENT" => "Tynn::Test" })
58
+ # ts = Tynn::Test.new
59
+ # ts.get("/", { foo: "foo" }, { "HTTP_USER_AGENT" => "Tynn::Test" })
61
60
  #
62
- # app.req.get?
61
+ # ts.req.get?
63
62
  # # => true
64
63
  #
65
- # app.req.params["foo"]
64
+ # ts.req.params["foo"]
66
65
  # # => "foo"
67
66
  #
68
- # app.req.env["HTTP_USER_AGENT"]
67
+ # ts.req.env["HTTP_USER_AGENT"]
69
68
  # # => "Tynn::Test"
70
69
  #
71
70
  def req
72
71
  @__req
73
72
  end
74
73
 
75
- # If a request has been issued, returns an instance of
76
- # Rack::MockResponse[http://www.rubydoc.info/gems/rack/Rack/MockResponse].
74
+ # If a request has been issued, returns an instance of Tynn::Response
77
75
  # Otherwise, returns <tt>nil</tt>.
78
76
  #
79
- # app = Tynn::Test.new
80
- # app.get("/", name: "Jane")
77
+ # ts = Tynn::Test.new
78
+ # ts.get("/", name: "Jane")
81
79
  #
82
- # app.res.status
80
+ # ts.res.status
83
81
  # # => 200
84
82
  #
85
- # app.res.body
83
+ # ts.res.body.join
86
84
  # # => "Hello Jane!"
87
85
  #
88
- # app.res["Content-Type"]
86
+ # ts.res.headers["Content-Type"]
89
87
  # # => "text/html"
90
88
  #
91
89
  def res
@@ -99,9 +97,9 @@ class Tynn
99
97
  # or <tt>nil</tt>.
100
98
  # [env] A Hash of Rack environment values.
101
99
  #
102
- # app = Tynn::Test.new
103
- # app.get("/search", name: "jane")
104
- # app.get("/cart", {}, { "HTTPS" => "on" })
100
+ # ts = Tynn::Test.new
101
+ # ts.get("/search", name: "jane")
102
+ # ts.get("/cart", {}, { "HTTPS" => "on" })
105
103
  #
106
104
  def get(path, params = {}, env = {})
107
105
  request(path, env.merge(method: "GET", params: params))
@@ -109,8 +107,8 @@ class Tynn
109
107
 
110
108
  # Issues a <tt>POST</tt> request. See #get for more information.
111
109
  #
112
- # app = Tynn::Test.new
113
- # app.post("/signup", username: "janedoe", password: "secret")
110
+ # ts = Tynn::Test.new
111
+ # ts.post("/signup", username: "janedoe", password: "secret")
114
112
  #
115
113
  def post(path, params = {}, env = {})
116
114
  request(path, env.merge(method: "POST", params: params))
@@ -118,8 +116,8 @@ class Tynn
118
116
 
119
117
  # Issues a <tt>PUT</tt> request. See #get for more information.
120
118
  #
121
- # app = Tynn::Test.new
122
- # app.put("/users/1", username: "johndoe", name: "John")
119
+ # ts = Tynn::Test.new
120
+ # ts.put("/users/1", username: "johndoe", name: "John")
123
121
  #
124
122
  def put(path, params = {}, env = {})
125
123
  request(path, env.merge(method: "PUT", params: params))
@@ -127,8 +125,8 @@ class Tynn
127
125
 
128
126
  # Issues a <tt>PATCH</tt> request. See #get for more information.
129
127
  #
130
- # app = Tynn::Test.new
131
- # app.patch("/users/1", username: "janedoe")
128
+ # ts = Tynn::Test.new
129
+ # ts.patch("/users/1", username: "janedoe")
132
130
  #
133
131
  def patch(path, params = {}, env = {})
134
132
  request(path, env.merge(method: "PATCH", params: params))
@@ -136,8 +134,8 @@ class Tynn
136
134
 
137
135
  # Issues a <tt>DELETE</tt> request. See #get for more information.
138
136
  #
139
- # app = Tynn::Test.new
140
- # app.delete("/users/1")
137
+ # ts = Tynn::Test.new
138
+ # ts.delete("/users/1")
141
139
  #
142
140
  def delete(path, params = {}, env = {})
143
141
  request(path, env.merge(method: "DELETE", params: params))
@@ -145,8 +143,8 @@ class Tynn
145
143
 
146
144
  # Issues a <tt>HEAD</tt> request. See #get for more information.
147
145
  #
148
- # app = Tynn::Test.new
149
- # app.head("/users/1")
146
+ # ts = Tynn::Test.new
147
+ # ts.head("/users/1")
150
148
  #
151
149
  def head(path, params = {}, env = {})
152
150
  request(path, env.merge(method: Rack::HEAD, params: params))
@@ -154,8 +152,8 @@ class Tynn
154
152
 
155
153
  # Issues a <tt>OPTIONS</tt> request. See #get for more information.
156
154
  #
157
- # app = Tynn::Test.new
158
- # app.options("/users")
155
+ # ts = Tynn::Test.new
156
+ # ts.options("/users")
159
157
  #
160
158
  def options(path, params = {}, env = {})
161
159
  request(path, env.merge(method: "OPTIONS", params: params))
@@ -164,8 +162,17 @@ class Tynn
164
162
  private
165
163
 
166
164
  def request(path, opts = {})
167
- @__req = Rack::Request.new(Rack::MockRequest.env_for(path, opts))
168
- @__res = Rack::MockResponse.new(*app.call(@__req.env))
165
+ @__req = Tynn::Request.new(Rack::MockRequest.env_for(path, opts))
166
+ @__res = make_response(*app.call(@__req.env))
167
+ end
168
+
169
+ def make_response(status, headers, body)
170
+ res = Tynn::Response.new(headers)
171
+ res.status = status
172
+
173
+ body.each { |b| res.write(b) }
174
+
175
+ res
169
176
  end
170
177
  end
171
178
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tynn
4
+ module Utils # :nodoc:
5
+ module_function
6
+
7
+ def deepclone_hash(hash)
8
+ default_proc, hash.default_proc = hash.default_proc, nil
9
+
10
+ Marshal.load(Marshal.dump(hash))
11
+ ensure
12
+ hash.default_proc = default_proc
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Tynn
4
- VERSION = "2.0.0.alpha" # :nodoc:
4
+ VERSION = "2.0.0.beta1" # :nodoc:
5
5
  end
@@ -4,7 +4,7 @@ require_relative "helper"
4
4
 
5
5
  class DefaultHeadersTest < Minitest::Test
6
6
  def setup
7
- @app = Class.new(Tynn)
7
+ @app = new_app
8
8
  end
9
9
 
10
10
  def test_set_and_get_headers
@@ -5,7 +5,7 @@ require_relative "../lib/tynn/environment"
5
5
 
6
6
  class EnvironmentTest < Minitest::Test
7
7
  def setup
8
- @app = Class.new(Tynn)
8
+ @app = new_app
9
9
  end
10
10
 
11
11
  def test_without_rack_env
@@ -5,3 +5,12 @@ require "minitest/autorun"
5
5
  require "minitest/pride"
6
6
  require_relative "../lib/tynn"
7
7
  require_relative "../lib/tynn/test"
8
+
9
+ class Minitest::Test
10
+ def new_app(parent: Tynn, lint: true)
11
+ app = Class.new(parent)
12
+ app.use(Rack::Lint) if lint
13
+
14
+ app
15
+ end
16
+ end
@@ -5,14 +5,14 @@ require_relative "../lib/tynn/json"
5
5
 
6
6
  class JSONTest < Minitest::Test
7
7
  def setup
8
- @app = Class.new(Tynn)
8
+ @app = new_app
9
9
  end
10
10
 
11
11
  def test_respond_json_object
12
12
  @app.plugin(Tynn::JSON)
13
13
 
14
14
  @app.define do
15
- get do
15
+ on get do
16
16
  json(foo: "foo")
17
17
  end
18
18
  end
@@ -20,7 +20,7 @@ class JSONTest < Minitest::Test
20
20
  ts = Tynn::Test.new(@app)
21
21
  ts.get("/")
22
22
 
23
- object = JSON.parse(ts.res.body)
23
+ object = JSON.parse(ts.res.body.join)
24
24
 
25
25
  assert_equal "foo", object["foo"]
26
26
  end
@@ -29,7 +29,7 @@ class JSONTest < Minitest::Test
29
29
  @app.plugin(Tynn::JSON)
30
30
 
31
31
  @app.define do
32
- get do
32
+ on get do
33
33
  json(%w(foo bar baz))
34
34
  end
35
35
  end
@@ -37,14 +37,14 @@ class JSONTest < Minitest::Test
37
37
  ts = Tynn::Test.new(@app)
38
38
  ts.get("/")
39
39
 
40
- assert_equal %w(foo bar baz), JSON.parse(ts.res.body)
40
+ assert_equal %w(foo bar baz), JSON.parse(ts.res.body.join)
41
41
  end
42
42
 
43
43
  def test_content_type
44
44
  @app.plugin(Tynn::JSON)
45
45
 
46
46
  @app.define do
47
- get do
47
+ on get do
48
48
  json(ok: true)
49
49
  end
50
50
  end
@@ -54,4 +54,19 @@ class JSONTest < Minitest::Test
54
54
 
55
55
  assert_equal "application/json", ts.res.content_type
56
56
  end
57
+
58
+ def test_custom_content_type
59
+ @app.plugin(Tynn::JSON, content_type: "application/js")
60
+
61
+ @app.define do
62
+ on get do
63
+ json(ok: true)
64
+ end
65
+ end
66
+
67
+ ts = Tynn::Test.new(@app)
68
+ ts.get("/")
69
+
70
+ assert_equal "application/js", ts.res.content_type
71
+ end
57
72
  end
@@ -16,12 +16,12 @@ class MiddlewareTest < Minitest::Test
16
16
  end
17
17
 
18
18
  def test_middleware_works
19
- app = Class.new(Tynn)
19
+ app = new_app
20
20
 
21
21
  app.use(Shrimp)
22
22
 
23
23
  app.define do
24
- get do
24
+ on get do
25
25
  res.write("1")
26
26
  res.write("2")
27
27
  end
@@ -31,23 +31,23 @@ class MiddlewareTest < Minitest::Test
31
31
  ts.get("/")
32
32
 
33
33
  assert_equal 200, ts.res.status
34
- assert_equal "21", ts.res.body
34
+ assert_equal "21", ts.res.body.join
35
35
  end
36
36
 
37
37
  def test_middleware_with_composition
38
- app = Class.new(Tynn)
39
- api = Class.new(Tynn)
38
+ app = new_app
39
+ api = new_app(lint: false)
40
40
 
41
41
  app.use(Shrimp)
42
42
 
43
43
  app.define do
44
- on("api") do
44
+ on "api" do
45
45
  run(api)
46
46
  end
47
47
  end
48
48
 
49
49
  api.define do
50
- get do
50
+ on get do
51
51
  res.write("1")
52
52
  res.write("2")
53
53
  end
@@ -57,23 +57,23 @@ class MiddlewareTest < Minitest::Test
57
57
  ts.get("/api")
58
58
 
59
59
  assert_equal 200, ts.res.status
60
- assert_equal "21", ts.res.body
60
+ assert_equal "21", ts.res.body.join
61
61
  end
62
62
 
63
63
  def test_middleware_for_sub_application
64
- app = Class.new(Tynn)
65
- api = Class.new(Tynn)
64
+ app = new_app
65
+ api = new_app
66
66
 
67
67
  api.use(Shrimp)
68
68
 
69
69
  app.define do
70
- on("api") do
70
+ on "api" do
71
71
  run(api)
72
72
  end
73
73
  end
74
74
 
75
75
  api.define do
76
- get do
76
+ on get do
77
77
  res.write("1")
78
78
  res.write("2")
79
79
  end
@@ -83,6 +83,16 @@ class MiddlewareTest < Minitest::Test
83
83
  ts.get("/api")
84
84
 
85
85
  assert_equal 200, ts.res.status
86
- assert_equal "21", ts.res.body
86
+ assert_equal "21", ts.res.body.join
87
+ end
88
+
89
+ def test_raise_if_frozen
90
+ app = new_app
91
+
92
+ app.define {}
93
+
94
+ assert_raises(Tynn::Error) do
95
+ app.use(Shrimp, name: "foo", bar: 1)
96
+ end
87
97
  end
88
98
  end