package handlers

import (
	"database/sql"
	"encoding/base64"
	"fmt"
	"net"
	"net/mail"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/valyala/fasthttp"
	"go.uber.org/mock/gomock"

	"github.com/authelia/authelia/v4/internal/mocks"
	"github.com/authelia/authelia/v4/internal/model"
	"github.com/authelia/authelia/v4/internal/random"
	"github.com/authelia/authelia/v4/internal/session"
	"github.com/authelia/authelia/v4/internal/templates"
)

func TestUserSessionElevationGET(t *testing.T) {
	testCases := []struct {
		name           string
		setup          func(t *testing.T, mock *mocks.MockAutheliaCtx)
		expected       string
		expectedStatus int
		expectedf      func(t *testing.T, mock *mocks.MockAutheliaCtx)
	}{
		{
			"ShouldHandleOneFactor",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				mock.StorageMock.EXPECT().LoadUserInfo(mock.Ctx, testUsername).Return(model.UserInfo{
					DisplayName: testDisplayName,
					Method:      "totp",
					HasTOTP:     true,
					HasWebAuthn: true,
					HasDuo:      true,
				}, nil)
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":false,"can_skip_second_factor":false,"factor_knowledge":true,"elevated":false,"expires":0}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleOneFactorRequireSecondFactor",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Configuration.IdentityValidation.ElevatedSession.RequireSecondFactor = true

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				mock.StorageMock.EXPECT().LoadUserInfo(mock.Ctx, testUsername).Return(model.UserInfo{
					DisplayName: testDisplayName,
					Method:      "totp",
					HasTOTP:     true,
					HasWebAuthn: true,
					HasDuo:      true,
				}, nil)
			},
			`{"status":"OK","data":{"require_second_factor":true,"skip_second_factor":false,"can_skip_second_factor":false,"factor_knowledge":true,"elevated":false,"expires":0}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleOneFactorRequireSecondFactorWithoutSecondFactor",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Configuration.IdentityValidation.ElevatedSession.RequireSecondFactor = true

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				mock.StorageMock.EXPECT().LoadUserInfo(mock.Ctx, testUsername).Return(model.UserInfo{
					DisplayName: testDisplayName,
					Method:      "totp",
					HasTOTP:     false,
					HasWebAuthn: false,
					HasDuo:      false,
				}, nil)
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":false,"can_skip_second_factor":false,"factor_knowledge":true,"elevated":false,"expires":0}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleOneFactorSkipSecondFactor",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Configuration.IdentityValidation.ElevatedSession.SkipSecondFactor = true

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				mock.StorageMock.EXPECT().LoadUserInfo(mock.Ctx, testUsername).Return(model.UserInfo{
					DisplayName: testDisplayName,
					Method:      "totp",
					HasTOTP:     true,
					HasWebAuthn: true,
					HasDuo:      true,
				}, nil)
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":false,"can_skip_second_factor":true,"factor_knowledge":true,"elevated":false,"expires":0}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleTwoFactor",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.WebAuthn = true

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":false,"can_skip_second_factor":false,"factor_knowledge":false,"elevated":false,"expires":0}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleTwoFactorSkip",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Configuration.IdentityValidation.ElevatedSession.SkipSecondFactor = true

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.WebAuthn = true

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":true,"can_skip_second_factor":false,"factor_knowledge":false,"elevated":false,"expires":0}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleAnonymous",
			nil,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred retrieving user session elevation state", "user is anonymous")
			},
		},
		{
			"ShouldHandleBadSessionDomain",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.notexample.com")
			},
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred retrieving user session elevation state: error occurred retrieving the user session data", "unable to retrieve session cookie domain provider: no configured session cookie domain matches the url 'https://auth.notexample.com'")
			},
		},
		{
			"ShouldHandleElevated",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Configuration.IdentityValidation.ElevatedSession.SkipSecondFactor = true

				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.WebAuthn = true
				us.Elevations.User = &session.Elevation{
					ID:       1,
					RemoteIP: mock.Ctx.RemoteIP(),
					Expires:  mock.Clock.Now().Add(10 * time.Minute),
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":true,"can_skip_second_factor":false,"factor_knowledge":false,"elevated":true,"expires":600}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleElevatedCanSkip",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.WebAuthn = true
				us.Elevations.User = &session.Elevation{
					ID:       1,
					RemoteIP: mock.Ctx.RemoteIP(),
					Expires:  mock.Clock.Now().Add(10 * time.Minute),
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":false,"can_skip_second_factor":false,"factor_knowledge":false,"elevated":true,"expires":600}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleElevationBadIP",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.WebAuthn = true
				us.Elevations.User = &session.Elevation{
					ID:       1,
					RemoteIP: net.ParseIP("1.1.1.1"),
					Expires:  mock.Clock.Now().Add(10 * time.Minute),
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":false,"can_skip_second_factor":false,"factor_knowledge":false,"elevated":false,"expires":0}}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.Elevations.User)

				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "The user session elevation was created from a different remote IP so it has been destroyed", "")
			},
		},
		{
			"ShouldHandleElevationExpired",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.AuthenticationMethodRefs.UsernameAndPassword = true
				us.AuthenticationMethodRefs.WebAuthn = true
				us.Elevations.User = &session.Elevation{
					ID:       1,
					RemoteIP: mock.Ctx.RemoteIP(),
					Expires:  mock.Clock.Now().Add(-time.Minute),
				}

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"status":"OK","data":{"require_second_factor":false,"skip_second_factor":false,"can_skip_second_factor":false,"factor_knowledge":false,"elevated":false,"expires":-60}}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				assert.Nil(t, us.Elevations.User)
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mock := mocks.NewMockAutheliaCtx(t)

			defer mock.Close()

			mock.Ctx.Clock = &mock.Clock

			if tc.setup != nil {
				tc.setup(t, mock)
			}

			UserSessionElevationGET(mock.Ctx)

			assert.Equal(t, tc.expectedStatus, mock.Ctx.Response.StatusCode())
			assert.Equal(t, tc.expected, string(mock.Ctx.Response.Body()))

			if tc.expectedf != nil {
				tc.expectedf(t, mock)
			}
		})
	}
}

func TestUserSessionElevationPOST(t *testing.T) {
	testCases := []struct {
		name           string
		setup          func(t *testing.T, mock *mocks.MockAutheliaCtx)
		expected       string
		expectedStatus int
		expectedf      func(t *testing.T, mock *mocks.MockAutheliaCtx)
	}{
		{
			"ShouldHandleOneFactor",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.RandomMock.EXPECT().
						Read(gomock.Any()).
						SetArg(0, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x22, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15}).
						Return(16, nil),
					mock.RandomMock.EXPECT().
						BytesCustomErr(10, []byte(random.CharSetUnambiguousUpper)).
						Return([]byte("ABC123ABC1"), nil),
					mock.StorageMock.EXPECT().
						SaveOneTimeCode(mock.Ctx, model.OneTimeCode{
							PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
							IssuedAt:  mock.Clock.Now(),
							IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
							ExpiresAt: mock.Clock.Now().Add(time.Minute),
							Username:  testUsername,
							Intent:    model.OTCIntentUserSessionElevation,
							Code:      []byte("ABC123ABC1"),
						}).
						Return("abc123", nil),
					mock.NotifierMock.EXPECT().Send(mock.Ctx, mail.Address{Name: testDisplayName, Address: "john@example.com"}, "Confirm your identity", gomock.Any(), templates.EmailIdentityVerificationOTCValues{
						Title:              "Confirm your identity",
						RevocationLinkURL:  "http://example.com/revoke/one-time-code?id=AQIDBAUGRyKJEBESExQVAA",
						RevocationLinkText: "Revoke",
						DisplayName:        testDisplayName,
						Domain:             "example.com",
						RemoteIP:           "0.0.0.0",
						OneTimeCode:        "ABC123ABC1",
					}).
						Return(nil),
				)
			},
			`{"status":"OK","data":{"delete_id":"AQIDBAUGRyKJEBESExQVAA"}}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleOneFactorFailEmail",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.RandomMock.EXPECT().
						Read(gomock.Any()).
						SetArg(0, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x22, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15}).
						Return(16, nil),
					mock.RandomMock.EXPECT().
						BytesCustomErr(10, []byte(random.CharSetUnambiguousUpper)).
						Return([]byte("ABC123ABC1"), nil),
					mock.StorageMock.EXPECT().
						SaveOneTimeCode(mock.Ctx, model.OneTimeCode{
							PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
							IssuedAt:  mock.Clock.Now(),
							IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
							ExpiresAt: mock.Clock.Now().Add(time.Minute),
							Username:  testUsername,
							Intent:    model.OTCIntentUserSessionElevation,
							Code:      []byte("ABC123ABC1"),
						}).
						Return("abc123", nil),
					mock.NotifierMock.EXPECT().Send(mock.Ctx, mail.Address{Name: testDisplayName, Address: "john@example.com"}, "Confirm your identity", gomock.Any(), templates.EmailIdentityVerificationOTCValues{
						Title:              "Confirm your identity",
						RevocationLinkURL:  "http://example.com/revoke/one-time-code?id=AQIDBAUGRyKJEBESExQVAA",
						RevocationLinkText: "Revoke",
						DisplayName:        testDisplayName,
						Domain:             "example.com",
						RemoteIP:           "0.0.0.0",
						OneTimeCode:        "ABC123ABC1",
					}).
						Return(fmt.Errorf("rejected")),
				)
			},
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred creating user session elevation One-Time Code challenge for user 'john': error occurred sending the user the notification", "rejected")
			},
		},
		{
			"ShouldHandleOneFactorFailInsert",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.RandomMock.EXPECT().
						Read(gomock.Any()).
						SetArg(0, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x22, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15}).
						Return(16, nil),
					mock.RandomMock.EXPECT().
						BytesCustomErr(10, []byte(random.CharSetUnambiguousUpper)).
						Return([]byte("ABC123ABC1"), nil),
					mock.StorageMock.EXPECT().
						SaveOneTimeCode(mock.Ctx, model.OneTimeCode{
							PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
							IssuedAt:  mock.Clock.Now(),
							IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
							ExpiresAt: mock.Clock.Now().Add(time.Minute),
							Username:  testUsername,
							Intent:    model.OTCIntentUserSessionElevation,
							Code:      []byte("ABC123ABC1"),
						}).
						Return("", fmt.Errorf("failed to insert")),
				)
			},
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred creating user session elevation One-Time Code challenge for user 'john': error occurred saving the challenge to the storage backend", "failed to insert")
			},
		},
		{
			"ShouldHandleOneFactorFailGenerateOTC",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.RandomMock.EXPECT().
						Read(gomock.Any()).
						SetArg(0, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x22, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15}).
						Return(16, nil),
					mock.RandomMock.EXPECT().
						BytesCustomErr(10, []byte(random.CharSetUnambiguousUpper)).
						Return(nil, fmt.Errorf("deadlock")),
				)
			},
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred creating user session elevation One-Time Code challenge for user 'john': error occurred generating the challenge", "failed to generate random bytes: deadlock")
			},
		},
		{
			"ShouldHandleOneFactorFailGenerateUUID",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.RandomMock.EXPECT().
						Read(gomock.Any()).
						Return(0, fmt.Errorf("random unavailable")),
				)
			},
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred creating user session elevation One-Time Code challenge for user 'john': error occurred generating the challenge", "failed to generate public id: random unavailable")
			},
		},
		{
			"ShouldHandleAnonymous",
			nil,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred creating user session elevation One-Time Code challenge", "user is anonymous")
			},
		},
		{
			"ShouldHandleGetSessionError",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.notexample.com")
			},
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred creating user session elevation One-Time Code challenge: error occurred retrieving the user session data", "unable to retrieve session cookie domain provider: no configured session cookie domain matches the url 'https://auth.notexample.com'")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mock := mocks.NewMockAutheliaCtx(t)

			defer mock.Close()

			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.Characters = 10
			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.ElevationLifespan = time.Minute
			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.CodeLifespan = time.Minute

			mock.Ctx.Clock = &mock.Clock
			mock.Ctx.Providers.Random = mock.RandomMock

			if tc.setup != nil {
				tc.setup(t, mock)
			}

			UserSessionElevationPOST(mock.Ctx)

			assert.Equal(t, tc.expectedStatus, mock.Ctx.Response.StatusCode())
			assert.Equal(t, tc.expected, string(mock.Ctx.Response.Body()))

			if tc.expectedf != nil {
				tc.expectedf(t, mock)
			}
		})
	}
}

