GitNexus/gitnexus/test/unit/spring-aop.test.ts
MyShining 1147646518
feat(spring): model AOP transactions, caching, and security (#2783)
* feat(spring): model AOP advice and proxy behavior

* fix(spring): address AOP review findings

---------

Co-authored-by: Shining <xuenning@qiyi.com>
2026-08-01 17:22:12 +01:00

372 lines
13 KiB
TypeScript

import type { GraphNode } from 'gitnexus-shared';
import { describe, expect, it } from 'vitest';
import {
decodeSpringAopReason,
encodeSpringAopReason,
isSpringAopEvidenceNode,
parseSpringAopPointcut,
springAopPointcutMatches,
type SpringAopReason,
} from '../../src/core/ingestion/frameworks/spring/aop.js';
import {
createSpringAopCandidateIndex,
type SpringAopOwnedMethod,
} from '../../src/core/ingestion/frameworks/spring/aop-candidates.js';
const owner: GraphNode = {
id: 'Class:com.example.service.OrderService',
label: 'Class',
properties: {
name: 'OrderService',
qualifiedName: 'com.example.service.OrderService',
filePath: 'src/OrderService.java',
startLine: 1,
endLine: 20,
isExported: true,
},
};
const method: GraphNode = {
id: 'Method:com.example.service.OrderService.run',
label: 'Method',
properties: {
name: 'run',
qualifiedName: 'com.example.service.OrderService.run',
filePath: 'src/OrderService.java',
startLine: 3,
endLine: 5,
isExported: true,
visibility: 'public',
parameterCount: 1,
},
};
describe('Spring AOP static pointcuts (#2416)', () => {
it('parses the deliberately narrow execution, within, and known @annotation subset', () => {
expect(parseSpringAopPointcut('execution(public * com.example..OrderService.r*( * ))')).toEqual(
{
kind: 'execution',
ownerPattern: 'com.example..OrderService',
methodPattern: 'r*',
visibility: 'public',
parameterCount: 1,
},
);
expect(parseSpringAopPointcut('within(com.example..service.*)')).toEqual({
kind: 'within',
ownerPattern: 'com.example..service.*',
});
expect(
parseSpringAopPointcut(
'@annotation(org.springframework.transaction.annotation.Transactional)',
),
).toEqual({
kind: 'annotation',
annotation: 'org.springframework.transaction.annotation.Transactional',
});
});
it('matches owner, method, visibility, arity, and resolved method annotations', () => {
const execution = parseSpringAopPointcut('execution(public * com.example..OrderService.r*(*))');
const within = parseSpringAopPointcut('within(com.example..OrderService)');
const annotation = parseSpringAopPointcut(
'@annotation(org.springframework.transaction.annotation.Transactional)',
);
expect(execution && springAopPointcutMatches(execution, owner, method)).toBe(true);
expect(within && springAopPointcutMatches(within, owner, method)).toBe(true);
expect(
annotation &&
springAopPointcutMatches(
annotation,
owner,
method,
new Set(['org.springframework.transaction.annotation.Transactional']),
),
).toBe(true);
});
it('treats an unmodified interface method as public for execution(public ...)', () => {
const interfaceOwner: GraphNode = { ...owner, label: 'Interface' };
const implicitPublicMethod: GraphNode = {
...method,
properties: { ...method.properties, visibility: 'package' },
};
const execution = parseSpringAopPointcut(
'execution(public * com.example..OrderService.run(*))',
);
expect(
execution && springAopPointcutMatches(execution, interfaceOwner, implicitPublicMethod),
).toBe(true);
expect(execution && springAopPointcutMatches(execution, owner, implicitPublicMethod)).toBe(
false,
);
});
it('matches unqualified wildcard type patterns against the owner simple name', () => {
const within = parseSpringAopPointcut('within(*Service)');
const execution = parseSpringAopPointcut('execution(* *Service.run(..))');
expect(within && springAopPointcutMatches(within, owner, method)).toBe(true);
expect(execution && springAopPointcutMatches(execution, owner, method)).toBe(true);
});
it.each(['within(OrderService)', 'execution(* OrderService.run(..))'])(
'fails closed when a simple exact type needs unavailable import context: %s',
(expression) => {
expect(parseSpringAopPointcut(expression)).toBeNull();
},
);
it.each([
'namedPointcut()',
'execution(* com.example..OrderService.*(..)) && args(orderId)',
'execution(String com.example.OrderService.run(..))',
'@annotation(com.example.DynamicMarker)',
'bean(orderService)',
])('fails closed for unsupported or dynamic expression %s', (expression) => {
expect(parseSpringAopPointcut(expression)).toBeNull();
});
it('rejects overlong input instead of truncating away a dynamic suffix', () => {
const start = 'execution(* ';
const end = '.run(..))';
const validPrefix = `${start}${'a'.repeat(1_000 - start.length - end.length)}${end}`;
expect(validPrefix).toHaveLength(1_000);
expect(parseSpringAopPointcut(`${validPrefix} && args(value)`)).toBeNull();
});
it('matches adversarial wildcard input without regex backtracking', () => {
const wildcardChain = '*a'.repeat(10);
const execution = parseSpringAopPointcut(
`execution(* com.example..OrderService.${wildcardChain}z(..))`,
);
const within = parseSpringAopPointcut(`within(com.example.${wildcardChain}z)`);
const adversarialMethod = {
...method,
properties: { ...method.properties, name: 'a'.repeat(24) },
};
const adversarialOwner = {
...owner,
properties: {
...owner.properties,
qualifiedName: `com.example.${'a'.repeat(24)}`,
},
};
const startedAt = performance.now();
expect(execution && springAopPointcutMatches(execution, owner, adversarialMethod)).toBe(false);
expect(within && springAopPointcutMatches(within, adversarialOwner, method)).toBe(false);
expect(performance.now() - startedAt).toBeLessThan(100);
});
});
describe('Spring AOP pointcut candidate index (#2416)', () => {
it('selects only methods that directly declare an @annotation behavior', () => {
const annotated = { method, owner } satisfies SpringAopOwnedMethod;
const plain = {
owner,
method: {
...method,
id: 'Method:com.example.service.OrderService.plain',
properties: { ...method.properties, name: 'plain' },
},
} satisfies SpringAopOwnedMethod;
const pointcut = parseSpringAopPointcut(
'@annotation(org.springframework.transaction.annotation.Transactional)',
);
expect(pointcut).not.toBeNull();
const index = createSpringAopCandidateIndex(
[annotated, plain],
new Map([[method.id, new Set(['org.springframework.transaction.annotation.Transactional'])]]),
);
expect(index.candidatesFor(pointcut!)).toEqual([annotated]);
});
it('narrows exact and literal-prefix owner pointcuts without excluding matches', () => {
const ownedMethod = (qualifiedName: string): SpringAopOwnedMethod => {
const ownerName = qualifiedName.slice(qualifiedName.lastIndexOf('.') + 1);
return {
owner: {
...owner,
id: `Class:${qualifiedName}`,
properties: { ...owner.properties, name: ownerName, qualifiedName },
},
method: {
...method,
id: `Method:${qualifiedName}.run`,
properties: { ...method.properties, qualifiedName: `${qualifiedName}.run` },
},
};
};
const order = ownedMethod('com.example.service.OrderService');
const payment = ownedMethod('com.example.billing.PaymentService');
const legacy = ownedMethod('org.legacy.LegacyService');
const candidates = [order, payment, legacy];
const index = createSpringAopCandidateIndex(candidates, new Map());
const exact = parseSpringAopPointcut('within(com.example.service.OrderService)');
const prefixed = parseSpringAopPointcut('within(com.example..*Service)');
const leadingWildcard = parseSpringAopPointcut('within(*..*Service)');
expect(exact && index.candidatesFor(exact)).toEqual([order]);
expect(prefixed && index.candidatesFor(prefixed)).toEqual([payment, order]);
expect(leadingWildcard && index.candidatesFor(leadingWildcard)).toEqual(candidates);
});
it('preserves brute-force results across exact, wildcard, descendant, and annotation pointcuts', () => {
const ownedMethod = (
qualifiedName: string,
methodName: string,
filePath: string,
): SpringAopOwnedMethod => ({
owner: {
...owner,
id: `Class:${filePath}:${qualifiedName}`,
properties: { ...owner.properties, qualifiedName, filePath },
},
method: {
...method,
id: `Method:${filePath}:${qualifiedName}.${methodName}`,
properties: {
...method.properties,
name: methodName,
qualifiedName: `${qualifiedName}.${methodName}`,
filePath,
},
},
});
const candidates = [
ownedMethod('com.example.service.OrderService', 'run', 'OrderService.java'),
ownedMethod('com.example.service.OrderService', 'read', 'OrderService.kt'),
ownedMethod('com.example.billing.InvoiceService', 'run', 'InvoiceService.kt'),
ownedMethod('org.legacy.LegacyService', 'run', 'LegacyService.java'),
];
const transactional = 'org.springframework.transaction.annotation.Transactional';
const methodAnnotations = new Map([[candidates[1]!.method.id, new Set([transactional])]]);
const index = createSpringAopCandidateIndex(candidates, methodAnnotations);
const expressions = [
'within(com.example.service.OrderService)',
'within(com.example..*Service)',
'within(*..*Service)',
'execution(public * com.example..*Service.r*( * ))',
`@annotation(${transactional})`,
];
for (const expression of expressions) {
const pointcut = parseSpringAopPointcut(expression);
expect(pointcut, expression).not.toBeNull();
const matchingIds = (pool: readonly SpringAopOwnedMethod[]) =>
pool
.filter((candidate) =>
springAopPointcutMatches(
pointcut!,
candidate.owner,
candidate.method,
methodAnnotations.get(candidate.method.id),
),
)
.map((candidate) => candidate.method.id)
.sort();
expect(matchingIds(index.candidatesFor(pointcut!)), expression).toEqual(
matchingIds(candidates),
);
}
});
});
describe('Spring AOP persisted reason contract (#2416)', () => {
const reasons: readonly SpringAopReason[] = [
{
kind: 'behavior',
annotation: 'org.springframework.transaction.annotation.Transactional',
behavior: 'transactional',
declaredOn: 'method',
activation: 'unknown',
proxy: 'possible',
},
{
kind: 'behavior',
annotation: 'org.springframework.cache.annotation.Caching',
behavior: 'caching',
declaredOn: 'method',
activation: 'unknown',
proxy: 'possible',
},
{
kind: 'advice',
annotation: 'org.aspectj.lang.annotation.Around',
advice: 'around',
pointcut: 'execution(* com.example..OrderService.*(..))',
match: 'static',
activation: 'unknown',
proxy: 'possible',
},
{
kind: 'pointcut',
annotation: 'org.aspectj.lang.annotation.Before',
pointcut: 'namedPointcut()',
match: 'unresolved',
resolution: 'unknown',
},
{
kind: 'pointcut',
annotation: 'org.aspectj.lang.annotation.After',
pointcut: null,
match: 'unresolved',
resolution: 'unknown',
},
{
kind: 'aspect',
annotation: 'org.aspectj.lang.annotation.Aspect',
activation: 'unknown',
registration: 'unknown',
},
];
it.each(reasons)('round-trips the $kind reason', (reason) => {
expect(decodeSpringAopReason(encodeSpringAopReason(reason))).toEqual(reason);
});
it.each([
'spring-aop:v2:{}',
'spring-aop:v1:not-json',
`spring-aop:v1:${JSON.stringify({
kind: 'behavior',
annotation: 'com.example.FakeTransactional',
behavior: 'transactional',
declaredOn: 'method',
activation: 'unknown',
proxy: 'possible',
})}`,
`spring-aop:v1:${JSON.stringify({
kind: 'pointcut',
annotation: 'org.aspectj.lang.annotation.Before',
pointcut: 'execution(* com.example.Service.run(..))',
match: 'static',
resolution: 'unknown',
})}`,
])('rejects malformed, foreign, or internally inconsistent reason %s', (reason) => {
expect(decodeSpringAopReason(reason)).toBeUndefined();
});
it('identifies owned evidence by its stable ID namespace, not mutable prose', () => {
expect(
isSpringAopEvidenceNode({
...method,
id: 'CodeElement:spring-aop:evidence',
label: 'CodeElement',
}),
).toBe(true);
expect(
isSpringAopEvidenceNode({
...method,
id: 'CodeElement:ordinary',
label: 'CodeElement',
properties: { ...method.properties, description: 'Spring AOP: lookalike' },
}),
).toBe(false);
});
});