Ctrl K
ComunidadGuías…

server/models/oauth/OAuthClient.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { randomUUID } from "node:crypto";
import { OAuthClient } from "@server/models";
import { sequelize } from "@server/storage/database";
import FileStorage from "@server/storage/files";
import {
  buildMetadataDocumentOAuthClient,
  buildOAuthClient,
  buildTeam,
} from "@server/test/factories";

describe("OAuthClient", () => {
  describe("isDCR and isCIMD", () => {
    it("should identify a dynamically registered client", async () => {
      const client = await buildOAuthClient({ createdById: null });

      expect(client.isDCR).toBe(true);
      expect(client.isCIMD).toBe(false);
      expect(client.clientSecret).toBeTruthy();
      expect(client.registrationAccessTokenHash).toBeTruthy();
    });

    it("should identify a user created client", async () => {
      const client = await buildOAuthClient();

      expect(client.isDCR).toBe(false);
      expect(client.isCIMD).toBe(false);
      expect(client.registrationAccessTokenHash).toBeNull();
    });

    it("should identify a metadata document client", async () => {
      const clientId = `https://${randomUUID()}.example.com/client.json`;
      const client = await buildMetadataDocumentOAuthClient({ clientId });

      expect(client.isDCR).toBe(false);
      expect(client.isCIMD).toBe(true);
      expect(client.clientId).toEqual(clientId);
      expect(client.clientSecret).toBeNull();
      expect(client.registrationAccessTokenHash).toBeNull();
    });
  });

  describe("constraints", () => {
    it("should not allow a client without a team unless it uses a metadata document", async () => {
      const client = await buildOAuthClient({ createdById: null });

      await expect(
        OAuthClient.update(
          { teamId: null },
          { where: { id: client.id }, hooks: false }
        )
      ).rejects.toThrow();
    });

    it("should not allow a metadata document client to have a team", async () => {
      const team = await buildTeam();
      const client = await buildMetadataDocumentOAuthClient();

      await expect(
        OAuthClient.update(
          { teamId: team.id },
          { where: { id: client.id }, hooks: false }
        )
      ).rejects.toThrow();
    });
  });

  describe("logos", () => {
    const buildHash = () => randomUUID().replace(/-/g, "").repeat(2);
    const buildLogoUrl = (clientId: string) =>
      FileStorage.getUrlForKey(
        OAuthClient.logoKey(clientId, buildHash(), "png")
      );
    const buildClientWithLogo = () => {
      const id = randomUUID();
      return buildMetadataDocumentOAuthClient({
        id,
        avatarUrl: buildLogoUrl(id),
      });
    };

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

    it("should find the key of a copied logo", async () => {
      const client = await buildMetadataDocumentOAuthClient();
      const key = OAuthClient.logoKey(client.id, "a".repeat(64), "png");

      expect(client.logoKeyFromUrl(FileStorage.getUrlForKey(key))).toBe(key);
    });

    it("should not find a key in a URL on another host", async () => {
      const client = await buildMetadataDocumentOAuthClient();
      const key = OAuthClient.logoKey(client.id, "a".repeat(64), "png");

      expect(
        client.logoKeyFromUrl(`https://evil.example.com/${key}`)
      ).toBeUndefined();
      expect(client.logoKeyFromUrl(null)).toBeUndefined();
    });

    it("should not find a key for the logo of another client", async () => {
      const client = await buildMetadataDocumentOAuthClient();

      expect(client.logoKeyFromUrl(buildLogoUrl(randomUUID()))).toBeUndefined();
    });

    it("should not find a key that leaves the folder of the client", async () => {
      const client = await buildMetadataDocumentOAuthClient();
      const key = `${OAuthClient.logoKeyPrefix(client.id)}..`;

      expect(
        client.logoKeyFromUrl(FileStorage.getUrlForKey(key))
      ).toBeUndefined();
    });

    it("should delete the logo when the client is deleted", async () => {
      const deleteFile = vi.spyOn(FileStorage, "deleteFile");
      const client = await buildClientWithLogo();
      const key = client.logoKeyFromUrl(client.avatarUrl);

      await client.destroy({ force: true });

      expect(deleteFile).toHaveBeenCalledWith(key);
    });

    it("should not delete the logo of another client", async () => {
      const deleteFile = vi.spyOn(FileStorage, "deleteFile");
      const other = await buildClientWithLogo();
      const client = await buildMetadataDocumentOAuthClient({
        avatarUrl: other.avatarUrl,
      });

      await client.destroy();

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

    it("should delete a replaced logo", async () => {
      const deleteFile = vi.spyOn(FileStorage, "deleteFile");
      const client = await buildClientWithLogo();
      const key = client.logoKeyFromUrl(client.avatarUrl);

      await client.update({ avatarUrl: buildLogoUrl(client.id) });

      expect(deleteFile).toHaveBeenCalledWith(key);
    });

    it("should not delete a logo when the transaction rolls back", async () => {
      const deleteFile = vi.spyOn(FileStorage, "deleteFile");
      const client = await buildClientWithLogo();

      await expect(
        sequelize.transaction(async (transaction) => {
          await client.destroy({ transaction });
          throw new Error("rollback");
        })
      ).rejects.toThrow("rollback");

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

    it("should not delete a logo that was not copied", async () => {
      const deleteFile = vi.spyOn(FileStorage, "deleteFile");
      const client = await buildOAuthClient({
        avatarUrl: "https://example.com/logo.png",
      });

      await client.destroy();

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