@j2blasco/ts-auth
Version:
TypeScript authentication abstraction library that eliminates vendor lock-in and provides mock-free testing for both frontend and backend authentication systems
299 lines (298 loc) • 13.2 kB
JavaScript
/**
* Helper functions for testing Result types
*/
function isResultSuccess(result) {
try {
result.unwrapOrThrow();
return true;
}
catch {
return false;
}
}
function getResultError(result) {
try {
result.unwrapOrThrow();
throw new Error('Result is not an error');
}
catch (error) {
return error;
}
}
/**
* Comprehensive test suite for IAuth implementations.
* Run this against any implementation to verify it satisfies the interface contract.
*/
export function testAuth(authFactory) {
describe('IAuth implementation tests', () => {
let auth;
beforeEach(async () => {
auth = authFactory();
// Ensure clean state before each test
try {
await auth.signOut();
}
catch {
// Ignore if already signed out
}
});
describe('authState$', () => {
it('should be an Observable', () => {
expect(auth.authState$).toBeDefined();
expect(typeof auth.authState$.subscribe).toBe('function');
});
it('should emit initial state', (done) => {
auth.authState$
.subscribe({
next: (state) => {
expect(state === null ||
state === undefined ||
(typeof state === 'object' && 'uid' in state)).toBe(true);
done();
},
})
.unsubscribe();
});
});
describe('signUp', () => {
it('should create a new user and return a UserId', async () => {
const email = `test-${Date.now()}@example.com`;
const password = 'testPassword123';
const userId = await auth.signUp(email, password);
expect(typeof userId).toBe('string');
expect(userId.length).toBeGreaterThan(0);
});
it('should reject duplicate email addresses', async () => {
const email = `duplicate-${Date.now()}@example.com`;
const password = 'testPassword123';
await auth.signUp(email, password);
await expect(auth.signUp(email, password)).rejects.toThrow();
});
});
describe('signInWithEmailAndPassword', () => {
const testEmail = `signin-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
let testUserId;
beforeEach(async () => {
testUserId = await auth.signUp(testEmail, testPassword);
});
it('should sign in with valid credentials', async () => {
const result = await auth.signInWithEmailAndPassword({
email: testEmail,
password: testPassword,
persistent: true,
});
expect(isResultSuccess(result)).toBe(true);
});
it('should update authState$ when signing in', async () => {
let authStateUpdated = false;
const subscription = auth.authState$.subscribe((state) => {
if (state && state.uid === testUserId) {
authStateUpdated = true;
}
});
await auth.signInWithEmailAndPassword({
email: testEmail,
password: testPassword,
persistent: true,
});
// Give some time for the observable to emit
await new Promise((resolve) => setTimeout(resolve, 10));
subscription.unsubscribe();
expect(authStateUpdated).toBe(true);
});
it('should return error for invalid email format', async () => {
const result = await auth.signInWithEmailAndPassword({
email: 'invalid-email',
password: testPassword,
persistent: true,
});
expect(isResultSuccess(result)).toBe(false);
const error = getResultError(result);
expect(error.code).toBe('invalid-email');
});
it('should return error for non-existent user', async () => {
const result = await auth.signInWithEmailAndPassword({
email: `nonexistent-${Date.now()}@example.com`,
password: testPassword,
persistent: true,
});
expect(isResultSuccess(result)).toBe(false);
const error = getResultError(result);
expect(error.code).toBe('user-not-found');
});
it('should return error for wrong password', async () => {
const result = await auth.signInWithEmailAndPassword({
email: testEmail,
password: 'wrongPassword',
persistent: true,
});
expect(isResultSuccess(result)).toBe(false);
const error = getResultError(result);
expect(error.code).toBe('wrong-password');
});
});
describe('getIdToken', () => {
const testEmail = `token-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
beforeEach(async () => {
await auth.signUp(testEmail, testPassword);
await auth.signInWithEmailAndPassword({
email: testEmail,
password: testPassword,
persistent: true,
});
});
it('should return a valid token when signed in', async () => {
const token = await auth.getIdToken();
expect(typeof token).toBe('string');
expect(token.length).toBeGreaterThan(0);
});
it('should throw error when not signed in', async () => {
await auth.signOut();
await expect(auth.getIdToken()).rejects.toThrow();
});
});
describe('signOut', () => {
const testEmail = `signout-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
beforeEach(async () => {
await auth.signUp(testEmail, testPassword);
await auth.signInWithEmailAndPassword({
email: testEmail,
password: testPassword,
persistent: true,
});
});
it('should sign out successfully', async () => {
await expect(auth.signOut()).resolves.not.toThrow();
});
it('should update authState$ to null when signing out', async () => {
let authStateUpdated = false;
const subscription = auth.authState$.subscribe((state) => {
if (state === null) {
authStateUpdated = true;
}
});
await auth.signOut();
// Give some time for the observable to emit
await new Promise((resolve) => setTimeout(resolve, 10));
subscription.unsubscribe();
expect(authStateUpdated).toBe(true);
});
});
describe('isEmailAvailable', () => {
const testEmail = `availability-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
it('should return true for available email', async () => {
const available = await auth.isEmailAvailable(`new-${Date.now()}@example.com`);
expect(available).toBe(true);
});
it('should return false for taken email', async () => {
await auth.signUp(testEmail, testPassword);
const available = await auth.isEmailAvailable(testEmail);
expect(available).toBe(false);
});
});
describe('changeEmail', () => {
const testEmail = `changeemail-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
const newEmail = `new-${Date.now()}@example.com`;
beforeEach(async () => {
await auth.signUp(testEmail, testPassword);
await auth.signInWithEmailAndPassword({
email: testEmail,
password: testPassword,
persistent: true,
});
});
it('should change email to available address', async () => {
const result = await auth.changeEmail(newEmail);
expect(isResultSuccess(result)).toBe(true);
});
it('should return error for unavailable email', async () => {
const takenEmail = `taken-${Date.now()}@example.com`;
await auth.signUp(takenEmail, 'anotherPassword');
const result = await auth.changeEmail(takenEmail);
expect(isResultSuccess(result)).toBe(false);
const error = getResultError(result);
expect(error.code).toBe('email-not-available');
});
});
describe('triggerResetPasswordFlow', () => {
const testEmail = `reset-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
beforeEach(async () => {
await auth.signUp(testEmail, testPassword);
});
it('should trigger reset flow for existing email', async () => {
const result = await auth.triggerResetPasswordFlow(testEmail);
expect(isResultSuccess(result)).toBe(true);
});
it('should return error for non-existent email', async () => {
const result = await auth.triggerResetPasswordFlow(`nonexistent-${Date.now()}@example.com`);
expect(isResultSuccess(result)).toBe(false);
const error = getResultError(result);
expect(error.code).toBe('email-not-in-database');
});
it('should rate limit repeated requests', async () => {
await auth.triggerResetPasswordFlow(testEmail);
const result = await auth.triggerResetPasswordFlow(testEmail);
expect(isResultSuccess(result)).toBe(false);
const error = getResultError(result);
expect(error.code).toBe('rate-limit-exceeded');
});
});
describe('requestChangePassword', () => {
const testEmail = `passwordchange-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
const newPassword = 'newPassword456';
beforeEach(async () => {
await auth.signUp(testEmail, testPassword);
});
it('should return error for invalid token', async () => {
const result = await auth.requestChangePassword({
passwordToken: 'invalid-token',
newPassword,
});
expect(isResultSuccess(result)).toBe(false);
const error = getResultError(result);
expect(error.code).toBe('token-not-found');
});
// Note: Testing valid token scenarios requires the specific implementation
// to expose token generation methods, which may not be available in all implementations
});
describe('deleteAccount', () => {
const testEmail = `delete-test-${Date.now()}@example.com`;
const testPassword = 'testPassword123';
beforeEach(async () => {
await auth.signUp(testEmail, testPassword);
await auth.signInWithEmailAndPassword({
email: testEmail,
password: testPassword,
persistent: true,
});
});
it('should delete account when signed in', async () => {
await expect(auth.deleteAccount()).resolves.not.toThrow();
});
it('should update authState$ to null after deletion', async () => {
let authStateUpdated = false;
const subscription = auth.authState$.subscribe((state) => {
if (state === null) {
authStateUpdated = true;
}
});
await auth.deleteAccount();
// Give some time for the observable to emit
await new Promise((resolve) => setTimeout(resolve, 10));
subscription.unsubscribe();
expect(authStateUpdated).toBe(true);
});
it('should throw error when not signed in', async () => {
await auth.signOut();
await expect(auth.deleteAccount()).rejects.toThrow();
});
});
});
}