Skip to content

Commit 7b5c854

Browse files
committed
Return fragment of HTML from govspeak POSTed to /preview
1 parent d469dac commit 7b5c854

7 files changed

Lines changed: 198 additions & 5 deletions

File tree

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ Lint/MissingSuper:
4848
Rails/SaveBang:
4949
Exclude:
5050
- 'Rakefile'
51+
- 'test/test_helper.rb'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class Admin::PreviewController < Admin::BaseController
2+
include GovspeakPreviewHelper
3+
4+
def preview
5+
if Govspeak::HtmlValidator.new(params[:body]).valid?
6+
render layout: false
7+
else
8+
render plain: "Content contains possible XSS exploits", status: :forbidden
9+
end
10+
end
11+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<section>
2+
<article class="document">
3+
<div class="body">
4+
<%= govspeak_to_html(
5+
params[:body],
6+
preview: true,
7+
) %>
8+
</div>
9+
</article>
10+
</section>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require "test_helper"
2+
3+
class Admin::PreviewControllerTest < ActionController::TestCase
4+
setup do
5+
login_as :writer
6+
end
7+
8+
should_be_an_admin_controller
9+
10+
view_test "renders the body param using govspeak into a document body template" do
11+
post :preview, params: { body: "# gov speak" }
12+
assert_select ".document .body h1", "gov speak"
13+
end
14+
15+
test "preview returns a 403 if the content contains potential XSS exploits" do
16+
post :preview, params: { body: "<script>alert('woah');</script>" }
17+
assert_response :forbidden
18+
end
19+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module ControllerTestHelpers
2+
extend ActiveSupport::Concern
3+
4+
module ClassMethods
5+
def should_be_an_admin_controller
6+
test "should be an admin controller" do
7+
assert @controller.is_a?(Admin::BaseController), "the controller should be an admin controller"
8+
end
9+
end
10+
end
11+
end

