@networkpro/web 1.14.3 → 1.15.1

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.
Files changed (38) hide show
  1. package/.node-version +1 -1
  2. package/.nvmrc +1 -1
  3. package/CHANGELOG.md +87 -2
  4. package/README.md +6 -4
  5. package/_redirects +4 -2
  6. package/netlify.toml +0 -5
  7. package/package.json +14 -15
  8. package/scripts/testRedirects.js +84 -0
  9. package/src/app.html +1 -1
  10. package/src/hooks.server.js +20 -1
  11. package/src/lib/components/RedirectPage.svelte +2 -34
  12. package/src/lib/components/layout/HeaderDefault.svelte +8 -3
  13. package/src/lib/components/layout/HeaderHome.svelte +8 -3
  14. package/src/lib/pages/AboutContent.svelte +1 -1
  15. package/src/lib/pages/LicenseContent.svelte +1 -2
  16. package/src/lib/pages/PrivacyDashboard.svelte +0 -1
  17. package/src/lib/styles/css/default.css +22 -0
  18. package/src/lib/styles/global.min.css +1 -1
  19. package/src/lib/utils/redirect.js +52 -0
  20. package/src/lib/utils/utm.js +1 -0
  21. package/src/routes/consultation/+page.svelte +16 -2
  22. package/src/routes/contact/+page.svelte +2 -4
  23. package/src/routes/links/+page.svelte +2 -4
  24. package/src/routes/posts/+page.svelte +2 -4
  25. package/src/routes/privacy-rights/+page.svelte +2 -4
  26. package/static/sitemap.xml +4 -4
  27. package/tests/unit/{unregisterServiceWorker.test.js → client/lib/unregisterServiceWorker.test.js} +2 -2
  28. package/tests/unit/client/lib/utils/redirect.test.js +80 -0
  29. package/tests/unit/{utm.test.js → client/lib/utils/utm.test.js} +1 -1
  30. package/tests/unit/{routes → client/routes}/page.svelte.test.js +2 -2
  31. package/tests/unit/{checkEnv.test.js → server/checkEnv.test.js} +2 -2
  32. package/tests/unit/{checkVersions.test.js → server/checkVersions.test.js} +2 -2
  33. package/tests/{internal → unit/server/internal}/auditCoverage.test.js +13 -6
  34. package/tests/unit/{lib → server/lib}/utils/purify.test.js +2 -2
  35. package/vitest.config.client.js +1 -4
  36. package/vitest.config.server.js +1 -1
  37. package/netlify/edge-functions/csp-report.js +0 -160
  38. package/tests/unit/csp-report.test.js +0 -81
