snice 3.7.0 → 3.8.0

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 (51) hide show
  1. package/README.md +2 -2
  2. package/bin/snice.js +3 -5
  3. package/bin/templates/CLAUDE.md +5 -5
  4. package/bin/templates/pwa/README.md +31 -16
  5. package/bin/templates/pwa/index.html +0 -1
  6. package/bin/templates/pwa/package.json +9 -2
  7. package/bin/templates/pwa/src/fetcher.ts +15 -0
  8. package/bin/templates/pwa/src/guards/auth.ts +6 -4
  9. package/bin/templates/pwa/src/middleware/auth.ts +7 -6
  10. package/bin/templates/pwa/src/middleware/error.ts +16 -5
  11. package/bin/templates/pwa/src/middleware/retry.ts +7 -3
  12. package/bin/templates/pwa/src/pages/dashboard.ts +4 -3
  13. package/bin/templates/pwa/src/pages/login.ts +7 -7
  14. package/bin/templates/pwa/src/pages/notifications.ts +2 -2
  15. package/bin/templates/pwa/src/pages/profile.ts +9 -8
  16. package/bin/templates/pwa/src/router.ts +8 -4
  17. package/bin/templates/pwa/src/types/auth.ts +1 -1
  18. package/bin/templates/pwa/tests/helpers/test-utils.ts +84 -0
  19. package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
  20. package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
  21. package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
  22. package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
  23. package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
  24. package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
  25. package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.esm.js +1 -1
  28. package/dist/index.iife.js +1 -1
  29. package/dist/symbols.cjs +1 -1
  30. package/dist/symbols.esm.js +1 -1
  31. package/dist/transitions.cjs +1 -1
  32. package/dist/transitions.esm.js +1 -1
  33. package/docs/ai/patterns.md +1 -1
  34. package/docs/routing.md +9 -8
  35. package/package.json +1 -1
  36. package/bin/templates/pwa/public/manifest.json +0 -24
  37. package/bin/templates/pwa/src/utils/fetch.ts +0 -39
  38. package/bin/templates/social/README.md +0 -42
  39. package/bin/templates/social/global.d.ts +0 -14
  40. package/bin/templates/social/index.html +0 -13
  41. package/bin/templates/social/package.json +0 -21
  42. package/bin/templates/social/public/vite.svg +0 -1
  43. package/bin/templates/social/src/main.ts +0 -33
  44. package/bin/templates/social/src/pages/feed-page.ts +0 -111
  45. package/bin/templates/social/src/pages/messages-page.ts +0 -102
  46. package/bin/templates/social/src/pages/not-found-page.ts +0 -46
  47. package/bin/templates/social/src/pages/profile-page.ts +0 -99
  48. package/bin/templates/social/src/pages/settings-page.ts +0 -119
  49. package/bin/templates/social/src/router.ts +0 -9
  50. package/bin/templates/social/src/styles/global.css +0 -156
  51. package/bin/templates/social/tsconfig.json +0 -22
package/README.md CHANGED
@@ -416,10 +416,10 @@ When you modify the application context, call `update()` to notify all subscribe
416
416
  ```typescript
417
417
  @page({ tag: 'login-page', routes: ['/login'] })
418
418
  class LoginPage extends HTMLElement {
419
- private ctx?: Context<AppContext>;
419
+ private ctx?: Context;
420
420
 
421
421
  @context()
422
- handleContext(ctx: Context<AppContext>) {
422
+ handleContext(ctx: Context) {
423
423
  this.ctx = ctx;
424
424
  this.requestRender();
425
425
  }
package/bin/snice.js CHANGED
@@ -46,14 +46,12 @@ Options:
46
46
 
47
47
  Templates:
48
48
  base - Minimal starter with counter example (default)
49
- social - Social media sample app showcasing components
50
49
  pwa - Progressive Web App with auth, middleware, and live notifications
51
50
 
52
51
  Examples:
53
52
  snice create-app my-app
54
- snice create-app my-app --template=social
55
- snice create-app --template=social my-app
56
- npx snice create-app my-app --template=social
53
+ snice create-app my-app --template=pwa
54
+ npx snice create-app my-app --template=pwa
57
55
  `);
58
56
  }
