You are on page 1of 37

Chapter6:Modelingusers|RubyonRailsTutorial(3rd

Ed.)
Modelingusers
InChapter5,weendedwithastubpageforcreatingnewusers(Section5.4).Overthecourseofthe
nextfivechapters,wellfulfillthepromiseimplicitinthisincipientsignuppage.Inthischapter,welltake
thefirstcriticalstepbycreatingadatamodelforusersofoursite,togetherwithawaytostorethatdata.
InChapter7,wellgiveuserstheabilitytosignupforoursiteandcreateauserprofilepage.Onceusers
cansignup,wellletthemloginandlogoutaswell(Chapter8),andinChapter9(Section9.2.1)well
learnhowtoprotectpagesfromimproperaccess.Finally,inChapter10welladdaccountactivation
(therebyconfirmingavalidemailaddress)andpasswordresets.Takentogether,thematerialin
Chapter6throughChapter10developsafullRailsloginandauthenticationsystem.Asyoumayknow,
therearevariousprebuiltauthenticationsolutionsforRailsBox6.1explainswhy,atleastatfirst,its
probablyabetterideatorollyourown.
Box6.1.Rollyourownauthenticationsystem
Virtuallyallwebapplicationsrequirealoginandauthenticationsystemofsomesort.Asaresult,most
webframeworkshaveaplethoraofoptionsforimplementingsuchsystems,andRailsisnoexception.
ExamplesofauthenticationandauthorizationsystemsincludeClearance,Authlogic,Devise,and
CanCan(aswellasnonRailsspecificsolutionsbuiltontopofOpenIDorOAuth).Itsreasonabletoask
whyweshouldreinventthewheel.Whynotjustuseanofftheshelfsolutioninsteadofrollingourown?
Forone,practicalexperienceshowsthatauthenticationonmostsitesrequiresextensivecustomization,
andmodifyingathirdpartyproductisoftenmoreworkthanwritingthesystemfromscratch.Inaddition,
offtheshelfsystemscanbeblackboxes,withpotentiallymysteriousinnardswhenyouwriteyourown
system,youarefarmorelikelytounderstandit.Moreover,recentadditionstoRails(Section6.3)makeit
easytowriteacustomauthenticationsystem.Finally,ifyoudoendupusingathirdpartysystemlater
on,youllbeinamuchbetterpositiontounderstandandmodifyitifyouvefirstbuiltoneyourself.
6.1Usermodel
Althoughtheultimategoalofthenextthreechaptersistomakeasignuppageforoursite(asmocked
upinFigure6.1),itwoulddolittlegoodnowtoacceptinformationfornewusers:wedontcurrentlyhave
anyplacetoputit.Thus,thefirststepinsigningupusersistomakeadatastructuretocaptureand
storetheirinformation.

Figure6.1:Amockupoftheusersignuppage.

InRails,thedefaultdatastructureforadatamodeliscalled,naturallyenough,amodel(theMinMVC
fromSection1.3.3).ThedefaultRailssolutiontotheproblemofpersistenceistouseadatabasefor
longtermdatastorage,andthedefaultlibraryforinteractingwiththedatabaseiscalledActiveRecord.1
ActiveRecordcomeswithahostofmethodsforcreating,saving,andfindingdataobjects,allwithout
havingtousethestructuredquerylanguage(SQL)2usedbyrelationaldatabases.Moreover,Railshasa
featurecalledmigrationstoallowdatadefinitionstobewritteninpureRuby,withouthavingtolearnan
SQLdatadefinitionlanguage(DDL).TheeffectisthatRailsinsulatesyoualmostentirelyfromthedetails
ofthedatastore.Inthisbook,byusingSQLitefordevelopmentandPostgreSQL(viaHeroku)for
deployment(Section1.5),wehavedevelopedthisthemeevenfurther,tothepointwherewebarelyever
havetothinkabouthowRailsstoresdata,evenforproductionapplications.
Asusual,ifyourefollowingalongusingGitforversioncontrol,nowwouldbeagoodtimetomakea
topicbranchformodelingusers:

$gitcheckoutmaster
$gitcheckoutbmodelingusers
6.1.1Databasemigrations
YoumayrecallfromSection4.4.5thatwehavealreadyencountered,viaacustombuiltUserclass,user
objectswithnameandemailattributes.Thatclassservedasausefulexample,butitlackedthecritical
propertyofpersistence:whenwecreatedaUserobjectattheRailsconsole,itdisappearedassoonas
weexited.Ourgoalinthissectionistocreateamodelforusersthatwontdisappearquitesoeasily.
AswiththeUserclassinSection4.4.5,wellstartbymodelingauserwithtwoattributes,anameandan
emailaddress,thelatterofwhichwelluseasauniqueusername.3(Welladdanattributefor
passwordsinSection6.3.)InListing4.13,wedidthiswithRubysattr_accessormethod:
classUser
attr_accessor:name,:email
.
.
.
end
Incontrast,whenusingRailstomodeluserswedontneedtoidentifytheattributesexplicitly.Asnoted
brieflyabove,tostoredataRailsusesarelationaldatabasebydefault,whichconsistsoftables
composedofdatarows,whereeachrowhascolumnsofdataattributes.Forexample,tostoreusers
withnamesandemailaddresses,wellcreateauserstablewithnameandemailcolumns(witheach
rowcorrespondingtooneuser).AnexampleofsuchatableappearsinFigure6.2,correspondingtothe
datamodelshowninFigure6.3.(Figure6.3isjustasketchthefulldatamodelappearsinFigure6.4.)
Bynamingthecolumnsnameandemail,wellletActiveRecordfigureouttheUserobjectattributesfor
us.

Figure6.2:Adiagramofsampledatainauserstable.

Figure6.3:AsketchoftheUserdatamodel.

YoumayrecallfromListing5.28thatwecreatedaUserscontroller(alongwithanewaction)usingthe
command
$railsgeneratecontrollerUsersnew
Theanalogouscommandformakingamodelisgeneratemodel,whichwecanusetogenerateaUser
modelwithnameandemailattributes,asshowninListing6.1.
Listing6.1:GeneratingaUsermodel.
$railsgeneratemodelUsername:stringemail:string
invokeactive_record
createdb/migrate/20140724010738_create_users.rb
createapp/models/user.rb
invoketest_unit
createtest/models/user_test.rb
createtest/fixtures/users.yml
(Notethat,incontrasttothepluralconventionforcontrollernames,modelnamesaresingular:aUsers
controller,butaUsermodel.)Bypassingtheoptionalparametersname:stringandemail:string,we
tellRailsaboutthetwoattributeswewant,alongwithwhichtypesthoseattributesshouldbe(inthis
case,string).ComparethiswithincludingtheactionnamesinListing3.4andListing5.28.
OneoftheresultsofthegeneratecommandinListing6.1isanewfilecalledamigration.Migrations
provideawaytoalterthestructureofthedatabaseincrementally,sothatourdatamodelcanadaptto
changingrequirements.InthecaseoftheUsermodel,themigrationiscreatedautomaticallybythe
modelgenerationscriptitcreatesauserstablewithtwocolumns,nameandemail,asshownin
Listing6.2.(WellseestartinginSection6.2.5howtomakeamigrationfromscratch.)
Listing6.2:MigrationfortheUsermodel(tocreateauserstable).
db/migrate/[timestamp]_create_users.rb
classCreateUsers<ActiveRecord::Migration
defchange
create_table:usersdo|t|

