mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(guardian): add guardian package - code quality analyzer
Add @puaros/guardian package v0.1.0 - code quality guardian for vibe coders and enterprise teams. Features: - Hardcode detection (magic numbers, magic strings) - Circular dependency detection - Naming convention enforcement (Clean Architecture) - Architecture violation detection - CLI tool with comprehensive reporting - 159 tests with 80%+ coverage - Smart suggestions for fixes - Built for AI-assisted development Built with Clean Architecture and DDD principles. Works with Claude, GPT, Copilot, Cursor, and any AI coding assistant.
This commit is contained in:
38
packages/guardian/tests/fixtures/code-samples/exported-constants.ts
vendored
Normal file
38
packages/guardian/tests/fixtures/code-samples/exported-constants.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
// Test fixture for exported constants detection
|
||||
|
||||
// Single-line export const with as const
|
||||
export const SINGLE_LINE_OBJECT = { value: 123 } as const
|
||||
export const SINGLE_LINE_ARRAY = [1, 2, 3] as const
|
||||
export const SINGLE_LINE_NUMBER = 999 as const
|
||||
export const SINGLE_LINE_STRING = "test" as const
|
||||
|
||||
// Multi-line export const with as const
|
||||
export const MULTI_LINE_CONFIG = {
|
||||
timeout: 5000,
|
||||
port: 8080,
|
||||
retries: 3,
|
||||
} as const
|
||||
|
||||
export const NESTED_CONFIG = {
|
||||
api: {
|
||||
baseUrl: "http://localhost",
|
||||
timeout: 10000,
|
||||
},
|
||||
db: {
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
},
|
||||
} as const
|
||||
|
||||
// Array with as const
|
||||
export const ALLOWED_PORTS = [3000, 8080, 9000] as const
|
||||
|
||||
// Without as const (should still be detected as hardcode)
|
||||
export const NOT_CONST = {
|
||||
value: 777,
|
||||
}
|
||||
|
||||
// Regular variable (not exported) - should detect hardcode
|
||||
const localConfig = {
|
||||
timeout: 4000,
|
||||
}
|
||||
91
packages/guardian/tests/fixtures/code-samples/hardcoded-values.ts
vendored
Normal file
91
packages/guardian/tests/fixtures/code-samples/hardcoded-values.ts
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
// Test fixture for hardcode detection
|
||||
|
||||
// ❌ Should be detected - Magic numbers
|
||||
export function badTimeouts() {
|
||||
setTimeout(() => {}, 5000)
|
||||
setInterval(() => {}, 3000)
|
||||
const timeout = 10000
|
||||
}
|
||||
|
||||
export function badRetries() {
|
||||
const maxRetries = 3
|
||||
const attempts = 5
|
||||
const retries = 7
|
||||
}
|
||||
|
||||
export function badPorts() {
|
||||
const port = 8080
|
||||
const PORT = 3000
|
||||
const serverPort = 9000
|
||||
}
|
||||
|
||||
export function badLimits() {
|
||||
const limit = 50
|
||||
const max = 100
|
||||
const min = 10
|
||||
const maxSize = 1024
|
||||
}
|
||||
|
||||
// ❌ Should be detected - Magic strings
|
||||
export function badUrls() {
|
||||
const apiUrl = "http://localhost:8080"
|
||||
const baseUrl = "https://api.example.com"
|
||||
const dbUrl = "mongodb://localhost:27017/mydb"
|
||||
}
|
||||
|
||||
export function badStrings() {
|
||||
const errorMessage = "Something went wrong"
|
||||
const configPath = "/etc/app/config"
|
||||
}
|
||||
|
||||
// ✅ Should NOT be detected - Allowed numbers
|
||||
export function allowedNumbers() {
|
||||
const items = []
|
||||
const index = 0
|
||||
const increment = 1
|
||||
const pair = 2
|
||||
const ten = 10
|
||||
const hundred = 100
|
||||
const thousand = 1000
|
||||
const notFound = -1
|
||||
}
|
||||
|
||||
// ✅ Should NOT be detected - Exported constants
|
||||
export const CONFIG = {
|
||||
timeout: 5000,
|
||||
port: 8080,
|
||||
maxRetries: 3,
|
||||
} as const
|
||||
|
||||
export const API_CONFIG = {
|
||||
baseUrl: "http://localhost:3000",
|
||||
timeout: 10000,
|
||||
} as const
|
||||
|
||||
export const SETTINGS = {
|
||||
nested: {
|
||||
deep: {
|
||||
value: 999,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
// ✅ Should NOT be detected - Console logs
|
||||
export function loggingAllowed() {
|
||||
console.log("Debug message")
|
||||
console.error("Error occurred")
|
||||
}
|
||||
|
||||
// ✅ Should NOT be detected - Test descriptions
|
||||
describe("test suite", () => {
|
||||
test("should work correctly", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ❌ Should be detected - Generic 3+ digit numbers
|
||||
export function suspiciousNumbers() {
|
||||
const code = 404
|
||||
const status = 200
|
||||
const buffer = 512
|
||||
}
|
||||
16
packages/guardian/tests/fixtures/code-samples/sample.ts
vendored
Normal file
16
packages/guardian/tests/fixtures/code-samples/sample.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
|
||||
export const multiply = (a: number, b: number): number => {
|
||||
return a * b
|
||||
}
|
||||
|
||||
export class Calculator {
|
||||
public divide(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
throw new Error("Division by zero")
|
||||
}
|
||||
return a / b
|
||||
}
|
||||
}
|
||||
46
packages/guardian/tests/unit/domain/BaseEntity.test.ts
Normal file
46
packages/guardian/tests/unit/domain/BaseEntity.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { BaseEntity } from "../../../src/domain/entities/BaseEntity"
|
||||
|
||||
class TestEntity extends BaseEntity {
|
||||
constructor(id?: string) {
|
||||
super(id)
|
||||
}
|
||||
}
|
||||
|
||||
describe("BaseEntity", () => {
|
||||
it("should create an entity with generated id", () => {
|
||||
const entity = new TestEntity()
|
||||
expect(entity.id).toBeDefined()
|
||||
expect(typeof entity.id).toBe("string")
|
||||
})
|
||||
|
||||
it("should create an entity with provided id", () => {
|
||||
const customId = "custom-id-123"
|
||||
const entity = new TestEntity(customId)
|
||||
expect(entity.id).toBe(customId)
|
||||
})
|
||||
|
||||
it("should have createdAt and updatedAt timestamps", () => {
|
||||
const entity = new TestEntity()
|
||||
expect(entity.createdAt).toBeInstanceOf(Date)
|
||||
expect(entity.updatedAt).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
it("should return true when comparing same entity", () => {
|
||||
const entity = new TestEntity()
|
||||
expect(entity.equals(entity)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true when comparing entities with same id", () => {
|
||||
const id = "same-id"
|
||||
const entity1 = new TestEntity(id)
|
||||
const entity2 = new TestEntity(id)
|
||||
expect(entity1.equals(entity2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when comparing entities with different ids", () => {
|
||||
const entity1 = new TestEntity()
|
||||
const entity2 = new TestEntity()
|
||||
expect(entity1.equals(entity2)).toBe(false)
|
||||
})
|
||||
})
|
||||
234
packages/guardian/tests/unit/domain/DependencyGraph.test.ts
Normal file
234
packages/guardian/tests/unit/domain/DependencyGraph.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { DependencyGraph } from "../../../src/domain/entities/DependencyGraph"
|
||||
import { SourceFile } from "../../../src/domain/entities/SourceFile"
|
||||
import { ProjectPath } from "../../../src/domain/value-objects/ProjectPath"
|
||||
|
||||
describe("DependencyGraph", () => {
|
||||
describe("basic operations", () => {
|
||||
it("should create an empty dependency graph", () => {
|
||||
const graph = new DependencyGraph()
|
||||
expect(graph.getAllNodes()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should add a file to the graph", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const path = ProjectPath.create("/project/src/file.ts", "/project")
|
||||
const file = new SourceFile(path, "const x = 1")
|
||||
|
||||
graph.addFile(file)
|
||||
|
||||
expect(graph.getAllNodes()).toHaveLength(1)
|
||||
expect(graph.getNode("src/file.ts")).toBeDefined()
|
||||
})
|
||||
|
||||
it("should add dependencies between files", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const path1 = ProjectPath.create("/project/src/file1.ts", "/project")
|
||||
const path2 = ProjectPath.create("/project/src/file2.ts", "/project")
|
||||
const file1 = new SourceFile(path1, "import { x } from './file2'")
|
||||
const file2 = new SourceFile(path2, "export const x = 1")
|
||||
|
||||
graph.addFile(file1)
|
||||
graph.addFile(file2)
|
||||
graph.addDependency("src/file1.ts", "src/file2.ts")
|
||||
|
||||
const node1 = graph.getNode("src/file1.ts")
|
||||
expect(node1?.dependencies).toContain("src/file2.ts")
|
||||
|
||||
const node2 = graph.getNode("src/file2.ts")
|
||||
expect(node2?.dependents).toContain("src/file1.ts")
|
||||
})
|
||||
|
||||
it("should get metrics", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const path1 = ProjectPath.create("/project/src/file1.ts", "/project")
|
||||
const path2 = ProjectPath.create("/project/src/file2.ts", "/project")
|
||||
const file1 = new SourceFile(path1, "")
|
||||
const file2 = new SourceFile(path2, "")
|
||||
|
||||
graph.addFile(file1)
|
||||
graph.addFile(file2)
|
||||
graph.addDependency("src/file1.ts", "src/file2.ts")
|
||||
|
||||
const metrics = graph.getMetrics()
|
||||
expect(metrics.totalFiles).toBe(2)
|
||||
expect(metrics.totalDependencies).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("findCycles", () => {
|
||||
it("should return empty array when no cycles exist", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const path1 = ProjectPath.create("/project/src/a.ts", "/project")
|
||||
const path2 = ProjectPath.create("/project/src/b.ts", "/project")
|
||||
const path3 = ProjectPath.create("/project/src/c.ts", "/project")
|
||||
|
||||
const fileA = new SourceFile(path1, "")
|
||||
const fileB = new SourceFile(path2, "")
|
||||
const fileC = new SourceFile(path3, "")
|
||||
|
||||
graph.addFile(fileA)
|
||||
graph.addFile(fileB)
|
||||
graph.addFile(fileC)
|
||||
|
||||
graph.addDependency("src/a.ts", "src/b.ts")
|
||||
graph.addDependency("src/b.ts", "src/c.ts")
|
||||
|
||||
const cycles = graph.findCycles()
|
||||
expect(cycles).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect simple two-file cycle (A → B → A)", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const pathA = ProjectPath.create("/project/src/a.ts", "/project")
|
||||
const pathB = ProjectPath.create("/project/src/b.ts", "/project")
|
||||
|
||||
const fileA = new SourceFile(pathA, "import { b } from './b'")
|
||||
const fileB = new SourceFile(pathB, "import { a } from './a'")
|
||||
|
||||
graph.addFile(fileA)
|
||||
graph.addFile(fileB)
|
||||
graph.addDependency("src/a.ts", "src/b.ts")
|
||||
graph.addDependency("src/b.ts", "src/a.ts")
|
||||
|
||||
const cycles = graph.findCycles()
|
||||
expect(cycles.length).toBeGreaterThan(0)
|
||||
|
||||
const cycle = cycles[0]
|
||||
expect(cycle).toContain("src/a.ts")
|
||||
expect(cycle).toContain("src/b.ts")
|
||||
})
|
||||
|
||||
it("should detect three-file cycle (A → B → C → A)", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const pathA = ProjectPath.create("/project/src/a.ts", "/project")
|
||||
const pathB = ProjectPath.create("/project/src/b.ts", "/project")
|
||||
const pathC = ProjectPath.create("/project/src/c.ts", "/project")
|
||||
|
||||
const fileA = new SourceFile(pathA, "")
|
||||
const fileB = new SourceFile(pathB, "")
|
||||
const fileC = new SourceFile(pathC, "")
|
||||
|
||||
graph.addFile(fileA)
|
||||
graph.addFile(fileB)
|
||||
graph.addFile(fileC)
|
||||
|
||||
graph.addDependency("src/a.ts", "src/b.ts")
|
||||
graph.addDependency("src/b.ts", "src/c.ts")
|
||||
graph.addDependency("src/c.ts", "src/a.ts")
|
||||
|
||||
const cycles = graph.findCycles()
|
||||
expect(cycles.length).toBeGreaterThan(0)
|
||||
|
||||
const cycle = cycles[0]
|
||||
expect(cycle).toContain("src/a.ts")
|
||||
expect(cycle).toContain("src/b.ts")
|
||||
expect(cycle).toContain("src/c.ts")
|
||||
})
|
||||
|
||||
it("should detect longer cycles (A → B → C → D → A)", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const pathA = ProjectPath.create("/project/src/a.ts", "/project")
|
||||
const pathB = ProjectPath.create("/project/src/b.ts", "/project")
|
||||
const pathC = ProjectPath.create("/project/src/c.ts", "/project")
|
||||
const pathD = ProjectPath.create("/project/src/d.ts", "/project")
|
||||
|
||||
const fileA = new SourceFile(pathA, "")
|
||||
const fileB = new SourceFile(pathB, "")
|
||||
const fileC = new SourceFile(pathC, "")
|
||||
const fileD = new SourceFile(pathD, "")
|
||||
|
||||
graph.addFile(fileA)
|
||||
graph.addFile(fileB)
|
||||
graph.addFile(fileC)
|
||||
graph.addFile(fileD)
|
||||
|
||||
graph.addDependency("src/a.ts", "src/b.ts")
|
||||
graph.addDependency("src/b.ts", "src/c.ts")
|
||||
graph.addDependency("src/c.ts", "src/d.ts")
|
||||
graph.addDependency("src/d.ts", "src/a.ts")
|
||||
|
||||
const cycles = graph.findCycles()
|
||||
expect(cycles.length).toBeGreaterThan(0)
|
||||
|
||||
const cycle = cycles[0]
|
||||
expect(cycle.length).toBe(4)
|
||||
expect(cycle).toContain("src/a.ts")
|
||||
expect(cycle).toContain("src/b.ts")
|
||||
expect(cycle).toContain("src/c.ts")
|
||||
expect(cycle).toContain("src/d.ts")
|
||||
})
|
||||
|
||||
it("should detect multiple independent cycles", () => {
|
||||
const graph = new DependencyGraph()
|
||||
|
||||
const pathA = ProjectPath.create("/project/src/a.ts", "/project")
|
||||
const pathB = ProjectPath.create("/project/src/b.ts", "/project")
|
||||
const pathC = ProjectPath.create("/project/src/c.ts", "/project")
|
||||
const pathD = ProjectPath.create("/project/src/d.ts", "/project")
|
||||
|
||||
const fileA = new SourceFile(pathA, "")
|
||||
const fileB = new SourceFile(pathB, "")
|
||||
const fileC = new SourceFile(pathC, "")
|
||||
const fileD = new SourceFile(pathD, "")
|
||||
|
||||
graph.addFile(fileA)
|
||||
graph.addFile(fileB)
|
||||
graph.addFile(fileC)
|
||||
graph.addFile(fileD)
|
||||
|
||||
graph.addDependency("src/a.ts", "src/b.ts")
|
||||
graph.addDependency("src/b.ts", "src/a.ts")
|
||||
|
||||
graph.addDependency("src/c.ts", "src/d.ts")
|
||||
graph.addDependency("src/d.ts", "src/c.ts")
|
||||
|
||||
const cycles = graph.findCycles()
|
||||
expect(cycles.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it("should handle complex graph with cycle and acyclic parts", () => {
|
||||
const graph = new DependencyGraph()
|
||||
|
||||
const pathA = ProjectPath.create("/project/src/a.ts", "/project")
|
||||
const pathB = ProjectPath.create("/project/src/b.ts", "/project")
|
||||
const pathC = ProjectPath.create("/project/src/c.ts", "/project")
|
||||
const pathD = ProjectPath.create("/project/src/d.ts", "/project")
|
||||
|
||||
const fileA = new SourceFile(pathA, "")
|
||||
const fileB = new SourceFile(pathB, "")
|
||||
const fileC = new SourceFile(pathC, "")
|
||||
const fileD = new SourceFile(pathD, "")
|
||||
|
||||
graph.addFile(fileA)
|
||||
graph.addFile(fileB)
|
||||
graph.addFile(fileC)
|
||||
graph.addFile(fileD)
|
||||
|
||||
graph.addDependency("src/a.ts", "src/b.ts")
|
||||
graph.addDependency("src/b.ts", "src/a.ts")
|
||||
|
||||
graph.addDependency("src/c.ts", "src/d.ts")
|
||||
|
||||
const cycles = graph.findCycles()
|
||||
expect(cycles.length).toBeGreaterThan(0)
|
||||
|
||||
const cycle = cycles[0]
|
||||
expect(cycle).toContain("src/a.ts")
|
||||
expect(cycle).toContain("src/b.ts")
|
||||
expect(cycle).not.toContain("src/c.ts")
|
||||
expect(cycle).not.toContain("src/d.ts")
|
||||
})
|
||||
|
||||
it("should handle single file without dependencies", () => {
|
||||
const graph = new DependencyGraph()
|
||||
const path = ProjectPath.create("/project/src/a.ts", "/project")
|
||||
const file = new SourceFile(path, "")
|
||||
|
||||
graph.addFile(file)
|
||||
|
||||
const cycles = graph.findCycles()
|
||||
expect(cycles).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
358
packages/guardian/tests/unit/domain/HardcodedValue.test.ts
Normal file
358
packages/guardian/tests/unit/domain/HardcodedValue.test.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { HardcodedValue } from "../../../src/domain/value-objects/HardcodedValue"
|
||||
import { HARDCODE_TYPES } from "../../../src/shared/constants"
|
||||
|
||||
describe("HardcodedValue", () => {
|
||||
describe("create", () => {
|
||||
it("should create a magic number value", () => {
|
||||
const value = HardcodedValue.create(
|
||||
5000,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
10,
|
||||
20,
|
||||
"setTimeout(() => {}, 5000)",
|
||||
)
|
||||
|
||||
expect(value.value).toBe(5000)
|
||||
expect(value.type).toBe(HARDCODE_TYPES.MAGIC_NUMBER)
|
||||
expect(value.line).toBe(10)
|
||||
expect(value.column).toBe(20)
|
||||
expect(value.context).toBe("setTimeout(() => {}, 5000)")
|
||||
})
|
||||
|
||||
it("should create a magic string value", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"http://localhost:8080",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
5,
|
||||
15,
|
||||
'const url = "http://localhost:8080"',
|
||||
)
|
||||
|
||||
expect(value.value).toBe("http://localhost:8080")
|
||||
expect(value.type).toBe(HARDCODE_TYPES.MAGIC_STRING)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isMagicNumber", () => {
|
||||
it("should return true for magic numbers", () => {
|
||||
const value = HardcodedValue.create(
|
||||
3000,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"timeout = 3000",
|
||||
)
|
||||
|
||||
expect(value.isMagicNumber()).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for magic strings", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"some string",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
"const str = 'some string'",
|
||||
)
|
||||
|
||||
expect(value.isMagicNumber()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isMagicString", () => {
|
||||
it("should return true for magic strings", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"http://localhost",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'url = "http://localhost"',
|
||||
)
|
||||
|
||||
expect(value.isMagicString()).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for magic numbers", () => {
|
||||
const value = HardcodedValue.create(
|
||||
8080,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"port = 8080",
|
||||
)
|
||||
|
||||
expect(value.isMagicString()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("suggestConstantName for numbers", () => {
|
||||
it("should suggest TIMEOUT_MS for timeout context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
5000,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const timeout = 5000",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("TIMEOUT_MS")
|
||||
})
|
||||
|
||||
it("should suggest MAX_RETRIES for retry context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
3,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const retry = 3",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("MAX_RETRIES")
|
||||
})
|
||||
|
||||
it("should suggest MAX_RETRIES for attempts context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
5,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const attempts = 5",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("MAX_RETRIES")
|
||||
})
|
||||
|
||||
it("should suggest MAX_LIMIT for limit context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
100,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const limit = 100",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("MAX_LIMIT")
|
||||
})
|
||||
|
||||
it("should suggest MAX_LIMIT for max context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
50,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const max = 50",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("MAX_LIMIT")
|
||||
})
|
||||
|
||||
it("should suggest DEFAULT_PORT for port context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
8080,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const port = 8080",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("DEFAULT_PORT")
|
||||
})
|
||||
|
||||
it("should suggest DELAY_MS for delay context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
1000,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const delay = 1000",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("DELAY_MS")
|
||||
})
|
||||
|
||||
it("should suggest MAGIC_NUMBER_<value> for unknown context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
999,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"const x = 999",
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("MAGIC_NUMBER_999")
|
||||
})
|
||||
})
|
||||
|
||||
describe("suggestConstantName for strings", () => {
|
||||
it("should suggest API_BASE_URL for http URLs", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"http://localhost:3000",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const url = "http://localhost:3000"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("API_BASE_URL")
|
||||
})
|
||||
|
||||
it("should suggest API_BASE_URL for https URLs", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"https://api.example.com",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const url = "https://api.example.com"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("API_BASE_URL")
|
||||
})
|
||||
|
||||
it("should suggest DEFAULT_DOMAIN for domain-like strings", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"example.com",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const domain = "example.com"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("DEFAULT_DOMAIN")
|
||||
})
|
||||
|
||||
it("should suggest DEFAULT_PATH for path-like strings", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"api.example.com/users",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const path = "api.example.com/users"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("DEFAULT_PATH")
|
||||
})
|
||||
|
||||
it("should suggest ERROR_MESSAGE for error context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"Something went wrong",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const error = "Something went wrong"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("ERROR_MESSAGE")
|
||||
})
|
||||
|
||||
it("should suggest ERROR_MESSAGE for message context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"Invalid input",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const message = "Invalid input"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("ERROR_MESSAGE")
|
||||
})
|
||||
|
||||
it("should suggest DEFAULT_VALUE for default context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"default value",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const default = "default value"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("DEFAULT_VALUE")
|
||||
})
|
||||
|
||||
it("should suggest MAGIC_STRING for unknown context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"some random string",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'const x = "some random string"',
|
||||
)
|
||||
|
||||
expect(value.suggestConstantName()).toBe("MAGIC_STRING")
|
||||
})
|
||||
})
|
||||
|
||||
describe("suggestLocation", () => {
|
||||
it("should suggest shared/constants when no layer specified", () => {
|
||||
const value = HardcodedValue.create(
|
||||
5000,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"timeout = 5000",
|
||||
)
|
||||
|
||||
expect(value.suggestLocation()).toBe("shared/constants")
|
||||
})
|
||||
|
||||
it("should suggest shared/constants for general values", () => {
|
||||
const value = HardcodedValue.create(
|
||||
8080,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"port = 8080",
|
||||
)
|
||||
|
||||
expect(value.suggestLocation("infrastructure")).toBe("shared/constants")
|
||||
})
|
||||
|
||||
it("should suggest layer/constants for domain context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
100,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"entity limit = 100",
|
||||
)
|
||||
|
||||
expect(value.suggestLocation("domain")).toBe("domain/constants")
|
||||
})
|
||||
|
||||
it("should suggest layer/constants for aggregate context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
50,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"aggregate max = 50",
|
||||
)
|
||||
|
||||
expect(value.suggestLocation("domain")).toBe("domain/constants")
|
||||
})
|
||||
|
||||
it("should suggest infrastructure/config for config context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
3000,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
1,
|
||||
1,
|
||||
"config timeout = 3000",
|
||||
)
|
||||
|
||||
expect(value.suggestLocation("infrastructure")).toBe("infrastructure/config")
|
||||
})
|
||||
|
||||
it("should suggest infrastructure/config for env context", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"production",
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
1,
|
||||
1,
|
||||
'env mode = "production"',
|
||||
)
|
||||
|
||||
expect(value.suggestLocation("infrastructure")).toBe("infrastructure/config")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,471 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { HardcodeDetector } from "../../../src/infrastructure/analyzers/HardcodeDetector"
|
||||
import { HARDCODE_TYPES } from "../../../src/shared/constants"
|
||||
|
||||
describe("HardcodeDetector", () => {
|
||||
let detector: HardcodeDetector
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new HardcodeDetector()
|
||||
})
|
||||
|
||||
describe("detectMagicNumbers", () => {
|
||||
describe("setTimeout and setInterval", () => {
|
||||
it("should detect timeout values in setTimeout", () => {
|
||||
const code = `setTimeout(() => {}, 5000)`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value === 5000)).toBe(true)
|
||||
expect(result[0].type).toBe(HARDCODE_TYPES.MAGIC_NUMBER)
|
||||
expect(result[0].line).toBe(1)
|
||||
})
|
||||
|
||||
it("should detect interval values in setInterval", () => {
|
||||
const code = `setInterval(() => {}, 3000)`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value === 3000)).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect multiple timeout values", () => {
|
||||
const code = `
|
||||
setTimeout(() => {}, 5000)
|
||||
setTimeout(() => {}, 10000)
|
||||
setInterval(() => {}, 3000)
|
||||
`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThanOrEqual(3)
|
||||
const values = result.map((r) => r.value)
|
||||
expect(values).toContain(5000)
|
||||
expect(values).toContain(10000)
|
||||
expect(values).toContain(3000)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retry and attempts", () => {
|
||||
it("should detect maxRetries values", () => {
|
||||
const code = `const maxRetries = 3`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe(3)
|
||||
})
|
||||
|
||||
it("should detect retries values", () => {
|
||||
const code = `const retries = 5`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe(5)
|
||||
})
|
||||
|
||||
it("should detect attempts values", () => {
|
||||
const code = `const attempts = 7`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ports", () => {
|
||||
it("should detect lowercase port", () => {
|
||||
const code = `const port = 8080`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value === 8080)).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect uppercase PORT", () => {
|
||||
const code = `const PORT = 3000`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value === 3000)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("limits", () => {
|
||||
it("should detect limit values", () => {
|
||||
const code = `const limit = 50`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe(50)
|
||||
})
|
||||
|
||||
it("should detect max values", () => {
|
||||
const code = `const max = 150`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value === 150)).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect min values", () => {
|
||||
const code = `const min = 15`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe(15)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delay and timeout", () => {
|
||||
it("should detect delay values", () => {
|
||||
const code = `const delay = 2000`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value === 2000)).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect timeout values", () => {
|
||||
const code = `const timeout = 5000`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value === 5000)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("allowed numbers", () => {
|
||||
it("should NOT detect -1", () => {
|
||||
const code = `const notFound = -1`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect 0", () => {
|
||||
const code = `const index = 0`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect 1", () => {
|
||||
const code = `const increment = 1`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect 2", () => {
|
||||
const code = `const pair = 2`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect 10, 100, 1000", () => {
|
||||
const code = `
|
||||
const ten = 10
|
||||
const hundred = 100
|
||||
const thousand = 1000
|
||||
`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("exported constants", () => {
|
||||
it("should NOT detect numbers in single-line export const with as const", () => {
|
||||
const code = `export const CONFIG = { timeout: 5000 } as const`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect numbers in multi-line export const with as const", () => {
|
||||
const code = `
|
||||
export const CONFIG = {
|
||||
timeout: 5000,
|
||||
port: 8080,
|
||||
retries: 3,
|
||||
} as const
|
||||
`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect numbers in nested export const", () => {
|
||||
const code = `
|
||||
export const SETTINGS = {
|
||||
api: {
|
||||
timeout: 10000,
|
||||
port: 3000,
|
||||
},
|
||||
db: {
|
||||
port: 5432,
|
||||
},
|
||||
} as const
|
||||
`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect numbers in export const WITHOUT as const", () => {
|
||||
const code = `export const CONFIG = { timeout: 5000 }`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("comments and strings", () => {
|
||||
it("should NOT detect numbers in comments", () => {
|
||||
const code = `// timeout is 5000ms`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect numbers in multi-line comments", () => {
|
||||
const code = `
|
||||
/*
|
||||
* timeout: 5000
|
||||
* port: 8080
|
||||
*/
|
||||
`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("generic 3+ digit numbers", () => {
|
||||
it("should detect suspicious 3-digit numbers with config context", () => {
|
||||
const code = `const timeout = 500`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should NOT detect 3-digit numbers without context", () => {
|
||||
const code = `const x = 123`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectMagicStrings", () => {
|
||||
describe("URLs and API endpoints", () => {
|
||||
it("should detect http URLs", () => {
|
||||
const code = `const url = "http://localhost:8080"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe("http://localhost:8080")
|
||||
expect(result[0].type).toBe(HARDCODE_TYPES.MAGIC_STRING)
|
||||
})
|
||||
|
||||
it("should detect https URLs", () => {
|
||||
const code = `const url = "https://api.example.com"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe("https://api.example.com")
|
||||
})
|
||||
|
||||
it("should detect mongodb connection strings", () => {
|
||||
const code = `const dbUrl = "mongodb://localhost:27017/mydb"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe("mongodb://localhost:27017/mydb")
|
||||
})
|
||||
})
|
||||
|
||||
describe("allowed strings", () => {
|
||||
it("should NOT detect single character strings", () => {
|
||||
const code = `const char = "a"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect empty strings", () => {
|
||||
const code = `const empty = ""`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect short strings (3 chars or less)", () => {
|
||||
const code = `const short = "abc"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("console logs", () => {
|
||||
it("should NOT detect strings in console.log", () => {
|
||||
const code = `console.log("Debug message")`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect strings in console.error", () => {
|
||||
const code = `console.error("Error occurred")`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("test descriptions", () => {
|
||||
it("should NOT detect strings in test()", () => {
|
||||
const code = `test("should work correctly", () => {})`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect strings in describe()", () => {
|
||||
const code = `describe("test suite", () => {})`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("imports", () => {
|
||||
it("should NOT detect strings in import statements", () => {
|
||||
const code = `import { foo } from "some-package"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect strings in require statements", () => {
|
||||
const code = `const foo = require("package-name")`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeLessThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("template literals", () => {
|
||||
it("should NOT detect template literals with interpolation", () => {
|
||||
const code = "const url = `http://localhost:${port}`"
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect backtick strings", () => {
|
||||
const code = "`some string`"
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("exported constants", () => {
|
||||
it("should NOT detect strings in single-line export const", () => {
|
||||
const code = `export const API_URL = "http://localhost" as const`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect strings in multi-line export const", () => {
|
||||
const code = `
|
||||
export const CONFIG = {
|
||||
baseUrl: "http://localhost:3000",
|
||||
apiKey: "secret-key",
|
||||
} as const
|
||||
`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should detect long meaningful strings", () => {
|
||||
const code = `const message = "Something went wrong"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].value).toBe("Something went wrong")
|
||||
})
|
||||
|
||||
it("should handle multiple strings on same line", () => {
|
||||
const code = `const a = "https://api.example.com"; const b = "another-url"`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.value.includes("api.example.com"))).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle different quote types", () => {
|
||||
const code = `
|
||||
const single = 'http://localhost'
|
||||
const double = "http://localhost"
|
||||
`
|
||||
const result = detector.detectMagicStrings(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectAll", () => {
|
||||
it("should detect both magic numbers and strings", () => {
|
||||
const code = `
|
||||
const timeout = 5000
|
||||
const url = "http://localhost:8080"
|
||||
`
|
||||
const result = detector.detectAll(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.isMagicNumber())).toBe(true)
|
||||
expect(result.some((r) => r.isMagicString())).toBe(true)
|
||||
})
|
||||
|
||||
it("should return empty array for clean code", () => {
|
||||
const code = `
|
||||
const index = 0
|
||||
const increment = 1
|
||||
console.log("debug")
|
||||
`
|
||||
const result = detector.detectAll(code, "test.ts")
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("context and line numbers", () => {
|
||||
it("should provide correct line numbers", () => {
|
||||
const code = `const a = 1
|
||||
const timeout = 5000
|
||||
const b = 2`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.some((r) => r.line === 2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should provide context string", () => {
|
||||
const code = `const timeout = 5000`
|
||||
const result = detector.detectMagicNumbers(code, "test.ts")
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result[0].context).toContain("timeout")
|
||||
expect(result[0].context).toContain("5000")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,734 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { NamingConventionDetector } from "../../../src/infrastructure/analyzers/NamingConventionDetector"
|
||||
import { LAYERS, NAMING_VIOLATION_TYPES } from "../../../src/shared/constants"
|
||||
|
||||
describe("NamingConventionDetector", () => {
|
||||
let detector: NamingConventionDetector
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new NamingConventionDetector()
|
||||
})
|
||||
|
||||
describe("Excluded Files", () => {
|
||||
it("should NOT detect violations for index.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"index.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/index.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for BaseUseCase.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"BaseUseCase.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/use-cases/BaseUseCase.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for BaseMapper.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"BaseMapper.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/mappers/BaseMapper.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for IBaseRepository.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"IBaseRepository.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/repositories/IBaseRepository.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for BaseEntity.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"BaseEntity.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/entities/BaseEntity.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for ValueObject.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"ValueObject.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/value-objects/ValueObject.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for BaseRepository.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"BaseRepository.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/repositories/BaseRepository.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for BaseError.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"BaseError.ts",
|
||||
LAYERS.SHARED,
|
||||
"src/shared/errors/BaseError.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for Suggestions.ts", () => {
|
||||
const result = detector.detectViolations(
|
||||
"Suggestions.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/constants/Suggestions.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Domain Layer", () => {
|
||||
describe("Entities (PascalCase nouns)", () => {
|
||||
it("should NOT detect violations for valid entity names", () => {
|
||||
const validNames = [
|
||||
"User.ts",
|
||||
"Order.ts",
|
||||
"Product.ts",
|
||||
"Email.ts",
|
||||
"ProjectPath.ts",
|
||||
]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.DOMAIN,
|
||||
`src/domain/entities/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase entity names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"user.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/entities/user.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
|
||||
expect(result[0].layer).toBe(LAYERS.DOMAIN)
|
||||
})
|
||||
|
||||
it("should detect violations for camelCase entity names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"userProfile.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/entities/userProfile.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
|
||||
})
|
||||
|
||||
it("should detect violations for kebab-case entity names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"user-profile.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/entities/user-profile.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Services (*Service.ts)", () => {
|
||||
it("should NOT detect violations for valid service names", () => {
|
||||
const validNames = ["UserService.ts", "EmailService.ts", "PaymentService.ts"]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.DOMAIN,
|
||||
`src/domain/services/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase service names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"userService.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/services/userService.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
|
||||
})
|
||||
|
||||
it("should detect violations for service names without suffix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"User.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/services/User.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Repository Interfaces (I*Repository.ts)", () => {
|
||||
it("should NOT detect violations for valid repository interface names", () => {
|
||||
const validNames = [
|
||||
"IUserRepository.ts",
|
||||
"IOrderRepository.ts",
|
||||
"IProductRepository.ts",
|
||||
]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.DOMAIN,
|
||||
`src/domain/repositories/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for repository interfaces without I prefix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"UserRepository.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/repositories/UserRepository.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase I prefix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"iUserRepository.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/repositories/iUserRepository.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_CASE)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Forbidden Patterns", () => {
|
||||
it("should detect Dto in domain layer", () => {
|
||||
const result = detector.detectViolations(
|
||||
"UserDto.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/UserDto.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.FORBIDDEN_PATTERN)
|
||||
expect(result[0].getMessage()).toContain("should not contain DTOs")
|
||||
})
|
||||
|
||||
it("should detect Request in domain layer", () => {
|
||||
const result = detector.detectViolations(
|
||||
"CreateUserRequest.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/CreateUserRequest.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.FORBIDDEN_PATTERN)
|
||||
})
|
||||
|
||||
it("should detect Response in domain layer", () => {
|
||||
const result = detector.detectViolations(
|
||||
"UserResponse.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/UserResponse.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.FORBIDDEN_PATTERN)
|
||||
})
|
||||
|
||||
it("should detect Controller in domain layer", () => {
|
||||
const result = detector.detectViolations(
|
||||
"UserController.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/UserController.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.FORBIDDEN_PATTERN)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Value Objects", () => {
|
||||
it("should NOT detect violations for valid value object names", () => {
|
||||
const validNames = ["Email.ts", "Money.ts", "Address.ts", "PhoneNumber.ts"]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.DOMAIN,
|
||||
`src/domain/value-objects/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Application Layer", () => {
|
||||
describe("Use Cases (Verb+Noun)", () => {
|
||||
it("should NOT detect violations for valid use case names", () => {
|
||||
const validNames = [
|
||||
"CreateUser.ts",
|
||||
"UpdateProfile.ts",
|
||||
"DeleteOrder.ts",
|
||||
"GetUser.ts",
|
||||
"FindProducts.ts",
|
||||
"AnalyzeProject.ts",
|
||||
"ValidateEmail.ts",
|
||||
"GenerateReport.ts",
|
||||
]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.APPLICATION,
|
||||
`src/application/use-cases/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for use cases starting with lowercase", () => {
|
||||
const result = detector.detectViolations(
|
||||
"createUser.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/use-cases/createUser.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN)
|
||||
})
|
||||
|
||||
it("should detect violations for use cases without verb", () => {
|
||||
const result = detector.detectViolations(
|
||||
"User.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/use-cases/User.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN)
|
||||
expect(result[0].getMessage()).toContain("should start with a verb")
|
||||
})
|
||||
|
||||
it("should detect violations for kebab-case use cases", () => {
|
||||
const result = detector.detectViolations(
|
||||
"create-user.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/use-cases/create-user.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN)
|
||||
})
|
||||
|
||||
it("should recognize all standard verbs", () => {
|
||||
const verbs = [
|
||||
"Analyze",
|
||||
"Create",
|
||||
"Update",
|
||||
"Delete",
|
||||
"Get",
|
||||
"Find",
|
||||
"List",
|
||||
"Search",
|
||||
"Validate",
|
||||
"Calculate",
|
||||
"Generate",
|
||||
"Send",
|
||||
"Fetch",
|
||||
"Process",
|
||||
"Execute",
|
||||
"Handle",
|
||||
"Register",
|
||||
"Authenticate",
|
||||
"Authorize",
|
||||
"Import",
|
||||
"Export",
|
||||
]
|
||||
|
||||
verbs.forEach((verb) => {
|
||||
const fileName = `${verb}Something.ts`
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.APPLICATION,
|
||||
`src/application/use-cases/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("DTOs (*Dto, *Request, *Response)", () => {
|
||||
it("should NOT detect violations for valid DTO names", () => {
|
||||
const validNames = [
|
||||
"UserDto.ts",
|
||||
"CreateUserRequest.ts",
|
||||
"UserResponseDto.ts",
|
||||
"UpdateProfileRequest.ts",
|
||||
"OrderResponse.ts",
|
||||
]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.APPLICATION,
|
||||
`src/application/dtos/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase DTO names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"userDto.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/dtos/userDto.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
|
||||
})
|
||||
|
||||
it("should detect violations for DTOs without proper suffix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"User.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/dtos/User.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should NOT detect violations for camelCase before suffix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"CreateUserRequestDto.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/dtos/CreateUserRequestDto.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Mappers (*Mapper)", () => {
|
||||
it("should NOT detect violations for valid mapper names", () => {
|
||||
const validNames = ["UserMapper.ts", "OrderMapper.ts", "ProductMapper.ts"]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.APPLICATION,
|
||||
`src/application/mappers/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase mapper names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"userMapper.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/mappers/userMapper.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
|
||||
})
|
||||
|
||||
it("should detect violations for mappers without suffix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"User.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/mappers/User.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Infrastructure Layer", () => {
|
||||
describe("Controllers (*Controller)", () => {
|
||||
it("should NOT detect violations for valid controller names", () => {
|
||||
const validNames = [
|
||||
"UserController.ts",
|
||||
"OrderController.ts",
|
||||
"ProductController.ts",
|
||||
]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
`src/infrastructure/controllers/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase controller names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"userController.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/controllers/userController.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
|
||||
})
|
||||
|
||||
it("should detect violations for controllers without suffix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"User.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/controllers/User.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Repository Implementations (*Repository)", () => {
|
||||
it("should NOT detect violations for valid repository implementation names", () => {
|
||||
const validNames = [
|
||||
"UserRepository.ts",
|
||||
"PrismaUserRepository.ts",
|
||||
"MongoUserRepository.ts",
|
||||
"InMemoryUserRepository.ts",
|
||||
]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
`src/infrastructure/repositories/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should NOT detect violations for I*Repository (interface) in infrastructure", () => {
|
||||
const result = detector.detectViolations(
|
||||
"IUserRepository.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/repositories/IUserRepository.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase repository names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"userRepository.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/repositories/userRepository.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Services (*Service, *Adapter)", () => {
|
||||
it("should NOT detect violations for valid service names", () => {
|
||||
const validNames = [
|
||||
"EmailService.ts",
|
||||
"S3StorageAdapter.ts",
|
||||
"PaymentService.ts",
|
||||
"LoggerAdapter.ts",
|
||||
]
|
||||
|
||||
validNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
`src/infrastructure/services/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect violations for lowercase service names", () => {
|
||||
const result = detector.detectViolations(
|
||||
"emailService.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/services/emailService.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_SUFFIX)
|
||||
})
|
||||
|
||||
it("should detect violations for services without suffix", () => {
|
||||
const result = detector.detectViolations(
|
||||
"Email.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/services/Email.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Shared Layer", () => {
|
||||
it("should NOT detect violations for any file in shared layer", () => {
|
||||
const fileNames = [
|
||||
"helpers.ts",
|
||||
"utils.ts",
|
||||
"constants.ts",
|
||||
"types.ts",
|
||||
"Guards.ts",
|
||||
"Result.ts",
|
||||
"anything.ts",
|
||||
]
|
||||
|
||||
fileNames.forEach((fileName) => {
|
||||
const result = detector.detectViolations(
|
||||
fileName,
|
||||
LAYERS.SHARED,
|
||||
`src/shared/${fileName}`,
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should return empty array when no layer is provided", () => {
|
||||
const result = detector.detectViolations("SomeFile.ts", undefined, "src/SomeFile.ts")
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should return empty array for unknown layer", () => {
|
||||
const result = detector.detectViolations(
|
||||
"SomeFile.ts",
|
||||
"unknown-layer",
|
||||
"src/unknown/SomeFile.ts",
|
||||
)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle files with numbers in name", () => {
|
||||
const result = detector.detectViolations(
|
||||
"User2Factor.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/entities/User2Factor.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should provide helpful suggestions", () => {
|
||||
const result = detector.detectViolations(
|
||||
"userDto.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/dtos/userDto.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].suggestion).toBeDefined()
|
||||
expect(result[0].suggestion).toContain("*Dto")
|
||||
})
|
||||
|
||||
it("should include file path in violation", () => {
|
||||
const filePath = "src/domain/UserDto.ts"
|
||||
const result = detector.detectViolations("UserDto.ts", LAYERS.DOMAIN, filePath)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].filePath).toBe(filePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Complex Scenarios", () => {
|
||||
it("should handle application layer file that looks like entity", () => {
|
||||
const result = detector.detectViolations(
|
||||
"User.ts",
|
||||
LAYERS.APPLICATION,
|
||||
"src/application/use-cases/User.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].violationType).toBe(NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN)
|
||||
})
|
||||
|
||||
it("should handle domain layer service vs entity distinction", () => {
|
||||
const entityResult = detector.detectViolations(
|
||||
"User.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/entities/User.ts",
|
||||
)
|
||||
expect(entityResult).toHaveLength(0)
|
||||
|
||||
const serviceResult = detector.detectViolations(
|
||||
"UserService.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/services/UserService.ts",
|
||||
)
|
||||
expect(serviceResult).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should distinguish between domain and infrastructure repositories", () => {
|
||||
const interfaceResult = detector.detectViolations(
|
||||
"IUserRepository.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/repositories/IUserRepository.ts",
|
||||
)
|
||||
expect(interfaceResult).toHaveLength(0)
|
||||
|
||||
const implResult = detector.detectViolations(
|
||||
"UserRepository.ts",
|
||||
LAYERS.INFRASTRUCTURE,
|
||||
"src/infrastructure/repositories/UserRepository.ts",
|
||||
)
|
||||
expect(implResult).toHaveLength(0)
|
||||
|
||||
const wrongResult = detector.detectViolations(
|
||||
"UserRepository.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/repositories/UserRepository.ts",
|
||||
)
|
||||
expect(wrongResult).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessage()", () => {
|
||||
it("should return descriptive error messages", () => {
|
||||
const result = detector.detectViolations(
|
||||
"UserDto.ts",
|
||||
LAYERS.DOMAIN,
|
||||
"src/domain/UserDto.ts",
|
||||
)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const message = result[0].getMessage()
|
||||
expect(message).toBeTruthy()
|
||||
expect(typeof message).toBe("string")
|
||||
expect(message.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
57
packages/guardian/tests/unit/shared/Guards.test.ts
Normal file
57
packages/guardian/tests/unit/shared/Guards.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { Guards } from "../../../src/shared/utils/Guards"
|
||||
|
||||
describe("Guards", () => {
|
||||
describe("isNullOrUndefined", () => {
|
||||
it("should return true for null", () => {
|
||||
expect(Guards.isNullOrUndefined(null)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true for undefined", () => {
|
||||
expect(Guards.isNullOrUndefined(undefined)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for other values", () => {
|
||||
expect(Guards.isNullOrUndefined(0)).toBe(false)
|
||||
expect(Guards.isNullOrUndefined("")).toBe(false)
|
||||
expect(Guards.isNullOrUndefined(false)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isString", () => {
|
||||
it("should return true for strings", () => {
|
||||
expect(Guards.isString("hello")).toBe(true)
|
||||
expect(Guards.isString("")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-strings", () => {
|
||||
expect(Guards.isString(123)).toBe(false)
|
||||
expect(Guards.isString(null)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isEmpty", () => {
|
||||
it("should return true for empty strings", () => {
|
||||
expect(Guards.isEmpty("")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true for empty arrays", () => {
|
||||
expect(Guards.isEmpty([])).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true for empty objects", () => {
|
||||
expect(Guards.isEmpty({})).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true for null/undefined", () => {
|
||||
expect(Guards.isEmpty(null)).toBe(true)
|
||||
expect(Guards.isEmpty(undefined)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-empty values", () => {
|
||||
expect(Guards.isEmpty("text")).toBe(false)
|
||||
expect(Guards.isEmpty([1])).toBe(false)
|
||||
expect(Guards.isEmpty({ key: "value" })).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user