aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Utility/feedToken.test.ts
blob: ffcb5fd7ee86494be9e83165be75d953b067eb91 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { describe, expect, it } from "vitest";
import { decryptFeedToken, encryptFeedToken } from "./feedToken";

// Fixed 32-byte keys so the cipher is exercised without the FEED_TOKEN_KEY env
// var (production omits the key argument and reads it from env).
const key = new Uint8Array(32).fill(7);
const otherKey = new Uint8Array(32).fill(9);

describe("feed token", () => {
	// Behaviour gate: the feed must still resolve to the same refresh token it
	// was minted from, and the cleartext must never appear in the URL blob.
	it("round-trips the refresh token without leaking it", async () => {
		const refreshToken = "anilist-refresh-token-abc123";
		const sealed = await encryptFeedToken(refreshToken, key);

		expect(sealed).not.toContain(refreshToken);
		expect(await decryptFeedToken(sealed, key)).toBe(refreshToken);
	});

	it("produces a fresh ciphertext each time (random IV)", async () => {
		expect(await encryptFeedToken("same-token", key)).not.toBe(
			await encryptFeedToken("same-token", key),
		);
	});

	it("rejects a token sealed with a different key", async () => {
		const sealed = await encryptFeedToken("secret", otherKey);

		expect(await decryptFeedToken(sealed, key)).toBeNull();
	});

	it("rejects a tampered ciphertext", async () => {
		const sealed = await encryptFeedToken("secret", key);
		const tampered = `${sealed[0] === "A" ? "B" : "A"}${sealed.slice(1)}`;

		expect(await decryptFeedToken(tampered, key)).toBeNull();
	});

	it("rejects malformed input", async () => {
		expect(await decryptFeedToken("", key)).toBeNull();
		expect(await decryptFeedToken("short", key)).toBeNull();
		expect(await decryptFeedToken("not valid base64url!!", key)).toBeNull();
	});
});