mirror of
https://github.com/strapi/strapi.git
synced 2026-05-03 16:22:30 +00:00
chore: add tests
This commit is contained in:
+53
-1
@@ -1,4 +1,4 @@
|
||||
import { mapRelation } from '../relations/utils/map-relation';
|
||||
import { mapRelation, traverseEntityRelations } from '../relations/utils/map-relation';
|
||||
|
||||
const mapper = mapRelation(async (relation) => {
|
||||
if (!relation) return 'default';
|
||||
@@ -114,3 +114,55 @@ describe('map relation', () => {
|
||||
expect(await mapper(relation)).toBe(expectedRelation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('traverseEntityRelations', () => {
|
||||
const CATEGORY_UID = 'api::category.category';
|
||||
|
||||
const schema = {
|
||||
uid: 'api::article.article',
|
||||
modelType: 'contentType' as const,
|
||||
attributes: {
|
||||
// morphToOne — visitor must be skipped (inline columns, not a join table)
|
||||
related: { type: 'relation' as const, relation: 'morphToOne' as const },
|
||||
// regular relation — visitor must be called
|
||||
category: {
|
||||
type: 'relation' as const,
|
||||
relation: 'manyToOne' as const,
|
||||
target: CATEGORY_UID,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
schema,
|
||||
getModel: jest.fn().mockReturnValue(null),
|
||||
};
|
||||
|
||||
it('does not call visitor for morphToOne relations', async () => {
|
||||
const visitor = jest.fn();
|
||||
// null value prevents traverseEntity from recursing into the attribute
|
||||
await traverseEntityRelations(visitor, options, { related: null });
|
||||
expect(visitor).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls visitor for regular relation attributes', async () => {
|
||||
const visitor = jest.fn();
|
||||
await traverseEntityRelations(visitor, options, { category: null });
|
||||
expect(visitor).toHaveBeenCalledTimes(1);
|
||||
expect(visitor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ key: 'category' }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('skips morphToOne but processes other relations in the same entity', async () => {
|
||||
const visitor = jest.fn();
|
||||
await traverseEntityRelations(visitor, options, { related: null, category: null });
|
||||
// only 'category' should trigger the visitor — 'related' (morphToOne) is skipped
|
||||
expect(visitor).toHaveBeenCalledTimes(1);
|
||||
expect(visitor).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ key: 'category' }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Internal } from '@strapi/types';
|
||||
|
||||
import { getDeepPopulate } from '../populate';
|
||||
|
||||
const ARTICLE_UID = 'api::article.article' as Internal.UID.ContentType;
|
||||
const CATEGORY_UID = 'api::category.category' as Internal.UID.ContentType;
|
||||
|
||||
const articleModel = {
|
||||
uid: ARTICLE_UID,
|
||||
modelType: 'contentType' as const,
|
||||
kind: 'collectionType' as const,
|
||||
info: { displayName: 'Article', singularName: 'article', pluralName: 'articles' },
|
||||
options: { draftAndPublish: true },
|
||||
attributes: {
|
||||
title: { type: 'string' as const },
|
||||
// Polymorphic relations that can point to any content type
|
||||
related: { type: 'relation' as const, relation: 'morphToOne' as const },
|
||||
relatedMany: { type: 'relation' as const, relation: 'morphToMany' as const },
|
||||
// Regular relation with a fixed target
|
||||
category: {
|
||||
type: 'relation' as const,
|
||||
relation: 'manyToOne' as const,
|
||||
target: CATEGORY_UID,
|
||||
},
|
||||
// Virtual relation not managed by the DB layer — must be excluded
|
||||
virtualRel: {
|
||||
type: 'relation' as const,
|
||||
relation: 'oneToMany' as const,
|
||||
target: CATEGORY_UID,
|
||||
unstable_virtual: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('getDeepPopulate', () => {
|
||||
let result: Record<string, unknown>;
|
||||
|
||||
beforeAll(() => {
|
||||
global.strapi = { getModel: () => articleModel } as any;
|
||||
result = getDeepPopulate(ARTICLE_UID);
|
||||
});
|
||||
|
||||
it('includes morphToOne relations', () => {
|
||||
expect(result).toHaveProperty('related');
|
||||
});
|
||||
|
||||
it('includes morphToMany relations', () => {
|
||||
expect(result).toHaveProperty('relatedMany');
|
||||
});
|
||||
|
||||
it('includes regular relations', () => {
|
||||
expect(result).toHaveProperty('category');
|
||||
});
|
||||
|
||||
it('excludes unstable_virtual relations', () => {
|
||||
expect(result).not.toHaveProperty('virtualRel');
|
||||
});
|
||||
|
||||
it('does not include scalar attributes', () => {
|
||||
expect(result).not.toHaveProperty('title');
|
||||
});
|
||||
});
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import applyPopulate from '../apply';
|
||||
|
||||
/**
|
||||
* Tests for the morphToOne populate helper in apply.ts.
|
||||
*
|
||||
* Key behaviour under test:
|
||||
* - The populated result row includes a `__type` field (or a custom typeField from morphColumn)
|
||||
* that carries the target content-type UID.
|
||||
* - When a result row has no morph target (null id / type columns) the attribute is set to null.
|
||||
*/
|
||||
|
||||
// fromRow is the only piece of transform we need to isolate — mock it so tests
|
||||
// don't depend on field/scalar serialisation internals.
|
||||
jest.mock('../../transform', () => ({
|
||||
fromRow: jest.fn((_meta: unknown, row: unknown) => {
|
||||
if (row == null) return null;
|
||||
return row; // pass-through for test purposes
|
||||
}),
|
||||
}));
|
||||
|
||||
const TARGET_TYPE = 'api::category.category';
|
||||
const SOURCE_UID = 'api::article.article';
|
||||
const ATTRIBUTE_NAME = 'related';
|
||||
|
||||
const buildMorphAttribute = (typeField?: string) => ({
|
||||
type: 'relation' as const,
|
||||
relation: 'morphToOne' as const,
|
||||
morphColumn: {
|
||||
idColumn: { name: 'related_id', referencedColumn: 'id' },
|
||||
typeColumn: { name: 'related_type' },
|
||||
...(typeField ? { typeField } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const buildCtx = (targetRows: Record<string, unknown>[], typeField?: string) => {
|
||||
const mockQb = {
|
||||
alias: 't',
|
||||
init: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
execute: jest.fn().mockResolvedValue(targetRows),
|
||||
};
|
||||
|
||||
const db = {
|
||||
metadata: {
|
||||
get: jest.fn((type: string) => {
|
||||
if (type === SOURCE_UID) {
|
||||
return {
|
||||
attributes: {
|
||||
[ATTRIBUTE_NAME]: buildMorphAttribute(typeField),
|
||||
},
|
||||
};
|
||||
}
|
||||
// target type meta — only needs to exist (fromRow is mocked)
|
||||
return { columnToAttribute: {}, attributes: {} };
|
||||
}),
|
||||
},
|
||||
entityManager: {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQb),
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
db,
|
||||
uid: SOURCE_UID,
|
||||
qb: { state: { filters: {} } },
|
||||
};
|
||||
|
||||
return { ctx, mockQb };
|
||||
};
|
||||
|
||||
describe('morphToOne populate', () => {
|
||||
it('includes __type in the populated result', async () => {
|
||||
const targetRow = { id: 10, name: 'Category A' };
|
||||
const { ctx } = buildCtx([targetRow]);
|
||||
|
||||
const results: Record<string, unknown>[] = [
|
||||
{ id: 1, related_id: 10, related_type: TARGET_TYPE },
|
||||
];
|
||||
|
||||
await applyPopulate(results, { [ATTRIBUTE_NAME]: {} }, ctx as any);
|
||||
|
||||
expect(results[0][ATTRIBUTE_NAME]).toEqual({
|
||||
__type: TARGET_TYPE,
|
||||
...targetRow,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses a custom typeField from morphColumn when provided', async () => {
|
||||
const targetRow = { id: 10, name: 'Category A' };
|
||||
const { ctx } = buildCtx([targetRow], 'contentType');
|
||||
|
||||
const results: Record<string, unknown>[] = [
|
||||
{ id: 1, related_id: 10, related_type: TARGET_TYPE },
|
||||
];
|
||||
|
||||
await applyPopulate(results, { [ATTRIBUTE_NAME]: {} }, ctx as any);
|
||||
|
||||
expect(results[0][ATTRIBUTE_NAME]).toEqual({
|
||||
contentType: TARGET_TYPE,
|
||||
...targetRow,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the attribute to null when the morph columns are empty', async () => {
|
||||
const { ctx } = buildCtx([]);
|
||||
|
||||
const results: Record<string, unknown>[] = [{ id: 2, related_id: null, related_type: null }];
|
||||
|
||||
await applyPopulate(results, { [ATTRIBUTE_NAME]: {} }, ctx as any);
|
||||
|
||||
expect(results[0][ATTRIBUTE_NAME]).toBeNull();
|
||||
});
|
||||
|
||||
it('handles mixed results — some with targets, some without', async () => {
|
||||
const targetRow = { id: 10, name: 'Category A' };
|
||||
const { ctx } = buildCtx([targetRow]);
|
||||
|
||||
const results: Record<string, unknown>[] = [
|
||||
{ id: 1, related_id: 10, related_type: TARGET_TYPE },
|
||||
{ id: 2, related_id: null, related_type: null },
|
||||
];
|
||||
|
||||
await applyPopulate(results, { [ATTRIBUTE_NAME]: {} }, ctx as any);
|
||||
|
||||
expect(results[0][ATTRIBUTE_NAME]).toEqual({ __type: TARGET_TYPE, ...targetRow });
|
||||
expect(results[1][ATTRIBUTE_NAME]).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user