| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  | import fs from "fs"; | 
					
						
							|  |  |  | import jsyaml from "js-yaml"; | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-07 06:10:15 +00:00
										 |  |  | type FormField = { | 
					
						
							|  |  |  |   type: "random"; | 
					
						
							|  |  |  |   required: boolean; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | interface AppConfig { | 
					
						
							|  |  |  |   id: string; | 
					
						
							|  |  |  |   port: number; | 
					
						
							|  |  |  |   categories: string[]; | 
					
						
							|  |  |  |   requirements?: { | 
					
						
							|  |  |  |     ports?: number[]; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   name: string; | 
					
						
							|  |  |  |   description: string; | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |   version?: string; | 
					
						
							|  |  |  |   tipi_version: number; | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |   short_desc: string; | 
					
						
							|  |  |  |   author: string; | 
					
						
							|  |  |  |   source: string; | 
					
						
							|  |  |  |   available: boolean; | 
					
						
							| 
									
										
										
										
											2023-07-07 06:10:15 +00:00
										 |  |  |   form_fields?: FormField[]; | 
					
						
							| 
									
										
										
										
											2023-10-29 19:43:17 +00:00
										 |  |  |   supported_architectures: string[]; | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  | const networkExceptions = [ | 
					
						
							|  |  |  |   "pihole", | 
					
						
							|  |  |  |   "tailscale", | 
					
						
							|  |  |  |   "homeassistant", | 
					
						
							|  |  |  |   "plex", | 
					
						
							|  |  |  |   "zerotier", | 
					
						
							|  |  |  |   "gladys", | 
					
						
							| 
									
										
										
										
											2023-08-30 20:12:09 +00:00
										 |  |  |   "scrypted", | 
					
						
							| 
									
										
										
										
											2023-08-30 22:20:48 +00:00
										 |  |  |   "homebridge", | 
					
						
							| 
									
										
										
										
											2023-09-27 05:53:48 +00:00
										 |  |  |   "cloudflared", | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  | ]; | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | const getAppConfigs = (): AppConfig[] => { | 
					
						
							|  |  |  |   const apps: AppConfig[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   const appsDir = fs.readdirSync("./apps"); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |   appsDir.forEach((app: string) => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const path = `./apps/${app}/config.json`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (fs.existsSync(path)) { | 
					
						
							|  |  |  |       const configFile = fs.readFileSync(path).toString(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         const config: AppConfig = JSON.parse(configFile); | 
					
						
							|  |  |  |         if (config.available) { | 
					
						
							|  |  |  |           apps.push(config); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } catch (e) { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |         console.error("Error parsing config file", app); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return apps; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  | describe("App configs", () => { | 
					
						
							|  |  |  |   it("Get app config should return at least one app", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     expect(apps.length).toBeGreaterThan(0); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have an id", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(app.id).toBeDefined(); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a md description", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         const path = `./apps/${app.id}/metadata/description.md`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (fs.existsSync(path)) { | 
					
						
							|  |  |  |           const description = fs.readFileSync(path).toString(); | 
					
						
							|  |  |  |           expect(description).toBeDefined(); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           expect(true).toBe(false); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have categories defined as an array", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(app.categories).toBeDefined(); | 
					
						
							|  |  |  |         expect(app.categories).toBeInstanceOf(Array); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a name", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(app.name).toBeDefined(); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a description", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(app.description).toBeDefined(); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a port", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(app.port).toBeDefined(); | 
					
						
							|  |  |  |         expect(app.port).toBeGreaterThan(999); | 
					
						
							|  |  |  |         expect(app.port).toBeLessThan(65535); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-29 19:43:17 +00:00
										 |  |  |   describe("Each app should have a supported architecture", () => { | 
					
						
							|  |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							|  |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(app.supported_architectures).toBeDefined(); | 
					
						
							|  |  |  |         expect(app.supported_architectures).toBeInstanceOf(Array); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   test("Each app should have a different port", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const appConfigs = getAppConfigs(); | 
					
						
							|  |  |  |     const ports = appConfigs.map((app) => app.port); | 
					
						
							|  |  |  |     expect(new Set(ports).size).toBe(appConfigs.length); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   test("Each app should have a unique id", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const appConfigs = getAppConfigs(); | 
					
						
							|  |  |  |     const ids = appConfigs.map((app) => app.id); | 
					
						
							|  |  |  |     expect(new Set(ids).size).toBe(appConfigs.length); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a version", () => { | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(app.version).toBeDefined(); | 
					
						
							|  |  |  |         expect(app.tipi_version).toBeDefined(); | 
					
						
							|  |  |  |         expect(app.tipi_version).toBeGreaterThan(0); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a docker-compose file beside it", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(fs.existsSync(`./apps/${app.id}/docker-compose.yml`)).toBe(true); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a metadata folder beside it", () => { | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(fs.existsSync(`./apps/${app.id}/metadata`)).toBe(true); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a file named logo.jpg in the metadata folder", () => { | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         expect(fs.existsSync(`./apps/${app.id}/metadata/logo.jpg`)).toBe(true); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-08-04 15:34:02 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have a container name equals to its id", () => { | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         const dockerComposeFile = fs | 
					
						
							|  |  |  |           .readFileSync(`./apps/${app.id}/docker-compose.yml`) | 
					
						
							|  |  |  |           .toString(); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |         const dockerCompose: any = jsyaml.load(dockerComposeFile); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |         expect(dockerCompose.services[app.id]).toBeDefined(); | 
					
						
							|  |  |  |         expect(dockerCompose.services[app.id].container_name).toBe(app.id); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |   describe("Each app should have the same version in config.json and docker-compose.yml", () => { | 
					
						
							|  |  |  |     const exceptions = ["revolt"]; | 
					
						
							|  |  |  |     const apps = getAppConfigs().filter((app) => !exceptions.includes(app.id)); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         const dockerComposeFile = fs | 
					
						
							|  |  |  |           .readFileSync(`./apps/${app.id}/docker-compose.yml`) | 
					
						
							|  |  |  |           .toString(); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         const dockerCompose: any = jsyaml.load(dockerComposeFile); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         expect(dockerCompose.services[app.id]).toBeDefined(); | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |         expect(dockerCompose.services[app.id].image).toBeDefined(); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |         const dockerImage = dockerCompose.services[app.id].image; | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 20:52:42 +00:00
										 |  |  |         const version = dockerImage.split(":")[1]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         expect(version).toContain(app.version); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe("Each app should have network tipi_main_network", () => { | 
					
						
							|  |  |  |     const apps = getAppConfigs(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     apps.forEach((app) => { | 
					
						
							|  |  |  |       test(app.id, () => { | 
					
						
							|  |  |  |         if (!networkExceptions.includes(app.id)) { | 
					
						
							|  |  |  |           const dockerComposeFile = fs | 
					
						
							|  |  |  |             .readFileSync(`./apps/${app.id}/docker-compose.yml`) | 
					
						
							|  |  |  |             .toString(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const dockerCompose: any = jsyaml.load(dockerComposeFile); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           expect(dockerCompose.services[app.id]).toBeDefined(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           expect(dockerCompose.services[app.id].networks).toBeDefined(); | 
					
						
							|  |  |  |           expect(dockerCompose.services[app.id].networks).toStrictEqual([ | 
					
						
							|  |  |  |             "tipi_main_network", | 
					
						
							|  |  |  |           ]); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2023-07-07 06:10:15 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   describe("All form fields with type random should not be marked as required", () => { | 
					
						
							|  |  |  |     const configs = getAppConfigs(); | 
					
						
							|  |  |  |     configs.forEach((config) => { | 
					
						
							|  |  |  |       const formFields = config.form_fields; | 
					
						
							|  |  |  |       if (formFields) { | 
					
						
							|  |  |  |         formFields.forEach((field) => { | 
					
						
							|  |  |  |           if (field.type === "random") { | 
					
						
							|  |  |  |             test(config.id, () => { | 
					
						
							|  |  |  |               expect(Boolean(field.required)).toBe(false); | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2022-07-28 17:46:12 +00:00
										 |  |  | }); |