/**
* Tests for src/dynamo/factory.ts — uncovered paths
*
* Targets:
* - Lambda mode: DynamoSessionStore + DynamoMandateStore creation (lines 54-56, 64-66)
* - Shopify API init success (lines 92-100)
* - Shopify API init failure / catch branch (lines 101-104)
* - setDeps() (lines 130-132)
*
* The basic getDeps / resetDeps paths are already tested in tests/integration/wiring.test.ts
* so we do NOT duplicate those here.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// ─── Hoisted mocks (available inside vi.mock factories) ───
const { mockShopifyClient, mockStorefrontAPI, mockAdminAPI } = vi.hoisted(() => ({
mockShopifyClient: vi.fn().mockImplementation(() => ({
adminQuery: vi.fn(),
storefrontQuery: vi.fn(),
})),
mockStorefrontAPI: vi.fn().mockImplementation(() => ({
__type: 'StorefrontAPI',
})),
mockAdminAPI: vi.fn().mockImplementation(() => ({
__type: 'AdminAPI',
})),
}));
// ─── Module mocks ───
// Mock DynamoDB stores to avoid real AWS SDK calls
vi.mock('../../src/dynamo/session-store.js', () => ({
DynamoSessionStore: vi.fn().mockImplementation((table: string) => ({
__type: 'DynamoSessionStore',
__table: table,
create: vi.fn(),
update: vi.fn(),
getStatus: vi.fn(),
transition: vi.fn(),
get: vi.fn(),
})),
}));
vi.mock('../../src/dynamo/mandate-store.js', () => ({
DynamoMandateStore: vi.fn().mockImplementation((table: string) => ({
__type: 'DynamoMandateStore',
__table: table,
store: vi.fn(),
get: vi.fn(),
getByCheckout: vi.fn(),
revoke: vi.fn(),
cleanup: vi.fn(),
})),
}));
vi.mock('../../src/shopify/client.js', () => ({
ShopifyClient: mockShopifyClient,
}));
vi.mock('../../src/shopify/storefront.js', () => ({
StorefrontAPI: mockStorefrontAPI,
}));
vi.mock('../../src/shopify/admin.js', () => ({
AdminAPI: mockAdminAPI,
}));
// Mock logger to suppress output and allow assertion
vi.mock('../../src/utils/logger.js', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
// ─── Imports (after mocks are declared) ───
import { getDeps, resetDeps, setDeps } from '../../src/dynamo/factory.js';
import type { GatewayDeps } from '../../src/dynamo/factory.js';
import { DynamoSessionStore } from '../../src/dynamo/session-store.js';
import { DynamoMandateStore } from '../../src/dynamo/mandate-store.js';
import { logger } from '../../src/utils/logger.js';
// ─── Env Var Helpers ───
const savedEnv: Record<string, string | undefined> = {};
function setEnv(vars: Record<string, string>) {
for (const [key, value] of Object.entries(vars)) {
savedEnv[key] = process.env[key];
process.env[key] = value;
}
}
function restoreEnv() {
for (const [key, value] of Object.entries(savedEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
// Clear the saved map
for (const key of Object.keys(savedEnv)) {
delete savedEnv[key];
}
}
// ─── Tests ───
describe('factory — uncovered paths', () => {
beforeEach(() => {
resetDeps();
vi.clearAllMocks();
});
afterEach(() => {
restoreEnv();
resetDeps();
});
// ── Lambda mode: DynamoDB stores ──
describe('Lambda mode with DynamoDB tables', () => {
it('creates DynamoSessionStore and DynamoMandateStore when running on Lambda', () => {
setEnv({
AWS_LAMBDA_FUNCTION_NAME: 'my-function',
DYNAMODB_TABLE_SESSIONS: 'prod-sessions',
DYNAMODB_TABLE_MANDATES: 'prod-mandates',
});
const deps = getDeps();
// DynamoSessionStore should have been constructed with the table name
expect(DynamoSessionStore).toHaveBeenCalledWith('prod-sessions');
expect((deps.sessionManager as unknown as Record<string, unknown>)['__type']).toBe(
'DynamoSessionStore',
);
expect((deps.sessionManager as unknown as Record<string, unknown>)['__table']).toBe(
'prod-sessions',
);
// DynamoMandateStore should have been constructed with the table name
expect(DynamoMandateStore).toHaveBeenCalledWith('prod-mandates');
expect((deps.mandateStore as unknown as Record<string, unknown>)['__type']).toBe(
'DynamoMandateStore',
);
expect((deps.mandateStore as unknown as Record<string, unknown>)['__table']).toBe(
'prod-mandates',
);
// Logger should report DynamoDB stores
expect(logger.info).toHaveBeenCalledWith(
'Factory: Using DynamoDB session store',
{ table: 'prod-sessions' },
);
expect(logger.info).toHaveBeenCalledWith(
'Factory: Using DynamoDB mandate store',
{ table: 'prod-mandates' },
);
});
});
// ── Shopify API init success ──
describe('Shopify API initialization success', () => {
it('creates StorefrontAPI and AdminAPI when Shopify credentials are configured', () => {
setEnv({
SHOPIFY_STORE_DOMAIN: 'test-shop.myshopify.com',
SHOPIFY_ACCESS_TOKEN: 'shpat_test123',
SHOPIFY_STOREFRONT_TOKEN: 'sf_token_456',
});
const deps = getDeps();
// ShopifyClient should have been constructed with the credentials
expect(mockShopifyClient).toHaveBeenCalledWith({
storeDomain: 'test-shop.myshopify.com',
accessToken: 'shpat_test123',
storefrontToken: 'sf_token_456',
});
// StorefrontAPI and AdminAPI should have been instantiated
expect(mockStorefrontAPI).toHaveBeenCalledTimes(1);
expect(mockAdminAPI).toHaveBeenCalledTimes(1);
// The deps should have non-null API objects
expect(deps.storefrontAPI).not.toBeNull();
expect(deps.adminAPI).not.toBeNull();
expect(logger.info).toHaveBeenCalledWith(
'Factory: Shopify APIs initialized',
{ storeDomain: 'test-shop.myshopify.com' },
);
});
});
// ── Shopify API init failure ──
describe('Shopify API initialization failure', () => {
it('catches ShopifyClient constructor errors and sets APIs to null', () => {
setEnv({
SHOPIFY_STORE_DOMAIN: 'test-shop.myshopify.com',
SHOPIFY_ACCESS_TOKEN: 'shpat_test123',
SHOPIFY_STOREFRONT_TOKEN: 'sf_token_456',
});
// Make ShopifyClient throw on construction
mockShopifyClient.mockImplementationOnce(() => {
throw new Error('Network timeout');
});
const deps = getDeps();
// APIs should be null because the constructor threw
expect(deps.storefrontAPI).toBeNull();
expect(deps.adminAPI).toBeNull();
// Logger should have warned about the failure
expect(logger.warn).toHaveBeenCalledWith(
'Factory: Failed to initialize Shopify APIs',
{ error: 'Network timeout' },
);
});
it('handles non-Error throws gracefully', () => {
setEnv({
SHOPIFY_STORE_DOMAIN: 'test-shop.myshopify.com',
SHOPIFY_ACCESS_TOKEN: 'shpat_test123',
SHOPIFY_STOREFRONT_TOKEN: 'sf_token_456',
});
// Throw a non-Error value
mockShopifyClient.mockImplementationOnce(() => {
throw 'string-error';
});
const deps = getDeps();
expect(deps.storefrontAPI).toBeNull();
expect(deps.adminAPI).toBeNull();
expect(logger.warn).toHaveBeenCalledWith(
'Factory: Failed to initialize Shopify APIs',
{ error: 'string-error' },
);
});
});
// ── setDeps() ──
describe('setDeps()', () => {
it('injects deps so that getDeps() returns them without re-creating', () => {
const mockDeps = {
config: {} as GatewayDeps['config'],
sessionManager: { __mock: 'session' } as unknown as GatewayDeps['sessionManager'],
mandateStore: { __mock: 'mandate' } as unknown as GatewayDeps['mandateStore'],
feeCollector: { __mock: 'fee' } as unknown as GatewayDeps['feeCollector'],
verifier: { __mock: 'verifier' } as unknown as GatewayDeps['verifier'],
guardrail: { __mock: 'guardrail' } as unknown as GatewayDeps['guardrail'],
storefrontAPI: null,
adminAPI: null,
} as GatewayDeps;
setDeps(mockDeps);
const result = getDeps();
expect(result).toBe(mockDeps);
expect(result.sessionManager).toBe(mockDeps.sessionManager);
expect(result.mandateStore).toBe(mockDeps.mandateStore);
});
it('overrides previously created deps', () => {
// First create deps normally
const original = getDeps();
// Then override via setDeps
const override = {
config: {} as GatewayDeps['config'],
sessionManager: { __override: true } as unknown as GatewayDeps['sessionManager'],
mandateStore: { __override: true } as unknown as GatewayDeps['mandateStore'],
feeCollector: { __override: true } as unknown as GatewayDeps['feeCollector'],
verifier: { __override: true } as unknown as GatewayDeps['verifier'],
guardrail: { __override: true } as unknown as GatewayDeps['guardrail'],
storefrontAPI: null,
adminAPI: null,
} as GatewayDeps;
setDeps(override);
const result = getDeps();
expect(result).toBe(override);
expect(result).not.toBe(original);
});
});
});