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/),
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
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