Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  push-apns.relay.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { generateKeyPairSync } from "node:crypto";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
  deriveDeviceIdFromPublicKey,
  publicKeyRawBase64UrlFromPem,
  verifyDeviceSignature,
} from "./device-identity.js";
import { resolveApnsRelayConfigFromEnv, sendApnsRelayPush } from "./push-apns.relay.js";

const relayGatewayIdentity = (() => {
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
  const publicKeyPem = publicKey.export({ format: "pem", type: "spki" });
  const publicKeyRaw = publicKeyRawBase64UrlFromPem(publicKeyPem);
  const deviceId = deriveDeviceIdFromPublicKey(publicKeyRaw);
  if (!deviceId) {
    throw new Error("failed to derive test gateway device id");
  }
  return {
    deviceId,
    publicKey: publicKeyRaw,
    privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }),
  };
})();

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllGlobals();
});

function createRelayPushParams() {
  return {
    relayConfig: {
      baseUrl: "https://relay.example.com",
      timeoutMs: 1000,
    },
    sendGrant: "send-grant-123",
    relayHandle: "relay-handle-123",
    payload: { aps: { "content-available": 1 } },
    pushType: "background" as const,
    priority: "5" as const,
    gatewayIdentity: relayGatewayIdentity,
  };
}

describe("push-apns.relay", () => {
  describe("resolveApnsRelayConfigFromEnv", () => {
    it("returns a missing-config error when no relay base URL is configured", () => {
      expect(resolveApnsRelayConfigFromEnv({} as NodeJS.ProcessEnv)).toEqual({
        ok: false,
        error:
          "APNs relay config missing: set gateway.push.apns.relay.baseUrl or OPENCLAW_APNS_RELAY_BASE_URL",
      });
    });

    it("lets env overrides win and clamps tiny timeout values", () => {
      const resolved = resolveApnsRelayConfigFromEnv(
        {
          OPENCLAW_APNS_RELAY_BASE_URL: " https://relay-override.example.com/base/ ",
          OPENCLAW_APNS_RELAY_TIMEOUT_MS: "999",
        } as NodeJS.ProcessEnv,
        {
          push: {
            apns: {
              relay: {
                baseUrl: "https://relay.example.com",
                timeoutMs: 2500,
              },
            },
          },
        },
      );

      expect(resolved).toMatchObject({
        ok: true,
        value: {
          baseUrl: "https://relay-override.example.com/base",
          timeoutMs: 1000,
        },
      });
    });

    it("allows loopback http URLs for alternate truthy env values", () => {
      const resolved = resolveApnsRelayConfigFromEnv({
        OPENCLAW_APNS_RELAY_BASE_URL: "http://[::1]:8787",
        OPENCLAW_APNS_RELAY_ALLOW_HTTP: "yes",
        OPENCLAW_APNS_RELAY_TIMEOUT_MS: "nope",
      } as NodeJS.ProcessEnv);

      expect(resolved).toMatchObject({
        ok: true,
        value: {
          baseUrl: "http://[::1]:8787",
          timeoutMs: 10_000,
        },
      });
    });

    it.each([
      {
        name: "unsupported protocol",
        env: { OPENCLAW_APNS_RELAY_BASE_URL: "ftp://relay.example.com" },
        expected: "unsupported protocol",
      },
      {
        name: "http non-loopback host",
        env: {
          OPENCLAW_APNS_RELAY_BASE_URL: "http://relay.example.com",
          OPENCLAW_APNS_RELAY_ALLOW_HTTP: "true",
        },
        expected: "loopback hosts",
      },
      {
        name: "query string",
        env: { OPENCLAW_APNS_RELAY_BASE_URL: "https://relay.example.com/path?debug=1" },
        expected: "query and fragment are not allowed",
      },
      {
        name: "userinfo",
        env: { OPENCLAW_APNS_RELAY_BASE_URL: "https://user:pass@relay.example.com/path" },
        expected: "userinfo is not allowed",
      },
    ])("rejects invalid relay URL: $name", ({ env, expected }) => {
      const resolved = resolveApnsRelayConfigFromEnv(env as NodeJS.ProcessEnv);
      expect(resolved.ok).toBe(false);
      if (!resolved.ok) {
        expect(resolved.error).toContain(expected);
      }
    });
  });

  describe("sendApnsRelayPush", () => {
    it("signs relay payloads and forwards the request through the injected sender", async () => {
      vi.spyOn(Date, "now").mockReturnValue(123_456_789);
      const sender = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        apnsId: "relay-apns-id",
        environment: "production",
        tokenSuffix: "abcd1234",
      });

      const result = await sendApnsRelayPush({
        relayConfig: {
          baseUrl: "https://relay.example.com",
          timeoutMs: 1000,
        },
        sendGrant: "send-grant-123",
        relayHandle: "relay-handle-123",
        payload: { aps: { alert: { title: "Wake", body: "Ping" } } },
        pushType: "alert",
        priority: "10",
        gatewayIdentity: relayGatewayIdentity,
        requestSender: sender,
      });

      expect(sender).toHaveBeenCalledTimes(1);
      const sent = sender.mock.calls[0]?.[0];
      expect(sent).toMatchObject({
        relayConfig: {
          baseUrl: "https://relay.example.com",
          timeoutMs: 1000,
        },
        sendGrant: "send-grant-123",
        relayHandle: "relay-handle-123",
        gatewayDeviceId: relayGatewayIdentity.deviceId,
        signedAtMs: 123_456_789,
        pushType: "alert",
        priority: "10",
        payload: { aps: { alert: { title: "Wake", body: "Ping" } } },
      });
      expect(sent?.bodyJson).toBe(
        JSON.stringify({
          relayHandle: "relay-handle-123",
          pushType: "alert",
          priority: 10,
          payload: { aps: { alert: { title: "Wake", body: "Ping" } } },
        }),
      );
      expect(
        verifyDeviceSignature(
          relayGatewayIdentity.publicKey,
          [
            "openclaw-relay-send-v1",
            sent?.gatewayDeviceId,
            String(sent?.signedAtMs),
            sent?.bodyJson,
          ].join("\n"),
          sent?.signature ?? "",
        ),
      ).toBe(true);
      expect(result).toMatchObject({
        ok: true,
        status: 200,
        apnsId: "relay-apns-id",
        environment: "production",
        tokenSuffix: "abcd1234",
      });
    });

    it("does not follow relay redirects", async () => {
      const fetchMock = vi.fn().mockResolvedValue({
        ok: false,
        status: 302,
        json: vi.fn().mockRejectedValue(new Error("no body")),
      });
      vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);

      const result = await sendApnsRelayPush(createRelayPushParams());

      expect(fetchMock).toHaveBeenCalledTimes(1);
      expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({ redirect: "manual" });
      expect(result).toMatchObject({
        ok: false,
        status: 302,
        reason: "RelayRedirectNotAllowed",
        environment: "production",
      });
    });

    it("falls back to fetch status when the relay body is not JSON", async () => {
      const fetchMock = vi.fn().mockResolvedValue({
        ok: true,
        status: 202,
        json: vi.fn().mockRejectedValue(new Error("bad json")),
      });
      vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);

      await expect(sendApnsRelayPush(createRelayPushParams())).resolves.toEqual({
        ok: true,
        status: 202,
        apnsId: undefined,
        reason: undefined,
        environment: "production",
        tokenSuffix: undefined,
      });
    });

    it("normalizes relay JSON response fields", async () => {
      const fetchMock = vi.fn().mockResolvedValue({
        ok: true,
        status: 202,
        json: vi.fn().mockResolvedValue({
          ok: false,
          status: 410,
          apnsId: " relay-apns-id ",
          reason: " Unregistered ",
          tokenSuffix: " abcd1234 ",
        }),
      });
      vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);

      await expect(sendApnsRelayPush(createRelayPushParams())).resolves.toEqual({
        ok: false,
        status: 410,
        apnsId: "relay-apns-id",
        reason: "Unregistered",
        environment: "production",
        tokenSuffix: "abcd1234",
      });
    });
  });
});

¤ Dauer der Verarbeitung: 0.31 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge