|
| 1 | +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. |
| 2 | +// This product includes software developed at Datadog (https://www.datadoghq.com/). |
| 3 | +// Copyright 2019-Present Datadog, Inc. |
| 4 | + |
| 5 | +import { discoverBackendFunctions } from '@dd/apps-plugin/backend/discovery'; |
| 6 | +import { getMockLogger, mockLogFn } from '@dd/tests/_jest/helpers/mocks'; |
| 7 | +import fs from 'fs'; |
| 8 | +import path from 'path'; |
| 9 | + |
| 10 | +const log = getMockLogger(); |
| 11 | +const backendDir = '/project/backend'; |
| 12 | + |
| 13 | +const fileStat = { isDirectory: () => false, isFile: () => true }; |
| 14 | +const dirStat = { isDirectory: () => true, isFile: () => false }; |
| 15 | + |
| 16 | +describe('Backend Functions - discoverBackendFunctions', () => { |
| 17 | + let readdirSpy: jest.SpyInstance; |
| 18 | + let statSpy: jest.SpyInstance; |
| 19 | + |
| 20 | + beforeEach(() => { |
| 21 | + readdirSpy = jest.spyOn(fs, 'readdirSync'); |
| 22 | + statSpy = jest.spyOn(fs, 'statSync'); |
| 23 | + }); |
| 24 | + |
| 25 | + afterEach(() => { |
| 26 | + jest.restoreAllMocks(); |
| 27 | + }); |
| 28 | + |
| 29 | + describe('file discovery', () => { |
| 30 | + const cases = [ |
| 31 | + { |
| 32 | + description: 'discover a single .ts file', |
| 33 | + entries: ['handler.ts'], |
| 34 | + stats: { [path.join(backendDir, 'handler.ts')]: fileStat }, |
| 35 | + expected: [{ name: 'handler', entryPath: path.join(backendDir, 'handler.ts') }], |
| 36 | + }, |
| 37 | + { |
| 38 | + description: 'discover a single .js file', |
| 39 | + entries: ['handler.js'], |
| 40 | + stats: { [path.join(backendDir, 'handler.js')]: fileStat }, |
| 41 | + expected: [{ name: 'handler', entryPath: path.join(backendDir, 'handler.js') }], |
| 42 | + }, |
| 43 | + { |
| 44 | + description: 'discover a directory with index.ts', |
| 45 | + entries: ['myFunc'], |
| 46 | + stats: { |
| 47 | + [path.join(backendDir, 'myFunc')]: dirStat, |
| 48 | + [path.join(backendDir, 'myFunc', 'index.ts')]: fileStat, |
| 49 | + }, |
| 50 | + expected: [ |
| 51 | + { |
| 52 | + name: 'myFunc', |
| 53 | + entryPath: path.join(backendDir, 'myFunc', 'index.ts'), |
| 54 | + }, |
| 55 | + ], |
| 56 | + }, |
| 57 | + { |
| 58 | + description: 'discover multiple functions (mix of files and directories)', |
| 59 | + entries: ['handler.ts', 'myFunc'], |
| 60 | + stats: { |
| 61 | + [path.join(backendDir, 'handler.ts')]: fileStat, |
| 62 | + [path.join(backendDir, 'myFunc')]: dirStat, |
| 63 | + [path.join(backendDir, 'myFunc', 'index.ts')]: fileStat, |
| 64 | + }, |
| 65 | + expected: [ |
| 66 | + { name: 'handler', entryPath: path.join(backendDir, 'handler.ts') }, |
| 67 | + { |
| 68 | + name: 'myFunc', |
| 69 | + entryPath: path.join(backendDir, 'myFunc', 'index.ts'), |
| 70 | + }, |
| 71 | + ], |
| 72 | + }, |
| 73 | + { |
| 74 | + description: 'skip non-matching extensions', |
| 75 | + entries: ['config.json', 'styles.css', 'handler.ts'], |
| 76 | + stats: { |
| 77 | + [path.join(backendDir, 'config.json')]: fileStat, |
| 78 | + [path.join(backendDir, 'styles.css')]: fileStat, |
| 79 | + [path.join(backendDir, 'handler.ts')]: fileStat, |
| 80 | + }, |
| 81 | + expected: [{ name: 'handler', entryPath: path.join(backendDir, 'handler.ts') }], |
| 82 | + }, |
| 83 | + { |
| 84 | + description: 'skip directory with no valid index file', |
| 85 | + entries: ['emptyDir'], |
| 86 | + stats: { |
| 87 | + [path.join(backendDir, 'emptyDir')]: dirStat, |
| 88 | + }, |
| 89 | + expected: [], |
| 90 | + }, |
| 91 | + { |
| 92 | + description: 'return empty array for empty directory', |
| 93 | + entries: [], |
| 94 | + stats: {}, |
| 95 | + expected: [], |
| 96 | + }, |
| 97 | + ]; |
| 98 | + |
| 99 | + test.each(cases)('Should $description', ({ entries, stats, expected }) => { |
| 100 | + readdirSpy.mockReturnValue(entries); |
| 101 | + statSpy.mockImplementation((p: string) => { |
| 102 | + const stat = stats[p]; |
| 103 | + if (!stat) { |
| 104 | + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); |
| 105 | + } |
| 106 | + return stat; |
| 107 | + }); |
| 108 | + |
| 109 | + const result = discoverBackendFunctions(backendDir, log); |
| 110 | + expect(result).toEqual(expected); |
| 111 | + }); |
| 112 | + }); |
| 113 | + |
| 114 | + describe('error handling', () => { |
| 115 | + test('Should return empty array and log debug when directory does not exist', () => { |
| 116 | + readdirSpy.mockImplementation(() => { |
| 117 | + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); |
| 118 | + }); |
| 119 | + |
| 120 | + const result = discoverBackendFunctions('/nonexistent', log); |
| 121 | + expect(result).toEqual([]); |
| 122 | + expect(mockLogFn).toHaveBeenCalledWith( |
| 123 | + expect.stringContaining('No backend directory found'), |
| 124 | + 'debug', |
| 125 | + ); |
| 126 | + }); |
| 127 | + |
| 128 | + test('Should rethrow non-ENOENT errors', () => { |
| 129 | + readdirSpy.mockImplementation(() => { |
| 130 | + throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); |
| 131 | + }); |
| 132 | + |
| 133 | + expect(() => discoverBackendFunctions(backendDir, log)).toThrow('EACCES'); |
| 134 | + }); |
| 135 | + }); |
| 136 | + |
| 137 | + describe('extension priority', () => { |
| 138 | + test('Should prefer .ts over .js for directory index', () => { |
| 139 | + readdirSpy.mockReturnValue(['myFunc']); |
| 140 | + statSpy.mockImplementation((p) => { |
| 141 | + if (p === path.join(backendDir, 'myFunc')) { |
| 142 | + return dirStat; |
| 143 | + } |
| 144 | + if (p === path.join(backendDir, 'myFunc', 'index.ts')) { |
| 145 | + return fileStat; |
| 146 | + } |
| 147 | + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); |
| 148 | + }); |
| 149 | + |
| 150 | + const result = discoverBackendFunctions(backendDir, log); |
| 151 | + expect(result).toEqual([ |
| 152 | + { |
| 153 | + name: 'myFunc', |
| 154 | + entryPath: path.join(backendDir, 'myFunc', 'index.ts'), |
| 155 | + }, |
| 156 | + ]); |
| 157 | + }); |
| 158 | + }); |
| 159 | +}); |
0 commit comments