Files
Krow-workspace/backend/command-api/test/mobile-routes.test.js

345 lines
12 KiB
JavaScript

import test, { beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import request from 'supertest';
import { createApp } from '../src/app.js';
import { __resetIdempotencyStoreForTests } from '../src/services/idempotency-store.js';
process.env.AUTH_BYPASS = 'true';
beforeEach(() => {
process.env.IDEMPOTENCY_STORE = 'memory';
delete process.env.IDEMPOTENCY_DATABASE_URL;
delete process.env.DATABASE_URL;
__resetIdempotencyStoreForTests();
});
function createMobileHandlers() {
return {
createClientOneTimeOrder: async (_actor, payload) => ({
orderId: 'order-1',
orderType: 'ONE_TIME',
eventName: payload.eventName,
}),
createClientRecurringOrder: async (_actor, payload) => ({
orderId: 'order-2',
orderType: 'RECURRING',
recurrenceDays: payload.recurrenceDays,
}),
createClientPermanentOrder: async (_actor, payload) => ({
orderId: 'order-3',
orderType: 'PERMANENT',
horizonDays: payload.horizonDays || 28,
}),
createEditedOrderCopy: async (_actor, payload) => ({
sourceOrderId: payload.orderId,
orderId: 'order-4',
cloned: true,
}),
cancelClientOrder: async (_actor, payload) => ({
orderId: payload.orderId,
status: 'CANCELLED',
}),
createHub: async (_actor, payload) => ({
hubId: 'hub-1',
name: payload.name,
costCenterId: payload.costCenterId,
}),
approveInvoice: async (_actor, payload) => ({
invoiceId: payload.invoiceId,
status: 'APPROVED',
}),
registerClientPushToken: async (_actor, payload) => ({
tokenId: 'push-token-client-1',
platform: payload.platform,
notificationsEnabled: payload.notificationsEnabled ?? true,
}),
unregisterClientPushToken: async () => ({
removedCount: 1,
}),
applyForShift: async (_actor, payload) => ({
shiftId: payload.shiftId,
status: 'APPLIED',
}),
registerStaffPushToken: async (_actor, payload) => ({
tokenId: 'push-token-staff-1',
platform: payload.platform,
notificationsEnabled: payload.notificationsEnabled ?? true,
}),
unregisterStaffPushToken: async () => ({
removedCount: 1,
}),
staffClockIn: async (_actor, payload) => ({
assignmentId: payload.assignmentId || 'assignment-1',
status: 'CLOCK_IN',
proofNonce: payload.proofNonce || null,
}),
staffClockOut: async (_actor, payload) => ({
assignmentId: payload.assignmentId || 'assignment-1',
status: 'CLOCK_OUT',
}),
submitLocationStreamBatch: async (_actor, payload) => ({
assignmentId: payload.assignmentId || 'assignment-1',
pointCount: payload.points.length,
status: 'RECORDED',
}),
saveTaxFormDraft: async (_actor, payload) => ({
formType: payload.formType,
status: 'DRAFT',
}),
addStaffBankAccount: async (_actor, payload) => ({
accountType: payload.accountType,
last4: payload.accountNumber.slice(-4),
}),
};
}
test('POST /commands/client/orders/one-time forwards one-time order payload', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/client/orders/one-time')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'client-order-1')
.send({
hubId: '11111111-1111-4111-8111-111111111111',
vendorId: '22222222-2222-4222-8222-222222222222',
eventName: 'Google Cafe Coverage',
orderDate: '2026-03-20',
positions: [
{
roleId: '33333333-3333-4333-8333-333333333333',
startTime: '09:00',
endTime: '17:00',
workerCount: 2,
hourlyRateCents: 2800,
},
],
});
assert.equal(res.status, 200);
assert.equal(res.body.orderId, 'order-1');
assert.equal(res.body.orderType, 'ONE_TIME');
assert.equal(res.body.eventName, 'Google Cafe Coverage');
});
test('POST /commands/client/orders/:orderId/edit injects order id from params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/client/orders/44444444-4444-4444-8444-444444444444/edit')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'client-order-edit-1')
.send({
eventName: 'Edited Order Copy',
});
assert.equal(res.status, 200);
assert.equal(res.body.sourceOrderId, '44444444-4444-4444-8444-444444444444');
assert.equal(res.body.cloned, true);
});
test('POST /commands/client/hubs returns injected hub response', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/client/hubs')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'hub-create-1')
.send({
tenantId: '11111111-1111-4111-8111-111111111111',
businessId: '22222222-2222-4222-8222-222222222222',
name: 'Google North Hub',
locationName: 'North Campus',
timezone: 'America/Los_Angeles',
latitude: 37.422,
longitude: -122.084,
geofenceRadiusMeters: 100,
clockInMode: 'GEO_REQUIRED',
allowClockInOverride: true,
costCenterId: '44444444-4444-4444-8444-444444444444',
});
assert.equal(res.status, 200);
assert.equal(res.body.hubId, 'hub-1');
assert.equal(res.body.name, 'Google North Hub');
});
test('POST /commands/client/billing/invoices/:invoiceId/approve injects invoice id from params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/client/billing/invoices/55555555-5555-4555-8555-555555555555/approve')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'invoice-approve-1')
.send({});
assert.equal(res.status, 200);
assert.equal(res.body.invoiceId, '55555555-5555-4555-8555-555555555555');
assert.equal(res.body.status, 'APPROVED');
});
test('POST /commands/client/devices/push-tokens registers a client push token', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/client/devices/push-tokens')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'client-push-token-1')
.send({
provider: 'FCM',
platform: 'IOS',
pushToken: 'f'.repeat(160),
deviceId: 'iphone-15-pro',
notificationsEnabled: true,
});
assert.equal(res.status, 200);
assert.equal(res.body.tokenId, 'push-token-client-1');
assert.equal(res.body.platform, 'IOS');
});
test('DELETE /commands/client/devices/push-tokens accepts tokenId from query params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.delete('/commands/client/devices/push-tokens?tokenId=11111111-1111-4111-8111-111111111111&reason=SMOKE_CLEANUP')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'client-push-token-delete-1');
assert.equal(res.status, 200);
assert.equal(res.body.removedCount, 1);
});
test('POST /commands/staff/shifts/:shiftId/apply injects shift id from params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/staff/shifts/66666666-6666-4666-8666-666666666666/apply')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'shift-apply-1')
.send({
note: 'Available tonight',
});
assert.equal(res.status, 200);
assert.equal(res.body.shiftId, '66666666-6666-4666-8666-666666666666');
assert.equal(res.body.status, 'APPLIED');
});
test('POST /commands/staff/clock-in accepts shift-based payload', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/staff/clock-in')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'clock-in-1')
.send({
shiftId: '77777777-7777-4777-8777-777777777777',
sourceType: 'GEO',
latitude: 37.422,
longitude: -122.084,
proofNonce: 'nonce-12345678',
overrideReason: 'GPS timed out near the hub',
});
assert.equal(res.status, 200);
assert.equal(res.body.status, 'CLOCK_IN');
assert.equal(res.body.proofNonce, 'nonce-12345678');
});
test('POST /commands/staff/clock-out accepts assignment-based payload', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/staff/clock-out')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'clock-out-1')
.send({
assignmentId: '88888888-8888-4888-8888-888888888888',
breakMinutes: 30,
});
assert.equal(res.status, 200);
assert.equal(res.body.status, 'CLOCK_OUT');
});
test('POST /commands/staff/location-streams accepts batched location payloads', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/staff/location-streams')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'location-stream-1')
.send({
assignmentId: '99999999-9999-4999-8999-999999999999',
sourceType: 'GEO',
deviceId: 'iphone-15',
points: [
{
capturedAt: '2026-03-16T08:00:00.000Z',
latitude: 37.422,
longitude: -122.084,
accuracyMeters: 12,
},
],
});
assert.equal(res.status, 200);
assert.equal(res.body.status, 'RECORDED');
assert.equal(res.body.pointCount, 1);
});
test('POST /commands/staff/devices/push-tokens registers a staff push token', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/staff/devices/push-tokens')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'staff-push-token-1')
.send({
provider: 'FCM',
platform: 'ANDROID',
pushToken: 'g'.repeat(170),
deviceId: 'pixel-9',
});
assert.equal(res.status, 200);
assert.equal(res.body.tokenId, 'push-token-staff-1');
assert.equal(res.body.platform, 'ANDROID');
});
test('DELETE /commands/staff/devices/push-tokens accepts tokenId from query params', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.delete('/commands/staff/devices/push-tokens?tokenId=22222222-2222-4222-8222-222222222222&reason=SMOKE_CLEANUP')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'staff-push-token-delete-1');
assert.equal(res.status, 200);
assert.equal(res.body.removedCount, 1);
});
test('PUT /commands/staff/profile/tax-forms/:formType uppercases form type', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.put('/commands/staff/profile/tax-forms/w4')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'tax-form-1')
.send({
fields: {
filingStatus: 'single',
},
});
assert.equal(res.status, 200);
assert.equal(res.body.formType, 'W4');
assert.equal(res.body.status, 'DRAFT');
});
test('POST /commands/staff/profile/bank-accounts uppercases account type', async () => {
const app = createApp({ mobileCommandHandlers: createMobileHandlers() });
const res = await request(app)
.post('/commands/staff/profile/bank-accounts')
.set('Authorization', 'Bearer test-token')
.set('Idempotency-Key', 'bank-account-1')
.send({
bankName: 'Demo Credit Union',
accountNumber: '1234567890',
routingNumber: '021000021',
accountType: 'checking',
});
assert.equal(res.status, 200);
assert.equal(res.body.accountType, 'CHECKING');
assert.equal(res.body.last4, '7890');
});