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


Quelle  server-runtime-config.test.ts

  Sprache: JAVA
 

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

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { __resetContainerCacheForTest } from "./net.js";
import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";

const TRUSTED_PROXY_AUTH = {
  mode: "trusted-proxy" as const,
  trustedProxy: {
    userHeader: "x-forwarded-user",
  },
};

const TOKEN_AUTH = {
  mode: "token" as const,
  token: "test-token-123",
};

describe("resolveGatewayRuntimeConfig", () => {
  describe("trusted-proxy auth mode", () => {
    // This test validates BOTH validation layers:
    // 1. CLI validation in src/cli/gateway-cli/run.ts (line 246)
    // 2. Runtime config validation in src/gateway/server-runtime-config.ts (line 99)
    // Both must allow lan binding when authMode === "trusted-proxy"
    it.each([
      {
        name: "lan binding",
        cfg: {
          gateway: {
            bind: "lan" as const,
            auth: TRUSTED_PROXY_AUTH,
            trustedProxies: ["192.168.1.1"],
            controlUi: { allowedOrigins: ["https://control.example.com"] },
          },
        },
        expectedBindHost: "0.0.0.0",
      },
      {
        name: "loopback binding with 127.0.0.1 proxy",
        cfg: {
          gateway: {
            bind: "loopback" as const,
            auth: TRUSTED_PROXY_AUTH,
            trustedProxies: ["127.0.0.1"],
          },
        },
        expectedBindHost: "127.0.0.1",
      },
      {
        name: "loopback binding with ::1 proxy",
        cfg: {
          gateway: { bind: "loopback" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: ["::1"] },
        },
        expectedBindHost: "127.0.0.1",
      },
      {
        name: "loopback binding with loopback cidr proxy",
        cfg: {
          gateway: {
            bind: "loopback" as const,
            auth: TRUSTED_PROXY_AUTH,
            trustedProxies: ["127.0.0.0/8"],
          },
        },
        expectedBindHost: "127.0.0.1",
      },
    ])("allows $name", async ({ cfg, expectedBindHost }) => {
      const result = await resolveGatewayRuntimeConfig({ cfg, port: 18789 });
      expect(result.authMode).toBe("trusted-proxy");
      expect(result.bindHost).toBe(expectedBindHost);
    });

    it.each([
      {
        name: "loopback binding without trusted proxies",
        cfg: {
          gateway: { bind: "loopback" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: [] },
        },
        expectedMessage:
          "gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured",
      },
      {
        name: "lan binding without trusted proxies",
        cfg: {
          gateway: {
            bind: "lan" as const,
            auth: TRUSTED_PROXY_AUTH,
            trustedProxies: [],
            controlUi: { allowedOrigins: ["https://control.example.com"] },
          },
        },
        expectedMessage:
          "gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured",
      },
    ])("rejects $name", async ({ cfg, expectedMessage }) => {
      await expect(resolveGatewayRuntimeConfig({ cfg, port: 18789 })).rejects.toThrow(
        expectedMessage,
      );
    });

    it("allows loopback binding with non-loopback trusted proxies", async () => {
      const result = await resolveGatewayRuntimeConfig({
        cfg: {
          gateway: {
            bind: "loopback",
            auth: TRUSTED_PROXY_AUTH,
            trustedProxies: ["10.0.0.1"],
          },
        },
        port: 18789,
      });

      expect(result.authMode).toBe("trusted-proxy");
      expect(result.bindHost).toBe("127.0.0.1");
    });
  });

  describe("token/password auth modes", () => {
    let originalToken: string | undefined;

    beforeEach(() => {
      originalToken = process.env.OPENCLAW_GATEWAY_TOKEN;
      delete process.env.OPENCLAW_GATEWAY_TOKEN;
    });

    afterEach(() => {
      if (originalToken !== undefined) {
        process.env.OPENCLAW_GATEWAY_TOKEN = originalToken;
      } else {
        delete process.env.OPENCLAW_GATEWAY_TOKEN;
      }
    });

    it.each([
      {
        name: "lan binding with token",
        cfg: {
          gateway: {
            bind: "lan" as const,
            auth: TOKEN_AUTH,
            controlUi: { allowedOrigins: ["https://control.example.com"] },
          },
        },
        expectedAuthMode: "token",
        expectedBindHost: "0.0.0.0",
      },
      {
        name: "loopback binding with explicit none auth",
        cfg: { gateway: { bind: "loopback" as const, auth: { mode: "none" as const } } },
        expectedAuthMode: "none",
        expectedBindHost: "127.0.0.1",
      },
    ])("allows $name", async ({ cfg, expectedAuthMode, expectedBindHost }) => {
      const result = await resolveGatewayRuntimeConfig({ cfg, port: 18789 });
      expect(result.authMode).toBe(expectedAuthMode);
      expect(result.bindHost).toBe(expectedBindHost);
    });

    it.each([
      {
        name: "token mode without token",
        cfg: { gateway: { bind: "lan" as const, auth: { mode: "token" as const } } },
        expectedMessage:
          "gateway auth mode is token, but no token was configured (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN)",
      },
      {
        name: "lan binding with explicit none auth",
        cfg: { gateway: { bind: "lan" as const, auth: { mode: "none" as const } } },
        expectedMessage: "refusing to bind gateway",
      },
      {
        name: "loopback binding that resolves to non-loopback host",
        cfg: { gateway: { bind: "loopback" as const, auth: { mode: "none" as const } } },
        host: "0.0.0.0",
        expectedMessage: "gateway bind=loopback resolved to non-loopback host",
      },
      {
        name: "custom bind without customBindHost",
        cfg: { gateway: { bind: "custom" as const, auth: TOKEN_AUTH } },
        expectedMessage: "gateway.bind=custom requires gateway.customBindHost",
      },
      {
        name: "custom bind with invalid customBindHost",
        cfg: {
          gateway: {
            bind: "custom" as const,
            customBindHost: "192.168.001.100",
            auth: TOKEN_AUTH,
          },
        },
        expectedMessage: "gateway.bind=custom requires a valid IPv4 customBindHost",
      },
      {
        name: "custom bind with mismatched resolved host",
        cfg: {
          gateway: {
            bind: "custom" as const,
            customBindHost: "192.168.1.100",
            auth: TOKEN_AUTH,
          },
        },
        host: "0.0.0.0",
        expectedMessage: "gateway bind=custom requested 192.168.1.100 but resolved 0.0.0.0",
      },
    ])("rejects $name", async ({ cfg, host, expectedMessage }) => {
      await expect(resolveGatewayRuntimeConfig({ cfg, port: 18789, host })).rejects.toThrow(
        expectedMessage,
      );
    });

    it.each([
      {
        name: "rejects non-loopback control UI when allowed origins are missing",
        cfg: {
          gateway: {
            bind: "lan" as const,
            auth: TOKEN_AUTH,
          },
        },
        expectedError: "non-loopback Control UI requires gateway.controlUi.allowedOrigins",
      },
      {
        name: "allows non-loopback control UI without allowed origins when dangerous fallback is enabled",
        cfg: {
          gateway: {
            bind: "lan" as const,
            auth: TOKEN_AUTH,
            controlUi: {
              dangerouslyAllowHostHeaderOriginFallback: true,
            },
          },
        },
        expectedBindHost: "0.0.0.0",
      },
      {
        name: "allows non-loopback control UI when allowed origins collapse after trimming",
        cfg: {
          gateway: {
            bind: "lan" as const,
            auth: TOKEN_AUTH,
            controlUi: {
              allowedOrigins: ["  https://control.example.com  "],
            },
          },
        },
        expectedBindHost: "0.0.0.0",
      },
    ])("$name", async ({ cfg, expectedError, expectedBindHost }) => {
      if (expectedError) {
        await expect(resolveGatewayRuntimeConfig({ cfg, port: 18789 })).rejects.toThrow(
          expectedError,
        );
        return;
      }
      const result = await resolveGatewayRuntimeConfig({ cfg, port: 18789 });
      expect(result.bindHost).toBe(expectedBindHost);
    });
  });

  describe("container-aware bind default", () => {
    afterEach(() => {
      __resetContainerCacheForTest();
      vi.restoreAllMocks();
    });

    it("defaults to auto (0.0.0.0) inside a container with auth configured", async () => {
      const fs = require("node:fs");
      vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); // /.dockerenv exists
      const result = await resolveGatewayRuntimeConfig({
        cfg: {
          gateway: {
            auth: TOKEN_AUTH,
            controlUi: { allowedOrigins: ["https://control.example.com"] },
          },
        },
        port: 18789,
      });
      expect(result.bindHost).toBe("0.0.0.0");
    });

    it("rejects container auto-bind with auth but without allowedOrigins (origin check preserved)", async () => {
      const fs = require("node:fs");
      vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); // /.dockerenv exists
      await expect(
        resolveGatewayRuntimeConfig({
          cfg: { gateway: { auth: TOKEN_AUTH } },
          port: 18789,
        }),
      ).rejects.toThrow(/non-loopback Control UI requires gateway\.controlUi\.allowedOrigins/);
    });

    it("rejects container auto-bind without auth (security invariant preserved)", async () => {
      const fs = require("node:fs");
      vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); // /.dockerenv exists
      await expect(
        resolveGatewayRuntimeConfig({
          cfg: { gateway: { auth: { mode: "none" } } },
          port: 18789,
        }),
      ).rejects.toThrow(/refusing to bind gateway/);
    });

    it("respects explicit loopback config even inside a container", async () => {
      const fs = require("node:fs");
      vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); // /.dockerenv exists
      const result = await resolveGatewayRuntimeConfig({
        cfg: { gateway: { bind: "loopback", auth: { mode: "none" } } },
        port: 18789,
      });
      expect(result.bindHost).toBe("127.0.0.1");
    });

    it("falls back to loopback inside a container when tailscale serve is enabled", async () => {
      const fs = require("node:fs");
      vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); // /.dockerenv exists
      const result = await resolveGatewayRuntimeConfig({
        cfg: {
          gateway: {
            auth: { mode: "none" },
            tailscale: { mode: "serve" },
          },
        },
        port: 18789,
      });
      // Tailscale serve requires loopback — container auto-detection must not
      // override this constraint when bind is unset.
      expect(result.bindHost).toBe("127.0.0.1");
    });

    it("falls back to loopback inside a container when tailscale funnel is enabled", async () => {
      const fs = require("node:fs");
      vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); // /.dockerenv exists
      const result = await resolveGatewayRuntimeConfig({
        cfg: {
          gateway: {
            auth: { mode: "password", password: "test-pw" },
            tailscale: { mode: "funnel" },
          },
        },
        port: 18789,
      });
      expect(result.bindHost).toBe("127.0.0.1");
    });

    it("respects explicit lan config inside a container (requires auth)", async () => {
      const fs = require("node:fs");
      vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); // /.dockerenv exists
      const result = await resolveGatewayRuntimeConfig({
        cfg: {
          gateway: {
            bind: "lan",
            auth: TOKEN_AUTH,
            controlUi: { allowedOrigins: ["https://control.example.com"] },
          },
        },
        port: 18789,
      });
      expect(result.bindHost).toBe("0.0.0.0");
    });
  });

  describe("HTTP security headers", () => {
    const cases = [
      {
        name: "resolves strict transport security headers from config",
        strictTransportSecurity: "  max-age=31536000; includeSubDomains  ",
        expected: "max-age=31536000; includeSubDomains",
      },
      {
        name: "does not set strict transport security when explicitly disabled",
        strictTransportSecurity: false,
        expected: undefined,
      },
      {
        name: "does not set strict transport security when the value is blank",
        strictTransportSecurity: "   ",
        expected: undefined,
      },
    ] satisfies ReadonlyArray<{
      name: string;
      strictTransportSecurity: string | false;
      expected: string | undefined;
    }>;

    it.each(cases)("$name", async ({ strictTransportSecurity, expected }) => {
      const result = await resolveGatewayRuntimeConfig({
        cfg: {
          gateway: {
            bind: "loopback",
            auth: { mode: "none" },
            http: {
              securityHeaders: {
                strictTransportSecurity,
              },
            },
          },
        },
        port: 18789,
      });

      expect(result.strictTransportSecurityHeader).toBe(expected);
    });
  });
});

¤ Dauer der Verarbeitung: 0.16 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