t.string:name
t.string:email
t.timestampsnull:false
end
end
end
Notethatthenameofthemigrationfileisprefixedbyatimestampbasedonwhenthemigrationwas
generated.Intheearlydaysofmigrations,thefilenameswereprefixedwithincrementingintegers,which
causedconflictsforcollaboratingteamsifmultipleprogrammershadmigrationswiththesamenumber.
Barringtheimprobablescenarioofmigrationsgeneratedthesamesecond,usingtimestamps
convenientlyavoidssuchcollisions.
Themigrationitselfconsistsofachangemethodthatdeterminesthechangetobemadetothe
database.InthecaseofListing6.2,changeusesaRailsmethodcalledcreate_tabletocreateatable
inthedatabaseforstoringusers.Thecreate_tablemethodacceptsablock(Section4.3.2)withone
blockvariable,inthiscasecalledt(fortable).Insidetheblock,thecreate_tablemethodusesthe
tobjecttocreatenameandemailcolumnsinthedatabase,bothoftypestring.4Herethetablename
isplural(users)eventhoughthemodelnameissingular(User),whichreflectsalinguisticconvention
followedbyRails:amodelrepresentsasingleuser,whereasadatabasetableconsistsofmanyusers.
Thefinallineintheblock,t.timestampsnull:false,isaspecialcommandthatcreatestwomagic
columnscalledcreated_atandupdated_at,whicharetimestampsthatautomaticallyrecordwhena
givenuseriscreatedandupdated.(Wellseeconcreteexamplesofthemagiccolumnsstartingin
Section6.1.3.)ThefulldatamodelrepresentedbythemigrationinListing6.2isshowninFigure6.4.
(Notetheadditionofthemagiccolumns,whichwerentpresentinthesketchshowninFigure6.3.)

Figure6.4:TheUserdatamodelproducedbyListing6.2.

Wecanrunthemigration,knownasmigratingup,usingtherakecommand(Box2.1)asfollows:
$bundleexecrakedb:migrate
(YoumayrecallthatweranthiscommandinasimilarcontextinSection2.2.)Thefirsttimedb:migrate
5

isrun,itcreatesafilecalleddb/development.sqlite3,whichisanSQLite5database.Wecanseethe
structureofthedatabasebyopeningdevelopment.sqlite3withDBBrowserforSQLite.(Ifyoure
usingthecloudIDE,youshouldfirstdownloadthedatabasefiletothelocaldisk,asshownin
Figure6.5.)TheresultappearsinFigure6.6comparewiththediagraminFigure6.4.Youmightnote
thattheresonecolumninFigure6.6notaccountedforinthemigration:theidcolumn.Asnotedbriefly
inSection2.2,thiscolumniscreatedautomatically,andisusedbyRailstoidentifyeachrowuniquely.

Figure6.5:DownloadingafilefromthecloudIDE.

Figure6.6:TheDBBrowserforSQLitewithournewuserstable.

Mostmigrations(includingalltheonesinthistutorial)arereversible,whichmeanswecanmigrate
downandundothemwithasingleRaketask,calleddb:rollback:
$bundleexecrakedb:rollback
(SeeBox3.1foranothertechniqueusefulforreversingmigrations.)Underthehood,thiscommand
executesthedrop_tablecommandtoremovetheuserstablefromthedatabase.Thereasonthis
worksisthatthechangemethodknowsthatdrop_tableistheinverseofcreate_table,whichmeans
thattherollbackmigrationcanbeeasilyinferred.Inthecaseofanirreversiblemigration,suchasoneto
removeadatabasecolumn,itisnecessarytodefineseparateupanddownmethodsinplaceofthe
singlechangemethod.ReadaboutmigrationsintheRailsGuidesformoreinformation.
Ifyourolledbackthedatabase,migrateupagainbeforeproceeding:
$bundleexecrakedb:migrate
6.1.2Themodelfile
WeveseenhowtheUsermodelgenerationinListing6.1generatedamigrationfile(Listing6.2),andwe

sawinFigure6.6theresultsofrunningthismigration:itupdatedafilecalleddevelopment.sqlite3by
creatingatableuserswithcolumnsid,name,email,created_at,andupdated_at.Listing6.1also
createdthemodelitself.Therestofthissectionisdedicatedtounderstandingit.
WebeginbylookingatthecodefortheUsermodel,whichlivesinthefileuser.rbinsidethe
app/models/directory.Itis,toputitmildly,verycompact(Listing6.3).
Listing6.3:ThebrandnewUsermodel.
app/models/user.rb
classUser<ActiveRecord::Base
end
RecallfromSection4.4.2thatthesyntaxclassUser<ActiveRecord::BasemeansthattheUser
classinheritsfromActiveRecord::Base,sothattheUsermodelautomaticallyhasallthefunctionality
oftheActiveRecord::Baseclass.Ofcourse,thisknowledgedoesntdousanygoodunlessweknow
whatActiveRecord::Basecontains,soletsgetstartedwithsomeconcreteexamples.
6.1.3Creatinguserobjects
AsinChapter4,ourtoolofchoiceforexploringdatamodelsistheRailsconsole.Sincewedont(yet)
wanttomakeanychangestoourdatabase,wellstarttheconsoleinasandbox:
$railsconsolesandbox
Loadingdevelopmentenvironmentinsandbox
Anymodificationsyoumakewillberolledbackonexit
>>
AsindicatedbythehelpfulmessageAnymodificationsyoumakewillberolledbackonexit,when
startedinasandboxtheconsolewillrollback(i.e.,undo)anydatabasechangesintroducedduringthe
session.
IntheconsolesessioninSection4.4.5,wecreatedanewuserobjectwithUser.new,whichwehad
accesstoonlyafterrequiringtheexampleuserfileinListing4.13.Withmodels,thesituationisdifferent
asyoumayrecallfromSection4.4.4,theRailsconsoleautomaticallyloadstheRailsenvironment,which
includesthemodels.Thismeansthatwecanmakeanewuserobjectwithoutanyfurtherwork:
>>User.new
=>#<Userid:nil,name:nil,email:nil,created_at:nil,updated_at:nil>
Weseeherethedefaultconsolerepresentationofauserobject.
Whencalledwithnoarguments,User.newreturnsanobjectwithallnilattributes.InSection4.4.5,we

