openapi-request-bodies.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { describe, expect, it } from 'vitest';
  2. import { buildSpec } from '../../scripts/build-openapi.mjs';
  3. interface OpenApiSchema {
  4. type?: string;
  5. format?: string;
  6. description?: string;
  7. default?: string | number | boolean;
  8. minLength?: number;
  9. pattern?: string;
  10. properties?: Record<string, OpenApiSchema>;
  11. required?: string[];
  12. items?: OpenApiSchema;
  13. anyOf?: OpenApiSchema[];
  14. }
  15. interface OpenApiRequestBody {
  16. required?: boolean;
  17. content: Record<
  18. string,
  19. {
  20. schema: OpenApiSchema;
  21. encoding?: Record<string, { style?: string; explode?: boolean; contentType?: string }>;
  22. }
  23. >;
  24. }
  25. interface OpenApiOperation {
  26. requestBody?: OpenApiRequestBody;
  27. }
  28. const paths = buildSpec().paths as Record<string, Record<string, OpenApiOperation>>;
  29. function requestBody(path: string): OpenApiRequestBody {
  30. const body = paths[path]?.post?.requestBody;
  31. if (!body) throw new Error(`${path} has no POST request body`);
  32. return body;
  33. }
  34. describe('generated OpenAPI request bodies', () => {
  35. it('preserves JSON, form, and multipart parameter declarations', () => {
  36. const login = requestBody('/login').content['application/json'];
  37. expect(login.schema.properties).toHaveProperty('username');
  38. expect(login.schema.required).toEqual(['username', 'password']);
  39. const json = requestBody('/panel/api/inbounds/pushClientTraffics').content['application/json'];
  40. expect(json.schema.properties).toHaveProperty('traffics');
  41. const form = requestBody('/panel/api/inbounds/import').content[
  42. 'application/x-www-form-urlencoded'
  43. ];
  44. expect(form.schema.properties).toHaveProperty('data');
  45. const logs = requestBody('/panel/api/server/logs/{count}');
  46. expect(logs.content).toHaveProperty('application/x-www-form-urlencoded');
  47. expect(logs.content['application/x-www-form-urlencoded'].schema.properties).toHaveProperty(
  48. 'syslog',
  49. );
  50. const outboundTest = requestBody('/panel/api/xray/testOutbound').content[
  51. 'application/x-www-form-urlencoded'
  52. ];
  53. expect(outboundTest.schema.required).toEqual(['outbound']);
  54. const outboundUpdate = requestBody('/panel/api/xray/outbound-subs/{id}').content[
  55. 'application/x-www-form-urlencoded'
  56. ];
  57. expect(outboundUpdate.schema.required).toEqual(['url']);
  58. expect(outboundUpdate.schema.properties).toHaveProperty('allowInsecure');
  59. const balancerUpdate = requestBody('/panel/api/sub-balancers/{id}').content[
  60. 'application/x-www-form-urlencoded'
  61. ];
  62. expect(balancerUpdate.schema.required).toEqual(['remark', 'inboundIds']);
  63. expect(balancerUpdate.encoding?.inboundIds).toEqual({ style: 'form', explode: true });
  64. expect(balancerUpdate.encoding?.memberWeights).toEqual({ contentType: 'application/json' });
  65. const inboundUpdate = requestBody('/panel/api/inbounds/update/{id}').content[
  66. 'application/json'
  67. ];
  68. expect(inboundUpdate.schema).toEqual({ type: 'object' });
  69. const multipart = requestBody('/panel/api/server/importDB').content['multipart/form-data'];
  70. expect(multipart.schema.properties?.db).toEqual({
  71. type: 'string',
  72. format: 'binary',
  73. description: 'Database backup or migration file to upload.',
  74. });
  75. expect(multipart.schema.properties).toHaveProperty('keepHostSettings');
  76. expect(multipart.schema.properties?.keepHostSettings?.default).toBe(true);
  77. const array = requestBody('/panel/api/server/clientIps').content['application/json'];
  78. expect(array.schema).toEqual({
  79. type: 'array',
  80. items: {
  81. type: 'object',
  82. properties: {
  83. clientEmail: { type: 'string' },
  84. ips: {
  85. type: 'array',
  86. nullable: true,
  87. items: {
  88. type: 'object',
  89. properties: { ip: { type: 'string' }, timestamp: { type: 'integer' } },
  90. required: ['ip', 'timestamp'],
  91. },
  92. },
  93. },
  94. required: ['clientEmail', 'ips'],
  95. },
  96. });
  97. const certHash = requestBody('/panel/api/server/getCertHash');
  98. expect(certHash.required).toBe(true);
  99. const certSchema = certHash.content['application/x-www-form-urlencoded'].schema;
  100. expect(certSchema.anyOf?.map((branch) => branch.required)).toEqual([
  101. ['certFile'],
  102. ['certContent'],
  103. ]);
  104. expect(certSchema.anyOf?.[0].properties?.certFile.pattern).toBe('.*\\S.*');
  105. expect(certSchema.anyOf?.[0].properties?.certContent).not.toHaveProperty('pattern');
  106. expect(certSchema.anyOf?.[1].properties?.certFile).not.toHaveProperty('pattern');
  107. expect(certSchema.anyOf?.[1].properties?.certContent.pattern).toBe('.*\\S.*');
  108. const routeSchema = requestBody('/panel/api/xray/routeTest').content[
  109. 'application/x-www-form-urlencoded'
  110. ].schema;
  111. expect(routeSchema.anyOf?.map((branch) => branch.required)).toEqual([['domain'], ['ip']]);
  112. expect(routeSchema.anyOf?.[0].properties?.domain?.minLength).toBe(1);
  113. expect(routeSchema.anyOf?.[0].properties?.ip).not.toHaveProperty('minLength');
  114. expect(routeSchema.anyOf?.[1].properties?.domain).not.toHaveProperty('minLength');
  115. expect(routeSchema.anyOf?.[1].properties?.ip?.minLength).toBe(1);
  116. const bulkAttach = requestBody('/panel/api/clients/bulkAttach').content['application/json'];
  117. expect(bulkAttach.schema.properties?.emails?.items).toEqual({ type: 'string' });
  118. expect(paths['/panel/api/server/updateGeofile'].post).not.toHaveProperty('requestBody');
  119. });
  120. });