UNPKG

@imigueldiaz/mongodb-labeler

Version:

A MongoDB-based labeling system for content moderation with cryptographic signing

381 lines (380 loc) 15 kB
// Mock @atproto/syntax before imports import { vi } from 'vitest'; vi.mock("@atproto/syntax", () => ({ AtUri: vi.fn().mockImplementation((uri) => { if (uri === "at://mock/error") { throw new Error("Mock AtUri error"); } return {}; }), })); import { validateCid, validateDid, validateVal, validateCts, validateExp, validateUri, validateAtUri } from "../util/validators"; import { AtProtocolValidationError } from "../errors"; import { describe, it, expect, beforeAll, afterAll } from 'vitest'; describe("DID Validation", () => { // Test valid DIDs it("should accept valid DIDs", () => { const validDids = [ "did:plc:user123", "did:web:example.com", "did:key:z6Mkf5rGMQm3xqMzLAMQm3xqMzLAM", "did:method:specific:with:many:colons", "did:test:abc123.456-789", ]; validDids.forEach(did => { expect(() => validateDid(did)).not.toThrow(); }); }); // Test empty DID it("should throw error for empty DID", () => { expect(() => validateDid("")).toThrow(AtProtocolValidationError); expect(() => validateDid("")).toThrow("DID cannot be empty"); }); // Test DID prefix it("should throw error for DIDs not starting with 'did:'", () => { const invalidPrefixes = [ "plc:user123", "did-plc:user123", "Did:plc:user123", ":plc:user123", ]; invalidPrefixes.forEach(did => { expect(() => validateDid(did)).toThrow(AtProtocolValidationError); expect(() => validateDid(did)).toThrow("DID must start with \"did:\""); }); }); // Test DID parts it("should throw error for DIDs with insufficient parts", () => { const insufficientParts = [ "did:", "did:plc", "did:", ]; insufficientParts.forEach(did => { expect(() => validateDid(did)).toThrow(AtProtocolValidationError); expect(() => validateDid(did)).toThrow("DID must have at least three parts"); }); }); // Test method validation it("should throw error for DIDs with invalid method", () => { const invalidMethods = [ "did:PLC:user123", "did:123:user123", "did:plc-method:user123", "did:a:user123", // too short "did:methodthatiswaytoolongforvalidation:user123", // too long ]; invalidMethods.forEach(did => { expect(() => validateDid(did)).toThrow(AtProtocolValidationError); }); }); // Test specific-id validation it("should throw error for DIDs with invalid specific-id", () => { const invalidSpecificIds = [ "did:plc:", "did:plc:user 123", "did:plc:user<123>", "did:plc:user{123}", "did:plc:user[123]", "did:plc:user\\123", "did:plc:user|123", "did:plc:user^123", "did:plc:user`123", "did:plc:" + "x".repeat(513), // too long ]; invalidSpecificIds.forEach(did => { expect(() => validateDid(did)).toThrow(AtProtocolValidationError); }); }); }); describe("CID Validation", () => { // Ejemplos reales de CIDs it("should accept valid CIDs", () => { const validCids = [ // CIDv0 - Un hash real de IPFS "QmPK1s3pNYLi9ERiq3BDxKa4XosgWwFRQUydHUtz4YgpqB", // CIDv1 - Otro hash real de IPFS "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" ]; validCids.forEach(cid => { expect(() => validateCid(cid)).not.toThrow(); }); }); it("should reject invalid CIDs", () => { const invalidCids = [ "", // vacío "NotACid", // formato completamente inválido "Qm123", // CIDv0 inválido "bafybei", // CIDv1 demasiado corto "kafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" // prefijo incorrecto ]; invalidCids.forEach(cid => { expect(() => validateCid(cid)).toThrow(AtProtocolValidationError); }); }); }); describe("URI Validation", () => { it("should accept valid URIs", () => { const validUris = [ "did:plc:user123", "did:web:example.com", "at://user.bsky.social/com.atproto.feed.post/123", "at://did:plc:user123/com.atproto.feed.post/123" ]; validUris.forEach(uri => { expect(() => validateUri(uri)).not.toThrow(); }); }); it("should throw error for invalid URIs", () => { const invalidUris = [ "", "http://example.com", "atp://invalid", "did:invalid", "at:invalid", "something:else" ]; invalidUris.forEach(uri => { expect(() => validateUri(uri)).toThrow(AtProtocolValidationError); }); }); it("should throw specific error for non did: or at:// URIs", () => { expect(() => validateUri("http://example.com")) .toThrow("URI must start with either \"did:\" or \"at://\""); }); }); /** @deprecated */ describe("validateAtUri (Deprecated)", () => { // Test invalid AT Protocol URIs it("should throw AtProtocolValidationError for URIs with invalid protocol", () => { const invalidProtocols = [ "http://example.com", // wrong protocol "atp://invalid", // wrong protocol "at:invalid", // missing double slash ]; invalidProtocols.forEach(uri => { expect(() => { validateAtUri(uri); }).toThrow(AtProtocolValidationError); expect(() => { validateAtUri(uri); }).toThrow("must start with \"at://\""); }); }); it("should throw AtProtocolValidationError when AtUri constructor fails", () => { expect(() => { validateAtUri("at://mock/error"); }).toThrow(AtProtocolValidationError); expect(() => { validateAtUri("at://mock/error"); }).toThrow("Invalid AT Protocol URI"); }); // Test edge cases it("should handle edge cases", () => { expect(() => validateAtUri("")).toThrow(AtProtocolValidationError); expect(() => validateAtUri("")).toThrow("URI cannot be null or empty"); expect(() => validateAtUri(null)).toThrow(AtProtocolValidationError); expect(() => validateAtUri(null)).toThrow("URI cannot be null or empty"); expect(() => validateAtUri(undefined)).toThrow(AtProtocolValidationError); expect(() => validateAtUri(undefined)).toThrow("URI cannot be null or empty"); }); }); describe("Label Value Validation", () => { // Test valid label values it("should accept valid label values", () => { const validValues = [ "spam", "adult", "!warn", "!takedown", "scam", "dmca", "abuse", "reasonablelengthvalue123", ]; validValues.forEach(val => { expect(() => validateVal(val)).not.toThrow(); }); }); // Test empty value it("should throw error for empty value", () => { expect(() => validateVal("")).toThrow(AtProtocolValidationError); expect(() => validateVal("")).toThrow("Label value cannot be empty"); }); // Test byte length it("should throw error for values exceeding 128 bytes", () => { const longValue = "x".repeat(129); expect(() => validateVal(longValue)).toThrow(AtProtocolValidationError); expect(() => validateVal(longValue)).toThrow("Label value cannot exceed 128 bytes"); }); // Test whitespace it("should throw error for values containing whitespace", () => { const valuesWithSpace = [ "label with space", "label\twith\ttab", "label\nwith\nnewline", " leadingspace", "trailingspace ", ]; valuesWithSpace.forEach(val => { expect(() => validateVal(val)).toThrow(AtProtocolValidationError); expect(() => validateVal(val)).toThrow("Label value cannot contain whitespace"); }); }); // Test non-ASCII characters it("should throw error for values with non-ASCII characters", () => { const nonAsciiValues = [ "label💡", "étiquette", "标签", "ラベル", ]; nonAsciiValues.forEach(val => { expect(() => validateVal(val)).toThrow(AtProtocolValidationError); expect(() => validateVal(val)).toThrow("Label value must only contain ASCII characters"); }); }); // Test punctuation it("should throw error for invalid punctuation", () => { const invalidPunctuation = [ "label.with.dots", "label,with,commas", "label:with:colons", "label;with;semicolons", "label#with#hash", "label_with_underscore", "label'with'quotes", "label>with>gt", "label<with<lt", "label\\with\\backslash", "label|with|pipe", "label^with^caret", "!label.with.dots", "!label,with,commas", ]; invalidPunctuation.forEach(val => { expect(() => validateVal(val)).toThrow(AtProtocolValidationError); expect(() => validateVal(val)).toThrow("Label value contains invalid punctuation characters"); }); }); // Test system labels it("should handle system labels correctly", () => { // Valid system labels const validSystemLabels = [ "!warn", "!takedown", "!suspend", ]; validSystemLabels.forEach(val => { expect(() => validateVal(val)).not.toThrow(); }); // Invalid system labels (! not at start) const invalidSystemLabels = [ "warn!", "take!down", "sus!pend", ]; invalidSystemLabels.forEach(val => { expect(() => validateVal(val)).toThrow(AtProtocolValidationError); expect(() => validateVal(val)).toThrow("Label value contains invalid punctuation characters"); }); }); }); describe("Timestamp Validation", () => { const mockNow = new Date("2023-12-25T12:00:00Z"); beforeAll(() => { // Mock Date.now() and new Date() using vi vi.useFakeTimers(); vi.setSystemTime(mockNow); }); afterAll(() => { vi.useRealTimers(); }); describe("Creation Timestamp (cts)", () => { it("should accept valid creation timestamps", () => { const validTimestamps = [ "2023-12-25T11:59:59Z", // Just before now "2023-12-25T11:59:59.999Z", // With milliseconds "2023-12-25T12:00:00+00:00", // Alternative timezone format "2023-12-25T13:00:00+01:00", // Different timezone "2023-12-25T07:00:00-05:00", // Different timezone ]; validTimestamps.forEach(timestamp => { expect(() => validateCts(timestamp)).not.toThrow(); }); }); it("should reject invalid creation timestamps", () => { const invalidTimestamps = [ "", // Empty string "not-a-date", // Invalid format "2023-13-25T12:00:00Z", // Invalid month "2023-12-32T12:00:00Z", // Invalid day "2023-12-25T24:00:00Z", // Invalid hour "2023-12-25T12:60:00Z", // Invalid minute "2023-12-25T12:00:60Z", // Invalid second "2023-12-25 12:00:00", // Missing T and timezone "2023-12-25", // Missing time part "12:00:00Z", // Missing date part ]; invalidTimestamps.forEach(timestamp => { expect(() => validateCts(timestamp)).toThrow(AtProtocolValidationError); }); }); it("should reject future creation timestamps", () => { const futureTimestamps = [ "2023-12-25T12:00:01Z", // 1 second in future "2023-12-25T13:00:00Z", // 1 hour in future "2023-12-26T12:00:00Z", // 1 day in future "2024-12-25T12:00:00Z", // 1 year in future ]; futureTimestamps.forEach(timestamp => { expect(() => validateCts(timestamp)).toThrow(AtProtocolValidationError); expect(() => validateCts(timestamp)).toThrow("Creation timestamp cannot be in the future"); }); }); }); describe("Expiration Timestamp (exp)", () => { it("should accept valid expiration timestamps", () => { const validTimestamps = [ "2024-12-25T12:00:00Z", // Future date "2024-12-25T12:00:00.000Z", // With milliseconds "2024-12-25T12:00:00+00:00", // Alternative timezone format "2024-12-25T13:00:00+01:00", // Different timezone "2024-12-25T07:00:00-05:00", // Different timezone ]; validTimestamps.forEach(timestamp => { expect(() => validateExp(timestamp)).not.toThrow(); }); }); it("should reject invalid expiration timestamps", () => { const invalidTimestamps = [ "", // Empty string "not-a-date", // Invalid format "2023-13-25T12:00:00Z", // Invalid month "2023-12-32T12:00:00Z", // Invalid day "2023-12-25T24:00:00Z", // Invalid hour "2023-12-25T12:60:00Z", // Invalid minute "2023-12-25T12:00:60Z", // Invalid second "2023-12-25 12:00:00", // Missing T and timezone "2023-12-25", // Missing time part "12:00:00Z", // Missing date part ]; invalidTimestamps.forEach(timestamp => { expect(() => validateExp(timestamp)).toThrow(); }); }); it("should reject past or present expiration timestamps", () => { const pastTimestamps = [ "2023-12-25T11:59:59Z", // Past "2023-12-25T12:00:00Z", // Present ]; pastTimestamps.forEach(timestamp => { expect(() => validateExp(timestamp)).toThrow('Expiration timestamp must be in the future'); }); // Should accept past timestamps when allowExpired is true pastTimestamps.forEach(timestamp => { expect(() => validateExp(timestamp, true)).not.toThrow(); }); }); }); });