designedtheexampleUserclasstotakeaninitializationhashtosettheobjectattributesthatdesign
choicewasmotivatedbyActiveRecord,whichallowsobjectstobeinitializedinthesameway:
>>user=User.new(name:"MichaelHartl",email:"mhartl@example.com")
=>#<Userid:nil,name:"MichaelHartl",email:"mhartl@example.com",
created_at:nil,updated_at:nil>
Hereweseethatthenameandemailattributeshavebeensetasexpected.
ThenotionofvalidityisimportantforunderstandingActiveRecordmodelobjects.Wellexplorethis
subjectinmoredepthinSection6.2,butfornowitsworthnotingthatourinitialuserobjectisvalid,
whichwecanverifybycallingthebooleanvalid?methodonit:
>>user.valid?
true
Sofar,wehaventtouchedthedatabase:User.newonlycreatesanobjectinmemory,while
user.valid?merelycheckstoseeiftheobjectisvalid.InordertosavetheUserobjecttothe
database,weneedtocallthesavemethodontheuservariable:
>>user.save
(0.2ms)begintransaction
UserExists(0.2ms)SELECT1ASoneFROM"users"WHERELOWER("users".
"email")=LOWER('mhartl@example.com')LIMIT1
SQL(0.5ms)INSERTINTO"users"("created_at","email","name","updated_at)
VALUES(?,?,?,?)[["created_at","2014091114:32:14.199519"],
["email","mhartl@example.com"],["name","MichaelHartl"],["updated_at",
"2014091114:32:14.199519"]]
(0.9ms)committransaction
=>true
Thesavemethodreturnstrueifitsucceedsandfalseotherwise.(Currently,allsavesshouldsucceed
becausethereareasyetnovalidationswellseecasesinSection6.2whensomewillfail.)For
reference,theRailsconsolealsoshowstheSQLcommandcorrespondingtouser.save(namely,
INSERTINTO"users").WellhardlyeverneedrawSQLinthisbook,6andIllomitdiscussionofthe
SQLcommandsfromnowon,butyoucanlearnalotbyreadingtheSQLcorrespondingtoActive
Recordcommands.
Youmayhavenoticedthatthenewuserobjecthadnilvaluesfortheidandthemagiccolumns
created_atandupdated_atattributes.Letsseeifoursavechangedanything:

>>user
=>#<Userid:1,name:"MichaelHartl",email:"mhartl@example.com",
created_at:"2014072400:57:46",updated_at:"2014072400:57:46">
Weseethattheidhasbeenassignedavalueof1,whilethemagiccolumnshavebeenassignedthe
currenttimeanddate.7Currently,thecreatedandupdatedtimestampsareidenticalwellseethem
differinSection6.1.5.
AswiththeUserclassinSection4.4.5,instancesoftheUsermodelallowaccesstotheirattributesusing
adotnotation:
>>user.name
=>"MichaelHartl"
>>user.email
=>"mhartl@example.com"
>>user.updated_at
=>Thu,24Jul201400:57:46UTC+00:00
AswellseeinChapter7,itsoftenconvenienttomakeandsaveamodelintwostepsaswehave
above,butActiveRecordalsoletsyoucombinethemintoonestepwithUser.create:
>>User.create(name:"ANother",email:"another@example.org")
#<Userid:2,name:"ANother",email:"another@example.org",created_at:
"2014072401:05:24",updated_at:"2014072401:05:24">
>>foo=User.create(name:"Foo",email:"foo@bar.com")
#<Userid:3,name:"Foo",email:"foo@bar.com",created_at:"20140724
01:05:42",updated_at:"2014072401:05:42">
NotethatUser.create,ratherthanreturningtrueorfalse,returnstheUserobjectitself,whichwe
canoptionallyassigntoavariable(suchasfoointhesecondcommandabove).
Theinverseofcreateisdestroy:
>>foo.destroy
=>#<Userid:3,name:"Foo",email:"foo@bar.com",created_at:"20140724
01:05:42",updated_at:"2014072401:05:42">
Likecreate,destroyreturnstheobjectinquestion,thoughIcantrecalleverhavingusedthereturn
valueofdestroy.Inaddition,thedestroyedobjectstillexistsinmemory:

>>foo
=>#<Userid:3,name:"Foo",email:"foo@bar.com",created_at:"20140724
01:05:42",updated_at:"2014072401:05:42">
Sohowdoweknowifwereallydestroyedanobject?Andforsavedandnondestroyedobjects,howcan
weretrieveusersfromthedatabase?Toanswerthesequestions,weneedtolearnhowtouseActive
Recordtofinduserobjects.
6.1.4Findinguserobjects
ActiveRecordprovidesseveraloptionsforfindingobjects.Letsusethemtofindthefirstuserwe
createdwhileverifyingthatthethirduser(foo)hasbeendestroyed.Wellstartwiththeexistinguser:
>>User.find(1)
=>#<Userid:1,name:"MichaelHartl",email:"mhartl@example.com",
created_at:"2014072400:57:46",updated_at:"2014072400:57:46">
HerewevepassedtheidoftheusertoUser.findActiveRecordreturnstheuserwiththatid.
Letsseeiftheuserwithanidof3stillexistsinthedatabase:
>>User.find(3)
ActiveRecord::RecordNotFound:Couldn'tfindUserwithID=3
SincewedestroyedourthirduserinSection6.1.3,ActiveRecordcantfinditinthedatabase.Instead,
findraisesanexception,whichisawayofindicatinganexceptionaleventintheexecutionofa
programinthiscase,anonexistentActiveRecordid,whichcausesfindtoraisean
ActiveRecord::RecordNotFoundexception.8
Inadditiontothegenericfind,ActiveRecordalsoallowsustofindusersbyspecificattributes:
>>User.find_by(email:"mhartl@example.com")
=>#<Userid:1,name:"MichaelHartl",email:"mhartl@example.com",
created_at:"2014072400:57:46",updated_at:"2014072400:57:46">
Sincewewillbeusingemailaddressesasusernames,thissortoffindwillbeusefulwhenwelearn
howtoletuserslogintooursite(Chapter7).Ifyoureworriedthatfind_bywillbeinefficientifthereare
alargenumberofusers,youreaheadofthegamewellcoverthisissue,anditssolutionviadatabase
indices,inSection6.2.5.
Wellendwithacoupleofmoregeneralwaysoffindingusers.First,theresfirst:

>>User.first
=>#<Userid:1,name:"MichaelHartl",email:"mhartl@example.com",
created_at:"2014072400:57:46",updated_at:"2014072400:57:46">
Naturally,firstjustreturnsthefirstuserinthedatabase.Theresalsoall:
>>User.all
=>#<ActiveRecord::Relation[#<Userid:1,name:"MichaelHartl",
email:"mhartl@example.com",created_at:"2014072400:57:46",
updated_at:"2014072400:57:46">,#<Userid:2,name:"ANother",
email:"another@example.org",created_at:"2014072401:05:24",
updated_at:"2014072401:05:24">]>
Asyoucanseefromtheconsoleoutput,User.allreturnsalltheusersinthedatabaseasanobjectof
classActiveRecord::Relation,whichiseffectivelyanarray(Section4.3.1).
6.1.5Updatinguserobjects
Oncewevecreatedobjects,weoftenwanttoupdatethem.Therearetwobasicwaystodothis.First,
wecanassignattributesindividually,aswedidinSection4.4.5:
>>user#Justareminderaboutouruser'sattributes
=>#<Userid:1,name:"MichaelHartl",email:"mhartl@example.com",
created_at:"2014072400:57:46",updated_at:"2014072400:57:46">
>>user.email="mhartl@example.net"
=>"mhartl@example.net"
>>user.save
=>true
Notethatthefinalstepisnecessarytowritethechangestothedatabase.Wecanseewhathappens
withoutasavebyusingreload,whichreloadstheobjectbasedonthedatabaseinformation:
>>user.email
=>"mhartl@example.net"
>>user.email="foo@bar.com"
=>"foo@bar.com"
>>user.reload.email
=>"mhartl@example.net"
Nowthatweveupdatedtheuserbyrunninguser.save,themagiccolumnsdiffer,aspromisedin
Section6.1.3:

