test: improve test coverage for domain files from 46-58% to 92-100%

- Add 31 tests for SourceFile.ts (46% → 100%)
- Add 31 tests for ProjectPath.ts (50% → 100%)
- Add 18 tests for ValueObject.ts (25% → 100%)
- Add 32 tests for RepositoryViolation.ts (58% → 92.68%)
- Total test count: 345 → 457 tests (all passing)
- Overall coverage: 95.4% statements, 86.25% branches, 96.68% functions
- Update version to 0.7.7
- Update ROADMAP.md and CHANGELOG.md
This commit is contained in:
imfozilbek
2025-11-25 16:50:00 +05:00
parent 0da25d9046
commit 0b9b8564bf
7 changed files with 1394 additions and 14 deletions

View File

@@ -5,6 +5,26 @@ All notable changes to @samiyev/guardian will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.7.7] - 2025-11-25
### Added
- 🧪 **Comprehensive test coverage for under-tested domain files**:
- Added 31 tests for `SourceFile.ts` - coverage improved from 46% to 100%
- Added 31 tests for `ProjectPath.ts` - coverage improved from 50% to 100%
- Added 18 tests for `ValueObject.ts` - coverage improved from 25% to 100%
- Added 32 tests for `RepositoryViolation.ts` - coverage improved from 58% to 92.68%
- Total test count increased from 345 to 457 tests
- Overall coverage improved to 95.4% statements, 86.25% branches, 96.68% functions
- All tests pass with no breaking changes
### Changed
- 📊 **Improved code quality and maintainability**:
- Enhanced test suite for core domain entities and value objects
- Better coverage of edge cases and error handling
- Increased confidence in domain layer correctness
## [0.7.6] - 2025-11-25
### Changed

View File

@@ -365,26 +365,29 @@ cli/
---
### Version 0.7.7 - Improve Test Coverage 🧪
### Version 0.7.7 - Improve Test Coverage 🧪 ✅ RELEASED
**Released:** 2025-11-25
**Priority:** MEDIUM
**Scope:** Single session (~128K tokens)
Increase coverage for under-tested domain files.
**Current State:**
| File | Coverage |
|------|----------|
| SourceFile.ts | 46% |
| ProjectPath.ts | 50% |
| ValueObject.ts | 25% |
| RepositoryViolation.ts | 58% |
**Results:**
| File | Before | After |
|------|--------|-------|
| SourceFile.ts | 46% | 100% ✅ |
| ProjectPath.ts | 50% | 100% ✅ |
| ValueObject.ts | 25% | 100% ✅ |
| RepositoryViolation.ts | 58% | 92.68% ✅ |
**Deliverables:**
- [ ] SourceFile.ts → 80%+
- [ ] ProjectPath.ts → 80%+
- [ ] ValueObject.ts → 80%+
- [ ] RepositoryViolation.ts → 80%+
- SourceFile.ts → 100% (31 tests)
- ProjectPath.ts → 100% (31 tests)
- ValueObject.ts → 100% (18 tests)
- RepositoryViolation.ts → 92.68% (32 tests)
- ✅ All 457 tests passing
- ✅ Overall coverage: 95.4% statements, 86.25% branches, 96.68% functions
- [ ] Publish to npm
---
@@ -2074,4 +2077,4 @@ Until we reach 1.0.0, minor version bumps (0.x.0) may include breaking changes a
---
**Last Updated:** 2025-11-25
**Current Version:** 0.7.4
**Current Version:** 0.7.7

View File

