This commit is contained in:
1 parent
e4843e7d1d
commit
1808f14cc6
17 files changed
+711
-23
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildOrgDirectory, resolveUserOrganization, getOrgDirectory, enrichUserOrganization, orgContextConfig } from '../src/org-context.js';
|
||||
|
||||
const ROOT = '038326b6-954a-48a7-a85f-efd83f62b82a';
|
||||
const COMPANY = '9951229e-eea5-4376-96f4-78989bc3a506';
|
||||
const DEPT = 'ee2f39ac-fe52-4cfb-b4e3-4ae1d114c916';
|
||||
const OTHER = 'd19c10f0-0224-4bbb-bf3e-ce579c5338ea';
|
||||
const USER = '01900000-0000-7000-8000-000000000001';
|
||||
let sequence = 0;
|
||||
function env(extra = {}) {
|
||||
return { ORG_CONTEXT_ENABLED: 'true', ORG_CONTEXT_TENANT_IDS: JSON.stringify([ROOT]), ORG_CONTEXT_KEY_ID: 'fixture', ORG_CONTEXT_KEY_SECRET: 'secret-' + ++sequence, ...extra };
|
||||
}
|
||||
function payload(root = ROOT) {
|
||||
return { scope: { tenantId: root }, tree: { id: root, name: '분류 루트', type: 'ROOT', children: [
|
||||
{ id: COMPANY, name: '실제 회사', type: 'COMPANY', children: [
|
||||
{ id: DEPT, name: '설계부', type: 'DEPARTMENT', members: [{ id: USER, isPrimary: true, phone: '+8201000000000' }] }
|
||||
] }
|
||||
] } };
|
||||
}
|
||||
const user = { userUuid: USER, ssoSubject: USER, requesterId: USER, requesterTenantId: ROOT, tenantId: ROOT, roles: ['GENERAL'], company: 'old', department: 'old' };
|
||||
const response = (value, status = 200) => new Response(JSON.stringify(value), { status });
|
||||
|
||||
test('disabled lookup preserves existing behavior and never fetches', async () => {
|
||||
const actual = await enrichUserOrganization(user, {}, { fetchImpl() { assert.fail('network disabled'); } });
|
||||
assert.equal(actual, user);
|
||||
assert.equal(orgContextConfig({ ORG_CONTEXT_ENABLED: 'false' }), null);
|
||||
});
|
||||
test('accepts UUID v4 roots, normalizes duplicates and validates config', () => {
|
||||
const config = orgContextConfig(env({ ORG_CONTEXT_TENANT_IDS: JSON.stringify([ROOT, ROOT.toUpperCase()]) }));
|
||||
assert.deepEqual(config.roots, [ROOT]);
|
||||
for (const invalid of ['[]', '"not-array"', '["invalid"]', '{']) {
|
||||
assert.throws(() => orgContextConfig(env({ ORG_CONTEXT_TENANT_IDS: invalid })), /invalid_tenants/);
|
||||
}
|
||||
assert.throws(() => orgContextConfig(env({ ORG_CONTEXT_API_URL: 'http://example.test' })), /invalid_endpoint/);
|
||||
assert.throws(() => orgContextConfig(env({ ORG_CONTEXT_KEY_SECRET: '' })), /missing_credentials/);
|
||||
});
|
||||
test('walks descendants and resolves the actual company instead of classification root', () => {
|
||||
const directory = buildOrgDirectory([payload()]);
|
||||
assert.equal(directory.tenantCount, 3);
|
||||
const organization = resolveUserOrganization(directory, user);
|
||||
assert.equal(organization.company, '실제 회사');
|
||||
assert.equal(organization.department, '설계부');
|
||||
assert.equal(organization.departmentPath, '설계부');
|
||||
});
|
||||
test('merges overlapping roots and flat tenants without losing memberships', () => {
|
||||
const data = payload();
|
||||
data.tenants = [data.tree.children[0].children[0]];
|
||||
const directory = buildOrgDirectory([data, { tree: data.tree.children[0] }]);
|
||||
assert.equal(directory.membershipCount, 1);
|
||||
assert.equal(directory.tenantCount, 3);
|
||||
assert.equal(resolveUserOrganization(directory, user).company, '실제 회사');
|
||||
});
|
||||
test('preserves multiple appointments and prefers the authenticated tenant context', () => {
|
||||
const data = payload();
|
||||
data.tree.children[0].children.push({ id: OTHER, type: 'DEPARTMENT', name: '겸직부서', members: [{ id: USER, isPrimary: false }] });
|
||||
const directory = buildOrgDirectory([data]);
|
||||
assert.equal(directory.membershipCount, 2);
|
||||
assert.equal(resolveUserOrganization(directory, { ...user, requesterTenantId: OTHER }).department, '겸직부서');
|
||||
assert.equal(resolveUserOrganization(directory, user).department, '설계부');
|
||||
for (const dept of data.tree.children[0].children) dept.members[0].isPrimary = false;
|
||||
assert.equal(resolveUserOrganization(buildOrgDirectory([data]), user).status, 'ambiguous_membership');
|
||||
});
|
||||
test('never matches a different person using email or copies a viewer organization', () => {
|
||||
const directory = buildOrgDirectory([payload()]);
|
||||
assert.equal(resolveUserOrganization(directory, { userUuid: OTHER, email: 'same@example.test' }).status, 'not_found');
|
||||
});
|
||||
test('does not mistake a company or classification root for a department', () => {
|
||||
const data = { tree: { id: ROOT, type: 'ROOT', name: '일반회사', members: [{ id: USER }] } };
|
||||
const resolved = resolveUserOrganization(buildOrgDirectory([data]), user);
|
||||
assert.equal(resolved.company, '');
|
||||
assert.equal(resolved.department, '');
|
||||
});
|
||||
test('rejects cycles and conflicting overlapping memberships', () => {
|
||||
assert.throws(() => buildOrgDirectory([{ tenants: [
|
||||
{ id: ROOT, parentId: COMPANY, members: [{ id: USER }] }, { id: COMPANY, parentId: ROOT }
|
||||
] }]), /cycle/);
|
||||
const first = payload(), second = payload();
|
||||
second.tree.children[0].children[0].members[0].isPrimary = false;
|
||||
assert.throws(() => buildOrgDirectory([first, second]), /conflicting_membership/);
|
||||
});
|
||||
test('fetches each root with identifiers and caches only complete responses', async () => {
|
||||
const config = env({ ORG_CONTEXT_TENANT_IDS: JSON.stringify([ROOT, OTHER]) });
|
||||
const urls = [];
|
||||
let currentTime = 1000;
|
||||
const fetchImpl = async (url, init) => {
|
||||
const parsed = new URL(url);
|
||||
urls.push(url);
|
||||
assert.equal(parsed.searchParams.get('exposure'), 'identifiers');
|
||||
assert.equal(init.headers['X-Baron-Key-ID'], config.ORG_CONTEXT_KEY_ID);
|
||||
assert.equal(init.headers['X-Baron-Key-Secret'], config.ORG_CONTEXT_KEY_SECRET);
|
||||
assert.equal(init.redirect, 'error');
|
||||
return response(payload(parsed.searchParams.get('tenantId')));
|
||||
};
|
||||
// Use disjoint roots with no repeated tenant parent to emulate two independent subtrees.
|
||||
const independentFetch = async (url, init) => {
|
||||
if (new URL(url).searchParams.get('tenantId') === OTHER) {
|
||||
urls.push(url);
|
||||
return response({ scope: { tenantId: OTHER }, tree: { id: OTHER, name: 'Other', type: 'ROOT' } });
|
||||
}
|
||||
return fetchImpl(url, init);
|
||||
};
|
||||
const options = { fetchImpl: independentFetch, now: () => currentTime };
|
||||
const first = await getOrgDirectory(config, options);
|
||||
assert.equal(first.tenantCount, 4);
|
||||
assert.equal(await getOrgDirectory(config, options), first);
|
||||
assert.equal(urls.length, 2);
|
||||
currentTime += 300001;
|
||||
await getOrgDirectory(config, options);
|
||||
assert.equal(urls.length, 4);
|
||||
});
|
||||
test('concurrent lookups share one in-flight request', async () => {
|
||||
const config = env();
|
||||
let count = 0;
|
||||
const fetchImpl = async () => { count++; await new Promise((resolve) => setTimeout(resolve, 5)); return response(payload()); };
|
||||
await Promise.all([getOrgDirectory(config, { fetchImpl }), getOrgDirectory(config, { fetchImpl })]);
|
||||
assert.equal(count, 1);
|
||||
});
|
||||
test('rejects wrong scope and never caches a partial or forbidden result', async () => {
|
||||
const config = env();
|
||||
await assert.rejects(getOrgDirectory(config, { fetchImpl: async () => response(payload(OTHER)) }), /scope_mismatch/);
|
||||
await assert.rejects(getOrgDirectory(config, { fetchImpl: async () => response({ secret: 'not logged' }, 403) }), /http_403/);
|
||||
assert.equal((await getOrgDirectory(config, { fetchImpl: async () => response(payload()) })).byUser.size, 1);
|
||||
});
|
||||
test('enrichment updates only profile fields and preserves authentication and roles', async () => {
|
||||
const result = await enrichUserOrganization(user, env(), { fetchImpl: async () => response(payload()) });
|
||||
assert.equal(result.company, '실제 회사');
|
||||
assert.equal(result.department, '설계부');
|
||||
for (const field of ['requesterId', 'requesterTenantId', 'ssoSubject', 'userUuid', 'tenantId', 'roles']) assert.deepEqual(result[field], user[field]);
|
||||
assert.ok(!JSON.stringify(result).includes('X-Baron'));
|
||||
assert.ok(!('byUser' in result));
|
||||
});
|
||||
test('upstream failure preserves existing profile and emits only a safe error code', async () => {
|
||||
const saved = console.error;
|
||||
const logs = [];
|
||||
console.error = (...args) => logs.push(args);
|
||||
try {
|
||||
const result = await enrichUserOrganization(user, env(), { fetchImpl: async () => { throw new Error('Secret header: private-contact'); } });
|
||||
assert.equal(result.orgContextStatus, 'unavailable');
|
||||
assert.equal(result.company, user.company);
|
||||
assert.deepEqual(logs, [['org_context_request_failed']]);
|
||||
} finally { console.error = saved; }
|
||||
});
|
||||
|
||||
test('a failing root is not accepted as a partial complete directory', async () => {
|
||||
const config = env({ ORG_CONTEXT_TENANT_IDS: JSON.stringify([ROOT, OTHER]) });
|
||||
await assert.rejects(getOrgDirectory(config, { fetchImpl: async (url) => {
|
||||
const root = new URL(url).searchParams.get('tenantId');
|
||||
return root === OTHER ? response({}, 403) : response(payload());
|
||||
} }), /http_403/);
|
||||
let fetched = 0;
|
||||
const directory = await getOrgDirectory(config, { fetchImpl: async (url) => {
|
||||
fetched++;
|
||||
const root = new URL(url).searchParams.get('tenantId');
|
||||
return response(root === ROOT ? payload() : { scope: { tenantId: OTHER }, tree: { id: OTHER, type: 'ROOT', name: 'Other' } });
|
||||
} });
|
||||
assert.equal(fetched, 2);
|
||||
assert.equal(directory.byUser.size, 1);
|
||||
});
|
||||
test('identifiers verification rejects a structure-only response with no members', async () => {
|
||||
await assert.rejects(getOrgDirectory(env(), { fetchImpl: async () => response({ scope: { tenantId: ROOT }, tree: { id: ROOT, name: 'Root' } }) }), /members_missing/);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const source = readFileSync(new URL('../egbim/app.js', import.meta.url), 'utf8');
|
||||
const context = vm.createContext({
|
||||
window: { QA_CONFIG: {}, location: { origin: 'https://qa.example.test' } },
|
||||
document: { cookie: '', addEventListener() {} },
|
||||
sessionStorage: { getItem() { return null; } },
|
||||
localStorage: { getItem() { return null; } }, URL
|
||||
});
|
||||
vm.runInContext(source.replace(/\}\)\(\);\s*$/, 'globalThis.testPage = { normalizeRemotePost, getPostCompanyLabel, normalizeAuthUser }; })();'), context);
|
||||
const page = context.testPage;
|
||||
|
||||
test('saved organization has priority over email-domain inference in list and detail', () => {
|
||||
const post = page.normalizeRemotePost({ id: 'fixture', requester_email: 'fixture@brsw.kr', requester_affiliation: '저장된 회사', requester_department: '저장된 부서' });
|
||||
assert.equal(post.company, '저장된 회사');
|
||||
assert.equal(post.department, '저장된 부서');
|
||||
assert.equal(page.getPostCompanyLabel(post, { company: '현재 조회자 회사' }), '저장된 회사');
|
||||
});
|
||||
test('a matching tenant does not copy the current viewer company to an unrelated author', () => {
|
||||
const label = page.getPostCompanyLabel({ requesterTenantId: 'shared', raw: {} }, { tenantId: 'shared', company: '다른 작성자 회사' });
|
||||
assert.notEqual(label, '다른 작성자 회사');
|
||||
});
|
||||
test('server organization has priority over stale browser custom attributes', () => {
|
||||
const user = page.normalizeAuthUser({ company: '서버 회사', department: '서버 부서', email: 'fixture@brsw.kr', customAttributes: { company: 'old', department: 'old' } }, {});
|
||||
assert.equal(user.company, '서버 회사');
|
||||
assert.equal(user.department, '서버 부서');
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHmac } from 'node:crypto';
|
||||
import worker from '../src/index.js';
|
||||
import { deploymentOrgSettings } from '../scripts/org-context-settings.mjs';
|
||||
|
||||
const ROOT = '038326b6-954a-48a7-a85f-efd83f62b82a';
|
||||
const COMPANY = '9951229e-eea5-4376-96f4-78989bc3a506';
|
||||
const DEPT = 'ee2f39ac-fe52-4cfb-b4e3-4ae1d114c916';
|
||||
const USER = '01900000-0000-7000-8000-000000000001';
|
||||
const FEEDBACK = '01900000-0000-7000-8000-000000000002';
|
||||
const profile = { userUuid: USER, requesterId: USER, ssoSubject: USER, tenantId: ROOT, requesterTenantId: ROOT, phone: '+8201000000000', email: 'fixture@brsw.kr', company: 'old inference', roles: ['GENERAL'] };
|
||||
let sequence = 0;
|
||||
const configuration = (extra = {}) => ({
|
||||
SESSION_COOKIE_NAME: 'fixture_session', SESSION_SECRET: 'signed-session-fixture', ABC_API_KEY: 'project-key',
|
||||
ABC_PROJECT_ID: FEEDBACK, ABC_CHANNEL_ID: FEEDBACK, ORG_CONTEXT_ENABLED: 'true',
|
||||
ORG_CONTEXT_KEY_ID: 'integration-key', ORG_CONTEXT_KEY_SECRET: 'fixture-' + ++sequence,
|
||||
ORG_CONTEXT_TENANT_IDS: JSON.stringify([ROOT]), ...extra
|
||||
});
|
||||
function sessionCookie(env) {
|
||||
const payload = Buffer.from(JSON.stringify({ user: profile, expiresAt: Date.now() + 60000 })).toString('base64url');
|
||||
return env.SESSION_COOKIE_NAME + '=' + payload + '.' + createHmac('sha256', env.SESSION_SECRET).update(payload).digest('base64url');
|
||||
}
|
||||
function organization() {
|
||||
return { scope: { tenantId: ROOT }, tree: { id: ROOT, name: '분류', type: 'ROOT', children: [
|
||||
{ id: COMPANY, name: '정확한 회사', type: 'COMPANY', children: [
|
||||
{ id: DEPT, name: '정확한 부서', type: 'DEPARTMENT', members: [{ id: USER, isPrimary: true }] }
|
||||
] }
|
||||
] } };
|
||||
}
|
||||
const json = (value, status = 200) => new Response(JSON.stringify(value), { status });
|
||||
async function mockedFetch(callback, body) {
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = callback;
|
||||
try { return await body(); } finally { globalThis.fetch = original; }
|
||||
}
|
||||
|
||||
test('unauthenticated create is rejected without contacting any upstream', async () => {
|
||||
await mockedFetch(() => assert.fail('unauthenticated network request'), async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/api/feedbacks', { method: 'POST' }), configuration());
|
||||
assert.equal(response.status, 401);
|
||||
});
|
||||
});
|
||||
for (const multipart of [false, true]) {
|
||||
test(`new ${multipart ? 'multipart' : 'JSON'} feedback stores server-resolved organization and preserves ownership`, async () => {
|
||||
const env = configuration();
|
||||
let saved;
|
||||
await mockedFetch(async (url, init) => {
|
||||
if (String(url).includes('/org-context')) return json(organization());
|
||||
saved = init;
|
||||
return json({ id: FEEDBACK });
|
||||
}, async () => {
|
||||
const input = { title: 'fixture', contents: 'body', Category: 'GENERAL_QNA', requester_affiliation: 'forged company', requester_id: 'forged owner' };
|
||||
let body, headers = { cookie: sessionCookie(env) };
|
||||
if (multipart) {
|
||||
body = new FormData();
|
||||
Object.entries(input).forEach(([key, value]) => body.append(key, value));
|
||||
body.append('images', new File(['fixture-image'], 'image.png', { type: 'image/png' }));
|
||||
} else { body = JSON.stringify(input); headers['content-type'] = 'application/json'; }
|
||||
const result = await worker.fetch(new Request('https://qa.example.test/api/feedbacks', { method: 'POST', headers, body }), env);
|
||||
assert.equal(result.status, 200);
|
||||
const payload = multipart ? Object.fromEntries(saved.body.entries()) : JSON.parse(saved.body);
|
||||
assert.equal(payload.requester_affiliation, '정확한 회사');
|
||||
assert.equal(payload.requester_department, '정확한 부서');
|
||||
assert.equal(payload.requester_id, USER);
|
||||
assert.equal(payload.requester_tenant_id, ROOT);
|
||||
assert.equal(saved.headers['x-api-key'], 'project-key');
|
||||
if (multipart) {
|
||||
assert.equal(saved.headers['content-type'], undefined);
|
||||
assert.equal((await payload.images.text()), 'fixture-image');
|
||||
}
|
||||
assert.ok(!JSON.stringify(await result.json()).includes('integration-key'));
|
||||
});
|
||||
});
|
||||
}
|
||||
test('existing signed sessions are enriched without requiring another login or changing session identity', async () => {
|
||||
const env = configuration();
|
||||
await mockedFetch(async () => json(organization()), async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/auth/session', { headers: { cookie: sessionCookie(env) } }), env);
|
||||
const body = await response.json();
|
||||
assert.equal(body.authenticated, true);
|
||||
assert.equal(body.user.company, '정확한 회사');
|
||||
assert.equal(body.user.requesterTenantId, ROOT);
|
||||
assert.deepEqual(body.user.roles, ['GENERAL']);
|
||||
assert.equal(response.headers.get('set-cookie'), null);
|
||||
assert.equal(body.user.byUser, undefined);
|
||||
});
|
||||
});
|
||||
test('organization service failure does not break an existing authenticated session', async () => {
|
||||
const env = configuration();
|
||||
const original = console.error;
|
||||
console.error = () => {};
|
||||
try {
|
||||
await mockedFetch(async () => json({}, 503), async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/auth/session', { headers: { cookie: sessionCookie(env) } }), env);
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.authenticated, true);
|
||||
assert.equal(body.user.requesterId, USER);
|
||||
assert.equal(body.user.orgContextStatus, 'unavailable');
|
||||
});
|
||||
} finally { console.error = original; }
|
||||
});
|
||||
test('list reads saved historical organization without calling the organization API', async () => {
|
||||
const env = configuration();
|
||||
let calls = 0;
|
||||
await mockedFetch(async (url) => {
|
||||
assert.ok(!String(url).includes('org-context'));
|
||||
calls++;
|
||||
return json({ items: [{ id: FEEDBACK, requester_affiliation: '작성 당시 회사', requester_department: '작성 당시 부서' }] });
|
||||
}, async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/api/feedbacks', { headers: { cookie: sessionCookie(env) } }), env);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal((await response.json()).items[0].requester_affiliation, '작성 당시 회사');
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
});
|
||||
test('disabled deployment works without organization credentials and enabled deployment requires both', () => {
|
||||
assert.equal(deploymentOrgSettings({}).vars.ORG_CONTEXT_ENABLED, 'false');
|
||||
assert.deepEqual(deploymentOrgSettings({}).secrets, {});
|
||||
assert.deepEqual(deploymentOrgSettings({ ORG_CONTEXT_ENABLED: 'false', ORG_CONTEXT_KEY_ID: 'partial-setup' }).secrets, {});
|
||||
assert.throws(() => deploymentOrgSettings({ ORG_CONTEXT_ENABLED: 'true' }), /invalid_tenants/);
|
||||
const result = deploymentOrgSettings(configuration());
|
||||
assert.equal(result.vars.ORG_CONTEXT_EXPOSURE, 'identifiers');
|
||||
assert.equal(result.vars.ORG_CONTEXT_KEY_SECRET, undefined);
|
||||
assert.ok(result.secrets.ORG_CONTEXT_KEY_SECRET);
|
||||
});
|
||||
|
||||
test('OAuth login still completes and uses explicit SSO company instead of email inference', async () => {
|
||||
const env = configuration({ ORG_CONTEXT_ENABLED: 'false', OAUTH_COOKIE_NAME: 'fixture_oauth', AUTH_CLIENT_ID: 'fixture-client',
|
||||
AUTH_AUTHORIZE_URL: 'https://identity.example.test/authorize', AUTH_TOKEN_URL: 'https://identity.example.test/token',
|
||||
AUTH_USERINFO_URL: 'https://identity.example.test/userinfo', AUTH_REDIRECT_URI: 'https://qa.example.test/auth/callback' });
|
||||
const start = await worker.fetch(new Request('https://qa.example.test/auth/login?return_url=%2Fegbim%2Fwrite.html'), env);
|
||||
assert.equal(start.status, 302);
|
||||
const authorize = new URL(start.headers.get('location'));
|
||||
const stateCookie = start.headers.getSetCookie()[0].split(';')[0];
|
||||
const claims = { sub: USER, nonce: authorize.searchParams.get('nonce'), tenant_id: ROOT, email: 'fixture@brsw.kr', name: '테스트 사용자', company: 'SSO 회사', tenants: [{ id: ROOT, name: 'SSO 부서', ancestors: [{ id: COMPANY, type: 'COMPANY', name: 'SSO 회사' }] }] };
|
||||
const token = Buffer.from('{}').toString('base64url') + '.' + Buffer.from(JSON.stringify(claims)).toString('base64url') + '.fixture';
|
||||
await mockedFetch(async (url) => String(url).endsWith('/token') ? json({ id_token: token, access_token: 'fixture-token' }) : json(claims), async () => {
|
||||
const callback = new URL(env.AUTH_REDIRECT_URI);
|
||||
callback.searchParams.set('state', authorize.searchParams.get('state'));
|
||||
callback.searchParams.set('code', 'fixture-code');
|
||||
const result = await worker.fetch(new Request(callback, { headers: { cookie: stateCookie } }), env);
|
||||
assert.equal(result.status, 302);
|
||||
assert.equal(result.headers.get('location'), 'https://qa.example.test/egbim/write.html');
|
||||
const cookie = result.headers.getSetCookie().find((value) => value.startsWith(env.SESSION_COOKIE_NAME + '=')).split(';')[0];
|
||||
const session = await worker.fetch(new Request('https://qa.example.test/auth/session', { headers: { cookie } }), env);
|
||||
const { user } = await session.json();
|
||||
assert.equal(user.requesterId, USER);
|
||||
assert.equal(user.requesterTenantId, ROOT);
|
||||
assert.equal(user.company, 'SSO 회사');
|
||||
assert.equal(user.department, 'SSO 부서');
|
||||
});
|
||||
});
|
||||
|
||||
test('feedback update keeps verified owner headers and cannot overwrite organization or ownership from browser input', async () => {
|
||||
const env = configuration();
|
||||
await mockedFetch(async (url, init) => {
|
||||
assert.ok(!String(url).includes('org-context'));
|
||||
assert.equal(init.headers['x-requester-id'], USER);
|
||||
assert.equal(init.headers['x-requester-tenant-id'], ROOT);
|
||||
const body = JSON.parse(init.body);
|
||||
assert.equal(body.requester_id, undefined);
|
||||
assert.equal(body.requester_affiliation, undefined);
|
||||
assert.equal(body.title, '수정 제목');
|
||||
return json({ id: FEEDBACK });
|
||||
}, async () => {
|
||||
const response = await worker.fetch(new Request('https://qa.example.test/api/feedbacks/' + FEEDBACK, {
|
||||
method: 'PATCH', headers: { cookie: sessionCookie(env), 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ title: '수정 제목', requester_id: 'forged', requester_affiliation: 'forged' })
|
||||
}), env);
|
||||
assert.equal(response.status, 200);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user