>>user.created_at
=>"2014072400:57:46"
>>user.updated_at
=>"2014072401:37:32"
Thesecondmainwaytoupdatemultipleattributesistouseupdate_attributes:9
>>user.update_attributes(name:"TheDude",email:"dude@abides.org")
=>true
>>user.name
=>"TheDude"
>>user.email
=>"dude@abides.org"
Theupdate_attributesmethodacceptsahashofattributes,andonsuccessperformsboththe
updateandthesaveinonestep(returningtruetoindicatethatthesavewentthrough).Notethatifany
ofthevalidationsfail,suchaswhenapasswordisrequiredtosavearecord(asimplementedin
Section6.3),thecalltoupdate_attributeswillfail.Ifweneedtoupdateonlyasingleattribute,using
thesingularupdate_attributebypassesthisrestriction:
>>user.update_attribute(:name,"TheDude")
=>true
>>user.name
=>"TheDude"
6.2Uservalidations
TheUsermodelwecreatedinSection6.1nowhasworkingnameandemailattributes,buttheyare
completelygeneric:anystring(includinganemptyone)iscurrentlyvalidineithercase.Andyet,names
andemailaddressesaremorespecificthanthis.Forexample,nameshouldbenonblank,andemail
shouldmatchthespecificformatcharacteristicofemailaddresses.Moreover,sincewellbeusingemail
addressesasuniqueusernameswhenuserslogin,weshouldntallowemailduplicatesinthedatabase.
Inshort,weshouldntallownameandemailtobejustanystringsweshouldenforcecertainconstraints
ontheirvalues.ActiveRecordallowsustoimposesuchconstraintsusingvalidations(seenbrieflybefore
inSection2.3.2).Inthissection,wellcoverseveralofthemostcommoncases,validatingpresence,
length,formatanduniqueness.InSection6.3.2welladdafinalcommonvalidation,confirmation.And
wellseeinSection7.3howvalidationsgiveusconvenienterrormessageswhenusersmake
submissionsthatviolatethem.
6.2.1Avaliditytest

AsnotedinBox3.3,testdrivendevelopmentisntalwaystherighttoolforthejob,butmodelvalidations
areexactlythekindoffeaturesforwhichTDDisaperfectfit.Itsdifficulttobeconfidentthatagiven
validationisdoingexactlywhatweexpectittowithoutwritingafailingtestandthengettingittopass.
Ourmethodwillbetostartwithavalidmodelobject,setoneofitsattributestosomethingwewanttobe
invalid,andthentestthatitinfactisinvalid.Asasafetynet,wellfirstwriteatesttomakesuretheinitial
modelobjectisvalid.Thisway,whenthevalidationtestsfailwellknowitsfortherightreason(andnot
becausetheinitialobjectwasinvalidinthefirstplace).
Togetusstarted,thecommandinListing6.1producedaninitialtestfortestingusers,thoughinthis
caseitspracticallyblank(Listing6.4).
Listing6.4:ThepracticallyblankdefaultUsertest.
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
#test"thetruth"do
#asserttrue
#end
end
Towriteatestforavalidobject,wellcreateaninitiallyvalidUsermodelobject@userusingthespecial
setupmethod(discussedbrieflyintheChapter3exercises),whichautomaticallygetsrunbeforeeach
test.Because@userisaninstancevariable,itsautomaticallyavailableinallthetests,andwecantest
itsvalidityusingthevalid?method(Section6.1.3).TheresultappearsinListing6.5.
Listing6.5:Atestforaninitiallyvaliduser.green
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
test"shouldbevalid"do
assert@user.valid?
end
end

Listing6.5usestheplainassertmethod,whichinthiscasesucceedsif@user.valid?returnstrue
andfailsifitreturnsfalse.
BecauseourUsermodeldoesntcurrentlyhaveanyvalidations,theinitialtestshouldpass:
Listing6.6:green
$bundleexecraketest:models
Hereweveusedraketest:modelstorunjustthemodeltests(comparetoraketest:integration
fromSection5.3.4).
6.2.2Validatingpresence
Perhapsthemostelementaryvalidationispresence,whichsimplyverifiesthatagivenattributeis
present.Forexample,inthissectionwellensurethatboththenameandemailfieldsarepresentbefore
ausergetssavedtothedatabase.InSection7.3.3,wellseehowtopropagatethisrequirementupto
thesignupformforcreatingnewusers.
WellstartwithatestforthepresenceofanameattributebybuildingonthetestinListing6.5.Asseenin
Listing6.7,allweneedtodoissetthe@uservariablesnameattributetoablankstring(inthiscase,a
stringofspaces)andthencheck(usingtheassert_notmethod)thattheresultingUserobjectisnot
valid.
Listing6.7:Atestforvalidationofthenameattribute.red
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
test"shouldbevalid"do
assert@user.valid?
end
test"nameshouldbepresent"do
@user.name=""
assert_not@user.valid?
end

end
Atthispoint,themodeltestsshouldbered:
Listing6.8:red
$bundleexecraketest:models
AswesawbrieflybeforeintheChapter2exercises,thewaytovalidatethepresenceofthename
attributeistousethevalidatesmethodwithargumentpresence:true,asshowninListing6.9.The
presence:trueargumentisaoneelementoptionshashrecallfromSection4.3.4thatcurlybraces
areoptionalwhenpassinghashesasthefinalargumentinamethod.(AsnotedinSection5.1.1,theuse
ofoptionshashesisarecurringthemeinRails.)
Listing6.9:Validatingthepresenceofanameattribute.green
app/models/user.rb
classUser<ActiveRecord::Base
validates:name,presence:true
end
Listing6.9maylooklikemagic,butvalidatesisjustamethod.AnequivalentformulationofListing6.9
usingparenthesesisasfollows:
classUser<ActiveRecord::Base
validates(:name,presence:true)
end
LetsdropintotheconsoletoseetheeffectsofaddingavalidationtoourUsermodel:10
$railsconsolesandbox
>>user=User.new(name:"",email:"mhartl@example.com")
>>user.valid?
=>false
Herewecheckthevalidityoftheuservariableusingthevalid?method,whichreturnsfalsewhen
theobjectfailsoneormorevalidations,andtruewhenallvalidationspass.Inthiscase,weonlyhave
onevalidation,soweknowwhichonefailed,butitcanstillbehelpfultocheckusingtheerrorsobject
generatedonfailure:
>>user.errors.full_messages
=>["Namecan'tbeblank"]

(TheerrormessageisahintthatRailsvalidatesthepresenceofanattributeusingtheblank?method,
whichwesawattheendofSection4.4.3.)
Becausetheuserisntvalid,anattempttosavetheusertothedatabaseautomaticallyfails:
>>user.save
=>false
Asaresult,thetestinListing6.7shouldnowbegreen:
Listing6.10:green
$bundleexecraketest:models
FollowingthemodelinListing6.7,writingatestforemailattributepresenceiseasy(Listing6.11),asis
theapplicationcodetogetittopass(Listing6.12).
Listing6.11:Atestforvalidationoftheemailattribute.red
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
test"shouldbevalid"do
assert@user.valid?
end
test"nameshouldbepresent"do
@user.name=""
assert_not@user.valid?
end
test"emailshouldbepresent"do
@user.email=""
assert_not@user.valid?
end
end

Listing6.12:Validatingthepresenceofanemailattribute.green
app/models/user.rb
Atthispoint,thepresencevalidationsarecomplete,andthetestsuiteshouldbegreen:
Listing6.13:green
$bundleexecraketest
6.2.3Lengthvalidation
WeveconstrainedourUsermodeltorequireanameforeachuser,butweshouldgofurther:theusers
nameswillbedisplayedonthesamplesite,soweshouldenforcesomelimitontheirlength.Withallthe
workwedidinSection6.2.2,thisstepiseasy.
Theresnosciencetopickingamaximumlengthwelljustpull50outofthinairasareasonableupper
bound,whichmeansverifyingthatnamesof51charactersaretoolong.Inaddition,althoughitsunlikely
evertobeaproblem,theresachancethatausersemailaddresscouldoverrunthemaximumlengthof
strings,whichformanydatabasesis255.BecausetheformatvalidationinSection6.2.4wontenforce
suchaconstraint,welladdoneinthissectionforcompleteness.Listing6.14showstheresultingtests.
Listing6.14:Atestfornamelengthvalidation.red
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
.
.
.
test"nameshouldnotbetoolong"do
@user.name="a"*51
assert_not@user.valid?
end
test"emailshouldnotbetoolong"do
@user.email="a"*244+"@example.com"
assert_not@user.valid?
end