@@ -1,6 +1,6 @@
{
"name": "@samiyev/guardian",
"version": "0.7.6",
"version": "0.7.7",
"description": "Research-backed code quality guardian for AI-assisted development. Detects hardcodes, circular deps, framework leaks, entity exposure, and 8 architecture violations. Enforces Clean Architecture/DDD principles. Works with GitHub Copilot, Cursor, Windsurf, Claude, ChatGPT, Cline, and any AI coding tool.",
"keywords": [
"puaros",

View File

@@ -0,0 +1,308 @@
import { describe, it, expect } from "vitest"
import { ProjectPath } from "../../../src/domain/value-objects/ProjectPath"
describe("ProjectPath", () => {
describe("create", () => {
it("should create a ProjectPath with absolute and relative paths", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.absolute).toBe(absolutePath)
expect(projectPath.relative).toBe("src/domain/User.ts")
})
it("should handle paths with same directory", () => {
const absolutePath = "/Users/dev/project/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.absolute).toBe(absolutePath)
expect(projectPath.relative).toBe("User.ts")
})
it("should handle nested directory structures", () => {
const absolutePath = "/Users/dev/project/src/domain/entities/user/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.absolute).toBe(absolutePath)
expect(projectPath.relative).toBe("src/domain/entities/user/User.ts")
})
it("should handle Windows-style paths", () => {
const absolutePath = "C:\\Users\\dev\\project\\src\\domain\\User.ts"
const projectRoot = "C:\\Users\\dev\\project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.absolute).toBe(absolutePath)
})
})
describe("absolute getter", () => {
it("should return the absolute path", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.absolute).toBe(absolutePath)
})
})
describe("relative getter", () => {
it("should return the relative path", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.relative).toBe("src/domain/User.ts")
})
})
describe("extension getter", () => {
it("should return .ts for TypeScript files", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.extension).toBe(".ts")
})
it("should return .tsx for TypeScript JSX files", () => {
const absolutePath = "/Users/dev/project/src/components/Button.tsx"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.extension).toBe(".tsx")
})
it("should return .js for JavaScript files", () => {
const absolutePath = "/Users/dev/project/src/utils/helper.js"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.extension).toBe(".js")
})
it("should return .jsx for JavaScript JSX files", () => {
const absolutePath = "/Users/dev/project/src/components/Button.jsx"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.extension).toBe(".jsx")
})
it("should return empty string for files without extension", () => {
const absolutePath = "/Users/dev/project/README"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.extension).toBe("")
})
})
describe("filename getter", () => {
it("should return the filename with extension", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.filename).toBe("User.ts")
})
it("should handle filenames with multiple dots", () => {
const absolutePath = "/Users/dev/project/src/domain/User.test.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.filename).toBe("User.test.ts")
})
it("should handle filenames without extension", () => {
const absolutePath = "/Users/dev/project/README"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.filename).toBe("README")
})
})
describe("directory getter", () => {
it("should return the directory path relative to project root", () => {
const absolutePath = "/Users/dev/project/src/domain/entities/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.directory).toBe("src/domain/entities")
})
it("should return dot for files in project root", () => {
const absolutePath = "/Users/dev/project/README.md"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.directory).toBe(".")
})
it("should handle single-level directories", () => {
const absolutePath = "/Users/dev/project/src/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.directory).toBe("src")
})
})
describe("isTypeScript", () => {
it("should return true for .ts files", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isTypeScript()).toBe(true)
})
it("should return true for .tsx files", () => {
const absolutePath = "/Users/dev/project/src/components/Button.tsx"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isTypeScript()).toBe(true)
})
it("should return false for .js files", () => {
const absolutePath = "/Users/dev/project/src/utils/helper.js"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isTypeScript()).toBe(false)
})
it("should return false for .jsx files", () => {
const absolutePath = "/Users/dev/project/src/components/Button.jsx"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isTypeScript()).toBe(false)
})
it("should return false for other file types", () => {
const absolutePath = "/Users/dev/project/README.md"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isTypeScript()).toBe(false)
})
})
describe("isJavaScript", () => {
it("should return true for .js files", () => {
const absolutePath = "/Users/dev/project/src/utils/helper.js"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isJavaScript()).toBe(true)
})
it("should return true for .jsx files", () => {
const absolutePath = "/Users/dev/project/src/components/Button.jsx"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isJavaScript()).toBe(true)
})
it("should return false for .ts files", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isJavaScript()).toBe(false)
})
it("should return false for .tsx files", () => {
const absolutePath = "/Users/dev/project/src/components/Button.tsx"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isJavaScript()).toBe(false)
})
it("should return false for other file types", () => {
const absolutePath = "/Users/dev/project/README.md"
const projectRoot = "/Users/dev/project"
const projectPath = ProjectPath.create(absolutePath, projectRoot)
expect(projectPath.isJavaScript()).toBe(false)
})
})
describe("equals", () => {
it("should return true for identical paths", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const path1 = ProjectPath.create(absolutePath, projectRoot)
const path2 = ProjectPath.create(absolutePath, projectRoot)
expect(path1.equals(path2)).toBe(true)
})
it("should return false for different absolute paths", () => {
const projectRoot = "/Users/dev/project"
const path1 = ProjectPath.create("/Users/dev/project/src/domain/User.ts", projectRoot)
const path2 = ProjectPath.create("/Users/dev/project/src/domain/Order.ts", projectRoot)
expect(path1.equals(path2)).toBe(false)
})
it("should return false for different relative paths", () => {
const path1 = ProjectPath.create(
"/Users/dev/project1/src/User.ts",
"/Users/dev/project1",
)
const path2 = ProjectPath.create(
"/Users/dev/project2/src/User.ts",
"/Users/dev/project2",
)
expect(path1.equals(path2)).toBe(false)
})
it("should return false when comparing with undefined", () => {
const absolutePath = "/Users/dev/project/src/domain/User.ts"
const projectRoot = "/Users/dev/project"
const path1 = ProjectPath.create(absolutePath, projectRoot)
expect(path1.equals(undefined)).toBe(false)
})
})
})