func TestUserSessionElevationPUT(t *testing.T) {
	testCases := []struct {
		name           string
		setup          func(t *testing.T, mock *mocks.MockAutheliaCtx)
		have           string
		expected       string
		expectedStatus int
		expectedf      func(t *testing.T, mock *mocks.MockAutheliaCtx)
	}{
		{
			"ShouldHandleValidCode",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
					mock.StorageMock.
						EXPECT().
						ConsumeOneTimeCode(mock.Ctx, code).
						Return(nil),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"OK"}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleValidCodeWithWhiteSpace",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
					mock.StorageMock.
						EXPECT().
						ConsumeOneTimeCode(mock.Ctx, code).
						Return(nil),
				)
			},
			`{"otc":"ABC123ABC1   "}`,
			`{"status":"OK"}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleAnonymous",
			nil,
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge", "user is anonymous")
			},
		},
		{
			"ShouldHandleGetSessionError",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				mock.Ctx.Request.Header.Set("X-Original-URL", "https://auth.notexample.com")
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge: error occurred retrieving the user session data", "unable to retrieve session cookie domain provider: no configured session cookie domain matches the url 'https://auth.notexample.com'")
			},
		},
		{
			"ShouldHandleInvalidCode",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("WRONG"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john'", "the code does not match the code stored in the challenge")
			},
		},
		{
			"ShouldHandleBadJSON",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"otc":ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusBadRequest,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john': error parsing the request body", "unable to parse body: invalid character 'A' looking for beginning of value")
			},
		},
		{
			"ShouldHandleLongCodes",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			`{"otc":"ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusBadRequest,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john': expected maximum code length is 20 but the user provided code was 360 characters in length", "")
			},
		},
		{
			"ShouldHandleConsumeError",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
					mock.StorageMock.
						EXPECT().
						ConsumeOneTimeCode(mock.Ctx, code).
						Return(fmt.Errorf("failed to consume")),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john': error occurred saving the consumption of the code to storage", "failed to consume")
			},
		},
		{
			"ShouldHandleAlreadyConsumedChallenge",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:         1,
					PublicID:   uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:   mock.Clock.Now(),
					IssuedIP:   model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt:  mock.Clock.Now().Add(time.Minute),
					Username:   testUsername,
					Intent:     model.OTCIntentUserSessionElevation,
					ConsumedAt: sql.NullTime{Valid: true, Time: mock.Clock.Now().Add(-time.Minute)},
					Code:       []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john'", "the code challenge has already been consumed")
			},
		},
		{
			"ShouldHandleAlreadyRevokedChallenge",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					RevokedAt: sql.NullTime{Valid: true, Time: mock.Clock.Now().Add(-time.Minute)},
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john'", "the code challenge has been revoked")
			},
		},
		{
			"ShouldHandleAlreadyExpiredChallenge",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(-time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john'", "the code challenge has expired")
			},
		},
		{
			"ShouldHandleInvalidIntent",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(time.Minute),
					Username:  testUsername,
					Intent:    "abc",
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(code, nil),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john'", "the code challenge has the 'abc' intent but the 'use' intent is required")
			},
		},
		{
			"ShouldHandleNilChallenge",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(nil, nil),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john': error occurred retrieving the code challenge from the storage backend", "the code didn't match any recorded code challenges")
			},
		},
		{
			"ShouldHandleStorageError",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCode(mock.Ctx, testUsername, model.OTCIntentUserSessionElevation, "ABC123ABC1").
						Return(nil, fmt.Errorf("not found")),
				)
			},
			`{"otc":"ABC123ABC1"}`,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusForbidden,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred validating user session elevation One-Time Code challenge for user 'john': error occurred retrieving the code challenge from the storage backend", "not found")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mock := mocks.NewMockAutheliaCtx(t)

			defer mock.Close()

			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.Characters = 10
			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.ElevationLifespan = time.Minute
			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.CodeLifespan = time.Minute

			mock.Ctx.Clock = &mock.Clock
			mock.Ctx.Providers.Random = mock.RandomMock

			if len(tc.have) != 0 {
				mock.Ctx.Request.SetBodyString(tc.have)
			}

			if tc.setup != nil {
				tc.setup(t, mock)
			}

			UserSessionElevationPUT(mock.Ctx)

			assert.Equal(t, tc.expectedStatus, mock.Ctx.Response.StatusCode())
			assert.Equal(t, tc.expected, string(mock.Ctx.Response.Body()))

			if tc.expectedf != nil {
				tc.expectedf(t, mock)
			}
		})
	}
}

