mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat: add repository pattern validation (v0.5.0)
Add comprehensive Repository Pattern validation to detect violations and ensure proper domain-infrastructure separation. Features: - ORM type detection in repository interfaces (25+ patterns) - Concrete repository usage detection in use cases - Repository instantiation detection (new Repository()) - Domain language validation for repository methods - Smart violation reporting with fix suggestions Tests: - 31 new tests for repository pattern detection - 292 total tests passing (100% pass rate) - 96.77% statement coverage, 83.82% branch coverage Examples: - 8 example files (4 bad patterns, 4 good patterns) - Demonstrates Clean Architecture and SOLID principles
This commit is contained in:
515
packages/guardian/tests/RepositoryPatternDetector.test.ts
Normal file
515
packages/guardian/tests/RepositoryPatternDetector.test.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { RepositoryPatternDetector } from "../src/infrastructure/analyzers/RepositoryPatternDetector"
|
||||
import { REPOSITORY_VIOLATION_TYPES } from "../src/shared/constants/rules"
|
||||
|
||||
describe("RepositoryPatternDetector", () => {
|
||||
let detector: RepositoryPatternDetector
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new RepositoryPatternDetector()
|
||||
})
|
||||
|
||||
describe("detectViolations - ORM Types in Interface", () => {
|
||||
it("should detect Prisma types in repository interface", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
findOne(query: Prisma.UserWhereInput): Promise<User>
|
||||
create(data: Prisma.UserCreateInput): Promise<User>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations.length).toBeGreaterThan(0)
|
||||
const ormViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
|
||||
)
|
||||
expect(ormViolations.length).toBeGreaterThan(0)
|
||||
expect(ormViolations[0].getMessage()).toContain("ORM-specific type")
|
||||
})
|
||||
|
||||
it("should detect TypeORM decorators in repository interface", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
@Column()
|
||||
findById(id: string): Promise<User>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const ormViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
|
||||
)
|
||||
expect(ormViolations.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should detect Mongoose types in repository interface", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
find(query: Model<User>): Promise<User[]>
|
||||
findOne(query: Schema): Promise<User>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const ormViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
|
||||
)
|
||||
expect(ormViolations.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should not detect ORM types in clean interface", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
findById(id: UserId): Promise<User | null>
|
||||
save(user: User): Promise<void>
|
||||
delete(id: UserId): Promise<void>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const ormViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
|
||||
)
|
||||
expect(ormViolations).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectViolations - Concrete Repository in Use Case", () => {
|
||||
it("should detect concrete repository in constructor", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
constructor(private userRepo: PrismaUserRepository) {}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const concreteViolations = violations.filter(
|
||||
(v) =>
|
||||
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(concreteViolations).toHaveLength(1)
|
||||
expect(concreteViolations[0].repositoryName).toBe("PrismaUserRepository")
|
||||
})
|
||||
|
||||
it("should detect concrete repository as field", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
private userRepo: MongoUserRepository
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const concreteViolations = violations.filter(
|
||||
(v) =>
|
||||
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(concreteViolations).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should not detect interface in constructor", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
constructor(private userRepo: IUserRepository) {}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const concreteViolations = violations.filter(
|
||||
(v) =>
|
||||
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(concreteViolations).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectViolations - new Repository() in Use Case", () => {
|
||||
it("should detect repository instantiation with new", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
async execute(data: CreateUserRequest) {
|
||||
const repo = new UserRepository()
|
||||
await repo.save(user)
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const newRepoViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(newRepoViolations).toHaveLength(1)
|
||||
expect(newRepoViolations[0].repositoryName).toBe("UserRepository")
|
||||
})
|
||||
|
||||
it("should detect multiple repository instantiations", () => {
|
||||
const code = `
|
||||
class ComplexUseCase {
|
||||
async execute() {
|
||||
const userRepo = new UserRepository()
|
||||
const orderRepo = new OrderRepository()
|
||||
await userRepo.save(user)
|
||||
await orderRepo.save(order)
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/ComplexUseCase.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const newRepoViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(newRepoViolations).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should not detect commented out new Repository()", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
async execute(data: CreateUserRequest) {
|
||||
// const repo = new UserRepository()
|
||||
await this.userRepo.save(user)
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const newRepoViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(newRepoViolations).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectViolations - Non-Domain Method Names", () => {
|
||||
it("should detect technical method names", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
findOne(id: string): Promise<User>
|
||||
findMany(filter: any): Promise<User[]>
|
||||
insert(user: User): Promise<void>
|
||||
updateOne(id: string, data: any): Promise<void>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const methodViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
|
||||
)
|
||||
expect(methodViolations.length).toBeGreaterThan(0)
|
||||
expect(methodViolations.some((v) => v.methodName === "findOne")).toBe(true)
|
||||
expect(methodViolations.some((v) => v.methodName === "insert")).toBe(true)
|
||||
})
|
||||
|
||||
it("should not detect domain language method names", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
findById(id: UserId): Promise<User | null>
|
||||
findByEmail(email: Email): Promise<User | null>
|
||||
save(user: User): Promise<void>
|
||||
delete(id: UserId): Promise<void>
|
||||
search(criteria: SearchCriteria): Promise<User[]>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const methodViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
|
||||
)
|
||||
expect(methodViolations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect SQL terminology", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
select(id: string): Promise<User>
|
||||
query(filter: any): Promise<User[]>
|
||||
execute(sql: string): Promise<any>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const methodViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
|
||||
)
|
||||
expect(methodViolations.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isOrmType", () => {
|
||||
it("should identify Prisma types", () => {
|
||||
expect(detector.isOrmType("Prisma.UserWhereInput")).toBe(true)
|
||||
expect(detector.isOrmType("PrismaClient")).toBe(true)
|
||||
})
|
||||
|
||||
it("should identify TypeORM decorators", () => {
|
||||
expect(detector.isOrmType("@Entity")).toBe(true)
|
||||
expect(detector.isOrmType("@Column")).toBe(true)
|
||||
expect(detector.isOrmType("@ManyToOne")).toBe(true)
|
||||
})
|
||||
|
||||
it("should identify Mongoose types", () => {
|
||||
expect(detector.isOrmType("Schema")).toBe(true)
|
||||
expect(detector.isOrmType("Model<User>")).toBe(true)
|
||||
expect(detector.isOrmType("Document")).toBe(true)
|
||||
})
|
||||
|
||||
it("should not identify domain types", () => {
|
||||
expect(detector.isOrmType("User")).toBe(false)
|
||||
expect(detector.isOrmType("UserId")).toBe(false)
|
||||
expect(detector.isOrmType("Email")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDomainMethodName", () => {
|
||||
it("should identify domain method names", () => {
|
||||
expect(detector.isDomainMethodName("findById")).toBe(true)
|
||||
expect(detector.isDomainMethodName("findByEmail")).toBe(true)
|
||||
expect(detector.isDomainMethodName("save")).toBe(true)
|
||||
expect(detector.isDomainMethodName("delete")).toBe(true)
|
||||
expect(detector.isDomainMethodName("create")).toBe(true)
|
||||
expect(detector.isDomainMethodName("search")).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject technical method names", () => {
|
||||
expect(detector.isDomainMethodName("findOne")).toBe(false)
|
||||
expect(detector.isDomainMethodName("findMany")).toBe(false)
|
||||
expect(detector.isDomainMethodName("insert")).toBe(false)
|
||||
expect(detector.isDomainMethodName("updateOne")).toBe(false)
|
||||
expect(detector.isDomainMethodName("query")).toBe(false)
|
||||
expect(detector.isDomainMethodName("execute")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isRepositoryInterface", () => {
|
||||
it("should identify repository interfaces in domain", () => {
|
||||
expect(
|
||||
detector.isRepositoryInterface(
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
detector.isRepositoryInterface(
|
||||
"src/domain/repositories/IOrderRepository.ts",
|
||||
"domain",
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should not identify repository implementations in infrastructure", () => {
|
||||
expect(
|
||||
detector.isRepositoryInterface(
|
||||
"src/infrastructure/repositories/UserRepository.ts",
|
||||
"infrastructure",
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should not identify non-repository files", () => {
|
||||
expect(detector.isRepositoryInterface("src/domain/entities/User.ts", "domain")).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isUseCase", () => {
|
||||
it("should identify use cases", () => {
|
||||
expect(
|
||||
detector.isUseCase("src/application/use-cases/CreateUser.ts", "application"),
|
||||
).toBe(true)
|
||||
expect(
|
||||
detector.isUseCase("src/application/use-cases/UpdateProfile.ts", "application"),
|
||||
).toBe(true)
|
||||
expect(
|
||||
detector.isUseCase("src/application/use-cases/DeleteOrder.ts", "application"),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should not identify DTOs as use cases", () => {
|
||||
expect(
|
||||
detector.isUseCase("src/application/dtos/UserResponseDto.ts", "application"),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should not identify use cases in wrong layer", () => {
|
||||
expect(detector.isUseCase("src/domain/use-cases/CreateUser.ts", "domain")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessage and getSuggestion", () => {
|
||||
it("should provide helpful message for ORM type violations", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
findOne(query: Prisma.UserWhereInput): Promise<User>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const ormViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
|
||||
)
|
||||
expect(ormViolations[0].getMessage()).toContain("ORM-specific type")
|
||||
expect(ormViolations[0].getSuggestion()).toContain("domain types")
|
||||
})
|
||||
|
||||
it("should provide helpful message for concrete repository violations", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
constructor(private userRepo: PrismaUserRepository) {}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const concreteViolations = violations.filter(
|
||||
(v) =>
|
||||
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(concreteViolations[0].getMessage()).toContain("concrete repository")
|
||||
expect(concreteViolations[0].getSuggestion()).toContain("interface")
|
||||
})
|
||||
|
||||
it("should provide helpful message for new repository violations", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
async execute() {
|
||||
const repo = new UserRepository()
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
const newRepoViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
|
||||
)
|
||||
expect(newRepoViolations[0].getMessage()).toContain("new")
|
||||
expect(newRepoViolations[0].getSuggestion()).toContain("dependency injection")
|
||||
})
|
||||
|
||||
it("should provide helpful message for non-domain method violations", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
findOne(id: string): Promise<User>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
const methodViolations = violations.filter(
|
||||
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
|
||||
)
|
||||
expect(methodViolations[0].getMessage()).toContain("technical name")
|
||||
expect(methodViolations[0].getSuggestion()).toContain("domain language")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Integration tests", () => {
|
||||
it("should detect multiple violation types in same file", () => {
|
||||
const code = `
|
||||
interface IUserRepository {
|
||||
findOne(query: Prisma.UserWhereInput): Promise<User>
|
||||
insert(user: User): Promise<void>
|
||||
findById(id: UserId): Promise<User | null>
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations.length).toBeGreaterThan(1)
|
||||
const types = violations.map((v) => v.violationType)
|
||||
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE)
|
||||
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME)
|
||||
})
|
||||
|
||||
it("should detect all violations in complex use case", () => {
|
||||
const code = `
|
||||
class CreateUser {
|
||||
constructor(private userRepo: PrismaUserRepository) {}
|
||||
|
||||
async execute(data: CreateUserRequest) {
|
||||
const repo = new OrderRepository()
|
||||
await this.userRepo.save(user)
|
||||
await repo.save(order)
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectViolations(
|
||||
code,
|
||||
"src/application/use-cases/CreateUser.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
expect(violations.length).toBeGreaterThanOrEqual(2)
|
||||
const types = violations.map((v) => v.violationType)
|
||||
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE)
|
||||
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user