1- import { APIGatewayRequestAuthorizerEvent , Callback , Context } from 'aws-lambda' ;
2- import { handler } from '../index' ;
1+ import { APIGatewayEventClientCertificate , APIGatewayRequestAuthorizerEvent , Callback , Context } from 'aws-lambda' ;
2+ import { Deps } from '../deps' ;
3+ import pino from 'pino' ;
4+ import { EnvVars } from '../env' ;
5+ import { createAuthorizerHandler } from '../authorizer' ;
6+
7+ const mockedDeps : jest . Mocked < Deps > = {
8+ logger : { info : jest . fn ( ) , error : jest . fn ( ) } as unknown as pino . Logger ,
9+ env : {
10+ CLOUDWATCH_NAMESPACE : 'cloudwatch-namespace' ,
11+ CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS : 14
12+ } as unknown as EnvVars ,
13+ cloudWatchClient : {
14+ send : jest . fn ( ) . mockResolvedValue ( { } ) ,
15+ } as any ,
16+ } as Deps ;
17+
318
419describe ( 'Authorizer Lambda Function' , ( ) => {
520 let mockEvent : APIGatewayRequestAuthorizerEvent ;
@@ -11,17 +26,20 @@ describe('Authorizer Lambda Function', () => {
1126 type : 'REQUEST' ,
1227 methodArn : 'arn:aws:execute-api:region:account-id:api-id/stage/GET/resource' ,
1328 headers : { } ,
14- pathParameters : { }
29+ pathParameters : { } ,
30+ requestContext : { identity : { clientCert : null } } ,
1531 } as APIGatewayRequestAuthorizerEvent ;
1632
1733 mockContext = { } as Context ;
1834 mockCallback = jest . fn ( ) ;
1935 } ) ;
2036
21- it ( 'Should allow access when headers match' , ( ) => {
37+ it ( 'Should allow access when headers match' , async ( ) => {
2238 mockEvent . headers = { headerauth1 : 'headervalue1' } ;
2339
40+ const handler = createAuthorizerHandler ( mockedDeps ) ;
2441 handler ( mockEvent , mockContext , mockCallback ) ;
42+ await new Promise ( process . nextTick ) ;
2543
2644 expect ( mockCallback ) . toHaveBeenCalledWith ( null , expect . objectContaining ( {
2745 policyDocument : expect . objectContaining ( {
@@ -34,10 +52,12 @@ describe('Authorizer Lambda Function', () => {
3452 } ) ) ;
3553 } ) ;
3654
37- it ( 'Should deny access when headers do not match' , ( ) => {
55+ it ( 'Should deny access when headers do not match' , async ( ) => {
3856 mockEvent . headers = { headerauth1 : 'wrongValue' } ;
3957
58+ const handler = createAuthorizerHandler ( mockedDeps ) ;
4059 handler ( mockEvent , mockContext , mockCallback ) ;
60+ await new Promise ( process . nextTick ) ;
4161
4262 expect ( mockCallback ) . toHaveBeenCalledWith ( null , expect . objectContaining ( {
4363 policyDocument : expect . objectContaining ( {
@@ -50,10 +70,12 @@ describe('Authorizer Lambda Function', () => {
5070 } ) ) ;
5171 } ) ;
5272
53- it ( 'Should handle null headers gracefully' , ( ) => {
73+ it ( 'Should handle null headers gracefully' , async ( ) => {
5474 mockEvent . headers = null ;
5575
76+ const handler = createAuthorizerHandler ( mockedDeps ) ;
5677 handler ( mockEvent , mockContext , mockCallback ) ;
78+ await new Promise ( process . nextTick ) ;
5779
5880 expect ( mockCallback ) . toHaveBeenCalledWith ( null , expect . objectContaining ( {
5981 policyDocument : expect . objectContaining ( {
@@ -66,10 +88,12 @@ describe('Authorizer Lambda Function', () => {
6688 } ) ) ;
6789 } ) ;
6890
69- it ( 'Should handle defined headers correctly' , ( ) => {
91+ it ( 'Should handle defined headers correctly' , async ( ) => {
7092 mockEvent . headers = { headerauth1 : 'headervalue1' } ;
7193
94+ const handler = createAuthorizerHandler ( mockedDeps ) ;
7295 handler ( mockEvent , mockContext , mockCallback ) ;
96+ await new Promise ( process . nextTick ) ;
7397
7498 expect ( mockCallback ) . toHaveBeenCalledWith ( null , expect . objectContaining ( {
7599 policyDocument : expect . objectContaining ( {
@@ -82,14 +106,16 @@ describe('Authorizer Lambda Function', () => {
82106 } ) ) ;
83107 } ) ;
84108
85- it ( 'Should handle additional headers correctly' , ( ) => {
109+ it ( 'Should handle additional headers correctly' , async ( ) => {
86110 mockEvent . headers = {
87111 headerauth1 : 'headervalue1' ,
88112 otherheader1 : 'headervalue2' ,
89113 otherheader2 : 'headervalue3'
90114 } ;
91115
116+ const handler = createAuthorizerHandler ( mockedDeps ) ;
92117 handler ( mockEvent , mockContext , mockCallback ) ;
118+ await new Promise ( process . nextTick ) ;
93119
94120 expect ( mockCallback ) . toHaveBeenCalledWith ( null , expect . objectContaining ( {
95121 policyDocument : expect . objectContaining ( {
@@ -101,4 +127,74 @@ describe('Authorizer Lambda Function', () => {
101127 } ) ,
102128 } ) ) ;
103129 } ) ;
130+
131+ describe ( 'Certificate expiry check' , ( ) => {
132+
133+ beforeEach ( ( ) => {
134+ jest . useFakeTimers ( { doNotFake : [ 'nextTick' ] } )
135+ . setSystemTime ( new Date ( '2025-11-03T14:19:00Z' ) ) ;
136+ } ) ;
137+
138+ afterEach ( ( ) => {
139+ jest . useRealTimers ( ) ;
140+ } )
141+
142+ it ( 'Should not send CloudWatch metric when certificate is null' , async ( ) => {
143+ mockEvent . headers = { headerauth1 : 'headervalue1' } ;
144+ mockEvent . requestContext . identity . clientCert = null ;
145+
146+ const handler = createAuthorizerHandler ( mockedDeps ) ;
147+ handler ( mockEvent , mockContext , mockCallback ) ;
148+ await new Promise ( process . nextTick ) ;
149+
150+ expect ( mockedDeps . cloudWatchClient . send ) . not . toHaveBeenCalled ( ) ;
151+ } ) ;
152+
153+ it ( 'Should send CloudWatch metric when the certificate expiry threshold is reached' , async ( ) => {
154+ mockEvent . headers = { headerauth1 : 'headervalue1' } ;
155+ mockEvent . requestContext . identity . clientCert = buildCertWithExpiry ( '2025-11-17T14:19:00Z' ) ;
156+
157+ const handler = createAuthorizerHandler ( mockedDeps ) ;
158+ handler ( mockEvent , mockContext , mockCallback ) ;
159+ await new Promise ( process . nextTick ) ;
160+
161+ expect ( mockedDeps . cloudWatchClient . send ) . toHaveBeenCalledWith (
162+ expect . objectContaining ( {
163+ input : {
164+ Namespace : 'cloudwatch-namespace' ,
165+ MetricData : [
166+ {
167+ MetricName : 'apim-client-certificate-near-expiry' ,
168+ Dimensions : [
169+ { Name : 'SUBJECT_DN' , Value : 'CN=test-subject' } ,
170+ { Name : 'NOT_AFTER' , Value : '2025-11-17T14:19:00Z' } ,
171+ ] ,
172+ } ,
173+ ] ,
174+ } ,
175+ } )
176+ ) ;
177+ } ) ;
178+
179+ it ( 'Should not send CloudWatch metric when the certificate expiry threshold is not yet reached' , async ( ) => {
180+ mockEvent . headers = { headerauth1 : 'headervalue1' } ;
181+ mockEvent . requestContext . identity . clientCert = buildCertWithExpiry ( '2025-11-18T14:19:00Z' ) ;
182+
183+ const handler = createAuthorizerHandler ( mockedDeps ) ;
184+ handler ( mockEvent , mockContext , mockCallback ) ;
185+ await new Promise ( process . nextTick ) ;
186+
187+ expect ( mockedDeps . cloudWatchClient . send ) . not . toHaveBeenCalled ( ) ;
188+ } ) ;
189+ } ) ;
190+
191+ function buildCertWithExpiry ( expiry : string ) : APIGatewayEventClientCertificate {
192+
193+ return {
194+ subjectDN : 'CN=test-subject' ,
195+ validity : {
196+ notAfter : expiry ,
197+ } as APIGatewayEventClientCertificate [ 'validity' ] ,
198+ } as APIGatewayEventClientCertificate ;
199+ }
104200} ) ;
0 commit comments