@@ -0,0 +1,52 @@
1
+ /* ==========================================================================
2
+ src/lib/utils/redirect.js
3
+
4
+ Copyright © 2025 Network Pro Strategies (Network Pro™)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * @file utm.js
11
+ * @description Determines if the current browser is Firefox and skips redirect
12
+ * visual if true.
13
+ *
14
+ * @module src/lib/utils/
15
+ * @author SunDevil311
16
+ * @updated 2025-07-01
17
+ */
18
+
19
+ /**
20
+ * Checks whether the current browser is Firefox.
21
+ * @returns {boolean} True if the browser is Firefox.
22
+ */
23
+ function isFirefox() {
24
+ return (
25
+ typeof navigator !== 'undefined' &&
26
+ navigator.userAgent.toLowerCase().includes('firefox')
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Redirects to a URL, immediately in Firefox (to avoid popup heuristics),
32
+ * and with a visual delay in all other browsers.
33
+ *
34
+ * @param {string} to - Destination URL
35
+ * @param {number} delay - Delay in seconds (ignored for Firefox)
36
+ */
37
+ export function redirectWithBrowserAwareness(to, delay = 2) {
38
+ if (!to) {
39
+ console.warn('⛔ No redirect target provided');
40
+ return;
41
+ }
42
+
43
+ if (isFirefox()) {
44
+ console.log('🦊 Firefox detected — redirecting immediately');
45
+ window.location.replace(to);
46
+ } else {
47
+ console.log(`✅ Delayed redirect to: ${to} after ${delay}s`);
48
+ setTimeout(() => {
49
+ window.location.replace(to);
50
+ }, delay * 1000);
51
+ }
52
+ }
@@ -34,6 +34,7 @@ export function appendUTM(url) {
34
34
  else if (pathname.startsWith('/links')) campaign = 'links';
35
35
  else if (pathname.startsWith('/posts')) campaign = 'posts';
36
36
  else if (pathname.startsWith('/privacy-rights')) campaign = 'prights';
37
+ else if (pathname.startsWith('/consultation')) campaign = 'consult';
37
38
  // add more if needed
38
39
 
39
40
  const utmParams = new URLSearchParams({
@@ -9,6 +9,10 @@ This file is part of Network Pro.
9
9
  <script>
10
10
  import RedirectPage from '$lib/components/RedirectPage.svelte';
11
11
  import { appendUTM } from '$lib/utils/utm.js';
12
+ import { getUTMParams } from '$lib/utils/getUTMParams.js';
13
+ import { trackingEnabled } from '$lib/stores/trackingPreferences';
14
+ import posthog from 'posthog-js';
15
+ import { get } from 'svelte/store';
12
16
  import { onMount } from 'svelte';
13
17
  import { browser } from '$app/environment';
14
18
 
@@ -21,10 +25,20 @@ This file is part of Network Pro.
21
25
  onMount(() => {
22
26
  if (!browser) return;
23
27
 
24
- target = appendUTM(
28
+ const url = appendUTM(
25
29
  'https://cloud.neteng.pro/index.php/apps/appointments/pub/8clCqQrt3AtGbNrr/form',
26
30
  );
27
- show = true;
31
+
32
+ if (get(trackingEnabled)) {
33
+ const utm = getUTMParams(url);
34
+ posthog.capture('redirect_to_consult', {
35
+ target_url: url,
36
+ ...utm,
37
+ });
38
+ }
39
+
40
+ target = url;
41
+ show = true; // Immediately show RedirectPage
28
42
  });
29
43
  </script>
30
44
 
@@ -37,10 +37,8 @@ This file is part of Network Pro.
37
37
  });
38
38
  }
39
39
 
40
- setTimeout(() => {
41
- target = url;
42
- show = true;
43
- }, 150);
40
+ target = url;
41
+ show = true; // Immediately show RedirectPage
44
42
  });
45
43
  </script>
46
44
 
@@ -40,10 +40,8 @@ This file is part of Network Pro.
40
40
  });
41
41
  }
42
42
 
43
- setTimeout(() => {
44
- target = url;
45
- show = true;
46
- }, 150);
43
+ target = url;
44
+ show = true; // Immediately show RedirectPage
47
45
  });
48
46
  </script>
49
47
 
@@ -40,10 +40,8 @@ This file is part of Network Pro.
40
40
  });
41
41
  }
42
42
 
43
- setTimeout(() => {
44
- target = url;
45
- show = true;
46
- }, 150);
43
+ target = url;
44
+ show = true; // Immediately show RedirectPage
47
45
  });
48
46
  </script>
49
47
 
@@ -37,10 +37,8 @@ This file is part of Network Pro.
37
37
  });
38
38
  }
39
39
 
40
- setTimeout(() => {
41
- target = url;
42
- show = true;
43
- }, 150);
40
+ target = url;
41
+ show = true; // Immediately show RedirectPage
44
42
  });
45
43
  </script>
46
44
 
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <!-- Sitemap last updated on 2025-06-11 -->
2
+ <!-- Sitemap last updated on July 12, 2025 -->
3
3
 
4
4
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
5
5
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  <loc>https://netwk.pro</loc>
9
9
 
10
- <lastmod>2025-06-10</lastmod>
10
+ <lastmod>2025-07-12</lastmod>
11
11
 
12
12
  <changefreq>weekly</changefreq>
13
13
 
@@ -43,7 +43,7 @@
43
43
 
44
44
  <loc>https://netwk.pro/privacy-dashboard</loc>
45
45
 
46
- <lastmod>2025-05-28</lastmod>
46
+ <lastmod>2025-06-30</lastmod>
47
47
 
48
48
  <changefreq>monthly</changefreq>
49
49
 
@@ -55,7 +55,7 @@
55
55
 
56
56
  <loc>https://netwk.pro/privacy</loc>
57
57
 
58
- <lastmod>2025-06-02</lastmod>
58
+ <lastmod>2025-06-30</lastmod>
59
59
 
60
60
  <changefreq>monthly</changefreq>
