From f9a77c5c27aede4e5978eb55d9b7af781b680a1d Mon Sep 17 00:00:00 2001 From: João Augusto Costa Branco Marado Torres Date: Tue, 24 Jun 2025 12:08:41 -0300 Subject: feat!: initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Augusto Costa Branco Marado Torres --- tests/e2e/user_flow_test.ts | 0 tests/fixtures/setup.ts | 146 ++++++++++++++++++++++++++++++++++++++++++ tests/fixtures/test_data.ts | 63 ++++++++++++++++++ tests/integration/api_test.ts | 0 tests/integration/db_test.ts | 0 5 files changed, 209 insertions(+) create mode 100644 tests/e2e/user_flow_test.ts create mode 100644 tests/fixtures/setup.ts create mode 100644 tests/fixtures/test_data.ts create mode 100644 tests/integration/api_test.ts create mode 100644 tests/integration/db_test.ts (limited to 'tests') diff --git a/tests/e2e/user_flow_test.ts b/tests/e2e/user_flow_test.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts new file mode 100644 index 0000000..0b23ec8 --- /dev/null +++ b/tests/fixtures/setup.ts @@ -0,0 +1,146 @@ +import { + createMessage, + decryptKey, + generateKey, + PrivateKey, + sign, +} from "npm:openpgp@^6.1.1"; +import { passphrase } from "./test_data.ts"; +import { MaybeIterable } from "../../src/utils/iterator.ts"; +import { afterEach, beforeEach } from "@std/testing/bdd"; +import { Stub, stub } from "@std/testing/mock"; + +export async function generateKeyPair( + name: string, + options?: Partial[0]>, +): ReturnType { + const key = await generateKey({ + type: "ecc", + userIDs: [{ + name, + email: `${ + name.toLowerCase().replaceAll(/\s/g, "") + }@localhost.localdomain`, + }], + passphrase, + format: "object", + ...options, + }); + const privateKey = await decryptKey({ + privateKey: key.privateKey, + passphrase, + }); + return { ...key, privateKey }; +} + +export function generateKeyPairWithSubkey( + name: string, +): ReturnType { + return generateKeyPair(name, { + curve: "nistP256", + subkeys: [{ type: "ecc", curve: "nistP256", sign: true }], + }); +} + +export async function createDetachedSignature( + data: Uint8Array, + signingKeys: MaybeIterable, +): Promise> { + const message = await createMessage({ binary: data }); + const signature = await sign({ + message, + signingKeys: Symbol.iterator in signingKeys + ? Iterator.from(signingKeys).toArray() + : signingKeys, + detached: true, + format: "object", + }); + return signature.write() as Uint8Array; +} + +export function corruptData(data: Uint8Array): Uint8Array { + const corrupted = new Uint8Array(data); + if (corrupted.length > 0) { + corrupted[0] += 1; + corrupted[0] %= 1 << 8; + } + return corrupted; +} + +export function corruptSignatureFormat( + signature: Uint8Array, +): Uint8Array { + const corrupted = new Uint8Array(signature); + + if (corrupted.length > 0) { + // Strategy 1: Change the packet tag byte + // The first byte contains the tag and format information. + // Changing the lower 6 bits (new format tag) or higher bits (packet format) + // can easily break parsing. Let's try flipping a bit. + // corrupted[0] = corrupted[0] ^ 0x01; // Flip the last bit + + // Strategy 2 (Alternative - more drastic): Truncate the signature + // return corrupted.slice(0, corrupted.length / 2); // Cut off half the signature + + // Strategy 3 (Alternative - modify length field): + // This is more complex as length encoding varies, but for typical new format + // packets, length information is in the bytes immediately following the tag. + // Modifying these can cause the parser to misinterpret the packet length. + // Example (simplified, might need adjustment based on actual encoding): + if (corrupted.length > 2) { + corrupted[1] = (corrupted[1] + 10) % 256; + } + } + return corrupted; +} + +const inMemoryFiles = new Map< + string, + { text?: string; bytes?: Uint8Array } +>(); + +export function startMockFs(): void { + inMemoryFiles.clear(); + + beforeEach(() => { + Deno.readTextFile = stub( + Deno, + "readTextFile", + async (path: string | URL) => { + const url = new URL(path).href; + const content = inMemoryFiles.get(url)?.text; + if (content === undefined) { + throw new Deno.errors.NotFound(`File not found: ${url}`); + } + return await Promise.resolve(content); + }, + ); + + Deno.readFile = stub(Deno, "readFile", async (path: string | URL) => { + const url = new URL(path).href; + const content = inMemoryFiles.get(url)?.bytes; + if (content === undefined) { + throw new Deno.errors.NotFound(`File not found: ${url}`); + } + return await Promise.resolve(content); + }); + + inMemoryFiles.clear(); + }); + + afterEach(() => { + (Deno.readTextFile as Stub).restore(); + (Deno.readFile as Stub).restore(); + }); +} + +export function createInMemoryFile( + url: URL, + content: string | Uint8Array, +): URL { + inMemoryFiles.set( + url.href, + typeof content === "string" ? { text: content } : { bytes: content }, + ); + return url; +} diff --git a/tests/fixtures/test_data.ts b/tests/fixtures/test_data.ts new file mode 100644 index 0000000..d2143ed --- /dev/null +++ b/tests/fixtures/test_data.ts @@ -0,0 +1,63 @@ +export const TRUE = true; +export const FALSE = false; + +export const passphrase = "Za39PSymj5EcuzMs9kSNsAS3KbfzKHHP"; + +export const gitDir = new URL("file:///home/user/project/"); + +const gitLogPrettyOutput = new TextEncoder().encode([ + "abcdef1234567890abcdef1234567890abcdef12", // long hash + "abcdef1", // short hash + "2024-06-17T12:00:00Z", // author date + "Alice", // author name + "alice@example.com", // author email + "2024-06-17T12:01:00Z", // committer date + "Bob", // committer name + "bob@example.com", // committer email + "bob@example.com", // signer + "ABCDEF", // key (short) + "ABCDEF123456", // key fingerprint (long) + "gpg: Signature made...", // raw line 1 + "gpg: Good signature from...", // raw line 2 +].join("\n")) as Uint8Array; + +const gitDiffTreeOutput = new TextEncoder().encode([ + "M\tfile.ts", + "A\tnew_file.ts", +].join("\n")) as Uint8Array; + +const emptyOutput = new TextEncoder().encode("") as Uint8Array; + +export const gitLogPrettyCommandOutput: Promise = Promise + .resolve({ + stdout: gitLogPrettyOutput, + code: 0, + signal: null, + stderr: emptyOutput, + success: true, + }); +export const gitDiffTreeCommandOutput: Promise = Promise + .resolve({ + stdout: gitDiffTreeOutput, + code: 0, + signal: null, + stderr: emptyOutput, + success: true, + }); +export const gitRevParseCommandOutput: Promise = Promise + .resolve({ + stdout: new TextEncoder().encode(gitDir.pathname) as Uint8Array< + ArrayBuffer + >, + code: 0, + signal: null, + stderr: emptyOutput, + success: true, + }); +export const emptyCommandOutput: Promise = Promise.resolve({ + stdout: emptyOutput, + code: 0, + signal: null, + stderr: emptyOutput, + success: true, +}); diff --git a/tests/integration/api_test.ts b/tests/integration/api_test.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/db_test.ts b/tests/integration/db_test.ts new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3