end
Forconvenience,weveusedstringmultiplicationinListing6.14tomakeastring51characterslong.
Wecanseehowthisworksusingtheconsole:
>>"a"*51
=>"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>>("a"*51).length
=>51
Theemaillengthvalidationarrangestomakeavalidemailaddressthatsonecharactertoolong:
>>"a"*244+"@example.com"
=>"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaa@example.com"
>>("a"*244+"@example.com").length
=>256
Atthispoint,thetestsinListing6.14shouldbered:
Listing6.15:red
$bundleexecraketest
Togetthemtopass,weneedtousethevalidationargumenttoconstrainlength,whichisjustlength,
alongwiththemaximumparametertoenforcetheupperbound(Listing6.16).
Listing6.16:Addingalengthvalidationforthenameattribute.green
app/models/user.rb
classUser<ActiveRecord::Base
validates:name,presence:true,length:{maximum:50}
validates:email,presence:true,length:{maximum:255}
end
Nowthetestsshouldbegreen:
Listing6.17:green
$bundleexecraketest

Withourtestsuitepassingagain,wecanmoveontoamorechallengingvalidation:emailformat.
6.2.4Formatvalidation
Ourvalidationsforthenameattributeenforceonlyminimalconstraintsanynonblanknameunder51
characterswilldobutofcoursetheemailattributemustsatisfythemorestringentrequirementof
beingavalidemailaddress.Sofarweveonlyrejectedblankemailaddressesinthissection,well
requireemailaddressestoconformtothefamiliarpatternuser@example.com.
Neitherthetestsnorthevalidationwillbeexhaustive,justgoodenoughtoacceptmostvalidemail
addressesandrejectmostinvalidones.Wellstartwithacoupleoftestsinvolvingcollectionsofvalidand
invalidaddresses.Tomakethesecollections,itsworthknowingabouttheuseful%w[]techniquefor
makingarraysofstrings,asseeninthisconsolesession:
>>%w[foobarbaz]
=>["foo","bar","baz"]
>>addresses=%w[USER@foo.COMTHE_USER@foo.bar.orgfirst.last@foo.jp]
=>["USER@foo.COM","THE_USER@foo.bar.org","first.last@foo.jp"]
>>addresses.eachdo|address|
?>putsaddress
>>end
USER@foo.COM
THE_USER@foo.bar.org
first.last@foo.jp
Hereweveiteratedovertheelementsoftheaddressesarrayusingtheeachmethod(Section4.3.2).
Withthistechniqueinhand,werereadytowritesomebasicemailformatvalidationtests.
Becauseemailformatvalidationistrickyanderrorprone,wellstartwithsomepassingtestsforvalid
emailaddressestocatchanyerrorsinthevalidation.Inotherwords,wewanttomakesurenotjustthat
invalidemailaddresseslikeuser@example,comarerejected,butalsothatvalidaddresseslike
user@example.comareaccepted,evenafterweimposethevalidationconstraint.(Rightnow,ofcourse,
theyllbeacceptedbecauseallnonblankemailaddressesarecurrentlyvalid.)Theresultfora
representativesampleofvalidemailaddressesappearsinListing6.18.
Listing6.18:Testsforvalidemailformats.green
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup

@user=User.new(name:"ExampleUser",email:"user@example.com")
end
.
.
.
test"emailvalidationshouldacceptvalidaddresses"do
valid_addresses=%w[user@example.comUSER@foo.COMA_USER@foo.bar.org
first.last@foo.jpalice+bob@baz.cn]
valid_addresses.eachdo|valid_address|
@user.email=valid_address
assert@user.valid?,"#{valid_address.inspect}shouldbevalid"
end
end
end
Notethatweveincludedanoptionalsecondargumenttotheassertionwithacustomerrormessage,
whichinthiscaseidentifiestheaddresscausingthetesttofail:
assert@user.valid?,"#{valid_address.inspect}shouldbevalid"
(ThisusestheinterpolatedinspectmethodmentionedinSection4.3.3.)Includingthespecificaddress
thatcausesanyfailureisespeciallyusefulinatestwithaneachlooplikeListing6.18otherwise,any
failurewouldmerelyidentifythelinenumber,whichisthesameforalltheemailaddresses,andwhich
wouldntbesufficienttoidentifythesourceoftheproblem.
Nextwelladdtestsfortheinvalidityofavarietyofinvalidemailaddresses,suchasuser@example,com
(commainplaceofdot)anduser_at_foo.org(missingthe@sign).AsinListing6.18,Listing6.19
includesacustomerrormessagetoidentifytheexactaddresscausinganyfailure.
Listing6.19:Testsforemailformatvalidation.red
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
.
.
.

test"emailvalidationshouldrejectinvalidaddresses"do
invalid_addresses=%w[user@example,comuser_at_foo.orguser.name@example.
foo@bar_baz.comfoo@bar+baz.com]
invalid_addresses.eachdo|invalid_address|
@user.email=invalid_address
assert_not@user.valid?,"#{invalid_address.inspect}shouldbeinvalid"
end
end
end
Atthispoint,thetestsshouldbered:
Listing6.20:red
$bundleexecraketest
Theapplicationcodeforemailformatvalidationusestheformatvalidation,whichworkslikethis:
validates:email,format:{with:/<regularexpression>/}
Thisvalidatestheattributewiththegivenregularexpression(orregex),whichisapowerful(andoften
cryptic)languageformatchingpatternsinstrings.Thismeansweneedtoconstructaregularexpression
tomatchvalidemailaddresseswhilenotmatchinginvalidones.
Thereactuallyexistsafullregexformatchingemailaddressesaccordingtotheofficialemailstandard,
butitsenormous,obscure,andquitepossiblycounterproductive.11Inthistutorial,welladoptamore
pragmaticregexthathasproventoberobustinpractice.Hereswhatitlookslike:
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i
Tohelpunderstandwherethiscomesfrom,Table6.1breaksitintobitesizedpieces.12
Expression

Meaning

/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i

fullregex

startofregex

\A

matchstartofastring

[\w+\.]+

atleastonewordcharacter,plus,hyphen,ordot

literalatsign

[az\d\.]+

atleastoneletter,digit,hyphen,ordot

\.

literaldot

[az]+

atleastoneletter

\z

matchendofastring

endofregex

caseinsensitive

Table6.1:Breakingdownthevalidemailregex.
AlthoughyoucanlearnalotbystudyingTable6.1,toreallyunderstandregularexpressionsIconsider
usinganinteractiveregularexpressionmatcherlikeRubulartobeessential(Figure6.7).13TheRubular
websitehasabeautifulinteractiveinterfaceformakingregularexpressions,alongwithahandyregex
quickreference.IencourageyoutostudyTable6.1withabrowserwindowopentoRubularnoamount
ofreadingaboutregularexpressionscanreplaceplayingwiththeminteractively.(Note:Ifyouusethe
regexfromTable6.1inRubular,Irecommendleavingoffthe\Aand\zcharacterssothatyoucanmatch
morethanoneemailaddressatatimeinthegiventeststring.)

Figure6.7:TheawesomeRubularregularexpressioneditor.