View File

@@ -0,0 +1,521 @@
import { describe, it, expect } from "vitest"
import { RepositoryViolation } from "../../../src/domain/value-objects/RepositoryViolation"
import { REPOSITORY_VIOLATION_TYPES } from "../../../src/shared/constants/rules"
describe("RepositoryViolation", () => {
describe("create", () => {
it("should create a repository violation for ORM type in interface", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Repository uses Prisma type",
"Prisma.UserWhereInput",
)
expect(violation.violationType).toBe(REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE)
expect(violation.filePath).toBe("src/domain/repositories/IUserRepository.ts")
expect(violation.layer).toBe("domain")
expect(violation.line).toBe(15)
expect(violation.details).toBe("Repository uses Prisma type")
expect(violation.ormType).toBe("Prisma.UserWhereInput")
})
it("should create a repository violation for concrete repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
10,
"Use case depends on concrete repository",
undefined,
"UserRepository",
)
expect(violation.violationType).toBe(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
)
expect(violation.repositoryName).toBe("UserRepository")
})
it("should create a repository violation for new repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
12,
"Use case creates repository with new",
undefined,
"UserRepository",
)
expect(violation.violationType).toBe(
REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
)
expect(violation.repositoryName).toBe("UserRepository")
})
it("should create a repository violation for non-domain method name", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
"src/domain/repositories/IUserRepository.ts",
"domain",
8,
"Method uses technical name. Consider: findById()",
undefined,
undefined,
"findOne",
)
expect(violation.violationType).toBe(REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME)
expect(violation.methodName).toBe("findOne")
})
it("should handle optional line parameter", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
undefined,
"Repository uses Prisma type",
)
expect(violation.line).toBeUndefined()
})
})
describe("getters", () => {
it("should return violation type", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
expect(violation.violationType).toBe(REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE)
})
it("should return file path", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
expect(violation.filePath).toBe("src/domain/repositories/IUserRepository.ts")
})
it("should return layer", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
expect(violation.layer).toBe("domain")
})
it("should return line number", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
expect(violation.line).toBe(15)
})
it("should return details", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Repository uses Prisma type",
)
expect(violation.details).toBe("Repository uses Prisma type")
})
it("should return ORM type", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
"Prisma.UserWhereInput",
)
expect(violation.ormType).toBe("Prisma.UserWhereInput")
})
it("should return repository name", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
10,
"Test",
undefined,
"UserRepository",
)
expect(violation.repositoryName).toBe("UserRepository")
})
it("should return method name", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
"src/domain/repositories/IUserRepository.ts",
"domain",
8,
"Test",
undefined,
undefined,
"findOne",
)
expect(violation.methodName).toBe("findOne")
})
})
describe("getMessage", () => {
it("should return message for ORM type in interface", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
"Prisma.UserWhereInput",
)
const message = violation.getMessage()
expect(message).toContain("ORM-specific type")
expect(message).toContain("Prisma.UserWhereInput")
})
it("should return message for concrete repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
10,
"Test",
undefined,
"UserRepository",
)
const message = violation.getMessage()
expect(message).toContain("depends on concrete repository")
expect(message).toContain("UserRepository")
})
it("should return message for new repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
12,
"Test",
undefined,
"UserRepository",
)
const message = violation.getMessage()
expect(message).toContain("creates repository with 'new")
expect(message).toContain("UserRepository")
})
it("should return message for non-domain method name", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
"src/domain/repositories/IUserRepository.ts",
"domain",
8,
"Test",
undefined,
undefined,
"findOne",
)
const message = violation.getMessage()
expect(message).toContain("uses technical name")
expect(message).toContain("findOne")
})
it("should handle unknown ORM type gracefully", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
const message = violation.getMessage()
expect(message).toContain("unknown")
})
})
describe("getSuggestion", () => {
it("should return suggestion for ORM type in interface", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
"Prisma.UserWhereInput",
)
const suggestion = violation.getSuggestion()
expect(suggestion).toContain("Remove ORM-specific types")
expect(suggestion).toContain("Use domain types")
})
it("should return suggestion for concrete repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
10,
"Test",
undefined,
"UserRepository",
)
const suggestion = violation.getSuggestion()
expect(suggestion).toContain("Depend on repository interface")
expect(suggestion).toContain("IUserRepository")
})
it("should return suggestion for new repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
12,
"Test",
undefined,
"UserRepository",
)
const suggestion = violation.getSuggestion()
expect(suggestion).toContain("Remove 'new Repository()'")
expect(suggestion).toContain("dependency injection")
})
it("should return suggestion for non-domain method name with smart suggestion", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
"src/domain/repositories/IUserRepository.ts",
"domain",
8,
"Method uses technical name. Consider: findById()",
undefined,
undefined,
"findOne",
)
const suggestion = violation.getSuggestion()
expect(suggestion).toContain("findById()")
})
it("should return fallback suggestion for known technical method", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
"src/domain/repositories/IUserRepository.ts",
"domain",
8,
"Method uses technical name",
undefined,
undefined,
"insert",
)
const suggestion = violation.getSuggestion()
expect(suggestion).toContain("save or create")
})
it("should return default suggestion for unknown method", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
"src/domain/repositories/IUserRepository.ts",
"domain",
8,
"Method uses technical name",
undefined,
undefined,
"unknownMethod",
)
const suggestion = violation.getSuggestion()
expect(suggestion).toBeDefined()
expect(suggestion.length).toBeGreaterThan(0)
})
})
describe("getExampleFix", () => {
it("should return example fix for ORM type in interface", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
const example = violation.getExampleFix()
expect(example).toContain("BAD")
expect(example).toContain("GOOD")
expect(example).toContain("IUserRepository")
})
it("should return example fix for concrete repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
10,
"Test",
)
const example = violation.getExampleFix()
expect(example).toContain("BAD")
expect(example).toContain("GOOD")
expect(example).toContain("CreateUser")
})
it("should return example fix for new repository in use case", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
"src/application/use-cases/CreateUser.ts",
"application",
12,
"Test",
)
const example = violation.getExampleFix()
expect(example).toContain("BAD")
expect(example).toContain("GOOD")
expect(example).toContain("new UserRepository")
})
it("should return example fix for non-domain method name", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
"src/domain/repositories/IUserRepository.ts",
"domain",
8,
"Test",
)
const example = violation.getExampleFix()
expect(example).toContain("BAD")
expect(example).toContain("GOOD")
expect(example).toContain("findOne")
})
})
describe("equals", () => {
it("should return true for violations with identical properties", () => {
const violation1 = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
"Prisma.UserWhereInput",
)
const violation2 = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
"Prisma.UserWhereInput",
)
expect(violation1.equals(violation2)).toBe(true)
})
it("should return false for violations with different types", () => {
const violation1 = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
const violation2 = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
expect(violation1.equals(violation2)).toBe(false)
})
it("should return false for violations with different file paths", () => {
const violation1 = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
const violation2 = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IOrderRepository.ts",
"domain",
15,
"Test",
)
expect(violation1.equals(violation2)).toBe(false)
})
it("should return false when comparing with undefined", () => {
const violation = RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
"src/domain/repositories/IUserRepository.ts",
"domain",
15,
"Test",
)
expect(violation.equals(undefined)).toBe(false)
})
})
})

