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.


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

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
79
80
# 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' => {
        # Canonical slash spelling (see router.rb's initiate/navigation
        # branch: the underscore spelling collides with Name.normalize).
        '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



82
83
84
85
# File 'lib/glib/test_helpers.rb', line 82

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



205
206
207
208
# File 'lib/glib/test_helpers.rb', line 205

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



210
211
212
# File 'lib/glib/test_helpers.rb', line 210

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

#glib_travel_backObject



218
219
220
# File 'lib/glib/test_helpers.rb', line 218

def glib_travel_back
  Timecop.return
end

#glib_travel_freeze(*args, &block) ⇒ Object



214
215
216
# File 'lib/glib/test_helpers.rb', line 214

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

#logoutObject



133
134
135
136
# File 'lib/glib/test_helpers.rb', line 133

def logout
  delete logout_url
  assert_response :success
end

#logout_urlObject



138
139
140
# File 'lib/glib/test_helpers.rb', line 138

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



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

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



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
190
191
# File 'lib/glib/test_helpers.rb', line 154

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