ApplyingtheregularexpressionfromTable6.1totheemailformatvalidationyieldsthecodein
Listing6.21.
Listing6.21:Validatingtheemailformatwitharegularexpression.green
app/models/user.rb
classUser<ActiveRecord::Base
validates:name,presence:true,length:{maximum:50}
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i
validates:email,presence:true,length:{maximum:255},
format:{with:VALID_EMAIL_REGEX}
end
HeretheregexVALID_EMAIL_REGEXisaconstant,indicatedinRubybyanamestartingwithacapital
letter.Thecode
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i
validates:email,presence:true,length:{maximum:255},
format:{with:VALID_EMAIL_REGEX}
ensuresthatonlyemailaddressesthatmatchthepatternwillbeconsideredvalid.(Theexpression
abovehasoneminorweakness:itallowsinvalidaddressesthatcontainconsecutivedots,suchas
foo@bar..com.UpdatingtheregexinListing6.21tofixthisblemishisleftasanexercise(Section6.5).)
Atthispoint,thetestsshouldbegreen:
Listing6.22:green
$bundleexecraketest:models
Thismeansthattheresonlyoneconstraintleft:enforcingemailuniqueness.
6.2.5Uniquenessvalidation
Toenforceuniquenessofemailaddresses(sothatwecanusethemasusernames),wellbeusingthe
:uniqueoptiontothevalidatesmethod.Butbewarned:theresamajorcaveat,sodontjustskimthis
sectionreaditcarefully.
Wellstartwithsomeshorttests.Inourpreviousmodeltests,wevemainlyusedUser.new,whichjust
createsaRubyobjectinmemory,butforuniquenesstestsweactuallyneedtoputarecordintothe
database.14TheinitialduplicateemailtestappearsinListing6.23.
Listing6.23:Atestfortherejectionofduplicateemailaddresses.red
test/models/user_test.rb

require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
.
.
.
test"emailaddressesshouldbeunique"do
duplicate_user=@user.dup
@user.save
assert_notduplicate_user.valid?
end
end
Themethodhereistomakeauserwiththesameemailaddressas@userusing@user.dup,which
createsaduplicateuserwiththesameattributes.Sincewethensave@user,theduplicateuserhasan
emailaddressthatalreadyexistsinthedatabase,andhenceshouldnotbevalid.
WecangetthenewtestinListing6.23topassbyaddinguniqueness:truetotheemailvalidation,
asshowninListing6.24.
Listing6.24:Validatingtheuniquenessofemailaddresses.green
app/models/user.rb
classUser<ActiveRecord::Base
validates:name,presence:true,length:{maximum:50}
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i
validates:email,presence:true,length:{maximum:255},
format:{with:VALID_EMAIL_REGEX},
uniqueness:true
end
Werenotquitedone,though.Emailaddressesaretypicallyprocessedasiftheywerecaseinsensitive
i.e.,foo@bar.comistreatedthesameasFOO@BAR.COMorFoO@BAr.coMsoourvalidationshould
incorporatethisaswell.15Itsthusimportanttotestforcaseinsensitivity,whichwedowiththecodein
Listing6.25.
Listing6.25:Testingcaseinsensitiveemailuniqueness.red
test/models/user_test.rb

require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
.
.
.
test"emailaddressesshouldbeunique"do
duplicate_user=@user.dup
duplicate_user.email=@user.email.upcase
@user.save
assert_notduplicate_user.valid?
end
end
Hereweareusingtheupcasemethodonstrings(seenbrieflyinSection4.3.2).Thistestdoesthesame
thingastheinitialduplicateemailtest,butwithanuppercaseemailaddressinstead.Ifthistestfeelsa
littleabstract,goaheadandfireuptheconsole:
$railsconsolesandbox
>>user=User.create(name:"ExampleUser",email:"user@example.com")
>>user.email.upcase
=>"USER@EXAMPLE.COM"
>>duplicate_user=user.dup
>>duplicate_user.email=user.email.upcase
>>duplicate_user.valid?
=>true
Ofcourse,duplicate_user.valid?iscurrentlytruebecausetheuniquenessvalidationiscase
sensitive,butwewantittobefalse.Fortunately,:uniquenessacceptsanoption,:case_sensitive,
forjustthispurpose(Listing6.26).
Listing6.26:Validatingtheuniquenessofemailaddresses,ignoringcase.green
app/models/user.rb
classUser<ActiveRecord::Base
validates:name,presence:true,length:{maximum:50}
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i

validates:email,presence:true,length:{maximum:255},
format:{with:VALID_EMAIL_REGEX},
uniqueness:{case_sensitive:false}
end
NotethatwehavesimplyreplacedtrueinListing6.24withcase_sensitive:falseinListing6.26.
(Railsinfersthatuniquenessshouldbetrueaswell.)
Atthispoint,ourapplicationwithanimportantcaveatenforcesemailuniqueness,andourtestsuite
shouldpass:
Listing6.27:green
$bundleexecraketest
Theresjustonesmallproblem,whichisthattheActiveRecorduniquenessvalidationdoesnot
guaranteeuniquenessatthedatabaselevel.Heresascenariothatexplainswhy:
1.Alicesignsupforthesampleapp,withaddressalice@wonderland.com.
2.AliceaccidentallyclicksonSubmittwice,sendingtworequestsinquicksuccession.
3.Thefollowingsequenceoccurs:request1createsauserinmemorythatpassesvalidation,request2
doesthesame,request1susergetssaved,request2susergetssaved.
4.Result:twouserrecordswiththeexactsameemailaddress,despitetheuniquenessvalidation
Iftheabovesequenceseemsimplausible,believeme,itisnt:itcanhappenonanyRailswebsitewith
significanttraffic(whichIoncelearnedthehardway).Luckily,thesolutionisstraightforwardto
implement:wejustneedtoenforceuniquenessatthedatabaselevelaswellasatthemodellevel.Our
methodistocreateadatabaseindexontheemailcolumn(Box6.2),andthenrequirethattheindexbe
unique.
Box6.2.Databaseindices
Whencreatingacolumninadatabase,itisimportanttoconsiderwhetherwewillneedtofindrecordsby
thatcolumn.Consider,forexample,theemailattributecreatedbythemigrationinListing6.2.Whenwe
allowuserstologintothesampleappstartinginChapter7,wewillneedtofindtheuserrecord
correspondingtothesubmittedemailaddress.Unfortunately,basedonthenavedatamodel,theonly
waytofindauserbyemailaddressistolookthrougheachuserrowinthedatabaseandcompareits
emailattributetothegivenemailwhichmeanswemighthavetoexamineeveryrow(sincetheuser
couldbethelastoneinthedatabase).Thisisknowninthedatabasebusinessasafulltablescan,and
forarealsitewiththousandsofusersitisaBadThing.
Puttinganindexontheemailcolumnfixestheproblem.Tounderstandadatabaseindex,itshelpfulto
considertheanalogyofabookindex.Inabook,tofindalltheoccurrencesofagivenstring,say

