Skip to content

Commit cdd5a9e

Browse files
committed
modified: internal/agent/executors.go
modified: internal/agent/executors_test.go modified: internal/agent/humanoid_executor_test.go new file: internal/agent/js_scripts/captcha_detection.js new file: internal/agent/js_scripts/captcha_detection.test.js modified: internal/agent/js_scripts/form_analysis.js modified: internal/agent/js_scripts/form_analysis.test.js modified: internal/agent/js_scripts/package.json modified: internal/agent/js_scripts/verification.test.js modified: internal/agent/js_scripts/verification_error.js modified: internal/agent/signup_executor.go modified: internal/agent/signup_executor_test.go modified: internal/config/config.go modified: internal/config/humanoid_config.go modified: package.json
1 parent c2c6367 commit cdd5a9e

15 files changed

Lines changed: 2694 additions & 932 deletions

internal/agent/executors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func NewExecutorRegistry(projectRoot string, globalCtx *core.GlobalContext) *Exe
5050
codebaseExec := NewCodebaseExecutor(projectRoot)
5151
analysisExec := NewAnalysisExecutor(globalCtx, safeProviderGetter)
5252
humanoidExec := NewHumanoidExecutor(safeHumanoidGetter)
53-
signUpExec, err := NewSignUpExecutor(safeHumanoidGetter, safeProviderGetter, globalCtx.Config)
53+
signUpExec, err := NewSignUpExecutor(safeHumanoidGetter, safeProviderGetter, globalCtx.Config, NewFileSystemSecListsLoader())
5454
if err != nil {
5555
// Log a warning if initialization fails for any reason (e.g., SecLists path invalid).
5656
// The feature will be unavailable.

internal/agent/executors_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
// -- BrowserExecutor Tests --
2525

2626
// Setup helper for BrowserExecutor tests
27-
func setupBrowserExecutorTest(t *testing.T) (*BrowserExecutor, *mocks.MockSessionContext) {
27+
func setupBrowserExecutorTest(_ *testing.T) (*BrowserExecutor, *mocks.MockSessionContext) {
2828
mockSession := mocks.NewMockSessionContext()
2929
provider := func() schemas.SessionContext { return mockSession }
3030
executor := NewBrowserExecutor(provider)

internal/agent/humanoid_executor_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func setupHumanoidExecutorTest(t *testing.T) (*HumanoidExecutor, *mocks.MockHuma
2525
}
2626

2727
// setupHumanoidExecutorTestWithHandlerConfig allows configuring whether to use real or mocked handlers.
28-
func setupHumanoidExecutorTestWithHandlerConfig(t *testing.T, useMockHandlers bool) (*HumanoidExecutor, *mocks.MockHumanoidController) {
28+
func setupHumanoidExecutorTestWithHandlerConfig(_ *testing.T, useMockHandlers bool) (*HumanoidExecutor, *mocks.MockHumanoidController) {
2929
mockHumanoid := new(mocks.MockHumanoidController)
3030

3131
// The dummyHumanoid needs a low-level executor (e.g., for mouse events).
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// js_scripts/captcha_detection.js
2+
3+
/**
4+
* Analyzes the DOM to find common CAPTCHA indicators.
5+
* @returns {string|null} The CSS selector of the first visible CAPTCHA indicator found, or null.
6+
*/
7+
function detectCaptcha() {
8+
const captchaSelectors = [
9+
'iframe[src*="recaptcha/api"]', // Google reCAPTCHA v2/v3
10+
'iframe[src*="hcaptcha.com"]', // hCaptcha
11+
'.g-recaptcha', // reCAPTCHA class
12+
'.h-captcha', // hCaptcha class
13+
'#cf-challenge-wrapper', // Cloudflare Turnstile/Challenge
14+
'iframe[src*="challenges.cloudflare.com"]',
15+
'[data-sitekey]' // Common attribute
16+
];
17+
18+
for (const selector of captchaSelectors) {
19+
try {
20+
const element = document.querySelector(selector);
21+
if (element) {
22+
// Basic visibility check heuristic
23+
const rect = element.getBoundingClientRect();
24+
const isVisible = (
25+
(element.offsetWidth > 0 || element.offsetHeight > 0) ||
26+
(rect.width > 0 && rect.height > 0)
27+
);
28+
29+
if (isVisible) {
30+
return selector;
31+
}
32+
}
33+
} catch (e) {
34+
// Silently ignore errors (e.g., invalid selector syntax)
35+
}
36+
}
37+
return null;
38+
}
39+
40+
// Export for testing environments
41+
if (typeof module !== 'undefined' && module.exports) {
42+
module.exports = { detectCaptcha };
43+
} else {
44+
// In a browser context (Go executor), the last evaluated expression is the return value.
45+
detectCaptcha();
46+
}
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
// Add these Node.js built-in modules
6+
const fs = require('fs');
7+
const path = require('path');
8+
9+
// Import the function to be tested
10+
const { detectCaptcha } = require('./captcha_detection.js');
11+
12+
// --- Script Content for 'eval' Test ---
13+
// This is the full content of 'captcha_detection.js'
14+
// KEPT for reference, but the 'eval' test is being refactored.
15+
const scriptContent = `
16+
// js_scripts/captcha_detection.js
17+
18+
/**
19+
* Analyzes the DOM to find common CAPTCHA indicators.
20+
* @returns {string|null} The CSS selector of the first visible CAPTCHA indicator found, or null.
21+
*/
22+
function detectCaptcha() {
23+
const captchaSelectors = [
24+
'iframe[src*="recaptcha/api"]', // Google reCAPTCHA v2/v3
25+
'iframe[src*="hcaptcha.com"]', // hCaptcha
26+
'.g-recaptcha', // reCAPTCHA class
27+
'.h-captcha', // hCaptcha class
28+
'#cf-challenge-wrapper', // Cloudflare Turnstile/Challenge
29+
'iframe[src*="challenges.cloudflare.com"]',
30+
'[data-sitekey]' // Common attribute
31+
];
32+
33+
for (const selector of captchaSelectors) {
34+
try {
35+
const element = document.querySelector(selector);
36+
if (element) {
37+
// Basic visibility check heuristic
38+
const rect = element.getBoundingClientRect();
39+
const isVisible = (
40+
(element.offsetWidth > 0 || element.offsetHeight > 0) ||
41+
(rect.width > 0 && rect.height > 0)
42+
);
43+
44+
if (isVisible) {
45+
return selector;
46+
}
47+
}
48+
} catch (e) {
49+
// Silently ignore errors (e.g., invalid selector syntax)
50+
}
51+
}
52+
return null;
53+
}
54+
55+
// Export for testing environments
56+
if (typeof module !== 'undefined' && module.exports) {
57+
module.exports = { detectCaptcha };
58+
} else {
59+
// In a browser context (Go executor), the last evaluated expression is the return value.
60+
detectCaptcha();
61+
}
62+
`;
63+
// --- End Script Content ---
64+
65+
// Helper function to set the document body HTML before running the analysis
66+
function setDocumentBody(html) {
67+
document.body.innerHTML = html;
68+
}
69+
70+
// Helper to mock visibility properties for an element
71+
function mockVisibility(selector, isVisible) {
72+
try {
73+
const element = document.querySelector(selector);
74+
if (element) {
75+
Object.defineProperty(element, 'offsetWidth', { configurable: true, value: isVisible ? 100 : 0 });
76+
Object.defineProperty(element, 'offsetHeight', { configurable: true, value: isVisible ? 100 : 0 });
77+
// Mock getBoundingClientRect as a fallback
78+
element.getBoundingClientRect = () => ({
79+
width: isVisible ? 100 : 0,
80+
height: isVisible ? 100 : 0,
81+
top: isVisible ? 10 : 0,
82+
left: isVisible ? 10 : 0,
83+
bottom: isVisible ? 110 : 0,
84+
right: isVisible ? 110 : 0,
85+
});
86+
}
87+
} catch (e) {
88+
// Handle cases where the selector might be invalid during setup
89+
}
90+
}
91+
92+
93+
describe('detectCaptcha', () => {
94+
95+
let originalModule;
96+
97+
beforeEach(() => {
98+
originalModule = global.module; // Save original module
99+
});
100+
101+
// Clean up the DOM after each test
102+
afterEach(() => {
103+
document.body.innerHTML = '';
104+
global.module = originalModule; // Restore module
105+
jest.restoreAllMocks(); // Restore any spies
106+
jest.resetModules(); // Reset module cache
107+
});
108+
109+
test('should return null when no CAPTCHA elements are present', () => {
110+
setDocumentBody(`
111+
<div>
112+
<h1>Just a regular page</h1>
113+
<form id="login-form">
114+
<input type="text" name="username">
115+
<button>Login</button>
116+
</form>
117+
</div>
118+
`);
119+
expect(detectCaptcha()).toBeNull();
120+
});
121+
122+
test('should detect a visible Google reCAPTCHA iframe', () => {
123+
const selector = 'iframe[src*="recaptcha/api"]';
124+
setDocumentBody(`
125+
<div>
126+
<iframe src="https://www.google.com/recaptcha/api/a-b-c"></iframe>
127+
</div>
128+
`);
129+
// Mock it as visible
130+
mockVisibility(selector, true);
131+
expect(detectCaptcha()).toBe(selector);
132+
});
133+
134+
test('should detect a visible .g-recaptcha element', () => {
135+
const selector = '.g-recaptcha';
136+
setDocumentBody(`
137+
<div>
138+
<div class="g-recaptcha" data-sitekey="some-key"></div>
139+
</div>
140+
`);
141+
mockVisibility(selector, true);
142+
expect(detectCaptcha()).toBe(selector);
143+
});
144+
145+
test('should detect a visible hCaptcha iframe', () => {
146+
const selector = 'iframe[src*="hcaptcha.com"]';
147+
setDocumentBody(`
148+
<div>
149+
<iframe src="https://js.hcaptcha.com/a-b-c"></iframe>
150+
</div>
151+
`);
152+
mockVisibility(selector, true);
153+
expect(detectCaptcha()).toBe(selector);
154+
});
155+
156+
test('should detect a visible .h-captcha element', () => {
157+
const selector = '.h-captcha';
158+
setDocumentBody(`
159+
<div>
160+
<div class="h-captcha" data-sitekey="some-key"></div>
161+
</div>
162+
`);
163+
mockVisibility(selector, true);
164+
expect(detectCaptcha()).toBe(selector);
165+
});
166+
167+
test('should detect a visible Cloudflare challenge wrapper', () => {
168+
const selector = '#cf-challenge-wrapper';
169+
setDocumentBody(`
170+
<div>
171+
<div id="cf-challenge-wrapper">
172+
</div>
173+
</div>
174+
`);
175+
mockVisibility(selector, true);
176+
expect(detectCaptcha()).toBe(selector);
177+
});
178+
179+
test('should detect a visible generic [data-sitekey] element', () => {
180+
const selector = '[data-sitekey]';
181+
setDocumentBody(`
182+
<div>
183+
<div class="some-custom-captcha" data-sitekey="some-key"></div>
184+
</div>
185+
`);
186+
mockVisibility(selector, true);
187+
expect(detectCaptcha()).toBe(selector);
188+
});
189+
190+
test('should ignore a hidden CAPTCHA element', () => {
191+
const selector = '.g-recaptcha';
192+
setDocumentBody(`
193+
<div>
194+
<div class="g-recaptcha" data-sitekey="some-key"></div>
195+
</div>
196+
`);
197+
// Mock it as hidden (offset properties are 0)
198+
mockVisibility(selector, false);
199+
expect(detectCaptcha()).toBeNull();
200+
});
201+
202+
test('should ignore a hidden hCaptcha iframe', () => {
203+
const selector = 'iframe[src*="hcaptcha.com"]';
204+
setDocumentBody(`
205+
<div>
206+
<iframe src="https://js.hcaptcha.com/a-b-c" style="display:none;"></iframe>
207+
</div>
208+
`);
209+
mockVisibility(selector, false);
210+
expect(detectCaptcha()).toBeNull();
211+
});
212+
213+
test('should correctly find the first visible CAPTCHA when multiple are present', () => {
214+
const visibleSelector = '.g-recaptcha';
215+
const hiddenSelector = '.h-captcha';
216+
setDocumentBody(`
217+
<div>
218+
<div class="g-recaptcha" data-sitekey="google-key"></div>
219+
<div class="h-captcha" data-sitekey="hcaptcha-key"></div>
220+
</div>
221+
`);
222+
223+
// Mock .g-recaptcha as visible
224+
mockVisibility(visibleSelector, true);
225+
// Mock .h-captcha as hidden
226+
mockVisibility(hiddenSelector, false);
227+
228+
// The script iterates in order, and .g-recaptcha comes *after* hcaptcha iframe in the list.
229+
// Let's re-order the test HTML to match the script's selector list order for a more precise test.
230+
231+
// Script list order:
232+
// 1. iframe[src*="recaptcha/api"]
233+
// 2. iframe[src*="hcaptcha.com"]
234+
// 3. .g-recaptcha
235+
// 4. .h-captcha
236+
// 5. #cf-challenge-wrapper
237+
// 6. iframe[src*="challenges.cloudflare.com"]
238+
// 7. [data-sitekey]
239+
240+
setDocumentBody(`
241+
<div>
242+
<iframe src="https://js.hcaptcha.com/a-b-c"></iframe>
243+
<div class="g-recaptcha" data-sitekey="google-key"></div>
244+
<div class="h-captcha" data-sitekey="hcaptcha-key"></div>
245+
</div>
246+
`);
247+
248+
mockVisibility('iframe[src*="hcaptcha.com"]', false);
249+
mockVisibility('.g-recaptcha', true);
250+
mockVisibility('.h-captcha', false);
251+
252+
// It should skip the hidden hCaptcha iframe and find the visible .g-recaptcha
253+
expect(detectCaptcha()).toBe('.g-recaptcha');
254+
});
255+
256+
// --- REFACTORED TEST ---
257+
// Test for line 45 (non-module environment)
258+
test('should execute detectCaptcha directly in non-module (browser) environment', () => {
259+
// 1. Spy on the document method *before* the script runs
260+
const querySelectorSpy = jest.spyOn(document, 'querySelector').mockImplementation(() => null);
261+
262+
// 2. Save original module
263+
const originalModule = global.module;
264+
265+
try {
266+
// 3. Read the script file content
267+
const scriptPath = path.resolve(__dirname, 'captcha_detection.js');
268+
const scriptContent = fs.readFileSync(scriptPath, 'utf8');
269+
270+
// 4. THE FIX: Shadow the test file's local 'module' variable
271+
const module = undefined;
272+
273+
// 5. 'eval' the script content. This will run in the current
274+
// scope where 'module' is undefined, triggering the 'else' block.
275+
eval(scriptContent);
276+
277+
// 6. Check that the spy was called by the 'else' block's execution
278+
expect(querySelectorSpy).toHaveBeenCalledWith('iframe[src*="recaptcha/api"]');
279+
280+
} finally {
281+
// 7. Restore
282+
global.module = originalModule; // Restore global
283+
querySelectorSpy.mockRestore();
284+
jest.resetModules(); // Clean up
285+
}
286+
});
287+
});

0 commit comments

Comments
 (0)