Spaces:
Running
Running
File size: 6,275 Bytes
6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e 6dcf170 6c3af4e | 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// `existsSync` is mocked so that if the read-from-file path ever comes back,
// this file fails on the assertion below rather than on a missing mock.
const mockExistsSync = vi.fn();
const mockReadFileSync = vi.fn();
const mockWriteFileSync = vi.fn();
const mockChmodSync = vi.fn();
vi.mock("node:fs", () => ({
default: {
existsSync: (...args: unknown[]) => mockExistsSync(...args),
readFileSync: (...args: unknown[]) => mockReadFileSync(...args),
writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args),
chmodSync: (...args: unknown[]) => mockChmodSync(...args),
},
existsSync: (...args: unknown[]) => mockExistsSync(...args),
readFileSync: (...args: unknown[]) => mockReadFileSync(...args),
writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args),
chmodSync: (...args: unknown[]) => mockChmodSync(...args),
}));
vi.mock("temp-dir", () => ({ default: "/tmp" }));
beforeEach(() => {
// The module holds the token for the life of the process, so every test
// needs its own copy of it.
vi.resetModules();
vi.clearAllMocks();
mockExistsSync.mockReset();
mockReadFileSync.mockReset();
mockWriteFileSync.mockReset();
mockChmodSync.mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("searchToken", () => {
it("should generate a fresh token instead of adopting one already in the file", async () => {
// The published image used to ship a token in the temp directory, and a
// container that read it shared one token with every other container built
// from that image, and with anyone who pulled the image and read the layer.
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue("token-baked-into-the-image");
const { getSearchToken } = await import("./searchToken");
const token = getSearchToken();
expect(token).not.toBe("token-baked-into-the-image");
expect(token).toMatch(/^[0-9a-f]{64}$/);
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
expect(token).toBe(mockWriteFileSync.mock.calls[0][1]);
});
it("should still serve a usable token when the file cannot be written", async () => {
// `getSearchToken()` runs inside the argon2 try in
// `verifyTokenAndRateLimit.ts`, so a throw out of the token module would
// read as a bad token and 401 every request with nothing in the log.
const accessError = Object.assign(new Error("EACCES: permission denied"), {
code: "EACCES",
});
mockWriteFileSync.mockImplementation(() => {
throw accessError;
});
const { getSearchToken } = await import("./searchToken");
const token = getSearchToken();
expect(token).toMatch(/^[0-9a-f]{64}$/);
expect(getSearchToken()).toBe(token);
});
it("should keep the token it generated when the file changes", async () => {
const { getSearchToken } = await import("./searchToken");
const firstToken = getSearchToken();
// Another process rewriting the file must not re-key this one: the clients
// already holding the old token have no way of being told about a new one.
mockReadFileSync.mockReturnValue("token-from-another-process");
expect(getSearchToken()).toBe(firstToken);
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
});
it("should serve the new token after regenerating it", async () => {
const { getSearchToken, regenerateSearchToken } = await import(
"./searchToken"
);
const firstToken = getSearchToken();
regenerateSearchToken();
expect(getSearchToken()).not.toBe(firstToken);
expect(getSearchToken()).toBe(
mockWriteFileSync.mock.calls[mockWriteFileSync.mock.calls.length - 1][1],
);
});
it("regenerateSearchToken should draw the token from a cryptographic source", async () => {
mockWriteFileSync.mockReturnValue(undefined);
const mathRandomSpy = vi.spyOn(Math, "random");
const { regenerateSearchToken } = await import("./searchToken");
regenerateSearchToken();
regenerateSearchToken();
const [firstToken, secondToken] = mockWriteFileSync.mock.calls.map(
(call) => call[1] as string,
);
expect(mathRandomSpy).not.toHaveBeenCalled();
// 32 random bytes, hex-encoded
expect(firstToken).toMatch(/^[0-9a-f]{64}$/);
expect(firstToken).not.toBe(secondToken);
});
it("regenerateSearchToken should restrict the token file to its owner", async () => {
mockWriteFileSync.mockReturnValue(undefined);
mockChmodSync.mockReturnValue(undefined);
const { regenerateSearchToken } = await import("./searchToken");
regenerateSearchToken();
const [filePath, , options] = mockWriteFileSync.mock.calls[0];
expect(options).toEqual({ mode: 0o600 });
// A file left behind by an earlier build keeps its old permissions,
// which `mode` alone would not correct.
expect(mockChmodSync).toHaveBeenCalledWith(filePath, 0o600);
});
});
describe("hasSearchTokenFileChanged", () => {
it("should be false before a token has been read", async () => {
const { hasSearchTokenFileChanged } = await import("./searchToken");
expect(hasSearchTokenFileChanged()).toBe(false);
expect(mockReadFileSync).not.toHaveBeenCalled();
});
it("should be false while the file still holds this process's token", async () => {
const { getSearchToken, hasSearchTokenFileChanged } = await import(
"./searchToken"
);
const token = getSearchToken();
mockReadFileSync.mockReturnValue(token);
expect(hasSearchTokenFileChanged()).toBe(false);
});
it("should be true once another process rewrites the file", async () => {
const { getSearchToken, hasSearchTokenFileChanged } = await import(
"./searchToken"
);
getSearchToken();
mockReadFileSync.mockReturnValue("token-from-another-process");
expect(hasSearchTokenFileChanged()).toBe(true);
});
it("should be true when the file is gone", async () => {
const { getSearchToken, hasSearchTokenFileChanged } = await import(
"./searchToken"
);
getSearchToken();
mockReadFileSync.mockImplementation(() => {
throw new Error("ENOENT");
});
expect(hasSearchTokenFileChanged()).toBe(true);
});
});
|