foobar,youwouldhavetoscaneachpageforfoobarthepaperversionofafulltablescan.Witha
bookindex,ontheotherhand,youcanjustlookupfoobarintheindextoseeallthepagescontaining
foobar.Adatabaseindexworksessentiallythesameway.
Theemailindexrepresentsanupdatetoourdatamodelingrequirements,which(asdiscussedin
Section6.1.1)ishandledinRailsusingmigrations.WesawinSection6.1.1thatgeneratingtheUser
modelautomaticallycreatedanewmigration(Listing6.2)inthepresentcase,weareaddingstructure
toanexistingmodel,soweneedtocreateamigrationdirectlyusingthemigrationgenerator:
$railsgeneratemigrationadd_index_to_users_email
Unlikethemigrationforusers,theemailuniquenessmigrationisnotpredefined,soweneedtofillinits
contentswithListing6.28.16
Listing6.28:Themigrationforenforcingemailuniqueness.
db/migrate/[timestamp]_add_index_to_users_email.rb
classAddIndexToUsersEmail<ActiveRecord::Migration
defchange
add_index:users,:email,unique:true
end
end
ThisusesaRailsmethodcalledadd_indextoaddanindexontheemailcolumnoftheuserstable.
Theindexbyitselfdoesntenforceuniqueness,buttheoptionunique:truedoes.
Thefinalstepistomigratethedatabase:
$bundleexecrakedb:migrate
(Ifthisfails,tryexitinganyrunningsandboxconsolesessions,whichcanlockthedatabaseandprevent
migrations.)
Atthispoint,thetestsuiteshouldberedduetoaviolationoftheuniquenessconstraintinthefixtures,
whichcontainsampledataforthetestdatabase.Userfixturesweregeneratedautomaticallyin
Listing6.1,andasshowninListing6.29theemailaddressesarenotunique.(Theyrenotvalideither,
butfixturedatadoesntgetrunthroughthevalidations.)
Listing6.29:Thedefaultuserfixtures.red
test/fixtures/users.yml
#Readaboutfixturesathttp://api.rubyonrails.org/classes/ActiveRecord/

#FixtureSet.html
one:
name:MyString
email:MyString
two:
name:MyString
email:MyString
BecausewewontneedfixturesuntilChapter8,fornowwelljustremovethem,leavinganempty
fixturesfile(Listing6.30).
Listing6.30:Anemptyfixturesfile.green
test/fixtures/users.yml
#empty
Havingaddressedtheuniquenesscaveat,theresonemorechangeweneedtomaketobeassuredof
emailuniqueness.Somedatabaseadaptersusecasesensitiveindices,consideringthestrings
Foo@ExAMPle.CoMandfoo@example.comtobedistinct,butourapplicationtreatsthoseaddresses
asthesame.Toavoidthisincompatibility,wellstandardizeonalllowercaseaddresses,converting
Foo@ExAMPle.CoMtofoo@example.combeforesavingittothedatabase.Thewaytodothisiswith
acallback,whichisamethodthatgetsinvokedataparticularpointinthelifecycleofanActiveRecord
object.Inthepresentcase,thatpointisbeforetheobjectissaved,sowelluseabefore_savecallback
todowncasetheemailattributebeforesavingtheuser.17TheresultappearsinListing6.31.(Thisisjust
afirstimplementationwelldiscussthissubjectagaininSection10.1.1,wherewellusethepreferred
methodreferenceconventionfordefiningcallbacks.)
Listing6.31:Ensuringemailuniquenessbydowncasingtheemailattribute.green
app/models/user.rb
classUser<ActiveRecord::Base
before_save{self.email=email.downcase}
validates:name,presence:true,length:{maximum:50}
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i
validates:email,presence:true,length:{maximum:255},
format:{with:VALID_EMAIL_REGEX},
uniqueness:{case_sensitive:false}
end
ThecodeinListing6.31passesablocktothebefore_savecallbackandsetstheusersemailaddress

toalowercaseversionofitscurrentvalueusingthedowncasestringmethod.(Writingatestforemail
downcasingisleftasanexercise(Section6.5).)
InListing6.31,wecouldhavewrittentheassignmentas
self.email=self.email.downcase
(whereselfreferstothecurrentuser),butinsidetheUsermodeltheselfkeywordisoptionalonthe
righthandside:
self.email=email.downcase
Weencounteredthisideabrieflyinthecontextofreverseinthepalindromemethod(Section4.4.2),
whichalsonotedthatselfisnotoptionalinanassignment,so
email=email.downcase
wouldntwork.(WelldiscussthissubjectinmoredepthinSection8.4.)
Atthispoint,theAlicescenarioabovewillworkfine:thedatabasewillsaveauserrecordbasedonthe
firstrequest,anditwillrejectthesecondsaveforviolatingtheuniquenessconstraint.(Anerrorwill
appearintheRailslog,butthatdoesntdoanyharm.)Moreover,addingthisindexontheemailattribute
accomplishesasecondgoal,alludedtobrieflyinSection6.1.4:asnotedinBox6.2,theindexonthe
emailattributefixesapotentialefficiencyproblembypreventingafulltablescanwhenfindingusersby
emailaddress.
6.3.1Ahashedpassword
MostofthesecurepasswordmachinerywillbeimplementedusingasingleRailsmethodcalled
has_secure_password,whichwellincludeintheUsermodelasfollows:
classUser<ActiveRecord::Base
.
.
.
has_secure_password
end
Whenincludedinamodelasabove,thisonemethodaddsthefollowingfunctionality:
Theabilitytosaveasecurelyhashedpassword_digestattributetothedatabase
Apairofvirtualattributes18(passwordandpassword_confirmation),includingpresencevalidations

uponobjectcreationandavalidationrequiringthattheymatch
Anauthenticatemethodthatreturnstheuserwhenthepasswordiscorrect(andfalseotherwise)
Theonlyrequirementforhas_secure_passwordtoworkitsmagicisforthecorrespondingmodelto
haveanattributecalledpassword_digest.(Thenamedigestcomesfromtheterminologyof
cryptographichashfunctions.Inthiscontext,hashedpasswordandpassworddigestaresynonyms.)19
InthecaseoftheUsermodel,thisleadstothedatamodelshowninFigure6.8.

Figure6.8:TheUserdatamodelwithanaddedpassword_digestattribute.

ToimplementthedatamodelinFigure6.8,wefirstgenerateanappropriatemigrationforthe
password_digestcolumn.Wecanchooseanymigrationnamewewant,butitsconvenienttoendthe
namewithto_users,sinceinthiscaseRailsautomaticallyconstructsamigrationtoaddcolumnstothe
userstable.Theresult,withmigrationnameadd_password_digest_to_users,appearsasfollows:
$railsgeneratemigrationadd_password_digest_to_userspassword_digest:string
Herewevealsosuppliedtheargumentpassword_digest:stringwiththenameandtypeofattribute
wewanttocreate.(ComparethistotheoriginalgenerationoftheuserstableinListing6.1,which
includedtheargumentsname:stringandemail:string.)Byincludingpassword_digest:string,
wevegivenRailsenoughinformationtoconstructtheentiremigrationforus,asseeninListing6.32.
Listing6.32:Themigrationtoaddapassword_digestcolumntotheuserstable.
db/migrate/[timestamp]_add_password_digest_to_users.rb
classAddPasswordDigestToUsers<ActiveRecord::Migration
defchange
add_column:users,:password_digest,:string
end
end
Listing6.32usestheadd_columnmethodtoaddapassword_digestcolumntotheuserstable.To
applyit,wejustmigratethedatabase:

$bundleexecrakedb:migrate
Tomakethepassworddigest,has_secure_passwordusesastateofthearthashfunctioncalled
bcrypt.Byhashingthepasswordwithbcrypt,weensurethatanattackerwontbeabletologintothe
siteeveniftheymanagetoobtainacopyofthedatabase.Tousebcryptinthesampleapplication,we
needtoaddthebcryptgemtoourGemfile(Listing6.33).
Listing6.33:AddingbcrypttotheGemfile.
source'https://rubygems.org'
gem'rails','4.2.2'
gem'bcrypt','3.1.7'
.
.
.
Thenrunbundleinstallasusual:
$bundleinstall
6.3.2Userhassecurepassword
NowthatwevesuppliedtheUsermodelwiththerequiredpassword_digestattributeandinstalled
bcrypt,werereadytoaddhas_secure_passwordtotheUsermodel,asshowninListing6.34.
Listing6.34:Addinghas_secure_passwordtotheUsermodel.red
app/models/user.rb
classUser<ActiveRecord::Base
before_save{self.email=email.downcase}
validates:name,presence:true,length:{maximum:50}
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i
validates:email,presence:true,length:{maximum:255},
format:{with:VALID_EMAIL_REGEX},
uniqueness:{case_sensitive:false}
has_secure_password
end
AsindicatedbytheredindicatorinListing6.34,thetestsarenowfailing,asyoucanconfirmatthe
commandline:
Listing6.35:red

