revenge-bundle-next/lib/patcher/src/index.test.ts
2026-08-09 07:27:56 +00:00

665 lines
21 KiB
TypeScript

import { beforeEach, describe, expect, mock, test } from 'bun:test'
import { after, before, HookPriority, instead } from '.'
describe('Patcher', () => {
let originalMethod: (...args: any[]) => string
let obj: { method: typeof originalMethod }
beforeEach(() => {
originalMethod = mock(s => s ?? 'original')
obj = {
method: originalMethod,
}
})
describe('before', () => {
test('should execute callback before original method', () => {
const beforeCallback = mock(args => args)
before(obj, 'method', beforeCallback)
obj.method('arg1', 'arg2')
expect(beforeCallback).toHaveBeenCalledWith(['arg1', 'arg2'])
expect(originalMethod).toHaveBeenCalled()
})
test('should allow modifying arguments', () => {
before(obj, 'method', args => {
args[0] = 'modified'
return args
})
obj.method('original')
expect(originalMethod).toHaveBeenCalledWith('modified')
})
test('should unpatch correctly', () => {
const beforeCallback = mock(args => {
args[0] = 'modified'
return args
})
const unpatch = before(obj, 'method', beforeCallback)
// Test that patch is working
obj.method('original')
expect(originalMethod).toHaveBeenCalledWith('modified')
// Unpatch
unpatch()
// Test that patch is removed
obj.method('original')
expect(originalMethod).toHaveBeenCalledWith('original')
expect(beforeCallback).toHaveBeenCalledTimes(1) // Only called before unpatch
})
test('should handle multiple before hooks and unpatch correctly', () => {
const firstHook = mock(args => {
args[0] = 'first'
return args
})
const secondHook = mock(args => {
args[0] = `${args[0]}-second`
return args
})
const unpatch2 = before(obj, 'method', secondHook)
const unpatch1 = before(obj, 'method', firstHook)
obj.method('original')
expect(originalMethod).toHaveBeenCalledWith('first-second')
// Unpatch second hook
unpatch2()
obj.method('original')
expect(originalMethod).toHaveBeenCalledWith('first')
// Unpatch first hook
unpatch1()
obj.method('original')
expect(originalMethod).toHaveBeenCalledWith('original')
})
test('should not crash when unpatch is called multiple times', () => {
const beforeCallback = mock(args => args)
const unpatch = before(obj, 'method', beforeCallback)
unpatch()
expect(() => unpatch()).not.toThrow()
expect(() => unpatch()).not.toThrow()
})
})
describe('after', () => {
test('should execute callback after original method', () => {
const afterCallback = mock(() => 'original')
after(obj, 'method', afterCallback)
const result = obj.method()
expect(afterCallback).toHaveBeenCalledWith('original')
expect(result).toBe('original')
})
test('should allow modifying return value', () => {
after(obj, 'method', () => 'modified')
const result = obj.method()
expect(result).toBe('modified')
})
test('should unpatch correctly', () => {
const afterCallback = mock(() => 'modified')
const unpatch = after(obj, 'method', afterCallback)
// Test that patch is working
let result = obj.method()
expect(result).toBe('modified')
expect(afterCallback).toHaveBeenCalledWith('original')
// Unpatch
unpatch()
// Test that patch is removed
result = obj.method()
expect(result).toBe('original')
expect(afterCallback).toHaveBeenCalledTimes(1) // Only called before unpatch
})
test('should handle multiple after hooks and unpatch correctly', () => {
const firstHook = mock(result => `${result}-first`)
const secondHook = mock(result => `${result}-second`)
const unpatch1 = after(obj, 'method', firstHook)
const unpatch2 = after(obj, 'method', secondHook)
let result = obj.method()
expect(result).toBe('original-second-first')
// Unpatch second hook (most recent)
unpatch2()
result = obj.method()
expect(result).toBe('original-first')
// Unpatch first hook
unpatch1()
result = obj.method()
expect(result).toBe('original')
})
test('should not crash when unpatch is called multiple times', () => {
const afterCallback = mock(result => result)
const unpatch = after(obj, 'method', afterCallback)
unpatch()
expect(() => unpatch()).not.toThrow()
expect(() => unpatch()).not.toThrow()
})
})
describe('instead', () => {
test('should replace original method', () => {
const replacement = mock(() => 'replaced')
instead(obj, 'method', replacement)
const result = obj.method('arg1')
expect(replacement).toHaveBeenCalledWith(
['arg1'],
expect.any(Function),
)
expect(originalMethod).not.toHaveBeenCalled()
expect(result).toBe('replaced')
})
test('should provide access to original method', () => {
instead(obj, 'method', (_args, original) => {
return `${original()} modified`
})
const result = obj.method()
expect(result).toBe('original modified')
})
test('should unpatch correctly', () => {
const replacement = mock(() => 'replaced')
const unpatch = instead(obj, 'method', replacement)
// Test that patch is working
let result = obj.method()
expect(result).toBe('replaced')
expect(originalMethod).not.toHaveBeenCalled()
// Unpatch
unpatch()
// Test that patch is removed
result = obj.method()
expect(result).toBe('original')
expect(originalMethod).toHaveBeenCalled()
expect(replacement).toHaveBeenCalledTimes(1) // Only called before unpatch
})
test('should handle multiple instead hooks and unpatch correctly', () => {
const firstHook = mock((_args, original) => `first-${original()}`)
const secondHook = mock((_args, original) => `second-${original()}`)
const unpatch1 = instead(obj, 'method', firstHook)
const unpatch2 = instead(obj, 'method', secondHook)
let result = obj.method()
expect(result).toBe('second-first-original')
// Unpatch second hook (most recent)
unpatch2()
result = obj.method()
expect(result).toBe('first-original')
// Unpatch first hook
unpatch1()
result = obj.method()
expect(result).toBe('original')
})
test('should not crash when unpatch is called multiple times', () => {
const replacement = mock(() => 'replaced')
const unpatch = instead(obj, 'method', replacement)
unpatch()
expect(() => unpatch()).not.toThrow()
expect(() => unpatch()).not.toThrow()
})
})
describe('mixed hooks', () => {
test('should handle before + after', () => {
const beforeHook = mock(args => {
args[0] = 'modified'
return args
})
const afterHook = mock(result => `${result}-after`)
const unpatchBefore = before(obj, 'method', beforeHook)
const unpatchAfter = after(obj, 'method', afterHook)
let result = obj.method('original')
expect(result).toBe('modified-after')
expect(beforeHook).toHaveBeenCalledWith(['modified'])
expect(afterHook).toHaveBeenCalledWith('modified')
// Unpatch after hook
unpatchAfter()
result = obj.method('original')
expect(result).toBe('modified')
// Unpatch before hook
unpatchBefore()
result = obj.method('original')
expect(result).toBe('original')
})
test('should handle instead + before', () => {
const insteadHook = mock(
(args, original) => `${original(...args)}-instead`,
)
const beforeHook = mock(args => {
args[0] = 'modified'
return args
})
const unpatchInstead = instead(obj, 'method', insteadHook)
const unpatchBefore = before(obj, 'method', beforeHook)
let result = obj.method('original')
expect(result).toBe('modified-instead')
expect(insteadHook).toHaveBeenCalledWith(
['modified'],
expect.any(Function),
)
expect(beforeHook).toHaveBeenCalledWith(['modified'])
// Unpatch before hook
unpatchBefore()
result = obj.method('original')
expect(result).toBe('original-instead')
// Unpatch instead hook
unpatchInstead()
result = obj.method('original')
expect(result).toBe('original')
})
test('should handle instead + after', () => {
const insteadHook = mock(() => 'oops')
const afterHook = mock(result => `${result}-after`)
const unpatchInstead = instead(obj, 'method', insteadHook)
const unpatchAfter = after(obj, 'method', afterHook)
let result = obj.method('original')
expect(result).toBe('oops-after')
expect(insteadHook).toHaveBeenCalledWith(
['original'],
expect.any(Function),
)
expect(afterHook).toHaveBeenCalledWith('oops')
// Unpatch after hook
unpatchAfter()
result = obj.method('original')
expect(result).toBe('oops')
// Unpatch instead hook
unpatchInstead()
result = obj.method('original')
expect(result).toBe('original')
})
test('should handle instead + before + after', () => {
const insteadHook = mock(
(args, original) => `${original(...args)}-instead`,
)
const beforeHook = mock(args => {
args[0] = 'modified'
return args
})
const afterHook = mock(result => `${result}-after`)
const unpatchInstead = instead(obj, 'method', insteadHook)
const unpatchBefore = before(obj, 'method', beforeHook)
const unpatchAfter = after(obj, 'method', afterHook)
let result = obj.method('original')
expect(result).toBe('modified-instead-after')
expect(insteadHook).toHaveBeenCalledWith(
['modified'],
expect.any(Function),
)
expect(beforeHook).toHaveBeenCalledWith(['modified'])
expect(afterHook).toHaveBeenCalledWith('modified-instead')
// Unpatch after hook
unpatchAfter()
result = obj.method('original')
expect(result).toBe('modified-instead')
// Unpatch before hook
unpatchBefore()
result = obj.method('original')
expect(result).toBe('original-instead')
// Unpatch instead hook
unpatchInstead()
result = obj.method('original')
expect(result).toBe('original')
})
test('should handle before + after + instead', () => {
const beforeHook = mock(args => {
args[0] = 'before-modified'
return args
})
const afterHook = mock(result => `${result}-after`)
const insteadHook = mock(
(args, original) => `${original(...args)}-instead`,
)
const unpatchBefore = before(obj, 'method', beforeHook)
const unpatchAfter = after(obj, 'method', afterHook)
const unpatchInstead = instead(obj, 'method', insteadHook)
let result = obj.method('original')
expect(result).toBe('before-modified-instead-after')
expect(beforeHook).toHaveBeenCalledWith(['before-modified'])
expect(afterHook).toHaveBeenCalledWith('before-modified-instead')
expect(insteadHook).toHaveBeenCalledWith(
['before-modified'],
expect.any(Function),
)
// Unpatch after hook
unpatchAfter()
result = obj.method('original')
expect(result).toBe('before-modified-instead')
// Unpatch before hook
unpatchBefore()
result = obj.method('original')
expect(result).toBe('original-instead')
// Unpatch instead hook
unpatchInstead()
result = obj.method('original')
expect(result).toBe('original')
})
test('should restore original method when all hooks are unpatched', () => {
const originalRef = obj.method
const unpatch1 = before(obj, 'method', args => args)
const unpatch2 = after(obj, 'method', result => result)
// Method should be patched
expect(obj.method).not.toBe(originalRef)
unpatch1()
// Still patched because after hook remains
expect(obj.method).not.toBe(originalRef)
unpatch2()
// Should be restored to original
expect(obj.method).toBe(originalRef)
})
})
// describe('error handling', () => {
// test('should throw error for invalid parent', () => {
// expect(() => before(null as any, 'method', () => [])).toThrow()
// expect(() => before(undefined as any, 'method', () => [])).toThrow()
// expect(() => before('string' as any, 'method', () => [])).toThrow()
// })
// test('should throw error for non-existent property', () => {
// expect(() => before(obj, 'nonExistent' as any, () => [])).toThrow(
// "Property 'nonExistent' does not exist on parent",
// )
// })
// test('should throw error for non-function property', () => {
// const objWithNonFunction = { prop: 'not a function' }
// expect(() =>
// before(objWithNonFunction, 'prop' as any, () => []),
// ).toThrow("Property 'prop' is not a function")
// })
// test('should throw error for invalid hook', () => {
// expect(() => before(obj, 'method', null as any)).toThrow()
// expect(() => before(obj, 'method', 'string' as any)).toThrow()
// })
// })
describe('constructor patching', () => {
test('should patch constructor functions', () => {
class TestClass {
value: string
constructor(value: string) {
this.value = value
}
}
const obj = { TestClass }
const beforeHook = mock(args => {
args[0] = 'modified'
return args
})
before(obj, 'TestClass', beforeHook)
const instance = new obj.TestClass('original')
expect(instance.value).toBe('modified')
expect(beforeHook).toHaveBeenCalledWith(['modified'])
})
})
describe('priority system', () => {
test('before hooks should execute in ascending priority order', () => {
const order: number[] = []
before(
obj,
'method',
args => {
order.push(2)
return args
},
{ priority: HookPriority.NORMAL },
)
before(
obj,
'method',
args => {
order.push(1)
return args
},
{ priority: HookPriority.LOWEST },
)
before(
obj,
'method',
args => {
order.push(3)
return args
},
{ priority: HookPriority.HIGHEST },
)
obj.method()
expect(order).toEqual([1, 2, 3])
})
test('after hooks should execute in descending priority order', () => {
const order: number[] = []
after(
obj,
'method',
result => {
order.push(2)
return result
},
{ priority: HookPriority.NORMAL },
)
after(
obj,
'method',
result => {
order.push(3)
return result
},
{ priority: HookPriority.LOWEST },
)
after(
obj,
'method',
result => {
order.push(1)
return result
},
{ priority: HookPriority.HIGHEST },
)
obj.method()
expect(order).toEqual([1, 2, 3])
})
test('instead hooks should execute highest priority first', () => {
const order: number[] = []
instead(
obj,
'method',
(args, original) => {
order.push(1)
return `1-${original(...args)}`
},
{ priority: HookPriority.LOWEST },
)
instead(
obj,
'method',
(args, original) => {
order.push(2)
return `2-${original(...args)}`
},
{ priority: HookPriority.NORMAL },
)
instead(
obj,
'method',
(args, original) => {
order.push(3)
return `3-${original(...args)}`
},
{ priority: HookPriority.HIGHEST },
)
const result = obj.method()
expect(order).toEqual([3, 2, 1])
expect(result).toBe('3-2-1-original')
})
test('before hooks with custom priority values', () => {
const order: number[] = []
before(
obj,
'method',
args => {
order.push(2)
return args
},
{ priority: 0 },
)
before(
obj,
'method',
args => {
order.push(1)
return args
},
{ priority: -100 },
)
before(
obj,
'method',
args => {
order.push(3)
return args
},
{ priority: 100 },
)
before(
obj,
'method',
args => {
order.push(4)
return args
},
{ priority: 150 },
)
obj.method()
expect(order).toEqual([1, 2, 3, 4])
})
test('hooks without priority should default to 0', () => {
const order: number[] = []
before(
obj,
'method',
args => {
order.push(2)
return args
},
{ priority: -10 },
)
before(obj, 'method', args => {
order.push(3)
return args
})
before(
obj,
'method',
args => {
order.push(4)
return args
},
{ priority: 10 },
)
obj.method()
expect(order).toEqual([2, 3, 4])
})
})
})