const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const vm = require('node:vm') const ts = require('typescript') const vue = require('vue') const { parse } = require('@vue/compiler-sfc') const { compile } = require('@vue/compiler-dom') function mountHome(auth) { const requests = [] const { descriptor } = parse(fs.readFileSync(__dirname + '/../pages/index.client.vue', 'utf8')) const context = vm.createContext({ useAuthStore: () => auth, setPageLayout() {} }) vm.runInContext(descriptor.scriptSetup.content.replace(/^import .*$/gm, ''), context) const remove = node => { if (node.parent) node.parent.children.splice(node.parent.children.indexOf(node), 1) node.parent = null } const renderer = vue.createRenderer({ createElement: type => ({ type, children: [], parent: null }), createText: text => ({ text, children: [], parent: null }), createComment: text => ({ text, children: [], parent: null }), setElementText: (node, text) => { node.text = text }, setText: (node, text) => { node.text = text }, parentNode: node => node.parent, nextSibling: node => node.parent?.children[node.parent.children.indexOf(node) + 1] || null, insert(node, parent, anchor) { remove(node) node.parent = parent const index = anchor ? parent.children.indexOf(anchor) : -1 if (index < 0) parent.children.push(node) else parent.children.splice(index, 0, node) }, remove, patchProp() {}, }) const component = { setup: () => vm.runInContext('({ auth })', context), render: new Function('Vue', compile(descriptor.template.content, { mode: 'function', prefixIdentifiers: true }).code)(vue), } const app = renderer.createApp(component) app.component('DashboardHome', { setup() { requests.push(auth.activeTenant) return () => vue.h('div', 'Dashboard ' + auth.activeTenant) }, }) for (const name of ['UDashboardNavbar', 'UDashboardPanelContent', 'UCard', 'UButton', 'TenantDropdown']) { app.component(name, { setup: (_, { slots }) => () => vue.h('div', slots.default?.()) }) } const root = { children: [] } app.mount(root) const content = node => [node.text || '', ...node.children.map(content)].join(' ') return { requests, close: () => app.unmount(), text: () => content(root) } } test('dashboard data only mounts with a tenant and remounts when the tenant changes', async t => { const auth = vue.reactive({ activeTenant: null, tenants: [{ id: '1' }, { id: '2' }], user: {} }) const ui = mountHome(auth) t.after(ui.close) assert.deepEqual(ui.requests, []) assert.match(ui.text(), /Mandant auswählen/) auth.activeTenant = '1' await vue.nextTick() assert.deepEqual(ui.requests, ['1']) auth.activeTenant = '2' await vue.nextTick() assert.deepEqual(ui.requests, ['1', '2']) auth.activeTenant = null await vue.nextTick() assert.deepEqual(ui.requests, ['1', '2']) assert.match(ui.text(), /Mandant auswählen/) }) test('an account without assigned tenants receives an explanation without loading widgets', t => { const ui = mountHome(vue.reactive({ activeTenant: null, tenants: [], user: { is_admin: true } })) t.after(ui.close) assert.deepEqual(ui.requests, []) assert.match(ui.text(), /noch kein Mandant zugeordnet/) assert.match(ui.text(), /Mandanten verwalten/) }) function authFixture() { const logs = [], calls = [], sessions = [] const token = 'test-access-token' const refreshToken = 'test-refresh-token' let me = { user: { email: 'test@example.com' }, activeTenant: null, tenants: [], permissions: [], profile: null } const context = vm.createContext({ exports: {}, process: { client: false }, defineStore: (_, definition) => definition, console: { log: (...args) => logs.push(args), error: (...args) => logs.push(args) }, useTempStore: () => ({ setStoredTempConfig() {} }), navigateTo() {}, useNuxtApp: () => ({ $api: async (url, options) => { calls.push({ url, options }) if (url === '/api/me') return me if (url === '/auth/login' || url === '/api/tenant/switch') return { token, refreshToken } throw Error('Unexpected API request: ' + url) } }), }) const source = fs.readFileSync(__dirname + '/../stores/auth.ts', 'utf8').replace(/^import .*$/gm, '') vm.runInContext(ts.transpile(source, { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }), context) const definition = context.exports.useAuthStore const store = definition.state() for (const [name, action] of Object.entries(definition.actions)) store[name] = action.bind(store) store.getStoredToken = () => token store.getStoredRefreshToken = () => refreshToken store.scheduleSessionTimers = () => {} store.setSession = (...args) => sessions.push(args) return { store, logs, calls, sessions, token, refreshToken, setMe: value => { me = value } } } test('refreshing a session without an active tenant clears the previous tenant state', async () => { const f = authFixture() f.store.activeTenant = '12' f.store.activeTenantData = { id: '12', name: 'Old tenant' } await f.store.fetchMe() assert.equal(f.store.activeTenant, null) assert.equal(f.store.activeTenantData, null) assert.deepEqual(f.calls.map(call => call.url), ['/api/me']) }) test('login and tenant switching store credentials without writing them to the console', async () => { const f = authFixture() await f.store.login('test@example.com', 'test-password') await f.store.switchTenant('1') assert.equal(f.sessions.length, 2) assert.equal(f.sessions[0][0], f.token) assert.equal(f.sessions[1][1], f.refreshToken) const output = JSON.stringify(f.logs) assert.ok(!output.includes(f.token)) assert.ok(!output.includes(f.refreshToken)) })