Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@
export default {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' },
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'^../src$': '<rootDir>/src/index.ts',
'^../src/(.*)$': '<rootDir>/src/$1'
},
extensionsToTreatAsEsm: ['.ts'],
transform: { '^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tsconfig.esm.json' }] },
transformIgnorePatterns: [
'node_modules/(?!(jose)/)'
],
setupFiles: ['./tests/setupJest.js'],
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.ts',
'!src/generated/**/*.ts', // Exclude auto-generated API files
'!src/**/*.d.ts', // Exclude TypeScript declaration files
],
coverageReporters: ['json', 'json-summary', 'lcov', 'text', 'clover'],
coverageDirectory: './coverage',
};
8 changes: 6 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import Assert from './helpers/assert.js';
import { BaseError } from './errors/index.js';

/* eslint-disable class-methods-use-this */
export interface ConfigInterface {
Expand Down Expand Up @@ -62,13 +63,16 @@ class Config implements ConfigInterface {

private validateProjectID(projectID: string): void {
if (!projectID || !projectID.startsWith('pro-')) {
throw new Error('ProjectID must not be empty and must start with "pro-".');
const description = 'ProjectID must not be empty and must start with "pro-".';
throw new BaseError(description, 400, description, true);
}
}

private validateAPISecret(apiSecret: string): void {
if (!apiSecret || !apiSecret.startsWith('corbado1_')) {
throw new Error('APISecret must not be empty and must start with "corbado1_".');
const description = 'APISecret must not be empty and must start with "corbado1_".';

throw new BaseError(description, 400, description, true);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/errors/baseError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ class BaseError extends Error {

isOperational: boolean;

get errorCode(): number {
return this.statusCode;
}

get isRetryable(): boolean {
return this.isOperational;
}

constructor(name: string, statusCode: number, description: string, isOperational: boolean = false) {
super(description);

Expand Down
20 changes: 20 additions & 0 deletions src/helpers/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ class Assert {
});
}

public static isString(data: unknown, errorName: string): void {
validate(
typeof data !== 'string',
errorName,
INVALID_DATA.code,
`${errorName} must be a string`,
INVALID_DATA.isOperational,
);
}

public static isNotEmpty(data: unknown, errorName: string): void {
validate(
data === null || data === undefined || data === '',
errorName,
EMPTY_STRING.code,
`${errorName} must not be empty`,
EMPTY_STRING.isOperational,
);
}

public static validURL(url: string, errorName: string): void {
validate(!url, errorName, INVALID_URL.code, 'parse_url() returned error', INVALID_URL.isOperational);

Expand Down
8 changes: 6 additions & 2 deletions src/services/sessionService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable class-methods-use-this */
import { createRemoteJWKSet, errors, JWTPayload, jwtVerify } from 'jose';
import { JOSEAlgNotAllowed } from 'jose/dist/types/util/errors';
import { Assert } from '../helpers/index.js';
import ValidationError, { ValidationErrorNames } from '../errors/validationError.js';
import {JOSEAlgNotAllowed} from "jose/dist/types/util/errors";

export interface SessionInterface {
validateToken(sessionToken: string): Promise<{ userId: string; fullName: string }>;
Expand Down Expand Up @@ -73,7 +73,11 @@ class Session implements SessionInterface {
throw new ValidationError(ValidationErrorNames.JWTExpired);
}

if (error instanceof errors.JWTInvalid || error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JOSENotSupported) {
if (
error instanceof errors.JWTInvalid ||
error instanceof errors.JWSSignatureVerificationFailed ||
error instanceof errors.JOSENotSupported
) {
throw new ValidationError(ValidationErrorNames.JWTInvalid);
}

Expand Down
42 changes: 41 additions & 1 deletion tests/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {DefaultCacheMaxAge, DefaultSessionTokenCookieName} from '../src/config.js';
import { DefaultCacheMaxAge, DefaultSessionTokenCookieName } from '../src/config.js';
import { BaseError } from '../src/errors/index.js';
import { Config } from '../src/index.js';

Expand Down Expand Up @@ -64,6 +64,46 @@ describe('Configuration class', () => {
expect(() => new Config(projectID, apiSecret, `${frontendAPI}/v2`, backendAPI)).toThrow('path needs to be empty');
});

it('should set session token cookie name using setSessionTokenCookieName', () => {
const config = new Config(projectID, apiSecret, frontendAPI, backendAPI);
const customCookieName = 'custom_session_token';

config.setSessionTokenCookieName(customCookieName);

expect(config.SessionTokenCookieName).toBe(customCookieName);
});

it('should throw error when setting empty session token cookie name', () => {
const config = new Config(projectID, apiSecret, frontendAPI, backendAPI);

expect(() => config.setSessionTokenCookieName('')).toThrow();
});

it('should set session token cookie name using deprecated setShortSessionCookieName', () => {
const config = new Config(projectID, apiSecret, frontendAPI, backendAPI);
const customCookieName = 'custom_short_session';

config.setShortSessionCookieName(customCookieName);

expect(config.SessionTokenCookieName).toBe(customCookieName);
});

it('should throw error when setting empty short session cookie name', () => {
const config = new Config(projectID, apiSecret, frontendAPI, backendAPI);

expect(() => config.setShortSessionCookieName('')).toThrow();
});

it('should set custom HTTP client', () => {
const config = new Config(projectID, apiSecret, frontendAPI, backendAPI);
const customClient = require('axios').create({ timeout: 5000 });

config.setHttpClient(customClient);

expect(config.Client).toBe(customClient);
expect(config.Client.defaults.timeout).toBe(5000);
});

it('should throw an error when backendAPI is wrong', () => {
expect(() => new Config(projectID, apiSecret, frontendAPI, `${backendAPI}/v2`)).toThrow('path needs to be empty');
});
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/services/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('User Validation Tests', () => {
await sdk.users().get(Utils.testConstants.TEST_USER_ID);
} catch (error) {
expect(error).toBeInstanceOf(ServerError);
expect((error as ServerError).httpStatusCode).toEqual(400);
expect((error as ServerError).httpStatusCode).toEqual(401);
}
});

Expand Down
18 changes: 16 additions & 2 deletions tests/setupJest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config();
require('dotenv').config({ path: '.env.test' });

module.export = {};
// Set default test environment variables if not already set
if (!process.env.CORBADO_PROJECT_ID) {
process.env.CORBADO_PROJECT_ID = 'pro-test-123456789';
}
if (!process.env.CORBADO_API_SECRET) {
process.env.CORBADO_API_SECRET = 'corbado1_test_secret_key_123456789';
}
if (!process.env.CORBADO_FRONTEND_API) {
process.env.CORBADO_FRONTEND_API = 'https://pro-test-123456789.frontendapi.cloud.corbado.io';
}
if (!process.env.CORBADO_BACKEND_API) {
process.env.CORBADO_BACKEND_API = 'https://backendapi.cloud.corbado.io';
}

module.exports = {};
69 changes: 69 additions & 0 deletions tests/unit/assert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Assert from '../../src/helpers/assert.js';
import { BaseError } from '../../src/errors/index.js';

describe('Assert Helper', () => {
it('should pass for valid assertions', () => {
expect(() => Assert.isString('test', 'value')).not.toThrow();
expect(() => Assert.isNotEmpty('test', 'value')).not.toThrow();
});

it('should throw BaseError for invalid string', () => {
expect(() => Assert.isString(123, 'number')).toThrow(BaseError);
expect(() => Assert.isString(null, 'null')).toThrow(BaseError);
});

it('should throw BaseError for empty values', () => {
expect(() => Assert.isNotEmpty('', 'empty string')).toThrow(BaseError);
expect(() => Assert.isNotEmpty(null, 'null')).toThrow(BaseError);
expect(() => Assert.isNotEmpty(undefined, 'undefined')).toThrow(BaseError);
});

it('should validate notNull correctly', () => {
expect(() => Assert.notNull('valid', 'test')).not.toThrow();
expect(() => Assert.notNull(0, 'zero')).not.toThrow();
expect(() => Assert.notNull(false, 'false')).not.toThrow();

expect(() => Assert.notNull(null, 'null value')).toThrow(BaseError);
expect(() => Assert.notNull(undefined, 'undefined value')).toThrow(BaseError);
});

it('should validate notEmptyString correctly', () => {
expect(() => Assert.notEmptyString('valid', 'test')).not.toThrow();
expect(() => Assert.notEmptyString('a', 'single char')).not.toThrow();

expect(() => Assert.notEmptyString('', 'empty string')).toThrow(BaseError);
});

it('should validate stringInSet correctly', () => {
const validValues = ['apple', 'banana', 'cherry'];

expect(() => Assert.stringInSet('apple', validValues, 'fruit')).not.toThrow();
expect(() => Assert.stringInSet('banana', validValues, 'fruit')).not.toThrow();

expect(() => Assert.stringInSet('orange', validValues, 'invalid fruit')).toThrow(BaseError);
expect(() => Assert.stringInSet('', validValues, 'empty fruit')).toThrow(BaseError);
});

it('should validate keysInObject correctly', () => {
const testObj = { name: 'test', age: 25, active: true };

expect(() => Assert.keysInObject(['name'], testObj, 'test object')).not.toThrow();
expect(() => Assert.keysInObject(['name', 'age'], testObj, 'test object')).not.toThrow();
expect(() => Assert.keysInObject(['name', 'age', 'active'], testObj, 'test object')).not.toThrow();

expect(() => Assert.keysInObject(['missing'], testObj, 'test object')).toThrow(BaseError);
expect(() => Assert.keysInObject(['name', 'missing'], testObj, 'test object')).toThrow(BaseError);
});

it('should validate URL correctly', () => {
expect(() => Assert.validURL('https://example.com', 'valid URL')).not.toThrow();
expect(() => Assert.validURL('http://test.com', 'http URL')).not.toThrow();

expect(() => Assert.validURL('', 'empty URL')).toThrow(BaseError);
expect(() => Assert.validURL('invalid-url', 'malformed URL')).toThrow(BaseError);
expect(() => Assert.validURL('https://user:pass@example.com', 'URL with credentials')).toThrow(BaseError);
expect(() => Assert.validURL('https://example.com/path', 'URL with path')).toThrow(BaseError);
expect(() => Assert.validURL('https://example.com?query=1', 'URL with query')).toThrow(BaseError);
expect(() => Assert.validURL('https://example.com#fragment', 'URL with fragment')).toThrow(BaseError);
});
});
37 changes: 37 additions & 0 deletions tests/unit/config-edge-cases.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Config } from '../../src/index.js';
import { BaseError } from '../../src/errors/index.js';

describe('Config Edge Cases', () => {
it('should handle empty string parameters', () => {
expect(() => new Config('', '', '', '')).toThrow(BaseError);
});

it('should handle null parameters', () => {
expect(
() =>
new Config(
null as unknown as string,
null as unknown as string,
null as unknown as string,
null as unknown as string,
),
).toThrow(BaseError);
});

it('should handle undefined parameters', () => {
expect(
() =>
new Config(
undefined as unknown as string,
undefined as unknown as string,
undefined as unknown as string,
undefined as unknown as string,
),
).toThrow(BaseError);
});

it('should validate URL formats', () => {
expect(() => new Config('valid', 'secret', 'invalid-url', 'backend')).toThrow(BaseError);
expect(() => new Config('valid', 'secret', 'frontend', 'invalid-url')).toThrow(BaseError);
});
});
Loading