first commit
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
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
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user