@@ -6,9 +6,14 @@ import {
66 SentinelDecision ,
77} from '@logto/schemas' ;
88import { addMinutes } from 'date-fns' ;
9+ import Sinon from 'sinon' ;
910
1011import { createMockCommonQueryMethods , expectSqlString } from '#src/test-utils/query.js' ;
1112
13+ import { mockSignInExperience } from '../__mocks__/sign-in-experience.js' ;
14+ import { EnvSet } from '../env-set/index.js' ;
15+ import { MockQueries } from '../test-utils/tenant.js' ;
16+
1217import BasicSentinel from './basic-sentinel.js' ;
1318
1419const { jest } = import . meta;
@@ -25,10 +30,27 @@ class TestSentinel extends BasicSentinel {
2530 override decide = super . decide ;
2631}
2732
33+ const findDefaultSignInExperienceMock = jest . fn ( ) ;
34+
2835const methods = createMockCommonQueryMethods ( ) ;
29- const sentinel = new TestSentinel ( methods ) ;
36+ const sentinel = new TestSentinel (
37+ methods ,
38+ new MockQueries ( {
39+ signInExperiences : {
40+ findDefaultSignInExperience : findDefaultSignInExperienceMock ,
41+ } ,
42+ } )
43+ ) ;
3044const mockedTime = new Date ( '2021-01-01T00:00:00.000Z' ) . valueOf ( ) ;
31- const mockedBlockedTime = addMinutes ( mockedTime , 10 ) . valueOf ( ) ;
45+ const mockedDefaultBlockedTime = addMinutes ( mockedTime , 10 ) . valueOf ( ) ;
46+ const customSentinelPolicy = {
47+ maxAttempts : 7 ,
48+ lockoutDuration : 15 ,
49+ } ;
50+ const mockedCustomBlockedTime = addMinutes (
51+ mockedTime ,
52+ customSentinelPolicy . lockoutDuration
53+ ) . valueOf ( ) ;
3254
3355beforeAll ( ( ) => {
3456 jest . useFakeTimers ( ) . setSystemTime ( mockedTime ) ;
@@ -39,6 +61,10 @@ afterEach(() => {
3961} ) ;
4062
4163describe ( 'BasicSentinel -> reportActivity()' , ( ) => {
64+ beforeEach ( ( ) => {
65+ findDefaultSignInExperienceMock . mockResolvedValue ( mockSignInExperience ) ;
66+ } ) ;
67+
4268 it ( 'should insert an activity' , async ( ) => {
4369 methods . maybeOne . mockResolvedValueOnce ( null ) ;
4470 methods . oneFirst . mockResolvedValueOnce ( 0 ) ;
@@ -55,11 +81,11 @@ describe('BasicSentinel -> reportActivity()', () => {
5581
5682 it ( 'should insert a blocked activity' , async ( ) => {
5783 // Mock the query method to return a blocked activity
58- methods . maybeOne . mockResolvedValueOnce ( { decisionExpiresAt : mockedBlockedTime } ) ;
84+ methods . maybeOne . mockResolvedValueOnce ( { decisionExpiresAt : mockedDefaultBlockedTime } ) ;
5985
6086 const activity = createMockActivityReport ( ) ;
6187 const decision = await sentinel . reportActivity ( activity ) ;
62- expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedBlockedTime ] ) ;
88+ expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedDefaultBlockedTime ] ) ;
6389 expect ( methods . query ) . toHaveBeenCalledTimes ( 1 ) ;
6490 expect ( methods . query ) . toHaveBeenCalledWith (
6591 expectSqlString ( 'insert into "sentinel_activities"' )
@@ -68,6 +94,10 @@ describe('BasicSentinel -> reportActivity()', () => {
6894} ) ;
6995
7096describe ( 'BasicSentinel -> decide()' , ( ) => {
97+ beforeEach ( ( ) => {
98+ findDefaultSignInExperienceMock . mockResolvedValue ( mockSignInExperience ) ;
99+ } ) ;
100+
71101 it ( 'should return existing blocked time if the activity is blocked' , async ( ) => {
72102 const existingBlockedTime = addMinutes ( mockedTime , 5 ) . valueOf ( ) ;
73103 methods . maybeOne . mockResolvedValueOnce ( { decisionExpiresAt : existingBlockedTime } ) ;
@@ -92,7 +122,7 @@ describe('BasicSentinel -> decide()', () => {
92122
93123 const activity = createMockActivityReport ( ) ;
94124 const decision = await sentinel . decide ( activity ) ;
95- expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedBlockedTime ] ) ;
125+ expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedDefaultBlockedTime ] ) ;
96126 } ) ;
97127
98128 it ( 'should return blocked if the activity is not blocked and there are 4 failed attempts and the current activity is failed' , async ( ) => {
@@ -103,6 +133,83 @@ describe('BasicSentinel -> decide()', () => {
103133 // eslint-disable-next-line @silverhand/fp/no-mutation
104134 activity . actionResult = SentinelActionResult . Failed ;
105135 const decision = await sentinel . decide ( activity ) ;
106- expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedBlockedTime ] ) ;
136+ expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedDefaultBlockedTime ] ) ;
137+ } ) ;
138+ } ) ;
139+
140+ describe ( 'BasicSentinel with custom policy' , ( ) => {
141+ // eslint-disable-next-line @silverhand/fp/no-let
142+ let stub : Sinon . SinonStub ;
143+
144+ // TODO: Remove this when the sentinel policy is fully integrated into the system.
145+ beforeAll ( ( ) => {
146+ // eslint-disable-next-line @silverhand/fp/no-mutation
147+ stub = Sinon . stub ( EnvSet , 'values' ) . value ( {
148+ ...EnvSet . values ,
149+ isProduction : true ,
150+ isDevFeaturesEnabled : true ,
151+ } ) ;
152+ } ) ;
153+
154+ afterAll ( ( ) => {
155+ stub . restore ( ) ;
156+ } ) ;
157+
158+ beforeEach ( ( ) => {
159+ findDefaultSignInExperienceMock . mockResolvedValue ( {
160+ ...mockSignInExperience ,
161+ sentinelPolicy : customSentinelPolicy ,
162+ } ) ;
163+ } ) ;
164+
165+ it ( 'should insert a blocked activity' , async ( ) => {
166+ // Mock the query method to return a blocked activity
167+ methods . maybeOne . mockResolvedValueOnce ( { decisionExpiresAt : mockedCustomBlockedTime } ) ;
168+
169+ const activity = createMockActivityReport ( ) ;
170+ const decision = await sentinel . reportActivity ( activity ) ;
171+ expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedCustomBlockedTime ] ) ;
172+ expect ( methods . query ) . toHaveBeenCalledTimes ( 1 ) ;
173+ expect ( methods . query ) . toHaveBeenCalledWith (
174+ expectSqlString ( 'insert into "sentinel_activities"' )
175+ ) ;
176+ } ) ;
177+
178+ it ( 'should return existing blocked time if the activity is blocked' , async ( ) => {
179+ const existingBlockedTime = addMinutes ( mockedTime , 5 ) . valueOf ( ) ;
180+ methods . maybeOne . mockResolvedValueOnce ( { decisionExpiresAt : existingBlockedTime } ) ;
181+
182+ const activity = createMockActivityReport ( ) ;
183+ const decision = await sentinel . decide ( activity ) ;
184+ expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , existingBlockedTime ] ) ;
185+ } ) ;
186+
187+ it ( 'should return allowed if the activity is not blocked and there are less than 7 failed attempts' , async ( ) => {
188+ methods . maybeOne . mockResolvedValueOnce ( null ) ;
189+ methods . oneFirst . mockResolvedValueOnce ( 6 ) ;
190+
191+ const activity = createMockActivityReport ( ) ;
192+ const decision = await sentinel . decide ( activity ) ;
193+ expect ( decision ) . toEqual ( [ SentinelDecision . Allowed , mockedTime ] ) ;
194+ } ) ;
195+
196+ it ( 'should return blocked if the activity is not blocked and there are 7 failed attempts' , async ( ) => {
197+ methods . maybeOne . mockResolvedValueOnce ( null ) ;
198+ methods . oneFirst . mockResolvedValueOnce ( 7 ) ;
199+
200+ const activity = createMockActivityReport ( ) ;
201+ const decision = await sentinel . decide ( activity ) ;
202+ expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedCustomBlockedTime ] ) ;
203+ } ) ;
204+
205+ it ( 'should return blocked if the activity is not blocked and there are 4 failed attempts and the current activity is failed' , async ( ) => {
206+ methods . maybeOne . mockResolvedValueOnce ( null ) ;
207+ methods . oneFirst . mockResolvedValueOnce ( 6 ) ;
208+
209+ const activity = createMockActivityReport ( ) ;
210+ // eslint-disable-next-line @silverhand/fp/no-mutation
211+ activity . actionResult = SentinelActionResult . Failed ;
212+ const decision = await sentinel . decide ( activity ) ;
213+ expect ( decision ) . toEqual ( [ SentinelDecision . Blocked , mockedCustomBlockedTime ] ) ;
107214 } ) ;
108215} ) ;
0 commit comments