buster/packages/access-controls/src/datasets/cache.test.ts

231 lines
8.0 KiB
TypeScript
Raw Normal View History

feat(access-controls): migrate Rust access control libraries to TypeScript This PR migrates the Rust and libraries to TypeScript in the new package. ## Changes ### New Access Controls Package - Implemented complete asset permissions system with CRUD operations - Added dataset permissions with all 5 access paths (admin, direct user, user→group, team→direct, team→group, org→default) - Implemented cascading permissions (metrics inherit from dashboards/chats/collections) - Added LRU caching to replace Redis (30-second TTL) - Comprehensive test coverage (148 tests passing, 3 skipped pending createUser) ### Database Package Updates - Fixed AssetType enum to include all 14 asset types - Added missing database queries for permissions - Fixed timestamp fields to use ISO strings - Added getUserOrganizationsByUserId function - Created organized query structure by table ### AI Package Updates - Updated getPermissionedDatasets API calls to new format - Changed property references from ymlFile to ymlContent - Fixed all test mocks to match new API structure ### Trigger App Updates - Updated to use new access controls API - Fixed test mocks to match DatasetListResult structure - Added proper type imports ### Server Shared Updates - Added request/response types for access control endpoints - Exported types for API consistency ## Testing - All packages build successfully with turbo build:dry-run - All tests pass with turbo test:unit - Full type safety maintained throughout ## Migration Notes - Drop-in replacement for Rust libraries - Same permission model and roles - Cache invalidation strategies implemented - Soft delete pattern maintained 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-29 00:17:35 +08:00
import { beforeEach, describe, expect, it } from 'vitest';
import type { PermissionedDataset } from '../types/dataset-permissions';
import {
clearAllCaches,
getCachedDatasetAccess,
getCachedPermissionedDatasets,
2025-10-10 00:19:04 +08:00
getCacheStats,
feat(access-controls): migrate Rust access control libraries to TypeScript This PR migrates the Rust and libraries to TypeScript in the new package. ## Changes ### New Access Controls Package - Implemented complete asset permissions system with CRUD operations - Added dataset permissions with all 5 access paths (admin, direct user, user→group, team→direct, team→group, org→default) - Implemented cascading permissions (metrics inherit from dashboards/chats/collections) - Added LRU caching to replace Redis (30-second TTL) - Comprehensive test coverage (148 tests passing, 3 skipped pending createUser) ### Database Package Updates - Fixed AssetType enum to include all 14 asset types - Added missing database queries for permissions - Fixed timestamp fields to use ISO strings - Added getUserOrganizationsByUserId function - Created organized query structure by table ### AI Package Updates - Updated getPermissionedDatasets API calls to new format - Changed property references from ymlFile to ymlContent - Fixed all test mocks to match new API structure ### Trigger App Updates - Updated to use new access controls API - Fixed test mocks to match DatasetListResult structure - Added proper type imports ### Server Shared Updates - Added request/response types for access control endpoints - Exported types for API consistency ## Testing - All packages build successfully with turbo build:dry-run - All tests pass with turbo test:unit - Full type safety maintained throughout ## Migration Notes - Drop-in replacement for Rust libraries - Same permission model and roles - Cache invalidation strategies implemented - Soft delete pattern maintained 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-29 00:17:35 +08:00
invalidateDataset,
invalidateOnPermissionChange,
invalidateUser,
resetCacheStats,
setCachedDatasetAccess,
setCachedPermissionedDatasets,
} from './cache';
describe('Dataset Permission Cache', () => {
beforeEach(() => {
clearAllCaches();
resetCacheStats();
});
describe('Dataset List Cache', () => {
const mockDatasets: PermissionedDataset[] = [
{
id: 'ds1',
name: 'Dataset 1',
organizationId: 'org1',
dataSourceId: 'source1',
ymlContent: {},
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: 'ds2',
name: 'Dataset 2',
organizationId: 'org1',
dataSourceId: 'source1',
ymlContent: {},
createdAt: new Date(),
updatedAt: new Date(),
},
];
it('should cache and retrieve dataset lists', () => {
const result = { datasets: mockDatasets, total: 2 };
// Should return undefined for uncached
expect(getCachedPermissionedDatasets('user1', 0, 20)).toBeUndefined();
// Set cache
setCachedPermissionedDatasets('user1', 0, 20, result);
// Should retrieve cached result
const cached = getCachedPermissionedDatasets('user1', 0, 20);
expect(cached).toEqual(result);
});
it('should handle different pagination keys', () => {
const page1 = { datasets: [mockDatasets[0]], total: 2 };
const page2 = { datasets: [mockDatasets[1]], total: 2 };
setCachedPermissionedDatasets('user1', 0, 1, page1);
setCachedPermissionedDatasets('user1', 1, 1, page2);
expect(getCachedPermissionedDatasets('user1', 0, 1)).toEqual(page1);
expect(getCachedPermissionedDatasets('user1', 1, 1)).toEqual(page2);
expect(getCachedPermissionedDatasets('user1', 0, 2)).toBeUndefined();
});
it('should track cache stats', () => {
const result = { datasets: mockDatasets, total: 2 };
// Miss
getCachedPermissionedDatasets('user1', 0, 20);
let stats = getCacheStats();
expect(stats.datasets.misses).toBe(1);
// Set and hit
setCachedPermissionedDatasets('user1', 0, 20, result);
getCachedPermissionedDatasets('user1', 0, 20);
stats = getCacheStats();
expect(stats.datasets.hits).toBe(1);
expect(stats.datasets.hitRate).toBe('50.00%');
});
});
describe('Dataset Access Cache', () => {
it('should cache and retrieve access results', () => {
// Should return undefined for uncached
expect(getCachedDatasetAccess('user1', 'ds1')).toBeUndefined();
// Set cache
setCachedDatasetAccess('user1', 'ds1', true);
setCachedDatasetAccess('user1', 'ds2', false);
// Should retrieve cached results
expect(getCachedDatasetAccess('user1', 'ds1')).toBe(true);
expect(getCachedDatasetAccess('user1', 'ds2')).toBe(false);
});
it('should track access cache stats separately', () => {
// Miss
getCachedDatasetAccess('user1', 'ds1');
let stats = getCacheStats();
expect(stats.access.misses).toBe(1);
expect(stats.datasets.misses).toBe(0); // Different cache
// Set and hit
setCachedDatasetAccess('user1', 'ds1', true);
getCachedDatasetAccess('user1', 'ds1');
stats = getCacheStats();
expect(stats.access.hits).toBe(1);
expect(stats.access.hitRate).toBe('50.00%');
});
});
describe('Cache Invalidation', () => {
beforeEach(() => {
// Set up some cached data
setCachedPermissionedDatasets('user1', 0, 20, { datasets: [], total: 0 });
setCachedPermissionedDatasets('user1', 1, 20, { datasets: [], total: 0 });
setCachedPermissionedDatasets('user2', 0, 20, { datasets: [], total: 0 });
setCachedDatasetAccess('user1', 'ds1', true);
setCachedDatasetAccess('user1', 'ds2', true);
setCachedDatasetAccess('user2', 'ds1', true);
});
it('should invalidate all caches for a user', () => {
invalidateUser('user1');
// User1 caches should be cleared
expect(getCachedPermissionedDatasets('user1', 0, 20)).toBeUndefined();
expect(getCachedPermissionedDatasets('user1', 1, 20)).toBeUndefined();
expect(getCachedDatasetAccess('user1', 'ds1')).toBeUndefined();
expect(getCachedDatasetAccess('user1', 'ds2')).toBeUndefined();
// User2 caches should remain
expect(getCachedPermissionedDatasets('user2', 0, 20)).toBeDefined();
expect(getCachedDatasetAccess('user2', 'ds1')).toBeDefined();
});
it('should invalidate all caches for a dataset', () => {
invalidateDataset('ds1');
// All dataset list caches cleared (broad invalidation)
expect(getCachedPermissionedDatasets('user1', 0, 20)).toBeUndefined();
expect(getCachedPermissionedDatasets('user2', 0, 20)).toBeUndefined();
// Specific dataset access cleared
expect(getCachedDatasetAccess('user1', 'ds1')).toBeUndefined();
expect(getCachedDatasetAccess('user2', 'ds1')).toBeUndefined();
// Other dataset access remains
expect(getCachedDatasetAccess('user1', 'ds2')).toBeDefined();
});
it('should handle user permission changes', () => {
invalidateOnPermissionChange('ds1', 'user1', 'user');
// Should invalidate the dataset
expect(getCachedDatasetAccess('user1', 'ds1')).toBeUndefined();
expect(getCachedDatasetAccess('user2', 'ds1')).toBeUndefined();
// Should invalidate all dataset lists (broad invalidation)
expect(getCachedPermissionedDatasets('user1', 0, 20)).toBeUndefined();
expect(getCachedPermissionedDatasets('user2', 0, 20)).toBeUndefined();
});
it('should clear all caches for team/group permission changes', () => {
const initialDatasets = getCachedPermissionedDatasets('user1', 0, 20);
const initialAccess = getCachedDatasetAccess('user1', 'ds1');
expect(initialDatasets).toBeDefined();
expect(initialAccess).toBeDefined();
// Team permission change
invalidateOnPermissionChange('ds1', 'team1', 'team');
// All caches should be cleared
expect(getCachedPermissionedDatasets('user1', 0, 20)).toBeUndefined();
expect(getCachedDatasetAccess('user1', 'ds1')).toBeUndefined();
// Set up new data
setCachedPermissionedDatasets('user1', 0, 20, { datasets: [], total: 0 });
setCachedDatasetAccess('user1', 'ds1', true);
// Permission group change
invalidateOnPermissionChange('ds1', 'group1', 'permission_group');
// All caches should be cleared again
expect(getCachedPermissionedDatasets('user1', 0, 20)).toBeUndefined();
expect(getCachedDatasetAccess('user1', 'ds1')).toBeUndefined();
});
});
describe('Cache Management', () => {
it('should clear all caches', () => {
setCachedPermissionedDatasets('user1', 0, 20, { datasets: [], total: 0 });
setCachedDatasetAccess('user1', 'ds1', true);
clearAllCaches();
expect(getCachedPermissionedDatasets('user1', 0, 20)).toBeUndefined();
expect(getCachedDatasetAccess('user1', 'ds1')).toBeUndefined();
});
it('should reset stats but keep cache data', () => {
// Generate stats
getCachedPermissionedDatasets('user1', 0, 20);
setCachedPermissionedDatasets('user1', 0, 20, { datasets: [], total: 0 });
getCachedPermissionedDatasets('user1', 0, 20);
let stats = getCacheStats();
expect(stats.datasets.hits).toBe(1);
expect(stats.datasets.misses).toBe(1);
resetCacheStats();
stats = getCacheStats();
expect(stats.datasets.hits).toBe(0);
expect(stats.datasets.misses).toBe(0);
expect(stats.datasets.size).toBe(1); // Data still cached
});
it('should respect cache size limits', () => {
const stats = getCacheStats();
expect(stats.datasets.maxSize).toBe(1000);
expect(stats.access.maxSize).toBe(5000);
});
});
});