proposal-approve-rbac-fix-e2e.test.mjs
114 lines 5.2 KB
Raw
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor ⚠ breaking 10 hours ago
1 /**
2 * E2E tests — proposal approve RBAC fix.
3 *
4 * Verifies the gateway's actual assertHostedProposalApproveDiscard behavior and the
5 * hub.js approveProposal error path at the structural (source-code wiring) level.
6 * These tests confirm the fix is fully wired end-to-end without requiring a live server.
7 */
8
9 import { test, describe } from 'node:test';
10 import assert from 'node:assert/strict';
11 import fs from 'node:fs';
12 import path from 'node:path';
13 import { fileURLToPath } from 'node:url';
14
15 const __dirname = path.dirname(fileURLToPath(import.meta.url));
16 const ROOT = path.resolve(__dirname, '..');
17
18 describe('E2E: assertHostedProposalApproveDiscard wiring', () => {
19 let src;
20 const load = () => {
21 if (!src) src = fs.readFileSync(path.join(ROOT, 'hub/gateway/server.mjs'), 'utf8');
22 return src;
23 };
24
25 test('assertHostedProposalApproveDiscard calls resolveHostedActorRole', () => {
26 const s = load();
27 const fnStart = s.indexOf('async function assertHostedProposalApproveDiscard');
28 assert.ok(fnStart > 0, 'assertHostedProposalApproveDiscard exists');
29 const fnEnd = s.indexOf('\n}\n', fnStart);
30 const fn = s.slice(fnStart, fnEnd + 3);
31 assert.ok(fn.includes('resolveHostedActorRole'), 'resolveHostedActorRole called inside assertHostedProposalApproveDiscard');
32 });
33
34 test('canApprove check includes admin and evaluator paths', () => {
35 const s = load();
36 const fnStart = s.indexOf('async function assertHostedProposalApproveDiscard');
37 const fn = s.slice(fnStart, s.indexOf('\n}\n', fnStart) + 3);
38 assert.ok(fn.includes("role === 'admin'"), 'admin role allows approve');
39 assert.ok(fn.includes('mayApproveProposals'), 'evaluator mayApproveProposals checked');
40 });
41
42 test('getUserId check prevents unauthenticated approve (401)', () => {
43 const s = load();
44 const fnStart = s.indexOf('async function assertHostedProposalApproveDiscard');
45 const fn = s.slice(fnStart, s.indexOf('\n}\n', fnStart) + 3);
46 assert.ok(fn.includes('getUserId'), '401 guard uses getUserId');
47 assert.ok(fn.includes('401'), '401 response when uid is missing');
48 });
49
50 test('403 returned with code FORBIDDEN when canApprove is false', () => {
51 const s = load();
52 const fnStart = s.indexOf('async function assertHostedProposalApproveDiscard');
53 const fn = s.slice(fnStart, s.indexOf('\n}\n', fnStart) + 3);
54 assert.ok(fn.includes('403'), '403 returned for unauthorized approve');
55 assert.ok(fn.includes("'FORBIDDEN'"), 'FORBIDDEN code in 403 response');
56 });
57
58 test('proxyToCanister calls assertHostedProposalApproveDiscard before proxying', () => {
59 const s = load();
60 const fnStart = s.indexOf('async function proxyToCanister');
61 const fn = s.slice(fnStart, s.indexOf('\n}\n', fnStart) + 3);
62 assert.ok(fn.includes('assertHostedProposalApproveDiscard'), 'RBAC gate is called before proxy');
63 });
64 });
65
66 describe('E2E: hub.js approve flow completeness', () => {
67 let src;
68 const load = () => {
69 if (!src) src = fs.readFileSync(path.join(ROOT, 'web/hub/hub.js'), 'utf8');
70 return src;
71 };
72
73 test('approveProposal sends POST to the correct proposals approve endpoint', () => {
74 const s = load();
75 const fnStart = s.indexOf('async function approveProposal');
76 const fn = s.slice(fnStart, s.indexOf('\n async function discardProposal', fnStart));
77 assert.ok(fn.includes("'/api/v1/proposals/'"), 'proposals API path used');
78 assert.ok(fn.includes("'/approve'"), 'approve path segment used');
79 assert.ok(fn.includes("method: 'POST'"), 'POST method used');
80 });
81
82 test('withButtonBusy wraps the API call (spinner behavior)', () => {
83 const s = load();
84 const fnStart = s.indexOf('async function approveProposal');
85 const fn = s.slice(fnStart, s.indexOf('\n async function discardProposal', fnStart));
86 assert.ok(fn.includes("withButtonBusy(btn, 'Approving"), 'withButtonBusy used for approveProposal spinner');
87 });
88
89 test('success path: hideDetailPanelChrome called after approval, before error handling', () => {
90 const s = load();
91 const fnStart = s.indexOf('async function approveProposal');
92 const fn = s.slice(fnStart, s.indexOf('\n async function discardProposal', fnStart));
93 const successIdx = fn.indexOf('hideDetailPanelChrome');
94 const catchIdx = fn.indexOf('} catch (e)');
95 assert.ok(successIdx < catchIdx, 'hideDetailPanelChrome is in success path before catch');
96 });
97
98 test('error path: showToast is called in catch (error is always visible)', () => {
99 const s = load();
100 const fnStart = s.indexOf('async function approveProposal');
101 const fn = s.slice(fnStart, s.indexOf('\n async function discardProposal', fnStart));
102 const catchIdx = fn.indexOf('} catch (e)');
103 const toastIdx = fn.indexOf('showToast', catchIdx);
104 assert.ok(catchIdx > 0 && toastIdx > catchIdx, 'showToast called inside catch block');
105 });
106
107 test('approval_log_written partial-success path preserved', () => {
108 const s = load();
109 const fnStart = s.indexOf('async function approveProposal');
110 const fn = s.slice(fnStart, s.indexOf('\n async function discardProposal', fnStart));
111 assert.ok(fn.includes('approval_log_written'), 'partial approve log warning preserved');
112 assert.ok(fn.includes('approval_log_error'), 'approval_log_error warning preserved');
113 });
114 });
File History 1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor 10 hours ago