mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
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:
@@ -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/),
|
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).
|
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
|
## [0.7.6] - 2025-11-25
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -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
|
**Priority:** MEDIUM
|
||||||
**Scope:** Single session (~128K tokens)
|
**Scope:** Single session (~128K tokens)
|
||||||
|
|
||||||
Increase coverage for under-tested domain files.
|
Increase coverage for under-tested domain files.
|
||||||
|
|
||||||
**Current State:**
|
**Results:**
|
||||||
| File | Coverage |
|
| File | Before | After |
|
||||||
|------|----------|
|
|------|--------|-------|
|
||||||
| SourceFile.ts | 46% |
|
| SourceFile.ts | 46% | 100% ✅ |
|
||||||
| ProjectPath.ts | 50% |
|
| ProjectPath.ts | 50% | 100% ✅ |
|
||||||
| ValueObject.ts | 25% |
|
| ValueObject.ts | 25% | 100% ✅ |
|
||||||
| RepositoryViolation.ts | 58% |
|
| RepositoryViolation.ts | 58% | 92.68% ✅ |
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] SourceFile.ts → 80%+
|
- ✅ SourceFile.ts → 100% (31 tests)
|
||||||
- [ ] ProjectPath.ts → 80%+
|
- ✅ ProjectPath.ts → 100% (31 tests)
|
||||||
- [ ] ValueObject.ts → 80%+
|
- ✅ ValueObject.ts → 100% (18 tests)
|
||||||
- [ ] RepositoryViolation.ts → 80%+
|
- ✅ RepositoryViolation.ts → 92.68% (32 tests)
|
||||||
|
- ✅ All 457 tests passing
|
||||||
|
- ✅ Overall coverage: 95.4% statements, 86.25% branches, 96.68% functions
|
||||||
- [ ] Publish to npm
|
- [ ] 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
|
**Last Updated:** 2025-11-25
|
||||||
**Current Version:** 0.7.4
|
**Current Version:** 0.7.7
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/guardian",
|
"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.",
|
"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": [
|
"keywords": [
|
||||||
"puaros",
|
"puaros",
|
||||||
|
|||||||
308
packages/guardian/tests/unit/domain/ProjectPath.test.ts
Normal file
308
packages/guardian/tests/unit/domain/ProjectPath.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
521
packages/guardian/tests/unit/domain/RepositoryViolation.test.ts
Normal file
521
packages/guardian/tests/unit/domain/RepositoryViolation.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
329
packages/guardian/tests/unit/domain/SourceFile.test.ts
Normal file
329
packages/guardian/tests/unit/domain/SourceFile.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
199
packages/guardian/tests/unit/domain/ValueObject.test.ts
Normal file
199
packages/guardian/tests/unit/domain/ValueObject.test.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user