FEATURE: Add group, category, and display restrictions for signatures (#105)
* FEATURE: Add group, category, and display restrictions for signatures Adds five new site settings to control signature behavior: - signatures_allowed_groups: restrict signatures to specific groups (server-side enforced in both serializers and user update hook) - signatures_show_in_categories: limit signature display to specific categories - signatures_first_post_only: only show signatures on the OP - signatures_max_length: cap advanced mode signature length - signatures_max_image_height: constrain signature image height Also hardens URL validation (HTTP/HTTPS only) and reduces signature_url max_length from 32KB to 2048. All defaults are backward-compatible (empty/false = no change).
This commit is contained in:
committed by
GitHub
parent
50ef7ff0b3
commit
3af3307708
@@ -8,7 +8,30 @@ export default class PostSignature extends Component {
|
||||
context.currentUser?.custom_fields?.see_signatures ??
|
||||
context.siteSettings.signatures_visible_by_default;
|
||||
|
||||
return enabled && args.post.user_signature;
|
||||
if (!enabled || !args.post.user_signature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
context.siteSettings.signatures_first_post_only &&
|
||||
args.post.post_number !== 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowedCategories =
|
||||
context.siteSettings.signatures_show_in_categories;
|
||||
if (allowedCategories) {
|
||||
const categoryIds = allowedCategories
|
||||
.split("|")
|
||||
.map((id) => parseInt(id, 10));
|
||||
const postCategoryId = args.post.topic?.category_id;
|
||||
if (!categoryIds.includes(postCategoryId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@service siteSettings;
|
||||
@@ -17,6 +40,10 @@ export default class PostSignature extends Component {
|
||||
return this.siteSettings.signatures_advanced_mode;
|
||||
}
|
||||
|
||||
get imageMaxHeight() {
|
||||
return `max-height: ${this.siteSettings.signatures_max_image_height}px`;
|
||||
}
|
||||
|
||||
<template>
|
||||
<hr />
|
||||
{{#if this.isAdvancedModeEnabled}}
|
||||
@@ -26,7 +53,11 @@ export default class PostSignature extends Component {
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<img class="signature-img" src={{@post.user_signature}} />
|
||||
<img
|
||||
class="signature-img"
|
||||
src={{@post.user_signature}}
|
||||
style={{this.imageMaxHeight}}
|
||||
/>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
+45
-16
@@ -6,8 +6,22 @@ import DEditor from "discourse/components/d-editor";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class SignaturePreferences extends Component {
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
get canHaveSignature() {
|
||||
return this.currentUser?.can_have_signature !== false;
|
||||
}
|
||||
|
||||
get maxLength() {
|
||||
return this.siteSettings.signatures_max_length;
|
||||
}
|
||||
|
||||
get charactersRemaining() {
|
||||
const raw = this.args.model.custom_fields?.signature_raw || "";
|
||||
return this.maxLength - raw.length;
|
||||
}
|
||||
|
||||
@action
|
||||
updateSeeSignatures(event) {
|
||||
const model = this.args.model;
|
||||
@@ -38,23 +52,38 @@ export default class SignaturePreferences extends Component {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group signatures">
|
||||
<label class="control-label">{{i18n
|
||||
"signatures.my_signature"
|
||||
}}</label>
|
||||
<div class="controls input-xxlarge">
|
||||
{{#if this.siteSettings.signatures_advanced_mode}}
|
||||
<DEditor @value={{@model.custom_fields.signature_raw}} />
|
||||
{{else}}
|
||||
<input
|
||||
type="text"
|
||||
placeholder={{i18n "signatures.signature_placeholder"}}
|
||||
value={{@model.custom_fields.signature_url}}
|
||||
{{on "input" this.updateSignatureUrl}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.canHaveSignature}}
|
||||
<div class="control-group signatures">
|
||||
<label class="control-label">{{i18n
|
||||
"signatures.my_signature"
|
||||
}}</label>
|
||||
<div class="controls input-xxlarge">
|
||||
{{#if this.siteSettings.signatures_advanced_mode}}
|
||||
<DEditor @value={{@model.custom_fields.signature_raw}} />
|
||||
<span class="signature-char-count">
|
||||
{{i18n
|
||||
"signatures.characters_remaining"
|
||||
count=this.charactersRemaining
|
||||
}}
|
||||
</span>
|
||||
{{else}}
|
||||
<input
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder={{i18n "signatures.signature_placeholder"}}
|
||||
value={{@model.custom_fields.signature_url}}
|
||||
{{on "input" this.updateSignatureUrl}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="control-group signatures">
|
||||
<p class="signature-not-allowed">{{i18n
|
||||
"signatures.not_allowed"
|
||||
}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
max-width: calc(100% - 5px);
|
||||
}
|
||||
|
||||
.signature-img {
|
||||
max-width: calc(100% - 5px);
|
||||
}
|
||||
|
||||
.signature-preferences .d-editor {
|
||||
width: 100%;
|
||||
|
||||
@@ -13,3 +17,15 @@
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.signature-char-count {
|
||||
display: block;
|
||||
margin-top: 0.25em;
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
.signature-not-allowed {
|
||||
color: var(--primary-medium);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -5,3 +5,7 @@ en:
|
||||
show_signatures: "See user signatures below posts"
|
||||
my_signature: "My Signature"
|
||||
signature_placeholder: "A valid URL to an image"
|
||||
not_allowed: "Your account is not in a group that is allowed to use signatures."
|
||||
characters_remaining:
|
||||
one: "%{count} character remaining"
|
||||
other: "%{count} characters remaining"
|
||||
|
||||
@@ -3,3 +3,8 @@ en:
|
||||
signatures_enabled: "Enable user-made signatures below posts?"
|
||||
signatures_advanced_mode: "Let users use a full blown post editor to create signatures. RESETS all existing signatures."
|
||||
signatures_visible_by_default: "Make signatures visible by default. This makes signatures visible for anonymous visitors as well."
|
||||
signatures_allowed_groups: "Only allow users in these groups to have signatures. Leave empty to allow all users."
|
||||
signatures_show_in_categories: "Only show signatures in these categories. Leave empty to show in all categories."
|
||||
signatures_first_post_only: "Only show signatures on the first post in a topic."
|
||||
signatures_max_length: "Maximum number of characters allowed in advanced mode signatures."
|
||||
signatures_max_image_height: "Maximum height in pixels for signature images."
|
||||
|
||||
@@ -8,3 +8,25 @@ plugins:
|
||||
signatures_visible_by_default:
|
||||
default: false
|
||||
client: true
|
||||
signatures_allowed_groups:
|
||||
type: group_list
|
||||
list_type: compact
|
||||
default: ""
|
||||
allow_any: false
|
||||
refresh: true
|
||||
signatures_show_in_categories:
|
||||
type: category_list
|
||||
client: true
|
||||
default: ""
|
||||
signatures_first_post_only:
|
||||
default: false
|
||||
client: true
|
||||
signatures_max_length:
|
||||
default: 500
|
||||
min: 50
|
||||
max: 10000
|
||||
signatures_max_image_height:
|
||||
default: 150
|
||||
min: 20
|
||||
max: 1000
|
||||
client: true
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# name: discourse-signatures
|
||||
# about: Adds signatures to Discourse posts
|
||||
# meta_topic_id: 42263
|
||||
# version: 2.1.0
|
||||
# version: 2.2.0
|
||||
# author: Rafael Silva <xfalcox@gmail.com>
|
||||
# url: https://github.com/discourse/discourse-signatures
|
||||
|
||||
@@ -15,8 +15,8 @@ DiscoursePluginRegistry.serialized_current_user_fields << "signature_raw"
|
||||
|
||||
after_initialize do
|
||||
register_user_custom_field_type("see_signatures", :boolean)
|
||||
register_user_custom_field_type("signature_url", :string, max_length: 32_000)
|
||||
register_user_custom_field_type("signature_raw", :string, max_length: 1000)
|
||||
register_user_custom_field_type("signature_url", :string, max_length: 2048)
|
||||
register_user_custom_field_type("signature_raw", :string, max_length: 10_000)
|
||||
|
||||
# add to class and serializer to allow for default value for the setting
|
||||
add_to_class(:user, :see_signatures) do
|
||||
@@ -29,6 +29,11 @@ after_initialize do
|
||||
|
||||
add_to_serializer(:user, :see_signatures) { object.see_signatures }
|
||||
|
||||
add_to_serializer(:current_user, :can_have_signature) do
|
||||
allowed_groups = SiteSetting.signatures_allowed_groups_map
|
||||
allowed_groups.blank? || object.in_any_groups?(allowed_groups)
|
||||
end
|
||||
|
||||
register_editable_user_custom_field :see_signatures
|
||||
register_editable_user_custom_field :signature_url
|
||||
register_editable_user_custom_field :signature_raw
|
||||
@@ -37,19 +42,48 @@ after_initialize do
|
||||
allow_public_user_custom_field :signature_url
|
||||
|
||||
add_to_serializer(:post, :user_signature) do
|
||||
return nil unless object.user
|
||||
|
||||
allowed_groups = SiteSetting.signatures_allowed_groups_map
|
||||
return nil if allowed_groups.present? && !object.user.in_any_groups?(allowed_groups)
|
||||
|
||||
if SiteSetting.signatures_advanced_mode
|
||||
object.user.custom_fields["signature_cooked"] if object.user
|
||||
object.user.custom_fields["signature_cooked"]
|
||||
else
|
||||
object.user.custom_fields["signature_url"] if object.user
|
||||
object.user.custom_fields["signature_url"]
|
||||
end
|
||||
end
|
||||
|
||||
# This is the code responsible for cooking a new advanced mode sig on user update
|
||||
on(:user_updated) do |user|
|
||||
allowed_groups = SiteSetting.signatures_allowed_groups_map
|
||||
if allowed_groups.present? && !user.in_any_groups?(allowed_groups)
|
||||
user.custom_fields.delete("signature_url")
|
||||
user.custom_fields.delete("signature_raw")
|
||||
user.custom_fields.delete("signature_cooked")
|
||||
user.save
|
||||
next
|
||||
end
|
||||
|
||||
if user.custom_fields["signature_url"].present?
|
||||
url = user.custom_fields["signature_url"]
|
||||
begin
|
||||
parsed = URI.parse(url)
|
||||
raise URI::InvalidURIError unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
|
||||
rescue URI::InvalidURIError
|
||||
user.custom_fields.delete("signature_url")
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
if SiteSetting.signatures_advanced_mode && user.custom_fields["signature_raw"]
|
||||
raw = user.custom_fields["signature_raw"]
|
||||
max_length = SiteSetting.signatures_max_length
|
||||
raw = raw[0...max_length] if raw.length > max_length
|
||||
user.custom_fields["signature_raw"] = raw
|
||||
|
||||
cooked_sig =
|
||||
PrettyText.cook(
|
||||
user.custom_fields["signature_raw"],
|
||||
raw,
|
||||
omit_nofollow: user.has_trust_level?(TrustLevel[3]) && !SiteSetting.tl3_links_no_follow,
|
||||
)
|
||||
# avoid infinite recursion
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Discourse Signatures" do
|
||||
before do
|
||||
enable_current_plugin
|
||||
SiteSetting.signatures_enabled = true
|
||||
end
|
||||
|
||||
describe "group-based restriction" do
|
||||
fab!(:user)
|
||||
fab!(:group)
|
||||
fab!(:topic)
|
||||
|
||||
before do
|
||||
SiteSetting.signatures_advanced_mode = false
|
||||
user.custom_fields["signature_url"] = "https://example.com/sig.png"
|
||||
user.save_custom_fields
|
||||
end
|
||||
|
||||
context "when signatures_allowed_groups is empty" do
|
||||
before { SiteSetting.signatures_allowed_groups = "" }
|
||||
|
||||
it "includes user_signature in the post serializer" do
|
||||
post = Fabricate(:post, topic:, user:)
|
||||
json = PostSerializer.new(post, scope: Guardian.new(user), root: false).as_json
|
||||
expect(json[:user_signature]).to eq("https://example.com/sig.png")
|
||||
end
|
||||
|
||||
it "sets can_have_signature to true for current user" do
|
||||
json = CurrentUserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
|
||||
expect(json[:can_have_signature]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is in the allowed group" do
|
||||
before do
|
||||
SiteSetting.signatures_allowed_groups = group.id.to_s
|
||||
group.add(user)
|
||||
end
|
||||
|
||||
it "includes user_signature in the post serializer" do
|
||||
post = Fabricate(:post, topic:, user:)
|
||||
json = PostSerializer.new(post, scope: Guardian.new(user), root: false).as_json
|
||||
expect(json[:user_signature]).to eq("https://example.com/sig.png")
|
||||
end
|
||||
|
||||
it "sets can_have_signature to true" do
|
||||
json = CurrentUserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
|
||||
expect(json[:can_have_signature]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is not in the allowed group" do
|
||||
before { SiteSetting.signatures_allowed_groups = group.id.to_s }
|
||||
|
||||
it "returns nil for user_signature in the post serializer" do
|
||||
post = Fabricate(:post, topic:, user:)
|
||||
json = PostSerializer.new(post, scope: Guardian.new(user), root: false).as_json
|
||||
expect(json[:user_signature]).to be_nil
|
||||
end
|
||||
|
||||
it "sets can_have_signature to false" do
|
||||
json = CurrentUserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
|
||||
expect(json[:can_have_signature]).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "signature length enforcement" do
|
||||
fab!(:user)
|
||||
|
||||
before do
|
||||
SiteSetting.signatures_advanced_mode = true
|
||||
SiteSetting.signatures_max_length = 100
|
||||
end
|
||||
|
||||
it "truncates signature_raw to max_length on user update" do
|
||||
user.custom_fields["signature_raw"] = "a" * 200
|
||||
user.save_custom_fields
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
user.reload
|
||||
|
||||
expect(user.custom_fields["signature_raw"].length).to eq(100)
|
||||
end
|
||||
|
||||
it "does not truncate signatures within the limit" do
|
||||
user.custom_fields["signature_raw"] = "short signature"
|
||||
user.save_custom_fields
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
user.reload
|
||||
|
||||
expect(user.custom_fields["signature_raw"]).to eq("short signature")
|
||||
end
|
||||
end
|
||||
|
||||
describe "URL validation" do
|
||||
fab!(:user)
|
||||
|
||||
before { SiteSetting.signatures_advanced_mode = false }
|
||||
|
||||
it "removes invalid URLs on user update" do
|
||||
user.custom_fields["signature_url"] = "not a valid url %%"
|
||||
user.save_custom_fields
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
user.reload
|
||||
|
||||
expect(user.custom_fields["signature_url"]).to be_nil
|
||||
end
|
||||
|
||||
it "keeps valid URLs on user update" do
|
||||
user.custom_fields["signature_url"] = "https://example.com/sig.png"
|
||||
user.save_custom_fields
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
user.reload
|
||||
|
||||
expect(user.custom_fields["signature_url"]).to eq("https://example.com/sig.png")
|
||||
end
|
||||
|
||||
it "removes URLs with non-HTTP schemes on user update" do
|
||||
user.custom_fields["signature_url"] = "javascript:alert(1)"
|
||||
user.save_custom_fields
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
user.reload
|
||||
|
||||
expect(user.custom_fields["signature_url"]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "group restriction on user update" do
|
||||
fab!(:user)
|
||||
fab!(:group)
|
||||
|
||||
before do
|
||||
SiteSetting.signatures_advanced_mode = false
|
||||
user.custom_fields["signature_url"] = "https://example.com/sig.png"
|
||||
user.save_custom_fields
|
||||
end
|
||||
|
||||
it "clears signature data when user is not in the allowed group" do
|
||||
SiteSetting.signatures_allowed_groups = group.id.to_s
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
user.reload
|
||||
|
||||
expect(user.custom_fields["signature_url"]).to be_nil
|
||||
end
|
||||
|
||||
it "keeps signature data when user is in the allowed group" do
|
||||
SiteSetting.signatures_allowed_groups = group.id.to_s
|
||||
group.add(user)
|
||||
|
||||
DiscourseEvent.trigger(:user_updated, user)
|
||||
user.reload
|
||||
|
||||
expect(user.custom_fields["signature_url"]).to eq("https://example.com/sig.png")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,9 @@ RSpec.describe "Image signatures" do
|
||||
fab!(:user)
|
||||
fab!(:topic) { Fabricate(:topic, category: Fabricate(:category)) }
|
||||
fab!(:post) { Fabricate(:post, topic:) }
|
||||
let(:signature_image_url) { "data:abcdef," }
|
||||
let(:signature_image_url) do
|
||||
"http://#{Discourse.current_hostname}/images/discourse-logo-sketch-small.png"
|
||||
end
|
||||
|
||||
context "when signatures plugin is enabled" do
|
||||
before do
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Signature restrictions" do
|
||||
fab!(:user)
|
||||
fab!(:category)
|
||||
fab!(:topic) { Fabricate(:topic, category:) }
|
||||
let(:signature_image_url) { "data:abcdef," }
|
||||
|
||||
before do
|
||||
enable_current_plugin
|
||||
SiteSetting.signatures_enabled = true
|
||||
SiteSetting.signatures_advanced_mode = false
|
||||
SiteSetting.signatures_visible_by_default = true
|
||||
end
|
||||
|
||||
describe "group-based restriction" do
|
||||
fab!(:group)
|
||||
fab!(:post_with_sig) { Fabricate(:post, topic:, user:) }
|
||||
|
||||
before do
|
||||
user.custom_fields["signature_url"] = signature_image_url
|
||||
user.save_custom_fields
|
||||
end
|
||||
|
||||
context "when signatures_allowed_groups is empty" do
|
||||
before { SiteSetting.signatures_allowed_groups = "" }
|
||||
|
||||
it "shows signatures for all users" do
|
||||
sign_in(user)
|
||||
visit topic.url
|
||||
expect(page).to have_css("img.signature-img")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is in an allowed group" do
|
||||
before do
|
||||
SiteSetting.signatures_allowed_groups = group.id.to_s
|
||||
group.add(user)
|
||||
end
|
||||
|
||||
it "shows the signature below their post" do
|
||||
sign_in(user)
|
||||
visit topic.url
|
||||
expect(page).to have_css("img.signature-img[src='#{signature_image_url}']")
|
||||
end
|
||||
|
||||
it "shows the signature editing UI in preferences" do
|
||||
sign_in(user)
|
||||
visit "/my/preferences/profile"
|
||||
expect(page).to have_content(I18n.t("js.signatures.my_signature"))
|
||||
expect(page).to have_no_content(I18n.t("js.signatures.not_allowed"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is not in an allowed group" do
|
||||
before { SiteSetting.signatures_allowed_groups = group.id.to_s }
|
||||
|
||||
it "does not show the signature below their post" do
|
||||
sign_in(Fabricate(:user))
|
||||
visit topic.url
|
||||
expect(page).to have_no_css("img.signature-img")
|
||||
end
|
||||
|
||||
it "shows the restriction message in preferences" do
|
||||
sign_in(user)
|
||||
visit "/my/preferences/profile"
|
||||
expect(page).to have_content(I18n.t("js.signatures.not_allowed"))
|
||||
expect(page).to have_no_field(placeholder: I18n.t("js.signatures.signature_placeholder"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "category-based display" do
|
||||
fab!(:other_category, :category)
|
||||
fab!(:other_topic) { Fabricate(:topic, category: other_category) }
|
||||
|
||||
before do
|
||||
user.custom_fields["signature_url"] = signature_image_url
|
||||
user.save_custom_fields
|
||||
end
|
||||
|
||||
context "when signatures_show_in_categories is empty" do
|
||||
before { SiteSetting.signatures_show_in_categories = "" }
|
||||
|
||||
it "shows signatures in all categories" do
|
||||
Fabricate(:post, topic:, user:)
|
||||
sign_in(user)
|
||||
visit topic.url
|
||||
expect(page).to have_css("img.signature-img")
|
||||
end
|
||||
end
|
||||
|
||||
context "when signatures_show_in_categories is set" do
|
||||
before { SiteSetting.signatures_show_in_categories = category.id.to_s }
|
||||
|
||||
it "shows signatures in the allowed category" do
|
||||
Fabricate(:post, topic:, user:)
|
||||
sign_in(user)
|
||||
visit topic.url
|
||||
expect(page).to have_css("img.signature-img")
|
||||
end
|
||||
|
||||
it "does not show signatures in other categories" do
|
||||
Fabricate(:post, topic: other_topic, user:)
|
||||
sign_in(user)
|
||||
visit other_topic.url
|
||||
expect(page).to have_no_css("img.signature-img")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "first-post-only display" do
|
||||
before do
|
||||
user.custom_fields["signature_url"] = signature_image_url
|
||||
user.save_custom_fields
|
||||
end
|
||||
|
||||
context "when signatures_first_post_only is false" do
|
||||
before { SiteSetting.signatures_first_post_only = false }
|
||||
|
||||
it "shows signatures on all posts" do
|
||||
Fabricate(:post, topic:, user:)
|
||||
Fabricate(:post, topic:, user:)
|
||||
sign_in(user)
|
||||
visit topic.url
|
||||
expect(page).to have_css("img.signature-img", count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
context "when signatures_first_post_only is true" do
|
||||
before { SiteSetting.signatures_first_post_only = true }
|
||||
|
||||
it "shows signature only on the first post" do
|
||||
Fabricate(:post, topic:, user:)
|
||||
Fabricate(:post, topic:, user:)
|
||||
sign_in(user)
|
||||
visit topic.url
|
||||
expect(page).to have_css("img.signature-img", count: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user