Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2553,7 +2553,25 @@ describe('RealUnitService', () => {
// a non-confirming call never touches a registration (the latch is only set on a 2xx)
expect(aktionariatManager.transaction).not.toHaveBeenCalled();
expect(aktionariatTxManager.save).not.toHaveBeenCalled();
// a 4xx is a handled rejection (mapped to INVALID above), not a fault — logged at warn
expect((service as any).logger.warn).toHaveBeenCalled();
expect((service as any).logger.error).not.toHaveBeenCalled();
});

it('keeps a 429 (throttling) at error — a systemic fault, not a rejected link', async () => {
mockEnvironment = 'prd';
mockRegisteredWallets([walletA]);
httpService.getRaw.mockRejectedValue({
response: { status: 429, data: { status: 429, message: 'Too many requests' } },
});

const result = await service.confirmAktionariat({ email, code, user }, rawRequest);

// the client-facing mapping still buckets it as INVALID (4xx), only the log level differs
expect(result.status).toBe(RealUnitAktionariatConfirmationStatus.INVALID);
expect((service as any).logger.error).toHaveBeenCalled();
const warnText = (service as any).logger.warn.mock.calls.flat().join(' ');
expect(warnText).not.toContain('Aktionariat confirmation call');
});

it('keeps the FIRST confirmedDate on a re-confirm and returns the stored (not transient) date', async () => {
Expand Down Expand Up @@ -2717,8 +2735,8 @@ describe('RealUnitService', () => {
expect(msg.response).toBeUndefined();
expect(audit.message).toContain(leakedEmail);

// Loki stays PII-free: the leaked email must never reach this.logger.error
const lokiText = (service as any).logger.error.mock.calls.flat().join(' ');
// Loki stays PII-free: the leaked email must never reach this.logger.warn (a 4xx logs at warn, not error)
const lokiText = (service as any).logger.warn.mock.calls.flat().join(' ');
expect(lokiText).toContain('status=403');
expect(lokiText).not.toContain(leakedEmail);
});
Expand Down
19 changes: 14 additions & 5 deletions src/subdomains/supporting/realunit/realunit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2080,11 +2080,20 @@ export class RealUnitService {
// Loki is the PII-free channel: log only the redacted status/type summary here (an Aktionariat error
// body may echo the submitted email). The FULL body still reaches the DB `log` audit store, via the
// returned raw error passed through describeError.
this.logger.error(
`Aktionariat confirmation call to ${endpoint} failed (httpStatus: ${httpStatus ?? 'none'}): ${this.summarizeError(
error,
)}`,
);
const logMessage = `Aktionariat confirmation call to ${endpoint} failed (httpStatus: ${httpStatus ?? 'none'}): ${this.summarizeError(
error,
)}`;
// A rejected confirm link (expired code, unknown email, ...) is the handled INVALID outcome of
// mapConfirmationStatus — there is no API key on this call (the code is the credential), so a 4xx
// cannot mean broken credentials. 429 is carved back out: throttling is a systemic fault. Anything
// else (5xx, timeout, no status) is genuinely unavailable; both stay at error.
const isRejectedLink =
this.mapConfirmationStatus(httpStatus) === RealUnitAktionariatConfirmationStatus.INVALID && httpStatus !== 429;
if (isRejectedLink) {
this.logger.warn(logMessage);
} else {
this.logger.error(logMessage);
}
return { httpStatus, responseBody, error };
}
}
Expand Down