belt 0.4.8 → 0.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21aaed39c7b6868b8c6f370cbc97a686bf369f7d3fe7a03c56b850c1bc49fd62
4
- data.tar.gz: 8825ef22ce89cee45435dcd1e7b405c954c9f630e90f569ee0d575de33db5b49
3
+ metadata.gz: 5247752ecef8dc1c7241d3098a0896005f1533b2e1315914bf58c7534fa315f6
4
+ data.tar.gz: 429bb361f14e70fd06988c6f0b026988e6623467ded6b8a3b7ee27849f607452
5
5
  SHA512:
6
- metadata.gz: 9bdf007e3adcc59dd1e852f2d9563002416272ae4f954ce72231856bead07c261cc6bad6a86a8b6bbdbc6a2896b321eb717dcf5f70bda7e77cc2538ed67b2970
7
- data.tar.gz: 95c20325ca7427776e6c08dbf9c6e2ad3a8dacb4293339bf24cb48dc74021a887dfd0152865a0cc295c311314047f605119e22ea8b293ba9e9dbb977a0a8c549
6
+ metadata.gz: da12badab39e69547f46b13c833c59a2a61902e62ba9a0442804586aaff9e7c8fe87bca9af9ffa970ea2e0ed68d0b9cc03b1721b71092661ec7d60fede9cb58f
7
+ data.tar.gz: 13984899af7ad0f1b0e2badb0cc244a084ad75856601b0bde24b4a03eedab642dfbb8d03016392b8d941733861f5c3bb9de79ebab7e0ba17c74dca42c96ba1b8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Added
6
+
7
+ - **In-process e2e test harness (`require 'belt/testing'`).** Belt now ships the real
8
+ request path as a reusable test tier: a synthetic API Gateway event routed through the
9
+ real `Belt::ActionRouter` — no AWS, no HTTP server, no browser. It sits between
10
+ controller unit tests (which dispatch an action directly and never touch routing) and
11
+ full cloud e2e. `Belt::Testing::E2E::Client` wraps one router and returns a parsed
12
+ `Response` (`status` / `headers` / `body` / `json`, with `ok?` and `[]`);
13
+ `Belt::Testing::E2E::Helpers` mixes `api_get` / `api_post` / `api_put` / `api_patch` /
14
+ `api_delete` / `api_request` and a `cognito_claims` builder into `Minitest::Test` or
15
+ RSpec example groups; and `Belt::Testing::E2E.manifest_from_belt_routes(app_root:)`
16
+ builds the router manifest from the app's own `belt routes -f json` so the harness can't
17
+ drift from Belt's route-building internals. The module is opt-in and never loaded by
18
+ `require 'belt'`, so it stays out of the production Lambda path. App-specific seams
19
+ (DynamoDB Local, Cognito claims, external services like Bedrock/Stripe/S3) stay in the
20
+ app's own test boot — Belt owns only the generic request mechanics. See
21
+ `belt explain testing`.
22
+
3
23
  ## 0.4.8
4
24
 
5
25
  ### Added
data/README.md CHANGED
@@ -731,6 +731,46 @@ puts "Created post: #{post.id}"
731
731
 
732
732
  Refuses to run against an environment that already has data (pass `--force` to override). `belt new` scaffolds a starter `config/seeds.rb`. See `belt explain data_seeding` for details.
733
733
 
