@open-loyalty/mcp-server 1.0.0 → 1.0.2

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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Storage abstraction for OAuth data
3
+ * Uses Redis if REDIS_URL is set, otherwise falls back to in-memory storage
4
+ */
5
+ import { Redis } from "ioredis";
6
+ /**
7
+ * In-memory storage for local development
8
+ */
9
+ class InMemoryStorage {
10
+ data = new Map();
11
+ async get(key) {
12
+ const entry = this.data.get(key);
13
+ if (!entry)
14
+ return null;
15
+ if (entry.expiresAt && entry.expiresAt < Date.now()) {
16
+ this.data.delete(key);
17
+ return null;
18
+ }
19
+ return entry.value;
20
+ }
21
+ async set(key, value, ttlMs) {
22
+ this.data.set(key, {
23
+ value,
24
+ expiresAt: ttlMs ? Date.now() + ttlMs : undefined,
25
+ });
26
+ }
27
+ async delete(key) {
28
+ this.data.delete(key);
29
+ }
30
+ }
31
+ /**
32
+ * Redis storage for production
33
+ */
34
+ class RedisStorage {
35
+ client;
36
+ constructor(redisUrl) {
37
+ this.client = new Redis(redisUrl, {
38
+ maxRetriesPerRequest: 3,
39
+ retryStrategy: (times) => Math.min(times * 100, 3000),
40
+ });
41
+ this.client.on("error", (err) => {
42
+ console.error("Redis connection error:", err.message);
43
+ });
44
+ this.client.on("connect", () => {
45
+ console.log("Connected to Redis");
46
+ });
47
+ }
48
+ async get(key) {
49
+ const data = await this.client.get(key);
50
+ if (!data)
51
+ return null;
52
+ try {
53
+ return JSON.parse(data);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ async set(key, value, ttlMs) {
60
+ const data = JSON.stringify(value);
61
+ if (ttlMs) {
62
+ await this.client.set(key, data, "PX", ttlMs);
63
+ }
64
+ else {
65
+ await this.client.set(key, data);
66
+ }
67
+ }
68
+ async delete(key) {
69
+ await this.client.del(key);
70
+ }
71
+ }
72
+ // Singleton storage instance
73
+ let storage = null;
74
+ /**
75
+ * Get the storage backend (Redis if available, otherwise in-memory)
76
+ */
77
+ export function getStorage() {
78
+ if (storage)
79
+ return storage;
80
+ const redisUrl = process.env.REDIS_URL;
81
+ if (redisUrl) {
82
+ console.log("Using Redis for OAuth storage");
83
+ storage = new RedisStorage(redisUrl);
84
+ }
85
+ else {
86
+ console.log("Using in-memory storage for OAuth (set REDIS_URL for persistence)");
87
+ storage = new InMemoryStorage();
88
+ }
89
+ return storage;
90
+ }
91
+ // Storage key prefixes
92
+ export const KEYS = {
93
+ client: (id) => `oauth:client:${id}`,
94
+ authCode: (code) => `oauth:code:${code}`,
95
+ session: (id) => `oauth:session:${id}`,
96
+ token: (token) => `oauth:token:${token}`,
97
+ config: (clientId) => `oauth:config:${clientId}`,
98
+ };
package/dist/config.d.ts CHANGED
@@ -13,5 +13,17 @@ declare const ConfigSchema: z.ZodObject<{
13
13
  defaultStoreCode: string;
14
14
  }>;
15
15
  export type Config = z.infer<typeof ConfigSchema>;
16
+ /**
17
+ * Sets a config override for the current request (OAuth mode)
18
+ */
19
+ export declare function setConfigOverride(override: {
20
+ apiUrl: string;
21
+ apiToken: string;
22
+ storeCode: string;
23
+ }): void;
24
+ /**
25
+ * Clears the config override after request completes
26
+ */
27
+ export declare function clearConfigOverride(): void;
16
28
  export declare function getConfig(): Config;
17
29
  export {};
package/dist/config.js CHANGED
@@ -5,7 +5,29 @@ const ConfigSchema = z.object({
5
5
  defaultStoreCode: z.string().min(1),
6
6
  });
7
7
  let config = null;
8
+ // Per-request config override for OAuth mode
9
+ let configOverride = null;
10
+ /**
11
+ * Sets a config override for the current request (OAuth mode)
12
+ */
13
+ export function setConfigOverride(override) {
14
+ configOverride = {
15
+ apiUrl: override.apiUrl,
16
+ apiToken: override.apiToken,
17
+ defaultStoreCode: override.storeCode,
18
+ };
19
+ }
20
+ /**
21
+ * Clears the config override after request completes
22
+ */
23
+ export function clearConfigOverride() {
24
+ configOverride = null;
25
+ }
8
26
  export function getConfig() {
27
+ // Return override if set (OAuth mode)
28
+ if (configOverride) {
29
+ return configOverride;
30
+ }
9
31
  if (config) {
10
32
  return config;
11
33
  }
package/dist/http.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/http.js ADDED
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import express from "express";
4
+ import cors from "cors";
5
+ import { randomUUID } from "crypto";
6
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
+ import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
8
+ import { createServer, SERVER_INSTRUCTIONS } from "./server.js";
9
+ import { getConfig, setConfigOverride, clearConfigOverride } from "./config.js";
10
+ import { createOAuthProvider, completeAuthorization, validateOpenLoyaltyCredentials, getClientConfig, } from "./auth/provider.js";
11
+ // Check if OAuth mode is enabled
12
+ const OAUTH_ENABLED = process.env.OAUTH_ENABLED === "true";
13
+ const BASE_URL = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
14
+ // In non-OAuth mode, validate config on startup
15
+ if (!OAUTH_ENABLED) {
16
+ try {
17
+ getConfig();
18
+ }
19
+ catch (error) {
20
+ console.error("Configuration error:", error instanceof Error ? error.message : error);
21
+ process.exit(1);
22
+ }
23
+ }
24
+ const app = express();
25
+ // CORS for ChatGPT
26
+ app.use(cors({
27
+ origin: "*",
28
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
29
+ allowedHeaders: ["Content-Type", "Authorization", "MCP-Session-Id", "MCP-Protocol-Version"],
30
+ exposedHeaders: ["MCP-Session-Id"],
31
+ }));
32
+ app.use(express.json());
33
+ // Store transports by session ID for stateful connections
34
+ const transports = new Map();
35
+ // Health check endpoint
36
+ app.get("/health", (_req, res) => {
37
+ res.json({ status: "ok", server: "openloyalty-mcp", oauth: OAUTH_ENABLED });
38
+ });
39
+ // OAuth mode setup
40
+ if (OAUTH_ENABLED) {
41
+ const provider = createOAuthProvider(BASE_URL);
42
+ // Add MCP SDK auth router (handles /.well-known/*, /authorize, /token, /register)
43
+ app.use(mcpAuthRouter({
44
+ provider,
45
+ issuerUrl: new URL(BASE_URL),
46
+ baseUrl: new URL(BASE_URL),
47
+ serviceDocumentationUrl: new URL("https://github.com/OpenLoyalty/openloyalty-mcp"),
48
+ }));
49
+ // Authorization form submission endpoint
50
+ app.post("/authorize/submit", async (req, res) => {
51
+ const { session_id, api_url, api_token, store_code } = req.body;
52
+ if (!session_id || !api_url || !api_token || !store_code) {
53
+ res.status(400).json({ error: "Missing required fields" });
54
+ return;
55
+ }
56
+ const config = {
57
+ apiUrl: api_url.replace(/\/$/, ""),
58
+ apiToken: api_token,
59
+ storeCode: store_code,
60
+ };
61
+ // Validate credentials
62
+ const validation = await validateOpenLoyaltyCredentials(config);
63
+ if (!validation.valid) {
64
+ res.status(400).json({ error: validation.error });
65
+ return;
66
+ }
67
+ // Complete authorization
68
+ const result = await completeAuthorization(session_id, config);
69
+ if ("error" in result) {
70
+ res.status(400).json({ error: result.error });
71
+ return;
72
+ }
73
+ res.json({ redirect_url: result.redirectUrl });
74
+ });
75
+ // Auth middleware for /mcp endpoint
76
+ const authMiddleware = async (req, res, next) => {
77
+ const authHeader = req.headers.authorization;
78
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
79
+ res.status(401).json({ error: "Missing or invalid Authorization header" });
80
+ return;
81
+ }
82
+ const token = authHeader.slice(7);
83
+ try {
84
+ const authInfo = await provider.verifyAccessToken(token);
85
+ // Get client's Open Loyalty config
86
+ const config = await getClientConfig(authInfo.clientId);
87
+ if (!config) {
88
+ res.status(401).json({ error: "Open Loyalty not configured. Please re-authorize." });
89
+ return;
90
+ }
91
+ // Set config override for this request
92
+ setConfigOverride(config);
93
+ // Store clientId for cleanup
94
+ req.clientId = authInfo.clientId;
95
+ next();
96
+ }
97
+ catch (error) {
98
+ res.status(401).json({
99
+ error: error instanceof Error ? error.message : "Authentication failed",
100
+ });
101
+ }
102
+ };
103
+ // Apply auth middleware to /mcp
104
+ app.use("/mcp", authMiddleware);
105
+ }
106
+ // MCP endpoint - handles both initialization and messages
107
+ app.all("/mcp", async (req, res) => {
108
+ const sessionId = req.headers["mcp-session-id"];
109
+ try {
110
+ // Handle GET requests for SSE streams
111
+ if (req.method === "GET") {
112
+ if (!sessionId || !transports.has(sessionId)) {
113
+ res.status(400).json({ error: "Invalid or missing session ID for SSE stream" });
114
+ return;
115
+ }
116
+ const transport = transports.get(sessionId);
117
+ await transport.handleRequest(req, res);
118
+ return;
119
+ }
120
+ // Handle DELETE requests for session cleanup
121
+ if (req.method === "DELETE") {
122
+ if (sessionId && transports.has(sessionId)) {
123
+ const transport = transports.get(sessionId);
124
+ await transport.close();
125
+ transports.delete(sessionId);
126
+ res.status(204).send();
127
+ }
128
+ else {
129
+ res.status(404).json({ error: "Session not found" });
130
+ }
131
+ return;
132
+ }
133
+ // Handle POST requests
134
+ if (req.method === "POST") {
135
+ // Check if this is an initialization request (no session ID)
136
+ if (!sessionId) {
137
+ // Create new session
138
+ const newSessionId = randomUUID();
139
+ const transport = new StreamableHTTPServerTransport({
140
+ sessionIdGenerator: () => newSessionId,
141
+ });
142
+ // Create and connect server
143
+ const server = createServer();
144
+ await server.connect(transport);
145
+ // Store transport for future requests
146
+ transports.set(newSessionId, transport);
147
+ // Clean up on close
148
+ transport.onclose = () => {
149
+ transports.delete(newSessionId);
150
+ };
151
+ // Handle the request
152
+ await transport.handleRequest(req, res, req.body);
153
+ return;
154
+ }
155
+ // Existing session - route to stored transport
156
+ const transport = transports.get(sessionId);
157
+ if (!transport) {
158
+ res.status(404).json({ error: "Session not found. Initialize a new session first." });
159
+ return;
160
+ }
161
+ await transport.handleRequest(req, res, req.body);
162
+ return;
163
+ }
164
+ // Unsupported method
165
+ res.status(405).json({ error: "Method not allowed" });
166
+ }
167
+ finally {
168
+ // Clean up config override in OAuth mode
169
+ if (OAUTH_ENABLED) {
170
+ clearConfigOverride();
171
+ }
172
+ }
173
+ });
174
+ // Server info endpoint
175
+ app.get("/", (_req, res) => {
176
+ const endpoints = {
177
+ mcp: "/mcp",
178
+ health: "/health",
179
+ };
180
+ if (OAUTH_ENABLED) {
181
+ endpoints.authorize = "/authorize";
182
+ endpoints.token = "/token";
183
+ endpoints.register = "/register";
184
+ endpoints.oauth_metadata = "/.well-known/oauth-authorization-server";
185
+ }
186
+ res.json({
187
+ name: "Open Loyalty MCP Server",
188
+ version: "1.0.0",
189
+ transport: "streamable-http",
190
+ oauth: OAUTH_ENABLED,
191
+ endpoints,
192
+ instructions: SERVER_INSTRUCTIONS.slice(0, 500) + "...",
193
+ });
194
+ });
195
+ const PORT = parseInt(process.env.MCP_HTTP_PORT || process.env.PORT || "3000", 10);
196
+ app.listen(PORT, () => {
197
+ console.log(`Open Loyalty MCP HTTP Server running on port ${PORT}`);
198
+ console.log(` - MCP endpoint: http://localhost:${PORT}/mcp`);
199
+ console.log(` - Health check: http://localhost:${PORT}/health`);
200
+ console.log(` - Server info: http://localhost:${PORT}/`);
201
+ if (OAUTH_ENABLED) {
202
+ console.log("");
203
+ console.log("OAuth 2.1 enabled:");
204
+ console.log(` - Authorize: ${BASE_URL}/authorize`);
205
+ console.log(` - Token: ${BASE_URL}/token`);
206
+ console.log(` - Register: ${BASE_URL}/register`);
207
+ console.log(` - Metadata: ${BASE_URL}/.well-known/oauth-authorization-server`);
208
+ }
209
+ else {
210
+ console.log("");
211
+ console.log("OAuth disabled. Using environment variables for API credentials.");
212
+ console.log("Set OAUTH_ENABLED=true and BASE_URL for OAuth mode.");
213
+ }
214
+ });
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import "dotenv/config";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "dotenv/config";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import { createServer } from "./server.js";
4
5
  import { getConfig } from "./config.js";
package/package.json CHANGED
@@ -1,18 +1,10 @@
1
1
  {
2
2
  "name": "@open-loyalty/mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "MCP server for Open Loyalty API - enables AI agents to manage loyalty programs, members, points, rewards, and transactions",
6
6
  "author": "Marcin Dyguda <md@openloyalty.io>",
7
7
  "license": "MIT",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/openloyalty/mcp-server.git"
11
- },
12
- "homepage": "https://github.com/openloyalty/mcp-server#readme",
13
- "bugs": {
14
- "url": "https://github.com/openloyalty/mcp-server/issues"
15
- },
16
8
  "keywords": [
17
9
  "mcp",
18
10
  "model-context-protocol",
@@ -25,7 +17,8 @@
25
17
  ],
26
18
  "main": "dist/index.js",
27
19
  "bin": {
28
- "openloyalty-mcp": "./dist/index.js"
20
+ "openloyalty-mcp": "./dist/index.js",
21
+ "openloyalty-mcp-http": "./dist/http.js"
29
22
  },
30
23
  "files": [
31
24
  "dist",
@@ -36,7 +29,9 @@
36
29
  "build": "tsc",
37
30
  "prepublishOnly": "npm run build",
38
31
  "start": "node dist/index.js",
32
+ "start:http": "node dist/http.js",
39
33
  "dev": "tsx src/index.ts",
34
+ "dev:http": "tsx src/http.ts",
40
35
  "typecheck": "tsc --noEmit",
41
36
  "test": "vitest",
42
37
  "test:run": "vitest run",
@@ -46,9 +41,15 @@
46
41
  "dependencies": {
47
42
  "@modelcontextprotocol/sdk": "^1.0.0",
48
43
  "axios": "^1.6.0",
44
+ "cors": "^2.8.5",
45
+ "dotenv": "^17.2.3",
46
+ "express": "^5.2.1",
47
+ "ioredis": "^5.9.2",
49
48
  "zod": "^3.22.0"
50
49
  },
51
50
  "devDependencies": {
51
+ "@types/cors": "^2.8.19",
52
+ "@types/express": "^5.0.6",
52
53
  "@types/node": "^20.10.0",
53
54
  "@vitest/coverage-v8": "^4.0.17",
54
55
  "axios-mock-adapter": "^2.1.0",
@@ -1 +0,0 @@
1
- export {};
@@ -1,213 +0,0 @@
1
- // src/tools/member.test.ts
2
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
- import { setupMockAxios, teardownMockAxios, getMockAxios } from '../../tests/mocks/http.mock.js';
4
- import { memberFixtures } from '../../tests/fixtures/member.fixtures.js';
5
- import { memberCreate, memberGet, memberList, memberUpdate, memberActivate, memberDeactivate, memberDelete, memberGetTierProgress, memberAssignTier, memberRemoveManualTier, } from './member.js';
6
- describe('Member Operations', () => {
7
- beforeEach(() => {
8
- setupMockAxios();
9
- });
10
- afterEach(() => {
11
- teardownMockAxios();
12
- });
13
- describe('memberCreate', () => {
14
- it('should create member with required fields only', async () => {
15
- const mockAxios = getMockAxios();
16
- mockAxios.onPost('/default/member').reply(200, memberFixtures.createResponse);
17
- const result = await memberCreate({
18
- email: 'test@example.com',
19
- });
20
- expect(result).toEqual({
21
- memberId: '550e8400-e29b-41d4-a716-446655440000',
22
- loyaltyCardNumber: 'CARD123456789',
23
- email: 'test@example.com',
24
- });
25
- // Verify request body format with customer wrapper
26
- const requestData = JSON.parse(mockAxios.history.post[0].data);
27
- expect(requestData).toEqual({
28
- customer: {
29
- email: 'test@example.com',
30
- },
31
- });
32
- });
33
- it('should create member with all optional fields', async () => {
34
- const mockAxios = getMockAxios();
35
- mockAxios.onPost('/default/member').reply(200, {
36
- customerId: 'full-uuid',
37
- loyaltyCardNumber: 'CUSTOM123',
38
- email: 'full@example.com',
39
- });
40
- const result = await memberCreate({
41
- email: 'full@example.com',
42
- firstName: 'John',
43
- lastName: 'Doe',
44
- phone: '+1234567890',
45
- birthDate: '1990-01-15',
46
- gender: 'male',
47
- loyaltyCardNumber: 'CUSTOM123',
48
- agreement1: true,
49
- agreement2: false,
50
- agreement3: true,
51
- address: {
52
- street: '123 Main St',
53
- city: 'New York',
54
- postal: '10001',
55
- country: 'US',
56
- },
57
- });
58
- expect(result.memberId).toBe('full-uuid');
59
- // Verify all fields in request
60
- const requestData = JSON.parse(mockAxios.history.post[0].data);
61
- expect(requestData.customer.firstName).toBe('John');
62
- expect(requestData.customer.lastName).toBe('Doe');
63
- expect(requestData.customer.address.city).toBe('New York');
64
- expect(requestData.customer.agreement1).toBe(true);
65
- expect(requestData.customer.agreement2).toBe(false);
66
- });
67
- it('should use custom storeCode when provided', async () => {
68
- const mockAxios = getMockAxios();
69
- mockAxios.onPost('/custom-store/member').reply(200, {
70
- customerId: 'uuid',
71
- email: 'test@example.com',
72
- });
73
- await memberCreate({
74
- storeCode: 'custom-store',
75
- email: 'test@example.com',
76
- });
77
- expect(mockAxios.history.post[0].url).toBe('/custom-store/member');
78
- });
79
- it('should throw formatted error on API error', async () => {
80
- const mockAxios = getMockAxios();
81
- mockAxios.onPost('/default/member').reply(400, {
82
- message: 'Email already exists',
83
- code: 400,
84
- });
85
- await expect(memberCreate({ email: 'duplicate@example.com' }))
86
- .rejects.toThrow();
87
- });
88
- });
89
- describe('memberGet', () => {
90
- it('should get member details and map customerId to memberId', async () => {
91
- const mockAxios = getMockAxios();
92
- mockAxios.onGet('/default/member/member-uuid').reply(200, memberFixtures.getResponse);
93
- const result = await memberGet({ memberId: 'member-uuid' });
94
- expect(result.memberId).toBe('550e8400-e29b-41d4-a716-446655440000');
95
- expect(result.email).toBe('test@example.com');
96
- expect(result.firstName).toBe('John');
97
- expect(result.levelName).toBe('Silver');
98
- });
99
- it('should use custom storeCode', async () => {
100
- const mockAxios = getMockAxios();
101
- mockAxios.onGet('/other-store/member/id123').reply(200, memberFixtures.getResponse);
102
- await memberGet({ storeCode: 'other-store', memberId: 'id123' });
103
- expect(mockAxios.history.get[0].url).toBe('/other-store/member/id123');
104
- });
105
- });
106
- describe('memberList', () => {
107
- it('should list members and map customerId to memberId', async () => {
108
- const mockAxios = getMockAxios();
109
- mockAxios.onGet('/default/member').reply(200, memberFixtures.listResponse);
110
- const result = await memberList({});
111
- expect(result.members).toHaveLength(2);
112
- expect(result.members[0].memberId).toBe('uuid-1');
113
- expect(result.members[0].email).toBe('user1@example.com');
114
- expect(result.total.all).toBe(100);
115
- expect(result.total.filtered).toBe(2);
116
- });
117
- it('should include pagination params with correct names', async () => {
118
- const mockAxios = getMockAxios();
119
- mockAxios.onGet(/\/default\/member/).reply(200, memberFixtures.listResponse);
120
- await memberList({ page: 2, perPage: 25 });
121
- const url = mockAxios.history.get[0].url;
122
- expect(url).toContain('_page=2');
123
- expect(url).toContain('_itemsOnPage=25');
124
- });
125
- it('should include filter params', async () => {
126
- const mockAxios = getMockAxios();
127
- mockAxios.onGet(/\/default\/member/).reply(200, memberFixtures.listResponse);
128
- await memberList({
129
- email: 'test@example.com',
130
- firstName: 'John',
131
- active: true,
132
- });
133
- const url = mockAxios.history.get[0].url;
134
- expect(url).toContain('email=test%40example.com');
135
- expect(url).toContain('firstName=John');
136
- expect(url).toContain('active=true');
137
- });
138
- });
139
- describe('memberUpdate', () => {
140
- it('should update member with customer wrapper', async () => {
141
- const mockAxios = getMockAxios();
142
- mockAxios.onPut('/default/member/member-uuid').reply(200);
143
- await memberUpdate({
144
- memberId: 'member-uuid',
145
- firstName: 'Jane',
146
- lastName: 'Smith',
147
- });
148
- const requestData = JSON.parse(mockAxios.history.put[0].data);
149
- expect(requestData).toEqual({
150
- customer: {
151
- firstName: 'Jane',
152
- lastName: 'Smith',
153
- },
154
- });
155
- });
156
- });
157
- describe('memberActivate', () => {
158
- it('should call activate endpoint', async () => {
159
- const mockAxios = getMockAxios();
160
- mockAxios.onPost('/default/member/member-uuid/activate').reply(200);
161
- await memberActivate({ memberId: 'member-uuid' });
162
- expect(mockAxios.history.post[0].url).toBe('/default/member/member-uuid/activate');
163
- });
164
- });
165
- describe('memberDeactivate', () => {
166
- it('should call deactivate endpoint', async () => {
167
- const mockAxios = getMockAxios();
168
- mockAxios.onPost('/default/member/member-uuid/deactivate').reply(200);
169
- await memberDeactivate({ memberId: 'member-uuid' });
170
- expect(mockAxios.history.post[0].url).toBe('/default/member/member-uuid/deactivate');
171
- });
172
- });
173
- describe('memberDelete', () => {
174
- it('should call delete endpoint', async () => {
175
- const mockAxios = getMockAxios();
176
- mockAxios.onDelete('/default/member/member-uuid').reply(200);
177
- await memberDelete({ memberId: 'member-uuid' });
178
- expect(mockAxios.history.delete[0].url).toBe('/default/member/member-uuid');
179
- });
180
- });
181
- describe('memberGetTierProgress', () => {
182
- it('should get tier progress information', async () => {
183
- const mockAxios = getMockAxios();
184
- mockAxios.onGet('/default/member/member-uuid/tier').reply(200, memberFixtures.tierProgressResponse);
185
- const result = await memberGetTierProgress({ memberId: 'member-uuid' });
186
- expect(result.currentTier?.name).toBe('Silver');
187
- expect(result.nextTier?.name).toBe('Gold');
188
- expect(result.currentValue).toBe(750);
189
- expect(result.requiredValue).toBe(1000);
190
- expect(result.progressPercent).toBe(75);
191
- });
192
- });
193
- describe('memberAssignTier', () => {
194
- it('should assign tier to member', async () => {
195
- const mockAxios = getMockAxios();
196
- mockAxios.onPost('/default/member/member-uuid/tier').reply(200);
197
- await memberAssignTier({
198
- memberId: 'member-uuid',
199
- levelId: 'gold-level-uuid',
200
- });
201
- const requestData = JSON.parse(mockAxios.history.post[0].data);
202
- expect(requestData.levelId).toBe('gold-level-uuid');
203
- });
204
- });
205
- describe('memberRemoveManualTier', () => {
206
- it('should remove manual tier assignment', async () => {
207
- const mockAxios = getMockAxios();
208
- mockAxios.onDelete('/default/member/member-uuid/tier').reply(200);
209
- await memberRemoveManualTier({ memberId: 'member-uuid' });
210
- expect(mockAxios.history.delete[0].url).toBe('/default/member/member-uuid/tier');
211
- });
212
- });
213
- });
@@ -1 +0,0 @@
1
- export {};