Module: Glib::TestHelpers

Extended by:
ActiveSupport::Concern
Defined in:
lib/glib/test_helpers.rb

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

HOST =

The best practice is to avoid using lvh.me due to security and performance concerns.

'www.localhost:3000'

Instance Method Summary collapse

Instance Method Details

#assert_attachable_types_crawl_covered(attachable_types:, router_dir:, signed_id_from:, exempt: [], pending: []) ⇒ Object

Asserts that every model with ActiveStorage attachments is actually exercised by the crawler: at least one of its file endpoints must appear in the recorded crawler output. A structural "is this type authorized?" check can only prove a type is handled, not that its file authorization is covered end-to-end. This proves the latter -- the crawler recorded the endpoint, so the permission test replays it per user -- which is what catches a wrong authorization on a newly-added attachable type (or a fixture that was never made reachable in the crawl).

attachable_types: model classes (or names) that must be covered.
router_dir:       dir holding the crawler router CSVs (the dump_path).
signed_id_from:   ->(url) returning the blob signed_id for a file URL, or
                nil for non-file URLs. Route-specific, e.g.
                ->(u) { u[%r{/blobs/([^/]+)/}, 1] }.
exempt:           type names whose files are served outside the crawl.
pending:          type names known-uncovered -- an explicit burn-down
                list, kept honest by the stale-pending check below.


102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/glib/test_helpers.rb', line 102

def assert_attachable_types_crawl_covered(attachable_types:, router_dir:, signed_id_from:, exempt: [], pending: [])
  required = attachable_types.map(&:to_s) - exempt.map(&:to_s) - pending.map(&:to_s)
  covered = __crawler_covered_record_types(router_dir, signed_id_from)

  uncovered = (required - covered).sort
  assert_empty(
    uncovered,
    'No crawled file endpoint covers these attachable types, so the permission test ' \
    'never exercises their file authorization (a wrong policy on them would ship ' \
    'silently). Give each a fixture whose file renders in a crawled page and regenerate ' \
    "the crawler snapshots, or list it in `exempt:`/`pending:` -- #{uncovered.join(', ')}"
  )

  graduated = (pending.map(&:to_s) & covered).sort
  assert_empty(
    graduated,
    'These `pending:` types are now crawl-covered -- remove them from the burn-down ' \
    "list so it keeps reflecting the real gap: #{graduated.join(', ')}"
  )
end

#crawl_json_pages(user, log_file: nil, dump_actions: false, dump_path: nil, skip_similar_page: false, &block) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/glib/test_helpers.rb', line 55

def crawl_json_pages(user, log_file: nil, dump_actions: false, dump_path: nil, skip_similar_page: false, &block)
  __execute_crawler(user, inspect_http: true) do |router, http|
    path = user[:path] ? "#{user[:path]}?format=json" : '/users/me?format=json&redirect=default'
    router.host = HOST
    router.skip_similar_page = skip_similar_page
    router.step(
      http,
      'onClick' => {
        'action' => user[:action] || 'initiate_navigation',
        'url' => user[:url] || "http://#{HOST}#{path}"
      }
    )

    if dump_actions
      csv_string = router.http_actions.map do |row|
        action, url, params = row
        [action, url, JSON.generate(params)].to_csv
      end.join

      filepath = crawler_output_file(user, file_dir: dump_path)
      File.write(filepath, csv_string)
    end
  end
end

#crawler_output_file(user, file_dir: nil) ⇒ Object



80
81
82
83
# File 'lib/glib/test_helpers.rb', line 80

def crawler_output_file(user, file_dir: nil)
  filename = "#{user[:email]}[#{user[:device]}][#{user[:version] || 'current'}].csv"
  File.join(file_dir || __crawler_log_dir, filename)
end

#find_form(page_payload) ⇒ Hash?

Locates the form in a rendered page payload without submitting it -- useful for asserting on a form's attributes (e.g. its HTTP method) in a test. Returns the form Hash, or nil when the page renders no form. When several forms are present it returns the one with the most childViews, the same selection submit_form uses internally.

Examples:

get new_chat_url(checklist_id: checklist.id), params: json_params
assert_equal 'post', find_form(response.body)['method']

Parameters:

  • page_payload (String, Hash)

    The JSON response body, or an already-parsed Hash

