@igstack/app-catalog-backend-core 6.1.1 → 6.1.2-alpha-20260925023055

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-backend-core",
3
- "version": "6.1.1",
3
+ "version": "6.1.2-alpha-20260925023055",
4
4
  "description": "Backend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "dotenv-defaults": "5.0.2",
38
38
  "express": "^5.1.0",
39
39
  "jsonwebtoken": "^9.0.2",
40
- "multer": "^1.4.5-lts.1",
40
+ "multer": "^2.4.0",
41
41
  "node-cron": "^4.2.1",
42
42
  "radashi": "12.5.0-beta.6d5c035",
43
43
  "reflect-metadata": "^0.2.2",
@@ -45,8 +45,8 @@
45
45
  "tsyringe": "^4.10.0",
46
46
  "yaml": "^2.8.0",
47
47
  "zod": "^4.3.5",
48
- "@igstack/app-catalog-shared-core": "6.1.1",
49
- "@igstack/app-catalog-table-sync": "6.1.1"
48
+ "@igstack/app-catalog-shared-core": "6.1.2-alpha-20260925023055",
49
+ "@igstack/app-catalog-table-sync": "6.1.2-alpha-20260925023055"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tanstack/vite-config": "^0.4.3",
@@ -54,7 +54,7 @@
54
54
  "@types/dotenv-defaults": "^2.0.4",
55
55
  "@types/express": "^5.0.6",
56
56
  "@types/jsonwebtoken": "^9.0.9",
57
- "@types/multer": "^1.4.12",
57
+ "@types/multer": "^2.2.0",
58
58
  "@types/node": "^24.3.0",
59
59
  "@types/pg": "^8.18.0",
60
60
  "esbuild": "^0.25.5",
@@ -73,7 +73,7 @@
73
73
  "engines": {
74
74
  "node": ">=24"
75
75
  },
76
- "gitHead": "dc60c949f96e7263f289e52060055d6aa05b80f4",
76
+ "gitHead": "ad5aa811599919bb0b679affe87e5d35c338fb5d",
77
77
  "scripts": {
78
78
  "build": "tsdown",
79
79
  "build:lenient": "tsdown",
@@ -0,0 +1,112 @@
1
+ // @vitest-environment node
2
+ import express from 'express'
3
+ import type { AddressInfo } from 'node:net'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import * as dbClient from '../db/client'
6
+ import type { PrismaClient } from '../generated/prisma/client'
7
+ import { registerAssetRestController } from '../modules/assets/assetRestController'
8
+ import { upsertAsset } from '../modules/assets/upsertAsset'
9
+ import { registerIconRestController } from '../modules/icons/iconRestController'
10
+
11
+ vi.mock('../modules/assets/upsertAsset', () => ({
12
+ upsertAsset: vi.fn().mockResolvedValue('id-1'),
13
+ }))
14
+
15
+ const PNG_1X1 = Buffer.from(
16
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
17
+ 'base64',
18
+ )
19
+
20
+ /**
21
+ * Drives the real multer middleware over HTTP, so a multer major bump that
22
+ * changes memoryStorage or fileFilter semantics fails here rather than on the
23
+ * deployed upload endpoints.
24
+ */
25
+ const startApp = async () => {
26
+ const app = express()
27
+ const router = express.Router()
28
+ registerIconRestController(router, { basePath: '/api/icons' })
29
+ registerAssetRestController(router, { basePath: '/api/assets' })
30
+ app.use(router)
31
+ const server = app.listen(0)
32
+ await new Promise((resolve) => server.once('listening', resolve))
33
+ const { port } = server.address() as AddressInfo
34
+ return { server, base: `http://127.0.0.1:${port}` }
35
+ }
36
+
37
+ /** What each route handed to persistence, normalised to one shape. */
38
+ const stored = {
39
+ icon: () => create.mock.calls.map(([{ data }]) => data.content),
40
+ asset: () => vi.mocked(upsertAsset).mock.calls.map(([a]) => a.buffer),
41
+ }
42
+
43
+ const create = vi.fn()
44
+
45
+ const upload = (url: string, field: string, file: Blob, filename: string) => {
46
+ const form = new FormData()
47
+ form.append('name', 'probe')
48
+ form.append(field, file, filename)
49
+ return fetch(url, { method: 'POST', body: form })
50
+ }
51
+
52
+ describe('upload endpoints (multer)', () => {
53
+ let server: Awaited<ReturnType<typeof startApp>>['server']
54
+ let base: string
55
+
56
+ beforeEach(async () => {
57
+ create.mockReset()
58
+ vi.mocked(upsertAsset).mockClear()
59
+ create.mockImplementation(({ data }) => ({
60
+ id: 'id-1',
61
+ createdAt: new Date(0),
62
+ ...data,
63
+ }))
64
+ vi.spyOn(dbClient, 'getDbClient').mockReturnValue({
65
+ dbAsset: { create },
66
+ } as unknown as PrismaClient)
67
+ vi.spyOn(console, 'error').mockImplementation(() => {})
68
+ ;({ server, base } = await startApp())
69
+ })
70
+
71
+ afterEach(() => {
72
+ server.close()
73
+ vi.mocked(console.error).mockRestore()
74
+ })
75
+
76
+ it.each([
77
+ ['/api/icons/upload', 'icon'],
78
+ ['/api/assets/upload', 'asset'],
79
+ ] as const)(
80
+ '%s stores an uploaded image in memory intact',
81
+ async (path, field) => {
82
+ const res = await upload(
83
+ base + path,
84
+ field,
85
+ new Blob([PNG_1X1], { type: 'image/png' }),
86
+ 'probe.png',
87
+ )
88
+
89
+ expect(res.status).toBe(201)
90
+ const [content] = stored[field]()
91
+ expect(Buffer.from(content).equals(PNG_1X1)).toBe(true)
92
+ },
93
+ )
94
+
95
+ it.each([
96
+ ['/api/icons/upload', 'icon'],
97
+ ['/api/assets/upload', 'asset'],
98
+ ] as const)(
99
+ '%s rejects a non-image before it reaches the DB',
100
+ async (path, field) => {
101
+ const res = await upload(
102
+ base + path,
103
+ field,
104
+ new Blob(['hello'], { type: 'text/plain' }),
105
+ 'probe.txt',
106
+ )
107
+
108
+ expect(res.status).toBeGreaterThanOrEqual(400)
109
+ expect(stored[field]()).toEqual([])
110
+ },
111
+ )
112
+ })