Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/analytics-forwarding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Lettermint } from './lettermint';
import type * as Types from './types';

it('uses the new analytics and report forwarding contracts', async () => {
const payload: Types.AnalyticsRequest = {
metrics: ['accepted', 'delivered'],
include: ['summary', 'time_series'],
interval: 'day',
filters: [{ dimension: 'project', operator: 'eq', values: ['project/id'] }],
sort: { metric: 'accepted', direction: 'desc' },
limit: 10,
};
const resource = { destination: null, verified: false, verified_at: null };
const mockFetch = jest
.fn()
.mockResolvedValue({ ok: true, status: 200, json: async () => ({ data: resource }) });
global.fetch = mockFetch;
const api = Lettermint.api('team-token');
const analyticsResponse: Types.AnalyticsResponse = {
data: { summary: { metrics: { accepted: 12, delivery_rate: null } } },
meta: { timezone: 'UTC' },
pagination: { total_groups: 0, returned_groups: 0, next_cursor: null, truncated: false },
};
mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => analyticsResponse });
const analytics = await api.analytics(payload);
expect(analytics.meta.timezone).toBe('UTC');
expect(analytics.data.summary?.metrics?.delivery_rate).toBeNull();
expect((await api.projects.retrieveReportForwarding('project/id')).data).toEqual(resource);
await api.projects.updateReportForwarding('project/id', { destination: 'reports@example.com' });
await api.projects.verifyReportForwarding('project/id', { code: '123456' });
await api.projects.resendReportForwardingCode('project/id');
const decode = jest.fn(() => {
throw new Error('204 must not be decoded');
});
mockFetch.mockResolvedValueOnce({ ok: true, status: 204, json: decode });
await expect(api.projects.deleteReportForwarding('project/id')).resolves.toBeUndefined();
expect(decode).not.toHaveBeenCalled();
const calls = mockFetch.mock.calls;
expect(
calls.map(([url, options]) => [url.replace('https://api.lettermint.co/v1', ''), options.method])
).toEqual([
['/analytics', 'POST'],
['/projects/project%2Fid/report-forwarding', 'GET'],
['/projects/project%2Fid/report-forwarding', 'PUT'],
['/projects/project%2Fid/report-forwarding/verify', 'POST'],
['/projects/project%2Fid/report-forwarding/resend-code', 'POST'],
['/projects/project%2Fid/report-forwarding', 'DELETE'],
]);
expect(JSON.parse(calls[0][1].body)).toEqual(payload);
expect(JSON.parse(calls[2][1].body)).toEqual({ destination: 'reports@example.com' });
expect(JSON.parse(calls[3][1].body)).toEqual({ code: '123456' });
for (const [, options] of calls) {
expect(options.headers.Authorization).toBe('Bearer team-token');
expect(options.headers).not.toHaveProperty('x-lettermint-token');
}
});

it('keeps both suppression deletion results and project creation tokens', () => {
const deleted: Types.SuppressionDestroyResponse = {
success: true,
message: 'Deleted',
status: 'removed',
confidence: 0.9,
};
const review: Types.SuppressionDestroyResponse = {
success: true,
message: 'Review',
status: 'review_ticket_created',
ticket_identifier: 'ticket-1',
};
const created: Types.ProjectStoreResponse = {
data: {} as Types.ProjectData,
message: 'Created',
api_token: 'project-token',
};
expect(deleted.message).toBe('Deleted');
expect(review.ticket_identifier).toBe('ticket-1');
expect(created.api_token).toBe('project-token');
});
10 changes: 8 additions & 2 deletions src/api-surface.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ describe('public SDK surface', () => {

describe('api endpoint coverage', () => {
const documentedMethods = {
'v1.analytics': 'analytics',
getReportForwarding: 'projects.retrieveReportForwarding',
updateReportForwarding: 'projects.updateReportForwarding',
deleteReportForwarding: 'projects.deleteReportForwarding',
verifyReportForwarding: 'projects.verifyReportForwarding',
resendReportForwardingCode: 'projects.resendReportForwardingCode',
'domain.index': 'domains.list',
'domain.store': 'domains.create',
'domain.show': 'domains.retrieve',
Expand Down Expand Up @@ -224,7 +230,7 @@ describe('api endpoint coverage', () => {
it('exposes documented API operations', () => {
const api = Lettermint.api('api-token') as unknown as Record<string, unknown>;

expect(Object.keys(documentedMethods)).toHaveLength(50);
expect(Object.keys(documentedMethods)).toHaveLength(56);

for (const exposedPath of Object.values(documentedMethods)) {
const segments = exposedPath.split('.');
Expand Down Expand Up @@ -293,7 +299,7 @@ describe('generated api types', () => {
};
const sendResponse: Types.SendEmailResponse = {
message_id: 'message_123',
status: 'delivered',
status: 'pending',
sandbox: true,
sandbox_result: 'clicked',
};
Expand Down
1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export class LettermintClient {
headers,
});

if (response.status === 204) return undefined as T;
return response.json();
}
}
37 changes: 37 additions & 0 deletions src/endpoints/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,43 @@ export class MessagesEndpoint extends Endpoint {
}

export class ProjectsEndpoint extends Endpoint {
public async retrieveReportForwarding(
projectId: string
): Promise<Types.GetReportForwardingResponse> {
return this.httpClient.get(`/projects/${this.pathSegment(projectId)}/report-forwarding`);
}

public async updateReportForwarding(
projectId: string,
payload: Types.UpdateReportForwardingRequest
): Promise<Types.UpdateReportForwardingResponse> {
return this.httpClient.put(
`/projects/${this.pathSegment(projectId)}/report-forwarding`,
payload
);
}

public async deleteReportForwarding(projectId: string): Promise<void> {
await this.httpClient.delete(`/projects/${this.pathSegment(projectId)}/report-forwarding`);
}

public async verifyReportForwarding(
projectId: string,
payload: Types.VerifyReportForwardingRequest
): Promise<Types.VerifyReportForwardingResponse> {
return this.httpClient.post(
`/projects/${this.pathSegment(projectId)}/report-forwarding/verify`,
payload
);
}

public async resendReportForwardingCode(
projectId: string
): Promise<Types.ResendReportForwardingCodeResponse> {
return this.httpClient.post(
`/projects/${this.pathSegment(projectId)}/report-forwarding/resend-code`
);
}
public list(params?: QueryParams): Promise<Types.ProjectIndexResponse> {
return this.httpClient.get('/projects', { params });
}
Expand Down
4 changes: 4 additions & 0 deletions src/lettermint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export class ApiClient {
public async blockedFileTypes(): Promise<Types.BlockedFileTypesResponse> {
return this.client.get('/blocked-file-types');
}

public async analytics(payload: Types.AnalyticsRequest): Promise<Types.AnalyticsResponse> {
return this.client.post<Types.AnalyticsResponse>('/analytics', payload);
}
}

export default Lettermint;
Loading
Loading