tududi/backend/tests/unit/modules/oidc/provisioningService.test.js
Chris b0321b02fa
fix: resolve OIDC authentication error with existing identities (#1021)
Fixed "Cannot read properties of undefined (reading 'id')" error that occurred
when existing OIDC users attempted to log in. The issue was a case mismatch in
the Sequelize association accessor.

Changes:
- Fixed case mismatch: identity.user -> identity.User (matches association alias)
- Removed invalid username field that doesn't exist in User model
- Fixed admin role assignment to use Role model instead of User model
- Improved transaction error handling to prevent double rollbacks
- Removed unused generateUsername function

Tests:
- Added comprehensive test suite with 12 tests covering all provisioning scenarios
- All tests passing including the previously failing login scenario
2026-04-14 00:11:32 +03:00

275 lines
9.1 KiB
JavaScript

const { sequelize, User, OIDCIdentity, Role } = require('../../../../models');
const provisioningService = require('../../../../modules/oidc/provisioningService');
const providerConfig = require('../../../../modules/oidc/providerConfig');
jest.mock('../../../../modules/oidc/providerConfig');
describe('OIDC Provisioning Service', () => {
beforeAll(async () => {
await sequelize.sync({ force: true });
});
afterAll(async () => {
await sequelize.close();
});
beforeEach(async () => {
await OIDCIdentity.destroy({ where: {}, force: true });
await Role.destroy({ where: {}, force: true });
await User.destroy({ where: {}, force: true });
providerConfig.getProvider.mockReturnValue({
slug: 'test-provider',
name: 'Test Provider',
autoProvision: true,
adminEmailDomains: ['admin.com'],
});
});
describe('provisionUser', () => {
it('should return user object when existing identity logs in', async () => {
const existingUser = await User.create({
email: 'test@example.com',
username: 'testuser',
password_digest: 'hashed',
verified_email: true,
});
await OIDCIdentity.create({
user_id: existingUser.id,
provider_slug: 'test-provider',
subject: 'sub-123',
email: 'test@example.com',
name: 'Test User',
first_login_at: new Date(),
last_login_at: new Date(),
});
const claims = {
sub: 'sub-123',
email: 'test@example.com',
name: 'Test User Updated',
};
const result = await provisioningService.provisionUser(
'test-provider',
claims,
{}
);
expect(result).toBeDefined();
expect(result.user).toBeDefined();
expect(result.user.id).toBe(existingUser.id);
expect(result.user.email).toBe('test@example.com');
expect(result.isNewUser).toBe(false);
});
it('should create new user when auto-provision is enabled and identity does not exist', async () => {
const claims = {
sub: 'sub-456',
email: 'newuser@example.com',
name: 'New User',
given_name: 'New',
family_name: 'User',
};
const result = await provisioningService.provisionUser(
'test-provider',
claims,
{}
);
expect(result).toBeDefined();
expect(result.user).toBeDefined();
expect(result.user.email).toBe('newuser@example.com');
expect(result.isNewUser).toBe(true);
const identity = await OIDCIdentity.findOne({
where: { subject: 'sub-456' },
});
expect(identity).toBeDefined();
expect(identity.user_id).toBe(result.user.id);
});
it('should link existing user by email when creating new identity', async () => {
const existingUser = await User.create({
email: 'existing@example.com',
username: 'existing',
password_digest: 'hashed',
verified_email: true,
});
const claims = {
sub: 'sub-789',
email: 'existing@example.com',
name: 'Existing User',
};
const result = await provisioningService.provisionUser(
'test-provider',
claims,
{}
);
expect(result).toBeDefined();
expect(result.user).toBeDefined();
expect(result.user.id).toBe(existingUser.id);
expect(result.isNewUser).toBe(false);
const identity = await OIDCIdentity.findOne({
where: { subject: 'sub-789' },
});
expect(identity).toBeDefined();
expect(identity.user_id).toBe(existingUser.id);
});
it('should throw error when auto-provision is disabled and user does not exist', async () => {
providerConfig.getProvider.mockReturnValue({
slug: 'test-provider',
name: 'Test Provider',
autoProvision: false,
});
const claims = {
sub: 'sub-999',
email: 'notallowed@example.com',
name: 'Not Allowed',
};
await expect(
provisioningService.provisionUser('test-provider', claims, {})
).rejects.toThrow('Auto-provisioning is disabled');
});
it('should throw error when email claim is missing', async () => {
const claims = {
sub: 'sub-000',
name: 'No Email User',
};
await expect(
provisioningService.provisionUser('test-provider', claims, {})
).rejects.toThrow('Email claim is required');
});
it('should set admin flag when email domain matches admin domains', async () => {
const claims = {
sub: 'sub-admin',
email: 'admin@admin.com',
name: 'Admin User',
};
const result = await provisioningService.provisionUser(
'test-provider',
claims,
{}
);
const role = await Role.findOne({
where: { user_id: result.user.id },
});
expect(role).toBeDefined();
expect(role.is_admin).toBe(true);
});
});
describe('linkIdentityToUser', () => {
it('should link new identity to existing user', async () => {
const user = await User.create({
email: 'link@example.com',
username: 'linkuser',
password_digest: 'hashed',
verified_email: true,
});
const claims = {
sub: 'sub-link-123',
email: 'link@example.com',
name: 'Link User',
};
const identity = await provisioningService.linkIdentityToUser(
user.id,
'test-provider',
claims
);
expect(identity).toBeDefined();
expect(identity.user_id).toBe(user.id);
expect(identity.subject).toBe('sub-link-123');
});
it('should throw error when identity is already linked to another user', async () => {
const user1 = await User.create({
email: 'user1@example.com',
username: 'user1',
password_digest: 'hashed',
verified_email: true,
});
const user2 = await User.create({
email: 'user2@example.com',
username: 'user2',
password_digest: 'hashed',
verified_email: true,
});
await OIDCIdentity.create({
user_id: user1.id,
provider_slug: 'test-provider',
subject: 'sub-existing',
email: 'user1@example.com',
name: 'User 1',
first_login_at: new Date(),
last_login_at: new Date(),
});
const claims = {
sub: 'sub-existing',
email: 'user2@example.com',
name: 'User 2',
};
await expect(
provisioningService.linkIdentityToUser(
user2.id,
'test-provider',
claims
)
).rejects.toThrow('already linked to another user');
});
});
describe('shouldBeAdmin', () => {
it('should return true when email domain matches admin domains', () => {
const config = { adminEmailDomains: ['admin.com', 'company.com'] };
expect(
provisioningService.shouldBeAdmin(config, 'user@admin.com')
).toBe(true);
expect(
provisioningService.shouldBeAdmin(config, 'user@company.com')
).toBe(true);
});
it('should return false when email domain does not match', () => {
const config = { adminEmailDomains: ['admin.com'] };
expect(
provisioningService.shouldBeAdmin(config, 'user@other.com')
).toBe(false);
});
it('should return false when adminEmailDomains is empty', () => {
const config = { adminEmailDomains: [] };
expect(
provisioningService.shouldBeAdmin(config, 'user@admin.com')
).toBe(false);
});
it('should return false when adminEmailDomains is not set', () => {
const config = {};
expect(
provisioningService.shouldBeAdmin(config, 'user@admin.com')
).toBe(false);
});
});
});