瀏覽代碼

refactor: use text as default field type to avoid breaking when future field types are added

Nicolas Meienberger 2 年之前
父節點
當前提交
d3bd0b0cf9

+ 37 - 1
src/server/services/apps/apps.helpers.test.ts

@@ -1,8 +1,9 @@
 import fs from 'fs-extra';
+import { fromAny } from '@total-typescript/shoehorn';
 import { App, PrismaClient } from '@prisma/client';
 import { faker } from '@faker-js/faker';
 import { setConfig } from '../../core/TipiConfig';
-import { AppInfo, checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from './apps.helpers';
+import { AppInfo, appInfoSchema, checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from './apps.helpers';
 import { createApp, createAppConfig } from '../../tests/apps.factory';
 import { Logger } from '../../core/Logger';
 import { getTestDbClient } from '../../../../tests/server/db-connection';
@@ -136,6 +137,41 @@ describe('Test: checkEnvFile', () => {
   });
 });
 
+describe('Test: appInfoSchema', () => {
+  it('should default form_field type to text if it is wrong', async () => {
+    // arrange
+    const config = createAppConfig(fromAny({ form_fields: [{ env_variable: 'test', type: 'wrong', label: 'yo', required: true }] }));
+    fs.writeFileSync(`/app/storage/app-data/${config.id}/config.json`, JSON.stringify(config));
+
+    // act
+    const appInfo = appInfoSchema.safeParse(config);
+
+    // assert
+    expect(appInfo.success).toBe(true);
+    if (appInfo.success) {
+      expect(appInfo.data.form_fields[0]?.type).toBe('text');
+    } else {
+      expect(true).toBe(false);
+    }
+  });
+
+  it('should default categories to ["utilities"] if it is wrong', async () => {
+    // arrange
+    const config = createAppConfig(fromAny({ categories: 'wrong' }));
+    fs.writeFileSync(`/app/storage/app-data/${config.id}/config.json`, JSON.stringify(config));
+
+    // act
+    const appInfo = appInfoSchema.safeParse(config);
+
+    // assert
+    expect(appInfo.success).toBe(true);
+    if (appInfo.success) {
+      expect(appInfo.data.categories).toStrictEqual(['utilities']);
+    } else {
+      expect(true).toBe(false);
+    }
+  });
+});
 describe('Test: generateEnvFile', () => {
   let app1: AppInfo;
   let appEntity1: App;

+ 12 - 2
src/server/services/apps/apps.helpers.ts

@@ -10,13 +10,17 @@ import { notEmpty } from '../../common/typescript.helpers';
 import { ARCHITECTURES } from '../../core/TipiConfig/TipiConfig';
 
 const formFieldSchema = z.object({
-  type: z.nativeEnum(FIELD_TYPES),
+  type: z.nativeEnum(FIELD_TYPES).catch(() => FIELD_TYPES.TEXT),
   label: z.string(),
   placeholder: z.string().optional(),
   max: z.number().optional(),
   min: z.number().optional(),
   hint: z.string().optional(),
+  options: z.object({ label: z.string(), value: z.string() }).array().optional(),
   required: z.boolean().optional().default(false),
+  default: z.union([z.boolean(), z.string()]).optional(),
+  regex: z.string().optional(),
+  pattern_error: z.string().optional(),
   env_variable: z.string(),
 });
 
@@ -32,7 +36,13 @@ export const appInfoSchema = z.object({
   author: z.string(),
   source: z.string(),
   website: z.string().optional(),
-  categories: z.nativeEnum(APP_CATEGORIES).array(),
+  categories: z
+    .nativeEnum(APP_CATEGORIES)
+    .array()
+    .catch((ctx) => {
+      Logger.warn(`Invalid categories "${JSON.stringify(ctx.input)}" defaulting to utilities`);
+      return [APP_CATEGORIES.UTILITIES];
+    }),
   url_suffix: z.string().optional(),
   form_fields: z.array(formFieldSchema).optional().default([]),
   https: z.boolean().optional().default(false),

+ 6 - 11
src/server/tests/apps.factory.ts

@@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker';
 import { App, PrismaClient } from '@prisma/client';
 import { Architecture } from '../core/TipiConfig/TipiConfig';
 import { AppInfo, appInfoSchema } from '../services/apps/apps.helpers';
-import { AppCategory, APP_CATEGORIES } from '../services/apps/apps.types';
+import { APP_CATEGORIES } from '../services/apps/apps.types';
 
 interface IProps {
   installed?: boolean;
@@ -15,24 +15,19 @@ interface IProps {
   supportedArchitectures?: Architecture[];
 }
 
-type CreateConfigParams = {
-  id?: string;
-  name?: string;
-  categories?: AppCategory[];
-};
-
-const createAppConfig = (props?: CreateConfigParams) =>
+const createAppConfig = (props?: Partial<AppInfo>) =>
   appInfoSchema.parse({
-    id: props?.id || faker.random.alphaNumeric(32),
+    id: faker.random.alphaNumeric(32),
     available: true,
     port: faker.datatype.number({ min: 30, max: 65535 }),
-    name: props?.name || faker.random.alphaNumeric(32),
+    name: faker.random.alphaNumeric(32),
     description: faker.random.alphaNumeric(32),
     tipi_version: 1,
     short_desc: faker.random.alphaNumeric(32),
     author: faker.random.alphaNumeric(32),
     source: faker.internet.url(),
-    categories: props?.categories || [APP_CATEGORIES.AUTOMATION],
+    categories: [APP_CATEGORIES.AUTOMATION],
+    ...props,
   });
 
 const createApp = async (props: IProps, db?: PrismaClient) => {

+ 1 - 0
tests/server/jest.setup.ts

@@ -8,6 +8,7 @@ jest.mock('../../src/server/core/Logger', () => ({
   Logger: {
     info: jest.fn(),
     error: jest.fn(),
+    warn: jest.fn(),
   },
 }));