Files
puaros/packages/guardian/tests/unit/infrastructure/NamingConventionDetector.test.ts

676 lines
23 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest"
import { NamingConventionDetector } from "../../../src/infrastructure/analyzers/NamingConventionDetector"
import { LAYERS, NAMING_VIOLATION_TYPES } from "../../../src/shared/constants"
describe("NamingConventionDetector - AST-based", () => {
let detector: NamingConventionDetector
beforeEach(() => {
detector = new NamingConventionDetector()
})
describe("Excluded Files", () => {
it("should NOT detect violations for index.ts", () => {
const code = `export class User {}`
const result = detector.detectViolations(
code,
"index.ts",
LAYERS.DOMAIN,
"src/domain/index.ts",
)
expect(result).toHaveLength(0)
})
it("should NOT detect violations for BaseUseCase.ts", () => {
const code = `export class BaseUseCase {}`
const result = detector.detectViolations(
code,
"BaseUseCase.ts",
LAYERS.APPLICATION,
"src/application/use-cases/BaseUseCase.ts",
)
expect(result).toHaveLength(0)
})
it("should NOT detect violations for empty content", () => {
const result = detector.detectViolations(
"",
"User.ts",
LAYERS.DOMAIN,
"src/domain/User.ts",
)
expect(result).toHaveLength(0)
})
})
describe("Domain Layer - Classes", () => {
it("should NOT detect violations for valid entity class names", () => {
const validClasses = ["User", "Order", "Product", "Email", "ProjectPath"]
validClasses.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.DOMAIN,
`src/domain/entities/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for lowercase class names", () => {
const code = `export class user {}`
const result = detector.detectViolations(
code,
"user.ts",
LAYERS.DOMAIN,
"src/domain/entities/user.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
expect(result[0].layer).toBe(LAYERS.DOMAIN)
})
it("should detect violations for camelCase class names", () => {
const code = `export class userProfile {}`
const result = detector.detectViolations(
code,
"userProfile.ts",
LAYERS.DOMAIN,
"src/domain/entities/userProfile.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
})
it("should NOT detect violations for valid service class names", () => {
const validNames = ["UserService", "EmailService", "PaymentService"]
validNames.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.DOMAIN,
`src/domain/services/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for lowercase service class names", () => {
const code = `export class userService {}`
const result = detector.detectViolations(
code,
"userService.ts",
LAYERS.DOMAIN,
"src/domain/services/userService.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
})
})
describe("Domain Layer - Interfaces", () => {
it("should NOT detect violations for valid repository interface names", () => {
const validNames = ["IUserRepository", "IOrderRepository", "IProductRepository"]
validNames.forEach((interfaceName) => {
const code = `export interface ${interfaceName} {}`
const result = detector.detectViolations(
code,
`${interfaceName}.ts`,
LAYERS.DOMAIN,
`src/domain/repositories/${interfaceName}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for repository interfaces without I prefix", () => {
const code = `export interface UserRepository {}`
const result = detector.detectViolations(
code,
"UserRepository.ts",
LAYERS.DOMAIN,
"src/domain/repositories/UserRepository.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_PREFIX)
})
it("should detect violations for lowercase interface names", () => {
const code = `export interface iUserRepository {}`
const result = detector.detectViolations(
code,
"iUserRepository.ts",
LAYERS.DOMAIN,
"src/domain/repositories/iUserRepository.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
})
})
describe("Application Layer - Classes", () => {
it("should NOT detect violations for valid use case class names", () => {
const validNames = [
"CreateUser",
"UpdateProfile",
"DeleteOrder",
"GetUser",
"FindProducts",
"AnalyzeProject",
"ValidateEmail",
"GenerateReport",
]
validNames.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.APPLICATION,
`src/application/use-cases/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for use case classes starting with lowercase", () => {
const code = `export class createUser {}`
const result = detector.detectViolations(
code,
"createUser.ts",
LAYERS.APPLICATION,
"src/application/use-cases/createUser.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN)
})
it("should NOT detect violations for valid DTO class names", () => {
const validNames = [
"UserDto",
"CreateUserRequest",
"UserResponseDto",
"UpdateProfileRequest",
"OrderResponse",
]
validNames.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.APPLICATION,
`src/application/dtos/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for lowercase DTO class names", () => {
const code = `export class userDto {}`
const result = detector.detectViolations(
code,
"userDto.ts",
LAYERS.APPLICATION,
"src/application/dtos/userDto.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
})
it("should NOT detect violations for valid mapper class names", () => {
const validNames = ["UserMapper", "OrderMapper", "ProductMapper"]
validNames.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.APPLICATION,
`src/application/mappers/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for lowercase mapper class names", () => {
const code = `export class userMapper {}`
const result = detector.detectViolations(
code,
"userMapper.ts",
LAYERS.APPLICATION,
"src/application/mappers/userMapper.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
})
})
describe("Infrastructure Layer - Classes", () => {
it("should NOT detect violations for valid controller class names", () => {
const validNames = ["UserController", "OrderController", "ProductController"]
validNames.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.INFRASTRUCTURE,
`src/infrastructure/controllers/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for lowercase controller class names", () => {
const code = `export class userController {}`
const result = detector.detectViolations(
code,
"userController.ts",
LAYERS.INFRASTRUCTURE,
"src/infrastructure/controllers/userController.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
})
it("should NOT detect violations for valid repository implementation class names", () => {
const validNames = [
"UserRepository",
"PrismaUserRepository",
"MongoUserRepository",
"InMemoryUserRepository",
]
validNames.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.INFRASTRUCTURE,
`src/infrastructure/repositories/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for lowercase repository class names", () => {
const code = `export class userRepository {}`
const result = detector.detectViolations(
code,
"userRepository.ts",
LAYERS.INFRASTRUCTURE,
"src/infrastructure/repositories/userRepository.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
})
it("should NOT detect violations for valid service class names", () => {
const validNames = [
"EmailService",
"S3StorageAdapter",
"PaymentService",
"LoggerAdapter",
]
validNames.forEach((className) => {
const code = `export class ${className} {}`
const result = detector.detectViolations(
code,
`${className}.ts`,
LAYERS.INFRASTRUCTURE,
`src/infrastructure/services/${className}.ts`,
)
expect(result).toHaveLength(0)
})
})
it("should detect violations for lowercase service class names", () => {
const code = `export class emailService {}`
const result = detector.detectViolations(
code,
"emailService.ts",
LAYERS.INFRASTRUCTURE,
"src/infrastructure/services/emailService.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
})
})
describe("Function and Method Names", () => {
it("should NOT detect violations for camelCase function names", () => {
const code = `
export class User {
getUserById() {}
createOrder() {}
validateEmail() {}
}
`
const result = detector.detectViolations(
code,
"User.ts",
LAYERS.DOMAIN,
"src/domain/User.ts",
)
expect(result).toHaveLength(0)
})
it("should detect violations for PascalCase method names", () => {
const code = `
export class User {
GetUserById() {}
}
`
const result = detector.detectViolations(
code,
"User.ts",
LAYERS.DOMAIN,
"src/domain/User.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
})
it("should detect violations for snake_case method names", () => {
const code = `
export class User {
get_user_by_id() {}
}
`
const result = detector.detectViolations(
code,
"User.ts",
LAYERS.DOMAIN,
"src/domain/User.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
})
it("should NOT detect violations for constructor", () => {
const code = `
export class User {
constructor() {}
}
`
const result = detector.detectViolations(
code,
"User.ts",
LAYERS.DOMAIN,
"src/domain/User.ts",
)
expect(result).toHaveLength(0)
})
})
describe("Variable and Constant Names", () => {
it("should NOT detect violations for camelCase variables", () => {
const code = `
export class User {
getUserById() {
const userId = "123"
const userName = "John"
return { userId, userName }
}
}
`
const result = detector.detectViolations(
code,
"User.ts",
LAYERS.DOMAIN,
"src/domain/User.ts",
)
expect(result).toHaveLength(0)
})
it("should NOT detect violations for UPPER_SNAKE_CASE constants", () => {
const code = `
export const MAX_RETRIES = 3
export const API_URL = "https://api.example.com"
export const DEFAULT_TIMEOUT = 5000
`
const result = detector.detectViolations(
code,
"constants.ts",
LAYERS.SHARED,
"src/shared/constants.ts",
)
expect(result).toHaveLength(0)
})
it("should NOT detect violations for static readonly UPPER_SNAKE_CASE class constants", () => {
const code = `
export class ValuePatternMatcher {
private static readonly EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
private static readonly IP_V4_PATTERN = /^(\\d{1,3}\\.){3}\\d{1,3}$/
private static readonly API_KEY_PATTERN = /^(sk_|pk_|api_|key_)[a-zA-Z0-9_-]{20,}$/
}
`
const result = detector.detectViolations(
code,
"ValuePatternMatcher.ts",
LAYERS.INFRASTRUCTURE,
"src/infrastructure/ValuePatternMatcher.ts",
)
expect(result).toHaveLength(0)
})
it("should NOT detect violations for readonly UPPER_SNAKE_CASE class constants", () => {
const code = `
export class AstConfigObjectAnalyzer {
private readonly MIN_HARDCODED_VALUES = 2
private readonly MAX_COMPLEXITY = 15
}
`
const result = detector.detectViolations(
code,
"AstConfigObjectAnalyzer.ts",
LAYERS.INFRASTRUCTURE,
"src/infrastructure/AstConfigObjectAnalyzer.ts",
)
expect(result).toHaveLength(0)
})
it("should detect violations for PascalCase variables", () => {
const code = `
export class User {
getUserById() {
const UserId = "123"
return UserId
}
}
`
const result = detector.detectViolations(
code,
"User.ts",
LAYERS.DOMAIN,
"src/domain/User.ts",
)
expect(result).toHaveLength(1)
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
})
})
describe("Shared Layer", () => {
it("should NOT detect violations for any file in shared layer", () => {
const code = `
export class Guards {}
export class Result {}
export function helper() {}
`
const fileNames = [
"helpers.ts",
"utils.ts",
"constants.ts",
"types.ts",
"Guards.ts",
"Result.ts",
]
fileNames.forEach((fileName) => {
const result = detector.detectViolations(
code,
fileName,
LAYERS.SHARED,
`src/shared/${fileName}`,
)
expect(result.length).toBeLessThanOrEqual(code.length)
})
})
})
describe("Edge Cases", () => {
it("should return empty array when no layer is provided", () => {
const code = `export class SomeClass {}`
const result = detector.detectViolations(
code,
"SomeFile.ts",
undefined,
"src/SomeFile.ts",
)
expect(result).toHaveLength(0)
})
it("should return empty array for unknown layer", () => {
const code = `export class SomeClass {}`
const result = detector.detectViolations(
code,
"SomeFile.ts",
"unknown-layer",
"src/unknown/SomeFile.ts",
)
expect(result.length).toBeGreaterThanOrEqual(0)
})
it("should handle classes with numbers in name", () => {
const code = `export class User2Factor {}`
const result = detector.detectViolations(
code,
"User2Factor.ts",
LAYERS.DOMAIN,
"src/domain/entities/User2Factor.ts",
)
expect(result).toHaveLength(0)
})
it("should provide helpful suggestions", () => {
const code = `export class userDto {}`
const result = detector.detectViolations(
code,
"userDto.ts",
LAYERS.APPLICATION,
"src/application/dtos/userDto.ts",
)
expect(result).toHaveLength(1)
expect(result[0].suggestion).toBeDefined()
})
it("should include file path in violation", () => {
const code = `export class user {}`
const filePath = "src/domain/user.ts"
const result = detector.detectViolations(code, "user.ts", LAYERS.DOMAIN, filePath)
expect(result).toHaveLength(1)
expect(result[0].filePath).toContain(filePath)
})
})
describe("Complex Scenarios", () => {
it("should detect multiple violations in one file", () => {
const code = `
export class userService {
GetUser() {
const UserId = "123"
return UserId
}
}
`
const result = detector.detectViolations(
code,
"userService.ts",
LAYERS.DOMAIN,
"src/domain/services/userService.ts",
)
expect(result.length).toBeGreaterThan(0)
})
it("should handle multiple classes in one file", () => {
const code = `
export class User {}
export class UserService {}
`
const result = detector.detectViolations(
code,
"User.ts",
LAYERS.DOMAIN,
"src/domain/entities/User.ts",
)
expect(result.length).toBeGreaterThanOrEqual(0)
})
it("should handle interfaces and classes together", () => {
const code = `
export interface IUserRepository {}
export class UserService {}
`
const result = detector.detectViolations(
code,
"UserRepository.ts",
LAYERS.DOMAIN,
"src/domain/repositories/UserRepository.ts",
)
expect(result.length).toBeGreaterThanOrEqual(0)
})
})
describe("getMessage()", () => {
it("should return descriptive error messages", () => {
const code = `export class userService {}`
const result = detector.detectViolations(
code,
"userService.ts",
LAYERS.DOMAIN,
"src/domain/userService.ts",
)
expect(result).toHaveLength(1)
const message = result[0].getMessage()
expect(message).toBeTruthy()
expect(typeof message).toBe("string")
expect(message.length).toBeGreaterThan(0)
})
})
})