buster/packages/access-controls/src/user-organizations.test.ts

449 lines
14 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import * as db from '@buster/database';
import { describe, expect, it, vi } from 'vitest';
import {
checkEmailDomainForOrganization,
checkUserInOrganization,
createUserInOrganization,
getOrganizationWithDefaults,
getUserOrganizations,
} from './user-organizations';
// Mock the database module
vi.mock('@buster/database', () => ({
getDb: vi.fn(),
and: vi.fn((...args) => ({ _and: args })),
eq: vi.fn((a, b) => ({ _eq: [a, b] })),
isNull: vi.fn((a) => ({ _isNull: a })),
organizations: {
id: 'organizations.id',
domains: 'organizations.domains',
deletedAt: 'organizations.deletedAt',
defaultRole: 'organizations.defaultRole',
},
users: {
id: 'users.id',
email: 'users.email',
},
usersToOrganizations: {
userId: 'usersToOrganizations.userId',
organizationId: 'usersToOrganizations.organizationId',
role: 'usersToOrganizations.role',
status: 'usersToOrganizations.status',
deletedAt: 'usersToOrganizations.deletedAt',
},
}));
// Generate test UUIDs
const TEST_USER_ID = randomUUID();
const TEST_ORG_ID = randomUUID();
const TEST_ORG_ID_2 = randomUUID();
const CREATOR_USER_ID = randomUUID();
describe('user-organizations', () => {
describe('checkUserInOrganization', () => {
it('should return user organization info when user exists in org', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([
{
userId: TEST_USER_ID,
organizationId: TEST_ORG_ID,
role: 'querier',
status: 'active',
},
]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkUserInOrganization(TEST_USER_ID, TEST_ORG_ID);
expect(result).toEqual({
userId: TEST_USER_ID,
organizationId: TEST_ORG_ID,
role: 'querier',
status: 'active',
});
expect(mockDb.where).toHaveBeenCalled();
expect(mockDb.limit).toHaveBeenCalledWith(1);
});
it('should return null when user does not exist in org', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkUserInOrganization(TEST_USER_ID, TEST_ORG_ID);
expect(result).toBeNull();
});
it('should validate input parameters', async () => {
await expect(checkUserInOrganization('invalid-uuid', TEST_ORG_ID)).rejects.toThrow();
await expect(checkUserInOrganization(TEST_USER_ID, 'invalid-uuid')).rejects.toThrow();
});
});
describe('getUserOrganizations', () => {
it('should return all organizations for a user', async () => {
const mockOrgs = [
{
userId: TEST_USER_ID,
organizationId: TEST_ORG_ID,
role: 'querier',
status: 'active',
},
{
userId: TEST_USER_ID,
organizationId: TEST_ORG_ID_2,
role: 'workspace_admin',
status: 'active',
},
];
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(mockOrgs),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await getUserOrganizations(TEST_USER_ID);
expect(result).toEqual(mockOrgs);
expect(result).toHaveLength(2);
});
it('should return empty array when user has no organizations', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await getUserOrganizations(TEST_USER_ID);
expect(result).toEqual([]);
});
});
describe('checkEmailDomainForOrganization', () => {
it('should return true when email domain matches org domain', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ domains: ['example.com', 'test.com'] }]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(true);
});
it('should return true when email domain matches with @ prefix', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ domains: ['@example.com', 'test.com'] }]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(true);
});
it('should return false when email domain does not match', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ domains: ['allowed.com', 'permitted.com'] }]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@notallowed.com', TEST_ORG_ID);
expect(result).toBe(false);
});
it('should return false when org has no domains', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ domains: null }]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(false);
});
it('should return false when org does not exist', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(false);
});
it('should be case insensitive for domain matching', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ domains: ['EXAMPLE.COM'] }]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(true);
});
});
describe('getOrganizationWithDefaults', () => {
it('should return organization with defaults', async () => {
const mockOrg = {
id: TEST_ORG_ID,
name: 'Test Org',
defaultRole: 'restricted_querier',
domains: ['example.com'],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
};
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([mockOrg]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await getOrganizationWithDefaults(TEST_ORG_ID);
expect(result).toEqual(mockOrg);
});
it('should return null when organization does not exist', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await getOrganizationWithDefaults(TEST_ORG_ID);
expect(result).toBeNull();
});
});
describe('createUserInOrganization', () => {
it('should create new user and add to organization', async () => {
const mockOrg = {
id: TEST_ORG_ID,
name: 'Test Org',
defaultRole: 'restricted_querier',
domains: ['example.com'],
};
const mockUser = {
id: 'new-user-123',
email: 'newuser@example.com',
name: 'New User',
config: {},
attributes: {},
};
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
// Check if we're querying users or organizations
const lastCall = mockDb.from.mock.lastCall;
if (lastCall && lastCall[0] === db.users) return Promise.resolve([]);
return Promise.resolve([mockOrg]);
}),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([mockUser]),
onConflictDoUpdate: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await createUserInOrganization(
'newuser@example.com',
'New User',
TEST_ORG_ID,
CREATOR_USER_ID
);
expect(result.user).toEqual(mockUser);
expect(result.membership).toEqual({
userId: 'new-user-123',
organizationId: TEST_ORG_ID,
role: 'restricted_querier',
status: 'active',
});
expect(mockDb.insert).toHaveBeenCalledTimes(2); // User and membership
});
it('should use existing user if email already exists', async () => {
const mockOrg = {
id: TEST_ORG_ID,
name: 'Test Org',
defaultRole: 'querier',
domains: ['example.com'],
};
const mockExistingUser = {
id: 'existing-user-123',
email: 'existing@example.com',
name: 'Existing User',
config: {},
attributes: {},
};
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
// Check if we're querying users or organizations
const lastCall = mockDb.from.mock.lastCall;
if (lastCall && lastCall[0] === db.users) return Promise.resolve([mockExistingUser]);
return Promise.resolve([mockOrg]);
}),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
onConflictDoUpdate: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const result = await createUserInOrganization(
'existing@example.com',
'Another Name',
TEST_ORG_ID,
CREATOR_USER_ID
);
expect(result.user).toEqual(mockExistingUser);
expect(result.membership.userId).toBe('existing-user-123');
// Should only insert membership, not user
expect(mockDb.insert).toHaveBeenCalledTimes(1);
});
it('should throw error if organization does not exist', async () => {
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
const nonExistentOrgId = randomUUID();
await expect(
createUserInOrganization('user@example.com', 'User', nonExistentOrgId, CREATOR_USER_ID)
).rejects.toThrow(`Organization ${nonExistentOrgId} not found`);
});
it('should use email prefix as default name when name not provided', async () => {
const mockOrg = {
id: TEST_ORG_ID,
name: 'Test Org',
defaultRole: 'restricted_querier',
domains: ['example.com'],
};
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
// Check if we're querying users or organizations
const lastCall = mockDb.from.mock.lastCall;
if (lastCall && lastCall[0] === db.users) return Promise.resolve([]);
return Promise.resolve([mockOrg]);
}),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([
{
id: 'new-user-123',
email: 'john.doe@example.com',
name: 'john.doe',
config: {},
attributes: {},
},
]),
onConflictDoUpdate: vi.fn().mockResolvedValue([]),
};
vi.mocked(db.getDb).mockReturnValue(mockDb as any);
await createUserInOrganization(
'john.doe@example.com',
undefined,
TEST_ORG_ID,
CREATOR_USER_ID
);
expect(mockDb.insert).toHaveBeenCalled();
// Find the user insert call (first insert is for users)
const insertCalls = mockDb.insert.mock.calls;
expect(insertCalls.length).toBeGreaterThan(0);
// Check the values call that corresponds to the user insert
const valuesCalls = mockDb.values.mock.calls;
expect(valuesCalls.length).toBeGreaterThan(0);
// The first values call should be for the user
const userInsertData = valuesCalls[0][0];
expect(userInsertData.name).toBe('john.doe');
});
});
});