734
+ ## Testing
735
+
736
+ Belt ships an in-process **end-to-end harness** that drives the real router — no AWS, no HTTP server, no browser. It's the tier between controller unit tests (dispatch an action directly, never touch routing) and full cloud e2e (needs a deployed stack).
737
+
738
+ Opt-in — it is **not** loaded by `require 'belt'`, so it never ships in the production Lambda path:
739
+
740
+ ```ruby
741
+ require "belt"
742
+ require "belt/testing"
743
+ ```
744
+
745
+ The harness builds a synthetic API Gateway event and routes it through the real `Belt::ActionRouter`, exactly as API Gateway delivers it: the router finds the route, extracts path params, instantiates the real controller, and runs the real `before_action` chain. Router, controllers, models, validations, and authorization are **real**; DynamoDB (e.g. DynamoDB Local), Cognito (a claims hash), and external services stay seams you wire in your own test boot.
746
+
747
+ ```ruby
748
+ # e2e_helper.rb — after booting your app (ENV, require 'belt', models, controllers, DynamoDB Local)
749
+ router = Belt::ActionRouter.new(
750
+ routes: Belt::Testing::E2E.manifest_from_belt_routes(app_root: APP_ROOT),
751
+ gateway: "api"
752
+ )
753
+ Belt::Testing::E2E.client = Belt::Testing::E2E::Client.new(router: router)
754
+ ```
755
+
756
+ ```ruby
757
+ class ProjectsE2ETest < Minitest::Test
758
+ include Belt::Testing::E2E::Helpers
759
+
760
+ def test_creates_a_project
761
+ res = api_post("/projects", body: { slug: "alpha" }, claims: cognito_claims(groups: "admins"))
762
+ assert_equal 201, res.status
763
+ assert_equal "alpha", res["project"]["slug"]
764
+ end
765
+
766
+ def test_rejects_anonymous
767
+ assert_equal 401, api_get("/projects").status
768
+ end
769
+ end
770
+ ```
771
+
772
+ `api_get` / `api_post` / `api_put` / `api_patch` / `api_delete` (and `api_request`) accept `body:`, `claims:`, `token:`, `headers:`, and `query:`. The returned `Response` exposes `status`, `headers`, `body`, `json`, `ok?`, and `[]` (a top-level key from the parsed JSON body). Full details — the real-vs-seamed boundary, per-test clients, and keeping the tier out of your normal suite — in `belt explain testing`.
773
+
734
774
  ## Plugins
735
775
 
736
776
  Belt is designed to stay lean. Optional capabilities ship as **separate gems** that plug into the CLI and runtime the same way Rails engines and generators do.
@@ -51,7 +51,13 @@ module Belt
51
51
  'db:seed' => 'data_seeding',
52
52
  'db:copy' => 'data_seeding',
53
53
  'copy' => 'data_seeding',
54
- 'db_copy' => 'data_seeding'
54
+ 'db_copy' => 'data_seeding',
55
+ 'e2e' => 'testing',
56
+ 'test' => 'testing',
57
+ 'tests' => 'testing',
58
+ 'harness' => 'testing',
59
+ 'spec' => 'testing',
60
+ 'specs' => 'testing'
55
61
  }.freeze
56
62
 
57
63
  def self.run(args)
@@ -114,6 +120,7 @@ module Belt
114
120
  irb, repl → console
115
121
  seeds, seed, db:seed → data_seeding
116
122
  db:copy, copy → data_seeding
123
+ e2e, test, harness → testing
117
124
 
118
125
  Examples:
119
126
  belt explain routing
