Files
accounted/lib/providers/bjornlunden/__tests__/activation.test.ts
T
a769bc9d03 feat(migration): Björn Lundén activation through Lundify's redirect flow (#2374)
* feat(migration): Björn Lundén activation through Lundify's redirect flow

BL issued our integration activation key on 2026-09-07. With
BJORN_LUNDEN_ACTIVATION_KEY set, the connect step offers "Aktivera i
Lundify": the customer logs in at Lundify, picks the company and accepts
the scopes, and Lundify returns the company's User-Key to our callback as
publicKey with our one-time state echoed as extra. The manual User-Key
field stays as a folded fallback for companies that activated inside
Lundify already.

The callback folds publicKey/extra into the OAuth-shaped locals, so the
atomic state consumption, initiator binding and white-label handoff run
unchanged; only the final step differs: submitProviderToken (the same
client-credentials probe as the manual field) instead of an OAuth code
exchange, owned by the consent's company read from the server-written row.
consumeOAuthState/consumeHandoff now return that company id.

Closes #2323.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGDm5S2XPm6np1sKWB4U6L

* fix(migration): reset the previous connect attempt before a new provider request

Review follow-up: a failed /connect used to leave the earlier consent id
and one-time activation URL in place, so the step kept offering a link
that completed the previous consent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGDm5S2XPm6np1sKWB4U6L

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:38:07 +02:00

57 lines
2.1 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest';
import {
LUNDIFY_ACTIVATION_BASE_URL,
buildLundifyActivationUrl,
getBjornLundenActivationKey,
} from '../activation';
const ORIGINAL_KEY = process.env.BJORN_LUNDEN_ACTIVATION_KEY;
afterEach(() => {
if (ORIGINAL_KEY === undefined) {
delete process.env.BJORN_LUNDEN_ACTIVATION_KEY;
} else {
process.env.BJORN_LUNDEN_ACTIVATION_KEY = ORIGINAL_KEY;
}
});
describe('getBjornLundenActivationKey', () => {
it('returns null when the env var is unset or blank', () => {
delete process.env.BJORN_LUNDEN_ACTIVATION_KEY;
expect(getBjornLundenActivationKey()).toBeNull();
process.env.BJORN_LUNDEN_ACTIVATION_KEY = ' ';
expect(getBjornLundenActivationKey()).toBeNull();
});
it('returns the trimmed key when set', () => {
process.env.BJORN_LUNDEN_ACTIVATION_KEY = ' 36b2bf61-0514-4825-a8a5-08cf151176f2 ';
expect(getBjornLundenActivationKey()).toBe('36b2bf61-0514-4825-a8a5-08cf151176f2');
});
});
describe('buildLundifyActivationUrl', () => {
const KEY = '36b2bf61-0514-4825-a8a5-08cf151176f2';
const CALLBACK = 'https://app.accounted.se/api/extensions/ext/arcim-migration/callback';
it('follows BL: /activate-integration/{key}/{encodedRedirectUrl}?extra={state}', () => {
const url = buildLundifyActivationUrl(KEY, CALLBACK, 'state-abc_123');
expect(url).toBe(
`${LUNDIFY_ACTIVATION_BASE_URL}/${KEY}/${encodeURIComponent(CALLBACK)}?extra=state-abc_123`,
);
});
it('encodes the redirect URL as ONE path segment so its slashes do not split the path', () => {
const url = new URL(buildLundifyActivationUrl(KEY, CALLBACK, 's'));
const segments = url.pathname.split('/').filter(Boolean);
expect(segments).toEqual(['activate-integration', KEY, encodeURIComponent(CALLBACK)]);
expect(decodeURIComponent(segments[2]!)).toBe(CALLBACK);
});
it('percent-encodes a state that carries URL-significant characters', () => {
const state = 'a+b&c=d/e';
const url = new URL(buildLundifyActivationUrl(KEY, CALLBACK, state));
expect(url.searchParams.get('extra')).toBe(state);
expect(url.search).not.toContain('&c=');
});
});