package entity import ( "testing" "github.com/pquerna/otp" "github.com/stretchr/testify/assert" "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/rnd" ) func TestNewAuthKey(t *testing.T) { t.Run("Valid", func(t *testing.T) { uid := "us7gqkzx1g9a82h4" keyUrl := "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=sha256&digits=8" recoveryCode := rnd.RecoveryCode() m, err := NewPasscode(uid, keyUrl, recoveryCode) if err != nil { t.Fatal(err) } t.Logf("NewPasscode/Valid: %#v", m) assert.NotNil(t, m) assert.Equal(t, uid, m.UID) assert.Equal(t, authn.KeyTOTP.String(), m.KeyType) assert.True(t, authn.KeyTOTP.Equal(m.KeyType)) assert.Equal(t, recoveryCode, m.RecoveryCode) }) t.Run("InvalidUID", func(t *testing.T) { m, err := NewPasscode("foo", "", "") t.Logf("TestNewAuthKey/Invalid: %#v", m) assert.Error(t, err) assert.NotNil(t, m) assert.Equal(t, "foo", m.UID) assert.True(t, authn.KeyTOTP.NotEqual(m.KeyType)) assert.Equal(t, "", m.RecoveryCode) }) t.Run("EmptyUrl", func(t *testing.T) { m, err := NewPasscode("us7gqkzx1g9axxxx", "", "") t.Logf("TestNewAuthKey/Invalid: %#v", m) assert.Error(t, err) assert.NotNil(t, m) assert.Equal(t, "us7gqkzx1g9axxxx", m.UID) assert.True(t, authn.KeyTOTP.NotEqual(m.KeyType)) assert.Equal(t, "", m.RecoveryCode) }) t.Run("InvalidUrl", func(t *testing.T) { m, err := NewPasscode("us7gqkzx1g9axxxx", "avcgo6842485%^^&", "") t.Logf("TestNewAuthKey/Invalid: %#v", m) assert.Error(t, err) assert.NotNil(t, m) assert.Equal(t, "us7gqkzx1g9axxxx", m.UID) assert.True(t, authn.KeyTOTP.NotEqual(m.KeyType)) assert.Equal(t, "", m.RecoveryCode) }) } func TestAuthKey_Key(t *testing.T) { t.Run("Valid", func(t *testing.T) { uid := "us7gqkzx1g9a82h4" keyUrl := "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=sha256&digits=8" recoveryCode := rnd.RecoveryCode() m, err := NewPasscode(uid, keyUrl, recoveryCode) if err != nil { t.Fatal(err) } key := m.Key() t.Logf("TestAuthKey_Key/Valid: %#v", m) assert.NotNil(t, m) assert.Equal(t, authn.KeyTOTP.String(), key.Type()) assert.Equal(t, keyUrl, key.URL()) assert.Equal(t, uint64(30), key.Period()) assert.Equal(t, "JBSWY3DPEHPK3PXP", key.Secret()) assert.Equal(t, keyUrl, key.String()) assert.Equal(t, "alice@google.com", key.AccountName()) assert.Equal(t, "SHA256", key.Algorithm().String()) assert.Equal(t, otp.Digits(8), key.Digits()) assert.Equal(t, 8, key.Digits().Length()) assert.Equal(t, recoveryCode, m.RecoveryCode) }) t.Run("Invalid", func(t *testing.T) { m, err := NewPasscode("foo", "", "") if err == nil { t.Fatal("error expected") } key := m.Key() t.Logf("TestAuthKey_Key/Invalid: %#v", m) assert.Error(t, err) assert.NotNil(t, m) assert.Equal(t, "", key.Type()) assert.Equal(t, "", key.URL()) assert.Equal(t, uint64(30), key.Period()) assert.Equal(t, "", key.Secret()) assert.Equal(t, "", key.String()) assert.Equal(t, "", key.AccountName()) assert.Equal(t, "SHA1", key.Algorithm().String()) assert.Equal(t, otp.DigitsSix, key.Digits()) assert.Equal(t, 6, key.Digits().Length()) assert.Equal(t, "", m.RecoveryCode) }) } func TestPasscode_Delete(t *testing.T) { t.Run("UidNotSet", func(t *testing.T) { m := &Passcode{ UID: "", KeyURL: "otpauth://totp/PhotoPrism:bob?algorithm=SHA1&digits=6&issuer=PhotoPrism%20Pro&period=30", RecoveryCode: "123", } err := m.Delete() assert.Error(t, err) }) } func TestPasscode_SetUID(t *testing.T) { t.Run("Valid", func(t *testing.T) { m := &Passcode{ UID: "123", KeyURL: "", RecoveryCode: "123", } assert.True(t, m.InvalidUID()) passcode := m.SetUID("uqxc08w3d0ej2283") assert.False(t, passcode.InvalidUID()) }) t.Run("Invalid", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "", RecoveryCode: "123", } assert.False(t, m.InvalidUID()) passcode := m.SetUID("xxx") assert.False(t, passcode.InvalidUID()) }) } func TestPasscode_SetKey(t *testing.T) { t.Run("Success", func(t *testing.T) { uid := "uqxc08w3d0ej2283" keyUrl := "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP" recoveryCode := "123" m, err := NewPasscode(uid, keyUrl, recoveryCode) if err != nil { t.Fatal(err) } assert.Equal(t, "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP", m.Key().String()) key, err := otp.NewKeyFromURL("otpauth://totp/Example:bob?secret=JBSWY3DPEHPK3PXP") if err != nil { t.Fatal(err) } err = m.SetKey(key) if err != nil { t.Fatal(err) } assert.Equal(t, "otpauth://totp/Example:bob?secret=JBSWY3DPEHPK3PXP", m.Key().String()) }) t.Run("InvalidKeyType", func(t *testing.T) { uid := "uqxc08w3d0ej2283" keyUrl := "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP" recoveryCode := "123" m, err := NewPasscode(uid, keyUrl, recoveryCode) if err != nil { t.Fatal(err) } assert.Equal(t, "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP", m.Key().String()) key, err := otp.NewKeyFromURL("otpauth://xxx/Example:bob?secret=JBSWY3DPEHPK3PXP") if err != nil { t.Fatal(err) } err = m.SetKey(key) assert.Error(t, err) assert.Equal(t, "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP", m.Key().String()) }) t.Run("NoSecret", func(t *testing.T) { uid := "uqxc08w3d0ej2283" keyUrl := "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP" recoveryCode := "123" m, err := NewPasscode(uid, keyUrl, recoveryCode) if err != nil { t.Fatal(err) } assert.Equal(t, "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP", m.Key().String()) key, err := otp.NewKeyFromURL("otpauth://totp/Example:bob") if err != nil { t.Fatal(err) } assert.Equal(t, "", key.Secret()) err = m.SetKey(key) assert.Error(t, err) assert.Equal(t, "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP", m.Key().String()) }) } func TestPasscode_Secret(t *testing.T) { t.Run("Success", func(t *testing.T) { uid := "uqxc08w3d0ej2283" keyUrl := "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP" recoveryCode := "123" m, err := NewPasscode(uid, keyUrl, recoveryCode) if err != nil { t.Fatal(err) } assert.Equal(t, "JBSWY3DPEHPK3PXP", m.Secret()) }) t.Run("NoSecret", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Equal(t, "", m.Secret()) }) } func TestPasscode_GenerateCode(t *testing.T) { t.Run("Success", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } code, err := m.GenerateCode() if err != nil { t.Fatal(err) } assert.Equal(t, 6, len(code)) }) t.Run("InvalidType", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://xxx/Example:alice", KeyType: "xxx", RecoveryCode: "123", } code, err := m.GenerateCode() assert.Error(t, err) assert.Equal(t, 0, len(code)) }) } func TestPasscode_Verify(t *testing.T) { t.Run("ValidCode", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Nil(t, m.VerifiedAt) code, err := m.GenerateCode() if err != nil { t.Fatal(err) } valid, recoveryCode, err := m.Valid(code) if err != nil { t.Fatal(err) } assert.True(t, valid) assert.False(t, recoveryCode) assert.NotNil(t, m.VerifiedAt) }) t.Run("InvalidCode", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Nil(t, m.VerifiedAt) valid, recoveryCode, err := m.Valid("123456") if err != nil { t.Fatal(err) } assert.False(t, valid) assert.False(t, recoveryCode) assert.Nil(t, m.VerifiedAt) }) t.Run("CodeTooShort", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Nil(t, m.VerifiedAt) valid, recoveryCode, err := m.Valid("111") assert.Error(t, err) assert.False(t, valid) assert.False(t, recoveryCode) assert.Nil(t, m.VerifiedAt) }) t.Run("ValidRecoveryCode", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Nil(t, m.VerifiedAt) valid, recoveryCode, err := m.Valid("123") if err != nil { t.Fatal(err) } assert.True(t, valid) assert.True(t, recoveryCode) assert.Nil(t, m.VerifiedAt) }) t.Run("ErrPasscodeRequired", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Nil(t, m.VerifiedAt) valid, recoveryCode, err := m.Valid("") assert.Error(t, err) assert.False(t, valid) assert.False(t, recoveryCode) assert.Nil(t, m.VerifiedAt) }) t.Run("ErrInvalidPasscodeFormat", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Nil(t, m.VerifiedAt) valid, recoveryCode, err := m.Valid("123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891") assert.Error(t, err) assert.False(t, valid) assert.False(t, recoveryCode) assert.Nil(t, m.VerifiedAt) }) t.Run("ErrInvalidPasscodeType", func(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://xxx/Example:alice", RecoveryCode: "123", KeyType: "xxx", } assert.Nil(t, m.VerifiedAt) valid, recoveryCode, err := m.Valid("123456") assert.Error(t, err) assert.False(t, valid) assert.False(t, recoveryCode) assert.Nil(t, m.VerifiedAt) }) } func TestPasscode_Activate(t *testing.T) { m := &Passcode{ UID: "uqxc08w3d0ej2283", KeyURL: "otpauth://totp/Example:alice", RecoveryCode: "123", } assert.Nil(t, m.VerifiedAt) assert.Nil(t, m.ActivatedAt) err := m.Activate() assert.Equal(t, authn.ErrPasscodeNotVerified, err) assert.Nil(t, m.ActivatedAt) code, err := m.GenerateCode() if err != nil { t.Fatal(err) } _, _, err = m.Valid(code) if err != nil { t.Fatal(err) } assert.NotNil(t, m.VerifiedAt) assert.Nil(t, m.ActivatedAt) err = m.Activate() if err != nil { t.Fatal(err) } assert.NotNil(t, m.ActivatedAt) err = m.Activate() assert.Equal(t, authn.ErrPasscodeAlreadyActivated, err) }