@@ -0,0 +1,145 @@
1
+ # Testing
2
+
3
+ Belt ships an in-process **end-to-end harness** that drives the real router — no AWS, no
4
+ HTTP server, no browser. It's the tier between controller unit tests (which dispatch an
5
+ action directly and never touch routing) and full cloud e2e (which needs a deployed stack).
6
+
7
+ Load it opt-in — it is **not** required by `require 'belt'`, so it never ships in the
8
+ production Lambda path:
9
+
10
+ ```ruby
11
+ require 'belt'
12
+ require 'belt/testing'
13
+ ```
14
+
15
+ ## What it exercises
16
+
17
+ The harness builds a synthetic API Gateway proxy event (method, path, body, and a
18
+ `requestContext` carrying Cognito-style claims) and routes it through the real
19
+ `Belt::ActionRouter` — exactly as API Gateway delivers it in production. The router finds
20
+ the route, extracts path params, resolves and instantiates the real controller, and runs
21
+ the real `before_action` chain. You assert on the HTTP-shaped response.
22
+
23
+ | Layer | In the harness |
24
+ |-------|----------------|
25
+ | Router, controllers, models, validations, authorization | **Real** |
26
+ | DynamoDB | Your choice (e.g. DynamoDB Local) — wired in your own test boot |
27
+ | Cognito | A claims hash in `requestContext.authorizer.claims` (the seam API Gateway's authorizer fills in production) |
28
+ | API Gateway | The synthetic event **is** the seam — the router is what APIGW dispatches to |
29
+
30
+ Anything genuinely external (Cognito token verification, Bedrock, Stripe, S3) stays a seam
31
+ you stub in your own boot file. Belt owns the generic request mechanics; your app owns its
32
+ data-store lifecycle and external stubs.
33
+
34
+ ## The pieces
35
+
36
+ - **`Belt::Testing::E2E::Client`** — wraps one `Belt::ActionRouter`, builds synthetic
37
+ events, dispatches, and returns a `Response`.
38
+ - **`Belt::Testing::E2E::Response`** — a parsed `{ status, headers, body, json }` struct
39
+ with `ok?` and `[]` (reads a top-level key out of the parsed JSON body).
40
+ - **`Belt::Testing::E2E::Helpers`** — a mixin for `Minitest::Test` or RSpec example groups
41
+ giving you `api_get` / `api_post` / `api_put` / `api_patch` / `api_delete` / `api_request`
42
+ plus a `cognito_claims` builder.
43
+ - **`Belt::Testing::E2E.manifest_from_belt_routes(app_root:)`** — loads the route manifest
44
+ via your app's own `belt routes -f json`, the same canonical path the deployed Lambda's
45
+ manifest comes from, so the harness can't drift from Belt's route-building internals.
46
+
47
+ ## Setup
48
+
49
+ Build the router once per process from your app's real manifest, then expose a client:
50
+
51
+ ```ruby
52
+ require 'belt'
53
+ require 'belt/testing'
54
+
55
+ # ... boot your app: set test ENV, require 'belt', load models + controllers,
56
+ # point Aws::DynamoDB::Client at DynamoDB Local, etc. ...
57
+
58
+ router = Belt::ActionRouter.new(
59
+ routes: Belt::Testing::E2E.manifest_from_belt_routes(app_root: APP_ROOT),
60
+ gateway: 'api'
61
+ )
62
+ Belt::Testing::E2E.client = Belt::Testing::E2E::Client.new(router: router)
63
+ ```
64
+
65
+ ## Writing tests
66
+
67
+ ```ruby
68
+ class ProjectsE2ETest < Minitest::Test
69
+ include Belt::Testing::E2E::Helpers
70
+
71
+ def test_lists_projects_for_an_admin
72
+ res = api_get('/projects', claims: cognito_claims(groups: 'admins'))
73
+
74
+ assert res.ok? # 2xx
75
+ assert_equal 200, res.status
76
+ assert_kind_of Array, res['projects'] # top-level key out of the JSON body
77
+ end
78
+
79
+ def test_creates_a_project
80
+ res = api_post('/projects', body: { slug: 'alpha', name: 'Alpha' },
81
+ claims: cognito_claims(groups: 'admins'))
82
+
83
+ assert_equal 201, res.status
84
+ assert_equal 'alpha', res['project']['slug']
85
+
86
+ # The write really happened — re-read it through the router.
87
+ show = api_get('/projects/alpha', claims: cognito_claims(groups: 'admins'))
88
+ assert show.ok?
89
+ end
90
+
91
+ def test_rejects_an_unauthenticated_request
92
+ res = api_get('/projects') # no claims → unauthenticated
93
+ assert_equal 401, res.status
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### Request options
99
+
100
+ Every `api_*` helper (and `Client#request`) accepts:
101
+
102
+ | Option | Meaning |
103
+ |--------|---------|
104
+ | `body:` | request body as a **parsed Hash** (the LambdaHandler JSON-parses it in production) |
105
+ | `claims:` | Cognito claims hash injected into `requestContext.authorizer.claims`; omit for an unauthenticated request |
106
+ | `token:` | a Bearer token (e.g. an API key) set as the `Authorization` header, for non-Cognito auth |
107
+ | `headers:` | extra request headers |
108
+ | `query:` | query string parameters (`event['queryStringParameters']`) |
109
+
110
+ ### Claims
111
+
112
+ `cognito_claims` builds the hash API Gateway's authorizer would inject:
113
+
114
+ ```ruby
115
+ cognito_claims # { 'sub' => 'e2e-user-sub', 'email' => '...' }
116
+ cognito_claims(sub: 'u-1', groups: 'admins') # platform staff
117
+ cognito_claims(groups: %w[admins editors]) # array → space-joined 'cognito:groups'
118
+ cognito_claims(tenant: 't-1') # extra claims merge straight in
119
+ ```
120
+
121
+ ## Per-test client
122
+
123
+ By default the helpers use the process-wide `Belt::Testing::E2E.client`. To use a different
124
+ client (e.g. a second gateway), override `#e2e_client` in your test base class:
125
+
126
+ ```ruby
127
+ class OpsE2ETest < Minitest::Test
128
+ include Belt::Testing::E2E::Helpers
129
+
130
+ def e2e_client
131
+ @e2e_client ||= Belt::Testing::E2E::Client.new(router: OPS_ROUTER)
132
+ end
133
+ end
134
+ ```
135
+
136
+ ## Keeping it out of the normal suite
137
+
138
+ The harness needs the **real** Belt stack (`require 'belt'`). If your unit suite shadows
139
+ `BeltController::Base` with a stub, keep the e2e tier in its own directory (e.g. `e2e/`,
140
+ not `spec/`) and run it as its own process so the two don't collide:
141
+
142
+ ```bash
143
+ cd lambda
144
+ bundle exec ruby -Ie2e -e "Dir.glob('e2e/**/*_test.rb').each { |f| require File.expand_path(f) }"
145
+ ```
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Belt
6
+ module Testing
7
+ # In-process end-to-end harness — the real request path, no AWS.
8
+ #
9
+ # Most backend tests stop at the controller: they instantiate a controller and
10
+ # dispatch an action directly. That proves action logic, but it never exercises the
11
+ # router, the route manifest, path-param extraction, or the Lambda-shaped event the
12
+ # controller actually receives in production.
13
+ #
14
+ # This harness closes that gap. It drives the REAL {Belt::ActionRouter} with a
15
+ # synthetic API Gateway proxy event — method, path, body, and a requestContext
16
+ # carrying Cognito-style claims — exactly as API Gateway would deliver it. The router
17
+ # finds the route, extracts path params, instantiates the real controller, and runs
18
+ # the real before_action chain. You assert on the HTTP-shaped response.
19
+ #
20
+ # What's real vs. seamed (the deliberate boundary):
21
+ # - Router, controllers, models, validations, authorization .... REAL
22
+ # - DynamoDB ............................................ your choice (e.g. DynamoDB Local)
23
+ # - Cognito ............................................ a claims hash in requestContext
24
+ # (the seam API Gateway's
25
+ # authorizer fills in production)
26
+ # - API Gateway ........................................ the synthetic event IS the seam
27
+ #
28
+ # The harness is framework-agnostic: use the {Client} directly, or mix {Helpers}
29
+ # into your Minitest::Test / RSpec example group for `api_get`/`api_post`/... sugar.
30
+ #
31
+ # @example Minitest
32
+ # router = Belt::ActionRouter.new(
33
+ # routes: Belt::Testing::E2E.manifest_from_belt_routes(app_root: APP_ROOT),
34
+ # gateway: 'api'
35
+ # )
36
+ # Belt::Testing::E2E.client = Belt::Testing::E2E::Client.new(router: router)
37
+ #
38
+ # class ProjectsE2ETest < Minitest::Test
39
+ # include Belt::Testing::E2E::Helpers
40
+ #
41
+ # def test_create_project
42
+ # res = api_post('/projects', body: { slug: 'x' }, claims: cognito_claims(groups: 'admins'))
43
+ # assert res.ok?
44
+ # assert_equal 'x', res['project']['slug']
45
+ # end
46
+ # end
47
+ module E2E
48
+ # A parsed response from the router. `json` is the body parsed as JSON (Hash) when
49
+ # possible; `[]` reads a top-level key out of that parsed body.
50
+ Response = Struct.new(:status, :headers, :body, :json, keyword_init: true) do
51
+ def ok?
52
+ status.is_a?(Integer) && status.between?(200, 299)
53
+ end
54
+
55
+ def [](key)
56
+ json&.[](key.to_s)
57
+ end
58
+ end
59
+
60
+ class << self
61
+ # A process-wide default client, so {Helpers} works without per-test setup.
62
+ # Assign one after booting your stack and building a router.
63
+ attr_accessor :client
64
+
65
+ # Build a route manifest from the app's own `belt routes -f json`.
66
+ #
67
+ # This is the app's canonical, supported way to produce a manifest — the same
68
+ # path the deployed Lambda's manifest comes from — so the harness can't drift
69
+ # from Belt's route-building internals.
70
+ #
71
+ # @param app_root [String] directory to run `belt routes` from (the app's `lambda/`)
72
+ # @param command [Array<String>] override the command (mainly for testing)
73
+ # @return [Array<Hash>] routes shaped as { verb:, path:, controller:, action: }
74
+ def manifest_from_belt_routes(app_root:, command: %w[bundle exec belt routes -f json])
75
+ require 'open3'
76
+ out, err, status = Open3.capture3(*command, chdir: app_root)
77
+ raise Error, "`#{command.join(' ')}` failed (#{status.exitstatus}): #{err.strip}" unless status.success?
78
+
79
+ parse_manifest(out)
80
+ end
81
+
82
+ # Parse the JSON produced by `belt routes -f json` into the manifest shape the
83
+ # router expects. Public so callers that already have the JSON (CI artifact,
84
+ # cached file) can reuse it without shelling out.
85
+ #
86
+ # @param json [String] output of `belt routes -f json`
87
+ # @return [Array<Hash>]
88
+ def parse_manifest(json)
89
+ JSON.parse(json).fetch('routes').map do |r|
90
+ {
91
+ verb: r['verb'],
92
+ path: r['path'],
93
+ controller: r['controller'],
94
+ action: r['action']
95
+ }
96
+ end
97
+ rescue KeyError => e
98
+ raise Error, "belt routes JSON missing expected key: #{e.message}"
99
+ end
100
+ end
101
+
102
+ # Raised when the harness can't do its job (manifest load failure, missing client).
103
+ class Error < StandardError; end
104
+
105
+ # Drives a {Belt::ActionRouter} with synthetic API Gateway proxy events.
106
+ #
107
+ # One client wraps one router. Build it once per process (routing is immutable) and
108
+ # reuse it across tests — isolation comes from your data-store teardown, not from
109
+ # rebuilding the router.
110
+ class Client
111
+ attr_reader :router
112
+
113
+ # @param router [Belt::ActionRouter] the real router to dispatch through
114
+ def initialize(router:)
115
+ @router = router
116
+ end
117
+
118
+ # Issue a request through the real router.
119
+ #
120
+ # @param method [Symbol, String] HTTP verb (:get, :post, ...)
121
+ # @param path [String] request path, e.g. '/projects/ws-1'
122
+ # @param body [Hash, nil] request body, already parsed (as the LambdaHandler
123
+ # delivers it in production). `nil` becomes `{}` inside the controller.
124
+ # @param claims [Hash, nil] Cognito claims the API Gateway authorizer would inject
125
+ # into `requestContext.authorizer.claims`. `nil` means an unauthenticated request.
126
+ # @param token [String, nil] a Bearer token (e.g. an API key) set as the
127
+ # Authorization header, for non-Cognito auth paths.
128
+ # @param headers [Hash] extra request headers.
129
+ # @param query [Hash, nil] query string parameters.
130
+ # @return [Response]
131
+ # rubocop:disable Metrics/ParameterLists -- these are the request's distinct, documented dimensions
132
+ def request(method, path, body: nil, claims: nil, token: nil, headers: {}, query: nil)
133
+ # rubocop:enable Metrics/ParameterLists
134
+ event = {
135
+ 'httpMethod' => method.to_s.upcase,
136
+ 'path' => path,
137
+ 'headers' => build_headers(headers, token),
138
+ 'pathParameters' => {},
139
+ 'queryStringParameters' => query,
140
+ 'requestContext' => request_context(claims)
141
+ }
142
+
143
+ result = router.route(event: event, body: body)
144
+ parse_response(result)
145
+ end
146
+
147
+ def get(path, **) = request(:get, path, **)
148
+ def post(path, **) = request(:post, path, **)
149
+ def put(path, **) = request(:put, path, **)
150
+ def patch(path, **) = request(:patch, path, **)
151
+ def delete(path, **) = request(:delete, path, **)
152
+
153
+ private
154
+
155
+ def build_headers(headers, token)
156
+ h = (headers || {}).dup
157
+ h['Authorization'] = "Bearer #{token}" if token
158
+ h
159
+ end
160
+
161
+ def request_context(claims)
162
+ return {} if claims.nil?
163
+
164
+ { 'authorizer' => { 'claims' => claims } }
165
+ end
166
+
167
+ # The real controller always returns a Lambda-shaped Hash:
168
+ # { statusCode:, headers:, body: <JSON string> }. So does the router's own error
169
+ # path. Normalize into a Response, tolerating string or symbol keys.
170
+ def parse_response(result)
171
+ status = result[:statusCode] || result['statusCode']
172
+ headers = result[:headers] || result['headers'] || {}
173
+ raw = result[:body] || result['body']
174
+
175
+ json =
176
+ begin
177
+ raw.is_a?(String) && !raw.empty? ? JSON.parse(raw) : nil
178
+ rescue JSON::ParserError
179
+ nil
180
+ end
181
+
182
+ Response.new(status: status, headers: headers, body: raw, json: json)
183
+ end
184
+ end
185
+
186
+ # Mixin for test classes (Minitest::Test or RSpec example groups).
187
+ #
188
+ # Provides `api_get`/`api_post`/`api_put`/`api_patch`/`api_delete`/`api_request`
189
+ # delegating to a client, plus a `cognito_claims` builder. By default it uses the
190
+ # process-wide {E2E.client}; override {#e2e_client} to supply a per-test client.
191
+ module Helpers
192
+ # The client requests route through. Override in your test base class to use a
193
+ # different client than the process-wide default.
194
+ #
195
+ # @return [Client]
196
+ def e2e_client
197
+ E2E.client || raise(Error, 'No Belt::Testing::E2E client configured. ' \
198
+ 'Set Belt::Testing::E2E.client or override #e2e_client.')
199
+ end
200
+
201
+ def api_request(method, path, **) = e2e_client.request(method, path, **)
202
+ def api_get(path, **) = e2e_client.get(path, **)
203
+ def api_post(path, **) = e2e_client.post(path, **)
204
+ def api_put(path, **) = e2e_client.put(path, **)
205
+ def api_patch(path, **) = e2e_client.patch(path, **)
206
+ def api_delete(path, **) = e2e_client.delete(path, **)
207
+
208
+ # Build a Cognito claims hash — what API Gateway's authorizer injects for an
209
+ # authenticated human.
210
+ #
211
+ # @param sub [String] the Cognito subject (user id)
212
+ # @param email [String, nil] the user's email claim
213
+ # @param groups [String, Array<String>, nil] `cognito:groups` (platform roles).
214
+ # Belt reads this as a space/comma-delimited string; an array is joined with ' '.
215
+ # @param extra [Hash] any additional claims to merge in
216
+ # @return [Hash]
217
+ def cognito_claims(sub: 'e2e-user-sub', email: 'user@example.com', groups: nil, **extra)
218
+ claims = { 'sub' => sub }
219
+ claims['email'] = email if email
220
+ claims['cognito:groups'] = groups.is_a?(Array) ? groups.join(' ') : groups if groups
221
+ claims.merge(extra.transform_keys(&:to_s))
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Belt's test support. Opt-in: this is NOT loaded by `require 'belt'`, so it never ships
4
+ # in the production Lambda path. Require it from your test harness instead:
5
+ #
6
+ # require 'belt'
7
+ # require 'belt/testing'
8
+ #
9
+ # See Belt::Testing::E2E for the in-process, real-router end-to-end harness.
10
+
11
+ require_relative 'action_router'
12
+ require_relative 'testing/e2e'
13
+
14
+ module Belt
15
+ # Namespace for Belt's test-support helpers (opt-in via `require 'belt/testing'`).
16
+ module Testing
17
+ end
18
+ end
data/lib/belt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.4.8'
4
+ VERSION = '0.5.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -173,6 +173,7 @@ files:
173
173
  - lib/belt/docs/plugins.md
174
174
  - lib/belt/docs/routing.md
175
175
  - lib/belt/docs/structure.md
176
+ - lib/belt/docs/testing.md
176
177
  - lib/belt/errors.rb
177
178
  - lib/belt/helpers/cors_origin.rb
178
179
  - lib/belt/helpers/error_logging.rb
@@ -186,6 +187,8 @@ files:
186
187
  - lib/belt/root.rb
187
188
  - lib/belt/route_dsl.rb
188
189
  - lib/belt/table_inference.rb
190
+ - lib/belt/testing.rb
191
+ - lib/belt/testing/e2e.rb
189
192
  - lib/belt/version.rb
190
193
  - lib/belt/views/welcome/show.html.erb
191
194
  - lib/belt_controller/base.rb