59
57
 
@@ -91,7 +89,7 @@ function createApp(projectPath, template = 'base') {
91
89
  // Check if template exists
92
90
  if (!existsSync(templateDir)) {
93
91
  console.error(`❌ Template "${template}" not found!`);
94
- console.error(`Available templates: base, social, pwa`);
92
+ console.error(`Available templates: base, pwa`);
95
93
  process.exit(1);
96
94
  }
97
95
 
@@ -27,13 +27,13 @@ src/
27
27
  ```
28
28
 
29
29
  **Separation of concerns:**
30
- - **Pages** - Orchestrate elements, handle URLs
30
+ - **Pages** - Orchestrate elements, handle URLs, most logic happens here
31
31
  - **Components** - Pure presentation, no business logic
32
- - **Controllers** - Attach to elements, add behavior
33
- - **Utils** - Pure helper functions
32
+ - **Controllers** - Attach to elements, add business behavior unsuitable for the page or components
34
33
  - **Services** - Stateless business logic, API calls
35
34
  - **Daemons** - Lifecycle-managed (WebSocket, P2P, intervals)
36
- - **Middleware** - Composable functions (auth, logging, retry)
35
+ - **Middleware** - Composable functions (auth, retry)
36
+ - **Utils** - Pure helper functions
37
37
 
38
38
  ## Decorators
39
39
 
@@ -145,7 +145,7 @@ html`
145
145
 
146
146
  **@request/@respond is NOT a service bus.** It's for element-to-element or element-to-controller communication only. Use utility functions for app-wide features.
147
147
 
148
- **Guards receive Context<T>, not T.** Check `ctx.application.property`, not `ctx.property`. Guards: `(ctx: Context<AppContext>) => boolean`
148
+ **Guards receive Context, not AppContext.** Check `ctx.application.property`, not `ctx.property`. Guards: `(ctx: Context) => boolean`
149
149
 
150
150
  **Context must be mutated then updated.** After changing `ctx.application`, call `ctx.update()` to notify subscribers. Pages need `@context()` to get context reference.
151
151
 
@@ -53,24 +53,36 @@ src/
53
53
 
54
54
  ## Architecture Patterns
55
55
 
56
- ### Middleware-Based Fetch
56
+ ### Context-Aware Fetcher
57
57
 
58
- Composable middleware for all API calls:
58
+ Built-in middleware system with context access:
59
59
 
60
60
  ```typescript
61
- import { createFetch } from './utils/fetch';
62
- import { authMiddleware } from './middleware/auth';
63
- import { errorMiddleware } from './middleware/error';
64
- import { retryMiddleware } from './middleware/retry';
65
-
66
- export const api = createFetch({
67
- baseURL: import.meta.env.VITE_API_URL,
68
- middleware: [authMiddleware, errorMiddleware, retryMiddleware()],
69
- headers: { 'Content-Type': 'application/json' }
70
- });
61
+ // fetcher.ts - Setup
62
+ import { ContextAwareFetcher } from 'snice';
63
+
64
+ const fetcher = new ContextAwareFetcher();
65
+ fetcher.use('request', authMiddleware);
66
+ fetcher.use('response', errorMiddleware);
67
+
68
+ // Middleware with context access
69
+ export async function authMiddleware(
70
+ this: Context,
71
+ request: Request,
72
+ next: () => Promise<Response>
73
+ ): Promise<Response> {
74
+ const token = getToken();
75
+ if (token) {
76
+ request.headers.set('Authorization', `Bearer ${token}`);
77
+ }
78
+ return next();
79
+ }
71
80
 
72
- // Usage
73
- const data = await api('/users').then(r => r.json());
81
+ // Usage in pages via ctx.fetch()
82
+ async loadData() {
83
+ const response = await this.ctx.fetch('/api/data');
84
+ const data = await response.json();
85
+ }
74
86
  ```
75
87
 
76
88
  ### Daemons for Lifecycle Management
@@ -112,9 +124,12 @@ export class DashboardPage extends HTMLElement {
112
124
  Access shared state via context:
113
125
 
114
126
  ```typescript
127
+ import type { Principal } from './types/auth';
128
+
115
129
  @context()
116
- handleContext(ctx: Context<AppContext>) {
117
- this.user = ctx.application.user;
130
+ handleContext(ctx: Context) {
131
+ const principal = ctx.application.principal as Principal | undefined;
132
+ this.user = principal?.user;
118
133
  }
119
134
  ```
120
135
 
@@ -6,7 +6,6 @@
6
6
  <meta name="description" content="{{projectName}} - PWA built with Snice" />
7
7
  <meta name="theme-color" content="#6366f1" />
8
8
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
9
- <link rel="manifest" href="/manifest.json" />
10
9
  <link rel="apple-touch-icon" href="/icons/icon-192.png" />
11
10
  <title>{{projectName}}</title>
12
11
  </head>
@@ -5,9 +5,12 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
- "build": "tsc && vite build",
8
+ "build": "vite build",
9
9
  "type-check": "tsc --noEmit",
10
- "preview": "vite preview"
10
+ "preview": "vite preview",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage"
11
14
  },
12
15
  "dependencies": {
13
16
  "snice": "^3.0.0"
@@ -15,11 +18,15 @@
15
18
  "devDependencies": {
16
19
  "@types/node": "^20.0.0",
17
20
  "@vite-pwa/assets-generator": "^0.2.4",
21
+ "@vitest/coverage-v8": "^1.0.4",
22
+ "@vitest/ui": "^1.0.4",
23
+ "happy-dom": "^12.10.3",
18
24
  "terser": "^5.24.0",
19
25
  "typescript": "^5.3.3",
20
26
  "unplugin-swc": "^1.5.7",
21
27
  "vite": "^5.0.10",
22
28
  "vite-plugin-pwa": "^0.17.4",
29
+ "vitest": "^1.0.4",
23
30
  "workbox-window": "^7.0.0"
24
31
  }
25
32
  }
@@ -0,0 +1,15 @@
1
+ import { ContextAwareFetcher } from 'snice';
2
+ import { authMiddleware } from './middleware/auth';
3
+ import { errorMiddleware } from './middleware/error';
4
+ import { createRetryMiddleware } from './middleware/retry';
5
+
6
+ const fetcher = new ContextAwareFetcher();
7
+
8
+ // Add request middleware (runs before fetch)
9
+ fetcher.use('request', authMiddleware);
10
+ fetcher.use('request', createRetryMiddleware());
11
+
12
+ // Add response middleware (runs after fetch)
13
+ fetcher.use('response', errorMiddleware);
14
+
15
+ export { fetcher };
@@ -1,8 +1,10 @@
1
- import type { Context } from 'snice';
2
- import type { AppContext } from '../types/auth';
1
+ import type { AppContext } from 'snice';
2
+ import type { Principal } from '../types/auth';
3
3
 
4
- export function authGuard(ctx: Context<AppContext>): boolean {
5
- if (!ctx.application.isAuthenticated) {
4
+ export function authGuard(appContext: AppContext): boolean {
5
+ const principal = appContext.principal as Principal | undefined;
6
+
7
+ if (!principal?.isAuthenticated) {
6
8
  window.location.href = '#/login';
7
9
  return false;
8
10
  }
@@ -1,14 +1,15 @@
1
- import type { Middleware } from '../utils/fetch';
1
+ import type { Context } from 'snice';
2
2
  import { getToken } from '../services/storage';
3
3
 
4
- export async function authMiddleware(url: string, options: RequestInit, next: () => Promise<Response>): Promise<Response> {
4
+ export async function authMiddleware(
5
+ this: Context,
6
+ request: Request,
7
+ next: () => Promise<Response>
8
+ ): Promise<Response> {
5
9
  const token = getToken();
6
10
 
7
11
  if (token) {
8
- options.headers = {
9
- ...options.headers,
10
- 'Authorization': `Bearer ${token}`,
11
- };
12
+ request.headers.set('Authorization', `Bearer ${token}`);
12
13
  }
13
14
 
14
15
  return next();
@@ -1,12 +1,23 @@
1
- import type { Middleware } from '../utils/fetch';
1
+ import type { Context } from 'snice';
2
+ import type { Principal } from '../types/auth';
2
3
  import { clearToken } from '../services/storage';
3
4
 
4
- export async function errorMiddleware(url: string, options: RequestInit, next: () => Promise<Response>): Promise<Response> {
5
- const response = await next();
6
-
5
+ export async function errorMiddleware(
6
+ this: Context,
7
+ response: Response,
8
+ next: () => Promise<Response>
9
+ ): Promise<Response> {
7
10
  // Handle 401 unauthorized - token expired or invalid
8
11
  if (response.status === 401) {
9
12
  clearToken();
13
+
14
+ // Update context to reflect logged out state
15
+ if (this.application.principal) {
16
+ const principal = this.application.principal as Principal;
17
+ principal.user = null;
18
+ principal.isAuthenticated = false;
19
+ }
20
+
10
21
  window.location.href = '#/login';
11
22
  throw new Error('Unauthorized - redirecting to login');
12
23
  }
@@ -21,5 +32,5 @@ export async function errorMiddleware(url: string, options: RequestInit, next: (
21
32
  throw new Error(`Request failed with status ${response.status}`);
22
33
  }
23
34
 
24
- return response;
35
+ return next();
25
36
  }
@@ -1,7 +1,11 @@
1
- import type { Middleware } from '../utils/fetch';
1
+ import type { Context } from 'snice';
2
2
 
3
- export function retryMiddleware(retries = 3, delay = 1000): Middleware {
4
- return async (url, options, next) => {
3
+ export function createRetryMiddleware(retries = 3, delay = 1000) {
4
+ return async function retryMiddleware(
5
+ this: Context,
6
+ _request: Request,
7
+ next: () => Promise<Response>
8
+ ): Promise<Response> {
5
9
  let lastError: Error;
6
10
 
7
11
  for (let i = 0; i < retries; i++) {
@@ -1,7 +1,7 @@
1
1
  import { page } from '../router';
2
2
  import { render, styles, html, css, context } from 'snice';
3
3
  import type { Placard, Context } from 'snice';
4
- import type { AppContext } from '../types/auth';
4
+ import type { Principal } from '../types/auth';
5
5
  import { authGuard } from '../guards/auth';
6
6
 
7
7
  const placard: Placard = {
@@ -17,8 +17,9 @@ export class DashboardPage extends HTMLElement {
17
17
  userName = '';
18
18
 
19
19
  @context()
20
- handleContext(ctx: Context<AppContext>) {
21
- this.userName = ctx.application.user?.name || 'User';
20
+ handleContext(ctx: Context) {
21
+ const principal = ctx.application.principal as Principal | undefined;
22
+ this.userName = principal?.user?.name || 'User';
22
23
  }
23
24
 
24
25
  @render()
@@ -1,7 +1,7 @@
1
1
  import { page } from '../router';
2
2
  import { render, styles, html, css, context } from 'snice';
3
3
  import type { Placard, Context } from 'snice';
4
- import type { AppContext } from '../types/auth';
4
+ import type { Principal } from '../types/auth';
5
5
  import { login } from '../services/auth';
6
6
 
7
7
  const placard: Placard = {
@@ -17,10 +17,10 @@ export class LoginPage extends HTMLElement {
17
17
  error = '';
18
18
  loading = false;
19
19
 
20
- private ctx?: Context<AppContext>;
20
+ private ctx?: Context;
21
21
 
22
22
  @context()
23
- handleContext(ctx: Context<AppContext>) {
23
+ handleContext(ctx: Context) {
24
24
  this.ctx = ctx;
25
25
  }
26
26
 
@@ -33,10 +33,10 @@ export class LoginPage extends HTMLElement {
33
33
  const result = await login({ email: this.email, password: this.password });
34
34
 
35
35
  // Update context with new auth state
36
- if (this.ctx) {
37
- this.ctx.application.user = result.user;
38
- this.ctx.application.isAuthenticated = true;
39
- this.ctx.update();
36
+ if (this.ctx && this.ctx.application.principal) {
37
+ const principal = this.ctx.application.principal as Principal;
38
+ principal.user = result.user;
39
+ principal.isAuthenticated = true;
40
40
  }
41
41
 
42
42
  window.location.href = '#/dashboard';
@@ -48,7 +48,7 @@ export class NotificationsPage extends HTMLElement {
48
48
  this.notifications = [];
49
49
  }
50
50
 
51
- remove(id: string) {
51
+ removeNotification(id: string) {
52
52
  this.notifications = this.notifications.filter(n => n.id !== id);
53
53
  }
54
54
 
@@ -84,7 +84,7 @@ export class NotificationsPage extends HTMLElement {
84
84
  key=${notification.id}
85
85
  variant="${this.getVariant(notification.type)}"
86
86
  dismissible
87
- @dismiss=${() => this.remove(notification.id)}
87
+ @dismiss=${() => this.removeNotification(notification.id)}
88
88
  >
89
89
  <strong>${notification.title}</strong>
90
90
  <p>${notification.message}</p>
@@ -1,7 +1,7 @@
1
1
  import { page } from '../router';
2
2
  import { render, styles, html, css, context } from 'snice';
3
3
  import type { Placard, Context } from 'snice';
4
- import type { AppContext, User } from '../types/auth';
4
+ import type { Principal, User } from '../types/auth';
5
5
  import { authGuard } from '../guards/auth';
6
6
  import { logout } from '../services/auth';
7
7
 
@@ -16,22 +16,23 @@ const placard: Placard = {
16
16
  @page({ tag: 'profile-page', routes: ['/profile'], guards: [authGuard], placard })
17
17
  export class ProfilePage extends HTMLElement {
18
18
  user: User | null = null;
19
- private ctx?: Context<AppContext>;
19
+ private ctx?: Context;
20
20
 
21
21
  @context()
22
- handleContext(ctx: Context<AppContext>) {
22
+ handleContext(ctx: Context) {
23
23
  this.ctx = ctx;
24
- this.user = ctx.application.user;
24
+ const principal = ctx.application.principal as Principal | undefined;
25
+ this.user = principal?.user || null;
25
26
  }
26
27
 
27
28
  async handleLogout() {
28
29
  await logout();
29
30
 
30
31
  // Update context to reflect logged out state
31
- if (this.ctx) {
32
- this.ctx.application.user = null;
33
- this.ctx.application.isAuthenticated = false;
34
- this.ctx.update();
32
+ if (this.ctx && this.ctx.application.principal) {
33
+ const principal = this.ctx.application.principal as Principal;
34
+ principal.user = null;
35
+ principal.isAuthenticated = false;
35
36
  }
36
37
 
37
38
  window.location.href = '#/login';
@@ -1,15 +1,19 @@
1
1
  import { Router } from 'snice';
2
- import type { AppContext } from './types/auth';
2
+ import type { Principal } from './types/auth';
3
3
  import { getUser } from './services/storage';
4
4
  import { isAuthenticated } from './services/auth';
5
+ import { fetcher } from './fetcher';
5
6
 
6
- const { page, initialize, navigate } = Router<AppContext>({
7
+ const { page, initialize, navigate } = Router({
7
8
  target: '#app',
8
9
  type: 'hash',
9
10
  layout: 'snice-layout',
11
+ fetcher,
10
12
  context: {
11
- user: getUser(),
12
- isAuthenticated: isAuthenticated()
13
+ principal: {
14
+ user: getUser(),
15
+ isAuthenticated: isAuthenticated()
16
+ } as Principal
13
17
  }
14
18
  });
15
19
 
@@ -15,7 +15,7 @@ export interface LoginResponse {
15
15
  user: User;
16
16
  }
17
17
 
18
- export interface AppContext {
18
+ export interface Principal {
19
19
  user: User | null;
20
20
  isAuthenticated: boolean;
21
21
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Wait for a specified number of milliseconds
3
+ */
4
+ export async function waitFor(ms: number): Promise<void> {
5
+ return new Promise(resolve => setTimeout(resolve, ms));
6
+ }
7
+
8
+ /**
9
+ * Wait for a condition to become true
10
+ */
11
+ export async function waitUntil(
12
+ condition: () => boolean,
13
+ timeout = 1000,
14
+ interval = 10
15
+ ): Promise<void> {
16
+ const start = Date.now();
17
+ while (!condition()) {
18
+ if (Date.now() - start > timeout) {
19
+ throw new Error('Timeout waiting for condition');
20
+ }
21
+ await waitFor(interval);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Create a container div and append it to document.body
27
+ */
28
+ export function createContainer(id = 'app'): HTMLDivElement {
29
+ const container = document.createElement('div');
30
+ container.id = id;
31
+ document.body.appendChild(container);
32
+ return container;
33
+ }
34
+
35
+ /**
36
+ * Clean up the document body
37
+ */
38
+ export function cleanup(): void {
39
+ document.body.innerHTML = '';
40
+ }
41
+
42
+ /**
43
+ * Mock localStorage for tests
44
+ */
45
+ export function mockLocalStorage(): {
46
+ getItem: ReturnType<typeof vi.fn>;
47
+ setItem: ReturnType<typeof vi.fn>;
48
+ removeItem: ReturnType<typeof vi.fn>;
49
+ clear: ReturnType<typeof vi.fn>;
50
+ } {
51
+ const store: Record<string, string> = {};
52
+
53
+ return {
54
+ getItem: vi.fn((key: string) => store[key] || null),
55
+ setItem: vi.fn((key: string, value: string) => {
56
+ store[key] = value;
57
+ }),
58
+ removeItem: vi.fn((key: string) => {
59
+ delete store[key];
60
+ }),
61
+ clear: vi.fn(() => {
62
+ Object.keys(store).forEach(key => delete store[key]);
63
+ }),
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Mock fetch for tests
69
+ */
70
+ export function mockFetch(
71
+ response: unknown = {},
72
+ status = 200,
73
+ ok = true
74
+ ): ReturnType<typeof vi.fn> {
75
+ return vi.fn(() =>
76
+ Promise.resolve({
77
+ ok,
78
+ status,
79
+ json: () => Promise.resolve(response),
80
+ text: () => Promise.resolve(JSON.stringify(response)),
81
+ headers: new Headers(),
82
+ } as Response)
83
+ );
84
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { Context } from 'snice';
3
+ import { authMiddleware } from '../../src/middleware/auth';
4
+ import * as storage from '../../src/services/storage';
5
+
6
+ describe('Auth Middleware', () => {
7
+ let mockContext: Context;
8
+ let mockRequest: Request;
9
+ let mockNext: ReturnType<typeof vi.fn>;
10
+
11
+ beforeEach(() => {
12
+ localStorage.clear();
13
+ vi.clearAllMocks();
14
+
15
+ // Create mock context
16
+ mockContext = {
17
+ application: {
18
+ principal: {
19
+ user: null,
20
+ isAuthenticated: false,
21
+ },
22
+ },
23
+ navigation: {
24
+ route: '/',
25
+ params: {},
26
+ },
27
+ update: vi.fn(),
28
+ } as unknown as Context;
29
+
30
+ // Create mock request
31
+ mockRequest = new Request('https://api.example.com/data', {
32
+ headers: new Headers(),
33
+ });
34
+
35
+ // Create mock next function
36
+ const mockResponse = new Response('{}', { status: 200 });
37
+ mockNext = vi.fn(() => Promise.resolve(mockResponse));
38
+ });
39
+
40
+ it('should add Authorization header when token exists', async () => {
41
+ const token = 'test-token-123';
42
+ storage.setToken(token);
43
+
44
+ await authMiddleware.call(mockContext, mockRequest, mockNext);
45
+
46
+ expect(mockRequest.headers.get('Authorization')).toBe(`Bearer ${token}`);
47
+ expect(mockNext).toHaveBeenCalled();
48
+ });
49
+
50
+ it('should not add Authorization header when no token exists', async () => {
51
+ await authMiddleware.call(mockContext, mockRequest, mockNext);
52
+
53
+ expect(mockRequest.headers.get('Authorization')).toBeNull();
54
+ expect(mockNext).toHaveBeenCalled();
55
+ });
56
+
57
+ it('should call next() and return response', async () => {
58
+ const token = 'test-token-123';
59
+ storage.setToken(token);
60
+
61
+ const response = await authMiddleware.call(mockContext, mockRequest, mockNext);
62
+
63
+ expect(mockNext).toHaveBeenCalledTimes(1);
64
+ expect(response).toBeInstanceOf(Response);
65
+ expect(response.status).toBe(200);
66
+ });
67
+ });