# Directory Structure
```
├── .gitignore
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── server.ts
│ ├── sse.ts
│ └── timezone-utils.ts
├── tests
│ └── timezone-utils.test.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependency directories
2 | node_modules/
3 |
4 | # Build output
5 | dist/
6 |
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Environment variables
15 | .env
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | # Editor directories and files
22 | .idea/
23 | .vscode/
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
30 | # OS specific
31 | .DS_Store
32 | Thumbs.db
33 |
34 | # Test coverage
35 | coverage/
36 | .nyc_output/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP DateTime
2 |
3 | A TypeScript implementation of a Model Context Protocol (MCP) server that provides datetime and timezone information to agentic systems and chat REPLs.
4 |
5 | ## Overview
6 |
7 | MCP DateTime is a simple server that implements the [Model Context Protocol](https://github.com/model-context-protocol/mcp) to provide datetime and timezone information to AI agents and chat interfaces. It allows AI systems to:
8 |
9 | - Get the current time in the local system timezone
10 | - Get the current time in any valid timezone
11 | - List all available timezones
12 | - Access timezone information through URI resources
13 |
14 | ## Installation
15 |
16 | ### From npm
17 |
18 | ```bash
19 | npm install -g mcp-datetime
20 | ```
21 |
22 | ### From source
23 |
24 | ```bash
25 | git clone https://github.com/odgrmi/mcp-datetime.git
26 | cd mcp-datetime
27 | npm install
28 | npm run build
29 | ```
30 |
31 | ## Usage
32 |
33 | ### Command Line
34 |
35 | MCP DateTime can be run in two modes:
36 |
37 | #### 1. Standard I/O Mode (Default)
38 |
39 | This mode is ideal for integrating with AI systems that support the MCP protocol through standard input/output:
40 |
41 | ```bash
42 | mcp-datetime
43 | ```
44 |
45 | #### 2. Server-Sent Events (SSE) Mode
46 |
47 | This mode starts an HTTP server that provides SSE transport for the MCP protocol:
48 |
49 | ```bash
50 | mcp-datetime --sse
51 | ```
52 |
53 | You can also specify a custom port and URI prefix:
54 |
55 | ```bash
56 | mcp-datetime --sse --port=8080 --prefix=/api/datetime
57 | ```
58 |
59 | ### Environment Variables
60 |
61 | - `PORT`: Sets the port for SSE mode (default: 3000)
62 | - `URI_PREFIX`: Sets the URI prefix for SSE mode (default: none)
63 |
64 | ## Available Tools
65 |
66 | MCP DateTime provides the following tools:
67 |
68 | ### `get-current-time`
69 |
70 | Returns the current time in the system's local timezone.
71 |
72 | ### `get-current-timezone`
73 |
74 | Returns the current system timezone.
75 |
76 | ### `get-time-in-timezone`
77 |
78 | Returns the current time in a specified timezone.
79 |
80 | Parameters:
81 | - `timezone`: The timezone to get the current time for (e.g., "America/New_York")
82 |
83 | ### `list-timezones`
84 |
85 | Returns a list of all available timezones.
86 |
87 | ## Resource URIs
88 |
89 | MCP DateTime also provides access to timezone information through resource URIs:
90 |
91 | ### `datetime://{timezone}`
92 |
93 | Returns the current time in the specified timezone.
94 |
95 | Example: `datetime://America/New_York`
96 |
97 | ### `datetime://list`
98 |
99 | Returns a list of all available timezones.
100 |
101 | ## Common Timezones
102 |
103 | The following common timezones are always available:
104 |
105 | - UTC
106 | - Europe/London
107 | - Europe/Paris
108 | - Europe/Berlin
109 | - America/New_York
110 | - America/Chicago
111 | - America/Denver
112 | - America/Los_Angeles
113 | - Asia/Tokyo
114 | - Asia/Shanghai
115 | - Asia/Kolkata
116 | - Australia/Sydney
117 | - Pacific/Auckland
118 |
119 | ## SSE Endpoints
120 |
121 | When running in SSE mode, the following endpoints are available:
122 |
123 | - `/sse`: SSE connection endpoint
124 | - `/message`: Message endpoint for client-to-server communication
125 | - `/info`: Basic server information
126 |
127 | If a URI prefix is specified, it will be prepended to all endpoints.
128 |
129 | ## Integration with AI Systems
130 |
131 | MCP DateTime can be integrated with AI systems that support the Model Context Protocol. This allows AI agents to access accurate timezone and datetime information.
132 |
133 | ## Development
134 |
135 | ### Prerequisites
136 |
137 | - Node.js 14.16 or higher
138 | - npm
139 |
140 | ### Setup
141 |
142 | ```bash
143 | git clone https://github.com/odgrim/mcp-datetime.git
144 | cd mcp-datetime
145 | npm install
146 | ```
147 |
148 | ### Build
149 |
150 | ```bash
151 | npm run build
152 | ```
153 |
154 | ### Run in Development Mode
155 |
156 | ```bash
157 | npm run dev # Standard I/O mode
158 | npm run dev:sse # SSE mode
159 | ```
160 |
161 | ## License
162 |
163 | This project is licensed under the Mozilla Public License 2.0 - see the [LICENSE](LICENSE) file for details.
164 |
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
1 | export default {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | extensionsToTreatAsEsm: ['.ts'],
5 | moduleNameMapper: {
6 | '^(\\.{1,2}/.*)\\.js$': '$1',
7 | },
8 | transform: {
9 | '^.+\\.tsx?$': [
10 | 'ts-jest',
11 | {
12 | useESM: true,
13 | },
14 | ],
15 | },
16 | collectCoverageFrom: [
17 | 'src/**/*.ts',
18 | '!src/**/*.d.ts',
19 | '!src/**/index.ts',
20 | ],
21 | coverageReporters: ['text', 'lcov', 'clover', 'html'],
22 | coverageDirectory: 'coverage',
23 | };
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@odgrim/mcp-datetime",
3 | "version": "0.2.0",
4 | "description": "A TypeScript implementation of a simple MCP server that exposes datetime information to agentic systems and chat REPLs",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "bin": {
8 | "mcp-datetime": "dist/index.js"
9 | },
10 | "files": [
11 | "dist"
12 | ],
13 | "scripts": {
14 | "build": "tsc && shx chmod +x dist/*.js",
15 | "prepare": "npm run build",
16 | "watch": "tsc --watch",
17 | "start": "node dist/index.js",
18 | "start:sse": "node dist/index.js --sse",
19 | "dev": "ts-node --esm src/index.ts",
20 | "dev:sse": "ts-node --esm src/index.ts --sse",
21 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
22 | "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
23 | },
24 | "keywords": [
25 | "mcp",
26 | "datetime",
27 | "timezone",
28 | "model-context-protocol"
29 | ],
30 | "author": "odgrim",
31 | "license": "MPL-2.0",
32 | "dependencies": {
33 | "@modelcontextprotocol/sdk": "^1.4.1",
34 | "@types/express": "^5.0.0",
35 | "express": "^4.21.2",
36 | "zod": "^3.22.4"
37 | },
38 | "devDependencies": {
39 | "@types/jest": "^29.5.14",
40 | "@types/node": "^20.11.24",
41 | "jest": "^29.7.0",
42 | "shx": "^0.3.4",
43 | "ts-jest": "^29.2.6",
44 | "ts-node": "^10.9.2",
45 | "typescript": "^5.3.3"
46 | },
47 | "engines": {
48 | "node": ">=14.16"
49 | },
50 | "publishConfig": {
51 | "access": "public"
52 | },
53 | "repository": {
54 | "type": "git",
55 | "url": "git+https://github.com/odgrim/mcp-datetime.git"
56 | },
57 | "bugs": {
58 | "url": "https://github.com/odgrim/mcp-datetime/issues"
59 | },
60 | "homepage": "https://github.com/odgrim/mcp-datetime#readme"
61 | }
62 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { server } from "./server.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import { startSSEServer } from "./sse.js";
6 |
7 | async function main() {
8 | console.error("Starting MCP DateTime server...");
9 |
10 | // Check if the --sse flag is provided
11 | const useSSE = process.argv.includes("--sse");
12 |
13 | // Check if a custom port is provided with --port=XXXX
14 | const portArg = process.argv.find(arg => arg.startsWith("--port="));
15 | // Use PORT environment variable or command line argument or default to 3000
16 | const defaultPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
17 | const port = portArg ? parseInt(portArg.split("=")[1], 10) : defaultPort;
18 |
19 | // Check if a URI prefix is provided with --prefix=XXXX
20 | const prefixArg = process.argv.find(arg => arg.startsWith("--prefix="));
21 | // Use URI_PREFIX environment variable or command line argument or default to empty string
22 | const defaultPrefix = process.env.URI_PREFIX || "";
23 | const uriPrefix = prefixArg ? prefixArg.split("=")[1] : defaultPrefix;
24 |
25 | try {
26 | if (useSSE) {
27 | // Start the SSE server
28 | console.error(`Starting MCP DateTime server with SSE transport on port ${port}...`);
29 | const cleanup = startSSEServer(port, uriPrefix);
30 |
31 | // Handle graceful shutdown
32 | process.on("SIGINT", async () => {
33 | console.error("Received SIGINT, shutting down...");
34 | await cleanup();
35 | process.exit(0);
36 | });
37 |
38 | process.on("SIGTERM", async () => {
39 | console.error("Received SIGTERM, shutting down...");
40 | await cleanup();
41 | process.exit(0);
42 | });
43 | } else {
44 | // Connect to stdio transport
45 | await server.connect(new StdioServerTransport());
46 | console.error("MCP DateTime server connected to stdio transport");
47 | }
48 | } catch (error) {
49 | console.error("Error starting MCP DateTime server:", error);
50 | process.exit(1);
51 | }
52 | }
53 |
54 | main().catch(error => {
55 | console.error("Unhandled error:", error);
56 | process.exit(1);
57 | });
```
--------------------------------------------------------------------------------
/src/sse.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2 | import express from "express";
3 | import { server } from "./server.js";
4 |
5 | /**
6 | * Creates and starts an Express server that provides SSE transport for the MCP DateTime server
7 | * @param port The port to listen on (defaults to PORT env var or 3000)
8 | * @param uriPrefix The URI prefix to prepend to all routes (for reverse proxy scenarios)
9 | * @returns A cleanup function to close the server
10 | */
11 | export function startSSEServer(
12 | port: number = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
13 | uriPrefix: string = ""
14 | ): () => Promise<void> {
15 | const app = express();
16 | let transport: SSEServerTransport;
17 |
18 | // Normalize the URI prefix to ensure it starts with a / and doesn't end with one
19 | const normalizedPrefix = uriPrefix
20 | ? (uriPrefix.startsWith('/') ? uriPrefix : `/${uriPrefix}`).replace(/\/+$/, '')
21 | : '';
22 |
23 | // Define the endpoint paths with the prefix
24 | const ssePath = `${normalizedPrefix}/sse`;
25 | const messagePath = `${normalizedPrefix}/message`;
26 | const infoPath = `${normalizedPrefix}/info`;
27 |
28 | // SSE endpoint for establishing a connection
29 | app.get(ssePath, async (req, res) => {
30 | console.error("Received SSE connection");
31 | transport = new SSEServerTransport(messagePath, res);
32 | await server.connect(transport);
33 | });
34 |
35 | // Endpoint for receiving messages from the client
36 | app.post(messagePath, async (req, res) => {
37 | console.error("Received message");
38 | await transport.handlePostMessage(req, res);
39 | });
40 |
41 | // Basic info endpoint
42 | app.get(infoPath, (req, res) => {
43 | res.json({
44 | name: "MCP DateTime Server",
45 | version: "0.1.0",
46 | transport: "SSE",
47 | endpoints: {
48 | sse: ssePath,
49 | message: messagePath,
50 | info: infoPath
51 | }
52 | });
53 | });
54 |
55 | // Start the server
56 | const httpServer = app.listen(port, () => {
57 | console.error(`MCP DateTime server listening on port ${port}`);
58 | console.error(`URI prefix: ${normalizedPrefix || '/'} (root)`);
59 | console.error(`SSE endpoint: http://localhost:${port}${ssePath}`);
60 | console.error(`Message endpoint: http://localhost:${port}${messagePath}`);
61 | console.error(`Info endpoint: http://localhost:${port}${infoPath}`);
62 | });
63 |
64 | // Return a cleanup function
65 | return async () => {
66 | console.error("Closing SSE server...");
67 | httpServer.close();
68 | if (transport) {
69 | await server.close();
70 | }
71 | };
72 | }
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { z } from "zod";
3 | import {
4 | COMMON_TIMEZONES,
5 | getAvailableTimezones,
6 | isValidTimezone,
7 | getCurrentTimeInTimezone,
8 | getCurrentTimezone,
9 | getFormattedTimezoneList
10 | } from "./timezone-utils.js";
11 |
12 | // Create an MCP server
13 | export const server = new McpServer({
14 | name: "mcp-datetime",
15 | version: "0.1.0"
16 | });
17 |
18 | // Add a tool to get the current time in the local timezone
19 | server.tool(
20 | "get-current-time",
21 | "Get the current time in the configured local timezone",
22 | async () => ({
23 | content: [{
24 | type: "text",
25 | text: `The current time is ${getCurrentTimeInTimezone(getCurrentTimezone())}`
26 | }]
27 | })
28 | );
29 |
30 | // Add a tool to get the current system timezone
31 | server.tool(
32 | "get-current-timezone",
33 | "Get the current system timezone",
34 | async () => {
35 | const timezone = getCurrentTimezone();
36 | return {
37 | content: [{
38 | type: "text",
39 | text: `The current system timezone is ${timezone}`
40 | }]
41 | };
42 | }
43 | );
44 |
45 | // Add a tool to get the current time in a specific timezone
46 | server.tool(
47 | "get-time-in-timezone",
48 | "Get the current time in a specific timezone",
49 | {
50 | timezone: z.string().describe("The timezone to get the current time for")
51 | },
52 | async (args) => {
53 | if (!isValidTimezone(args.timezone)) {
54 | return {
55 | content: [{
56 | type: "text",
57 | text: `Error: Invalid timezone "${args.timezone}". Use the "list-timezones" tool to see available options.`
58 | }],
59 | isError: true
60 | };
61 | }
62 |
63 | return {
64 | content: [{
65 | type: "text",
66 | text: `The current time in ${args.timezone} is ${getCurrentTimeInTimezone(args.timezone)}`
67 | }]
68 | };
69 | }
70 | );
71 |
72 | // Add a tool to list all available timezones
73 | server.tool(
74 | "list-timezones",
75 | "List all available timezones",
76 | async () => {
77 | return {
78 | content: [{
79 | type: "text",
80 | text: getFormattedTimezoneList()
81 | }]
82 | };
83 | }
84 | );
85 |
86 | // Create a resource template for datetime URIs
87 | // The list method is defined to return a list of common timezones
88 | // to avoid overwhelming the client with all available timezones
89 | const datetimeTemplate = new ResourceTemplate("datetime://{timezone}", {
90 | list: async () => {
91 | return {
92 | resources: COMMON_TIMEZONES.map(timezone => ({
93 | uri: `datetime://${encodeURIComponent(timezone)}`,
94 | name: `Current time in ${timezone}`,
95 | description: `Get the current time in the ${timezone} timezone`,
96 | mimeType: "text/plain"
97 | }))
98 | };
99 | }
100 | });
101 |
102 | // Register the template with the server
103 | server.resource(
104 | "datetime-template",
105 | datetimeTemplate,
106 | async (uri, variables) => {
107 | // Decode the timezone from the URI
108 | const encodedTimezone = variables.timezone as string;
109 | const timezone = decodeURIComponent(encodedTimezone);
110 |
111 | if (!timezone || !isValidTimezone(timezone)) {
112 | throw new Error(`Invalid timezone: ${timezone}`);
113 | }
114 |
115 | const formattedTime = getCurrentTimeInTimezone(timezone);
116 | return {
117 | contents: [{
118 | uri: decodeURIComponent(uri.href),
119 | text: `Current time in ${timezone}: ${formattedTime}`,
120 | mimeType: "text/plain"
121 | }]
122 | };
123 | }
124 | );
125 |
126 | // Add a resource to list all available timezones
127 | server.resource(
128 | "datetime-list",
129 | "datetime://list",
130 | async () => {
131 | return {
132 | contents: [{
133 | uri: "datetime://list",
134 | text: getFormattedTimezoneList("All available timezones"),
135 | mimeType: "text/plain"
136 | }]
137 | };
138 | }
139 | );
```
--------------------------------------------------------------------------------
/src/timezone-utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Common timezones to ensure they're always available
2 | export const COMMON_TIMEZONES = [
3 | "UTC",
4 | "Europe/London",
5 | "Europe/Paris",
6 | "Europe/Berlin",
7 | "America/New_York",
8 | "America/Chicago",
9 | "America/Denver",
10 | "America/Los_Angeles",
11 | "Asia/Tokyo",
12 | "Asia/Shanghai",
13 | "Asia/Kolkata",
14 | "Australia/Sydney",
15 | "Pacific/Auckland"
16 | ];
17 |
18 | /**
19 | * Get all available timezones using the Intl API
20 | * @returns Array of timezone strings
21 | */
22 | export function getAvailableTimezones(): string[] {
23 | try {
24 | const timezones = new Set<string>(Intl.supportedValuesOf('timeZone'));
25 |
26 | // Ensure common timezones are always included
27 | COMMON_TIMEZONES.forEach(tz => timezones.add(tz));
28 |
29 | return Array.from(timezones).sort();
30 | } catch (error) {
31 | console.error("Error getting timezones from Intl API:", error);
32 | // Fallback to common timezones if the Intl API fails
33 | return COMMON_TIMEZONES;
34 | }
35 | }
36 |
37 | /**
38 | * Format the list of available timezones as a string
39 | * @param prefix Optional prefix text to include before the list (default: "Available timezones")
40 | * @returns Formatted string with timezone count and comma-separated list
41 | */
42 | export function getFormattedTimezoneList(prefix: string = "Available timezones"): string {
43 | const timezones = getAvailableTimezones();
44 | return `${prefix} (${timezones.length}): ${timezones.join(', ')}`;
45 | }
46 |
47 | /**
48 | * Check if a timezone is valid
49 | * @param timezone Timezone string to validate
50 | * @returns boolean indicating if the timezone is valid
51 | */
52 | export function isValidTimezone(timezone: string): boolean {
53 | try {
54 | // Try to use the timezone with Intl.DateTimeFormat
55 | Intl.DateTimeFormat(undefined, { timeZone: timezone });
56 | return true;
57 | } catch (error) {
58 | return false;
59 | }
60 | }
61 |
62 | /**
63 | * Get the current system timezone
64 | * @returns The current system timezone string, or "UTC" as fallback
65 | */
66 | export function getCurrentTimezone(): string {
67 | try {
68 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
69 | return processTimezone(timezone);
70 | } catch (error) {
71 | console.error("Error getting current timezone:", error);
72 | return "UTC"; // Default to UTC if there's an error
73 | }
74 | }
75 |
76 | // Export this function to make it testable
77 | export function processTimezone(timezone: string): string {
78 | // Verify it's a valid timezone
79 | if (isValidTimezone(timezone)) {
80 | return timezone;
81 | }
82 | return handleInvalidTimezone(timezone);
83 | }
84 |
85 | // Export this function to make it testable
86 | export function handleInvalidTimezone(timezone: string): string {
87 | console.warn(`System timezone ${timezone} is not valid, falling back to UTC`);
88 | return "UTC";
89 | }
90 |
91 | /**
92 | * Format the current date and time for a given timezone in ISO8601 format
93 | * @param timezone Timezone string
94 | * @returns Formatted date-time string in ISO8601 format
95 | */
96 | export function getCurrentTimeInTimezone(timezone: string): string {
97 | try {
98 | const date = new Date();
99 |
100 | // Create a formatter that includes the timezone
101 | const options: Intl.DateTimeFormatOptions = {
102 | timeZone: timezone,
103 | timeZoneName: 'short'
104 | };
105 |
106 | // Get the timezone offset from the formatter
107 | const formatter = new Intl.DateTimeFormat('en-US', options);
108 | const formattedDate = formatter.format(date);
109 | const timezonePart = formattedDate.split(' ').pop() || '';
110 |
111 | // Format the date in ISO8601 format with the timezone
112 | // First get the date in the specified timezone
113 | const tzFormatter = new Intl.DateTimeFormat('en-US', {
114 | timeZone: timezone,
115 | year: 'numeric',
116 | month: '2-digit',
117 | day: '2-digit',
118 | hour: '2-digit',
119 | minute: '2-digit',
120 | second: '2-digit',
121 | hour12: false,
122 | fractionalSecondDigits: 3
123 | });
124 |
125 | const parts = tzFormatter.formatToParts(date);
126 | const dateParts: Record<string, string> = {};
127 |
128 | parts.forEach(part => {
129 | if (part.type !== 'literal') {
130 | dateParts[part.type] = part.value;
131 | }
132 | });
133 |
134 | // Format as YYYY-MM-DDTHH:MM:SS.sss±HH:MM (ISO8601)
135 | const isoDate = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}.${dateParts.fractionalSecond || '000'}`;
136 |
137 | // For proper ISO8601, we need to add the timezone offset
138 | // We can use the Intl.DateTimeFormat to get the timezone offset
139 | const tzOffset = new Date().toLocaleString('en-US', { timeZone: timezone, timeZoneName: 'longOffset' }).split(' ').pop() || '';
140 |
141 | // Format the final ISO8601 string
142 | return `${isoDate}${tzOffset.replace('GMT', '')}`;
143 | } catch (error) {
144 | console.error(`Error formatting time for timezone ${timezone}:`, error);
145 | return 'Invalid timezone';
146 | }
147 | }
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2022",
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "libReplacement": true, /* Enable lib replacement. */
18 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
27 |
28 | /* Modules */
29 | "module": "NodeNext",
30 | "moduleResolution": "NodeNext",
31 | // "rootDir": "./", /* Specify the root folder within your source files. */
32 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
35 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
38 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
39 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
40 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
41 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
42 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
43 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
44 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
45 | // "resolveJsonModule": true, /* Enable importing .json files. */
46 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
47 | // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
48 |
49 | /* JavaScript Support */
50 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
51 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
52 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
53 |
54 | /* Emit */
55 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
56 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
57 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
58 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
60 | // "noEmit": true, /* Disable emitting files from a compilation. */
61 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
62 | "outDir": "./dist",
63 | // "removeComments": true, /* Disable emitting comments. */
64 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
65 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
66 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
68 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
69 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
70 | // "newLine": "crlf", /* Set the newline character for emitting files. */
71 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
72 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
73 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
74 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
75 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
76 |
77 | /* Interop Constraints */
78 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
79 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
80 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
81 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
82 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
83 | "esModuleInterop": true,
84 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
85 | "forceConsistentCasingInFileNames": true,
86 |
87 | /* Type Checking */
88 | "strict": true,
89 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
90 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
91 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
92 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
93 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
94 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
95 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
96 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
97 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
98 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
99 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
100 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
101 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
102 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
103 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
104 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
105 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
106 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
107 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
108 |
109 | /* Completeness */
110 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
111 | "skipLibCheck": true
112 | },
113 | "include": ["src/**/*"],
114 | "exclude": ["node_modules"]
115 | }
116 |
```
--------------------------------------------------------------------------------
/tests/timezone-utils.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
2 | import * as tzUtils from '../src/timezone-utils.js';
3 | import {
4 | getAvailableTimezones,
5 | getFormattedTimezoneList,
6 | isValidTimezone,
7 | getCurrentTimezone,
8 | getCurrentTimeInTimezone,
9 | handleInvalidTimezone,
10 | processTimezone,
11 | COMMON_TIMEZONES
12 | } from '../src/timezone-utils.js';
13 |
14 | describe('timezone-utils', () => {
15 | // Store original methods to restore after tests
16 | const originalSupportedValuesOf = Intl.supportedValuesOf;
17 | const originalDateTimeFormat = Intl.DateTimeFormat;
18 | const originalConsoleWarn = console.warn;
19 | const originalConsoleError = console.error;
20 | const originalToLocaleString = Date.prototype.toLocaleString;
21 |
22 | // Restore original methods after each test
23 | afterEach(() => {
24 | Intl.supportedValuesOf = originalSupportedValuesOf;
25 | Intl.DateTimeFormat = originalDateTimeFormat;
26 | console.warn = originalConsoleWarn;
27 | console.error = originalConsoleError;
28 | Date.prototype.toLocaleString = originalToLocaleString;
29 |
30 | // Restore any mocked functions
31 | jest.restoreAllMocks();
32 | });
33 |
34 | // Helper function to create a mock formatter
35 | const createMockFormatter = (options: {
36 | formattedDate?: string;
37 | timeZone?: string;
38 | timeZoneValue?: string;
39 | includeFractionalSecond?: boolean;
40 | } = {}) => {
41 | const {
42 | formattedDate = '2023-01-01T12:00:00.000+00:00',
43 | timeZone = 'UTC',
44 | timeZoneValue = 'GMT+00:00',
45 | includeFractionalSecond = true
46 | } = options;
47 |
48 | const parts = [
49 | { type: 'year', value: '2023' },
50 | { type: 'literal', value: '-' },
51 | { type: 'month', value: '01' },
52 | { type: 'literal', value: '-' },
53 | { type: 'day', value: '01' },
54 | { type: 'literal', value: 'T' },
55 | { type: 'hour', value: '12' },
56 | { type: 'literal', value: ':' },
57 | { type: 'minute', value: '00' },
58 | { type: 'literal', value: ':' },
59 | { type: 'second', value: '00' },
60 | { type: 'literal', value: '.' }
61 | ];
62 |
63 | if (includeFractionalSecond) {
64 | parts.push({ type: 'fractionalSecond', value: '000' });
65 | }
66 |
67 | parts.push({ type: 'timeZoneName', value: timeZoneValue });
68 |
69 | return {
70 | format: () => formattedDate,
71 | formatToParts: () => parts,
72 | resolvedOptions: () => ({ timeZone })
73 | };
74 | };
75 |
76 | // Helper function to mock DateTimeFormat
77 | const mockDateTimeFormat = (formatter: any) => {
78 | // @ts-ignore - TypeScript doesn't like us mocking built-in objects
79 | Intl.DateTimeFormat = jest.fn().mockImplementation(() => formatter);
80 | };
81 |
82 | describe('getAvailableTimezones', () => {
83 | it('should return a sorted array of timezones', () => {
84 | const timezones = getAvailableTimezones();
85 |
86 | // Check that we have an array of strings
87 | expect(Array.isArray(timezones)).toBe(true);
88 | expect(timezones.length).toBeGreaterThan(0);
89 |
90 | // Check that all common timezones are included
91 | COMMON_TIMEZONES.forEach(tz => {
92 | expect(timezones).toContain(tz);
93 | });
94 |
95 | // Check that the array is sorted
96 | const sortedTimezones = [...timezones].sort();
97 | expect(timezones).toEqual(sortedTimezones);
98 | });
99 |
100 | it('should fall back to common timezones if Intl API fails', () => {
101 | // Mock the Intl.supportedValuesOf to throw an error
102 | Intl.supportedValuesOf = function() {
103 | throw new Error('API not available');
104 | } as any;
105 |
106 | const timezones = getAvailableTimezones();
107 |
108 | // Should fall back to common timezones
109 | expect(timezones).toEqual(COMMON_TIMEZONES);
110 | });
111 | });
112 |
113 | describe('getFormattedTimezoneList', () => {
114 | it('should format the timezone list with default prefix', () => {
115 | const timezones = getAvailableTimezones();
116 | const formattedList = getFormattedTimezoneList();
117 |
118 | expect(formattedList).toContain('Available timezones');
119 | expect(formattedList).toContain(`(${timezones.length})`);
120 | });
121 |
122 | it('should format the timezone list with custom prefix', () => {
123 | const timezones = getAvailableTimezones();
124 | const customPrefix = 'Custom prefix';
125 | const formattedList = getFormattedTimezoneList(customPrefix);
126 |
127 | expect(formattedList).toContain(customPrefix);
128 | expect(formattedList).toContain(`(${timezones.length})`);
129 | });
130 | });
131 |
132 | describe('isValidTimezone', () => {
133 | it('should return true for valid timezones', () => {
134 | expect(isValidTimezone('UTC')).toBe(true);
135 | expect(isValidTimezone('Europe/London')).toBe(true);
136 | });
137 |
138 | it('should return false for invalid timezones', () => {
139 | expect(isValidTimezone('Invalid/Timezone')).toBe(false);
140 | expect(isValidTimezone('')).toBe(false);
141 | });
142 | });
143 |
144 | describe('getCurrentTimezone', () => {
145 | beforeEach(() => {
146 | console.warn = jest.fn();
147 | console.error = jest.fn();
148 | });
149 |
150 | it('should return the current timezone', () => {
151 | const timezone = getCurrentTimezone();
152 | expect(typeof timezone).toBe('string');
153 | expect(timezone.length).toBeGreaterThan(0);
154 | });
155 |
156 | it('should fall back to UTC if there is an error', () => {
157 | // Mock Intl.DateTimeFormat to throw an error
158 | Intl.DateTimeFormat = jest.fn().mockImplementation(() => {
159 | throw new Error('API error');
160 | }) as any;
161 |
162 | const timezone = getCurrentTimezone();
163 | expect(timezone).toBe('UTC');
164 | expect(console.error).toHaveBeenCalledWith(
165 | 'Error getting current timezone:',
166 | expect.any(Error)
167 | );
168 | });
169 |
170 | it('should log a warning and fall back to UTC for invalid timezones', () => {
171 | const invalidTimezone = 'Invalid/Timezone';
172 | const result = handleInvalidTimezone(invalidTimezone);
173 |
174 | expect(result).toBe('UTC');
175 | expect(console.warn).toHaveBeenCalledWith(
176 | `System timezone ${invalidTimezone} is not valid, falling back to UTC`
177 | );
178 | });
179 |
180 | it('should call handleInvalidTimezone for invalid system timezone', () => {
181 | // Create a function that simulates getCurrentTimezone with an invalid timezone
182 | const simulateGetCurrentTimezoneWithInvalidTimezone = () => {
183 | try {
184 | const timezone = 'Invalid/Timezone';
185 | if (isValidTimezone(timezone)) {
186 | return timezone;
187 | } else {
188 | return handleInvalidTimezone(timezone);
189 | }
190 | } catch (error) {
191 | console.error("Error getting current timezone:", error);
192 | return "UTC";
193 | }
194 | };
195 |
196 | const timezone = simulateGetCurrentTimezoneWithInvalidTimezone();
197 | expect(timezone).toBe('UTC');
198 | expect(console.warn).toHaveBeenCalledWith(
199 | 'System timezone Invalid/Timezone is not valid, falling back to UTC'
200 | );
201 | });
202 | });
203 |
204 | describe('getCurrentTimeInTimezone', () => {
205 | beforeEach(() => {
206 | console.error = jest.fn();
207 | });
208 |
209 | it('should format the current time in UTC', () => {
210 | // Mock the DateTimeFormat constructor with our helper
211 | mockDateTimeFormat(createMockFormatter());
212 |
213 | const time = getCurrentTimeInTimezone('UTC');
214 |
215 | // Check that it returns a string
216 | expect(typeof time).toBe('string');
217 |
218 | // Check that it's not the error message
219 | expect(time).not.toBe('Invalid timezone');
220 | });
221 |
222 | it('should format the current time in a specific timezone', () => {
223 | // Mock the DateTimeFormat constructor with our helper
224 | mockDateTimeFormat(createMockFormatter({
225 | formattedDate: '2023-01-01T12:00:00.000+01:00',
226 | timeZone: 'Europe/London',
227 | timeZoneValue: 'GMT+01:00'
228 | }));
229 |
230 | const time = getCurrentTimeInTimezone('Europe/London');
231 |
232 | // Check that it returns a string
233 | expect(typeof time).toBe('string');
234 |
235 | // Check that it's not the error message
236 | expect(time).not.toBe('Invalid timezone');
237 | });
238 |
239 | it('should return an error message for invalid timezones', () => {
240 | const time = getCurrentTimeInTimezone('Invalid/Timezone');
241 | expect(time).toBe('Invalid timezone');
242 | });
243 |
244 | it('should handle edge cases in date formatting', () => {
245 | // Mock DateTimeFormat to throw an error
246 | Intl.DateTimeFormat = jest.fn().mockImplementation(() => {
247 | throw new Error('Mock error');
248 | }) as any;
249 |
250 | // This should trigger the catch block in getCurrentTimeInTimezone
251 | const result = getCurrentTimeInTimezone('UTC');
252 |
253 | // Verify we got the error message
254 | expect(result).toBe('Invalid timezone');
255 |
256 | // Verify console.error was called
257 | expect(console.error).toHaveBeenCalled();
258 | });
259 |
260 | it('should handle empty timezone parts in formatting', () => {
261 | // First mock for the formatter that gets the timezone offset
262 | const mockEmptyFormatter = {
263 | format: jest.fn().mockReturnValue('2023-01-01') // No timezone part
264 | };
265 |
266 | // Second mock for the formatter that gets the date parts
267 | const mockTzFormatter = {
268 | formatToParts: jest.fn().mockReturnValue([
269 | { type: 'year', value: '2023' },
270 | { type: 'month', value: '01' },
271 | { type: 'day', value: '01' },
272 | { type: 'hour', value: '12' },
273 | { type: 'minute', value: '00' },
274 | { type: 'second', value: '00' }
275 | // No fractionalSecond to test that case
276 | ])
277 | };
278 |
279 | // Mock Date.toLocaleString to return a string without timezone
280 | Date.prototype.toLocaleString = jest.fn().mockReturnValue('January 1, 2023') as any;
281 |
282 | // Mock DateTimeFormat to return our formatters
283 | Intl.DateTimeFormat = jest.fn()
284 | .mockImplementationOnce(() => mockEmptyFormatter)
285 | .mockImplementationOnce(() => mockTzFormatter) as any;
286 |
287 | const result = getCurrentTimeInTimezone('UTC');
288 |
289 | // Verify the result contains the expected date format
290 | expect(result).toContain('2023-01-01T12:00:00.000');
291 | });
292 |
293 | it('should handle null values in split operations', () => {
294 | // Mock formatter with null format result
295 | const mockNullFormatter = {
296 | format: jest.fn().mockReturnValue(null)
297 | };
298 |
299 | // Mock DateTimeFormat to return our formatter
300 | Intl.DateTimeFormat = jest.fn().mockImplementation(() => mockNullFormatter) as any;
301 |
302 | const result = getCurrentTimeInTimezone('UTC');
303 |
304 | // The function should handle the null values and return an error
305 | expect(result).toBe('Invalid timezone');
306 |
307 | // Verify console.error was called
308 | expect(console.error).toHaveBeenCalled();
309 | });
310 |
311 | it('should handle empty result from formatter.format()', () => {
312 | // Mock for the formatter that returns empty string
313 | const mockEmptyFormatter = {
314 | format: jest.fn().mockReturnValue('')
315 | };
316 |
317 | // Mock for the formatter that gets the date parts
318 | const mockTzFormatter = {
319 | formatToParts: jest.fn().mockReturnValue([
320 | { type: 'year', value: '2023' },
321 | { type: 'month', value: '01' },
322 | { type: 'day', value: '01' },
323 | { type: 'hour', value: '12' },
324 | { type: 'minute', value: '00' },
325 | { type: 'second', value: '00' },
326 | { type: 'fractionalSecond', value: '123' }
327 | ])
328 | };
329 |
330 | // Mock Date.toLocaleString to return a valid string
331 | Date.prototype.toLocaleString = jest.fn().mockReturnValue('1/1/2023, 12:00:00 PM GMT+0000') as any;
332 |
333 | // Mock DateTimeFormat to return our formatters
334 | Intl.DateTimeFormat = jest.fn()
335 | .mockImplementationOnce(() => mockEmptyFormatter)
336 | .mockImplementationOnce(() => mockTzFormatter) as any;
337 |
338 | const result = getCurrentTimeInTimezone('UTC');
339 |
340 | // The function should handle the empty string and still return a result
341 | expect(result).toContain('2023-01-01T12:00:00.123');
342 | });
343 |
344 | it('should handle empty result from toLocaleString()', () => {
345 | // Mock for the formatter that returns valid string
346 | const mockFormatter = {
347 | format: jest.fn().mockReturnValue('1/1/2023, 12:00:00 PM GMT+0000')
348 | };
349 |
350 | // Mock for the formatter that gets the date parts
351 | const mockTzFormatter = {
352 | formatToParts: jest.fn().mockReturnValue([
353 | { type: 'year', value: '2023' },
354 | { type: 'month', value: '01' },
355 | { type: 'day', value: '01' },
356 | { type: 'hour', value: '12' },
357 | { type: 'minute', value: '00' },
358 | { type: 'second', value: '00' },
359 | { type: 'fractionalSecond', value: '123' }
360 | ])
361 | };
362 |
363 | // Mock Date.toLocaleString to return an empty string
364 | Date.prototype.toLocaleString = jest.fn().mockReturnValue('') as any;
365 |
366 | // Mock DateTimeFormat to return our formatters
367 | Intl.DateTimeFormat = jest.fn()
368 | .mockImplementationOnce(() => mockFormatter)
369 | .mockImplementationOnce(() => mockTzFormatter) as any;
370 |
371 | const result = getCurrentTimeInTimezone('UTC');
372 |
373 | // The function should handle the empty string and still return a result
374 | expect(result).toContain('2023-01-01T12:00:00.123');
375 | });
376 | });
377 |
378 | describe('handleInvalidTimezone', () => {
379 | beforeEach(() => {
380 | console.warn = jest.fn();
381 | });
382 |
383 | it('should log a warning and return UTC', () => {
384 | const invalidTimezone = 'Invalid/Timezone';
385 | const result = handleInvalidTimezone(invalidTimezone);
386 |
387 | expect(result).toBe('UTC');
388 | expect(console.warn).toHaveBeenCalledWith(
389 | `System timezone ${invalidTimezone} is not valid, falling back to UTC`
390 | );
391 | });
392 |
393 | it('should be called when isValidTimezone returns false', () => {
394 | // Create a test function that simulates the exact code path
395 | const testInvalidTimezone = (timezone: string) => {
396 | if (isValidTimezone(timezone)) {
397 | return timezone;
398 | }
399 | return handleInvalidTimezone(timezone);
400 | };
401 |
402 | // Use a timezone that we know is invalid
403 | const result = testInvalidTimezone('Invalid/Timezone');
404 |
405 | expect(result).toBe('UTC');
406 | expect(console.warn).toHaveBeenCalledWith(
407 | 'System timezone Invalid/Timezone is not valid, falling back to UTC'
408 | );
409 | });
410 | });
411 |
412 | describe('processTimezone', () => {
413 | beforeEach(() => {
414 | console.warn = jest.fn();
415 | });
416 |
417 | it('should return the timezone if it is valid', () => {
418 | const validTimezone = 'UTC';
419 | const result = processTimezone(validTimezone);
420 | expect(result).toBe(validTimezone);
421 | expect(console.warn).not.toHaveBeenCalled();
422 | });
423 |
424 | it('should call handleInvalidTimezone for invalid timezones', () => {
425 | const invalidTimezone = 'Invalid/Timezone';
426 | const result = processTimezone(invalidTimezone);
427 | expect(result).toBe('UTC');
428 | expect(console.warn).toHaveBeenCalledWith(
429 | `System timezone ${invalidTimezone} is not valid, falling back to UTC`
430 | );
431 | });
432 | });
433 | });
```