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


Quelle  chat.test.ts

  Sprache: JAVA
 

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

import { describe, expect, it, vi } from "vitest";
import "./test-mocks.js";
import {
  addBlueBubblesParticipant,
  editBlueBubblesMessage,
  leaveBlueBubblesChat,
  markBlueBubblesChatRead,
  removeBlueBubblesParticipant,
  renameBlueBubblesChat,
  sendBlueBubblesTyping,
  setGroupIconBlueBubbles,
  unsendBlueBubblesMessage,
} from "./chat.js";
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
import { installBlueBubblesFetchTestHooks } from "./test-harness.js";

const mockFetch = vi.fn();

installBlueBubblesFetchTestHooks({
  mockFetch,
  privateApiStatusMock: vi.mocked(getCachedBlueBubblesPrivateApiStatus),
});

describe("chat", () => {
  function mockOkTextResponse() {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      text: () => Promise.resolve(""),
    });
  }

  function mockTwoOkTextResponses() {
    mockOkTextResponse();
    mockOkTextResponse();
  }

  async function expectCalledUrlIncludesPassword(params: {
    password: string;
    invoke: () => Promise<void>;
  }) {
    mockOkTextResponse();
    await params.invoke();
    const calledUrl = mockFetch.mock.calls[0][0] as string;
    expect(calledUrl).toContain(`password=${params.password}`);
  }

  async function expectCalledUrlUsesConfigCredentials(params: {
    serverHost: string;
    password: string;
    invoke: (cfg: {
      channels: { bluebubbles: { serverUrl: string; password: string } };
    }) => Promise<void>;
  }) {
    mockOkTextResponse();
    await params.invoke({
      channels: {
        bluebubbles: {
          serverUrl: `http://${params.serverHost}`,
          password: params.password,
        },
      },
    });
    const calledUrl = mockFetch.mock.calls[0][0] as string;
    expect(calledUrl).toContain(params.serverHost);
    expect(calledUrl).toContain(`password=${params.password}`);
  }

  describe("markBlueBubblesChatRead", () => {
    it("does nothing when chatGuid is empty or whitespace", async () => {
      for (const chatGuid of ["", "   "]) {
        await markBlueBubblesChatRead(chatGuid, {
          serverUrl: "http://localhost:1234",
          password: "test",
        });
      }
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it("throws when required credentials are missing", async () => {
      await expect(markBlueBubblesChatRead("chat-guid", {})).rejects.toThrow(
        "serverUrl is required",
      );
      await expect(
        markBlueBubblesChatRead("chat-guid", {
          serverUrl: "http://localhost:1234",
        }),
      ).rejects.toThrow("password is required");
    });

    it("marks chat as read successfully", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await markBlueBubblesChatRead("iMessage;-;+15551234567", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });

      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining("/api/v1/chat/iMessage%3B-%3B%2B15551234567/read"),
        expect.objectContaining({ method: "POST" }),
      );
    });

    it("does not send read receipt when private API is disabled", async () => {
      vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);

      await markBlueBubblesChatRead("iMessage;-;+15551234567", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });

      expect(mockFetch).not.toHaveBeenCalled();
    });

    it("includes password in URL query", async () => {
      await expectCalledUrlIncludesPassword({
        password: "my-secret",
        invoke: () =>
          markBlueBubblesChatRead("chat-123", {
            serverUrl: "http://localhost:1234",
            password: "my-secret",
          }),
      });
    });

    it("throws on non-ok response", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
        text: () => Promise.resolve("Chat not found"),
      });

      await expect(
        markBlueBubblesChatRead("missing-chat", {
          serverUrl: "http://localhost:1234",
          password: "test",
        }),
      ).rejects.toThrow("read failed (404): Chat not found");
    });

    it("trims chatGuid before using", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await markBlueBubblesChatRead("  chat-with-spaces  ", {
        serverUrl: "http://localhost:1234",
        password: "test",
      });

      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain("/api/v1/chat/chat-with-spaces/read");
      expect(calledUrl).not.toContain("%20chat");
    });

    it("resolves credentials from config", async () => {
      await expectCalledUrlUsesConfigCredentials({
        serverHost: "config-server:9999",
        password: "config-pass",
        invoke: (cfg) =>
          markBlueBubblesChatRead("chat-123", {
            cfg,
          }),
      });
    });
  });

  describe("sendBlueBubblesTyping", () => {
    it("does nothing when chatGuid is empty or whitespace", async () => {
      for (const chatGuid of ["", "   "]) {
        await sendBlueBubblesTyping(chatGuid, true, {
          serverUrl: "http://localhost:1234",
          password: "test",
        });
      }
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it("throws when required credentials are missing", async () => {
      await expect(sendBlueBubblesTyping("chat-guid", true, {})).rejects.toThrow(
        "serverUrl is required",
      );
      await expect(
        sendBlueBubblesTyping("chat-guid", true, {
          serverUrl: "http://localhost:1234",
        }),
      ).rejects.toThrow("password is required");
    });

    it("does not send typing when private API is disabled", async () => {
      vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);

      await sendBlueBubblesTyping("iMessage;-;+15551234567", true, {
        serverUrl: "http://localhost:1234",
        password: "test",
      });

      expect(mockFetch).not.toHaveBeenCalled();
    });

    it("uses POST for start and DELETE for stop", async () => {
      mockTwoOkTextResponses();

      await sendBlueBubblesTyping("iMessage;-;+15551234567", true, {
        serverUrl: "http://localhost:1234",
        password: "test",
      });
      await sendBlueBubblesTyping("iMessage;-;+15551234567", false, {
        serverUrl: "http://localhost:1234",
        password: "test",
      });

      expect(mockFetch).toHaveBeenCalledTimes(2);
      expect(mockFetch.mock.calls[0][0]).toContain(
        "/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing",
      );
      expect(mockFetch.mock.calls[0][1].method).toBe("POST");
      expect(mockFetch.mock.calls[1][0]).toContain(
        "/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing",
      );
      expect(mockFetch.mock.calls[1][1].method).toBe("DELETE");
    });

    it("includes password in URL query", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await sendBlueBubblesTyping("chat-123", true, {
        serverUrl: "http://localhost:1234",
        password: "typing-secret",
      });

      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain("password=typing-secret");
    });

    it("throws on non-ok response", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        text: () => Promise.resolve("Internal error"),
      });

      await expect(
        sendBlueBubblesTyping("chat-123", true, {
          serverUrl: "http://localhost:1234",
          password: "test",
        }),
      ).rejects.toThrow("typing failed (500): Internal error");
    });

    it("trims chatGuid before using", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await sendBlueBubblesTyping("  trimmed-chat  ", true, {
        serverUrl: "http://localhost:1234",
        password: "test",
      });

      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain("/api/v1/chat/trimmed-chat/typing");
    });

    it("encodes special characters in chatGuid", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await sendBlueBubblesTyping("iMessage;+;group@chat.com", true, {
        serverUrl: "http://localhost:1234",
        password: "test",
      });

      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain("iMessage%3B%2B%3Bgroup%40chat.com");
    });

    it("resolves credentials from config", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await sendBlueBubblesTyping("chat-123", true, {
        cfg: {
          channels: {
            bluebubbles: {
              serverUrl: "http://typing-server:8888",
              password: "typing-pass",
            },
          },
        },
      });

      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain("typing-server:8888");
      expect(calledUrl).toContain("password=typing-pass");
    });
  });

  describe("editBlueBubblesMessage", () => {
    it("throws when required args are missing", async () => {
      await expect(editBlueBubblesMessage("", "updated", {})).rejects.toThrow("messageGuid");
      await expect(editBlueBubblesMessage("message-guid", "   ", {})).rejects.toThrow("newText");
    });

    it("sends edit request with default payload values", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await editBlueBubblesMessage(" message-guid ", " updated text ", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });

      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining("/api/v1/message/message-guid/edit"),
        expect.objectContaining({
          method: "POST",
          headers: { "Content-Type": "application/json" },
        }),
      );
      const body = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(body).toEqual({
        editedMessage: "updated text",
        backwardsCompatibilityMessage: "Edited to: updated text",
        partIndex: 0,
      });
    });

    it("supports custom part index and backwards compatibility message", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await editBlueBubblesMessage("message-guid", "new text", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
        partIndex: 3,
        backwardsCompatMessage: "custom-backwards-message",
      });

      const body = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(body.partIndex).toBe(3);
      expect(body.backwardsCompatibilityMessage).toBe("custom-backwards-message");
    });

    it("throws on non-ok response", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 422,
        text: () => Promise.resolve("Unprocessable"),
      });

      await expect(
        editBlueBubblesMessage("message-guid", "new text", {
          serverUrl: "http://localhost:1234",
          password: "test-password",
        }),
      ).rejects.toThrow("edit failed (422): Unprocessable");
    });
  });

  describe("unsendBlueBubblesMessage", () => {
    it("throws when messageGuid is missing", async () => {
      await expect(unsendBlueBubblesMessage("", {})).rejects.toThrow("messageGuid");
    });

    it("sends unsend request with default part index", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await unsendBlueBubblesMessage(" msg-123 ", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });

      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining("/api/v1/message/msg-123/unsend"),
        expect.objectContaining({
          method: "POST",
          headers: { "Content-Type": "application/json" },
        }),
      );
      const body = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(body.partIndex).toBe(0);
    });

    it("uses custom part index", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await unsendBlueBubblesMessage("msg-123", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
        partIndex: 2,
      });

      const body = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(body.partIndex).toBe(2);
    });
  });

  describe("group chat mutation actions", () => {
    it("renames chat", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await renameBlueBubblesChat(" chat-guid ", "New Group Name", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });

      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining("/api/v1/chat/chat-guid"),
        expect.objectContaining({ method: "PUT" }),
      );
      const body = JSON.parse(mockFetch.mock.calls[0][1].body);
      expect(body.displayName).toBe("New Group Name");
    });

    it("adds and removes participant using matching endpoint", async () => {
      mockTwoOkTextResponses();

      await addBlueBubblesParticipant("chat-guid", "+15551234567", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });
      await removeBlueBubblesParticipant("chat-guid", "+15551234567", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });

      expect(mockFetch).toHaveBeenCalledTimes(2);
      expect(mockFetch.mock.calls[0][0]).toContain("/api/v1/chat/chat-guid/participant");
      expect(mockFetch.mock.calls[0][1].method).toBe("POST");
      expect(mockFetch.mock.calls[1][0]).toContain("/api/v1/chat/chat-guid/participant");
      expect(mockFetch.mock.calls[1][1].method).toBe("DELETE");

      const addBody = JSON.parse(mockFetch.mock.calls[0][1].body);
      const removeBody = JSON.parse(mockFetch.mock.calls[1][1].body);
      expect(addBody.address).toBe("+15551234567");
      expect(removeBody.address).toBe("+15551234567");
    });

    it("leaves chat without JSON body", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await leaveBlueBubblesChat("chat-guid", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      });

      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining("/api/v1/chat/chat-guid/leave"),
        expect.objectContaining({ method: "POST" }),
      );
      expect(mockFetch.mock.calls[0][1].body).toBeUndefined();
      expect(mockFetch.mock.calls[0][1].headers).toBeUndefined();
    });
  });

  describe("setGroupIconBlueBubbles", () => {
    it("throws when chatGuid is empty", async () => {
      await expect(
        setGroupIconBlueBubbles("", new Uint8Array([1, 2, 3]), "icon.png", {
          serverUrl: "http://localhost:1234",
          password: "test",
        }),
      ).rejects.toThrow("chatGuid");
    });

    it("throws when buffer is empty", async () => {
      await expect(
        setGroupIconBlueBubbles("chat-guid", new Uint8Array(0), "icon.png", {
          serverUrl: "http://localhost:1234",
          password: "test",
        }),
      ).rejects.toThrow("image buffer");
    });

    it("throws when required credentials are missing", async () => {
      await expect(
        setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {}),
      ).rejects.toThrow("serverUrl is required");
      await expect(
        setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {
          serverUrl: "http://localhost:1234",
        }),
      ).rejects.toThrow("password is required");
    });

    it("throws when private API is disabled", async () => {
      vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
      await expect(
        setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {
          serverUrl: "http://localhost:1234",
          password: "test",
        }),
      ).rejects.toThrow("requires Private API");
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it("sets group icon successfully", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); // PNG magic bytes
      await setGroupIconBlueBubbles("iMessage;-;chat-guid", buffer, "icon.png", {
        serverUrl: "http://localhost:1234",
        password: "test-password",
        contentType: "image/png",
      });

      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining("/api/v1/chat/iMessage%3B-%3Bchat-guid/icon"),
        expect.objectContaining({
          method: "POST",
          headers: expect.objectContaining({
            "Content-Type": expect.stringContaining("multipart/form-data"),
          }),
        }),
      );
    });

    it("includes password in URL query", async () => {
      await expectCalledUrlIncludesPassword({
        password: "my-secret",
        invoke: () =>
          setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
            serverUrl: "http://localhost:1234",
            password: "my-secret",
          }),
      });
    });

    it("throws on non-ok response", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        text: () => Promise.resolve("Internal error"),
      });

      await expect(
        setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
          serverUrl: "http://localhost:1234",
          password: "test",
        }),
      ).rejects.toThrow("setGroupIcon failed (500): Internal error");
    });

    it("trims chatGuid before using", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await setGroupIconBlueBubbles("  chat-with-spaces  ", new Uint8Array([1]), "icon.png", {
        serverUrl: "http://localhost:1234",
        password: "test",
      });

      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain("/api/v1/chat/chat-with-spaces/icon");
      expect(calledUrl).not.toContain("%20chat");
    });

    it("resolves credentials from config", async () => {
      await expectCalledUrlUsesConfigCredentials({
        serverHost: "config-server:9999",
        password: "config-pass",
        invoke: (cfg) =>
          setGroupIconBlueBubbles("chat-123", new Uint8Array([1]), "icon.png", {
            cfg,
          }),
      });
    });

    it("includes filename in multipart body", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        text: () => Promise.resolve(""),
      });

      await setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "custom-icon.jpg", {
        serverUrl: "http://localhost:1234",
        password: "test",
        contentType: "image/jpeg",
      });

      const body = mockFetch.mock.calls[0][1].body as Uint8Array;
      const bodyString = new TextDecoder().decode(body);
      expect(bodyString).toContain('filename="custom-icon.jpg"');
      expect(bodyString).toContain("image/jpeg");
    });
  });
});

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