I have the following test in rspec
it 'passes regex rules' do
job = create(:job)
job.valid?
expect(job.title).to match(/\A[\w\d .,:-#]+\z/)
end
This regex pattern matches the model pattern. What is the recommended way to test to make sure this pattern does not change in the model from future developers?
Basically I want to test for conditions that do not fall in the approved: can only have 0-9, A-Z, periods, colons, hypens, underscores, and spaces. No new lines (enter keys)
Update
Based on Generate random string based on Regex? I decided to go with (0..255).map(&:chr).select{|x| x != /\A[\w\d .,:-#]+\z/}.sample(5).join for now which appears to work, thoughts?
Based on the update, I went with the following:
describe 'title' do
let(:bad_string) { (0..255).map(&:chr).select{|x| x != /\A[\w\d .,:-#]+\z/}.sample(20).join }
it 'should exist' do
job = build(:job, title: nil)
job.valid?
expect(job.errors[:title].size).to eq(3)
end
it 'passes regex rules' do
job = build(:job, title: bad_string)
job.valid?
expect(job.errors[:title].size).to eq(1)
end
end
Related
json[:errors] = ["Username can't be blank", "Email can't be blank"]
The error, in en.yml, itself is provided as:
username: "can't be blank",
email: "can't be blank"
and the test:
expect(json[:errors]).to include t('activerecord.errors.messages.email')
Which fails because it's looking at the string "Email can't be blank", and "can't be blank" doesn't match it.
My question is what is the best (and by that I mean best practice) way to test
that the substring is included in a string contained inside the array json[:errors]
RSpec offers a range of matchers. In this case you'll need to use the include matcher (docs) to check each element of the array. And, you'll need to use the match regex matcher (docs) to match the substring:
expect(json[:errors]).to include(match(/can't be blank/))
For readability, the match regex matcher is aliased as a_string_matching, like this:
expect(json[:errors]).to include(a_string_matching(/can't be blank/))
UPDATE:
I just noticed that the OP's question include an array with multiple matching elements. The include matcher checks to see if ANY elements of the array match the criteria. If you to check to see if ALL elements of the array match the criteria, you can use the all matcher (docs).
expect(json[:errors]).to all(match(/can't be blank/))
Originally asked this question about Regex for Usernames: Usernames that cannot start or end with characters
How can I achieve this with correct syntax for Ruby on Rails?
Here's my current User.rb validation:
validates_format_of :username, with: /\A[\w\._]{3,28}\z/i
This validation allows underscores and periods, but the goal is to not allow them at the start or end of a username.
I'm trying to achieve these rules with Rails regex:
Can contain lowercase and uppercase letters and numbers
Can contain underscores and periods
Cannot contain 2 underscores in a row
Cannot contain 2 periods in a row
Cannot begin or end with an underscore or period
Cannot contain letters with accents
Must be between 3 and 28 letters in length
Valid:
Spicy_Pizza
97Indigos
Infinity.Beyond
Invalid:
_yahoo
powerup.
un__real
no..way
You may use
/\A(?=.{3,28}\z)[a-zA-Z0-9]+(?:[._][a-zA-Z0-9]+)*\z/
See the Rubular demo.
Details
\A - start of string
(?=.{3,28}\z) - 3 to 28 chars other than line break chars up to the end of the string are allowed/required
[a-zA-Z0-9]+ - one or more ASCII letters / digits
(?:[._][a-zA-Z0-9]+)* - 0+ sequences of:
[._] - a . or _
[a-zA-Z0-9]+ - one or more ASCII letters / digits
\z - end of string.
Although totally possible, as Wiktor's answer shows, my recommendation would be to not define this in a single regular expression, since:
The solution is quite confusing to understand, unless you know regular expressions quite well.
Similarly, the solution is quite difficult to update with new requirements, unless you understand regular expressions quite well.
By performing this entire check in one go, if a validation fails then you'll inevitably end up with one generic error message, e.g. "Invalid Format", which does not explain why it's invalid. The exercise is then left to the user to re-read the nontrivial format rules and understand why.
Instead, I would recommend defining a custom validation class, which can perform each of these checks separately (via easy to understand methods), and add a different error message upon each check failing.
Something along the lines of:
# app/models/user.rb
class User < ApplicationRecord
validates :username, presence: true, username: true
end
# app/validators/username_validator.rb
class UsernameValidator < ActiveModel::EachValidator
def validate(record, attribute, value)
validate_length(record, attribute, value)
validate_allowed_chars(record, attribute, value)
validate_sequential_chars(record, attribute, value)
validate_first_and_last_chars(record, attribute, value)
end
private
def validate_length(record, attribute, value)
unless value.length >= 3 && value.length <= 28
record.errors[attribute] << "must be between 3 and 28 characters long"
end
end
def validate_allowed_chars(record, attribute, value)
unless value =~ /\A[._a-zA-Z0-9]*\z/
record.errors[attribute] << "must only contain periods, underscores, a-z, A-Z or 0-9"
end
end
def validate_sequential_chars(record, attribute, value)
if value =~ /[._]{2}/
record.errors[attribute] << "cannot contain two consecutive periods or underscores"
end
end
def validate_first_and_last_chars(record, attribute, value)
if value =~ /\A[._]/ || value =~ /[._]\z/
record.errors[attribute] << "cannot start/end with a period or underscore"
end
end
end
So for instance, you asked above: "What if I needed to extend this to allow lowercase letters only?" I think it's now quite obvious how the code could be updated to accommodate such behaviour, but to be clear - all you'd need to do is:
def validate_allowed_chars(record, attribute, value)
unless value =~ /\A[._a-z0-9]*\z/
record.errors[attribute] << "must only contain periods, underscores, a-z or 0-9"
end
end
You could also now, quite easily, write tests for these validation checks, and assert that the correct validation is being performed by verifying against the contents of the error message; something that is not possible when all validation failures result in the same error,
Another benefit to this approach is that the code can easily be shared (perhaps with some slight behavioural differences). You could perform the same validation on multiple attributes, or multiple models, perhaps with different allowed lengths or formats.
After a bit of hacking I have come up with the following tests to make sure the regex pattern on my model validator is working correctly. I am wondering if there is a better way to test these conditions instead of building a bad string. I want to account for any and all characters outside the approved regex pattern. Different columns may have different validators too.
Model
validates :provider_unique_id,
presence: true,
length: { maximum: 50 },
format: { with: /\A[A-Za-z0-9]+\z/ }
Spec
describe 'provider unique id' do
let(:bad_string) { (0..255).map(&:chr).select { |x| x != /\A[A-Za-z0-9]+\z/ }.sample(20).join }
it 'should exist' do
shop.provider_unique_id = nil
expect(shop.valid?).to be_falsey
end
it 'passes regex rules' do
shop.provider_unique_id = bad_string
expect(shop.valid?).to be_falsey
end
end
Here's what I'd write if I were being extremely thorough. Imagine test-driving the validations, adding one test at a time and adding to the validations to make it pass.
describe '#provider_unique_id' do
%w(a z).each do |letter|
it "can be letter #{letter}" do
expect_to_be_valid letter
end
end
# after making this pass, I'd change the regex to use the i flag so I wouldn't need to test for Z
it "can be uppercase" do
expect_to_be_valid 'A'
end
[0, 9].each do |digit|
it "can be digit #{digit}" do
expect_to_be_valid digit
end
end
it "can be more than one character" do
expect_to_be_valid '00'
end
it "isn't nil" do
expect_to_be_invalid nil
end
it "isn't blank" do
expect_to_be_invalid ""
end
it "can be 50 characters long" do
expect_to_be_valid('0' * 50)
end
it "can't be longer than 50 characters" do
expect_to_be_invalid('0' * 51)
end
# I chose _ as a non-alphanumeric since it's the only non-alphanumeric word character.
# That is, it's as close to a valid character as it can be without be valid.
it "can't contain a non-alphanumeric character" do
expect_to_be_invalid '_'
end
# this example forces you to add \A
it "can't begin with a non-alphanumeric character" do
expect_to_be_invalid '_0'
end
# this example forces you to add \z
it "can't end with a non-alphanumeric character" do
expect_to_be_invalid '0_'
end
def expect_to_be_valid(provider_unique_id)
shop.provider_unique_id = provider_unique_id
expect(shop).to be_valid
end
def expect_to_be_invalid(provider_unique_id)
shop.provider_unique_id = provider_unique_id
expect(shop).to_not be_valid
end
end
I wouldn't randomly generate a bad string, because it wouldn't force you to write any additional code. I think the tests with _ are sufficient. Note that there are many more characters than ASCII 0-255, and it would be impractical to test them all.
You could imagine boundary-checking the ranges in the regex (a-z, A-Z, 0-9) by testing characters that come immediately before and after each range, but it's unlikely that someone would write code that would incorrectly include those characters, so I wouldn't go that far.
I'd like to test the validation of a model's attribute with rspec and factory_girl. The 'special' thing is, that one attribute (the name) isn't allowed to start with Numbers or special signs like %,&,$, ...
For testing this it would be great to write only one example like
it "is invalid with name starting by special character" do
["1","$","%"].each do |i|
name = i + "test"
FactoryGirl.build(:tutu, name: name).should_not be_valid
end
end
This work's for the first case but it won't return the result for the whole list. Is it possible to tell rspec not to stop on the error?
Do this instead:
["1","$","%"].each do |i|
it "is invalid with name starting by '#{i}'" do
FactoryGirl.build(:tutu, name: "#{i}test").should_not be_valid
end
end
I've written a regex to help validate a String for game character names. It's somehow passing seemingly invalid strings and not passing seemingly valid strings.
Requirements:
Starts with a capital letter
Has any number of alphanumeric characters after that (this includes spaces)
This is the rails code that does the validation in the Character Model:
validates :name, format: { with: %r{[A-Z][a-zA-Z0-9\s]*} }
Here's the unit test I'm using
test "character name should be properly formatted and does not contain any special characters" do
character = get_valid_character
assert character.valid?
character.name = "aBcd"
assert character.invalid?, "#{character.name} should be invalid"
character.name = "Number 1"
assert character.valid?, "#{character.name} should be valid"
character.name = "McDonalds"
assert character.valid?, "#{character.name} should be valid"
character.name = "Abcd."
assert character.invalid?, "#{character.name} should be invalid"
character.name = "Abcd%"
assert character.invalid?, "#{character.name} should be invalid"
end
The problems:
The regex passes "aBcd", "Abcd.", and "Abcd%" when it shouldn't. Now, I know this works because I tested this out in Python and it works just as you would expect.
What gives?
Thank you for your help!
Regular expressions look for matches anywhere in the given string unless told otherwise.
So the test string 'aBcd' is invalid, but it contains a valid substring: 'Bcd'. Same with 'Abcd%', where the valid substring is 'Abcd'.
If you want to match the entire string, use this as your regex:
# \A matches string beginning, \z matches string end
%r{\A[A-Z][a-zA-Z0-9\s]*\z}
PS: Some people will say to match the beginning of a string with ^ and the end with $. In Ruby, those symbols match the beginning and end of a line, not a string. So "ABCD\n%" would still match if you used ^ and $, but won't match if you use \A and \z. See the Rails security guide for more on this.
If you only want to match the capital letter at the beginning of the string, you need to put in the "start of line" marker ^ so it would look like:
validates :name, format: { with: %r{^[A-Z][a-zA-Z0-9\s]*} }
Check out Rubular to play around with your regex