onairos
Version:
The Onairos Library is a collection of functions that enable Applications to connect and communicate data with Onairos Identities via User Authorization. Integration for developers is seamless, simple and effective for all applications. LLM SDK capabiliti
793 lines (674 loc) • 24.9 kB
JavaScript
import request from 'supertest';
import express from 'express';
import jwt from 'jsonwebtoken';
import { jest } from '@jest/globals';
import linkedinRoutes from '../../routes/linkedin-enhanced.js';
// Mock dependencies
jest.mock('../../utils/tokenManager.js');
jest.mock('../../utils/connectionHealth.js');
jest.mock('../../utils/databaseUtils.js');
jest.mock('../../middleware/unifiedApiKeyAuth.js');
jest.mock('node-fetch');
const app = express();
app.use(express.json());
// Mock middleware
const mockSmartAuth = (req, res, next) => {
req.authMethod = 'api_key';
req.apiKeyInfo = {
keyId: 'test_key_id',
permissions: ['oauth:linkedin']
};
next();
};
const mockAuthenticateApiKey = (req, res, next) => {
req.apiKeyInfo = {
keyId: 'test_key_id',
permissions: ['oauth:linkedin']
};
next();
};
// Mock auth middleware
jest.mock('../../middleware/unifiedApiKeyAuth.js', () => ({
smartAuth: mockSmartAuth,
authenticateApiKey: mockAuthenticateApiKey
}));
app.use('/linkedin', linkedinRoutes);
describe('LinkedIn Enhanced Routes', () => {
let mockDbUtils;
let mockHealthMonitor;
let mockTokenManager;
let mockFetch;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Mock database utils
mockDbUtils = {
getUserWithSmartLookup: jest.fn(),
updateUserConnection: jest.fn(),
getUserConnections: jest.fn()
};
// Mock health monitor
mockHealthMonitor = {
checkConnectionHealth: jest.fn()
};
// Mock token manager
mockTokenManager = {
refreshAccessToken: jest.fn(),
refreshTokenIfNeeded: jest.fn()
};
// Mock fetch
mockFetch = jest.fn();
// Apply mocks
const { DatabaseUtils } = await import('../../utils/databaseUtils.js');
const { ConnectionHealthMonitor } = await import('../../utils/connectionHealth.js');
const { TokenManager } = await import('../../utils/tokenManager.js');
const fetch = await import('node-fetch');
DatabaseUtils.prototype.getUserWithSmartLookup = mockDbUtils.getUserWithSmartLookup;
DatabaseUtils.prototype.updateUserConnection = mockDbUtils.updateUserConnection;
DatabaseUtils.prototype.getUserConnections = mockDbUtils.getUserConnections;
ConnectionHealthMonitor.prototype.checkConnectionHealth = mockHealthMonitor.checkConnectionHealth;
TokenManager.prototype.refreshAccessToken = mockTokenManager.refreshAccessToken;
TokenManager.prototype.refreshTokenIfNeeded = mockTokenManager.refreshTokenIfNeeded;
fetch.default = mockFetch;
});
describe('POST /linkedin/native-auth', () => {
it('should successfully authenticate LinkedIn connection', async () => {
// Mock successful user lookup
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: {
_id: 'user_id',
userName: 'test_user',
email: 'test@example.com'
},
userType: 'onairos'
});
// Mock successful LinkedIn API response
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
id: 'linkedin_profile_id',
localizedFirstName: 'John',
localizedLastName: 'Doe'
})
});
// Mock successful database update
mockDbUtils.updateUserConnection.mockResolvedValue(true);
// Mock healthy connection
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'healthy',
message: 'Connection is healthy',
lastChecked: new Date()
});
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_linkedin_token',
refreshToken: 'valid_refresh_token',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
}
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.userType).toBe('onairos');
expect(response.body.connectionData.platform).toBe('linkedin');
expect(response.body.connectionData.userName).toBe('John Doe');
expect(response.body.connectionHealth.status).toBe('healthy');
});
it('should return error for missing access token', async () => {
const response = await request(app)
.post('/linkedin/native-auth')
.send({
userAccountInfo: {
username: 'test_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Missing access token');
expect(response.body.code).toBe('MISSING_ACCESS_TOKEN');
});
it('should return error for missing user account info', async () => {
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_linkedin_token'
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Missing user account information');
expect(response.body.code).toBe('MISSING_USER_INFO');
});
it('should return error for user not found', async () => {
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: null,
userType: null
});
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_linkedin_token',
userAccountInfo: {
username: 'nonexistent_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('User not found');
expect(response.body.code).toBe('USER_NOT_FOUND');
});
it('should return error for invalid LinkedIn token', async () => {
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: {
_id: 'user_id',
userName: 'test_user',
email: 'test@example.com'
},
userType: 'onairos'
});
// Mock LinkedIn API error response
mockFetch.mockResolvedValue({
ok: false,
status: 401,
text: async () => 'Invalid token'
});
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'invalid_linkedin_token',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid LinkedIn access token');
expect(response.body.code).toBe('INVALID_ACCESS_TOKEN');
});
it('should handle LinkedIn profile with only ID', async () => {
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: {
_id: 'user_id',
userName: 'test_user',
email: 'test@example.com'
},
userType: 'onairos'
});
// Mock LinkedIn API response with only ID
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
id: 'linkedin_profile_id_only'
})
});
mockDbUtils.updateUserConnection.mockResolvedValue(true);
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'healthy',
message: 'Connection is healthy'
});
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_linkedin_token',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.connectionData.userName).toBe('linkedin_profile_id_only');
expect(response.body.connectionData.profileId).toBe('linkedin_profile_id_only');
});
it('should handle refresh token detection from multiple sources', async () => {
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: {
_id: 'user_id',
userName: 'test_user',
email: 'test@example.com'
},
userType: 'onairos'
});
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
id: 'linkedin_profile_id',
localizedFirstName: 'John',
localizedLastName: 'Doe'
})
});
mockDbUtils.updateUserConnection.mockResolvedValue(true);
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'healthy',
message: 'Connection is healthy'
});
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_linkedin_token',
refresh_token: 'refresh_from_body',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com',
refreshToken: 'refresh_from_userinfo'
}
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.connectionData.hasRefreshToken).toBe(true);
});
it('should warn about missing refresh token', async () => {
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: {
_id: 'user_id',
userName: 'test_user',
email: 'test@example.com'
},
userType: 'onairos'
});
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
id: 'linkedin_profile_id',
localizedFirstName: 'John',
localizedLastName: 'Doe'
})
});
mockDbUtils.updateUserConnection.mockResolvedValue(true);
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'healthy',
message: 'Connection is healthy'
});
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_linkedin_token',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.connectionData.hasRefreshToken).toBe(false);
expect(response.body.recommendations).toContainEqual(
expect.objectContaining({
type: 'refresh_token_warning'
})
);
});
});
describe('GET /linkedin/connection-status/:username', () => {
it('should return connection status for valid user', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'healthy',
message: 'Connection is healthy',
lastChecked: new Date(),
needsReauth: false
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: {
accessToken: 'token',
userName: 'John Doe',
profileId: 'linkedin_profile_id',
connectedAt: new Date(),
hasRefreshToken: true
}
});
const response = await request(app)
.get('/linkedin/connection-status/test_user');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.platform).toBe('linkedin');
expect(response.body.username).toBe('test_user');
expect(response.body.connectionHealth.status).toBe('healthy');
expect(response.body.connectionDetails.connected).toBe(true);
expect(response.body.connectionDetails.userName).toBe('John Doe');
});
it('should return error for non-existent user', async () => {
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: null,
userType: null
});
const response = await request(app)
.get('/linkedin/connection-status/nonexistent_user');
expect(response.status).toBe(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('User not found');
expect(response.body.code).toBe('USER_NOT_FOUND');
});
it('should include recommendations for re-authentication', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'expired_no_refresh',
message: 'Token expired and no refresh token available',
needsReauth: true
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: {
accessToken: 'expired_token',
userName: 'John Doe',
hasRefreshToken: false
}
});
const response = await request(app)
.get('/linkedin/connection-status/test_user');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.recommendations).toContainEqual(
expect.objectContaining({
type: 'reauth_required',
actionRequired: true
})
);
});
});
describe('POST /linkedin/refresh-token', () => {
it('should successfully refresh token', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: {
accessToken: 'old_token',
refreshToken: 'valid_refresh_token'
}
});
mockTokenManager.refreshAccessToken.mockResolvedValue({
access_token: 'new_access_token'
});
mockDbUtils.updateUserConnection.mockResolvedValue(true);
const response = await request(app)
.post('/linkedin/refresh-token')
.send({
username: 'test_user'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toBe('LinkedIn token refreshed successfully');
expect(response.body.refreshedAt).toBeDefined();
expect(response.body.newTokenExpiry).toBeDefined();
});
it('should return error for missing username', async () => {
const response = await request(app)
.post('/linkedin/refresh-token')
.send({});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Username is required');
expect(response.body.code).toBe('MISSING_USERNAME');
});
it('should return error for connection not found', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: null
});
const response = await request(app)
.post('/linkedin/refresh-token')
.send({
username: 'test_user'
});
expect(response.status).toBe(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('LinkedIn connection not found');
expect(response.body.code).toBe('CONNECTION_NOT_FOUND');
});
it('should return error for no refresh token', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: {
accessToken: 'token',
refreshToken: null
}
});
const response = await request(app)
.post('/linkedin/refresh-token')
.send({
username: 'test_user'
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('No refresh token available');
expect(response.body.code).toBe('NO_REFRESH_TOKEN');
expect(response.body.guidance).toBe('User needs to reconnect their LinkedIn account to get a new refresh token');
});
});
describe('POST /linkedin/validate-connection/:username', () => {
it('should validate healthy connection', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: {
accessToken: 'valid_token',
refreshToken: 'valid_refresh_token',
userName: 'John Doe',
profileId: 'linkedin_profile_id'
}
});
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'healthy',
message: 'Connection is healthy'
});
// Mock successful LinkedIn API call
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
id: 'linkedin_profile_id',
localizedFirstName: 'John',
localizedLastName: 'Doe'
})
});
const response = await request(app)
.post('/linkedin/validate-connection/test_user');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.platform).toBe('linkedin');
expect(response.body.tokenValidation.isValid).toBe(true);
expect(response.body.tokenValidation.canRefresh).toBe(true);
expect(response.body.recommendations).toContainEqual(
expect.objectContaining({
type: 'connection_healthy'
})
);
});
it('should detect expired token that needs reconnection', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: {
accessToken: 'expired_token',
refreshToken: null,
userName: 'John Doe'
}
});
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'expired_no_refresh',
message: 'Token expired and no refresh token available'
});
// Mock expired token response
mockFetch.mockResolvedValue({
ok: false,
status: 401,
text: async () => 'Token expired'
});
const response = await request(app)
.post('/linkedin/validate-connection/test_user');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.tokenValidation.isValid).toBe(false);
expect(response.body.tokenValidation.isExpired).toBe(true);
expect(response.body.tokenValidation.canRefresh).toBe(false);
expect(response.body.recommendations).toContainEqual(
expect.objectContaining({
type: 'reconnection_required'
})
);
});
it('should handle no connection found', async () => {
const mockUser = {
_id: 'user_id',
userName: 'test_user'
};
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: mockUser,
userType: 'onairos'
});
mockDbUtils.getUserConnections.mockReturnValue({
linkedin: null
});
mockHealthMonitor.checkConnectionHealth.mockResolvedValue({
status: 'not_connected',
message: 'No connection found'
});
const response = await request(app)
.post('/linkedin/validate-connection/test_user');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.connectionDetails.connected).toBe(false);
expect(response.body.recommendations).toContainEqual(
expect.objectContaining({
type: 'no_connection'
})
);
});
});
describe('POST /linkedin/test-auth', () => {
it('should return successful authentication test', async () => {
const response = await request(app)
.post('/linkedin/test-auth');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toBe('LinkedIn authentication test passed');
expect(response.body.authMethod).toBe('api_key');
expect(response.body.configuration.platform).toBe('linkedin');
expect(response.body.configuration.enabled).toBeDefined();
});
it('should include API key information in test response', async () => {
const response = await request(app)
.post('/linkedin/test-auth');
expect(response.status).toBe(200);
expect(response.body.apiKeyInfo).toEqual({
keyId: 'test_key_id',
permissions: ['oauth:linkedin']
});
});
});
describe('Error handling', () => {
it('should handle unexpected errors gracefully', async () => {
mockDbUtils.getUserWithSmartLookup.mockRejectedValue(
new Error('Database connection failed')
);
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_token',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Internal server error during LinkedIn authentication');
expect(response.body.code).toBe('INTERNAL_ERROR');
expect(response.body.requestId).toBeDefined();
});
it('should handle LinkedIn API network errors', async () => {
mockDbUtils.getUserWithSmartLookup.mockResolvedValue({
user: {
_id: 'user_id',
userName: 'test_user',
email: 'test@example.com'
},
userType: 'onairos'
});
// Mock network error
mockFetch.mockRejectedValue(new Error('Network error'));
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_token',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Failed to validate LinkedIn access token');
expect(response.body.code).toBe('TOKEN_VALIDATION_ERROR');
});
it('should include development error details in development mode', async () => {
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
mockDbUtils.getUserWithSmartLookup.mockRejectedValue(
new Error('Specific database error')
);
const response = await request(app)
.post('/linkedin/native-auth')
.send({
accessToken: 'valid_token',
userAccountInfo: {
username: 'test_user',
email: 'test@example.com'
}
});
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.details).toBe('Specific database error');
process.env.NODE_ENV = originalNodeEnv;
});
});
});