View File

@@ -0,0 +1,329 @@
import { describe, it, expect } from "vitest"
import { SourceFile } from "../../../src/domain/entities/SourceFile"
import { ProjectPath } from "../../../src/domain/value-objects/ProjectPath"
import { LAYERS } from "../../../src/shared/constants/rules"
describe("SourceFile", () => {
describe("constructor", () => {
it("should create a SourceFile instance with all properties", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const content = "class User {}"
const imports = ["./BaseEntity"]
const exports = ["User"]
const id = "test-id"
const sourceFile = new SourceFile(path, content, imports, exports, id)
expect(sourceFile.path).toBe(path)
expect(sourceFile.content).toBe(content)
expect(sourceFile.imports).toEqual(imports)
expect(sourceFile.exports).toEqual(exports)
expect(sourceFile.id).toBe(id)
})
it("should create a SourceFile with empty imports and exports by default", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const content = "class User {}"
const sourceFile = new SourceFile(path, content)
expect(sourceFile.imports).toEqual([])
expect(sourceFile.exports).toEqual([])
})
it("should generate an id if not provided", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const content = "class User {}"
const sourceFile = new SourceFile(path, content)
expect(sourceFile.id).toBeDefined()
expect(typeof sourceFile.id).toBe("string")
expect(sourceFile.id.length).toBeGreaterThan(0)
})
})
describe("layer detection", () => {
it("should detect domain layer from path", () => {
const path = ProjectPath.create("/project/src/domain/entities/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
expect(sourceFile.layer).toBe(LAYERS.DOMAIN)
})
it("should detect application layer from path", () => {
const path = ProjectPath.create(
"/project/src/application/use-cases/CreateUser.ts",
"/project",
)
const sourceFile = new SourceFile(path, "")
expect(sourceFile.layer).toBe(LAYERS.APPLICATION)
})
it("should detect infrastructure layer from path", () => {
const path = ProjectPath.create(
"/project/src/infrastructure/database/UserRepository.ts",
"/project",
)
const sourceFile = new SourceFile(path, "")
expect(sourceFile.layer).toBe(LAYERS.INFRASTRUCTURE)
})
it("should detect shared layer from path", () => {
const path = ProjectPath.create("/project/src/shared/utils/helpers.ts", "/project")
const sourceFile = new SourceFile(path, "")
expect(sourceFile.layer).toBe(LAYERS.SHARED)
})
it("should return undefined for unknown layer", () => {
const path = ProjectPath.create("/project/src/unknown/Test.ts", "/project")
const sourceFile = new SourceFile(path, "")
expect(sourceFile.layer).toBeUndefined()
})
it("should handle uppercase layer names in path", () => {
const path = ProjectPath.create("/project/src/DOMAIN/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
expect(sourceFile.layer).toBe(LAYERS.DOMAIN)
})
it("should handle mixed case layer names in path", () => {
const path = ProjectPath.create("/project/src/Application/UseCase.ts", "/project")
const sourceFile = new SourceFile(path, "")
expect(sourceFile.layer).toBe(LAYERS.APPLICATION)
})
})
describe("path getter", () => {
it("should return the project path", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
expect(sourceFile.path).toBe(path)
})
})
describe("content getter", () => {
it("should return the file content", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const content = "class User { constructor(public name: string) {} }"
const sourceFile = new SourceFile(path, content)
expect(sourceFile.content).toBe(content)
})
})
describe("imports getter", () => {
it("should return a copy of imports array", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const imports = ["./BaseEntity", "./ValueObject"]
const sourceFile = new SourceFile(path, "", imports)
const returnedImports = sourceFile.imports
expect(returnedImports).toEqual(imports)
expect(returnedImports).not.toBe(imports)
})
it("should not allow mutations of internal imports array", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const imports = ["./BaseEntity"]
const sourceFile = new SourceFile(path, "", imports)
const returnedImports = sourceFile.imports
returnedImports.push("./NewImport")
expect(sourceFile.imports).toEqual(["./BaseEntity"])
})
})
describe("exports getter", () => {
it("should return a copy of exports array", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const exports = ["User", "UserProps"]
const sourceFile = new SourceFile(path, "", [], exports)
const returnedExports = sourceFile.exports
expect(returnedExports).toEqual(exports)
expect(returnedExports).not.toBe(exports)
})
it("should not allow mutations of internal exports array", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const exports = ["User"]
const sourceFile = new SourceFile(path, "", [], exports)
const returnedExports = sourceFile.exports
returnedExports.push("NewExport")
expect(sourceFile.exports).toEqual(["User"])
})
})
describe("addImport", () => {
it("should add a new import to the list", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
sourceFile.addImport("./BaseEntity")
expect(sourceFile.imports).toEqual(["./BaseEntity"])
})
it("should not add duplicate imports", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "", ["./BaseEntity"])
sourceFile.addImport("./BaseEntity")
expect(sourceFile.imports).toEqual(["./BaseEntity"])
})
it("should update updatedAt timestamp when adding new import", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
const originalUpdatedAt = sourceFile.updatedAt
setTimeout(() => {
sourceFile.addImport("./BaseEntity")
expect(sourceFile.updatedAt.getTime()).toBeGreaterThanOrEqual(
originalUpdatedAt.getTime(),
)
}, 10)
})
it("should not update timestamp when adding duplicate import", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "", ["./BaseEntity"])
const originalUpdatedAt = sourceFile.updatedAt
setTimeout(() => {
sourceFile.addImport("./BaseEntity")
expect(sourceFile.updatedAt).toBe(originalUpdatedAt)
}, 10)
})
it("should add multiple different imports", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
sourceFile.addImport("./BaseEntity")
sourceFile.addImport("./ValueObject")
sourceFile.addImport("./DomainEvent")
expect(sourceFile.imports).toEqual(["./BaseEntity", "./ValueObject", "./DomainEvent"])
})
})
describe("addExport", () => {
it("should add a new export to the list", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
sourceFile.addExport("User")
expect(sourceFile.exports).toEqual(["User"])
})
it("should not add duplicate exports", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "", [], ["User"])
sourceFile.addExport("User")
expect(sourceFile.exports).toEqual(["User"])
})
it("should update updatedAt timestamp when adding new export", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
const originalUpdatedAt = sourceFile.updatedAt
setTimeout(() => {
sourceFile.addExport("User")
expect(sourceFile.updatedAt.getTime()).toBeGreaterThanOrEqual(
originalUpdatedAt.getTime(),
)
}, 10)
})
it("should not update timestamp when adding duplicate export", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "", [], ["User"])
const originalUpdatedAt = sourceFile.updatedAt
setTimeout(() => {
sourceFile.addExport("User")
expect(sourceFile.updatedAt).toBe(originalUpdatedAt)
}, 10)
})
it("should add multiple different exports", () => {
const path = ProjectPath.create("/project/src/domain/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
sourceFile.addExport("User")
sourceFile.addExport("UserProps")
sourceFile.addExport("UserFactory")
expect(sourceFile.exports).toEqual(["User", "UserProps", "UserFactory"])
})
})
describe("importsFrom", () => {
it("should return true if imports contain the specified layer", () => {
const path = ProjectPath.create("/project/src/application/User.ts", "/project")
const imports = ["../../domain/entities/User", "../use-cases/CreateUser"]
const sourceFile = new SourceFile(path, "", imports)
expect(sourceFile.importsFrom("domain")).toBe(true)
})
it("should return false if imports do not contain the specified layer", () => {
const path = ProjectPath.create("/project/src/application/User.ts", "/project")
const imports = ["../use-cases/CreateUser", "../dtos/UserDto"]
const sourceFile = new SourceFile(path, "", imports)
expect(sourceFile.importsFrom("domain")).toBe(false)
})
it("should be case-insensitive", () => {
const path = ProjectPath.create("/project/src/application/User.ts", "/project")
const imports = ["../../DOMAIN/entities/User"]
const sourceFile = new SourceFile(path, "", imports)
expect(sourceFile.importsFrom("domain")).toBe(true)
})
it("should return false for empty imports", () => {
const path = ProjectPath.create("/project/src/application/User.ts", "/project")
const sourceFile = new SourceFile(path, "")
expect(sourceFile.importsFrom("domain")).toBe(false)
})
it("should handle partial matches in import paths", () => {
const path = ProjectPath.create("/project/src/application/User.ts", "/project")
const imports = ["../../infrastructure/database/UserRepository"]
const sourceFile = new SourceFile(path, "", imports)
expect(sourceFile.importsFrom("infrastructure")).toBe(true)
expect(sourceFile.importsFrom("domain")).toBe(false)
})
})
})