$bundleexecraketest
Thereasonisthat,asnotedinSection6.3.1,has_secure_passwordenforcesvalidationsonthevirtual
passwordandpassword_confirmationattributes,butthetestsinListing6.25createan@user
variablewithouttheseattributes:
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com")
end
So,togetthetestsuitepassingagain,wejustneedtoaddapasswordanditsconfirmation,asshownin
Listing6.36.
Listing6.36:Addingapasswordanditsconfirmation.green
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com",
password:"foobar",password_confirmation:"foobar")
end
.
.
.
end
Nowthetestsshouldbegreen:
Listing6.37:green
$bundleexecraketest
Wellseeinjustamomentthebenefitsofaddinghas_secure_passwordtotheUsermodel
(Section6.3.4),butfirstwelladdaminimalrequirementonpasswordsecurity.
6.3.3Minimumpasswordstandards
Itsgoodpracticeingeneraltoenforcesomeminimumstandardsonpasswordstomakethemharderto
guess.TherearemanyoptionsforenforcingpasswordstrengthinRails,butforsimplicitywelljust
enforceaminimumlengthandtherequirementthatthepasswordnotbeblank.Pickingalengthof6as

areasonableminimumleadstothevalidationtestshowninListing6.38.
Listing6.38:Testingforaminimumpasswordlength.red
test/models/user_test.rb
require'test_helper'
classUserTest<ActiveSupport::TestCase
defsetup
@user=User.new(name:"ExampleUser",email:"user@example.com",
password:"foobar",password_confirmation:"foobar")
end
.
.
.
test"passwordshouldbepresent(nonblank)"do
@user.password=@user.password_confirmation=""*6
assert_not@user.valid?
end
test"passwordshouldhaveaminimumlength"do
@user.password=@user.password_confirmation="a"*5
assert_not@user.valid?
end
end
Notetheuseofthecompactmultipleassignment
@user.password=@user.password_confirmation="a"*5
inListing6.38.Thisarrangestoassignaparticularvaluetothepasswordanditsconfirmationatthe
sametime(inthiscase,astringoflength5,constructedusingstringmultiplicationasinListing6.14).
Youmaybeabletoguessthecodeforenforcingaminimumlengthconstraintbyreferringtothe
correspondingmaximumvalidationfortheusersname(Listing6.16):
validates:password,length:{minimum:6}
Combiningthiswithapresencevalidation(Section6.2.2)toensurenonblankpasswords,thisleadsto
theUsermodelshowninListing6.39.(Itturnsoutthehas_secure_passwordmethodincludesa

presencevalidation,butunfortunatelyitonlyappliestorecordswithemptypasswords,whichallows
userstocreateinvalidpasswordslike(sixspaces).)
Listing6.39:Thecompleteimplementationforsecurepasswords.green
app/models/user.rb
classUser<ActiveRecord::Base
before_save{self.email=email.downcase}
validates:name,presence:true,length:{maximum:50}
VALID_EMAIL_REGEX=/\A[\w+\.]+@[az\d\.]+\.[az]+\z/i
validates:email,presence:true,length:{maximum:255},
format:{with:VALID_EMAIL_REGEX},
uniqueness:{case_sensitive:false}
has_secure_password
validates:password,presence:true,length:{minimum:6}
end
Atthispoint,thetestsshouldbegreen:
Listing6.40:green
$bundleexecraketest:models
6.3.4Creatingandauthenticatingauser
NowthatthebasicUsermodeliscomplete,wellcreateauserinthedatabaseaspreparationfor
makingapagetoshowtheusersinformationinSection7.1.Wellalsotakeamoreconcretelookatthe
effectsofaddinghas_secure_passwordtotheUsermodel,includinganexaminationoftheimportant
authenticatemethod.
SinceuserscantyetsignupforthesampleapplicationthroughthewebthatsthegoalofChapter7
wellusetheRailsconsoletocreateanewuserbyhand.Forconvenience,wellusethecreatemethod
discussedinSection6.1.3,butinthepresentcasewelltakecarenottostartinasandboxsothatthe
resultinguserwillbesavedtothedatabase.Thismeansstartinganordinaryrailsconsolesession
andthencreatingauserwithavalidnameandemailaddresstogetherwithavalidpasswordand
matchingconfirmation:
$railsconsole
>>User.create(name:"MichaelHartl",email:"mhartl@example.com",
?>password:"foobar",password_confirmation:"foobar")
=>#<Userid:1,name:"MichaelHartl",email:"mhartl@example.com",
created_at:"2014091114:26:42",updated_at:"2014091114:26:42",
password_digest:"$2a$10$sLcMI2f8VglgirzjSJOln.Fv9NdLMbqmR4rdTWIXY1G...">

Tocheckthatthisworked,letslookattheresultinguserstableinthedevelopmentdatabaseusingDB
BrowserforSQLite,asshowninFigure6.9.20(IfyoureusingthecloudIDE,youshoulddownloadthe
databasefileasinFigure6.5.)Notethatthecolumnscorrespondtotheattributesofthedatamodel
definedinFigure6.8.

Figure6.9:AuserrowintheSQLitedatabasedb/development.sqlite3.

Returningtotheconsole,wecanseetheeffectofhas_secure_passwordfromListing6.39bylookingat
thepassword_digestattribute:
>>user=User.find_by(email:"mhartl@example.com")
>>user.password_digest
=>"$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQWITUYlG3XVy"
Thisisthehashedversionofthepassword("foobar")usedtoinitializetheuserobject.Becauseits
constructedusingbcrypt,itiscomputationallyimpracticaltousethedigesttodiscovertheoriginal
password.21
AsnotedinSection6.3.1,has_secure_passwordautomaticallyaddsanauthenticatemethodtothe
correspondingmodelobjects.Thismethoddeterminesifagivenpasswordisvalidforaparticularuser
bycomputingitsdigestandcomparingtheresulttopassword_digestinthedatabase.Inthecaseof

theuserwejustcreated,wecantryacoupleofinvalidpasswordsasfollows:
>>user.authenticate("not_the_right_password")
false
>>user.authenticate("foobaz")
false
Hereuser.authenticatereturnsfalseforinvalidpassword.Ifweinsteadauthenticatewiththe
correctpassword,authenticatereturnstheuseritself:
>>user.authenticate("foobar")
=>#<Userid:1,name:"MichaelHartl",email:"mhartl@example.com",
created_at:"2014072502:58:28",updated_at:"2014072502:58:28",
password_digest:"$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW...">
InChapter8,wellusetheauthenticatemethodtosignregisteredusersintooursite.Infact,itwillturn
outnottobeimportantthatauthenticatereturnstheuseritselfallthatwillmatteristhatitreturnsa
valuethatistrueinabooleancontext.Sinceauserobjectisneithernilnorfalse,itdoesthejob
nicely:22
>>!!user.authenticate("foobar")
=>true

You might also like