|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { EJSON, ObjectId } from 'bson'; |
| 7 | + |
| 8 | +// Mock vscode l10n so the localized throw message is deterministic. |
| 9 | +jest.mock('vscode', () => ({ |
| 10 | + l10n: { |
| 11 | + t: (message: string, ...args: unknown[]) => { |
| 12 | + let result = message; |
| 13 | + args.forEach((arg, index) => { |
| 14 | + result = result.replace(`{${index}}`, String(arg)); |
| 15 | + }); |
| 16 | + return result; |
| 17 | + }, |
| 18 | + }, |
| 19 | +})); |
| 20 | + |
| 21 | +// Capture output-channel diagnostics emitted on failure. |
| 22 | +const outputChannelError = jest.fn(); |
| 23 | +jest.mock('../../extensionVariables', () => ({ |
| 24 | + ext: { |
| 25 | + outputChannel: { |
| 26 | + error: (...args: unknown[]) => outputChannelError(...args), |
| 27 | + }, |
| 28 | + }, |
| 29 | +})); |
| 30 | + |
| 31 | +import { parseDocumentId } from './parseDocumentId'; |
| 32 | + |
| 33 | +/** |
| 34 | + * The ids passed to parseDocumentId are produced by EJSON.stringify (canonical |
| 35 | + * for the table grid, relaxed for the document view). Build inputs the same way |
| 36 | + * so the tests exercise the real contract. |
| 37 | + */ |
| 38 | +const canonical = (value: unknown): string => EJSON.stringify(value, { relaxed: false }); |
| 39 | + |
| 40 | +describe('parseDocumentId', () => { |
| 41 | + beforeEach(() => { |
| 42 | + outputChannelError.mockClear(); |
| 43 | + }); |
| 44 | + |
| 45 | + it('parses an ObjectId from its EJSON $oid form (no hex guessing needed)', () => { |
| 46 | + const oid = new ObjectId(); |
| 47 | + // This is exactly how the UI serializes an ObjectId _id: {"$oid":"..."}. |
| 48 | + expect(canonical(oid)).toBe(`{"$oid":"${oid.toHexString()}"}`); |
| 49 | + |
| 50 | + const result = parseDocumentId(canonical(oid)); |
| 51 | + expect(result).toBeInstanceOf(ObjectId); |
| 52 | + expect((result as ObjectId).toHexString()).toBe(oid.toHexString()); |
| 53 | + }); |
| 54 | + |
| 55 | + it('does NOT guess: a bare 24-character hex string (no $oid) throws', () => { |
| 56 | + const hex = 'a'.repeat(24); |
| 57 | + expect(() => parseDocumentId(hex)).toThrow(/Unable to parse the document _id/); |
| 58 | + expect(outputChannelError).toHaveBeenCalledTimes(1); |
| 59 | + }); |
| 60 | + |
| 61 | + it('keeps a string _id as a string', () => { |
| 62 | + expect(parseDocumentId(canonical('my-key'))).toBe('my-key'); |
| 63 | + }); |
| 64 | + |
| 65 | + it('keeps a numeric-looking string _id as a string (not coerced to a number)', () => { |
| 66 | + expect(parseDocumentId(canonical('42'))).toBe('42'); |
| 67 | + }); |
| 68 | + |
| 69 | + it('keeps a 24-hex-char string _id as a string when it arrives as EJSON', () => { |
| 70 | + const hexLikeString = 'f'.repeat(24); |
| 71 | + expect(parseDocumentId(canonical(hexLikeString))).toBe(hexLikeString); |
| 72 | + }); |
| 73 | + |
| 74 | + it('parses a number _id to a number', () => { |
| 75 | + expect(parseDocumentId(canonical(2343345))).toBe(2343345); |
| 76 | + }); |
| 77 | + |
| 78 | + it('keeps a UUID string _id as a string', () => { |
| 79 | + const uuid = '550e8400-e29b-41d4-a716-446655440000'; |
| 80 | + expect(parseDocumentId(canonical(uuid))).toBe(uuid); |
| 81 | + }); |
| 82 | + |
| 83 | + it('parses an embedded-document _id, preserving field order and value types', () => { |
| 84 | + const embedded = { author: 'John', userId: 2343345 }; |
| 85 | + const result = parseDocumentId(canonical(embedded)) as Record<string, unknown>; |
| 86 | + |
| 87 | + expect(result).toEqual(embedded); |
| 88 | + // Field order matters for embedded-document matching in the driver. |
| 89 | + expect(Object.keys(result)).toEqual(['author', 'userId']); |
| 90 | + expect(typeof result.userId).toBe('number'); |
| 91 | + // Round-trips back to the exact canonical EJSON the driver would match on. |
| 92 | + expect(canonical(result)).toBe(canonical(embedded)); |
| 93 | + }); |
| 94 | + |
| 95 | + it('parses an array _id', () => { |
| 96 | + expect(parseDocumentId(canonical([1, 'a']))).toEqual([1, 'a']); |
| 97 | + }); |
| 98 | + |
| 99 | + describe('unparseable ids', () => { |
| 100 | + it.each(['', ' ', 'not json', '{bad json', 'undefined'])( |
| 101 | + 'throws and logs to the output channel for %p', |
| 102 | + (badId) => { |
| 103 | + expect(() => parseDocumentId(badId)).toThrow(/Unable to parse the document _id/); |
| 104 | + expect(outputChannelError).toHaveBeenCalledTimes(1); |
| 105 | + expect(outputChannelError.mock.calls[0][0]).toContain('Unable to parse document _id'); |
| 106 | + }, |
| 107 | + ); |
| 108 | + |
| 109 | + it('echoes the offending value into the diagnostics', () => { |
| 110 | + expect(() => parseDocumentId('not json')).toThrow(); |
| 111 | + expect(outputChannelError.mock.calls[0][0]).toContain('"not json"'); |
| 112 | + }); |
| 113 | + |
| 114 | + it('caps the echoed value for very long ids', () => { |
| 115 | + const longId = 'x'.repeat(5000); // not valid EJSON, not a hex ObjectId |
| 116 | + expect(() => parseDocumentId(longId)).toThrow(); |
| 117 | + |
| 118 | + const logged = String(outputChannelError.mock.calls[0][0]); |
| 119 | + expect(logged).toContain('chars total'); |
| 120 | + expect(logged).toContain('5000'); |
| 121 | + // The raw value must not be dumped in full. |
| 122 | + expect(logged).not.toContain('x'.repeat(300)); |
| 123 | + }); |
| 124 | + }); |
| 125 | +}); |
0 commit comments