mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
Implement dependency direction detection to enforce Clean Architecture rules: - Domain layer can only import from Domain and Shared - Application layer can only import from Domain, Application, and Shared - Infrastructure layer can import from all layers - Shared layer can be imported by all layers Added: - IDependencyDirectionDetector interface in domain layer - DependencyViolation value object with detailed suggestions and examples - DependencyDirectionDetector implementation in infrastructure - Integration with AnalyzeProject use case - New DEPENDENCY_DIRECTION rule in constants - 43 comprehensive tests covering all scenarios (100% passing) - Good and bad examples in examples directory Improvements: - Optimized extractLayerFromImport method to reduce complexity - Fixed indentation in DependencyGraph.ts - Updated getExampleFix to avoid false positives in old detector Test Results: - All 261 tests passing - Build successful - Self-check: 0 architecture violations in src code
512 lines
20 KiB
TypeScript
512 lines
20 KiB
TypeScript
import { describe, it, expect } from "vitest"
|
|
import { DependencyDirectionDetector } from "../src/infrastructure/analyzers/DependencyDirectionDetector"
|
|
import { LAYERS } from "../src/shared/constants/rules"
|
|
|
|
describe("DependencyDirectionDetector", () => {
|
|
const detector = new DependencyDirectionDetector()
|
|
|
|
describe("extractLayerFromImport", () => {
|
|
it("should extract domain layer from import path", () => {
|
|
expect(detector.extractLayerFromImport("../domain/entities/User")).toBe(LAYERS.DOMAIN)
|
|
expect(detector.extractLayerFromImport("../../domain/value-objects/Email")).toBe(
|
|
LAYERS.DOMAIN,
|
|
)
|
|
expect(detector.extractLayerFromImport("../../../domain/services/UserService")).toBe(
|
|
LAYERS.DOMAIN,
|
|
)
|
|
})
|
|
|
|
it("should extract application layer from import path", () => {
|
|
expect(detector.extractLayerFromImport("../application/use-cases/CreateUser")).toBe(
|
|
LAYERS.APPLICATION,
|
|
)
|
|
expect(detector.extractLayerFromImport("../../application/dtos/UserDto")).toBe(
|
|
LAYERS.APPLICATION,
|
|
)
|
|
expect(detector.extractLayerFromImport("../../../application/mappers/UserMapper")).toBe(
|
|
LAYERS.APPLICATION,
|
|
)
|
|
})
|
|
|
|
it("should extract infrastructure layer from import path", () => {
|
|
expect(
|
|
detector.extractLayerFromImport("../infrastructure/controllers/UserController"),
|
|
).toBe(LAYERS.INFRASTRUCTURE)
|
|
expect(
|
|
detector.extractLayerFromImport("../../infrastructure/repositories/UserRepository"),
|
|
).toBe(LAYERS.INFRASTRUCTURE)
|
|
})
|
|
|
|
it("should extract shared layer from import path", () => {
|
|
expect(detector.extractLayerFromImport("../shared/types/Result")).toBe(LAYERS.SHARED)
|
|
expect(detector.extractLayerFromImport("../../shared/constants/rules")).toBe(
|
|
LAYERS.SHARED,
|
|
)
|
|
})
|
|
|
|
it("should return undefined for non-layer imports", () => {
|
|
expect(detector.extractLayerFromImport("express")).toBeUndefined()
|
|
expect(detector.extractLayerFromImport("../utils/helper")).toBeUndefined()
|
|
expect(detector.extractLayerFromImport("../../lib/logger")).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("isViolation", () => {
|
|
it("should allow domain to import from domain", () => {
|
|
expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.DOMAIN)).toBe(false)
|
|
})
|
|
|
|
it("should allow domain to import from shared", () => {
|
|
expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.SHARED)).toBe(false)
|
|
})
|
|
|
|
it("should NOT allow domain to import from application", () => {
|
|
expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.APPLICATION)).toBe(true)
|
|
})
|
|
|
|
it("should NOT allow domain to import from infrastructure", () => {
|
|
expect(detector.isViolation(LAYERS.DOMAIN, LAYERS.INFRASTRUCTURE)).toBe(true)
|
|
})
|
|
|
|
it("should allow application to import from domain", () => {
|
|
expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.DOMAIN)).toBe(false)
|
|
})
|
|
|
|
it("should allow application to import from application", () => {
|
|
expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.APPLICATION)).toBe(false)
|
|
})
|
|
|
|
it("should allow application to import from shared", () => {
|
|
expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.SHARED)).toBe(false)
|
|
})
|
|
|
|
it("should NOT allow application to import from infrastructure", () => {
|
|
expect(detector.isViolation(LAYERS.APPLICATION, LAYERS.INFRASTRUCTURE)).toBe(true)
|
|
})
|
|
|
|
it("should allow infrastructure to import from domain", () => {
|
|
expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.DOMAIN)).toBe(false)
|
|
})
|
|
|
|
it("should allow infrastructure to import from application", () => {
|
|
expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.APPLICATION)).toBe(false)
|
|
})
|
|
|
|
it("should allow infrastructure to import from infrastructure", () => {
|
|
expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.INFRASTRUCTURE)).toBe(false)
|
|
})
|
|
|
|
it("should allow infrastructure to import from shared", () => {
|
|
expect(detector.isViolation(LAYERS.INFRASTRUCTURE, LAYERS.SHARED)).toBe(false)
|
|
})
|
|
|
|
it("should allow shared to import from any layer", () => {
|
|
expect(detector.isViolation(LAYERS.SHARED, LAYERS.DOMAIN)).toBe(false)
|
|
expect(detector.isViolation(LAYERS.SHARED, LAYERS.APPLICATION)).toBe(false)
|
|
expect(detector.isViolation(LAYERS.SHARED, LAYERS.INFRASTRUCTURE)).toBe(false)
|
|
expect(detector.isViolation(LAYERS.SHARED, LAYERS.SHARED)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("detectViolations", () => {
|
|
describe("Domain layer violations", () => {
|
|
it("should detect domain importing from application", () => {
|
|
const code = `
|
|
import { UserDto } from '../../application/dtos/UserDto'
|
|
|
|
export class User {
|
|
constructor(private id: string) {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(1)
|
|
expect(violations[0].fromLayer).toBe(LAYERS.DOMAIN)
|
|
expect(violations[0].toLayer).toBe(LAYERS.APPLICATION)
|
|
expect(violations[0].importPath).toBe("../../application/dtos/UserDto")
|
|
expect(violations[0].line).toBe(2)
|
|
})
|
|
|
|
it("should detect domain importing from infrastructure", () => {
|
|
const code = `
|
|
import { PrismaClient } from '../../infrastructure/database/PrismaClient'
|
|
|
|
export class UserRepository {
|
|
constructor(private prisma: PrismaClient) {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/repositories/UserRepository.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(1)
|
|
expect(violations[0].fromLayer).toBe(LAYERS.DOMAIN)
|
|
expect(violations[0].toLayer).toBe(LAYERS.INFRASTRUCTURE)
|
|
expect(violations[0].importPath).toBe("../../infrastructure/database/PrismaClient")
|
|
})
|
|
|
|
it("should NOT detect domain importing from domain", () => {
|
|
const code = `
|
|
import { Email } from '../value-objects/Email'
|
|
import { UserId } from '../value-objects/UserId'
|
|
|
|
export class User {
|
|
constructor(
|
|
private id: UserId,
|
|
private email: Email
|
|
) {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
|
|
it("should NOT detect domain importing from shared", () => {
|
|
const code = `
|
|
import { Result } from '../../shared/types/Result'
|
|
|
|
export class User {
|
|
static create(id: string): Result<User> {
|
|
return Result.ok(new User(id))
|
|
}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("Application layer violations", () => {
|
|
it("should detect application importing from infrastructure", () => {
|
|
const code = `
|
|
import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService'
|
|
|
|
export class SendWelcomeEmail {
|
|
constructor(private emailService: SmtpEmailService) {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/application/use-cases/SendWelcomeEmail.ts",
|
|
LAYERS.APPLICATION,
|
|
)
|
|
|
|
expect(violations).toHaveLength(1)
|
|
expect(violations[0].fromLayer).toBe(LAYERS.APPLICATION)
|
|
expect(violations[0].toLayer).toBe(LAYERS.INFRASTRUCTURE)
|
|
expect(violations[0].getMessage()).toContain(
|
|
"Application layer should not import from Infrastructure layer",
|
|
)
|
|
})
|
|
|
|
it("should NOT detect application importing from domain", () => {
|
|
const code = `
|
|
import { User } from '../../domain/entities/User'
|
|
import { IUserRepository } from '../../domain/repositories/IUserRepository'
|
|
|
|
export class CreateUser {
|
|
constructor(private userRepo: IUserRepository) {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/application/use-cases/CreateUser.ts",
|
|
LAYERS.APPLICATION,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
|
|
it("should NOT detect application importing from application", () => {
|
|
const code = `
|
|
import { UserResponseDto } from '../dtos/UserResponseDto'
|
|
import { UserMapper } from '../mappers/UserMapper'
|
|
|
|
export class GetUser {
|
|
execute(id: string): UserResponseDto {
|
|
return UserMapper.toDto(user)
|
|
}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/application/use-cases/GetUser.ts",
|
|
LAYERS.APPLICATION,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
|
|
it("should NOT detect application importing from shared", () => {
|
|
const code = `
|
|
import { Result } from '../../shared/types/Result'
|
|
|
|
export class CreateUser {
|
|
execute(): Result<User> {
|
|
return Result.ok(user)
|
|
}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/application/use-cases/CreateUser.ts",
|
|
LAYERS.APPLICATION,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("Infrastructure layer", () => {
|
|
it("should NOT detect infrastructure importing from domain", () => {
|
|
const code = `
|
|
import { User } from '../../domain/entities/User'
|
|
import { IUserRepository } from '../../domain/repositories/IUserRepository'
|
|
|
|
export class PrismaUserRepository implements IUserRepository {
|
|
async save(user: User): Promise<void> {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/infrastructure/repositories/PrismaUserRepository.ts",
|
|
LAYERS.INFRASTRUCTURE,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
|
|
it("should NOT detect infrastructure importing from application", () => {
|
|
const code = `
|
|
import { CreateUser } from '../../application/use-cases/CreateUser'
|
|
import { UserResponseDto } from '../../application/dtos/UserResponseDto'
|
|
|
|
export class UserController {
|
|
constructor(private createUser: CreateUser) {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/infrastructure/controllers/UserController.ts",
|
|
LAYERS.INFRASTRUCTURE,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
|
|
it("should NOT detect infrastructure importing from infrastructure", () => {
|
|
const code = `
|
|
import { DatabaseConnection } from '../database/DatabaseConnection'
|
|
|
|
export class PrismaUserRepository {
|
|
constructor(private db: DatabaseConnection) {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/infrastructure/repositories/PrismaUserRepository.ts",
|
|
LAYERS.INFRASTRUCTURE,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("Multiple violations", () => {
|
|
it("should detect multiple violations in same file", () => {
|
|
const code = `
|
|
import { UserDto } from '../../application/dtos/UserDto'
|
|
import { EmailService } from '../../infrastructure/email/EmailService'
|
|
import { Logger } from '../../infrastructure/logging/Logger'
|
|
|
|
export class User {
|
|
constructor() {}
|
|
}`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(3)
|
|
expect(violations[0].toLayer).toBe(LAYERS.APPLICATION)
|
|
expect(violations[1].toLayer).toBe(LAYERS.INFRASTRUCTURE)
|
|
expect(violations[2].toLayer).toBe(LAYERS.INFRASTRUCTURE)
|
|
})
|
|
})
|
|
|
|
describe("Import statement formats", () => {
|
|
it("should detect violations in named imports", () => {
|
|
const code = `import { UserDto, UserRequest } from '../../application/dtos/UserDto'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(1)
|
|
})
|
|
|
|
it("should detect violations in default imports", () => {
|
|
const code = `import UserDto from '../../application/dtos/UserDto'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(1)
|
|
})
|
|
|
|
it("should detect violations in namespace imports", () => {
|
|
const code = `import * as Dtos from '../../application/dtos'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(1)
|
|
})
|
|
|
|
it("should detect violations in require statements", () => {
|
|
const code = `const UserDto = require('../../application/dtos/UserDto')`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe("Edge cases", () => {
|
|
it("should return empty array for shared layer", () => {
|
|
const code = `
|
|
import { User } from '../../domain/entities/User'
|
|
import { CreateUser } from '../../application/use-cases/CreateUser'
|
|
`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/shared/types/Result.ts",
|
|
LAYERS.SHARED,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
|
|
it("should return empty array for undefined layer", () => {
|
|
const code = `import { UserDto } from '../../application/dtos/UserDto'`
|
|
const violations = detector.detectViolations(code, "src/utils/helper.ts", undefined)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
|
|
it("should handle empty code", () => {
|
|
const violations = detector.detectViolations(
|
|
"",
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("getMessage", () => {
|
|
it("should return correct message for domain -> application violation", () => {
|
|
const code = `import { UserDto } from '../../application/dtos/UserDto'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/entities/User.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
expect(violations[0].getMessage()).toBe(
|
|
"Domain layer should not import from Application layer",
|
|
)
|
|
})
|
|
|
|
it("should return correct message for application -> infrastructure violation", () => {
|
|
const code = `import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/application/use-cases/SendEmail.ts",
|
|
LAYERS.APPLICATION,
|
|
)
|
|
|
|
expect(violations[0].getMessage()).toBe(
|
|
"Application layer should not import from Infrastructure layer",
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("getSuggestion", () => {
|
|
it("should return suggestions for domain layer violations", () => {
|
|
const code = `import { PrismaClient } from '../../infrastructure/database'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/services/UserService.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
const suggestion = violations[0].getSuggestion()
|
|
expect(suggestion).toContain("Domain layer should be independent")
|
|
expect(suggestion).toContain("dependency inversion")
|
|
})
|
|
|
|
it("should return suggestions for application layer violations", () => {
|
|
const code = `import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/application/use-cases/SendEmail.ts",
|
|
LAYERS.APPLICATION,
|
|
)
|
|
|
|
const suggestion = violations[0].getSuggestion()
|
|
expect(suggestion).toContain(
|
|
"Application layer should not depend on infrastructure",
|
|
)
|
|
expect(suggestion).toContain("Port")
|
|
expect(suggestion).toContain("Adapter")
|
|
})
|
|
})
|
|
|
|
describe("getExampleFix", () => {
|
|
it("should return example fix for domain -> infrastructure violation", () => {
|
|
const code = `import { PrismaClient } from '../../infrastructure/database'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/domain/services/UserService.ts",
|
|
LAYERS.DOMAIN,
|
|
)
|
|
|
|
const example = violations[0].getExampleFix()
|
|
expect(example).toContain("// ❌ Bad")
|
|
expect(example).toContain("// ✅ Good")
|
|
expect(example).toContain("IUserRepository")
|
|
})
|
|
|
|
it("should return example fix for application -> infrastructure violation", () => {
|
|
const code = `import { SmtpEmailService } from '../../infrastructure/email/SmtpEmailService'`
|
|
const violations = detector.detectViolations(
|
|
code,
|
|
"src/application/use-cases/SendEmail.ts",
|
|
LAYERS.APPLICATION,
|
|
)
|
|
|
|
const example = violations[0].getExampleFix()
|
|
expect(example).toContain("// ❌ Bad")
|
|
expect(example).toContain("// ✅ Good")
|
|
expect(example).toContain("IEmailService")
|
|
})
|
|
})
|
|
})
|
|
})
|