You've already forked hugo-extended
mirror of
https://github.com/jakejarvis/hugo-extended.git
synced 2026-06-24 10:25:57 -04:00
test: improved OS-specific coverage (#181)
Co-authored-by: jake <jake@jarv.is> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, lstatSync, readlinkSync, statSync } from "node:fs";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import hugo, { execWithOutput, getHugoBinary } from "../../src/hugo";
|
||||
import {
|
||||
getBinFilename,
|
||||
getBinPath,
|
||||
getPkgVersion,
|
||||
getReleaseFilename,
|
||||
isExtended,
|
||||
} from "../../src/lib/utils";
|
||||
|
||||
/**
|
||||
* End-to-end tests for Hugo installation.
|
||||
*
|
||||
* These tests verify:
|
||||
* - Binary is installed correctly for the current platform
|
||||
* - Binary has correct permissions
|
||||
* - Binary is executable and returns expected version
|
||||
* - Extended version is installed where supported
|
||||
*
|
||||
* Note: These tests use the actual installed Hugo binary and require
|
||||
* npm install/postinstall to have completed successfully.
|
||||
*/
|
||||
describe("Hugo Installation E2E", () => {
|
||||
let binaryPath: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure Hugo is installed
|
||||
binaryPath = await hugo();
|
||||
});
|
||||
|
||||
describe("Binary Installation", () => {
|
||||
it("should have Hugo binary installed", () => {
|
||||
expect(existsSync(binaryPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have binary at expected path", () => {
|
||||
const expectedPath = getBinPath();
|
||||
expect(binaryPath).toBe(expectedPath);
|
||||
});
|
||||
|
||||
it("should have correct binary filename for platform", () => {
|
||||
const expectedFilename = getBinFilename();
|
||||
const actualFilename = binaryPath.split(/[\\/]/).pop();
|
||||
expect(actualFilename).toBe(expectedFilename);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Binary Permissions (Unix)", () => {
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"should have executable permissions",
|
||||
() => {
|
||||
const stats = statSync(binaryPath);
|
||||
// Check that at least owner has execute permission (0o100)
|
||||
const hasExecute = (stats.mode & 0o100) !== 0;
|
||||
expect(hasExecute).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"should be a symlink to /usr/local/bin/hugo on macOS",
|
||||
() => {
|
||||
const isSymlink = lstatSync(binaryPath).isSymbolicLink();
|
||||
expect(isSymlink).toBe(true);
|
||||
|
||||
const target = readlinkSync(binaryPath);
|
||||
expect(target).toBe("/usr/local/bin/hugo");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Binary Execution", () => {
|
||||
it("should execute successfully", () => {
|
||||
const result = execFileSync(binaryPath, ["version"]);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return version string", () => {
|
||||
const result = execFileSync(binaryPath, ["version"]).toString().trim();
|
||||
expect(result).toContain("hugo v");
|
||||
});
|
||||
|
||||
it("should match package version", () => {
|
||||
const pkgVersion = getPkgVersion();
|
||||
const result = execFileSync(binaryPath, ["version"]).toString().trim();
|
||||
expect(result).toContain(`v${pkgVersion}`);
|
||||
});
|
||||
|
||||
it.skipIf(!isExtended(getReleaseFilename(getPkgVersion()) ?? ""))(
|
||||
"should be Extended version where supported",
|
||||
() => {
|
||||
const result = execFileSync(binaryPath, ["version"]).toString().trim();
|
||||
expect(result).toContain("+extended");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("API Integration", () => {
|
||||
it("should work with default export (callable)", async () => {
|
||||
const path = await hugo();
|
||||
expect(path).toBe(binaryPath);
|
||||
});
|
||||
|
||||
it("should work with getHugoBinary()", async () => {
|
||||
const path = await getHugoBinary();
|
||||
expect(path).toBe(binaryPath);
|
||||
});
|
||||
|
||||
it("should work with execWithOutput()", async () => {
|
||||
const { stdout } = await execWithOutput("version");
|
||||
expect(stdout).toContain("hugo v");
|
||||
});
|
||||
|
||||
it("should work with builder API", async () => {
|
||||
// hugo.version() uses exec() which inherits stdio, so we use execWithOutput
|
||||
const { stdout } = await execWithOutput("version");
|
||||
expect(stdout).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Environment Info", () => {
|
||||
it("should report correct GOOS", async () => {
|
||||
const { stdout } = await execWithOutput("env");
|
||||
|
||||
const expectedGoos =
|
||||
process.platform === "win32"
|
||||
? "windows"
|
||||
: process.platform === "darwin"
|
||||
? "darwin"
|
||||
: "linux";
|
||||
|
||||
expect(stdout).toContain(`GOOS="${expectedGoos}"`);
|
||||
});
|
||||
|
||||
it("should report correct GOARCH", async () => {
|
||||
const { stdout } = await execWithOutput("env");
|
||||
|
||||
const expectedGoarch =
|
||||
process.arch === "x64"
|
||||
? "amd64"
|
||||
: process.arch === "arm64"
|
||||
? "arm64"
|
||||
: process.arch;
|
||||
|
||||
expect(stdout).toContain(`GOARCH="${expectedGoarch}"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
import crypto from "node:crypto";
|
||||
import { afterEach, assert, beforeEach, describe, expect, it } from "vitest";
|
||||
import { getArchiveType, parseChecksumFile } from "../../src/lib/install";
|
||||
import { getReleaseFilename } from "../../src/lib/utils";
|
||||
|
||||
/**
|
||||
* Unit tests for installation logic that can be tested without network calls.
|
||||
* These tests verify:
|
||||
* - Checksum file parsing
|
||||
* - Archive type detection
|
||||
* - SHA-256 computation
|
||||
*/
|
||||
describe("Installation Logic", () => {
|
||||
describe("SHA-256 Computation", () => {
|
||||
it("should correctly compute SHA-256 hash", () => {
|
||||
const testData = "Hello, Hugo!";
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(Buffer.from(testData));
|
||||
const digest = hash.digest("hex");
|
||||
|
||||
// Expected SHA-256 hash for "Hello, Hugo!"
|
||||
// Computed via: echo -n "Hello, Hugo!" | sha256sum
|
||||
// Cross-verified by computing with a second method below
|
||||
const expectedHash =
|
||||
"766a2e18bc3e2f7e217b4566b7988ca3a28e1de8cd70d995219088497a0830e5";
|
||||
|
||||
expect(digest).toBe(expectedHash);
|
||||
expect(digest).toHaveLength(64);
|
||||
|
||||
// Cross-verify by computing with a fresh hash instance
|
||||
const verifyHash = crypto.createHash("sha256");
|
||||
verifyHash.update(testData, "utf8");
|
||||
expect(verifyHash.digest("hex")).toBe(expectedHash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseChecksumFile", () => {
|
||||
it("should parse checksums file format correctly", () => {
|
||||
const checksumContent = `
|
||||
abc123def456 hugo_0.154.3_linux-amd64.tar.gz
|
||||
def789abc012 hugo_extended_0.154.3_linux-amd64.tar.gz
|
||||
ghi345jkl678 hugo_0.154.3_windows-amd64.zip
|
||||
`.trim();
|
||||
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
expect(checksums.size).toBe(3);
|
||||
expect(checksums.get("hugo_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"abc123def456",
|
||||
);
|
||||
expect(checksums.get("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"def789abc012",
|
||||
);
|
||||
expect(checksums.get("hugo_0.154.3_windows-amd64.zip")).toBe(
|
||||
"ghi345jkl678",
|
||||
);
|
||||
});
|
||||
|
||||
it("should find correct checksum for a given filename", () => {
|
||||
const checksumContent = `
|
||||
abc123def456 hugo_0.154.3_linux-amd64.tar.gz
|
||||
def789abc012 hugo_extended_0.154.3_linux-amd64.tar.gz
|
||||
ghi345jkl678 hugo_0.154.3_windows-amd64.zip
|
||||
`;
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
expect(checksums.get("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"def789abc012",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return undefined when filename not in checksums", () => {
|
||||
const checksumContent = `
|
||||
abc123def456 hugo_0.154.3_linux-amd64.tar.gz
|
||||
`;
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
expect(checksums.get("hugo_0.154.3_windows-amd64.zip")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle empty content", () => {
|
||||
const checksums = parseChecksumFile("");
|
||||
expect(checksums.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle content with only whitespace lines", () => {
|
||||
const checksums = parseChecksumFile(" \n\n \n");
|
||||
expect(checksums.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle real-world Hugo checksums format", () => {
|
||||
// Real format from Hugo releases uses two spaces between hash and filename
|
||||
const realChecksumContent = `
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 hugo_0.154.3_checksums.txt
|
||||
a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890 hugo_extended_0.154.3_darwin-universal.pkg
|
||||
f0e9d8c7b6a5432109876543210fedcba0987654321fedcba0987654321fedc hugo_extended_0.154.3_linux-amd64.tar.gz
|
||||
`;
|
||||
const checksums = parseChecksumFile(realChecksumContent);
|
||||
|
||||
expect(checksums.size).toBe(3);
|
||||
expect(checksums.get("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
"a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getArchiveType", () => {
|
||||
it("should identify zip files", () => {
|
||||
expect(getArchiveType("hugo_extended_0.154.3_windows-amd64.zip")).toBe(
|
||||
"zip",
|
||||
);
|
||||
});
|
||||
|
||||
it("should identify tar.gz files", () => {
|
||||
expect(getArchiveType("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should identify pkg files", () => {
|
||||
expect(getArchiveType("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
"pkg",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null for unknown extensions", () => {
|
||||
expect(getArchiveType("hugo_0.154.3_readme.txt")).toBeNull();
|
||||
expect(getArchiveType("hugo.exe")).toBeNull();
|
||||
expect(getArchiveType("checksums.txt")).toBeNull();
|
||||
});
|
||||
|
||||
it("should correctly detect archive type for all platform release filenames", () => {
|
||||
// Windows x64 -> zip
|
||||
expect(getArchiveType("hugo_extended_0.154.3_windows-amd64.zip")).toBe(
|
||||
"zip",
|
||||
);
|
||||
|
||||
// Windows arm64 -> zip
|
||||
expect(getArchiveType("hugo_0.154.3_windows-arm64.zip")).toBe("zip");
|
||||
|
||||
// Linux x64 -> tar.gz
|
||||
expect(getArchiveType("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
|
||||
// Linux arm64 -> tar.gz
|
||||
expect(getArchiveType("hugo_extended_0.154.3_linux-arm64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
|
||||
// macOS -> pkg
|
||||
expect(getArchiveType("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
"pkg",
|
||||
);
|
||||
|
||||
// FreeBSD -> tar.gz
|
||||
expect(getArchiveType("hugo_0.154.3_freebsd-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
|
||||
// OpenBSD -> tar.gz
|
||||
expect(getArchiveType("hugo_0.154.3_openbsd-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReleaseFilename + getArchiveType integration", () => {
|
||||
let originalPlatform: NodeJS.Platform;
|
||||
let originalArch: NodeJS.Architecture;
|
||||
|
||||
beforeEach(() => {
|
||||
originalPlatform = process.platform;
|
||||
originalArch = process.arch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
Object.defineProperty(process, "arch", { value: originalArch });
|
||||
});
|
||||
|
||||
it("should return zip for Windows release filenames", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
|
||||
const filename = getReleaseFilename("0.154.3");
|
||||
assert(filename !== null, "Expected Windows x64 to have a release file");
|
||||
expect(getArchiveType(filename)).toBe("zip");
|
||||
});
|
||||
|
||||
it("should return tar.gz for Linux release filenames", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
|
||||
const filename = getReleaseFilename("0.154.3");
|
||||
assert(filename !== null, "Expected Linux x64 to have a release file");
|
||||
expect(getArchiveType(filename)).toBe("tar.gz");
|
||||
});
|
||||
|
||||
it("should return pkg for macOS release filenames", () => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
|
||||
const filename = getReleaseFilename("0.154.3");
|
||||
assert(filename !== null, "Expected macOS arm64 to have a release file");
|
||||
expect(getArchiveType(filename)).toBe("pkg");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
getBinFilename,
|
||||
getChecksumFilename,
|
||||
getReleaseFilename,
|
||||
getReleaseUrl,
|
||||
isExtended,
|
||||
} from "../../src/lib/utils";
|
||||
|
||||
describe("utils", () => {
|
||||
describe("getBinFilename", () => {
|
||||
let originalPlatform: NodeJS.Platform;
|
||||
|
||||
beforeEach(() => {
|
||||
originalPlatform = process.platform;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
});
|
||||
|
||||
it("should return hugo.exe on Windows", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
expect(getBinFilename()).toBe("hugo.exe");
|
||||
});
|
||||
|
||||
it("should return hugo on Linux", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
expect(getBinFilename()).toBe("hugo");
|
||||
});
|
||||
|
||||
it("should return hugo on macOS", () => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
expect(getBinFilename()).toBe("hugo");
|
||||
});
|
||||
|
||||
it("should return hugo on other platforms", () => {
|
||||
Object.defineProperty(process, "platform", { value: "freebsd" });
|
||||
expect(getBinFilename()).toBe("hugo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReleaseFilename", () => {
|
||||
let originalPlatform: NodeJS.Platform;
|
||||
let originalArch: NodeJS.Architecture;
|
||||
|
||||
beforeEach(() => {
|
||||
originalPlatform = process.platform;
|
||||
originalArch = process.arch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
Object.defineProperty(process, "arch", { value: originalArch });
|
||||
});
|
||||
|
||||
describe("macOS", () => {
|
||||
it("should return universal pkg for darwin x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_darwin-universal.pkg",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return universal pkg for darwin arm64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_darwin-universal.pkg",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Windows", () => {
|
||||
it("should return extended zip for win32 x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_windows-amd64.zip",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return vanilla zip for win32 arm64 (no extended support)", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_0.154.3_windows-arm64.zip",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Linux", () => {
|
||||
it("should return extended tar.gz for linux x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_linux-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return extended tar.gz for linux arm64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_linux-arm64.tar.gz",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BSD", () => {
|
||||
it("should return vanilla tar.gz for freebsd x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "freebsd" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_0.154.3_freebsd-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return vanilla tar.gz for openbsd x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "openbsd" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_0.154.3_openbsd-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported platforms", () => {
|
||||
it("should return null for unsupported platform", () => {
|
||||
Object.defineProperty(process, "platform", { value: "sunos" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unsupported arch on linux", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "ia32" });
|
||||
expect(getReleaseFilename("0.154.3")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unsupported arch on freebsd", () => {
|
||||
Object.defineProperty(process, "platform", { value: "freebsd" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getChecksumFilename", () => {
|
||||
it("should return correct checksum filename", () => {
|
||||
expect(getChecksumFilename("0.154.3")).toBe("hugo_0.154.3_checksums.txt");
|
||||
});
|
||||
|
||||
it("should handle different version formats", () => {
|
||||
expect(getChecksumFilename("1.0.0")).toBe("hugo_1.0.0_checksums.txt");
|
||||
expect(getChecksumFilename("0.100.0")).toBe("hugo_0.100.0_checksums.txt");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isExtended", () => {
|
||||
it("should return true for extended releases", () => {
|
||||
expect(isExtended("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(true);
|
||||
expect(isExtended("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isExtended("hugo_extended_0.154.3_windows-amd64.zip")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for vanilla releases", () => {
|
||||
expect(isExtended("hugo_0.154.3_windows-arm64.zip")).toBe(false);
|
||||
expect(isExtended("hugo_0.154.3_freebsd-amd64.tar.gz")).toBe(false);
|
||||
expect(isExtended("hugo_0.154.3_openbsd-amd64.tar.gz")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReleaseUrl", () => {
|
||||
it("should return correct GitHub release URL", () => {
|
||||
expect(
|
||||
getReleaseUrl("0.154.3", "hugo_extended_0.154.3_linux-amd64.tar.gz"),
|
||||
).toBe(
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.154.3/hugo_extended_0.154.3_linux-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should work with checksum files", () => {
|
||||
expect(getReleaseUrl("0.154.3", "hugo_0.154.3_checksums.txt")).toBe(
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.154.3/hugo_0.154.3_checksums.txt",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user