mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
refactor: migrate hardcode detector from regex to AST-based analysis
- Replace regex-based matchers with tree-sitter AST traversal - Add duplicate value tracking across files - Implement boolean literal detection - Add value type classification (email, url, ip, api_key, etc.) - Improve context awareness with AST node analysis - Reduce false positives with better constant detection Breaking changes removed: - BraceTracker.ts - ExportConstantAnalyzer.ts - MagicNumberMatcher.ts - MagicStringMatcher.ts New components added: - AstTreeTraverser for AST walking - DuplicateValueTracker for cross-file tracking - AstContextChecker for node context analysis - AstNumberAnalyzer, AstStringAnalyzer, AstBooleanAnalyzer - ValuePatternMatcher for type detection Test coverage: 87.97% statements, 96.75% functions
This commit is contained in:
358
packages/guardian/tests/unit/domain/EntityExposure.test.ts
Normal file
358
packages/guardian/tests/unit/domain/EntityExposure.test.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { EntityExposure } from "../../../src/domain/value-objects/EntityExposure"
|
||||
|
||||
describe("EntityExposure", () => {
|
||||
describe("create", () => {
|
||||
it("should create entity exposure with all properties", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
expect(exposure.entityName).toBe("User")
|
||||
expect(exposure.returnType).toBe("User")
|
||||
expect(exposure.filePath).toBe("src/controllers/UserController.ts")
|
||||
expect(exposure.layer).toBe("infrastructure")
|
||||
expect(exposure.line).toBe(25)
|
||||
expect(exposure.methodName).toBe("getUser")
|
||||
})
|
||||
|
||||
it("should create entity exposure without optional properties", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
)
|
||||
|
||||
expect(exposure.entityName).toBe("Order")
|
||||
expect(exposure.line).toBeUndefined()
|
||||
expect(exposure.methodName).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should create entity exposure with line but without method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Product",
|
||||
"Product",
|
||||
"src/api/ProductApi.ts",
|
||||
"infrastructure",
|
||||
15,
|
||||
)
|
||||
|
||||
expect(exposure.line).toBe(15)
|
||||
expect(exposure.methodName).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessage", () => {
|
||||
it("should return message with method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
const message = exposure.getMessage()
|
||||
|
||||
expect(message).toContain("Method 'getUser'")
|
||||
expect(message).toContain("returns domain entity 'User'")
|
||||
expect(message).toContain("instead of DTO")
|
||||
})
|
||||
|
||||
it("should return message without method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
30,
|
||||
)
|
||||
|
||||
const message = exposure.getMessage()
|
||||
|
||||
expect(message).toContain("returns domain entity 'Order'")
|
||||
expect(message).toContain("instead of DTO")
|
||||
expect(message).not.toContain("undefined")
|
||||
})
|
||||
|
||||
it("should handle different entity names", () => {
|
||||
const exposures = [
|
||||
EntityExposure.create(
|
||||
"Customer",
|
||||
"Customer",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
1,
|
||||
"getCustomer",
|
||||
),
|
||||
EntityExposure.create(
|
||||
"Invoice",
|
||||
"Invoice",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
2,
|
||||
"findInvoice",
|
||||
),
|
||||
EntityExposure.create(
|
||||
"Payment",
|
||||
"Payment",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
3,
|
||||
"processPayment",
|
||||
),
|
||||
]
|
||||
|
||||
exposures.forEach((exposure) => {
|
||||
const message = exposure.getMessage()
|
||||
expect(message).toContain(exposure.entityName)
|
||||
expect(message).toContain("instead of DTO")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSuggestion", () => {
|
||||
it("should return multi-line suggestion", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
const suggestion = exposure.getSuggestion()
|
||||
|
||||
expect(suggestion).toContain("Create a DTO class")
|
||||
expect(suggestion).toContain("UserResponseDto")
|
||||
expect(suggestion).toContain("Create a mapper")
|
||||
expect(suggestion).toContain("Update the method")
|
||||
})
|
||||
|
||||
it("should suggest appropriate DTO name based on entity", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
)
|
||||
|
||||
const suggestion = exposure.getSuggestion()
|
||||
|
||||
expect(suggestion).toContain("OrderResponseDto")
|
||||
expect(suggestion).toContain("convert Order to OrderResponseDto")
|
||||
})
|
||||
|
||||
it("should provide step-by-step suggestions", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Product",
|
||||
"Product",
|
||||
"src/api/ProductApi.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
)
|
||||
|
||||
const suggestion = exposure.getSuggestion()
|
||||
const lines = suggestion.split("\n")
|
||||
|
||||
expect(lines.length).toBeGreaterThan(1)
|
||||
expect(lines.some((line) => line.includes("Create a DTO"))).toBe(true)
|
||||
expect(lines.some((line) => line.includes("mapper"))).toBe(true)
|
||||
expect(lines.some((line) => line.includes("Update the method"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getExampleFix", () => {
|
||||
it("should return example with method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("Bad: Exposing domain entity")
|
||||
expect(example).toContain("Good: Using DTO")
|
||||
expect(example).toContain("getUser()")
|
||||
expect(example).toContain("Promise<User>")
|
||||
expect(example).toContain("Promise<UserResponseDto>")
|
||||
expect(example).toContain("UserMapper.toDto")
|
||||
})
|
||||
|
||||
it("should return example without method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
30,
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("Promise<Order>")
|
||||
expect(example).toContain("Promise<OrderResponseDto>")
|
||||
expect(example).toContain("OrderMapper.toDto")
|
||||
expect(example).not.toContain("undefined")
|
||||
})
|
||||
|
||||
it("should show both bad and good examples", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Product",
|
||||
"Product",
|
||||
"src/api/ProductApi.ts",
|
||||
"infrastructure",
|
||||
15,
|
||||
"findProduct",
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("❌ Bad")
|
||||
expect(example).toContain("✅ Good")
|
||||
})
|
||||
|
||||
it("should include async/await pattern", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Customer",
|
||||
"Customer",
|
||||
"src/api/CustomerApi.ts",
|
||||
"infrastructure",
|
||||
20,
|
||||
"getCustomer",
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("async")
|
||||
expect(example).toContain("await")
|
||||
})
|
||||
})
|
||||
|
||||
describe("value object behavior", () => {
|
||||
it("should be equal to another instance with same values", () => {
|
||||
const exposure1 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
const exposure2 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
expect(exposure1.equals(exposure2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not be equal to instance with different values", () => {
|
||||
const exposure1 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
const exposure2 = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
expect(exposure1.equals(exposure2)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not be equal to instance with different method name", () => {
|
||||
const exposure1 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
const exposure2 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"findUser",
|
||||
)
|
||||
|
||||
expect(exposure1.equals(exposure2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty entity name", () => {
|
||||
const exposure = EntityExposure.create("", "", "file.ts", "infrastructure")
|
||||
|
||||
expect(exposure.entityName).toBe("")
|
||||
expect(exposure.getMessage()).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should handle very long entity names", () => {
|
||||
const longName = "VeryLongEntityNameThatIsUnusuallyLong"
|
||||
const exposure = EntityExposure.create(longName, longName, "file.ts", "infrastructure")
|
||||
|
||||
expect(exposure.entityName).toBe(longName)
|
||||
const suggestion = exposure.getSuggestion()
|
||||
expect(suggestion).toContain(`${longName}ResponseDto`)
|
||||
})
|
||||
|
||||
it("should handle special characters in method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"get$User",
|
||||
)
|
||||
|
||||
const message = exposure.getMessage()
|
||||
expect(message).toContain("get$User")
|
||||
})
|
||||
|
||||
it("should handle line number 0", () => {
|
||||
const exposure = EntityExposure.create("User", "User", "file.ts", "infrastructure", 0)
|
||||
|
||||
expect(exposure.line).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle very large line numbers", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
999999,
|
||||
)
|
||||
|
||||
expect(exposure.line).toBe(999999)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,465 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { DuplicateValueTracker } from "../../../src/infrastructure/analyzers/DuplicateValueTracker"
|
||||
import { HardcodedValue } from "../../../src/domain/value-objects/HardcodedValue"
|
||||
|
||||
describe("DuplicateValueTracker", () => {
|
||||
let tracker: DuplicateValueTracker
|
||||
|
||||
beforeEach(() => {
|
||||
tracker = new DuplicateValueTracker()
|
||||
})
|
||||
|
||||
describe("track", () => {
|
||||
it("should track a single hardcoded value", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"test-value",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'test-value'",
|
||||
)
|
||||
|
||||
tracker.track(value, "file1.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should track multiple occurrences of the same value", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"test-value",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'test-value'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"test-value",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'test-value'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(1)
|
||||
expect(duplicates[0].value).toBe("test-value")
|
||||
expect(duplicates[0].count).toBe(2)
|
||||
})
|
||||
|
||||
it("should track values with different types separately", () => {
|
||||
const stringValue = HardcodedValue.create(
|
||||
"100",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = '100'",
|
||||
)
|
||||
const numberValue = HardcodedValue.create(100, "magic-number", 20, 5, "const y = 100")
|
||||
|
||||
tracker.track(stringValue, "file1.ts")
|
||||
tracker.track(numberValue, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should track boolean values", () => {
|
||||
const value1 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 10, 5, "const x = true")
|
||||
const value2 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 20, 5, "const y = true")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(1)
|
||||
expect(duplicates[0].value).toBe("true")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDuplicates", () => {
|
||||
it("should return empty array when no duplicates exist", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should return duplicates sorted by count in descending order", () => {
|
||||
const value1a = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value1b = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value1'",
|
||||
)
|
||||
const value2a = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
30,
|
||||
5,
|
||||
"const z = 'value2'",
|
||||
)
|
||||
const value2b = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
40,
|
||||
5,
|
||||
"const a = 'value2'",
|
||||
)
|
||||
const value2c = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
50,
|
||||
5,
|
||||
"const b = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1a, "file1.ts")
|
||||
tracker.track(value1b, "file2.ts")
|
||||
tracker.track(value2a, "file3.ts")
|
||||
tracker.track(value2b, "file4.ts")
|
||||
tracker.track(value2c, "file5.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(2)
|
||||
expect(duplicates[0].value).toBe("value2")
|
||||
expect(duplicates[0].count).toBe(3)
|
||||
expect(duplicates[1].value).toBe("value1")
|
||||
expect(duplicates[1].count).toBe(2)
|
||||
})
|
||||
|
||||
it("should include location information for duplicates", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates[0].locations).toHaveLength(2)
|
||||
expect(duplicates[0].locations[0]).toEqual({
|
||||
file: "file1.ts",
|
||||
line: 10,
|
||||
context: "const x = 'test'",
|
||||
})
|
||||
expect(duplicates[0].locations[1]).toEqual({
|
||||
file: "file2.ts",
|
||||
line: 20,
|
||||
context: "const y = 'test'",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDuplicateLocations", () => {
|
||||
it("should return null when value is not duplicated", () => {
|
||||
const value = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
|
||||
tracker.track(value, "file1.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations("test", "magic-string")
|
||||
expect(locations).toBeNull()
|
||||
})
|
||||
|
||||
it("should return locations when value is duplicated", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations("test", "magic-string")
|
||||
expect(locations).toHaveLength(2)
|
||||
expect(locations).toEqual([
|
||||
{ file: "file1.ts", line: 10, context: "const x = 'test'" },
|
||||
{ file: "file2.ts", line: 20, context: "const y = 'test'" },
|
||||
])
|
||||
})
|
||||
|
||||
it("should return null for non-existent value", () => {
|
||||
const locations = tracker.getDuplicateLocations("non-existent", "magic-string")
|
||||
expect(locations).toBeNull()
|
||||
})
|
||||
|
||||
it("should handle numeric values", () => {
|
||||
const value1 = HardcodedValue.create(100, "magic-number", 10, 5, "const x = 100")
|
||||
const value2 = HardcodedValue.create(100, "magic-number", 20, 5, "const y = 100")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations(100, "magic-number")
|
||||
expect(locations).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDuplicate", () => {
|
||||
it("should return false for non-duplicated value", () => {
|
||||
const value = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
|
||||
tracker.track(value, "file1.ts")
|
||||
|
||||
expect(tracker.isDuplicate("test", "magic-string")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true for duplicated value", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate("test", "magic-string")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-existent value", () => {
|
||||
expect(tracker.isDuplicate("non-existent", "magic-string")).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle boolean values", () => {
|
||||
const value1 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 10, 5, "const x = true")
|
||||
const value2 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 20, 5, "const y = true")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate(true, "MAGIC_BOOLEAN")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getStats", () => {
|
||||
it("should return zero stats for empty tracker", () => {
|
||||
const stats = tracker.getStats()
|
||||
|
||||
expect(stats.totalValues).toBe(0)
|
||||
expect(stats.duplicateValues).toBe(0)
|
||||
expect(stats.duplicatePercentage).toBe(0)
|
||||
})
|
||||
|
||||
it("should calculate stats correctly with no duplicates", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(2)
|
||||
expect(stats.duplicateValues).toBe(0)
|
||||
expect(stats.duplicatePercentage).toBe(0)
|
||||
})
|
||||
|
||||
it("should calculate stats correctly with duplicates", () => {
|
||||
const value1a = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value1b = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value1'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
30,
|
||||
5,
|
||||
"const z = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1a, "file1.ts")
|
||||
tracker.track(value1b, "file2.ts")
|
||||
tracker.track(value2, "file3.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(2)
|
||||
expect(stats.duplicateValues).toBe(1)
|
||||
expect(stats.duplicatePercentage).toBe(50)
|
||||
})
|
||||
|
||||
it("should handle multiple duplicates", () => {
|
||||
const value1a = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value1b = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value1'",
|
||||
)
|
||||
const value2a = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
30,
|
||||
5,
|
||||
"const z = 'value2'",
|
||||
)
|
||||
const value2b = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
40,
|
||||
5,
|
||||
"const a = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1a, "file1.ts")
|
||||
tracker.track(value1b, "file2.ts")
|
||||
tracker.track(value2a, "file3.ts")
|
||||
tracker.track(value2b, "file4.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(2)
|
||||
expect(stats.duplicateValues).toBe(2)
|
||||
expect(stats.duplicatePercentage).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe("clear", () => {
|
||||
it("should clear all tracked values", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.getDuplicates()).toHaveLength(1)
|
||||
|
||||
tracker.clear()
|
||||
|
||||
expect(tracker.getDuplicates()).toHaveLength(0)
|
||||
expect(tracker.getStats().totalValues).toBe(0)
|
||||
})
|
||||
|
||||
it("should allow tracking new values after clear", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"test1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'test1'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.clear()
|
||||
|
||||
const value2 = HardcodedValue.create(
|
||||
"test2",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'test2'",
|
||||
)
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle values with colons in them", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"url:http://example.com",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'url:http://example.com'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"url:http://example.com",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'url:http://example.com'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(1)
|
||||
expect(duplicates[0].value).toBe("url:http://example.com")
|
||||
})
|
||||
|
||||
it("should handle empty string values", () => {
|
||||
const value1 = HardcodedValue.create("", "magic-string", 10, 5, "const x = ''")
|
||||
const value2 = HardcodedValue.create("", "magic-string", 20, 5, "const y = ''")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate("", "magic-string")).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle zero as a number", () => {
|
||||
const value1 = HardcodedValue.create(0, "magic-number", 10, 5, "const x = 0")
|
||||
const value2 = HardcodedValue.create(0, "magic-number", 20, 5, "const y = 0")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate(0, "magic-number")).toBe(true)
|
||||
})
|
||||
|
||||
it("should track same file multiple times", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 5, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file1.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations("test", "magic-string")
|
||||
expect(locations).toHaveLength(2)
|
||||
expect(locations?.[0].file).toBe("file1.ts")
|
||||
expect(locations?.[1].file).toBe("file1.ts")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -274,4 +274,68 @@ describe("SecretDetector", () => {
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
|
||||
describe("real secret detection", () => {
|
||||
it("should detect AWS access key pattern", async () => {
|
||||
const code = `const awsKey = "AKIAIOSFODNN7EXAMPLE"`
|
||||
|
||||
const violations = await detector.detectAll(code, "aws.ts")
|
||||
|
||||
if (violations.length > 0) {
|
||||
expect(violations[0].secretType).toContain("AWS")
|
||||
}
|
||||
})
|
||||
|
||||
it("should detect basic auth credentials", async () => {
|
||||
const code = `const auth = "https://user:password@example.com"`
|
||||
|
||||
const violations = await detector.detectAll(code, "auth.ts")
|
||||
|
||||
if (violations.length > 0) {
|
||||
expect(violations[0].file).toBe("auth.ts")
|
||||
expect(violations[0].line).toBeGreaterThan(0)
|
||||
expect(violations[0].column).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it("should detect private SSH key", async () => {
|
||||
const code = `
|
||||
const privateKey = \`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBogIBAAJBALRiMLAA...
|
||||
-----END RSA PRIVATE KEY-----\`
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "ssh.ts")
|
||||
|
||||
if (violations.length > 0) {
|
||||
expect(violations[0].secretType).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it("should return violation objects with required properties", async () => {
|
||||
const code = `const key = "AKIAIOSFODNN7EXAMPLE"`
|
||||
|
||||
const violations = await detector.detectAll(code, "test.ts")
|
||||
|
||||
violations.forEach((v) => {
|
||||
expect(v).toHaveProperty("file")
|
||||
expect(v).toHaveProperty("line")
|
||||
expect(v).toHaveProperty("column")
|
||||
expect(v).toHaveProperty("secretType")
|
||||
expect(v.getMessage).toBeDefined()
|
||||
expect(v.getSuggestion).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle files with multiple secrets", async () => {
|
||||
const code = `
|
||||
const key1 = "AKIAIOSFODNN7EXAMPLE"
|
||||
const key2 = "AKIAIOSFODNN8EXAMPLE"
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "multiple.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user