GitNexus/gitnexus/test/unit/spring-annotation-arguments.test.ts
azizur100389 1fa751d76d
fix(spring): extract method-level RequestMapping routes (#2857)
* fix(spring): extract RequestMapping route methods

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(spring): address RequestMapping review findings

* fix(spring): accept trivia in request methods

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-08-07 11:43:19 +01:00

262 lines
9.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
normalizeSpringBeanType,
parseSpringAnnotationArguments,
parseStaticClassLiteral,
parseStaticStringLiteral,
parseStaticStringValues,
} from '../../src/core/ingestion/frameworks/spring/annotation-arguments.js';
import {
decodeSpringBeanFactoryReason,
encodeSpringBeanFactoryReason,
springBeanNames,
} from '../../src/core/ingestion/frameworks/spring/bean-factories.js';
import {
springResourceDefaultName,
springResourceInjectionMatch,
} from '../../src/core/ingestion/frameworks/spring/resource-injection.js';
import {
intersectSpringHttpMethods,
springAnnotationHttpMethods,
} from '../../src/core/ingestion/route-extractors/spring-shared.js';
describe('Spring annotation static arguments', () => {
it('parses Java and Kotlin named arrays without splitting nested values', () => {
expect(
parseSpringAnnotationArguments('@Bean(name = {"one", "two"}, value = ["three"])'),
).toEqual([
{ name: 'name', value: '{"one", "two"}' },
{ name: 'value', value: '["three"]' },
]);
expect(parseStaticStringValues('["one", "two"]')).toEqual(['one', 'two']);
expect(parseStaticStringValues('NAMES')).toBeNull();
});
it('keeps generic call expressions intact while splitting top-level arguments', () => {
expect(
parseSpringAnnotationArguments(
'@Bean(factory = helper<Map<String, Int>>(left, right), name = ["one", "two"])',
),
).toEqual([
{ name: 'factory', value: 'helper<Map<String, Int>>(left, right)' },
{ name: 'name', value: '["one", "two"]' },
]);
});
it('rejects Kotlin templates and accepts Java/Kotlin class literals', () => {
expect(parseStaticStringValues('"bean-${suffix}"')).toBeNull();
expect(parseStaticClassLiteral('Concrete.class')).toBe('Concrete');
expect(parseStaticClassLiteral('Concrete::class')).toBe('Concrete');
expect(parseStaticClassLiteral('Object.class')).toBe('');
});
it('parses static Kotlin raw strings and rejects raw string templates', () => {
expect(parseStaticStringLiteral('"""within(com.example.service.KotlinOrderService)"""')).toBe(
'within(com.example.service.KotlinOrderService)',
);
expect(parseStaticStringValues('["one", """two, three"""]')).toEqual(['one', 'two, three']);
expect(parseStaticStringLiteral('"""bean-$name"""')).toBeNull();
expect(parseStaticStringLiteral('"""bean-${expression}"""')).toBeNull();
expect(parseStaticStringValues('["""static""", """$name"""]')).toBeNull();
});
it('keeps commas, assignments, and ordinary quotes inside Kotlin raw arguments', () => {
expect(
parseSpringAnnotationArguments(
'@AfterReturning(pointcut = """execution(* com.example.Service.run(*,*)) && args("quoted=value")""", returning = "result")',
),
).toEqual([
{
name: 'pointcut',
value: '"""execution(* com.example.Service.run(*,*)) && args("quoted=value")"""',
},
{ name: 'returning', value: '"result"' },
]);
});
it('normalizes Kotlin nullability, mutable collections, projections, and bean generics', () => {
expect(normalizeSpringBeanType('MutableList<out Gateway?>?')).toBe('List');
expect(normalizeSpringBeanType('com.example.Gateway')).toBe('com.example.Gateway');
expect(normalizeSpringBeanType('outputStream')).toBe('outputStream');
expect(normalizeSpringBeanType('inside.Type')).toBe('inside.Type');
expect(normalizeSpringBeanType('Unit')).toBeNull();
});
});
describe('Spring request mapping methods', () => {
it('resolves shortcut, wildcard, scalar, and array method declarations', () => {
expect(springAnnotationHttpMethods('GetMapping', '@GetMapping("/x")')).toEqual(['GET']);
expect(springAnnotationHttpMethods('RequestMapping', '@RequestMapping("/x")')).toEqual(['*']);
expect(
springAnnotationHttpMethods(
'RequestMapping',
'@RequestMapping(path = "/x", method = RequestMethod.POST)',
),
).toEqual(['POST']);
expect(
springAnnotationHttpMethods(
'RequestMapping',
'@RequestMapping(path = "/x", method = {RequestMethod.GET, RequestMethod.HEAD})',
),
).toEqual(['GET', 'HEAD']);
expect(
springAnnotationHttpMethods('RequestMapping', '@RequestMapping(path = "/x", method = {})'),
).toEqual(['*']);
});
it('accepts a terminal array comma and intersects class/method constraints', () => {
expect(
springAnnotationHttpMethods(
'RequestMapping',
'@RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD,})',
),
).toEqual(['GET', 'HEAD']);
expect(intersectSpringHttpMethods(['GET'], ['*'])).toEqual(['GET']);
expect(intersectSpringHttpMethods(['*'], ['POST'])).toEqual(['POST']);
expect(intersectSpringHttpMethods(['GET', 'HEAD'], ['HEAD', 'POST'])).toEqual(['HEAD']);
expect(intersectSpringHttpMethods(['GET'], ['POST'])).toEqual([]);
});
it('accepts Java whitespace and comments around static RequestMethod values', () => {
expect(
springAnnotationHttpMethods(
'RequestMapping',
'@RequestMapping(path = "/x", method = RequestMethod . GET)',
),
).toEqual(['GET']);
expect(
springAnnotationHttpMethods(
'RequestMapping',
`@RequestMapping(method = {
RequestMethod.GET, // read
/* write */ RequestMethod.POST,
})`,
),
).toEqual(['GET', 'POST']);
});
it('fails closed for runtime, malformed, and duplicate method members', () => {
expect(
springAnnotationHttpMethods('RequestMapping', '@RequestMapping(path = "/x", method = VERB)'),
).toEqual([]);
expect(
springAnnotationHttpMethods(
'RequestMapping',
'@RequestMapping(path = "/x", method = RequestMethod.GET, method = RequestMethod.POST)',
),
).toEqual([]);
expect(
springAnnotationHttpMethods(
'RequestMapping',
'@RequestMapping(path = "/x", method = RequestMethod./* unterminated)',
),
).toEqual([]);
});
});
describe('Spring Bean factory metadata', () => {
it('uses the method default and recognizes Java/Kotlin aliases', () => {
expect(springBeanNames('@Bean', 'gateway')).toEqual({
names: ['gateway'],
namesKnown: true,
});
expect(springBeanNames('@Bean({"primary", "alias"})', 'gateway')).toEqual({
names: ['primary', 'alias'],
namesKnown: true,
});
expect(springBeanNames('@Bean(name = ["primary", "alias"])', 'gateway')).toEqual({
names: ['primary', 'alias'],
namesKnown: true,
});
expect(springBeanNames('@Bean(name = "")', 'gateway')).toEqual({
names: ['gateway'],
namesKnown: true,
});
expect(springBeanNames('@Bean(name = NAMES)', 'gateway')).toEqual({
names: [],
namesKnown: false,
});
});
it('round-trips the persisted DECLARES reason', () => {
const reason = encodeSpringBeanFactoryReason({
names: ['gateway', 'alias'],
namesKnown: true,
providedType: 'Gateway',
});
expect(decodeSpringBeanFactoryReason(reason)).toEqual({
framework: 'spring',
role: 'factory-method',
annotation: 'org.springframework.context.annotation.Bean',
names: ['gateway', 'alias'],
providedType: 'Gateway',
});
});
});
describe('Spring Resource semantics', () => {
it('derives field/property and JavaBeans setter names', () => {
expect(springResourceDefaultName('field', 'gateway', 1)).toBe('gateway');
expect(springResourceDefaultName('method', 'setGateway', 1)).toBe('gateway');
expect(springResourceDefaultName('method', 'setURL', 1)).toBe('URL');
expect(springResourceDefaultName('method', 'configure', 1)).toBeNull();
expect(springResourceDefaultName('method', 'setGateway', 2)).toBeNull();
});
it('keeps explicit names strict and default names type-fallback capable', () => {
const explicit = springResourceInjectionMatch(
'@Resource(name = "slowGateway")',
'gateway',
'Gateway',
'gateway',
);
expect(explicit).toMatchObject({
targetTypeName: 'Gateway',
cardinality: 'single',
namedSelection: { name: 'slowGateway' },
});
expect(explicit?.namedSelection).not.toHaveProperty('fallbackToType');
expect(
springResourceInjectionMatch('@Resource', 'gateway', 'Gateway', 'gateway'),
).toMatchObject({
namedSelection: { name: 'gateway', fallbackToType: true },
});
});
it('does not type-fallback an implicit-name generic Resource site', () => {
const generic = springResourceInjectionMatch(
'@Resource',
'handlers',
'List<Handler>',
'handlers',
);
expect(generic).toMatchObject({
targetTypeName: 'List',
namedSelection: { name: 'handlers' },
});
expect(generic?.namedSelection).not.toHaveProperty('fallbackToType');
});
it('honors static type overrides and rejects runtime-only metadata', () => {
expect(
springResourceInjectionMatch('@Resource(type = Concrete::class)', 'repo', 'Any', 'repo'),
).toMatchObject({ targetTypeName: 'Concrete' });
expect(
springResourceInjectionMatch(
'@Resource(lookup = "java:global/repo")',
'repo',
'Concrete',
'repo',
),
).toBeNull();
expect(
springResourceInjectionMatch(
'@Resource(mappedName = MAPPED_NAME)',
'repo',
'Concrete',
'repo',
),
).toBeNull();
});
});