mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
test(ai): add unit tests for attachmentService, syncMetadataStore, s3Service, dmSignalEvents (agent)
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
7c33ecf6e4
commit
959082d327
4 files changed
+513
No files matched your search
@@ -0,0 +1,166 @@
|
||||
import { test, describe, before, after, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import * as attachmentService from "../../js/services/attachmentService.js";
|
||||
|
||||
// Mock URL.createObjectURL and revokeObjectURL
|
||||
const originalURL = globalThis.URL;
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
describe("attachmentService", () => {
|
||||
before(() => {
|
||||
if (!globalThis.URL) {
|
||||
globalThis.URL = {};
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.URL.createObjectURL = mock.fn(() => "blob:mock-url");
|
||||
globalThis.URL.revokeObjectURL = mock.fn();
|
||||
globalThis.fetch = mock.fn();
|
||||
|
||||
// Clear cache before each test to ensure isolation,
|
||||
// but we need to be careful not to trigger revokeObjectURL calls that we count?
|
||||
// Actually, we should just clear it.
|
||||
attachmentService.clearAttachmentCache();
|
||||
// Then reset calls
|
||||
globalThis.URL.revokeObjectURL.mock.resetCalls();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (originalURL) {
|
||||
globalThis.URL = originalURL;
|
||||
} else {
|
||||
delete globalThis.URL;
|
||||
}
|
||||
if (originalFetch) {
|
||||
globalThis.fetch = originalFetch;
|
||||
} else {
|
||||
delete globalThis.fetch;
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
attachmentService.clearAttachmentCache();
|
||||
});
|
||||
|
||||
describe("prepareAttachmentUpload", () => {
|
||||
test("should prepare upload without encryption", async () => {
|
||||
const file = new File(["test content"], "test.txt", { type: "text/plain" });
|
||||
const result = await attachmentService.prepareAttachmentUpload({ file, encrypt: false });
|
||||
|
||||
assert.strictEqual(result.name, "test.txt");
|
||||
assert.strictEqual(result.type, "text/plain");
|
||||
assert.strictEqual(result.size, 12);
|
||||
assert.strictEqual(result.encrypted, false);
|
||||
assert.strictEqual(result.key, "");
|
||||
// sha256 of "test content"
|
||||
assert.strictEqual(result.sha256, "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72");
|
||||
});
|
||||
|
||||
test("should prepare upload with encryption", async () => {
|
||||
const file = new File(["test content"], "test.txt", { type: "text/plain" });
|
||||
const result = await attachmentService.prepareAttachmentUpload({ file, encrypt: true });
|
||||
|
||||
assert.strictEqual(result.name, "test.txt");
|
||||
assert.strictEqual(result.type, "text/plain"); // Metadata preserves original type
|
||||
assert.strictEqual(result.encrypted, true);
|
||||
assert.ok(result.key.length > 0, "Should return a key");
|
||||
assert.notStrictEqual(result.uploadBlob, file, "Should return a new blob");
|
||||
assert.strictEqual(result.uploadBlob.type, "application/octet-stream"); // Actual blob is generic binary
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadAttachment", () => {
|
||||
test("should download attachment", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const result = await attachmentService.downloadAttachment({ url: "https://example.com/file" });
|
||||
|
||||
assert.strictEqual(globalThis.fetch.mock.callCount(), 1);
|
||||
assert.strictEqual(result.objectUrl, "blob:mock-url");
|
||||
assert.strictEqual(result.size, 12);
|
||||
});
|
||||
|
||||
test("should verify hash", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const expectedHash = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72";
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file", expectedHash });
|
||||
|
||||
// Should not throw
|
||||
});
|
||||
|
||||
test("should throw on hash mismatch", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const expectedHash = "wronghash";
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file", expectedHash });
|
||||
},
|
||||
/Attachment hash mismatch/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("caching", () => {
|
||||
test("should cache downloaded attachments", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
|
||||
const expectedHash = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72";
|
||||
|
||||
// First download
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file1", expectedHash });
|
||||
assert.strictEqual(globalThis.fetch.mock.callCount(), 1);
|
||||
|
||||
// Second download (should be cached)
|
||||
const result = await attachmentService.downloadAttachment({ url: "https://example.com/file1", expectedHash });
|
||||
// Fetch should NOT be called again
|
||||
assert.strictEqual(globalThis.fetch.mock.callCount(), 1);
|
||||
assert.strictEqual(result.cached, true);
|
||||
});
|
||||
|
||||
test("should clear cache", async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
blob: async () => new Blob(["test content"]),
|
||||
headers: new Map(),
|
||||
};
|
||||
globalThis.fetch.mock.mockImplementation(async () => mockResponse);
|
||||
const expectedHash = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72";
|
||||
|
||||
await attachmentService.downloadAttachment({ url: "https://example.com/file", expectedHash });
|
||||
assert.strictEqual(attachmentService.getAttachmentCacheStats().size, 1);
|
||||
|
||||
// Clear call count for revokeObjectURL before clearing cache to isolate the count
|
||||
globalThis.URL.revokeObjectURL.mock.resetCalls();
|
||||
|
||||
attachmentService.clearAttachmentCache();
|
||||
assert.strictEqual(attachmentService.getAttachmentCacheStats().size, 0);
|
||||
assert.strictEqual(globalThis.URL.revokeObjectURL.mock.callCount(), 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { test, describe, afterEach } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { createUiDom } from "../ui/helpers/jsdom-test-helpers.mjs";
|
||||
|
||||
// Disable network imports to prevent S3 SDK loading
|
||||
globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__ = true;
|
||||
|
||||
const s3Service = await import("../../js/services/s3Service.js");
|
||||
|
||||
describe("s3Service", () => {
|
||||
describe("validateS3Connection", () => {
|
||||
test("should validate correct configuration", () => {
|
||||
const config = {
|
||||
endpoint: "https://s3.example.com",
|
||||
region: "us-east-1",
|
||||
accessKeyId: "key",
|
||||
secretAccessKey: "secret",
|
||||
bucket: "my-bucket",
|
||||
forcePathStyle: true,
|
||||
};
|
||||
|
||||
const result = s3Service.validateS3Connection(config);
|
||||
assert.strictEqual(result.endpoint, "https://s3.example.com");
|
||||
assert.strictEqual(result.bucket, "my-bucket");
|
||||
assert.ok(result.publicBaseUrl.includes("my-bucket"));
|
||||
});
|
||||
|
||||
test("should throw on missing required fields", () => {
|
||||
assert.throws(() => s3Service.validateS3Connection({}), /S3 endpoint is required/);
|
||||
// Region defaults to auto, so next check is Access Key ID
|
||||
assert.throws(() => s3Service.validateS3Connection({ endpoint: "x" }), /S3 access key ID is required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCorsOrigins", () => {
|
||||
let uiDom;
|
||||
const originalWindow = global.window;
|
||||
|
||||
afterEach(() => {
|
||||
if (uiDom) {
|
||||
uiDom.cleanup();
|
||||
uiDom = null;
|
||||
}
|
||||
global.window = originalWindow;
|
||||
});
|
||||
|
||||
test("should return current origin", () => {
|
||||
uiDom = createUiDom({ url: "https://app.bitvid.invalid/" });
|
||||
global.window = uiDom.window;
|
||||
|
||||
const origins = s3Service.getCorsOrigins();
|
||||
assert.ok(origins.includes("https://app.bitvid.invalid"));
|
||||
});
|
||||
|
||||
test("should handle localhost", () => {
|
||||
uiDom = createUiDom({ url: "http://localhost:3000/" });
|
||||
global.window = uiDom.window;
|
||||
|
||||
const origins = s3Service.getCorsOrigins();
|
||||
assert.ok(origins.includes("http://localhost:3000"));
|
||||
assert.ok(origins.includes("https://localhost:3000")); // Should add https version
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user