@@ -35,9 +35,10 @@ func TestFactor(t *testing.T) {
3535// TestAMRMethodForFactorType pins the factor-type -> AMR method mapping.
3636func TestAMRMethodForFactorType (t * testing.T ) {
3737 for factorType , want := range map [string ]AuthenticationMethod {
38- TOTP : TOTPSignIn ,
39- Phone : MFAPhone ,
40- WebAuthn : MFAWebAuthn ,
38+ TOTP : TOTPSignIn ,
39+ Phone : MFAPhone ,
40+ WebAuthn : MFAWebAuthn ,
41+ RecoveryCode : MFARecoveryCode ,
4142 } {
4243 got , err := amrMethodForFactorType (factorType )
4344 require .NoError (t , err , "factor type %q must map" , factorType )
@@ -50,7 +51,7 @@ func TestAMRMethodForFactorType(t *testing.T) {
5051
5152// TestAuthenticationMethodRoundTrip guards the String() <-> ParseAuthenticationMethod symmetry.
5253func TestAuthenticationMethodRoundTrip (t * testing.T ) {
53- for _ , m := range []AuthenticationMethod {TOTPSignIn , MFAPhone , MFAWebAuthn } {
54+ for _ , m := range []AuthenticationMethod {TOTPSignIn , MFAPhone , MFAWebAuthn , MFARecoveryCode } {
5455 parsed , err := ParseAuthenticationMethod (m .String ())
5556 require .NoError (t , err , "method %q must round-trip" , m .String ())
5657 require .Equal (t , m , parsed )
@@ -78,6 +79,41 @@ func (ts *FactorTestSuite) TestFindFactorByFactorID() {
7879 require .EqualError (ts .T (), err , FactorNotFoundError {}.Error ())
7980}
8081
82+ func (ts * FactorTestSuite ) TestNewRecoveryCodeFactor () {
83+ user , err := NewUser ("" , "recoveryfactor@example.com" , "secret" , "test" , nil )
84+ require .NoError (ts .T (), err )
85+
86+ factor := NewRecoveryCodeFactor (user , "my recovery codes" )
87+ require .Equal (ts .T (), RecoveryCode , factor .FactorType )
88+ require .Equal (ts .T (), FactorStateVerified .String (), factor .Status )
89+ require .True (ts .T (), factor .IsVerified ())
90+ require .Equal (ts .T (), "my recovery codes" , factor .FriendlyName )
91+ require .True (ts .T (), factor .IsRecoveryCodeFactor ())
92+ require .False (ts .T (), ts .TestFactor .IsRecoveryCodeFactor ())
93+
94+ // blank names fall back to the default
95+ require .Equal (ts .T (), DefaultRecoveryCodeFriendlyName , NewRecoveryCodeFactor (user , "" ).FriendlyName )
96+ require .Equal (ts .T (), DefaultRecoveryCodeFriendlyName , NewRecoveryCodeFactor (user , " " ).FriendlyName )
97+ }
98+
99+ func (ts * FactorTestSuite ) TestFindRecoveryCodeFactorByUser () {
100+ // the fixture user has only a TOTP factor
101+ _ , err := FindRecoveryCodeFactorByUser (ts .db , ts .TestFactor .UserID )
102+ require .EqualError (ts .T (), err , FactorNotFoundError {}.Error ())
103+
104+ user , err := NewUser ("" , "findrecovery@example.com" , "secret" , "test" , nil )
105+ require .NoError (ts .T (), err )
106+ require .NoError (ts .T (), ts .db .Create (user ))
107+
108+ factor := NewRecoveryCodeFactor (user , "" )
109+ require .NoError (ts .T (), ts .db .Create (factor ))
110+
111+ found , err := FindRecoveryCodeFactorByUser (ts .db , user .ID )
112+ require .NoError (ts .T (), err )
113+ require .Equal (ts .T (), factor .ID , found .ID )
114+ require .True (ts .T (), found .IsRecoveryCodeFactor ())
115+ }
116+
81117func (ts * FactorTestSuite ) TestUpdateStatus () {
82118 newFactorStatus := FactorStateVerified
83119 require .NoError (ts .T (), ts .TestFactor .UpdateStatus (ts .db , newFactorStatus ))
@@ -122,6 +158,11 @@ func (ts *FactorTestSuite) TestDowngradeSessionsToAAL1RemovesAMRClaim() {
122158 newFactor : func (u * User ) * Factor { return NewTOTPFactor (u , "totpfactor" ) },
123159 authMethod : TOTPSignIn ,
124160 },
161+ {
162+ desc : "recovery_code" ,
163+ newFactor : func (u * User ) * Factor { return NewRecoveryCodeFactor (u , "" ) },
164+ authMethod : MFARecoveryCode ,
165+ },
125166 }
126167
127168 for i , c := range cases {
0 commit comments