Skip to content

Commit 0d708dd

Browse files
committed
fix(core): flaky tests, fix interact with string
1 parent db60f97 commit 0d708dd

File tree

9 files changed

+210
-34
lines changed

9 files changed

+210
-34
lines changed

client/lib/Interactor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default class Interactor {
4949
}
5050

5151
function convertToInteractionGroups(interactions: IInteractions): IInteractionGroups {
52-
const lastPosition: IMousePosition = [0, 0];
52+
let lastPosition: ICoreMousePosition = [0, 0];
5353
const interactionGroups: IInteractionGroups = [];
5454
interactions.forEach(interaction => {
5555
if (typeof interaction === 'string') {
@@ -58,6 +58,7 @@ function convertToInteractionGroups(interactions: IInteractions): IInteractionGr
5858
interactionGroups.push([interactionStep]);
5959
} else {
6060
const interactionGroup = convertInteractionToInteractionGroup(interaction);
61+
lastPosition = interactionGroup[interactionGroup.length - 1].mousePosition;
6162
interactionGroups.push(interactionGroup);
6263
}
6364
});

core/lib/Session.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ export default class Session extends TypedEventEmitter<{
6363
return this._isClosing;
6464
}
6565

66+
public mitmErrorsByUrl = new Map<
67+
string,
68+
{
69+
resourceId: number;
70+
event: IRequestSessionHttpErrorEvent;
71+
}[]
72+
>();
73+
6674
private isolatedMitmProxy?: MitmProxy;
6775
private _isClosing = false;
6876

@@ -292,14 +300,41 @@ export default class Session extends TypedEventEmitter<{
292300
}
293301

294302
private onMitmError(event: IRequestSessionHttpErrorEvent) {
295-
const tabId = this.mitmRequestSession.browserRequestMatcher.requestIdToTabId.get(
296-
event.request.browserRequestId,
303+
const { request } = event;
304+
let tabId = this.mitmRequestSession.browserRequestMatcher.requestIdToTabId.get(
305+
request.browserRequestId,
297306
);
307+
const url = request.request?.url;
308+
const isDocument = request?.resourceType === 'Document';
309+
if (isDocument && !tabId) {
310+
for (const tab of this.tabs) {
311+
const isMatch = tab.findFrameWithUnresolvedNavigation(
312+
request.browserRequestId,
313+
request.request?.method,
314+
url,
315+
request.response?.url,
316+
);
317+
if (isMatch) {
318+
tabId = tab.id;
319+
break;
320+
}
321+
}
322+
}
323+
324+
// record errors
325+
const resource = this.sessionState.captureResourceError(tabId, request, event.error);
326+
if (!request.browserRequestId && url) {
327+
const existing = this.mitmErrorsByUrl.get(url) ?? [];
328+
existing.push({
329+
resourceId: resource.id,
330+
event,
331+
});
332+
this.mitmErrorsByUrl.set(url, existing);
333+
}
298334

299-
const resource = this.sessionState.captureResourceError(tabId, event.request, event.error);
300-
if (event.request?.resourceType === 'Document') {
335+
if (tabId && isDocument) {
301336
const tab = this.tabs.find(x => x.id === tabId);
302-
tab?.checkForResolvedNavigation(event.request.browserRequestId, resource, event.error);
337+
tab?.checkForResolvedNavigation(request.browserRequestId, resource, event.error);
303338
}
304339
}
305340

core/lib/SessionState.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default class SessionState {
4242
private readonly scriptInstanceMeta: IScriptInstanceMeta;
4343
private readonly createDate = new Date();
4444
private readonly frames: { [frameId: number]: IFrameRecord } = {};
45-
private readonly resources: IResourceMeta[] = [];
45+
private readonly resourcesById = new Map<number, IResourceMeta>();
4646
private readonly websocketMessages: IWebsocketResourceMessage[] = [];
4747
private websocketListeners: {
4848
[resourceId: string]: ((msg: IWebsocketResourceMessage) => any)[];
@@ -209,7 +209,7 @@ export default class SessionState {
209209
let resourceMeta = this.getResourceMeta(resourceId);
210210
if (!resourceMeta) {
211211
resourceMeta = convertedMeta;
212-
this.resources.push(resourceMeta);
212+
this.resourcesById.set(resourceMeta.id, resourceMeta);
213213
}
214214
if (convertedMeta.response) {
215215
resourceMeta.response ??= convertedMeta.response;
@@ -229,8 +229,28 @@ export default class SessionState {
229229
const resource = this.resourceEventToMeta(tabId, resourceEvent);
230230
this.db.resources.insert(tabId, resource, null, resourceEvent, error);
231231

232-
if (!this.getResourceMeta(resource.id)) {
233-
this.resources.push(resource);
232+
if (!this.resourcesById.has(resource.id)) {
233+
this.resourcesById.set(resource.id, resource);
234+
}
235+
return resource;
236+
}
237+
238+
public captureResourceRequestId(
239+
resourceId: number,
240+
browserRequestId: string,
241+
tabId: number,
242+
): IResourceMeta {
243+
const resource = this.resourcesById.get(resourceId);
244+
if (resource) {
245+
resource.tabId = tabId;
246+
247+
// NOTE: browserRequestId can be shared amongst redirects
248+
this.browserRequestIdToResources[browserRequestId] ??= [];
249+
this.browserRequestIdToResources[browserRequestId].push({
250+
resourceId,
251+
url: resource.url,
252+
});
253+
this.db.resources.updateResource(resourceId, { browserRequestId, tabId });
234254
}
235255
return resource;
236256
}
@@ -246,7 +266,7 @@ export default class SessionState {
246266
this.db.resources.insert(tabId, resource, resourceResponseEvent.body, resourceEvent);
247267

248268
if (isResponse) {
249-
this.resources.push(resource);
269+
this.resourcesById.set(resource.id, resource);
250270
}
251271
return resource;
252272
}
@@ -271,9 +291,7 @@ export default class SessionState {
271291

272292
if (browserRequestId) {
273293
// NOTE: browserRequestId can be shared amongst redirects
274-
if (!this.browserRequestIdToResources[browserRequestId]) {
275-
this.browserRequestIdToResources[browserRequestId] = [];
276-
}
294+
this.browserRequestIdToResources[browserRequestId] ??= [];
277295
this.browserRequestIdToResources[browserRequestId].push({
278296
resourceId: resourceEvent.id,
279297
url: request.url,
@@ -304,7 +322,7 @@ export default class SessionState {
304322

305323
public getResourceLookupMap(tabId: number): { [method_url: string]: IResourceMeta[] } {
306324
const result: { [method_url: string]: IResourceMeta[] } = {};
307-
for (const resource of this.resources) {
325+
for (const resource of this.resourcesById.values()) {
308326
if (resource.tabId === tabId) {
309327
const key = `${resource.request.method}_${resource.request.url}`;
310328
result[key] ??= [];
@@ -315,15 +333,19 @@ export default class SessionState {
315333
}
316334

317335
public getResources(tabId: number): IResourceMeta[] {
318-
return this.resources.filter(x => x.tabId === tabId);
336+
const resources: IResourceMeta[] = [];
337+
for (const resource of this.resourcesById.values()) {
338+
if (resource.tabId === tabId) resources.push(resource);
339+
}
340+
return resources;
319341
}
320342

321343
public getResourceData(id: number, decompress: boolean): Promise<Buffer> {
322344
return this.db.resources.getResourceBodyById(id, decompress);
323345
}
324346

325347
public getResourceMeta(id: number): IResourceMeta {
326-
return this.resources.find(x => x.id === id);
348+
return this.resourcesById.get(id);
327349
}
328350

329351
/////// FRAMES ///////

core/lib/Tab.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,38 @@ export default class Tab extends TypedEventEmitter<ITabEventParams> {
141141
browserRequestId: string,
142142
resource: IResourceMeta,
143143
error?: Error,
144-
) {
145-
if (resource.request.method !== 'GET') return;
144+
): boolean {
145+
const frame = this.findFrameWithUnresolvedNavigation(
146+
browserRequestId,
147+
resource.request?.method,
148+
resource.request?.url,
149+
resource.response?.url,
150+
);
151+
if (frame) {
152+
frame.navigations.onResourceLoaded(resource.id, resource.response?.statusCode, error);
153+
return true;
154+
}
155+
return false;
156+
}
146157

147-
for (const frame of this.frameEnvironmentsById.values()) {
148-
if (!frame.isAttached) continue;
158+
public findFrameWithUnresolvedNavigation(
159+
browserRequestId: string,
160+
method: string,
161+
requestedUrl: string,
162+
finalUrl: string,
163+
): FrameEnvironment {
164+
if (method !== 'GET') return null;
149165

166+
for (const frame of this.frameEnvironmentsById.values()) {
150167
const top = frame.navigations.top;
151168
if (!top || top.resourceId.isResolved) continue;
152169

153170
if (
154-
resource.response?.url === top.finalUrl ||
155-
resource.request.url === top.requestedUrl ||
171+
finalUrl === top.finalUrl ||
172+
requestedUrl === top.requestedUrl ||
156173
browserRequestId === top.browserRequestId
157174
) {
158-
frame.navigations.onResourceLoaded(resource.id, resource.response?.statusCode, error);
159-
break;
175+
return frame;
160176
}
161177
}
162178
}
@@ -664,8 +680,29 @@ export default class Tab extends TypedEventEmitter<ITabEventParams> {
664680
event.resource.browserRequestId,
665681
);
666682

667-
// if we get a cached response, it might never hit mitm, so record now
668683
if (!resourcesWithBrowserRequestId?.length) {
684+
// first check if this is a mitm error
685+
const errorsMatchingUrl = this.session.mitmErrorsByUrl.get(event.resource.url.href);
686+
687+
// if this resource error-ed out,
688+
for (let i = 0; i < errorsMatchingUrl?.length ?? 0; i += 1) {
689+
const error = errorsMatchingUrl[i];
690+
const request = error.event?.request?.request;
691+
if (!request) continue;
692+
if (
693+
request.method === event.resource.method &&
694+
Math.abs(request.timestamp - event.resource.requestTime.getTime()) < 500
695+
) {
696+
errorsMatchingUrl.splice(i, 1);
697+
this.sessionState.captureResourceRequestId(
698+
error.resourceId,
699+
event.resource.browserRequestId,
700+
this.id,
701+
);
702+
return;
703+
}
704+
}
705+
669706
const ctx = MitmRequestContext.createFromPuppetResourceRequest(event.resource);
670707
const resourceDetails = MitmRequestContext.toEmittedResource(ctx);
671708
if (!event.resource.browserServedFromCache) {

core/models/ResourcesTable.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ export default class ResourcesTable extends SqliteTable<IResourcesRecord> {
5050
);
5151
}
5252

53+
public updateResource(id: number, data: { tabId: number; browserRequestId: string }): void {
54+
if (this.hasPending(x => x[0] === id)) {
55+
this.flush();
56+
}
57+
this.db
58+
.prepare(`update ${this.tableName} set tabId=?, devtoolsRequestId=? where id=?`)
59+
.run(data.tabId, data.browserRequestId, id);
60+
}
61+
5362
public insert(
5463
tabId: number,
5564
meta: IResourceMeta,

core/test/resources.test.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { Helpers } from '@secret-agent/testing';
22
import Core, { Session } from '@secret-agent/core';
3+
import Resolvable from '@secret-agent/commons/Resolvable';
4+
import ConnectionToClient from '../server/ConnectionToClient';
35

6+
let connection: ConnectionToClient;
7+
beforeAll(() => {
8+
connection = Core.addConnection();
9+
Helpers.onClose(() => connection.disconnect(), true);
10+
});
411
afterAll(Helpers.afterAll);
512
afterEach(Helpers.afterEach);
6-
process.env.MITM_ALLOW_INSECURE = 'true';
713

814
test('loads http2 resources', async () => {
915
const server = await Helpers.runHttp2Server((req, res) => {
@@ -21,14 +27,46 @@ test('loads http2 resources', async () => {
2127
res.end(`<html><body><img src="/img.png"/></body></html>`);
2228
});
2329

24-
const connection = Core.addConnection();
25-
Helpers.onClose(() => connection.disconnect());
2630
const meta = await connection.createSession();
2731
const tab = Session.getTab(meta);
32+
const session = Session.get(meta.sessionId);
33+
Helpers.needsClosing.push(session);
2834
await tab.goto(server.url);
2935
await tab.waitForLoad('DomContentLoaded');
3036

3137
const resources = await tab.waitForResource({ url: /.*\/img.png/ });
3238
expect(resources).toHaveLength(1);
3339
expect(resources[0].type).toBe('Image');
40+
await session.close();
41+
});
42+
43+
test('records a single resource for failed mitm requests', async () => {
44+
const meta = await connection.createSession();
45+
const session = Session.get(meta.sessionId);
46+
Helpers.needsClosing.push(session);
47+
const tab = Session.getTab(meta);
48+
49+
const resolvable = new Resolvable<void>();
50+
const originalEmit = tab.puppetPage.emit.bind(tab.puppetPage);
51+
// @ts-ignore
52+
jest.spyOn(tab.puppetPage.networkManager, 'emit').mockImplementation((evt, args) => {
53+
// eslint-disable-next-line promise/always-return,promise/catch-or-return
54+
resolvable.promise.then(() => {
55+
originalEmit(evt, args);
56+
});
57+
return true;
58+
});
59+
const goToPromise = tab.goto(`http://localhost:2344/not-there`);
60+
61+
await expect(goToPromise).rejects.toThrowError();
62+
expect(session.mitmErrorsByUrl.get(`http://localhost:2344/not-there`)).toHaveLength(1);
63+
expect(session.sessionState.getResources(meta.tabId)).toHaveLength(1);
64+
// @ts-ignore
65+
expect(Object.keys(session.sessionState.browserRequestIdToResources)).toHaveLength(0);
66+
resolvable.resolve();
67+
await new Promise(resolve => setTimeout(resolve, 100));
68+
expect(session.sessionState.getResources(meta.tabId)).toHaveLength(1);
69+
// @ts-ignore
70+
expect(Object.keys(session.sessionState.browserRequestIdToResources)).toHaveLength(1);
71+
await session.close();
3472
});

full-client/test/interact.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,36 @@ describe('basic Interact tests', () => {
169169
await expect(agent.interact({ click: logo })).resolves.toBeUndefined();
170170
await agent.close();
171171
});
172+
173+
it('can accept empty commands', async () => {
174+
koaServer.get('/empty-click', ctx => {
175+
ctx.body = `
176+
<body>
177+
<div style="margin-top: 200px">
178+
<a href="#none" onclick="clickit()">Empty clicker</a>
179+
</div>
180+
<script>
181+
let lastClicked = '';
182+
function clickit() {
183+
lastClicked = 'clickedit';
184+
}
185+
</script>
186+
</body>
187+
`;
188+
});
189+
const agent = await handler.createAgent({
190+
humanEmulatorId: 'basic',
191+
});
192+
Helpers.needsClosing.push(agent);
193+
await agent.goto(`${koaServer.baseUrl}/empty-click`);
194+
await agent.activeTab.waitForLoad(LocationStatus.PaintingStable);
195+
await agent.interact(
196+
{
197+
[Command.move]: agent.document.querySelector('a'),
198+
},
199+
'click',
200+
);
201+
202+
expect(await agent.activeTab.getJsValue('lastClicked')).toBe('clickedit');
203+
});
172204
});

mitm/lib/DnsOverTlsSocket.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ export default class DnsOverTlsSocket {
8585
this.mitmSocket.on('eof', async () => {
8686
removeEventListeners([registration]);
8787
this.mitmSocket.close();
88-
await this.connect();
88+
this.isConnected = this.connect();
89+
90+
await this.isConnected;
8991
// re-run pending queries
9092
for (const [id, entry] of this.pending) {
9193
this.pending.delete(id);

0 commit comments

Comments
 (0)