test/support/view_rendering.rb

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# This is mostly taken from rspec.
2+
# Specifically: https://raw.github.com/rspec/rspec-rails/52493649d70754377b5e41be1c385a742f268bd7/lib/rspec/rails/view_rendering.rb
3+
# Helpers for optionally rendering views in controller specs.
4+
module ViewRendering
5+
extend ActiveSupport::Concern
6+
7+
attr_accessor :controller
8+
9+
def render_views?
10+
self.class.view_tests.include?(method_name)
11+
end
12+
13+
module ClassMethods
14+
def view_test(name, &block)
15+
test(name, &block)
16+
add_view_test("test_#{name.gsub(/\s+/, '_')}")
17+
end
18+
19+
def view_tests
20+
@view_tests ||= []
21+
end
22+
23+
def add_view_test(test_name)
24+
view_tests << test_name
25+
end
26+
end
27+
28+
class EmptyTemplateResolver
29+
def self.build(path)
30+
if path.is_a?(::ActionView::Resolver)
31+
ResolverDecorator.new(path)
32+
else
33+
FileSystemResolver.new(path)
34+
end
35+
end
36+
37+
def self.nullify_template_rendering(templates)
38+
templates.map do |template|
39+
::ActionView::Template.new(
40+
"",
41+
template.identifier,
42+
EmptyTemplateHandler,
43+
virtual_path: template.virtual_path,
44+
format: template.format,
45+
locals: [],
46+
)
47+
end
48+
end
49+
50+
# Delegates all methods to the submitted resolver and for all methods
51+
# that return a collection of `ActionView::Template` instances, return
52+
# templates with modified source
53+
class ResolverDecorator < ::ActionView::Resolver
54+
(::ActionView::Resolver.instance_methods - Object.instance_methods).each do |method|
55+
undef_method method
56+
end
57+
58+
(::ActionView::Resolver.methods - Object.methods).each do |method|
59+
singleton_class.undef_method method
60+
end
61+
62+
# rubocop:disable Lint/MissingSuper
63+
def initialize(resolver)
64+
@resolver = resolver
65+
end
66+
# rubocop:enable Lint/MissingSuper
67+
68+
# rubocop:disable Style/MissingRespondToMissing
69+
def method_missing(name, *args, &block)
70+
result = @resolver.send(name, *args, &block)
71+
nullify_templates(result)
72+
end
73+
# rubocop:enable Style/MissingRespondToMissing
74+
75+
private
76+
77+
def nullify_templates(collection)
78+
return collection unless collection.is_a?(Enumerable)
79+
return collection unless collection.all? { |element| element.is_a?(::ActionView::Template) }
80+
81+
EmptyTemplateResolver.nullify_template_rendering(collection)
82+
end
83+
end
84+
85+
# Delegates find_templates to the submitted path set and then returns
86+
# templates with modified source
87+
class FileSystemResolver < ::ActionView::FileSystemResolver
88+
private
89+
90+
def find_templates(*args)
91+
templates = super
92+
EmptyTemplateResolver.nullify_template_rendering(templates)
93+
end
94+
end
95+
end
96+
97+
class EmptyTemplateHandler
98+
def self.call(_template, _source = nil)
99+
::Rails.logger.info(" Template rendering was prevented by rspec-rails. Use `render_views` to verify rendered view contents if necessary.")
100+
101+
%("")
102+
end
103+
end
104+
105+
# Used to null out view rendering in controller specs.
106+
module EmptyTemplates
107+
def prepend_view_path(new_path)
108+
super(_path_decorator(*new_path))
109+
end
110+
111+
def append_view_path(new_path)
112+
super(_path_decorator(*new_path))
113+
end
114+
115+
private
116+
117+
def _path_decorator(*paths)
118+
paths.map { |path| EmptyTemplateResolver.build(path) }
119+
end
120+
end
121+
122+
RESOLVER_CACHE = Hash.new do |hash, path|
123+
hash[path] = EmptyTemplateResolver.build(path)
124+
end
125+
126+
included do
127+
setup do
128+
unless render_views?
129+
@_original_path_set = controller.class.view_paths
130+
path_set = @_original_path_set.map { |resolver| RESOLVER_CACHE[resolver] }
131+
132+
controller.class.view_paths = path_set
133+
controller.extend(EmptyTemplates)
134+
end
135+
end
136+
137+
teardown do
138+
controller.class.view_paths = @_original_path_set unless render_views?
139+
end
140+
end
141+
end

test/test_helper.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,18 @@ def fixture_path
162162
end
163163

164164
class ActionController::TestCase
165-
# include HtmlAssertions
165+
include HtmlAssertions
166166
# include AdminControllerTestHelpers
167167
# include AdminEditionControllerTestHelpers
168168
# include AdminEditionControllerScheduledPublishingTestHelpers
169169
# include AdminEditionRolesBehaviour
170170
# include AdminEditionWorldLocationsBehaviour
171171
# include DocumentControllerTestHelpers
172-
# include ControllerTestHelpers
172+
include ControllerTestHelpers
173173
# include ResourceTestHelpers
174174
# include AtomTestHelpers
175175
# include CacheControlTestHelpers
176-
# include ViewRendering
176+
include ViewRendering
177177
#
178178
# include Admin::EditionRoutesHelper
179179

@@ -194,8 +194,8 @@ class ActionController::TestCase
194194
stub_request(:get, %r{\A#{Plek.find('publishing-api')}/v2/expanded-links/}).to_return(body: { links: {} }.to_json)
195195
end
196196

197-
def login_as(role_or_user, organisation = nil)
198-
@current_user = role_or_user.is_a?(Symbol) ? create(role_or_user, organisation:) : role_or_user
197+
def login_as(role_or_user)
198+
@current_user = role_or_user.is_a?(Symbol) ? create(role_or_user) : role_or_user
199199
request.env["warden"] = stub(authenticate!: true, authenticated?: true, user: @current_user)
200200
Current.user = @current_user
201201
@current_user

0 commit comments

Comments
 (0)