Skip to content

Commit 7250e06

Browse files
committed
test: add token validation tests
1 parent dc4b92c commit 7250e06

1 file changed

Lines changed: 79 additions & 1 deletion

File tree

test/services/validation.service.test.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test, vi } from 'vitest';
2-
import { auth } from '@internxt/lib';
2+
import { auth, TokenStatus } from '@internxt/lib';
33
import { randomInt, randomUUID } from 'node:crypto';
44
import { UserFixture } from '../fixtures/auth.fixture';
55
import { ValidationService } from '../../src/services/validation.service';
@@ -69,4 +69,82 @@ describe('Validation Service', () => {
6969
expect(ValidationService.instance.validateStringIsNotEmpty('\t')).to.be.equal(false);
7070
expect(ValidationService.instance.validateStringIsNotEmpty('\t\n')).to.be.equal(false);
7171
});
72+
73+
describe('validateTokenAndCheckExpiration', () => {
74+
const nowInSeconds = () => Math.floor(Date.now() / 1000);
75+
const ONE_HOUR_IN_SECONDS = 60 * 60;
76+
// The library decodes the payload with Buffer.from(..., 'base64') which
77+
// handles both standard and URL-safe base64 after character substitution.
78+
const encodeSegment = (payload: object): string => Buffer.from(JSON.stringify(payload)).toString('base64');
79+
const createToken = (payload: object): string =>
80+
[encodeSegment({ alg: 'RS256', typ: 'JWT' }), encodeSegment(payload), 'signature'].join('.');
81+
82+
test('when a token expires far in the future and most of its lifetime remains, then it is VALID', () => {
83+
const token = createToken({ exp: nowInSeconds() + 24 * ONE_HOUR_IN_SECONDS, iat: nowInSeconds() });
84+
expect(ValidationService.instance.validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.VALID);
85+
});
86+
87+
test('when more than half of the token lifetime has elapsed, then it is REFRESH_REQUIRED', () => {
88+
const token = createToken({
89+
exp: nowInSeconds() + ONE_HOUR_IN_SECONDS,
90+
iat: nowInSeconds() - 2 * ONE_HOUR_IN_SECONDS,
91+
});
92+
expect(ValidationService.instance.validateTokenAndCheckExpiration(token)).to.be.equal(
93+
TokenStatus.REFRESH_REQUIRED,
94+
);
95+
});
96+
97+
test('when the expiration timestamp is in the past, then it is EXPIRED', () => {
98+
const token = createToken({
99+
exp: nowInSeconds() - ONE_HOUR_IN_SECONDS,
100+
iat: nowInSeconds() - 3 * ONE_HOUR_IN_SECONDS,
101+
});
102+
expect(ValidationService.instance.validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.EXPIRED);
103+
});
104+
105+
test('when the token has no iat claim, then a fixed six-hour refresh margin applies', () => {
106+
const farFromExpiring = createToken({ exp: nowInSeconds() + 7 * ONE_HOUR_IN_SECONDS });
107+
expect(ValidationService.instance.validateTokenAndCheckExpiration(farFromExpiring)).to.be.equal(
108+
TokenStatus.VALID,
109+
);
110+
111+
const closeToExpiring = createToken({ exp: nowInSeconds() + 5 * ONE_HOUR_IN_SECONDS });
112+
expect(ValidationService.instance.validateTokenAndCheckExpiration(closeToExpiring)).to.be.equal(
113+
TokenStatus.REFRESH_REQUIRED,
114+
);
115+
});
116+
117+
test('when a token is malformed or unparseable, then it is INVALID', () => {
118+
expect(ValidationService.instance.validateTokenAndCheckExpiration('')).to.be.equal(TokenStatus.INVALID);
119+
expect(ValidationService.instance.validateTokenAndCheckExpiration('not-a-token')).to.be.equal(
120+
TokenStatus.INVALID,
121+
);
122+
expect(ValidationService.instance.validateTokenAndCheckExpiration('only.twosegments')).to.be.equal(
123+
TokenStatus.INVALID,
124+
);
125+
expect(
126+
ValidationService.instance.validateTokenAndCheckExpiration('header.!!!not-base64!!!.signature'),
127+
).to.be.equal(TokenStatus.INVALID);
128+
const notJsonPayload = ['header', Buffer.from('plain text').toString('base64'), 'signature'].join('.');
129+
expect(ValidationService.instance.validateTokenAndCheckExpiration(notJsonPayload)).to.be.equal(
130+
TokenStatus.INVALID,
131+
);
132+
});
133+
134+
test('when the payload has no numeric exp claim, then it is INVALID', () => {
135+
const missingExp = createToken({ iat: nowInSeconds() });
136+
expect(ValidationService.instance.validateTokenAndCheckExpiration(missingExp)).to.be.equal(TokenStatus.INVALID);
137+
138+
const nonNumericExp = createToken({ exp: 'tomorrow', iat: nowInSeconds() });
139+
expect(ValidationService.instance.validateTokenAndCheckExpiration(nonNumericExp)).to.be.equal(
140+
TokenStatus.INVALID,
141+
);
142+
});
143+
144+
test('when the payload encoding contains base64url-specific characters, then it is correctly decoded', () => {
145+
const base64UrlToken =
146+
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQxMDI0NDQ4MDAsImlhdCI6MTU3NzgzNjgwMCwic3ViIjoiw7_DvsO9In0.signature';
147+
expect(ValidationService.instance.validateTokenAndCheckExpiration(base64UrlToken)).to.be.equal(TokenStatus.VALID);
148+
});
149+
});
72150
});

0 commit comments

Comments
 (0)