func TestUserSessionElevationDELETE(t *testing.T) {
	id := uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500"))

	eid := base64.RawURLEncoding.EncodeToString(id[:])

	testCases := []struct {
		name           string
		setup          func(t *testing.T, mock *mocks.MockAutheliaCtx)
		have           string
		expected       string
		expectedStatus int
		expectedf      func(t *testing.T, mock *mocks.MockAutheliaCtx)
	}{
		{
			"ShouldHandleValidCode",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(-time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCodeByPublicID(mock.Ctx, id).
						Return(code, nil),
					mock.StorageMock.
						EXPECT().
						RevokeOneTimeCode(mock.Ctx, id, model.NewIP(net.ParseIP("0.0.0.0"))).
						Return(nil),
				)
			},
			eid,
			`{"status":"OK"}`,
			fasthttp.StatusOK,
			nil,
		},
		{
			"ShouldHandleStorageRevokeError",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(-time.Minute),
					Username:  testUsername,
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCodeByPublicID(mock.Ctx, id).
						Return(code, nil),
					mock.StorageMock.
						EXPECT().
						RevokeOneTimeCode(mock.Ctx, id, model.NewIP(net.ParseIP("0.0.0.0"))).
						Return(fmt.Errorf("failed to update")),
				)
			},
			eid,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred revoking user session elevation One-Time Code challenge: error occurred saving the revocation to the storage backend", "failed to update")
			},
		},
		{
			"ShouldHandleStorageRevokeErrorBadIntent",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(-time.Minute),
					Username:  testUsername,
					Intent:    "abc",
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCodeByPublicID(mock.Ctx, id).
						Return(code, nil),
				)
			},
			eid,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred revoking user session elevation One-Time Code challenge", "the code challenge has the 'abc' intent but the 'use' intent is required")
			},
		},
		{
			"ShouldHandleStorageRevokeErrorConsumed",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:         1,
					PublicID:   uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:   mock.Clock.Now(),
					IssuedIP:   model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt:  mock.Clock.Now().Add(-time.Minute),
					Username:   testUsername,
					ConsumedAt: sql.NullTime{Valid: true, Time: mock.Clock.Now().Add(-time.Minute)},
					Intent:     model.OTCIntentUserSessionElevation,
					Code:       []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCodeByPublicID(mock.Ctx, id).
						Return(code, nil),
				)
			},
			eid,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred revoking user session elevation One-Time Code challenge", "the code challenge has already been consumed")
			},
		},
		{
			"ShouldHandleStorageRevokeErrorRevoked",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				code := &model.OneTimeCode{
					ID:        1,
					PublicID:  uuid.Must(uuid.Parse("01020304-0506-4722-8910-111213141500")),
					IssuedAt:  mock.Clock.Now(),
					IssuedIP:  model.NewIP(net.ParseIP("0.0.0.0")),
					ExpiresAt: mock.Clock.Now().Add(-time.Minute),
					Username:  testUsername,
					RevokedAt: sql.NullTime{Valid: true, Time: mock.Clock.Now().Add(-time.Minute)},
					Intent:    model.OTCIntentUserSessionElevation,
					Code:      []byte("ABC123ABC1"),
				}

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCodeByPublicID(mock.Ctx, id).
						Return(code, nil),
				)
			},
			eid,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred revoking user session elevation One-Time Code challenge", "the code challenge has already been revoked")
			},
		},
		{
			"ShouldHandleStorageRevokeErrorStorage",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))

				gomock.InOrder(
					mock.StorageMock.
						EXPECT().
						LoadOneTimeCodeByPublicID(mock.Ctx, id).
						Return(nil, fmt.Errorf("invalid user")),
				)
			},
			eid,
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred revoking user session elevation One-Time Code challenge: error occurred retrieving the code challenge from the storage backend", "invalid user")
			},
		},
		{
			"ShouldHandleBadUUID",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			base64.RawURLEncoding.EncodeToString([]byte("abc")),
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred revoking user session elevation One-Time Code challenge: error occurred parsing the identifier", "invalid UUID (got 3 bytes)")
			},
		},
		{
			"ShouldHandleBadBase64",
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				us, err := mock.Ctx.GetSession()

				require.NoError(t, err)

				us.Username = testUsername
				us.DisplayName = testDisplayName
				us.Emails = []string{"john@example.com"}

				us.AuthenticationMethodRefs.UsernameAndPassword = true

				require.NoError(t, mock.Ctx.SaveSession(us))
			},
			"=====123123",
			`{"status":"KO","message":"Operation failed."}`,
			fasthttp.StatusOK,
			func(t *testing.T, mock *mocks.MockAutheliaCtx) {
				AssertLogEntryMessageAndError(t, mock.Hook.LastEntry(), "Error occurred revoking user session elevation One-Time Code challenge: error occurred decoding the identifier", "illegal base64 data at input byte 0")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			mock := mocks.NewMockAutheliaCtx(t)

			defer mock.Close()

			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.Characters = 10
			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.ElevationLifespan = time.Minute
			mock.Ctx.Configuration.IdentityValidation.ElevatedSession.CodeLifespan = time.Minute

			mock.Ctx.Clock = &mock.Clock
			mock.Ctx.Providers.Random = mock.RandomMock

			if len(tc.have) != 0 {
				mock.Ctx.SetUserValue("id", tc.have)
			}

			if tc.setup != nil {
				tc.setup(t, mock)
			}

			UserSessionElevateDELETE(mock.Ctx)

			assert.Equal(t, tc.expectedStatus, mock.Ctx.Response.StatusCode())
			assert.Equal(t, tc.expected, string(mock.Ctx.Response.Body()))

			if tc.expectedf != nil {
				tc.expectedf(t, mock)
			}
		})
	}
}
