import { AuthMiddleware } from '../../src/middleware/auth.js';
import { createHmac } from 'node:crypto';
const TEST_API_KEY = 'test-shop-key';
const TEST_API_SECRET = 'test-shop-secret';
function createMiddleware(): AuthMiddleware {
return new AuthMiddleware({ apiKey: TEST_API_KEY, apiSecret: TEST_API_SECRET });
}
/**
* Helper: compute Shopify-style HMAC for a set of query params.
* Filters out "hmac" and "signature", sorts by key, joins with &,
* then HMAC-SHA256 with the secret.
*/
function computeHMAC(
params: Record<string, string>,
secret: string,
): string {
const entries = Object.entries(params)
.filter(([key]) => key !== 'hmac' && key !== 'signature')
.sort(([a], [b]) => a.localeCompare(b));
const message = entries.map(([key, value]) => `${key}=${value}`).join('&');
return createHmac('sha256', secret).update(message).digest('hex');
}
// ─── validateShopifyHMAC ───
describe('AuthMiddleware.validateShopifyHMAC', () => {
const auth = createMiddleware();
it('returns true for a valid HMAC', () => {
const params: Record<string, string> = {
code: 'abc123',
shop: 'myshop.myshopify.com',
timestamp: '1234567890',
};
const hmac = computeHMAC(params, TEST_API_SECRET);
params['hmac'] = hmac;
expect(auth.validateShopifyHMAC(params)).toBe(true);
});
it('returns false for an invalid HMAC', () => {
const params: Record<string, string> = {
code: 'abc123',
shop: 'myshop.myshopify.com',
timestamp: '1234567890',
hmac: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
};
expect(auth.validateShopifyHMAC(params)).toBe(false);
});
it('returns false when hmac param is missing', () => {
const params: Record<string, string> = {
code: 'abc123',
shop: 'myshop.myshopify.com',
timestamp: '1234567890',
};
expect(auth.validateShopifyHMAC(params)).toBe(false);
});
it('ignores the signature param when computing the HMAC', () => {
const paramsWithSignature: Record<string, string> = {
code: 'abc123',
shop: 'myshop.myshopify.com',
signature: 'should-be-ignored',
timestamp: '1234567890',
};
// computeHMAC also strips "signature", so the HMAC should match
const hmac = computeHMAC(paramsWithSignature, TEST_API_SECRET);
paramsWithSignature['hmac'] = hmac;
expect(auth.validateShopifyHMAC(paramsWithSignature)).toBe(true);
});
it('returns false for tampered query params', () => {
const params: Record<string, string> = {
code: 'abc123',
shop: 'myshop.myshopify.com',
timestamp: '1234567890',
};
const hmac = computeHMAC(params, TEST_API_SECRET);
params['hmac'] = hmac;
// Tamper after computing the HMAC
params['code'] = 'tampered';
expect(auth.validateShopifyHMAC(params)).toBe(false);
});
});
// ─── validateAccessToken ───
describe('AuthMiddleware.validateAccessToken', () => {
const auth = createMiddleware();
afterEach(() => {
vi.restoreAllMocks();
});
it('returns false for an empty token', async () => {
const result = await auth.validateAccessToken('');
expect(result).toBe(false);
});
it('returns true when Shopify API responds with ok=true', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: true }),
);
const result = await auth.validateAccessToken('shpat_valid_token');
expect(result).toBe(true);
expect(fetch).toHaveBeenCalledWith(
`https://${TEST_API_KEY}.myshopify.com/admin/api/2024-01/shop.json`,
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'X-Shopify-Access-Token': 'shpat_valid_token',
}),
}),
);
});
it('returns false when Shopify API responds with ok=false', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: false, status: 401 }),
);
const result = await auth.validateAccessToken('shpat_expired_token');
expect(result).toBe(false);
});
it('returns false when fetch throws a network error', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockRejectedValue(new Error('Network unreachable')),
);
const result = await auth.validateAccessToken('shpat_some_token');
expect(result).toBe(false);
});
});
// ─── validateUCPProfile ───
describe('AuthMiddleware.validateUCPProfile', () => {
const auth = createMiddleware();
afterEach(() => {
vi.restoreAllMocks();
});
it('returns false for an empty URL', async () => {
const result = await auth.validateUCPProfile('');
expect(result).toBe(false);
});
it('returns false for an HTTP (non-HTTPS) URL', async () => {
const result = await auth.validateUCPProfile('http://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns true for a valid HTTPS profile with proper structure', async () => {
const validProfile = {
version: '2026-01-11',
services: [
{
type: 'dev.ucp.shopping',
transports: [{ type: 'rest', endpoint: 'https://example.com/api' }],
capabilities: [{ name: 'dev.ucp.shopping.checkout', version: '2026-01-11' }],
},
],
};
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => validProfile,
}),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(true);
});
it('returns false if the profile has no services array', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ version: '2026-01-11' }),
}),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns false if services array is empty', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ version: '2026-01-11', services: [] }),
}),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns false if a service is missing the type field', async () => {
const profileMissingType = {
version: '2026-01-11',
services: [
{
transports: [{ type: 'rest', endpoint: 'https://example.com/api' }],
capabilities: [{ name: 'dev.ucp.shopping.checkout', version: '2026-01-11' }],
},
],
};
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => profileMissingType,
}),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns false if a service has empty transports array', async () => {
const profileEmptyTransports = {
version: '2026-01-11',
services: [
{
type: 'dev.ucp.shopping',
transports: [],
capabilities: [{ name: 'dev.ucp.shopping.checkout', version: '2026-01-11' }],
},
],
};
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => profileEmptyTransports,
}),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns false if a service has empty capabilities array', async () => {
const profileEmptyCaps = {
version: '2026-01-11',
services: [
{
type: 'dev.ucp.shopping',
transports: [{ type: 'rest', endpoint: 'https://example.com/api' }],
capabilities: [],
},
],
};
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => profileEmptyCaps,
}),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns false when the profile version is missing', async () => {
const profileNoVersion = {
services: [
{
type: 'dev.ucp.shopping',
transports: [{ type: 'rest', endpoint: 'https://example.com/api' }],
capabilities: [{ name: 'dev.ucp.shopping.checkout', version: '2026-01-11' }],
},
],
};
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => profileNoVersion,
}),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns false when fetch response is not ok', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
it('returns false when fetch throws an error', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockRejectedValue(new Error('DNS resolution failed')),
);
const result = await auth.validateUCPProfile('https://example.com/.well-known/ucp.json');
expect(result).toBe(false);
});
});
// ─── extractAgentProfile ───
describe('AuthMiddleware.extractAgentProfile', () => {
const auth = createMiddleware();
it('parses a valid UCP-Agent header with profile, cap, and cred', () => {
const headers = {
'UCP-Agent':
'profile="https://agent.example.com/.well-known/ucp.json";cap="dev.ucp.shopping.checkout,dev.ucp.shopping.catalog";cred="urn:ietf:params:oauth:grant-type:jwt-bearer"',
};
const profile = auth.extractAgentProfile(headers);
expect(profile).not.toBeNull();
expect(profile!.profile_url).toBe('https://agent.example.com/.well-known/ucp.json');
expect(profile!.capabilities).toEqual([
'dev.ucp.shopping.checkout',
'dev.ucp.shopping.catalog',
]);
expect(profile!.credentials).toEqual([
'urn:ietf:params:oauth:grant-type:jwt-bearer',
]);
});
it('returns null when the UCP-Agent header is missing', () => {
const headers = {
'Content-Type': 'application/json',
Authorization: 'Bearer token123',
};
const profile = auth.extractAgentProfile(headers);
expect(profile).toBeNull();
});
it('returns null when the profile= value is missing from the header', () => {
const headers = {
'UCP-Agent': 'cap="dev.ucp.shopping.checkout";cred="some-cred"',
};
const profile = auth.extractAgentProfile(headers);
expect(profile).toBeNull();
});
it('performs case-insensitive header key lookup', () => {
const headers = {
'ucp-agent':
'profile="https://agent.example.com/ucp.json";cap="checkout";cred="jwt"',
};
const profile = auth.extractAgentProfile(headers);
expect(profile).not.toBeNull();
expect(profile!.profile_url).toBe('https://agent.example.com/ucp.json');
});
it('handles mixed-case header keys', () => {
const headers = {
'Ucp-Agent':
'profile="https://agent.example.com/ucp.json";cap="checkout";cred="jwt"',
};
const profile = auth.extractAgentProfile(headers);
expect(profile).not.toBeNull();
expect(profile!.profile_url).toBe('https://agent.example.com/ucp.json');
});
it('returns empty arrays when cap and cred are missing', () => {
const headers = {
'UCP-Agent': 'profile="https://agent.example.com/ucp.json"',
};
const profile = auth.extractAgentProfile(headers);
expect(profile).not.toBeNull();
expect(profile!.profile_url).toBe('https://agent.example.com/ucp.json');
expect(profile!.capabilities).toEqual([]);
expect(profile!.credentials).toEqual([]);
});
it('parses multiple capabilities separated by commas', () => {
const headers = {
'UCP-Agent':
'profile="https://agent.example.com/ucp.json";cap="cap1,cap2,cap3"',
};
const profile = auth.extractAgentProfile(headers);
expect(profile).not.toBeNull();
expect(profile!.capabilities).toEqual(['cap1', 'cap2', 'cap3']);
});
it('returns null for completely empty headers object', () => {
const profile = auth.extractAgentProfile({});
expect(profile).toBeNull();
});
});