From 16905598879cb9a706c7cdfbedc9270baf9b0ab8 Mon Sep 17 00:00:00 2001
From: Richard Petersen <richard.petersen@open-xchange.com>
Date: Mon, 12 Sep 2022 17:03:19 +0200
Subject: [PATCH] Fix: UI-Middleware responds with a 404 when the trailing
 slash is not found

Root cause: Express automatically redirected, fastify does not
Solution: Manually do the redirect
---
 spec/app_root_test.js      | 87 ++++++++++++++++++++++++++++++++++++++
 src/create-app.js          |  7 ++-
 src/plugins/serve-files.js |  3 +-
 3 files changed, 95 insertions(+), 2 deletions(-)
 create mode 100644 spec/app_root_test.js

diff --git a/spec/app_root_test.js b/spec/app_root_test.js
new file mode 100644
index 0000000..f80fa82
--- /dev/null
+++ b/spec/app_root_test.js
@@ -0,0 +1,87 @@
+import request from 'supertest'
+import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { expect } from 'chai'
+import * as td from 'testdouble'
+import RedisMock from 'ioredis-mock'
+
+describe('With different app root', function () {
+  let app
+
+  beforeEach(async function () {
+    let count = 0
+    mockConfig({ urls: ['http://ui-server/'] })
+    mockRedis()
+    mockFetch({
+      'http://ui-server': {
+        '/manifest.json': generateSimpleViteManifest({
+          'example.js': { imports: ['test.txt'] },
+          'test.txt': { },
+          'main.css': {},
+          'index.html': {
+            file: 'index.html.js',
+            isEntry: true,
+            imports: ['example.js'],
+            css: ['main.css']
+          },
+          'image.png': {}
+        }),
+        '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
+        '/test.txt': () => new Response('this is test', { headers: { 'content-type': 'text/plain' } }),
+        '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }),
+        '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
+        '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }),
+        '/favicon.ico': 'not really a favicon, though',
+        '/test.svg': () => {
+          if (count > 0) {
+            return new Response(null, { status: 404 })
+          }
+          count++
+          return new Response('<svg></svg>', { headers: { 'content-type': 'image/svg' } })
+        }
+      }
+    })
+    process.env.APP_ROOT = '/appsuite/'
+    app = await mockApp()
+  })
+
+  afterEach(async function () {
+    process.env.APP_ROOT = '/'
+    await new RedisMock().flushdb()
+    td.reset()
+  })
+
+  it('serves files defined in manifest.json file', async function () {
+    const response = await request(app.server).get('/appsuite/example.js')
+    expect(response.statusCode).to.equal(200)
+    expect(response.headers['content-type']).to.equal('application/javascript')
+    expect(response.text).to.equal('this is example')
+    const response2 = await request(app.server).get('/appsuite/test.txt')
+    expect(response2.statusCode).to.equal(200)
+    expect(response2.headers['content-type']).to.equal('text/plain')
+    expect(response2.text).to.equal('this is test')
+  })
+
+  it('serves / as index.html', async function () {
+    const response = await request(app.server).get('/appsuite/')
+    expect(response.statusCode).to.equal(200)
+    expect(response.headers['content-type']).to.equal('text/html')
+    expect(response.text).to.equal('<html><head></head><body>it\'s me</body></html>')
+  })
+
+  it('serves approot without slash as index.html', async function () {
+    const response = await request(app.server).get('/appsuite')
+    expect(response.statusCode).to.equal(302)
+    expect(response.headers.location).to.equal('/appsuite/')
+  })
+
+  it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () {
+    const response = await request(app.server).get('/appsuite/favicon.ico')
+    expect(response.statusCode).to.equal(200)
+    expect(response.text).to.equal('not really a favicon, though')
+  })
+
+  it('returns 404 if a file misses the app-root', async function () {
+    const response = await request(app.server).get('/example.js')
+    expect(response.statusCode).to.equal(404)
+  })
+})
diff --git a/src/create-app.js b/src/create-app.js
index 1b685de..e5c231b 100644
--- a/src/create-app.js
+++ b/src/create-app.js
@@ -68,8 +68,13 @@ export async function createApp (basePath) {
   await app.register(manifestsPlugin, { prefix: process.env.APP_ROOT })
   await app.register(metadataPlugin, { prefix: process.env.APP_ROOT })
   await app.register(redirectsPlugin, { prefix: process.env.APP_ROOT })
+  if (process.env.APP_ROOT.length > 1) {
+    app.get(process.env.APP_ROOT.slice(0, -1), async (req, res) => {
+      res.redirect(process.env.APP_ROOT)
+    })
+  }
 
-  await app.register(serveFilePlugin, { prefix: process.env.APP_ROOT })
+  await app.register(serveFilePlugin)
 
   return app
 }
diff --git a/src/plugins/serve-files.js b/src/plugins/serve-files.js
index 4e64051..11bfaac 100644
--- a/src/plugins/serve-files.js
+++ b/src/plugins/serve-files.js
@@ -6,7 +6,8 @@ export default async function serveFilePlugin (fastify, options) {
   fastify.get('*', async (req, reply) => {
     try {
       const version = reply.version
-      const url = req.urlData('path').substr((options.prefix || '/').length - 1)
+      const url = req.urlData('path').substr(process.env.APP_ROOT.length - 1)
+
       const path = url === '/' ? '/index.html' : url
       const { body, headers } = await getFile({ version, path })
 
-- 
GitLab