fix(stalker): discard stale import attempts

This commit is contained in:
4gray committed 2026-07-27 12:08:04 +02:00
1 parent a44f578ddc
commit c1f7306ed5
2 files changed
+94

No files matched your search

@@ -1,3 +1,4 @@
/* eslint-disable max-lines -- the import lifecycle matrix shares one state-machine harness */
import { TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
@@ -407,6 +408,83 @@ describe('StalkerPortalImportComponent', () => {
expect(component.connectionStage()).toBe('idle');
});
it('discards a ready attempt returned after the form was cleared during open', async () => {
const open = deferred<StalkerSessionConnectionOutcome>();
session.open.mockReturnValueOnce(open.promise);
const add = component.addPlaylist();
await Promise.resolve();
component.clearForm();
open.resolve(READY_OUTCOME);
await add;
expect(session.discard).toHaveBeenCalledWith('attempt-import-1');
expect(playlists.persistStalkerConnection).not.toHaveBeenCalled();
expect(component.connectionStage()).toBe('idle');
});
it('discards a challenge returned after the form was cleared during continuation', async () => {
session.open.mockResolvedValueOnce({
attemptRef: 'attempt-origin-stale',
challengeRef: 'challenge-origin-stale',
finalOrigin: 'https://redirected.example',
kind: 'origin-approval-required',
requestId: 'request-origin-stale',
sourceOrigin: 'https://portal.example',
});
const continuation = deferred<StalkerSessionConnectionOutcome>();
session.continue.mockReturnValueOnce(continuation.promise);
await component.addPlaylist();
const approval = component.respondToOriginApproval(true);
await Promise.resolve();
component.clearForm();
session.discard.mockClear();
continuation.resolve({
attemptNumber: 1,
attemptRef: 'attempt-origin-stale',
challengeRef: 'challenge-credentials-stale',
kind: 'credentials-required',
requestId: 'request-credentials-stale',
});
await approval;
expect(session.discard).toHaveBeenCalledWith('attempt-origin-stale');
expect(component.connectionStage()).toBe('idle');
});
it('discards a ready attempt returned after destruction during credential validation', async () => {
session.open.mockResolvedValueOnce({
attemptNumber: 1,
attemptRef: 'attempt-credentials-stale',
challengeRef: 'challenge-credentials-stale',
kind: 'credentials-required',
requestId: 'request-credentials-stale',
});
const continuation = deferred<StalkerSessionConnectionOutcome>();
session.continue.mockReturnValueOnce(continuation.promise);
await component.addPlaylist();
component.form.patchValue({
password: 'entered-password',
username: 'entered-user',
});
const submit = component.submitCredentials();
await Promise.resolve();
component.ngOnDestroy();
session.discard.mockClear();
continuation.resolve({
...READY_OUTCOME,
attemptRef: 'attempt-credentials-stale',
});
await submit;
expect(session.discard).toHaveBeenCalledWith(
'attempt-credentials-stale'
);
expect(playlists.persistStalkerConnection).not.toHaveBeenCalled();
});
function setValidForm(): void {
component.form.patchValue({
_id: 'playlist-1',
@@ -1,3 +1,4 @@
/* eslint-disable max-lines -- the challenge-driven import state machine is kept in one lifecycle owner */
import {
Component,
OnDestroy,
@@ -155,6 +156,7 @@ export class StalkerPortalImportComponent implements OnDestroy {
provisionalReason: 'import',
});
if (runId !== this.runId) {
await this.discardStaleOutcome(outcome);
return;
}
await this.handleOutcome(outcome, runId);
@@ -186,6 +188,8 @@ export class StalkerPortalImportComponent implements OnDestroy {
});
if (runId === this.runId) {
await this.handleOutcome(outcome, runId);
} else {
await this.discardStaleOutcome(outcome);
}
} catch {
if (runId === this.runId) {
@@ -242,6 +246,8 @@ export class StalkerPortalImportComponent implements OnDestroy {
});
if (runId === this.runId) {
await this.handleOutcome(outcome, runId);
} else {
await this.discardStaleOutcome(outcome);
}
} catch {
if (runId === this.runId) {
@@ -376,6 +382,16 @@ export class StalkerPortalImportComponent implements OnDestroy {
}
}
private async discardStaleOutcome(
outcome: StalkerSessionConnectionOutcome
): Promise<void> {
const attemptRef =
'attemptRef' in outcome ? outcome.attemptRef : undefined;
if (attemptRef !== undefined) {
await this.session.discard(attemptRef).catch(() => undefined);
}
}
private fail(reason: StalkerSessionFailureReason): void {
this.failureReason.set(reason);
this.connectionStage.set('failure');