pgGraphqlSchemaComment.test.ts313 lines · main
| 1 | import { describe, expect, it } from 'vitest' |
| 2 | |
| 3 | import { |
| 4 | buildSchemaCommentWith, |
| 5 | isIntrospectionEnabled, |
| 6 | isPgGraphqlIntrospectionOptIn, |
| 7 | parseSchemaComment, |
| 8 | } from './pgGraphqlSchemaComment' |
| 9 | |
| 10 | describe('parseSchemaComment', () => { |
| 11 | it('returns no directive for null', () => { |
| 12 | expect(parseSchemaComment(null)).toEqual({ |
| 13 | options: {}, |
| 14 | hasDirective: false, |
| 15 | isMalformed: false, |
| 16 | prefix: '', |
| 17 | suffix: '', |
| 18 | }) |
| 19 | }) |
| 20 | |
| 21 | it('returns no directive for undefined', () => { |
| 22 | expect(parseSchemaComment(undefined)).toEqual({ |
| 23 | options: {}, |
| 24 | hasDirective: false, |
| 25 | isMalformed: false, |
| 26 | prefix: '', |
| 27 | suffix: '', |
| 28 | }) |
| 29 | }) |
| 30 | |
| 31 | it('returns no directive for empty string', () => { |
| 32 | expect(parseSchemaComment('')).toEqual({ |
| 33 | options: {}, |
| 34 | hasDirective: false, |
| 35 | isMalformed: false, |
| 36 | prefix: '', |
| 37 | suffix: '', |
| 38 | }) |
| 39 | }) |
| 40 | |
| 41 | it('parses a directive with no options', () => { |
| 42 | expect(parseSchemaComment('@graphql({})')).toEqual({ |
| 43 | options: {}, |
| 44 | hasDirective: true, |
| 45 | isMalformed: false, |
| 46 | prefix: '', |
| 47 | suffix: '', |
| 48 | }) |
| 49 | }) |
| 50 | |
| 51 | it('parses a directive with introspection: true', () => { |
| 52 | expect(parseSchemaComment('@graphql({"introspection": true})')).toMatchObject({ |
| 53 | options: { introspection: true }, |
| 54 | hasDirective: true, |
| 55 | isMalformed: false, |
| 56 | }) |
| 57 | }) |
| 58 | |
| 59 | it('parses a directive with introspection: false', () => { |
| 60 | expect(parseSchemaComment('@graphql({"introspection": false})')).toMatchObject({ |
| 61 | options: { introspection: false }, |
| 62 | hasDirective: true, |
| 63 | isMalformed: false, |
| 64 | }) |
| 65 | }) |
| 66 | |
| 67 | it('parses multiple option keys', () => { |
| 68 | expect( |
| 69 | parseSchemaComment( |
| 70 | '@graphql({"introspection": true, "inflect_names": true, "max_rows": 100})' |
| 71 | ) |
| 72 | ).toMatchObject({ |
| 73 | options: { introspection: true, inflect_names: true, max_rows: 100 }, |
| 74 | hasDirective: true, |
| 75 | }) |
| 76 | }) |
| 77 | |
| 78 | it('preserves surrounding text', () => { |
| 79 | const result = parseSchemaComment( |
| 80 | 'a user-written prefix @graphql({"introspection": true}) and a suffix' |
| 81 | ) |
| 82 | expect(result).toMatchObject({ |
| 83 | options: { introspection: true }, |
| 84 | hasDirective: true, |
| 85 | prefix: 'a user-written prefix ', |
| 86 | suffix: ' and a suffix', |
| 87 | }) |
| 88 | }) |
| 89 | |
| 90 | it('treats a comment without a directive as prefix-only', () => { |
| 91 | expect(parseSchemaComment('Just a plain comment.')).toEqual({ |
| 92 | options: {}, |
| 93 | hasDirective: false, |
| 94 | isMalformed: false, |
| 95 | prefix: 'Just a plain comment.', |
| 96 | suffix: '', |
| 97 | }) |
| 98 | }) |
| 99 | |
| 100 | it('handles whitespace between @graphql and the opening paren', () => { |
| 101 | expect(parseSchemaComment('@graphql ({"introspection": true})')).toMatchObject({ |
| 102 | options: { introspection: true }, |
| 103 | hasDirective: true, |
| 104 | }) |
| 105 | }) |
| 106 | |
| 107 | it('handles whitespace inside the directive parens', () => { |
| 108 | expect(parseSchemaComment('@graphql( {"introspection": true} )')).toMatchObject({ |
| 109 | options: { introspection: true }, |
| 110 | hasDirective: true, |
| 111 | }) |
| 112 | }) |
| 113 | |
| 114 | it('handles JSON with nested objects and arrays', () => { |
| 115 | const comment = |
| 116 | '@graphql({"introspection": true, "schema": {"nested": {"deep": [1, 2, 3]}, "list": []}})' |
| 117 | expect(parseSchemaComment(comment)).toMatchObject({ |
| 118 | options: { |
| 119 | introspection: true, |
| 120 | schema: { nested: { deep: [1, 2, 3] }, list: [] }, |
| 121 | }, |
| 122 | hasDirective: true, |
| 123 | }) |
| 124 | }) |
| 125 | |
| 126 | it('handles string values containing braces and parens', () => { |
| 127 | const comment = '@graphql({"label": "value with } { ( ) braces"})' |
| 128 | expect(parseSchemaComment(comment)).toMatchObject({ |
| 129 | options: { label: 'value with } { ( ) braces' }, |
| 130 | hasDirective: true, |
| 131 | }) |
| 132 | }) |
| 133 | |
| 134 | it('handles escaped quotes inside string values', () => { |
| 135 | const comment = '@graphql({"label": "she said \\"hi\\""})' |
| 136 | expect(parseSchemaComment(comment)).toMatchObject({ |
| 137 | options: { label: 'she said "hi"' }, |
| 138 | hasDirective: true, |
| 139 | }) |
| 140 | }) |
| 141 | |
| 142 | it('treats invalid JSON as malformed', () => { |
| 143 | expect(parseSchemaComment('@graphql({not valid json})')).toMatchObject({ |
| 144 | options: {}, |
| 145 | hasDirective: true, |
| 146 | isMalformed: true, |
| 147 | }) |
| 148 | }) |
| 149 | |
| 150 | it('treats trailing-comma JSON as malformed', () => { |
| 151 | expect(parseSchemaComment('@graphql({"introspection": true,})')).toMatchObject({ |
| 152 | options: {}, |
| 153 | hasDirective: true, |
| 154 | isMalformed: true, |
| 155 | }) |
| 156 | }) |
| 157 | |
| 158 | it('ignores incomplete @graphql with no closing paren', () => { |
| 159 | expect(parseSchemaComment('@graphql({"introspection": true}')).toEqual({ |
| 160 | options: {}, |
| 161 | hasDirective: false, |
| 162 | isMalformed: false, |
| 163 | prefix: '@graphql({"introspection": true}', |
| 164 | suffix: '', |
| 165 | }) |
| 166 | }) |
| 167 | |
| 168 | it('ignores @graphql followed by something other than {', () => { |
| 169 | expect(parseSchemaComment('@graphql(true)')).toEqual({ |
| 170 | options: {}, |
| 171 | hasDirective: false, |
| 172 | isMalformed: false, |
| 173 | prefix: '@graphql(true)', |
| 174 | suffix: '', |
| 175 | }) |
| 176 | }) |
| 177 | |
| 178 | it('only matches the first @graphql directive', () => { |
| 179 | const comment = '@graphql({"introspection": true}) tail @graphql({"max_rows": 5})' |
| 180 | const result = parseSchemaComment(comment) |
| 181 | expect(result.options).toEqual({ introspection: true }) |
| 182 | expect(result.hasDirective).toBe(true) |
| 183 | expect(result.prefix).toBe('') |
| 184 | expect(result.suffix).toBe(' tail @graphql({"max_rows": 5})') |
| 185 | }) |
| 186 | }) |
| 187 | |
| 188 | describe('buildSchemaCommentWith', () => { |
| 189 | it('produces a directive when comment is null', () => { |
| 190 | expect(buildSchemaCommentWith(null, { introspection: true })).toBe( |
| 191 | '@graphql({"introspection":true})' |
| 192 | ) |
| 193 | }) |
| 194 | |
| 195 | it('produces a directive when comment is undefined', () => { |
| 196 | expect(buildSchemaCommentWith(undefined, { introspection: true })).toBe( |
| 197 | '@graphql({"introspection":true})' |
| 198 | ) |
| 199 | }) |
| 200 | |
| 201 | it('produces a directive when comment is empty', () => { |
| 202 | expect(buildSchemaCommentWith('', { introspection: true })).toBe( |
| 203 | '@graphql({"introspection":true})' |
| 204 | ) |
| 205 | }) |
| 206 | |
| 207 | it('appends a directive after existing non-directive text', () => { |
| 208 | expect(buildSchemaCommentWith('User notes about this schema', { introspection: true })).toBe( |
| 209 | 'User notes about this schema @graphql({"introspection":true})' |
| 210 | ) |
| 211 | }) |
| 212 | |
| 213 | it('avoids double-spacing when existing text already ends in a space', () => { |
| 214 | expect(buildSchemaCommentWith('User notes ', { introspection: true })).toBe( |
| 215 | 'User notes @graphql({"introspection":true})' |
| 216 | ) |
| 217 | }) |
| 218 | |
| 219 | it('replaces only the directive when one exists, preserving surrounding text', () => { |
| 220 | expect( |
| 221 | buildSchemaCommentWith('prefix @graphql({"inflect_names": true}) suffix', { |
| 222 | introspection: true, |
| 223 | }) |
| 224 | ).toBe('prefix @graphql({"inflect_names":true,"introspection":true}) suffix') |
| 225 | }) |
| 226 | |
| 227 | it('merges new keys with existing keys', () => { |
| 228 | expect( |
| 229 | buildSchemaCommentWith('@graphql({"inflect_names": true, "max_rows": 100})', { |
| 230 | introspection: true, |
| 231 | }) |
| 232 | ).toBe('@graphql({"inflect_names":true,"max_rows":100,"introspection":true})') |
| 233 | }) |
| 234 | |
| 235 | it('overrides existing keys with new values', () => { |
| 236 | expect( |
| 237 | buildSchemaCommentWith('@graphql({"introspection": false, "max_rows": 100})', { |
| 238 | introspection: true, |
| 239 | }) |
| 240 | ).toBe('@graphql({"introspection":true,"max_rows":100})') |
| 241 | }) |
| 242 | |
| 243 | it('preserves nested object values when merging', () => { |
| 244 | expect( |
| 245 | buildSchemaCommentWith('@graphql({"schema": {"foo": "bar"}, "max_rows": 100})', { |
| 246 | introspection: true, |
| 247 | }) |
| 248 | ).toBe('@graphql({"schema":{"foo":"bar"},"max_rows":100,"introspection":true})') |
| 249 | }) |
| 250 | |
| 251 | it('discards malformed directive content and writes a clean directive', () => { |
| 252 | expect( |
| 253 | buildSchemaCommentWith('prefix @graphql({not valid json}) suffix', { introspection: true }) |
| 254 | ).toBe('prefix @graphql({"introspection":true}) suffix') |
| 255 | }) |
| 256 | |
| 257 | it('supports disabling introspection', () => { |
| 258 | expect( |
| 259 | buildSchemaCommentWith('@graphql({"introspection": true, "max_rows": 100})', { |
| 260 | introspection: false, |
| 261 | }) |
| 262 | ).toBe('@graphql({"introspection":false,"max_rows":100})') |
| 263 | }) |
| 264 | }) |
| 265 | |
| 266 | describe('isIntrospectionEnabled', () => { |
| 267 | it('returns true only when introspection is the boolean true', () => { |
| 268 | expect(isIntrospectionEnabled({ introspection: true })).toBe(true) |
| 269 | }) |
| 270 | |
| 271 | it.each([ |
| 272 | [{}], |
| 273 | [{ introspection: false }], |
| 274 | [{ introspection: 'true' }], |
| 275 | [{ introspection: 1 }], |
| 276 | [{ introspection: null }], |
| 277 | [{ other: true }], |
| 278 | ])('returns false for %j', (options) => { |
| 279 | expect(isIntrospectionEnabled(options as Record<string, unknown>)).toBe(false) |
| 280 | }) |
| 281 | }) |
| 282 | |
| 283 | describe('isPgGraphqlIntrospectionOptIn', () => { |
| 284 | it.each([ |
| 285 | ['1.6.0', true], |
| 286 | ['1.6.1', true], |
| 287 | ['1.7.0', true], |
| 288 | ['2.0.0', true], |
| 289 | ['1.5.9', false], |
| 290 | ['1.5.0', false], |
| 291 | ['1.0.0', false], |
| 292 | ['0.9.0', false], |
| 293 | ])('version %s returns %s', (version, expected) => { |
| 294 | expect(isPgGraphqlIntrospectionOptIn(version)).toBe(expected) |
| 295 | }) |
| 296 | |
| 297 | it('handles versions without patch', () => { |
| 298 | expect(isPgGraphqlIntrospectionOptIn('1.6')).toBe(true) |
| 299 | expect(isPgGraphqlIntrospectionOptIn('1.5')).toBe(false) |
| 300 | }) |
| 301 | |
| 302 | it('ignores pre-release / build suffixes', () => { |
| 303 | expect(isPgGraphqlIntrospectionOptIn('1.6.0-rc.1')).toBe(true) |
| 304 | expect(isPgGraphqlIntrospectionOptIn('1.5.9-rc.1')).toBe(false) |
| 305 | }) |
| 306 | |
| 307 | it.each([[null], [undefined], [''], ['not-a-version'], ['x.y.z']])( |
| 308 | 'returns false for unparseable input %j', |
| 309 | (version) => { |
| 310 | expect(isPgGraphqlIntrospectionOptIn(version)).toBe(false) |
| 311 | } |
| 312 | ) |
| 313 | }) |