61
61
 
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/unregisterServiceWorker.test.js
2
+ tests/unit/client/lib/unregisterServiceWorker.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -7,7 +7,7 @@ This file is part of Network Pro.
7
7
  ========================================================================== */
8
8
 
9
9
  import { beforeEach, describe, expect, it, vi } from 'vitest';
10
- import { unregisterServiceWorker } from '../../src/lib/unregisterServiceWorker.js';
10
+ import { unregisterServiceWorker } from '../../../../src/lib/unregisterServiceWorker.js';
11
11
 
12
12
  describe('unregisterServiceWorker()', () => {
13
13
  beforeEach(() => {
@@ -0,0 +1,80 @@
1
+ /* ==========================================================================
2
+ tests/unit/client/lib/utils/redirect.test.js
3
+
4
+ Copyright © 2025 Network Pro Strategies (Network Pro™)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * @file redirect.test.js
11
+ * @description Unit test for src/lib/utils/redirect.js
12
+ * @module tests/unit/lib/util/
13
+ * @author SunDevil311
14
+ * @updated 2025-07-01
15
+ */
16
+
17
+ import { redirectWithBrowserAwareness } from '$lib/utils/redirect.js';
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
19
+
20
+ describe('redirectWithBrowserAwareness', () => {
21
+ let originalLocation;
22
+ let originalNavigator;
23
+
24
+ /** @type {{ url: string; delay: number }} */
25
+ const commonMocks = {
26
+ url: 'https://example.com',
27
+ delay: 1,
28
+ };
29
+
30
+ beforeEach(() => {
31
+ // Preserve globals
32
+ originalLocation = window.location;
33
+ originalNavigator = global.navigator;
34
+
35
+ // Stub window.location.replace
36
+ delete window.location;
37
+ window.location = {
38
+ replace: vi.fn(),
39
+ };
40
+ });
41
+
42
+ afterEach(() => {
43
+ // Restore globals
44
+ window.location = originalLocation;
45
+ global.navigator = originalNavigator;
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ it('should redirect immediately in Firefox', () => {
50
+ global.navigator = {
51
+ userAgent:
52
+ 'Mozilla/5.0 (Windows NT 10.0; rv:99.0) Gecko/20100101 Firefox/99.0',
53
+ };
54
+
55
+ redirectWithBrowserAwareness(commonMocks.url, commonMocks.delay);
56
+
57
+ expect(window.location.replace).toHaveBeenCalledWith(commonMocks.url);
58
+ });
59
+
60
+ it('should redirect after delay in non-Firefox', () => {
61
+ vi.useFakeTimers();
62
+
63
+ global.navigator = {
64
+ userAgent:
65
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0 Safari/537.36',
66
+ };
67
+
68
+ redirectWithBrowserAwareness(commonMocks.url, commonMocks.delay);
69
+
70
+ expect(window.location.replace).not.toHaveBeenCalled();
71
+
72
+ vi.advanceTimersByTime(commonMocks.delay * 1000);
73
+
74
+ expect(window.location.replace).toHaveBeenCalledWith(commonMocks.url);
75
+
76
+ vi.useRealTimers();
77
+ });
78
+ });
79
+
80
+ // cspell:ignore khtml
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/utm.test.js
2
+ tests/unit/client/lib/utils/utm.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/routes/page.svelte.test.js
2
+ tests/unit/client/routes/page.svelte.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -9,7 +9,7 @@ This file is part of Network Pro.
9
9
  import '@testing-library/jest-dom/vitest';
10
10
  import { render, screen } from '@testing-library/svelte';
11
11
  import { describe, expect, test } from 'vitest';
12
- import Page from '../../../src/routes/+page.svelte';
12
+ import Page from '../../../../src/routes/+page.svelte';
13
13
 
14
14
  describe('/+page.svelte', () => {
15
15
  test('should render the home page section', () => {
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/checkEnv.test.js
2
+ tests/unit/server/checkEnv.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -11,7 +11,7 @@ This file is part of Network Pro.
11
11
  */
12
12
 
13
13
  import { afterEach, describe, expect, it } from 'vitest';
14
- import { checkEnv } from '../../scripts/checkEnv.js';
14
+ import { checkEnv } from '../../../scripts/checkEnv.js';
15
15
 
16
16
  describe('checkEnv()', () => {
17
17
  const originalEnv = process.env.ENV_MODE;
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/checkVersions.test.js
2
+ tests/unit/server/checkVersions.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -11,7 +11,7 @@ This file is part of Network Pro.
11
11
  */
12
12
 
13
13
  import { describe, expect, it } from 'vitest';
14
- import { checkVersions } from '../../scripts/checkVersions.js';
14
+ import { checkVersions } from '../../../scripts/checkVersions.js';
15
15
 
16
16
  describe('checkVersions()', () => {
17
17
  it('should match current Node and NPM versions to engine ranges', () => {
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/internal/auditCoverage.test.js
2
+ tests/unit/server/internal/auditCoverage.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -8,10 +8,11 @@ This file is part of Network Pro.
8
8
 
9
9
  /**
10
10
  * @file auditCoverage.test.js
11
- * @description Scans all .js files in src/ and scripts/ for matching unit test
11
+ * @description Scans all .js files in src/ and scripts/ for matching unit
12
+ * tests
12
13
  * @module tests/internal
13
14
  * @author SunDevil311
14
- * @updated 2025-06-01
15
+ * @updated 2025-07-01
15
16
  */
16
17
 
17
18
  import fs from 'fs';
@@ -60,20 +61,26 @@ describe('auditCoverage', () => {
60
61
  const allFiles = [...srcFiles, ...scriptsFiles].map((f) =>
61
62
  path
62
63
  .relative(process.cwd(), f)
63
- .replace(/\\/g, '/') // Normalize Windows slashes
64
+ .replace(/\\/g, '/')
64
65
  .replace(/^src\//, '')
65
66
  .replace(/^scripts\//, '')
66
67
  .replace(/\.js$/, ''),
67
68
  );
68
69
 
69
- const testFiles = getAllJsFiles(path.resolve('tests/unit'), {
70
+ const clientTests = getAllJsFiles(path.resolve('tests/unit/client'), {
71
+ includeTests: true,
72
+ });
73
+ const serverTests = getAllJsFiles(path.resolve('tests/unit/server'), {
70
74
  includeTests: true,
71
75
  });
76
+ const testFiles = [...clientTests, ...serverTests];
72
77
 
73
78
  const testFilesNormalized = testFiles.map((f) =>
74
79
  path
75
- .relative(path.resolve('tests/unit'), f)
80
+ .relative(process.cwd(), f)
76
81
  .replace(/\\/g, '/')
82
+ .replace(/^tests\/unit\/client\//, '')
83
+ .replace(/^tests\/unit\/server\//, '')
77
84
  .replace(/\.test\.js$|\.spec\.js$/, ''),
78
85
  );
79
86
 
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/lib/utils/purify.test.js
2
+ tests/unit/server/lib/utils/purify.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -15,7 +15,7 @@ This file is part of Network Pro.
15
15
  */
16
16
 
17
17
  import { describe, expect, it } from 'vitest';
18
- import { sanitizeHtml } from '../../../../src/lib/utils/purify.js';
18
+ import { sanitizeHtml } from '../../../../../src/lib/utils/purify.js';
19
19
 
20
20
  describe('sanitizeHtml', () => {
21
21
  it('removes dangerous tags like <script>', async () => {
@@ -26,10 +26,7 @@ export default defineConfig({
26
26
  name: 'client',
27
27
  environment: 'jsdom',
28
28
  clearMocks: true,
29
- include: [
30
- 'tests/unit/**/*.test.{js,mjs,svelte}',
31
- 'tests/internal/**/*.test.{js,mjs,svelte}',
32
- ],
29
+ include: ['tests/unit/client/**/*.test.{js,mjs,svelte}'],
33
30
  exclude: [],
34
31
  setupFiles: ['./vitest-setup-client.js'],
35
32
  reporters: ['default', 'json'],
@@ -23,7 +23,7 @@ export default defineConfig({
23
23
  test: {
24
24
  name: 'server',
25
25
  environment: 'node',
26
- include: ['tests/unit/**/*.test.{js,mjs}'],
26
+ include: ['tests/unit/server/**/*.test.{js,mjs}'],
27
27
  exclude: ['tests/unit/**/*.svelte.test.{js,mjs}'],
28
28
  reporters: ['default', 'json'],
29
29
  testTimeout: 10000,
@@ -1,160 +0,0 @@
1
- /* ==========================================================================
2
- edge-functions/csp-report.js
3
-
4
- Copyright © 2025 Network Pro Strategies (Network Pro™)
5
- SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
- This file is part of Network Pro.
7
- ========================================================================== */
8
-
9
- /**
10
- * @file csp-report.js
11
- * @description Netlify Edge Function to handle CSP violation reports.
12
- *
13
- * Accepts POST requests to /api/csp-report and logs relevant CSP reports.
14
- * Filters out common low-value reports (e.g., img-src) to reduce invocation
15
- * cost. Alerts on high-risk violations via ntfy topic.
16
- *
17
- * @module netlify/edge-functions
18
- * @author SunDevil311
19
- * @updated 2025-05-31
20
- */
21
-
22
- /**
23
- * Netlify Edge Function entry point for CSP reporting.
24
- *
25
- * @param {Request} request - The incoming HTTP request object
26
- * @param {import('@netlify/edge-functions').Context} _context - The Netlify Edge Function context (unused)
27
- * @returns {Promise<Response>} HTTP Response with status 204 or 405
28
- */
29
- export default async (request, _context) => {
30
- if (request.method !== 'POST') {
31
- return new Response('Method Not Allowed', { status: 405 });
32
- }
33
-
34
- try {
35
- const body = await request.json();
36
- const report = body['csp-report'];
37
-
38
- // Ignore if report is missing or malformed
39
- if (!report || typeof report !== 'object') {
40
- return new Response(null, { status: 204 });
41
- }
42
-
43
- const violated = report['violated-directive'] ?? '';
44
- const blockedUri = report['blocked-uri'] ?? '';
45
-
46
- // Filter: Skip noisy or unactionable reports
47
- const ignored = [
48
- violated.startsWith('img-src'),
49
- blockedUri === '',
50
- blockedUri === 'eval',
51
- blockedUri === 'about',
52
- blockedUri.startsWith('chrome-extension://'),
53
- blockedUri.startsWith('moz-extension://'),
54
- !report['source-file'],
55
- !report['document-uri'],
56
- ].some(Boolean);
57
-
58
- if (ignored) {
59
- console.log('[CSP-Edge] Ignored low-value violation:', {
60
- directive: violated,
61
- uri: blockedUri,
62
- });
63
- return new Response(null, { status: 204 });
64
- }
65
-
66
- // Send alert for high-risk directives
67
- await sendToNtfy(violated, blockedUri, report);
68
-
69
- // Log useful violations
70
- console.log('[CSP-Edge] Violation:', {
71
- directive: violated,
72
- uri: blockedUri,
73
- referrer: report['referrer'],
74
- source: report['source-file'],
75
- line: report['line-number'],
76
- });
77
- } catch (err) {
78
- console.warn('[CSP-Edge] Failed to parse CSP report:', err.message);
79
- }
80
-
81
- return new Response(null, { status: 204 });
82
- };
83
-
84
- const recentViolations = new Map();
85
- const VIOLATION_TTL_MS = 60_000;
86
-
87
- /**
88
- * Sends a high-priority alert to your ntfy topic for high-risk CSP violations.
89
- * Applies rate-limiting to avoid sending duplicate alerts within 60 seconds.
90
- *
91
- * @param {string} violated - The violated CSP directive
92
- * @param {string} blockedUri - The URI that was blocked
93
- * @param {Record<string, any>} report - The full CSP report object
94
- */
95
- async function sendToNtfy(violated, blockedUri, report) {
96
- const highRiskDirectives = [
97
- 'script-src',
98
- 'form-action',
99
- 'frame-ancestors',
100
- 'base-uri',
101
- ];
102
-
103
- const directiveKey = violated.split(' ')[0]; // strip fallback values or sources
104
- const isHighRisk = highRiskDirectives.includes(directiveKey);
105
- console.log(
106
- `[CSP-Edge] Directive ${directiveKey} is ${isHighRisk ? '' : 'not '}high-risk`,
107
- );
108
- if (!isHighRisk) return;
109
-
110
- const key = `${violated}|${blockedUri}`;
111
- const now = Date.now();
112
-
113
- // Skip and log if violation was reported recently
114
- if (
115
- recentViolations.has(key) &&
116
- now - recentViolations.get(key) < VIOLATION_TTL_MS
117
- ) {
118
- console.log(`[CSP-Edge] Skipped duplicate alert for ${key}`);
119
- return;
120
- }
121
-
122
- // Record the current timestamp
123
- recentViolations.set(key, now);
124
-
125
- // Cleanup old entries (memory-safe for low volume)
126
- for (const [k, t] of recentViolations.entries()) {
127
- if (now - t > VIOLATION_TTL_MS) {
128
- recentViolations.delete(k);
129
- }
130
- }
131
-
132
- const topicUrl = 'https://ntfy.neteng.pro/csp-alerts';
133
-
134
- const message = [
135
- `🚨 CSP Violation Detected`,
136
- `Directive: ${violated}`,
137
- `Blocked URI: ${blockedUri}`,
138
- `Referrer: ${report.referrer || 'N/A'}`,
139
- `Source: ${report['source-file'] || 'N/A'}`,
140
- `Line: ${report['line-number'] || 'N/A'}`,
141
- ].join('\n');
142
-
143
- await fetch(topicUrl, {
144
- method: 'POST',
145
- headers: {
146
- 'Content-Type': 'text/plain',
147
- 'X-Title': 'High-Risk CSP Violation',
148
- 'X-Priority': '5',
149
- },
150
- body: message,
151
- });
152
- }
153
-
154
- /**
155
- * Configuration block for the Edge Function.
156
- * This sets the endpoint route to /api/csp-report
157
- */
158
- export const config = {
159
- path: '/api/csp-report',
160
- };
@@ -1,81 +0,0 @@
1
- /* ==========================================================================
2
- tests/unit/csp-report.test.js
3
-
4
- Copyright © 2025 Network Pro Strategies (Network Pro™)
5
- SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
- This file is part of Network Pro.
7
- ========================================================================== */
8
-
9
- /**
10
- * Tests the edge-functions/csp-report.js CSP reporting endpoint
11
- *
12
- * @module tests/unit
13
- * @author SunDevil311
14
- * @updated 2025-05-31
15
- */
16
-
17
- /** @file Unit tests for edge-functions/csp-report.js using Vitest */
18
- /** @typedef {import('vitest').TestContext} TestContext */
19
-
20
- import { beforeEach, describe, expect, it, vi } from 'vitest';
21
- import handler from '../../netlify/edge-functions/csp-report.js';
22
-
23
- // 🧪 Mock fetch used by sendToNtfy inside the Edge Function
24
- global.fetch = vi.fn(() =>
25
- Promise.resolve({ ok: true, status: 200, text: () => 'OK' }),
26
- );
27
-
28
- describe('csp-report.js', () => {
29
- beforeEach(() => {
30
- vi.clearAllMocks();
31
- });
32
-
33
- it('should handle a valid CSP report', async () => {
34
- const req = new Request('http://localhost/api/csp-report', {
35
- method: 'POST',
36
- headers: { 'Content-Type': 'application/json' },
37
- body: JSON.stringify({
38
- 'csp-report': {
39
- 'document-uri': 'https://example.com',
40
- 'violated-directive': 'script-src',
41
- 'blocked-uri': 'https://malicious.site',
42
- },
43
- }),
44
- });
45
-
46
- const res = await handler(req, {});
47
- expect(res.status).toBe(204);
48
- });
49
-
50
- it('should reject non-POST requests', async () => {
51
- const req = new Request('http://localhost/api/csp-report', {
52
- method: 'GET',
53
- });
54
-
55
- const res = await handler(req, {});
56
- const text = await res.text();
57
- expect(res.status).toBe(405);
58
- expect(text).toContain('Method Not Allowed');
59
- });
60
-
61
- it('should handle malformed JSON', async () => {
62
- const badJson = '{ invalid json }';
63
- const req = new Request('http://localhost/api/csp-report', {
64
- method: 'POST',
65
- headers: { 'Content-Type': 'application/json' },
66
- body: badJson,
67
- });
68
-
69
- const res = await handler(req, {});
70
- expect(res.status).toBe(204); // The current handler swallows errors silently
71
- });
72
-
73
- it('should handle missing body', async () => {
74
- const req = new Request('http://localhost/api/csp-report', {
75
- method: 'POST',
76
- });
77
-
78
- const res = await handler(req, {});
79
- expect(res.status).toBe(204); // No body is also treated silently
80
- });
81
- });