Building Trust Through Testing: Security-First SDK Development
Introduction
In security-critical systems like smart lock management, comprehensive testing isn't just about catching bugs—it's about building trust. Recent improvements to the UnlockOS SDK demonstrate how systematic testing coverage, security hardening, and robust error handling create a foundation for reliable access control systems.
Comprehensive Testing Strategy
Multi-Layer Test Coverage
The SDK implements a comprehensive testing strategy across multiple layers:
// State machine testing with type safety
interface LockState {
status: 'locked' | 'unlocked' | 'error' | 'processing';
lastOperation: string;
errorCode?: string;
}
describe('Lock State Machine', () => {
it('should handle unlock sequence with proper error recovery', async () => {
const stateMachine = createLockStateMachine();
// Test happy path
await stateMachine.send('UNLOCK');
expect(stateMachine.state.value).toBe('unlocked');
// Test error recovery
await stateMachine.send('NETWORK_ERROR');
expect(stateMachine.state.value).toBe('error');
expect(stateMachine.state.context.canRetry).toBe(true);
});
});Integration Testing for Critical Flows
The checkin-checkout flow receives particular attention with end-to-end testing:
// Integration test for complete access flow
it('should complete full checkin-checkout with proper state persistence', async () => {
const mockReservation = createMockReservation();
// Test checkin flow
const checkinResult = await checkinService.processCheckin({
reservationId: mockReservation.id,
guestVerification: validGuestData
});
expect(checkinResult.lockCode).toBeDefined();
expect(checkinResult.expiresAt).toBeInstanceOf(Date);
// Verify state persistence on refresh (F5 test)
const persistedState = await stateService.getCheckinState(mockReservation.id);
expect(persistedState.status).toBe('checked_in');
});Security Hardening Through Code
Row-Level Security (RLS) Implementation
Database security receives critical attention with tightened RLS policies:
-- Harden reservation_checkins access
CREATE POLICY "checkins_tenant_isolation" ON reservation_checkins
FOR ALL USING (
tenant_id = auth.jwt() ->> 'tenant_id'
AND (auth.jwt() ->> 'role')::text = ANY('{host,admin}'::text[])
);
-- Secure guest_users with time-based access
CREATE POLICY "guest_access_window" ON guest_users
FOR SELECT USING (
tenant_id = auth.jwt() ->> 'tenant_id'
AND checkin_time <= NOW()
AND checkout_time >= NOW()
);Authentication Bypass Hardening
OTP bypass logic receives security improvements with redundant checks removal:
// Before: Multiple redundant security checks
if (shouldBypassOtp(user) && canBypassOtp(user) && isOtpBypassed(user)) {
// Redundant checks create confusion
}
// After: Single, clear security boundary
interface AuthBypassConfig {
readonly enabled: boolean;
readonly allowedRoles: ReadonlyArray<UserRole>;
readonly auditLog: boolean;
}
function shouldBypassOtp(user: User, config: AuthBypassConfig): boolean {
if (!config.enabled) return false;
const hasPermission = config.allowedRoles.includes(user.role);
if (config.auditLog && hasPermission) {
auditLogger.log('otp_bypass_used', {
userId: user.id,
role: user.role,
timestamp: new Date().toISOString()
});
}
return hasPermission;
}Type Safety and Error Prevention
Preventing Data Coercion Issues
Type safety improvements prevent subtle bugs in price plan management:
// Problem: Implicit string-to-number coercion
interface PricePlanForm {
name: string; // Could be coerced to number accidentally
basePrice: number;
}
// Solution: Explicit type guards and validation
interface PricePlanFormSafe {
readonly name: string;
readonly basePrice: number;
}
function validatePricePlanForm(input: unknown): PricePlanFormSafe {
const parsed = PricePlanFormSchema.parse(input);
// Explicit validation prevents coercion
if (typeof parsed.name !== 'string' || parsed.name.trim().length === 0) {
throw new ValidationError('Plan name must be a non-empty string');
}
if (typeof parsed.basePrice !== 'number' || parsed.basePrice < 0) {
throw new ValidationError('Base price must be a non-negative number');
}
return parsed;
}Fee Calculation Robustness
Precise handling of partial-minute billing prevents overcharging:
interface BillingPeriod {
start: Date;
end: Date;
ratePerMinute: number;
}
function calculateUsageFee(period: BillingPeriod): number {
const totalMinutes = Math.ceil(
(period.end.getTime() - period.start.getTime()) / (1000 * 60)
);
// Prevent partial-minute over-billing
const billableMinutes = Math.max(1, totalMinutes); // Minimum 1 minute
return Number((billableMinutes * period.ratePerMinute).toFixed(2));
}
// Test ensures billing accuracy
it('should not over-bill for partial minutes', () => {
const period = {
start: new Date('2026-05-08T10:00:00Z'),
end: new Date('2026-05-08T10:00:30Z'), // 30 seconds
ratePerMinute: 1.0
};
expect(calculateUsageFee(period)).toBe(1.0); // Bills for 1 minute, not fractional
});Performance with Security
Optimized Queries with Access Control
Database query optimization maintains security boundaries:
// Parallelize queries while maintaining isolation
const [reservation, guestData, lockSettings] = await Promise.all([
db.reservations
.select('id', 'property_id', 'status')
.where('tenant_id', tenantId) // Security boundary maintained
.where('id', reservationId)
.first(),
db.guest_users
.select('id', 'email', 'phone')
.where('tenant_id', tenantId) // Consistent isolation
.where('reservation_id', reservationId)
.first(),
db.lock_settings
.select('auto_unlock_duration', 'max_attempts')
.where('tenant_id', tenantId) // Security-first optimization
.where('property_id', propertyId)
.first()
]);Audit and Observability
Measurement Infrastructure
Performance monitoring with security context:
interface SecurityMetrics {
operationType: 'checkin' | 'checkout' | 'unlock';
userId: string;
tenantId: string;
duration: number;
success: boolean;
errorCode?: string;
}
class SecureMetricsCollector {
private metrics: SecurityMetrics[] = [];
recordOperation(metric: SecurityMetrics): void {
// Sanitize sensitive data before logging
const sanitized = {
...metric,
userId: this.hashUserId(metric.userId),
timestamp: new Date().toISOString()
};
this.metrics.push(sanitized);
// Alert on suspicious patterns
if (metric.operationType === 'unlock' && !metric.success) {
this.checkFailurePattern(metric.userId);
}
}
private checkFailurePattern(userId: string): void {
const recentFailures = this.metrics
.filter(m => m.userId === userId && !m.success)
.filter(m => Date.now() - new Date(m.timestamp).getTime() < 300000); // 5 min window
if (recentFailures.length >= 3) {
securityAlerts.triggerSuspiciousActivity(userId);
}
}
}Testing State Machines
Finite State Machine Validation
// Test state machine transitions comprehensively
describe('Lock State Machine Security', () => {
it('should prevent invalid state transitions', () => {
const machine = createLockMachine();
// Test valid transitions
expect(() => machine.transition('locked', 'UNLOCK')).not.toThrow();
// Test invalid transitions are blocked
expect(() => machine.transition('error', 'UNLOCK')).toThrow('Invalid transition');
// Test security-critical transitions
const errorState = machine.transition('unlocked', 'BATTERY_LOW');
expect(errorState.context.requiresManualIntervention).toBe(true);
});
});Summary
Building trust in security-critical systems requires a holistic approach to testing and validation. The UnlockOS SDK demonstrates how comprehensive test coverage, type safety, security hardening, and robust error handling work together to create reliable access control systems. By implementing multi-layer testing strategies, maintaining strict security boundaries, and preventing common pitfalls through type safety, developers can build systems that property managers and guests can trust with their physical security.
The key is treating testing not as an afterthought, but as a fundamental part of security architecture—where every test case is a security assertion and every type guard is a trust boundary.