fix: 收敛STUN消息源契约
This commit is contained in:
parent
3dbc37e500
commit
ce60b45575
@ -20,7 +20,7 @@ import { classifyStunEndpointSource } from './network-source-eligibility';
|
||||
const SOURCE_KEY = 'network.stun.mapping-port-changed';
|
||||
const SNOWFLAKE_ID_PATTERN = /^[1-9]\d{0,23}$/;
|
||||
const RFC3339_PATTERN =
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|([+-])(\d{2}):(\d{2}))$/;
|
||||
|
||||
type StunSubscriptionConfig = {
|
||||
ddnsRecordId: string;
|
||||
@ -194,8 +194,9 @@ export class NetworkStunMessageSourceAdapter
|
||||
valid: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof SystemMessageContractError)) throw error;
|
||||
return {
|
||||
invalidReasonCode: messageSourceErrorCode(error),
|
||||
invalidReasonCode: error.code,
|
||||
sourceSummary: '未选择有效的 STUN 映射与 DDNS',
|
||||
valid: false,
|
||||
};
|
||||
@ -256,7 +257,7 @@ export class NetworkStunMessageSourceAdapter
|
||||
payload: Record<string, unknown>,
|
||||
): Record<string, SystemMessageScalar> {
|
||||
if (!isPlainRecord(payload)) {
|
||||
throw new SystemMessageContractError('invalid_message_event_payload');
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
const changedAt = normalizeRfc3339(payload.changedAt);
|
||||
const currentPort = normalizePort(payload.currentPort);
|
||||
@ -267,7 +268,7 @@ export class NetworkStunMessageSourceAdapter
|
||||
isIP(payload.publicIpv4) !== 4 ||
|
||||
currentPort === previousPort
|
||||
) {
|
||||
throw new SystemMessageContractError('invalid_message_event_payload');
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
return {
|
||||
changedAt,
|
||||
@ -291,25 +292,27 @@ export class NetworkStunMessageSourceAdapter
|
||||
try {
|
||||
resolved = await this.resolveSubscription(input.subscriptionConfig);
|
||||
} catch (error) {
|
||||
return { reasonCode: messageSourceErrorCode(error), status: 'cancelled' };
|
||||
if (!(error instanceof SystemMessageContractError)) throw error;
|
||||
return { reasonCode: error.code, status: 'cancelled' };
|
||||
}
|
||||
let event: StunEventPayload;
|
||||
try {
|
||||
event = this.validateEventPayload(input.eventPayload) as StunEventPayload;
|
||||
} catch (error) {
|
||||
return { reasonCode: messageSourceErrorCode(error), status: 'cancelled' };
|
||||
if (!(error instanceof SystemMessageContractError)) throw error;
|
||||
return { reasonCode: error.code, status: 'cancelled' };
|
||||
}
|
||||
if (event.portForwardId !== resolved.config.portForwardId) {
|
||||
return { reasonCode: 'event_resource_mismatch', status: 'cancelled' };
|
||||
return { reasonCode: 'invalid_source_config', status: 'cancelled' };
|
||||
}
|
||||
if (!hasCurrentEndpoint(resolved.mapping)) {
|
||||
return { reasonCode: 'endpoint_unavailable', status: 'superseded' };
|
||||
return { reasonCode: 'endpoint_superseded', status: 'superseded' };
|
||||
}
|
||||
if (
|
||||
resolved.mapping.currentPublicPort !== event.currentPort ||
|
||||
resolved.mapping.currentPublicIpv4 !== event.publicIpv4
|
||||
) {
|
||||
return { reasonCode: 'endpoint_changed', status: 'superseded' };
|
||||
return { reasonCode: 'endpoint_superseded', status: 'superseded' };
|
||||
}
|
||||
const variables = deliveryVariables(resolved, event);
|
||||
if (
|
||||
@ -334,7 +337,7 @@ export class NetworkStunMessageSourceAdapter
|
||||
input: unknown,
|
||||
): Promise<ResolvedSubscription> {
|
||||
if (!isPlainRecord(input)) {
|
||||
throw new SystemMessageContractError('invalid_message_source_config');
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
let config: StunSubscriptionConfig;
|
||||
try {
|
||||
@ -343,24 +346,28 @@ export class NetworkStunMessageSourceAdapter
|
||||
portForwardId: normalizeSnowflakeId(input.portForwardId),
|
||||
};
|
||||
} catch {
|
||||
throw new SystemMessageContractError('invalid_message_source_config');
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
const mapping = await this.mappingRepository.findOne({
|
||||
where: { id: config.portForwardId },
|
||||
});
|
||||
if (!mapping) {
|
||||
throw new SystemMessageContractError('source_not_found');
|
||||
throw new SystemMessageContractError('mapping_not_found');
|
||||
}
|
||||
const sourceEligibility = classifyStunEndpointSource(mapping);
|
||||
if (!sourceEligibility.eligible) {
|
||||
throw new SystemMessageContractError(
|
||||
sourceEligibility.disabledReasonCode as string,
|
||||
messageSourceEligibilityReason(
|
||||
sourceEligibility.disabledReasonCode as NonNullable<
|
||||
typeof sourceEligibility.disabledReasonCode
|
||||
>,
|
||||
),
|
||||
);
|
||||
}
|
||||
const ddnsRecord = await this.ddnsRepository.findOne({
|
||||
where: { id: config.ddnsRecordId },
|
||||
});
|
||||
const ddnsReason = ddnsOptionReason(ddnsRecord, mapping);
|
||||
const ddnsReason = ddnsMessageSourceReason(ddnsRecord, mapping);
|
||||
if (ddnsReason) {
|
||||
throw new SystemMessageContractError(ddnsReason);
|
||||
}
|
||||
@ -389,7 +396,7 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
*/
|
||||
function normalizeSnowflakeId(value: unknown): string {
|
||||
if (typeof value !== 'string' || !SNOWFLAKE_ID_PATTERN.test(value)) {
|
||||
throw new SystemMessageContractError('invalid_message_source_config');
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@ -406,7 +413,7 @@ function normalizePort(value: unknown): number {
|
||||
value < 1 ||
|
||||
value > 65_535
|
||||
) {
|
||||
throw new SystemMessageContractError('invalid_message_event_payload');
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@ -417,16 +424,74 @@ function normalizePort(value: unknown): number {
|
||||
* @returns Canonical ISO string safe for later Shanghai rendering.
|
||||
*/
|
||||
function normalizeRfc3339(value: unknown): string {
|
||||
if (typeof value !== 'string' || !RFC3339_PATTERN.test(value)) {
|
||||
throw new SystemMessageContractError('invalid_message_event_payload');
|
||||
if (typeof value !== 'string') {
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
const match = RFC3339_PATTERN.exec(value);
|
||||
if (!match || !isValidRfc3339Calendar(match)) {
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
throw new SystemMessageContractError('invalid_message_event_payload');
|
||||
throw new SystemMessageContractError('invalid_source_config');
|
||||
}
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates RFC3339 calendar and offset components before JavaScript can normalize them.
|
||||
* @param match - Capture groups produced by the source timestamp pattern.
|
||||
* @returns Whether all calendar, wall-clock, and offset fields are in range.
|
||||
*/
|
||||
function isValidRfc3339Calendar(match: RegExpExecArray): boolean {
|
||||
const [
|
||||
,
|
||||
yearText,
|
||||
monthText,
|
||||
dayText,
|
||||
hourText,
|
||||
minuteText,
|
||||
secondText,
|
||||
,
|
||||
,
|
||||
offsetHourText,
|
||||
offsetMinuteText,
|
||||
] = match;
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
const day = Number(dayText);
|
||||
const hour = Number(hourText);
|
||||
const minute = Number(minuteText);
|
||||
const second = Number(secondText);
|
||||
const offsetHour = offsetHourText ? Number(offsetHourText) : 0;
|
||||
const offsetMinute = offsetMinuteText ? Number(offsetMinuteText) : 0;
|
||||
const daysInMonth = [
|
||||
31,
|
||||
year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
];
|
||||
return (
|
||||
month >= 1 &&
|
||||
month <= 12 &&
|
||||
day >= 1 &&
|
||||
day <= daysInMonth[month - 1] &&
|
||||
hour <= 23 &&
|
||||
minute <= 59 &&
|
||||
second <= 59 &&
|
||||
offsetHour <= 23 &&
|
||||
offsetMinute <= 59
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a persisted DDNS record is a usable structural subscription target.
|
||||
* @param record - Candidate A-record binding, possibly absent or deleted.
|
||||
@ -450,6 +515,65 @@ function ddnsOptionReason(
|
||||
return source.disabledReasonCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a shared Network structural reason to the locked message-source contract.
|
||||
* @param reason - Existing uppercase Network eligibility reason.
|
||||
* @returns Lowercase reason code safe for message subscription views and delivery state.
|
||||
*/
|
||||
function messageSourceEligibilityReason(
|
||||
reason: NonNullable<
|
||||
ReturnType<typeof classifyStunEndpointSource>['disabledReasonCode']
|
||||
>,
|
||||
):
|
||||
| 'keeper_disabled'
|
||||
| 'mapping_not_managed'
|
||||
| 'mapping_not_udp'
|
||||
| 'mapping_port_mismatch' {
|
||||
switch (reason) {
|
||||
case 'KEEPER_DISABLED':
|
||||
return 'keeper_disabled';
|
||||
case 'PORT_MISMATCH':
|
||||
return 'mapping_port_mismatch';
|
||||
case 'SOURCE_DELETING':
|
||||
return 'mapping_not_managed';
|
||||
case 'UDP_REQUIRED':
|
||||
return 'mapping_not_udp';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the locked message-source reason for a DDNS record and its selected mapping.
|
||||
* @param record - Candidate DDNS record, including deleted or absent state.
|
||||
* @param mapping - Canonical selected mapping after its structural validation.
|
||||
* @returns Null when valid or a locked lowercase message-source reason.
|
||||
*/
|
||||
function ddnsMessageSourceReason(
|
||||
record: NetworkDdnsRecord | null | undefined,
|
||||
mapping: NetworkPortForward,
|
||||
):
|
||||
| 'ddns_disabled'
|
||||
| 'ddns_mapping_mismatch'
|
||||
| 'ddns_not_found'
|
||||
| 'ddns_not_ipv4'
|
||||
| 'keeper_disabled'
|
||||
| 'mapping_not_managed'
|
||||
| 'mapping_not_udp'
|
||||
| 'mapping_port_mismatch'
|
||||
| null {
|
||||
if (!record || record.isDeleted) return 'ddns_not_found';
|
||||
if (!record.enabled) return 'ddns_disabled';
|
||||
if (record.recordType !== 'A' || record.sourceType !== 'port_forward_ipv4') {
|
||||
return 'ddns_not_ipv4';
|
||||
}
|
||||
if (String(record.portForwardId) !== String(mapping.id)) {
|
||||
return 'ddns_mapping_mismatch';
|
||||
}
|
||||
const source = classifyStunEndpointSource(mapping);
|
||||
return source.disabledReasonCode
|
||||
? messageSourceEligibilityReason(source.disabledReasonCode)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms the normalized fully-qualified hostname from persisted server-owned DDNS fields.
|
||||
* @param record - Canonical DDNS binding.
|
||||
@ -520,14 +644,3 @@ function formatShanghaiDateTime(value: string): string {
|
||||
parts.find((item) => item.type === type)?.value || '';
|
||||
return `${part('year')}-${part('month')}-${part('day')} ${part('hour')}:${part('minute')}:${part('second')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts adapter validation failures to one stable, non-sensitive reason code.
|
||||
* @param error - Unknown caught adapter or repository error.
|
||||
* @returns Domain code for expected contract failures or a generic source-unavailable code.
|
||||
*/
|
||||
function messageSourceErrorCode(error: unknown): string {
|
||||
return error instanceof SystemMessageContractError
|
||||
? error.code
|
||||
: 'message_source_unavailable';
|
||||
}
|
||||
|
||||
@ -7,7 +7,9 @@ import { SystemMessageSourceRegistry } from '../../../src/modules/qqbot/core/app
|
||||
type Harness = {
|
||||
adapter: NetworkStunMessageSourceAdapter;
|
||||
ddns: NetworkDdnsRecord;
|
||||
ddnsRepository: Repository<NetworkDdnsRecord>;
|
||||
mapping: NetworkPortForward;
|
||||
mappingRepository: Repository<NetworkPortForward>;
|
||||
registry: SystemMessageSourceRegistry;
|
||||
};
|
||||
|
||||
@ -62,7 +64,9 @@ function createHarness(): Harness {
|
||||
registry,
|
||||
),
|
||||
ddns,
|
||||
ddnsRepository: recordRepository,
|
||||
mapping,
|
||||
mappingRepository,
|
||||
registry,
|
||||
};
|
||||
}
|
||||
@ -81,6 +85,15 @@ function eventPayload(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('NetworkStunMessageSourceAdapter', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2026-07-24T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('registers once and only unregisters its own source instance', () => {
|
||||
const { adapter, registry } = createHarness();
|
||||
adapter.onModuleInit();
|
||||
@ -111,43 +124,94 @@ describe('NetworkStunMessageSourceAdapter', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
['tcp', (harness: Harness) => (harness.mapping.protocol = 'tcp')],
|
||||
['unequal ports', (harness: Harness) => (harness.mapping.internalPort = 1)],
|
||||
[
|
||||
'tcp',
|
||||
'mapping_not_udp',
|
||||
(harness: Harness) => (harness.mapping.protocol = 'tcp'),
|
||||
],
|
||||
[
|
||||
'unequal ports',
|
||||
'mapping_port_mismatch',
|
||||
(harness: Harness) => (harness.mapping.internalPort = 1),
|
||||
],
|
||||
[
|
||||
'disabled keeper',
|
||||
'keeper_disabled',
|
||||
(harness: Harness) => (harness.mapping.keeperDesiredEnabled = false),
|
||||
],
|
||||
[
|
||||
'deleted mapping',
|
||||
'mapping_not_managed',
|
||||
(harness: Harness) => (harness.mapping.isDeleted = true),
|
||||
],
|
||||
['disabled DDNS', (harness: Harness) => (harness.ddns.enabled = false)],
|
||||
['deleted DDNS', (harness: Harness) => (harness.ddns.isDeleted = true)],
|
||||
['non-A DDNS', (harness: Harness) => (harness.ddns.recordType = 'AAAA')],
|
||||
[
|
||||
'disabled DDNS',
|
||||
'ddns_disabled',
|
||||
(harness: Harness) => (harness.ddns.enabled = false),
|
||||
],
|
||||
[
|
||||
'deleted DDNS',
|
||||
'ddns_not_found',
|
||||
(harness: Harness) => (harness.ddns.isDeleted = true),
|
||||
],
|
||||
[
|
||||
'non-A DDNS',
|
||||
'ddns_not_ipv4',
|
||||
(harness: Harness) => (harness.ddns.recordType = 'AAAA'),
|
||||
],
|
||||
[
|
||||
'non-port-forward DDNS',
|
||||
'ddns_not_ipv4',
|
||||
(harness: Harness) => (harness.ddns.sourceType = 'agent_ipv6'),
|
||||
],
|
||||
[
|
||||
'mismatched DDNS mapping',
|
||||
'ddns_mapping_mismatch',
|
||||
(harness: Harness) =>
|
||||
(harness.ddns.portForwardId = '2041700000000000003'),
|
||||
],
|
||||
])('rejects %s subscriptions', async (_name, mutate) => {
|
||||
])(
|
||||
'rejects %s subscriptions with the locked %s code',
|
||||
async (_name, code, mutate) => {
|
||||
const harness = createHarness();
|
||||
mutate(harness);
|
||||
await expect(
|
||||
harness.adapter.normalizeSubscriptionConfig({
|
||||
const config = {
|
||||
ddnsRecordId: harness.ddns.id,
|
||||
portForwardId: harness.mapping.id,
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
};
|
||||
await expect(
|
||||
harness.adapter.normalizeSubscriptionConfig(config),
|
||||
).rejects.toMatchObject({
|
||||
code,
|
||||
});
|
||||
await expect(
|
||||
harness.adapter.inspectSubscription(config),
|
||||
).resolves.toEqual({
|
||||
invalidReasonCode: code,
|
||||
sourceSummary: '未选择有效的 STUN 映射与 DDNS',
|
||||
valid: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('rejects malformed Snowflake IDs and strips unknown event/config fields', async () => {
|
||||
it('rejects malformed subscription/event payloads and strips unknown fields', async () => {
|
||||
const { adapter } = createHarness();
|
||||
await expect(
|
||||
adapter.normalizeSubscriptionConfig({
|
||||
ddnsRecordId: 2041700000000000002,
|
||||
portForwardId: 'not-an-id',
|
||||
}),
|
||||
).rejects.toThrow('invalid_message_source_config');
|
||||
).rejects.toMatchObject({ code: 'invalid_source_config' });
|
||||
await expect(
|
||||
adapter.inspectSubscription({
|
||||
ddnsRecordId: 2041700000000000002,
|
||||
portForwardId: 'not-an-id',
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
invalidReasonCode: 'invalid_source_config',
|
||||
sourceSummary: '未选择有效的 STUN 映射与 DDNS',
|
||||
valid: false,
|
||||
});
|
||||
expect(adapter.validateEventPayload(eventPayload())).toEqual({
|
||||
changedAt: '2026-07-24T12:30:00.000Z',
|
||||
currentPort: 38213,
|
||||
@ -157,7 +221,88 @@ describe('NetworkStunMessageSourceAdapter', () => {
|
||||
});
|
||||
expect(() =>
|
||||
adapter.validateEventPayload(eventPayload({ currentPort: '38213' })),
|
||||
).toThrow('invalid_message_event_payload');
|
||||
).toThrow('invalid_source_config');
|
||||
expect(() =>
|
||||
adapter.validateEventPayload(eventPayload({ publicIpv4: '2001:db8::1' })),
|
||||
).toThrow('invalid_source_config');
|
||||
expect(() => adapter.validateEventPayload(null as never)).toThrow(
|
||||
'invalid_source_config',
|
||||
);
|
||||
});
|
||||
|
||||
it('maps an absent mapping to the locked subscription and inspection code', async () => {
|
||||
const { adapter } = createHarness();
|
||||
const config = {
|
||||
ddnsRecordId: '2041700000000000002',
|
||||
portForwardId: '2041700000000000003',
|
||||
};
|
||||
await expect(
|
||||
adapter.normalizeSubscriptionConfig(config),
|
||||
).rejects.toMatchObject({
|
||||
code: 'mapping_not_found',
|
||||
});
|
||||
await expect(adapter.inspectSubscription(config)).resolves.toEqual({
|
||||
invalidReasonCode: 'mapping_not_found',
|
||||
sourceSummary: '未选择有效的 STUN 映射与 DDNS',
|
||||
valid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('maps an absent DDNS record to the locked subscription and inspection code', async () => {
|
||||
const { adapter } = createHarness();
|
||||
const config = {
|
||||
ddnsRecordId: '2041700000000000003',
|
||||
portForwardId: '2041700000000000001',
|
||||
};
|
||||
await expect(
|
||||
adapter.normalizeSubscriptionConfig(config),
|
||||
).rejects.toMatchObject({
|
||||
code: 'ddns_not_found',
|
||||
});
|
||||
await expect(adapter.inspectSubscription(config)).resolves.toEqual({
|
||||
invalidReasonCode: 'ddns_not_found',
|
||||
sourceSummary: '未选择有效的 STUN 映射与 DDNS',
|
||||
valid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves uppercase Network eligibility reasons in subscription options', async () => {
|
||||
const { adapter, mapping } = createHarness();
|
||||
mapping.protocol = 'tcp';
|
||||
await expect(adapter.listSubscriptionOptions()).resolves.toMatchObject({
|
||||
portForwards: [
|
||||
{
|
||||
disabledReasonCode: 'UDP_REQUIRED',
|
||||
eligible: false,
|
||||
externalPort: 8213,
|
||||
id: mapping.id,
|
||||
internalPort: 8213,
|
||||
name: '帕鲁新世界',
|
||||
protocol: 'tcp',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['invalid month', '2026-13-01T12:00:00Z'],
|
||||
['non-leap-day', '2026-02-29T12:00:00Z'],
|
||||
['invalid February day', '2026-02-30T12:00:00Z'],
|
||||
['invalid offset', '2026-02-28T12:00:00+24:00'],
|
||||
])('rejects an RFC3339 timestamp with %s', (_name, changedAt) => {
|
||||
const { adapter } = createHarness();
|
||||
expect(() =>
|
||||
adapter.validateEventPayload(eventPayload({ changedAt })),
|
||||
).toThrow('invalid_source_config');
|
||||
});
|
||||
|
||||
it('normalizes a leap-day RFC3339 timestamp with an offset', () => {
|
||||
const { adapter } = createHarness();
|
||||
expect(
|
||||
adapter.validateEventPayload(
|
||||
eventPayload({ changedAt: '2024-02-29T23:59:59.123+08:00' }),
|
||||
),
|
||||
).toMatchObject({ changedAt: '2024-02-29T15:59:59.123Z' });
|
||||
});
|
||||
|
||||
it('returns ready variables derived from the server-owned DDNS FQDN and Shanghai time', async () => {
|
||||
@ -185,7 +330,7 @@ describe('NetworkStunMessageSourceAdapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('waits for DDNS, supersedes a replaced endpoint, and cancels a changed relationship', async () => {
|
||||
it('waits for DDNS, supersedes replaced or expired endpoints, and cancels a changed relationship', async () => {
|
||||
const waiting = createHarness();
|
||||
waiting.ddns.appliedAddress = null;
|
||||
await expect(
|
||||
@ -211,7 +356,25 @@ describe('NetworkStunMessageSourceAdapter', () => {
|
||||
portForwardId: superseded.mapping.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({ status: 'superseded' });
|
||||
).resolves.toMatchObject({
|
||||
reasonCode: 'endpoint_superseded',
|
||||
status: 'superseded',
|
||||
});
|
||||
|
||||
const expired = createHarness();
|
||||
expired.mapping.currentValidUntil = new Date('2026-07-24T11:59:59.999Z');
|
||||
await expect(
|
||||
expired.adapter.resolveDelivery({
|
||||
eventPayload: eventPayload(),
|
||||
subscriptionConfig: {
|
||||
ddnsRecordId: expired.ddns.id,
|
||||
portForwardId: expired.mapping.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
reasonCode: 'endpoint_superseded',
|
||||
status: 'superseded',
|
||||
});
|
||||
|
||||
const cancelled = createHarness();
|
||||
cancelled.ddns.enabled = false;
|
||||
@ -223,6 +386,62 @@ describe('NetworkStunMessageSourceAdapter', () => {
|
||||
portForwardId: cancelled.mapping.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({ status: 'cancelled' });
|
||||
).resolves.toMatchObject({
|
||||
reasonCode: 'ddns_disabled',
|
||||
status: 'cancelled',
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels event mapping identity mismatches with invalid_source_config', async () => {
|
||||
const { adapter } = createHarness();
|
||||
await expect(
|
||||
adapter.resolveDelivery({
|
||||
eventPayload: eventPayload({ portForwardId: '2041700000000000003' }),
|
||||
subscriptionConfig: {
|
||||
ddnsRecordId: '2041700000000000002',
|
||||
portForwardId: '2041700000000000001',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
reasonCode: 'invalid_source_config',
|
||||
status: 'cancelled',
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels malformed event payloads with invalid_source_config', async () => {
|
||||
const { adapter } = createHarness();
|
||||
await expect(
|
||||
adapter.resolveDelivery({
|
||||
eventPayload: eventPayload({ currentPort: '38213' }),
|
||||
subscriptionConfig: {
|
||||
ddnsRecordId: '2041700000000000002',
|
||||
portForwardId: '2041700000000000001',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
reasonCode: 'invalid_source_config',
|
||||
status: 'cancelled',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['mapping', 'mappingRepository'],
|
||||
['DDNS', 'ddnsRepository'],
|
||||
] as const)(
|
||||
'rethrows an unexpected %s repository error for delivery retry',
|
||||
async (_name, repository) => {
|
||||
const harness = createHarness();
|
||||
const failure = new Error(`${repository} unavailable`);
|
||||
jest.spyOn(harness[repository], 'findOne').mockRejectedValueOnce(failure);
|
||||
await expect(
|
||||
harness.adapter.resolveDelivery({
|
||||
eventPayload: eventPayload(),
|
||||
subscriptionConfig: {
|
||||
ddnsRecordId: harness.ddns.id,
|
||||
portForwardId: harness.mapping.id,
|
||||
},
|
||||
}),
|
||||
).rejects.toBe(failure);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user