Returns:

  • (Hash, nil)

    The located form view, or nil if none is rendered



203
204
205
206
# File 'lib/glib/test_helpers.rb', line 203

def find_form(page_payload)
  json = page_payload.is_a?(String) ? JSON.parse(page_payload) : page_payload
  __find_form(json)
end

#glib_travel(*args, &block) ⇒ Object



208
209
210
# File 'lib/glib/test_helpers.rb', line 208

def glib_travel(*args, &block)
  Timecop.travel(*args, &block)
end

#glib_travel_backObject



216
217
218
# File 'lib/glib/test_helpers.rb', line 216

def glib_travel_back
  Timecop.return
end

#glib_travel_freeze(*args, &block) ⇒ Object



212
213
214
# File 'lib/glib/test_helpers.rb', line 212

def glib_travel_freeze(*args, &block)
  Timecop.freeze(*args, &block)
end

#logoutObject



131
132
133
134
# File 'lib/glib/test_helpers.rb', line 131

def logout
  delete logout_url
  assert_response :success
end

#logout_urlObject



136
137
138
# File 'lib/glib/test_helpers.rb', line 136

def logout_url
  "http://#{HOST}/users/sign_out.json"
end

#response_assert_equalObject

LOG_DIR = File.expand_path( File.join(File.dirname(FILE), 'integration/json_ui_crawler_test_results') )



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/glib/test_helpers.rb', line 39

def response_assert_equal
  expected = __get_previous_result_from_git
  result = __log_controller_data(response.body)
  # The message is a proc so the `git diff` subprocess only runs on FAILURE
  # (a positional string message is built eagerly — one shell-out per
  # passing assertion, and needless git-index contention under parallel
  # workers). The path must be QUOTED: log filenames derive from test
  # names, and an unquoted name containing `->` makes the shell treat `>`
  # as a redirection, creating stray empty files in the app root.
  failure_message = proc do
    diff = __git_is_available? ? `git diff "#{File.join(__controller_log_dir, __controller_log_file)}"` : ''
    "Result mismatch! #{diff}"
  end
  assert_equal JSON.parse(expected), JSON.parse(result), failure_message
end

#retrace_json_pages(user, past_actions:) ⇒ Object



123
124
125
126
127
128
129
# File 'lib/glib/test_helpers.rb', line 123

def retrace_json_pages(user, past_actions:)
  __execute_crawler(user, inspect_http: false) do |router, http|
    # router.follow(http, guiding_routers)
    # router.follow_v2(Glib::JsonCrawler::Http.new(self, user, router), actions)
    router.follow_v2(http, past_actions)
  end
end

#submit_form(page_payload, data, url: nil, method: nil) ⇒ Object

Submits a form by parsing the page payload and extracting form data. This ensures the view renders successfully before submitting.

Examples:

get new_chat_url(post_id: @post.id), params: json_params
submit_form(response.body, { chat: { message_body: 'Hello' } })

Parameters:

  • page_payload (String)

    The JSON response body containing the form

  • data (Hash)

    The form data to submit (e.g., { chat: { message_body: 'hello' } })

  • url (String, nil) (defaults to: nil)

    Optional URL override (if form URL is nil)

  • method (Symbol, nil) (defaults to: nil)

    Optional HTTP method override



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/glib/test_helpers.rb', line 152

def submit_form(page_payload, data, url: nil, method: nil)
  form = find_form(page_payload)

  raise 'No form found in page. Ensure the form is rendered properly.' unless form

  # Get URL from form, or use provided URL
  form_url = form['url']
  if form_url.blank? && form['onSubmit']
    on_submit = form['onSubmit']
    form_url = on_submit['httpPost']&.[]('url') || on_submit['http']&.[]('url')
  end
  url ||= form_url

  raise "Form missing 'url'. Please provide url: parameter. Form keys: #{form.keys.join(', ')}" if url.blank?

  # Get method from parameter, form, or default to post
  method ||= form['method']
  method = 'post' if method.blank?

  # Build params: hidden fields from form + provided data
  params = __build_form_params(form, data)

  # Submit using the appropriate HTTP method
  case method.to_sym
  when :get
    get url, params: params
  when :post
    post url, params: params
  when :patch
    patch url, params: params
  when :put
    put url, params: params
  else
    send(method, url, params: params)
  end

  assert_response :success
end