Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

489 lines
13 KiB
TypeScript

import { testDb } from '@n8n/backend-test-utils';
import { type Scope, ScopeRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import type { Scope as ScopeType } from '@n8n/permissions';
import { createScope, createScopes, createTestScopes } from '../../shared/db/roles';
describe('ScopeRepository', () => {
let scopeRepository: ScopeRepository;
beforeAll(async () => {
await testDb.init();
scopeRepository = Container.get(ScopeRepository);
});
beforeEach(async () => {
// Truncate in the correct order to respect foreign key constraints
// user table references role via roleSlug
// project_relation references role
// role_scope references scope, so truncate it first
await testDb.truncate(['User', 'ProjectRelation', 'Role', 'Scope']);
});
afterAll(async () => {
await testDb.terminate();
});
describe('findByList()', () => {
describe('successful queries', () => {
it('should return empty array when given empty slug array', async () => {
//
// ARRANGE
//
await createTestScopes(); // Create some scopes but don't query for them
//
// ACT
//
const scopes = await scopeRepository.findByList([]);
//
// ASSERT
//
expect(scopes).toEqual([]);
});
it('should return empty array when no scopes exist', async () => {
//
// ARRANGE & ACT
//
const scopes = await scopeRepository.findByList(['non-existent:scope']);
//
// ASSERT
//
expect(scopes).toEqual([]);
});
it('should return single scope when one slug matches', async () => {
//
// ARRANGE
//
const { readScope } = await createTestScopes();
//
// ACT
//
const scopes = await scopeRepository.findByList([readScope.slug]);
//
// ASSERT
//
expect(scopes).toHaveLength(1);
expect(scopes[0]).toEqual(
expect.objectContaining({
slug: readScope.slug,
displayName: readScope.displayName,
description: readScope.description,
}),
);
});
it('should return multiple scopes when multiple slugs match', async () => {
//
// ARRANGE
//
const { readScope, writeScope, deleteScope, adminScope } = await createTestScopes();
//
// ACT
//
const scopes = await scopeRepository.findByList([
readScope.slug,
writeScope.slug,
deleteScope.slug,
]);
//
// ASSERT
//
expect(scopes).toHaveLength(3);
expect(scopes).toEqual(
expect.arrayContaining([
expect.objectContaining({ slug: readScope.slug }),
expect.objectContaining({ slug: writeScope.slug }),
expect.objectContaining({ slug: deleteScope.slug }),
]),
);
// Verify adminScope is NOT included
expect(scopes.find((s) => s.slug === adminScope.slug)).toBeUndefined();
});
it('should return all existing scopes when all slugs match', async () => {
//
// ARRANGE
//
const { readScope, writeScope, deleteScope, adminScope } = await createTestScopes();
//
// ACT
//
const scopes = await scopeRepository.findByList([
readScope.slug,
writeScope.slug,
deleteScope.slug,
adminScope.slug,
]);
//
// ASSERT
//
expect(scopes).toHaveLength(4);
expect(scopes).toEqual(
expect.arrayContaining([
expect.objectContaining({ slug: readScope.slug }),
expect.objectContaining({ slug: writeScope.slug }),
expect.objectContaining({ slug: deleteScope.slug }),
expect.objectContaining({ slug: adminScope.slug }),
]),
);
});
});
describe('partial matches', () => {
it('should return only existing scopes when some slugs do not exist', async () => {
//
// ARRANGE
//
const { readScope, writeScope } = await createTestScopes();
//
// ACT
//
const scopes = await scopeRepository.findByList([
readScope.slug,
'non-existent:scope:1' as ScopeType,
writeScope.slug,
'non-existent:scope:2' as ScopeType,
]);
//
// ASSERT
//
expect(scopes).toHaveLength(2);
expect(scopes).toEqual(
expect.arrayContaining([
expect.objectContaining({ slug: readScope.slug }),
expect.objectContaining({ slug: writeScope.slug }),
]),
);
});
it('should return empty array when none of the slugs exist', async () => {
//
// ARRANGE
//
await createTestScopes(); // Create scopes but don't query for them
//
// ACT
//
const scopes = await scopeRepository.findByList([
'non-existent:scope:1' as ScopeType,
'non-existent:scope:2' as ScopeType,
'non-existent:scope:3' as ScopeType,
]);
//
// ASSERT
//
expect(scopes).toEqual([]);
});
});
describe('duplicate handling', () => {
it('should return each scope only once when slug array contains duplicates', async () => {
//
// ARRANGE
//
const { readScope, writeScope } = await createTestScopes();
//
// ACT
//
const scopes = await scopeRepository.findByList([
readScope.slug,
writeScope.slug,
readScope.slug, // Duplicate
writeScope.slug, // Duplicate
readScope.slug, // Another duplicate
]);
//
// ASSERT
//
expect(scopes).toHaveLength(2);
expect(scopes).toEqual(
expect.arrayContaining([
expect.objectContaining({ slug: readScope.slug }),
expect.objectContaining({ slug: writeScope.slug }),
]),
);
// Verify no duplicates in result
const slugs = scopes.map((s) => s.slug);
const uniqueSlugs = [...new Set(slugs)];
expect(slugs).toEqual(uniqueSlugs);
});
it('should handle mix of valid, invalid, and duplicate slugs', async () => {
//
// ARRANGE
//
const { readScope } = await createTestScopes();
//
// ACT
//
const scopes = await scopeRepository.findByList([
readScope.slug,
'invalid:scope:1' as ScopeType,
readScope.slug, // Duplicate valid
'invalid:scope:2' as ScopeType,
'invalid:scope:1' as ScopeType, // Duplicate invalid
readScope.slug, // Another duplicate valid
]);
//
// ASSERT
//
expect(scopes).toHaveLength(1);
expect(scopes[0]).toEqual(expect.objectContaining({ slug: readScope.slug }));
});
});
describe('large datasets', () => {
it('should handle querying for many scopes efficiently', async () => {
//
// ARRANGE
//
const createdScopes = await createScopes(50, { description: 'Bulk test scope' });
const slugsToQuery = createdScopes.slice(0, 25).map((s) => s.slug);
//
// ACT
//
const startTime = Date.now();
const scopes = await scopeRepository.findByList(slugsToQuery);
const endTime = Date.now();
//
// ASSERT
//
expect(scopes).toHaveLength(25);
expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second
// Verify all requested scopes are returned
const returnedSlugs = scopes.map((s) => s.slug).sort();
const expectedSlugs = slugsToQuery.sort();
expect(returnedSlugs).toEqual(expectedSlugs);
});
it('should maintain data integrity with complex scope structures', async () => {
//
// ARRANGE
//
const complexScopes = await Promise.all([
createScope({
slug: 'complex:scope:with:colons' as ScopeType,
displayName: 'Complex Scope With Colons',
description: 'A scope with multiple colons in the slug',
}),
createScope({
slug: 'scope-with-dashes' as ScopeType,
displayName: 'Scope With Dashes',
description: 'A scope with dashes',
}),
createScope({
slug: 'scope_with_underscores' as ScopeType,
displayName: 'Scope With Underscores',
description: 'A scope with underscores',
}),
]);
const slugsToQuery = complexScopes.map((s) => s.slug);
//
// ACT
//
const scopes = await scopeRepository.findByList(slugsToQuery);
//
// ASSERT
//
expect(scopes).toHaveLength(3);
for (const originalScope of complexScopes) {
const foundScope = scopes.find((s) => s.slug === originalScope.slug);
expect(foundScope).toBeDefined();
expect(foundScope!.displayName).toBe(originalScope.displayName);
expect(foundScope!.description).toBe(originalScope.description);
}
});
});
describe('edge cases and validation', () => {
it('should handle null and undefined values gracefully', async () => {
//
// ARRANGE
//
const scope = await createScope({
slug: 'scope-with-nulls' as ScopeType,
displayName: null,
description: null,
});
//
// ACT
//
const scopes = await scopeRepository.findByList([scope.slug]);
//
// ASSERT
//
expect(scopes).toHaveLength(1);
expect(scopes[0].slug).toBe(scope.slug);
expect(scopes[0].displayName).toBeNull();
expect(scopes[0].description).toBeNull();
});
it('should preserve order consistency across multiple queries', async () => {
//
// ARRANGE
//
const { readScope, writeScope, deleteScope, adminScope } = await createTestScopes();
const slugsToQuery = [adminScope.slug, readScope.slug, deleteScope.slug, writeScope.slug];
//
// ACT
//
const scopes1 = await scopeRepository.findByList(slugsToQuery);
const scopes2 = await scopeRepository.findByList(slugsToQuery);
const scopes3 = await scopeRepository.findByList(slugsToQuery);
//
// ASSERT
//
expect(scopes1).toHaveLength(4);
expect(scopes2).toHaveLength(4);
expect(scopes3).toHaveLength(4);
// All queries should return the same scopes (though order may vary due to SQL implementation)
const getSortedSlugs = (scopeList: Scope[]) => scopeList.map((s) => s.slug).sort();
expect(getSortedSlugs(scopes1)).toEqual(getSortedSlugs(scopes2));
expect(getSortedSlugs(scopes2)).toEqual(getSortedSlugs(scopes3));
});
it('should verify database state remains consistent after queries', async () => {
//
// ARRANGE
//
const { readScope, writeScope } = await createTestScopes();
//
// ACT
//
const scopesBefore = await scopeRepository.find();
await scopeRepository.findByList([readScope.slug, writeScope.slug]);
const scopesAfter = await scopeRepository.find();
//
// ASSERT
//
expect(scopesBefore).toHaveLength(4); // readScope, writeScope, deleteScope, adminScope
expect(scopesAfter).toHaveLength(4);
expect(scopesBefore.map((s) => s.slug).sort()).toEqual(
scopesAfter.map((s) => s.slug).sort(),
);
// Verify specific scopes are unchanged
const readScopeBefore = scopesBefore.find((s) => s.slug === readScope.slug);
const readScopeAfter = scopesAfter.find((s) => s.slug === readScope.slug);
expect(readScopeBefore).toEqual(readScopeAfter);
});
});
});
describe('findByListOrFail()', () => {
describe('success path', () => {
it('should return empty array when given empty slug array', async () => {
await createTestScopes();
const scopes = await scopeRepository.findByListOrFail([]);
expect(scopes).toEqual([]);
});
it('should return single scope when one slug matches', async () => {
const { readScope } = await createTestScopes();
const scopes = await scopeRepository.findByListOrFail([readScope.slug]);
expect(scopes).toHaveLength(1);
expect(scopes[0]).toEqual(
expect.objectContaining({
slug: readScope.slug,
displayName: readScope.displayName,
description: readScope.description,
}),
);
});
it('should return multiple scopes when multiple slugs match', async () => {
const { readScope, writeScope, deleteScope } = await createTestScopes();
const scopes = await scopeRepository.findByListOrFail([
readScope.slug,
writeScope.slug,
deleteScope.slug,
]);
expect(scopes).toHaveLength(3);
expect(scopes).toEqual(
expect.arrayContaining([
expect.objectContaining({ slug: readScope.slug }),
expect.objectContaining({ slug: writeScope.slug }),
expect.objectContaining({ slug: deleteScope.slug }),
]),
);
});
});
describe('error path (invalid scopes)', () => {
it('should throw when one slug does not exist', async () => {
await createTestScopes();
await expect(
scopeRepository.findByListOrFail(['non-existent:scope' as ScopeType]),
).rejects.toThrow('The following scopes are invalid: non-existent:scope');
});
it('should throw when some slugs do not exist and list invalid scopes in message', async () => {
const { readScope } = await createTestScopes();
await expect(
scopeRepository.findByListOrFail([
readScope.slug,
'non-existent:1' as ScopeType,
'non-existent:2' as ScopeType,
]),
).rejects.toThrow('The following scopes are invalid: non-existent:1, non-existent:2');
});
it('should throw when all slugs do not exist', async () => {
await createTestScopes();
await expect(
scopeRepository.findByListOrFail([
'non-existent:1' as ScopeType,
'non-existent:2' as ScopeType,
]),
).rejects.toThrow('The following scopes are invalid: non-existent:1, non-existent:2');
});
});
});
});