Ctrl K
ComunidadGuías…

src/lib/passkey-name.test.ts

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

Archivo original ↗Preguntar a KiBot
import { describe, expect, it } from "vitest";
import {
  PASSKEY_NAME_MAX_LENGTH,
  deviceNameForUserAgent,
  passkeyNameForUserAgent,
} from "./passkey-name";

// Real UA strings, because the whole risk here is in the tokens browsers share:
// every Chromium browser says "Chrome", Chrome on iOS says "Safari" too, and an
// iPad in desktop mode says "Macintosh". A hand-written string would quietly stop
// testing that.
const USER_AGENTS = {
  chromeMac:
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
  safariMac:
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15",
  edgeWindows:
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
  operaWindows:
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 OPR/125.0.0.0",
  firefoxLinux:
    "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0",
  chromeAndroid:
    "Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36",
  samsungAndroid:
    "Mozilla/5.0 (Linux; Android 15; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/27.0 Chrome/125.0.0.0 Mobile Safari/537.36",
  safariIphone:
    "Mozilla/5.0 (iPhone; CPU iPhone OS 18_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Mobile/15E148 Safari/604.1",
  chromeIphone:
    "Mozilla/5.0 (iPhone; CPU iPhone OS 18_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.0.0 Mobile/15E148 Safari/604.1",
  chromeOS:
    "Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
};

describe("passkeyNameForUserAgent", () => {
  it("names the browser and the platform", () => {
    expect(passkeyNameForUserAgent(USER_AGENTS.chromeMac)).toBe(
      "Chrome on macOS",
    );
    expect(passkeyNameForUserAgent(USER_AGENTS.safariMac)).toBe(
      "Safari on macOS",
    );
    expect(passkeyNameForUserAgent(USER_AGENTS.firefoxLinux)).toBe(
      "Firefox on Linux",
    );
    expect(passkeyNameForUserAgent(USER_AGENTS.chromeAndroid)).toBe(
      "Chrome on Android",
    );
  });

  // The case the table's ordering exists for: both of these say "Chrome" and
  // "Safari" as well, and a first-match-wins table written the other way round
  // would call every Edge passkey "Chrome on Windows".
  it("prefers the specific Chromium badge over the tokens it also carries", () => {
    expect(passkeyNameForUserAgent(USER_AGENTS.edgeWindows)).toBe(
      "Edge on Windows",
    );
    expect(passkeyNameForUserAgent(USER_AGENTS.operaWindows)).toBe(
      "Opera on Windows",
    );
    expect(passkeyNameForUserAgent(USER_AGENTS.samsungAndroid)).toBe(
      "Samsung Internet on Android",
    );
  });

  // Every browser on iOS is the same WebKit, so naming it would draw a
  // distinction the authenticator doesn't have - and "Android" is the platform
  // token an iOS-first table would have to look past.
  it("names the device alone on iOS, whichever browser is asking", () => {
    expect(passkeyNameForUserAgent(USER_AGENTS.safariIphone)).toBe("iPhone");
    expect(passkeyNameForUserAgent(USER_AGENTS.chromeIphone)).toBe("iPhone");
  });

  // "CrOS" also contains no Linux token, but the string carries X11 - so this
  // pins the order in the platform table too.
  it("calls ChromeOS ChromeOS rather than Linux", () => {
    expect(passkeyNameForUserAgent(USER_AGENTS.chromeOS)).toBe(
      "Chrome on ChromeOS",
    );
  });

  // A name is a label, not an answer that can be wrong: a UA string this doesn't
  // recognise still has to produce something the API will accept.
  it("falls back to a plain label rather than an empty name", () => {
    expect(passkeyNameForUserAgent("")).toBe("Passkey");
    expect(passkeyNameForUserAgent("SomeKiosk/1.0")).toBe("Passkey");
  });

  it("names whichever half it does recognise", () => {
    expect(passkeyNameForUserAgent("Mozilla/5.0 (Windows NT 10.0)")).toBe(
      "Windows",
    );
    expect(passkeyNameForUserAgent("Firefox/135.0")).toBe("Firefox");
  });

  // The API caps the column at 50 characters, and a suggestion it rejects would
  // fail a ceremony the diver already completed.
  it("never suggests a name the API would refuse", () => {
    for (const userAgent of Object.values(USER_AGENTS)) {
      const name = passkeyNameForUserAgent(userAgent);
      expect(name.length).toBeGreaterThan(0);
      expect(name.length).toBeLessThanOrEqual(PASSKEY_NAME_MAX_LENGTH);
    }
  });
});

describe("deviceNameForUserAgent", () => {
  // The reason both readers share one function: a session row and a passkey row
  // on the same settings page must never call the same browser two different
  // things. Everything a UA string is actually recognised by is the same
  // reading, and the fallback is the only difference there is.
  it("reads a recognised string exactly as the passkey label does", () => {
    for (const userAgent of Object.values(USER_AGENTS)) {
      expect(deviceNameForUserAgent(userAgent)).toBe(
        passkeyNameForUserAgent(userAgent),
      );
    }
  });

  // Without this the generalization could silently have done nothing: every
  // other case in this file passes whether or not the fallback was ever
  // parameterized, because every other case is recognised by the tables.
  //
  // Both inputs are the same class of unreadable string, and the API produces
  // both: `curl/8.7.1` is a client that sent a header the tables know nothing
  // about, and the empty string is what `user_agent` holds where the client sent
  // no header at all.
  it.each([["curl/8.7.1"], [""]])(
    "falls back to a device-shaped label rather than to a passkey's, for %j",
    (userAgent) => {
      expect(deviceNameForUserAgent(userAgent)).toBe("Unknown device");
      expect(deviceNameForUserAgent(userAgent)).not.toBe("Passkey");
      // And the passkey side is unmoved: the ceremony still files an
      // unrecognised browser's credential under the word for what it is.
      expect(passkeyNameForUserAgent(userAgent)).toBe("Passkey");
    },
  );
});