View File

@@ -0,0 +1,199 @@
import { describe, it, expect } from "vitest"
import { ValueObject } from "../../../src/domain/value-objects/ValueObject"
interface TestProps {
readonly value: string
readonly count: number
}
class TestValueObject extends ValueObject<TestProps> {
constructor(value: string, count: number) {
super({ value, count })
}
public get value(): string {
return this.props.value
}
public get count(): number {
return this.props.count
}
}
interface ComplexProps {
readonly name: string
readonly items: string[]
readonly metadata: { key: string; value: number }
}
class ComplexValueObject extends ValueObject<ComplexProps> {
constructor(name: string, items: string[], metadata: { key: string; value: number }) {
super({ name, items, metadata })
}
public get name(): string {
return this.props.name
}
public get items(): string[] {
return this.props.items
}
public get metadata(): { key: string; value: number } {
return this.props.metadata
}
}
describe("ValueObject", () => {
describe("constructor", () => {
it("should create a value object with provided properties", () => {
const vo = new TestValueObject("test", 42)
expect(vo.value).toBe("test")
expect(vo.count).toBe(42)
})
it("should freeze the properties object", () => {
const vo = new TestValueObject("test", 42)
expect(Object.isFrozen(vo["props"])).toBe(true)
})
it("should prevent modification of properties", () => {
const vo = new TestValueObject("test", 42)
expect(() => {
;(vo["props"] as any).value = "modified"
}).toThrow()
})
it("should handle complex nested properties", () => {
const vo = new ComplexValueObject("test", ["item1", "item2"], {
key: "key1",
value: 100,
})
expect(vo.name).toBe("test")
expect(vo.items).toEqual(["item1", "item2"])
expect(vo.metadata).toEqual({ key: "key1", value: 100 })
})
})
describe("equals", () => {
it("should return true for value objects with identical properties", () => {
const vo1 = new TestValueObject("test", 42)
const vo2 = new TestValueObject("test", 42)
expect(vo1.equals(vo2)).toBe(true)
})
it("should return false for value objects with different values", () => {
const vo1 = new TestValueObject("test1", 42)
const vo2 = new TestValueObject("test2", 42)
expect(vo1.equals(vo2)).toBe(false)
})
it("should return false for value objects with different counts", () => {
const vo1 = new TestValueObject("test", 42)
const vo2 = new TestValueObject("test", 43)
expect(vo1.equals(vo2)).toBe(false)
})
it("should return false when comparing with undefined", () => {
const vo1 = new TestValueObject("test", 42)
expect(vo1.equals(undefined)).toBe(false)
})
it("should return false when comparing with null", () => {
const vo1 = new TestValueObject("test", 42)
expect(vo1.equals(null as any)).toBe(false)
})
it("should handle complex nested property comparisons", () => {
const vo1 = new ComplexValueObject("test", ["item1", "item2"], {
key: "key1",
value: 100,
})
const vo2 = new ComplexValueObject("test", ["item1", "item2"], {
key: "key1",
value: 100,
})
expect(vo1.equals(vo2)).toBe(true)
})
it("should detect differences in nested arrays", () => {
const vo1 = new ComplexValueObject("test", ["item1", "item2"], {
key: "key1",
value: 100,
})
const vo2 = new ComplexValueObject("test", ["item1", "item3"], {
key: "key1",
value: 100,
})
expect(vo1.equals(vo2)).toBe(false)
})
it("should detect differences in nested objects", () => {
const vo1 = new ComplexValueObject("test", ["item1", "item2"], {
key: "key1",
value: 100,
})
const vo2 = new ComplexValueObject("test", ["item1", "item2"], {
key: "key2",
value: 100,
})
expect(vo1.equals(vo2)).toBe(false)
})
it("should return true for same instance", () => {
const vo1 = new TestValueObject("test", 42)
expect(vo1.equals(vo1)).toBe(true)
})
it("should handle empty string values", () => {
const vo1 = new TestValueObject("", 0)
const vo2 = new TestValueObject("", 0)
expect(vo1.equals(vo2)).toBe(true)
})
it("should distinguish between zero and undefined in comparisons", () => {
const vo1 = new TestValueObject("test", 0)
const vo2 = new TestValueObject("test", 0)
expect(vo1.equals(vo2)).toBe(true)
})
})
describe("immutability", () => {
it("should freeze props object after creation", () => {
const vo = new TestValueObject("original", 42)
expect(Object.isFrozen(vo["props"])).toBe(true)
})
it("should not allow adding new properties", () => {
const vo = new TestValueObject("test", 42)
expect(() => {
;(vo["props"] as any).newProp = "new"
}).toThrow()
})
it("should not allow deleting properties", () => {
const vo = new TestValueObject("test", 42)
expect(() => {
delete (vo["props"] as any).value
}).toThrow()
})
})
})