0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/spec/helpers/application_helper_spec.rb
Régis Hanol 97e92041af
UX: Hide signup when the site is in read only or staff only mode (#42101)
Previously, a site in read only or staff only mode still offered
anonymous visitors every way in: the header "Sign Up" button, the signup
call to action at the end of a topic, the create account link on the
login page, and the server-rendered header — including the one on the
read only error page itself. Account creation is blocked in both modes,
so all of them were dead ends. The banner made it worse by describing
what members lose (replying, likes) rather than what an anonymous
visitor is actually unable to do.

This change gates the JS `canSignUp` getter and its Ruby twin
`can_sign_up?` on read only state, which covers every signup entry point
at once. `canSignUp` had to drop its `@computed` decorator to do so:
with no dependent keys it cached permanently, so the new term would have
been evaluated once at boot and then frozen — leaving the button visible
if read only mode started later, and hidden long after it ended.

The "Log In" button deliberately stays, because staff can still log in
during staff only mode. Anonymous visitors now get banner copy naming
signup and login instead, a refused login says only staff can log in
rather than claiming login is disabled outright, and the email and code
login forms show that inline instead of a generic "an error occurred"
dialog.

It also fixes a pre-existing blank page. Both `/login` and `/signup`
aborted the transition when read only, which on a direct URL load left
the application template unrendered — and with it the dialog holder, so
the explanation never appeared either. They now redirect home when there
is no route to stay on, and keep aborting when there is.

Meta ref: /t/408703
2026-07-28 20:05:04 +02:00

1420 lines
50 KiB
Ruby
Vendored

# coding: utf-8
# frozen_string_literal: true
RSpec.describe ApplicationHelper do
describe "#discourse_pageview_tracking_meta_tags" do
it "includes beacon tracking meta tags for anonymous users when dashboard_improvements is enabled" do
SiteSetting.dashboard_improvements = true
helper.stubs(:current_user).returns(nil)
tags = helper.discourse_pageview_tracking_meta_tags
expect(tags).to include('name="discourse-track-view-session-id"')
expect(tags).to include('name="discourse-beacon-pageview-enabled"')
end
it "omits beacon tracking meta tags when dashboard_improvements is disabled" do
SiteSetting.dashboard_improvements = false
tags = helper.discourse_pageview_tracking_meta_tags
expect(tags).to include('name="discourse-track-view-session-id"')
expect(tags).not_to include('name="discourse-beacon-pageview-enabled"')
end
it "includes beacon tracking meta tags for browser pageview event triggers" do
SiteSetting.dashboard_improvements = true
SiteSetting.persist_browser_pageview_events = false
SiteSetting.trigger_browser_pageview_events = true
tags = helper.discourse_pageview_tracking_meta_tags
expect(tags).to include('name="discourse-track-view-session-id"')
expect(tags).to include('name="discourse-beacon-pageview-enabled"')
end
it "includes the engagement tracking meta tag when pageview events are persisted" do
SiteSetting.persist_browser_pageview_events = true
tags = helper.discourse_pageview_tracking_meta_tags
expect(tags).to include('name="discourse-engagement-tracking-enabled"')
end
it "omits the engagement tracking meta tag in a trigger-only config so the browser does not send /srv/se beacons the server rejects" do
SiteSetting.dashboard_improvements = true
SiteSetting.persist_browser_pageview_events = false
SiteSetting.trigger_browser_pageview_events = true
tags = helper.discourse_pageview_tracking_meta_tags
expect(tags).to include('name="discourse-beacon-pageview-enabled"')
expect(tags).not_to include('name="discourse-engagement-tracking-enabled"')
end
end
describe "preload_script" do
def script_tag(url, entrypoint, nonce)
<<~HTML
<script defer src="#{url}" data-discourse-entrypoint="#{entrypoint}" nonce="#{nonce}"></script>
HTML
end
it "does not send crawler content to logged on users" do
controller.stubs(:use_crawler_layout?).returns(false)
helper.stubs(:current_user).returns(Fabricate(:user))
helper.request.user_agent = "Firefox"
expect(helper.include_crawler_content?).to eq(false)
end
it "sends crawler content to logged on users who wants to print" do
helper.stubs(:current_user).returns(Fabricate(:user))
controller.stubs(:use_crawler_layout?).returns(false)
helper.stubs(:params).returns(print: true)
expect(helper.include_crawler_content?).to eq(true)
end
it "sends crawler content to logged on users with a crawler user agent" do
helper.stubs(:current_user).returns(Fabricate(:user))
controller.stubs(:use_crawler_layout?).returns(true)
expect(helper.include_crawler_content?).to eq(true)
end
it "sends crawler content to old mobiles" do
controller.stubs(:use_crawler_layout?).returns(false)
helper.request.user_agent =
"Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25"
expect(helper.include_crawler_content?).to eq(true)
end
it "does not send crawler content to new mobiles" do
controller.stubs(:use_crawler_layout?).returns(false)
helper.request.user_agent =
"Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Mobile Safari/537.36 (compatible"
expect(helper.include_crawler_content?).to eq(false)
end
context "with s3 CDN" do
before do
global_setting :s3_bucket, "test_bucket"
global_setting :s3_region, "ap-australia"
global_setting :s3_access_key_id, "123"
global_setting :s3_secret_access_key, "123"
global_setting :s3_cdn_url, "https://s3cdn.com"
# Backend RSpec tests might be run without real manifest/assets
EmberAssets.stubs(:script_chunks).returns(
{ "discourse" => ["js/discourse-20n62q6s.digested"] },
)
end
it "deals correctly with subfolder" do
set_subfolder "/community"
expect(helper.preload_script("discourse")).to include(
%r{https://s3cdn.com/assets/js/discourse-\w{8}.digested.js},
)
end
it "replaces cdn URLs with s3 cdn subfolder paths" do
global_setting :s3_cdn_url, "https://s3cdn.com/s3_subpath"
set_cdn_url "https://awesome.com"
set_subfolder "/community"
expect(helper.preload_script("discourse")).to include(
%r{https://s3cdn.com/s3_subpath/assets/js/discourse-\w{8}.digested.js},
)
end
it "returns magic brotli mangling for brotli requests" do
helper.request.env["HTTP_ACCEPT_ENCODING"] = "br"
link = helper.preload_script("discourse")
expect(link).to include(%r{https://s3cdn.com/assets/br/discourse-\w{8}.digested.js})
end
it "gives s3 cdn if asset host is not set" do
link = helper.preload_script("discourse")
expect(link).to include(%r{https://s3cdn.com/assets/js/discourse-\w{8}.digested.js})
end
it "can fall back to gzip compression" do
helper.request.env["HTTP_ACCEPT_ENCODING"] = "gzip"
link = helper.preload_script("discourse")
expect(link).to include(%r{https://s3cdn.com/assets/gz/discourse-\w{8}.digested.js})
end
it "gives s3 cdn even if asset host is set" do
set_cdn_url "https://awesome.com"
link = helper.preload_script("discourse")
expect(link).to include(%r{https://s3cdn.com/assets/js/discourse-\w{8}.digested.js})
end
it "uses separate asset CDN if configured" do
global_setting :s3_asset_cdn_url, "https://s3-asset-cdn.example.com"
expect(helper.preload_script("discourse")).to include(
%r{https://s3-asset-cdn.example.com/assets/js/discourse-\w{8}.digested.js},
)
end
end
it "includes extra attrs when provided" do
result =
helper.preload_script(
"plugins/my-plugin",
attrs: {
"data-plugin-name": "my-plugin",
"data-preinstalled": "true",
"data-official": "true",
},
)
expect(result).to include('data-plugin-name="my-plugin"')
expect(result).to include('data-preinstalled="true"')
expect(result).to include('data-official="true"')
end
it "does not include extra attrs when none are provided" do
result = helper.preload_script("discourse")
expect(result).not_to include("data-plugin-name")
expect(result).not_to include("data-preinstalled")
expect(result).not_to include("data-official")
end
it "escapes attr values" do
result =
helper.preload_script("plugins/test", attrs: { "data-plugin-name": "<script>xss</script>" })
expect(result).not_to include("<script>xss</script>")
expect(result).to include("&lt;script&gt;xss&lt;/script&gt;")
end
it "renders preinstalled and official as false for non-preinstalled plugins" do
result =
helper.preload_script(
"plugins/my-plugin",
attrs: {
"data-plugin-name": "my-plugin",
"data-preinstalled": "false",
"data-official": "false",
},
)
expect(result).to include('data-preinstalled="false"')
expect(result).to include('data-official="false"')
end
end
describe "add_resource_preload_list" do
it "adds resources to the preload list" do
add_resource_preload_list("/assets/discourse.js", "script")
add_resource_preload_list("/assets/discourse.css", "style")
expect(controller.instance_variable_get(:@asset_preload_links).size).to eq(2)
end
it "adds resources to the preload list when preload_script is called" do
helper.preload_script("discourse")
expect(controller.instance_variable_get(:@asset_preload_links).size).to eq(1)
end
it "adds resources to the preload list when discourse_stylesheet_link_tag is called" do
helper.discourse_stylesheet_link_tag(:common)
expect(controller.instance_variable_get(:@asset_preload_links).size).to eq(1)
end
it "adds resources as the correct type" do
helper.discourse_stylesheet_link_tag(:common)
helper.preload_script("discourse")
expect(controller.instance_variable_get(:@asset_preload_links)[0]).to match(/as="style"/)
expect(controller.instance_variable_get(:@asset_preload_links)[1]).to match(/as="script"/)
end
end
describe "escape_unicode" do
it "encodes tags" do
expect(helper.escape_unicode("<tag>")).to eq("\u003ctag>")
end
it "survives junk text" do
expect(helper.escape_unicode("hello \xc3\x28 world")).to match(/hello.*world/)
end
end
describe "render_sitelinks_search_tag" do
context "for non-subfolder install" do
context "when on homepage" do
it "will return sitelinks search tag" do
helper.stubs(:current_page?).returns(false)
helper.stubs(:current_page?).with("/").returns(true)
sitelinks_search_tag =
JSON.parse(
helper
.render_sitelinks_search_tag
.gsub('<script type="application/ld+json">', "")
.gsub("</script>", ""),
)
expect(sitelinks_search_tag["@type"]).to eq("WebSite")
expect(sitelinks_search_tag["potentialAction"]["@type"]).to eq("SearchAction")
expect(sitelinks_search_tag["name"]).to eq(SiteSetting.title)
expect(sitelinks_search_tag["url"]).to eq(Discourse.base_url)
end
end
context "when not on homepage" do
it "will not return sitelinks search tag" do
helper.stubs(:current_page?).returns(true)
helper.stubs(:current_page?).with("/").returns(false)
helper.stubs(:current_page?).with(Discourse.base_path).returns(false)
expect(helper.render_sitelinks_search_tag).to be_nil
end
end
end
context "for subfolder install" do
context "when on homepage" do
it "will return sitelinks search tag" do
Discourse.stubs(:base_path).returns("/subfolder-base-path/")
helper.stubs(:current_page?).returns(false)
helper.stubs(:current_page?).with(Discourse.base_path).returns(true)
expect(helper.render_sitelinks_search_tag).to include('"@type":"SearchAction"')
expect(helper.render_sitelinks_search_tag).to include("subfolder-base-path")
end
end
context "when not on homepage" do
it "will not return sitelinks search tag" do
Discourse.stubs(:base_path).returns("/subfolder-base-path/")
helper.stubs(:current_page?).returns(true)
helper.stubs(:current_page?).with("/").returns(false)
helper.stubs(:current_page?).with(Discourse.base_path).returns(false)
expect(helper.render_sitelinks_search_tag).to be_nil
end
end
end
end
describe "application_logo_url" do
context "when a dark color scheme is active" do
before do
dark_theme =
Theme.create(
name: "Dark",
user_id: Discourse::SYSTEM_USER_ID,
color_scheme_id:
ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Dark"]).id,
)
helper.request.env[:resolved_theme_id] = dark_theme.id
end
context "when on desktop" do
before { helper.stubs(:mobile_device?).returns(false) }
context "when logo_dark is not set" do
it "will return site_logo_url instead" do
expect(helper.application_logo_url).to eq(SiteSetting.site_logo_url)
end
end
context "when logo_dark is set" do
before { SiteSetting.logo_dark = Fabricate(:upload, url: "/images/logo-dark.png") }
it "will return site_logo_dark_url" do
expect(helper.application_logo_url).to eq(SiteSetting.site_logo_dark_url)
end
end
end
context "when on mobile" do
before { helper.stubs(:mobile_device?).returns(true) }
context "when mobile_logo_dark is not set" do
it "will return site_mobile_logo_url instead" do
expect(helper.application_logo_url).to eq(SiteSetting.site_mobile_logo_url)
end
end
context "when mobile_logo_dark is set" do
before do
SiteSetting.mobile_logo_dark = Fabricate(:upload, url: "/images/mobile-logo-dark.png")
end
it "will return site_mobile_logo_dark_url" do
expect(helper.application_logo_url).to eq(SiteSetting.site_mobile_logo_dark_url)
end
end
end
end
end
describe "application_logo_dark_url" do
context "when dark theme is not present" do
context "when dark logo is not present" do
it "should return nothing" do
expect(helper.application_logo_dark_url.present?).to eq(false)
end
end
end
context "when dark theme is present" do
before do
_dark_theme =
Theme.create(
name: "Dark",
user_id: Discourse::SYSTEM_USER_ID,
color_scheme_id:
ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Dark"]).id,
)
end
context "when dark logo is not present" do
it "should return nothing" do
expect(helper.application_logo_dark_url.present?).to eq(false)
end
end
context "when dark logo is present" do
before { SiteSetting.logo_dark = Fabricate(:upload, url: "/images/logo-dark.png") }
it "should return correct url" do
expect(helper.application_logo_dark_url).to eq(SiteSetting.site_logo_dark_url)
end
end
end
context "when dark theme is present and selected" do
before do
dark_theme =
Theme.create(
name: "Dark",
user_id: Discourse::SYSTEM_USER_ID,
color_scheme_id:
ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Dark"]).id,
)
helper.request.env[:resolved_theme_id] = dark_theme.id
SiteSetting.logo_dark = Fabricate(:upload, url: "/images/logo-dark.png")
end
it "should return nothing" do
expect(helper.application_logo_url).to eq(SiteSetting.site_logo_dark_url)
expect(helper.application_logo_dark_url.present?).to eq(false)
end
end
end
describe "#mobile_device?" do
it "is false if user agent is not mobile" do
controller
.request
.stubs(:user_agent)
.returns(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
)
expect(helper.mobile_device?).to be_falsey
end
it "is true for iPhone" do
controller
.request
.stubs(:user_agent)
.returns(
"Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1",
)
expect(helper.mobile_device?).to eq(true)
end
it "is true for Android Samsung Galaxy" do
controller
.request
.stubs(:user_agent)
.returns(
"Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-G925F Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36",
)
expect(helper.mobile_device?).to eq(true)
end
it "is true for Android Google Nexus 5X" do
controller
.request
.stubs(:user_agent)
.returns(
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5X Build/MDB08I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.43 Mobile Safari/537.36",
)
expect(helper.mobile_device?).to eq(true)
end
it "is false for iPad" do
controller
.request
.stubs(:user_agent)
.returns(
"Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B14 3 Safari/601.1",
)
expect(helper.mobile_device?).to eq(false)
end
it "is false for Nexus 10 tablet" do
controller
.request
.stubs(:user_agent)
.returns(
"Mozilla/5.0 (Linux; Android 5.1.1; Nexus 10 Build/LMY49G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.91 Safari/537.36",
)
expect(helper.mobile_device?).to be_falsey
end
it "is false for Nexus 7 tablet" do
controller
.request
.stubs(:user_agent)
.returns(
"Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MMB29Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.91 Safari/537.36",
)
expect(helper.mobile_device?).to be_falsey
end
end
describe "#html_classes" do
fab!(:user)
it "includes 'rtl' when the I18n.locale is rtl" do
I18n.stubs(:locale).returns(:he)
expect(helper.html_classes.split(" ")).to include("rtl")
end
it "returns an empty string when the I18n.locale is not rtl" do
I18n.stubs(:locale).returns(:zh_TW)
expect(helper.html_classes.split(" ")).not_to include("rtl")
end
describe "text size" do
context "with a user option" do
before do
user.user_option.text_size = "larger"
user.user_option.save!
helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
end
it "ignores invalid text sizes" do
helper.request.cookies["text_size"] = "invalid"
expect(helper.html_classes.split(" ")).to include("text-size-larger")
end
it "ignores missing text size" do
helper.request.cookies["text_size"] = nil
expect(helper.html_classes.split(" ")).to include("text-size-larger")
end
it "ignores cookies with lower sequence" do
user.user_option.update!(text_size_seq: 2)
helper.request.cookies["text_size"] = "normal|1"
expect(helper.html_classes.split(" ")).to include("text-size-larger")
end
it "prioritises the cookie specified text size" do
user.user_option.update!(text_size_seq: 2)
helper.request.cookies["text_size"] = "largest|4"
expect(helper.html_classes.split(" ")).to include("text-size-largest")
end
it "includes the user specified text size" do
helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
expect(helper.html_classes.split(" ")).to include("text-size-larger")
end
end
it "falls back to the default text size for anon" do
expect(helper.html_classes.split(" ")).to include("text-size-normal")
SiteSetting.default_text_size = "largest"
expect(helper.html_classes.split(" ")).to include("text-size-largest")
end
end
it "includes 'anon' for anonymous users and excludes when logged in" do
expect(helper.html_classes.split(" ")).to include("anon")
helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
expect(helper.html_classes.split(" ")).not_to include("anon")
end
end
describe "gsub_emoji_to_unicode" do
it "converts all emoji to unicode" do
expect(
helper.gsub_emoji_to_unicode("Boat Talk: my :sailboat: boat: why is it so slow? :snail:"),
).to eq("Boat Talk: my ⛵ boat: why is it so slow? 🐌")
end
end
describe "preloaded_json" do
fab!(:user)
it "returns empty JSON if preloader is not initialized" do
@application_layout_preloader = nil
expect(helper.preloaded_json).to eq("{}")
end
it "escapes and strips invalid unicode and strips in json body" do
@application_layout_preloader =
ApplicationLayoutPreloader.new(
guardian: Guardian.new(user),
theme_id: nil,
theme_target: nil,
login_method: nil,
)
@application_layout_preloader.store_preloaded("test", %{["</script><script> \x80"]})
expect(helper.preloaded_json).to include(
%{"test":"[\\"\\u003c\\\\/script\\u003e\\u003cscript\\u003e \uFFFD\\"]"},
)
expect(helper.preloaded_json).not_to include("</script>")
end
end
describe "client_side_setup_data" do
context "when Rails.env.development? is true" do
before { Rails.env.stubs(:development?).returns(true) }
it "returns the correct service worker url" do
expect(helper.client_side_setup_data[:service_worker_url]).to eq("service-worker.js")
end
it "returns the svg_icon_list in the setup data" do
expect(helper.client_side_setup_data[:svg_icon_list]).not_to eq(nil)
end
it "does not return debug_preloaded_app_data without the env var" do
expect(helper.client_side_setup_data.key?(:debug_preloaded_app_data)).to eq(false)
end
context "if the DEBUG_PRELOADED_APP_DATA env var is provided" do
before { ENV["DEBUG_PRELOADED_APP_DATA"] = "true" }
it "returns that key as true" do
expect(helper.client_side_setup_data[:debug_preloaded_app_data]).to eq(true)
end
end
end
end
describe "crawlable_meta_data" do
it "escapes the description exactly once" do
result =
helper.crawlable_meta_data(description: %(Tom & O'Reilly "><script>alert(1)</script>))
expect(result).to include(
%(<meta property="og:description" content="Tom &amp; O&#39;Reilly &quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;" />),
)
expect(result).not_to include("<script>")
end
it "Supports ASCII URLs with odd chars" do
result =
helper.crawlable_meta_data(
url: (+"http://localhost/ión").force_encoding("ASCII-8BIT").freeze,
)
expect(result).to include("ión")
end
context "with opengraph image" do
it "returns the correct image" do
SiteSetting.opengraph_image = Fabricate(:upload, url: "/images/og-image.png")
SiteSetting.x_summary_large_image = Fabricate(:upload, url: "/images/twitter.png")
SiteSetting.large_icon = Fabricate(:upload, url: "/images/large_icon.png")
SiteSetting.apple_touch_icon =
Fabricate(:upload, url: "/images/default-apple-touch-icon.png")
SiteSetting.logo = Fabricate(:upload, url: "/images/d-logo-sketch.png")
expect(helper.crawlable_meta_data(image: "some-image.png")).to include("some-image.png")
expect(helper.crawlable_meta_data).to include(SiteSetting.site_opengraph_image_url)
SiteSetting.opengraph_image = ""
expect(helper.crawlable_meta_data).to include(SiteSetting.site_x_summary_large_image_url)
SiteSetting.x_summary_large_image = ""
expect(helper.crawlable_meta_data).to include(SiteSetting.site_large_icon_url)
SiteSetting.large_icon = ""
SiteSetting.logo_small = ""
expect(helper.crawlable_meta_data).to include(SiteSetting.site_logo_url)
SiteSetting.logo = ""
expect(helper.crawlable_meta_data).to include(
Upload.find(SiteIconManager::SKETCH_LOGO_ID).url,
)
end
it "does not allow SVG images for twitter:image, falls back to site logo or nothing if site logo is SVG too" do
SiteSetting.logo = Fabricate(:upload, url: "/images/d-logo-sketch.png")
SiteSetting.opengraph_image = Fabricate(:upload, url: "/images/og-image.png")
expect(helper.crawlable_meta_data).to include(<<~HTML)
<meta name=\"twitter:image\" content=\"#{SiteSetting.site_opengraph_image_url}\" />
HTML
SiteSetting.opengraph_image = Fabricate(:upload, url: "/images/og-image.svg")
expect(helper.crawlable_meta_data).to include(<<~HTML)
<meta name=\"twitter:image\" content=\"#{SiteSetting.site_logo_url}\" />
HTML
SiteSetting.x_summary_large_image = Fabricate(:upload, url: "/images/twitter.png")
expect(helper.crawlable_meta_data).to include(<<~HTML)
<meta name=\"twitter:image\" content=\"#{SiteSetting.site_x_summary_large_image_url}\" />
HTML
SiteSetting.x_summary_large_image = Fabricate(:upload, url: "/images/twitter.svg")
expect(helper.crawlable_meta_data).to include(<<~HTML)
<meta name=\"twitter:image\" content=\"#{SiteSetting.site_logo_url}\" />
HTML
SiteSetting.logo = Fabricate(:upload, url: "/images/d-logo-sketch.svg")
expect(helper.crawlable_meta_data).not_to include("twitter:image")
end
end
context "with opengraph image dimensions" do
it "includes og:image:width and og:image:height when provided" do
result =
helper.crawlable_meta_data(image: "some-image.png", image_width: 1024, image_height: 768)
expect(result).to include('<meta property="og:image:width" content="1024" />')
expect(result).to include('<meta property="og:image:height" content="768" />')
end
it "does not include og:image dimensions when not provided" do
result = helper.crawlable_meta_data(image: "some-image.png")
expect(result).not_to include("og:image:width")
expect(result).not_to include("og:image:height")
end
end
context "with opengraph image type" do
it "includes og:image:type when provided" do
result = helper.crawlable_meta_data(image: "some-image.png", image_type: "image/png")
expect(result).to include('<meta property="og:image:type" content="image/png" />')
end
it "does not include og:image:type when not provided" do
result = helper.crawlable_meta_data(image: "some-image.png")
expect(result).not_to include("og:image:type")
end
end
context "with breadcrumbs" do
subject(:metadata) { helper.crawlable_meta_data(breadcrumbs: breadcrumbs) }
let(:breadcrumbs) do
[{ name: "section1", color: "ff0000" }, { name: "section2", color: "0000ff" }]
end
let(:tags) { <<~HTML.strip }
<meta property="og:article:section" content="section1" />
<meta property="og:article:section:color" content="ff0000" />
<meta property="og:article:section" content="section2" />
<meta property="og:article:section:color" content="0000ff" />
HTML
it "generates section and color tags" do
expect(metadata).to include tags
end
end
context "with tags" do
subject(:metadata) { helper.crawlable_meta_data(tags: tags) }
let(:tags) { %w[tag1 tag2] }
let(:output_tags) { <<~HTML.strip }
<meta property="og:article:tag" content="tag1" />
<meta property="og:article:tag" content="tag2" />
HTML
it "generates tag tags" do
expect(metadata).to include output_tags
end
end
context "with custom site name" do
before { SiteSetting.title = "Default Site Title" }
it "uses the provided site name in og:site_name" do
custom_site_name = "Custom Site Name"
result = helper.crawlable_meta_data(site_name: custom_site_name)
expect(result).to include(
"<meta property=\"og:site_name\" content=\"#{custom_site_name}\" />",
)
end
it "falls back to the default site title if no custom site name is provided" do
result = helper.crawlable_meta_data
expect(result).to include(
"<meta property=\"og:site_name\" content=\"#{SiteSetting.title}\" />",
)
end
end
context "with reading time and likes" do
it "uses translated strings for reading time and likes" do
result = helper.crawlable_meta_data(read_time: 5, like_count: 10)
expect(result).to include(
"<meta name=\"twitter:label1\" value=\"#{I18n.t("reading_time")}\" />",
)
expect(result).to include(
"<meta name=\"twitter:data1\" value=\"#{I18n.t("reading_time_minutes", count: 5)}\" />",
)
expect(result).to include("<meta name=\"twitter:label2\" value=\"#{I18n.t("likes")}\" />")
expect(result).to include(
"<meta name=\"twitter:data2\" value=\"#{I18n.t("likes_count", count: 10)}\" />",
)
end
it "handles singular reading time correctly" do
result = helper.crawlable_meta_data(read_time: 1, like_count: 1)
expect(result).to include("1 min 🕑")
expect(result).not_to include("1 mins")
end
it "does not include twitter card labels when read_time or like_count is missing" do
result = helper.crawlable_meta_data(read_time: 5)
expect(result).not_to include("twitter:label1")
result = helper.crawlable_meta_data(like_count: 10)
expect(result).not_to include("twitter:label1")
end
end
end
describe "#title_content" do
it "returns the correct title" do
SiteSetting.title = "Test Title"
result = helper.title_content
expect(result).to include("Test Title")
end
it "accepts a content argument" do
helper.stubs(:content_for?).with(:title).returns(true)
helper.stubs(:content_for).with(:title).returns("Custom Title")
result = helper.title_content
expect(result).to include("Custom Title")
end
end
describe "#description_content" do
it "returns the correct description" do
SiteSetting.site_description = "Test Description"
result = helper.description_content
expect(result).to include("Test Description")
end
it "accepts a content argument" do
@description_meta = "Custom Description"
result = helper.description_content
expect(result).to include("Custom Description")
end
end
describe "when a plugin registers the :meta_data_content modifier" do
let!(:plugin) { Plugin::Instance.new }
let!(:modifier) { :meta_data_content }
let!(:block) do
Proc.new do |content, property, opts|
next "modified by plugin" if property == :description
next "BIG TITLE" if property == :title
content
end
end
before { DiscoursePluginRegistry.register_modifier(plugin, modifier, &block) }
after { DiscoursePluginRegistry.unregister_modifier(plugin, modifier, &block) }
it "allows the plugin to modify the meta tags" do
result =
helper.crawlable_meta_data(
description: "This is a test description",
title: "to be overridden",
)
expect(result).to include(
"<meta property=\"og:description\" content=\"modified by plugin\" />",
)
expect(result).to include("<meta property=\"og:title\" content=\"BIG TITLE\" />")
end
it "modifies the title tag" do
title = helper.title_content
expect(title).to include("BIG TITLE")
end
it "modifies the description tag" do
description = helper.description_content
expect(description).to include("modified by plugin")
end
it "does not modify the `title` SiteSetting" do
SiteSetting.title = "Test Title"
result = helper.title_content
expect(result).to include("BIG TITLE")
expect(SiteSetting.title).to eq("Test Title")
end
it "does not modify the `site_description` SiteSetting" do
SiteSetting.site_description = "Test Description"
result = helper.description_content
expect(result).to include("modified by plugin")
expect(SiteSetting.site_description).to eq("Test Description")
end
end
describe "discourse_color_scheme_stylesheets" do
fab!(:user)
it "returns a stylesheet link tag by default" do
cs_stylesheets = helper.discourse_color_scheme_stylesheets
expect(cs_stylesheets).to include("stylesheets/color_definitions")
end
it "returns two color scheme link tags when dark mode is enabled" do
Theme.find_default.update!(dark_color_scheme_id: ColorScheme.where(name: "Dark").pick(:id))
cs_stylesheets = helper.discourse_color_scheme_stylesheets
expect(cs_stylesheets).to include("(prefers-color-scheme: dark)")
expect(cs_stylesheets.scan("stylesheets/color_definitions").size).to eq(2)
end
it "handles a missing dark color scheme gracefully" do
scheme = ColorScheme.create!(name: "pyramid")
Theme.find_default.update!(dark_color_scheme_id: scheme.id)
scheme.destroy!
cs_stylesheets = helper.discourse_color_scheme_stylesheets
expect(cs_stylesheets).to include("stylesheets/color_definitions")
expect(cs_stylesheets).not_to include("(prefers-color-scheme: dark)")
end
context "with custom light scheme" do
before do
@new_cs = Fabricate(:color_scheme, name: "Flamboyant", user_selectable: true)
user.user_option.color_scheme_id = @new_cs.id
user.user_option.save!
helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
end
it "returns color scheme from user option value" do
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).to include("color_definitions_flamboyant")
end
it "falls back to base scheme when the scheme is no longer user selectable" do
@new_cs.update!(user_selectable: false)
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).not_to include("color_definitions_flamboyant")
expect(color_stylesheets).to include("color_definitions_light-default")
end
it "keeps a non-user-selectable scheme that is the theme's own color scheme" do
@new_cs.update!(user_selectable: false)
Theme.find_default.update!(color_scheme_id: @new_cs.id)
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).to include("color_definitions_flamboyant")
end
it "returns color scheme from cookie value" do
cs = ColorScheme.where(name: "Dark").first
helper.request.cookies["color_scheme_id"] = cs.id
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).to include("color_definitions_dark")
expect(color_stylesheets).not_to include("color_definitions_flamboyant")
end
it "falls back to base scheme with invalid cookie value" do
helper.request.cookies["color_scheme_id"] = -50
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).not_to include("color_definitions_flamboyant")
expect(color_stylesheets).to include("color_definitions_light-default")
end
end
context "with dark scheme with user option and/or cookies" do
before do
user.user_option.interface_color_mode = UserOption::LIGHT_MODE
user.user_option.save!
helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
@new_cs = Fabricate(:color_scheme, name: "Custom Color Scheme", user_selectable: true)
Theme.find_default.update!(dark_color_scheme_id: ColorScheme.where(name: "Dark").pick(:id))
end
it "returns no dark scheme stylesheet when user has disabled that option" do
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).to include("stylesheets/color_definitions")
expect(color_stylesheets).not_to include("(prefers-color-scheme: dark)")
end
it "returns user-selected dark color scheme stylesheet" do
user.user_option.update!(
dark_scheme_id: @new_cs.id,
interface_color_mode: UserOption::AUTO_MODE,
)
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).to include("(prefers-color-scheme: dark)")
expect(color_stylesheets).to include("custom-color-scheme")
end
it "respects cookie value over user option for dark color scheme" do
user.user_option.update!(interface_color_mode: UserOption::AUTO_MODE)
helper.request.cookies["dark_scheme_id"] = @new_cs.id
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).to include("(prefers-color-scheme: dark)")
expect(color_stylesheets).to include("custom-color-scheme")
end
it "returns no dark scheme with invalid cookie value" do
helper.request.cookies["dark_scheme_id"] = -10
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).not_to include("(prefers-color-scheme: dark)")
end
end
end
describe "dark_color_scheme?" do
it "returns false for the base color scheme" do
expect(helper.dark_color_scheme?).to eq(false)
end
it "works correctly for a dark scheme" do
dark_theme =
Theme.create(
name: "Dark",
user_id: Discourse::SYSTEM_USER_ID,
color_scheme_id:
ColorScheme.find_by(base_scheme_id: ColorScheme::NAMES_TO_ID_MAP["Dark"]).id,
)
helper.request.env[:resolved_theme_id] = dark_theme.id
expect(helper.dark_color_scheme?).to eq(true)
end
end
describe "html_lang" do
fab!(:user)
before do
I18n.locale = :de
SiteSetting.default_locale = :fr
end
it "returns default locale if no request" do
helper.request = nil
expect(helper.html_lang).to eq(SiteSetting.default_locale)
end
it "returns current user locale if request" do
helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
expect(helper.html_lang).to eq(I18n.locale.to_s)
end
end
describe "#discourse_theme_color_meta_tags" do
before do
light = Fabricate(:color_scheme, user_selectable: true)
light.color_scheme_colors << ColorSchemeColor.new(name: "header_background", hex: "abcdef")
light.save!
helper.request.cookies["color_scheme_id"] = light.id
dark = Fabricate(:color_scheme, user_selectable: true)
dark.color_scheme_colors << ColorSchemeColor.new(name: "header_background", hex: "defabc")
dark.save!
helper.request.cookies["dark_scheme_id"] = dark.id
end
it "renders theme-color meta for the light scheme with media=(prefers-color-scheme: light) and another one for the dark scheme with media=(prefers-color-scheme: dark)" do
expect(helper.discourse_theme_color_meta_tags).to eq(<<~HTML)
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#abcdef">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#defabc">
HTML
end
it "doesn't render theme-color meta tag for the dark scheme if none is set" do
Theme.find_default.update!(dark_color_scheme_id: -1)
helper.request.cookies.delete("dark_scheme_id")
expect(helper.discourse_theme_color_meta_tags).to eq(<<~HTML)
<meta name="theme-color" media="all" content="#abcdef">
HTML
end
end
describe "#discourse_color_scheme_meta_tag" do
fab!(:color_scheme)
before { Theme.find_default.update!(dark_color_scheme_id: -1) }
it "renders a 'light' color-scheme if no dark scheme is set and the current scheme is light" do
ColorSchemeRevisor.revise(
color_scheme,
{ colors: [{ name: "primary", hex: "333333" }, { name: "secondary", hex: "DDDDDD" }] },
)
helper.request.cookies["color_scheme_id"] = color_scheme.id
expect(helper.discourse_color_scheme_meta_tag).to eq(<<~HTML)
<meta name="color-scheme" content="light">
HTML
end
it "renders a 'dark' color-scheme if no dark scheme is set and the default scheme is dark" do
ColorSchemeRevisor.revise(
color_scheme,
{ colors: [{ name: "primary", hex: "F8F8F8" }, { name: "secondary", hex: "232323" }] },
)
@scheme_id = color_scheme.id
expect(helper.discourse_color_scheme_meta_tag).to eq(<<~HTML)
<meta name="color-scheme" content="dark">
HTML
end
it "renders a 'light dark' color-scheme if a dark scheme is set" do
dark = Fabricate(:color_scheme, user_selectable: true)
dark.save!
helper.request.cookies["dark_scheme_id"] = dark.id
expect(helper.discourse_color_scheme_meta_tag).to eq(<<~HTML)
<meta name="color-scheme" content="light dark">
HTML
end
end
describe "#dark_scheme_id" do
fab!(:dark_scheme) { Fabricate(:color_scheme, user_selectable: true) }
fab!(:light_scheme) { Fabricate(:color_scheme, user_selectable: true) }
before do
helper.request.cookies["color_scheme_id"] = light_scheme.id
helper.request.cookies["dark_scheme_id"] = dark_scheme.id
end
it "returns the value set in the dark_scheme_id cookie" do
expect(helper.dark_scheme_id).to eq(dark_scheme.id)
end
end
describe "#forced_light_mode?" do
fab!(:user)
context "when the user preference in the database is set to light" do
before do
user.user_option.update!(interface_color_mode: UserOption::LIGHT_MODE)
helper.stubs(:current_user).returns(user)
end
it "returns true if the forced_color_mode cookie is set to `light`" do
helper.request.cookies["forced_color_mode"] = "light"
expect(helper.forced_light_mode?).to eq(true)
end
it "returns false if the forced_color_mode cookie is set to `dark`" do
helper.request.cookies["forced_color_mode"] = "dark"
expect(helper.forced_light_mode?).to eq(false)
end
it "returns false if the forced_color_mode cookie is set to `auto`" do
helper.request.cookies["forced_color_mode"] = "auto"
expect(helper.forced_light_mode?).to eq(false)
end
it "returns true if the forced_color_mode cookie is not set" do
helper.request.cookies["forced_color_mode"] = nil
expect(helper.forced_light_mode?).to eq(true)
end
end
context "when the user preference in the database is set to dark" do
before do
user.user_option.update!(interface_color_mode: UserOption::DARK_MODE)
helper.stubs(:current_user).returns(user)
end
it "returns true if the forced_color_mode cookie is set to `light`" do
helper.request.cookies["forced_color_mode"] = "light"
expect(helper.forced_light_mode?).to eq(true)
end
it "returns false if the forced_color_mode cookie is set to `dark`" do
helper.request.cookies["forced_color_mode"] = "dark"
expect(helper.forced_light_mode?).to eq(false)
end
it "returns false if the forced_color_mode cookie is set to `auto`" do
helper.request.cookies["forced_color_mode"] = "auto"
expect(helper.forced_light_mode?).to eq(false)
end
it "returns false if the forced_color_mode cookie is not set" do
helper.request.cookies["forced_color_mode"] = nil
expect(helper.forced_light_mode?).to eq(false)
end
end
context "when the user preference in the database is set to auto" do
before do
user.user_option.update!(interface_color_mode: UserOption::AUTO_MODE)
helper.stubs(:current_user).returns(user)
end
it "returns true if the forced_color_mode cookie is set to `light`" do
helper.request.cookies["forced_color_mode"] = "light"
expect(helper.forced_light_mode?).to eq(true)
end
it "returns false if the forced_color_mode cookie is set to `dark`" do
helper.request.cookies["forced_color_mode"] = "dark"
expect(helper.forced_light_mode?).to eq(false)
end
it "returns false if the forced_color_mode cookie is set to `auto`" do
helper.request.cookies["forced_color_mode"] = "auto"
expect(helper.forced_light_mode?).to eq(false)
end
it "returns false if the forced_color_mode cookie is not set" do
helper.request.cookies["forced_color_mode"] = nil
expect(helper.forced_light_mode?).to eq(false)
end
end
end
describe "#forced_dark_mode?" do
fab!(:user)
context "when the user preference in the database is set to light" do
before do
user.user_option.update!(interface_color_mode: UserOption::LIGHT_MODE)
helper.stubs(:current_user).returns(user)
end
it "returns false if the forced_color_mode cookie is set to `light`" do
helper.request.cookies["forced_color_mode"] = "light"
expect(helper.forced_dark_mode?).to eq(false)
end
it "returns true if the forced_color_mode cookie is set to `dark`" do
helper.request.cookies["forced_color_mode"] = "dark"
expect(helper.forced_dark_mode?).to eq(true)
end
it "returns false if the forced_color_mode cookie is set to `auto`" do
helper.request.cookies["forced_color_mode"] = "auto"
expect(helper.forced_dark_mode?).to eq(false)
end
it "returns true if the forced_color_mode cookie is not set" do
helper.request.cookies["forced_color_mode"] = nil
expect(helper.forced_dark_mode?).to eq(false)
end
end
context "when the user preference in the database is set to dark" do
before do
user.user_option.update!(interface_color_mode: UserOption::DARK_MODE)
helper.stubs(:current_user).returns(user)
end
it "returns false if the forced_color_mode cookie is set to `light`" do
helper.request.cookies["forced_color_mode"] = "light"
expect(helper.forced_dark_mode?).to eq(false)
end
it "returns true if the forced_color_mode cookie is set to `dark`" do
helper.request.cookies["forced_color_mode"] = "dark"
expect(helper.forced_dark_mode?).to eq(true)
end
it "returns false if the forced_color_mode cookie is set to `auto`" do
helper.request.cookies["forced_color_mode"] = "auto"
expect(helper.forced_dark_mode?).to eq(false)
end
it "returns true if the forced_color_mode cookie is not set" do
helper.request.cookies["forced_color_mode"] = nil
expect(helper.forced_dark_mode?).to eq(true)
end
end
context "when the user preference in the database is set to auto" do
before do
user.user_option.update!(interface_color_mode: UserOption::AUTO_MODE)
helper.stubs(:current_user).returns(user)
end
it "returns false if the forced_color_mode cookie is set to `light`" do
helper.request.cookies["forced_color_mode"] = "light"
expect(helper.forced_dark_mode?).to eq(false)
end
it "returns true if the forced_color_mode cookie is set to `dark`" do
helper.request.cookies["forced_color_mode"] = "dark"
expect(helper.forced_dark_mode?).to eq(true)
end
it "returns false if the forced_color_mode cookie is set to `auto`" do
helper.request.cookies["forced_color_mode"] = "auto"
expect(helper.forced_dark_mode?).to eq(false)
end
it "returns false if the forced_color_mode cookie is not set" do
helper.request.cookies["forced_color_mode"] = nil
expect(helper.forced_dark_mode?).to eq(false)
end
end
end
describe "#crawler_topic_container_schema" do
fab!(:topic)
it "returns DiscussionForumPosting schema attributes by default" do
result = helper.crawler_topic_container_schema(topic)
expect(result).to include("itemscope")
expect(result).to include('itemtype="http://schema.org/DiscussionForumPosting"')
end
end
describe "#crawler_post_schema" do
fab!(:topic)
fab!(:first_post) { Fabricate(:post, topic: topic) }
fab!(:reply) { Fabricate(:post, topic: topic) }
it "returns empty string for the first post" do
expect(helper.crawler_post_schema(first_post, topic)).to eq("")
end
it "returns Comment schema attributes for reply posts" do
result = helper.crawler_post_schema(reply, topic)
expect(result).to include('itemprop="comment"')
expect(result).to include("itemscope")
expect(result).to include('itemtype="http://schema.org/Comment"')
end
end
describe "#shared_session_key" do
fab!(:user)
before { SiteSetting.long_polling_base_url = "https://mb.example.com/" }
context "when the request carries an auth token" do
let(:auth_token) { UserAuthToken.generate!(user_id: user.id) }
before do
helper.stubs(:current_user).returns(user)
helper.request.env[Auth::DefaultCurrentUserProvider::USER_TOKEN_KEY] = auth_token
end
it "binds the stored value to the auth token" do
key = helper.shared_session_key
expect(
Discourse.redis.get(Auth::DefaultCurrentUserProvider.shared_session_redis_key(key)),
).to eq(auth_token.id.to_s)
end
end
context "when the auth token does not belong to the current user" do
fab!(:other_user, :user)
before do
helper.stubs(:current_user).returns(user)
helper.request.env[
Auth::DefaultCurrentUserProvider::USER_TOKEN_KEY
] = UserAuthToken.generate!(user_id: other_user.id)
end
it "returns no shared session key" do
expect(helper.shared_session_key).to eq(nil)
end
end
context "when the auth token is impersonating the current user" do
fab!(:admin)
let(:auth_token) do
UserAuthToken
.generate!(user_id: admin.id)
.tap do |token|
token.update!(impersonated_user_id: user.id, impersonation_expires_at: 1.hour.from_now)
end
end
before do
helper.stubs(:current_user).returns(user)
helper.request.env[Auth::DefaultCurrentUserProvider::USER_TOKEN_KEY] = auth_token
end
it "binds the stored value to the impersonating token" do
key = helper.shared_session_key
expect(
Discourse.redis.get(Auth::DefaultCurrentUserProvider.shared_session_redis_key(key)),
).to eq(auth_token.id.to_s)
end
end
context "when the request carries no auth token" do
before do
helper.stubs(:current_user).returns(user)
helper.request.env[Auth::DefaultCurrentUserProvider::USER_TOKEN_KEY] = nil
end
it "returns no shared session key" do
expect(helper.shared_session_key).to eq(nil)
end
end
end
describe "#can_sign_up?" do
it "returns false when the site is in read only mode" do
expect(helper.can_sign_up?).to eq(true)
helper.instance_variable_set(:@readonly_mode, true)
expect(helper.can_sign_up?).to eq(false)
end
end
end