import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtModule } from '@nestjs/jwt'; import * as request from 'supertest'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { DevicesModule } from '../src/devices/devices.module'; import { AuthModule } from '../src/auth/auth.module'; import { Device } from '../src/entities/device.entity'; import { InventoryDevice } from '../src/entities/inventory-device.entity'; import { UserProfile } from '../src/entities/user-profile.entity'; import { UserIdentity } from '../src/entities/user-identity.entity'; describe('Devices (e2e)', () => { let app: INestApplication; let deviceRepository: Repository; let inventoryDeviceRepository: Repository; let userProfileRepository: Repository; let userIdentityRepository: Repository; const testDatabaseUrl = process.env.TEST_DATABASE_URL; beforeAll(async () => { if (!testDatabaseUrl) { throw new Error('TEST_DATABASE_URL environment variable is required'); } const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'postgres', url: testDatabaseUrl, entities: [UserProfile, UserIdentity, Device, InventoryDevice], synchronize: false, logging: false, }), AuthModule, DevicesModule, JwtModule.register({ secret: 'test-secret', signOptions: { expiresIn: '1h' }, }), ], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.init(); deviceRepository = moduleFixture.get>( getRepositoryToken(Device), ); inventoryDeviceRepository = moduleFixture.get>( getRepositoryToken(InventoryDevice), ); userProfileRepository = moduleFixture.get>( getRepositoryToken(UserProfile), ); userIdentityRepository = moduleFixture.get>( getRepositoryToken(UserIdentity), ); }); afterAll(async () => { await app.close(); }); beforeEach(async () => { // Clean up test data await deviceRepository.delete({}); await inventoryDeviceRepository.delete({}); await userIdentityRepository.delete({}); await userProfileRepository.delete({}); }); describe('POST /api/v1/devices/register', () => { let authToken: string; let testUserProfile: UserProfile; beforeEach(async () => { // Create test user testUserProfile = userProfileRepository.create({ displayName: 'Test User', }); await userProfileRepository.save(testUserProfile); const testUserIdentity = userIdentityRepository.create({ userProfileId: testUserProfile.id, provider: 'email', providerId: 'test@example.com', email: 'test@example.com', passwordHash: 'hashed_password', }); await userIdentityRepository.save(testUserIdentity); // Register user and get token const registerResponse = await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ email: 'auth-test@example.com', password: 'TestPassword123', displayName: 'Auth Test User', }); const loginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'auth-test@example.com', password: 'TestPassword123', }); authToken = loginResponse.body.accessToken; }); it('should reject unauthenticated requests', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/register') .send({ hardwareId: 'TEST_DEVICE_001', }); expect(response.status).toBe(401); }); it('should reject invalid hardware_id format', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'a@b', // Invalid format }); expect(response.status).toBe(400); expect(response.body.message).toContain( 'Hardware ID can only contain alphanumeric characters', ); }); it('should reject hardware_id that is too short', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'ab', // Too short }); expect(response.status).toBe(400); expect(response.body.message).toContain( 'Hardware ID must be at least 3 characters long', ); }); it('should reject device not in inventory', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'NON_EXISTENT_DEVICE', }); expect(response.status).toBe(404); expect(response.body.message).toContain('not found in inventory'); }); it('should reject already claimed device', async () => { // Create inventory device that is already claimed const inventoryDevice = inventoryDeviceRepository.create({ hardwareId: 'CLAIMED_DEVICE_001', isClaimed: true, }); await inventoryDeviceRepository.save(inventoryDevice); const response = await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'CLAIMED_DEVICE_001', }); expect(response.status).toBe(409); expect(response.body.message).toContain('has already been claimed'); }); it('should successfully register a legitimate unclaimed device', async () => { // Create unclaimed inventory device const inventoryDevice = inventoryDeviceRepository.create({ hardwareId: 'LEGITIMATE_DEVICE_001', isClaimed: false, deviceModel: 'TestModel v1.0', }); await inventoryDeviceRepository.save(inventoryDevice); const response = await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'LEGITIMATE_DEVICE_001', }); expect(response.status).toBe(201); expect(response.body.message).toBe('Device registered successfully'); expect(response.body.device).toBeDefined(); expect(response.body.device.hardwareId).toBe('LEGITIMATE_DEVICE_001'); expect(response.body.device.userProfileId).toBeDefined(); // Verify device was created in devices table const createdDevice = await deviceRepository.findOne({ where: { hardwareId: 'LEGITIMATE_DEVICE_001' }, }); expect(createdDevice).toBeDefined(); // Verify inventory device was marked as claimed const updatedInventoryDevice = await inventoryDeviceRepository.findOne({ where: { hardwareId: 'LEGITIMATE_DEVICE_001' }, }); expect(updatedInventoryDevice?.isClaimed).toBe(true); }); it('should reject registering the same device twice', async () => { // Create unclaimed inventory device const inventoryDevice = inventoryDeviceRepository.create({ hardwareId: 'DUPLICATE_TEST_DEVICE', isClaimed: false, }); await inventoryDeviceRepository.save(inventoryDevice); // First registration should succeed const firstResponse = await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'DUPLICATE_TEST_DEVICE', }); expect(firstResponse.status).toBe(201); // Second registration should fail const secondResponse = await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'DUPLICATE_TEST_DEVICE', }); expect(secondResponse.status).toBe(409); expect(secondResponse.body.message).toContain('has already been claimed'); }); }); describe('GET /api/v1/devices', () => { let authToken: string; beforeEach(async () => { // Register and login user await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ email: 'list-test@example.com', password: 'TestPassword123', displayName: 'List Test User', }); const loginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'list-test@example.com', password: 'TestPassword123', }); authToken = loginResponse.body.accessToken; }); it('should return empty list for user with no devices', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/devices') .set('Authorization', `Bearer ${authToken}`); expect(response.status).toBe(200); expect(response.body.devices).toEqual([]); }); it('should require authentication', async () => { const response = await request(app.getHttpServer()).get( '/api/v1/devices', ); expect(response.status).toBe(401); }); }); describe('POST /api/v1/devices/heartbeat', () => { let authToken: string; let testDevice: Device; beforeEach(async () => { // Register and login user const registerResponse = await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ email: 'heartbeat-test@example.com', password: 'TestPassword123', displayName: 'Heartbeat Test User', }); const loginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'heartbeat-test@example.com', password: 'TestPassword123', }); authToken = loginResponse.body.accessToken; // Create inventory device const inventoryDevice = inventoryDeviceRepository.create({ hardwareId: 'HEARTBEAT_TEST_DEVICE', isClaimed: false, }); await inventoryDeviceRepository.save(inventoryDevice); // Register device await request(app.getHttpServer()) .post('/api/v1/devices/register') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'HEARTBEAT_TEST_DEVICE', }); // Get the created device testDevice = await deviceRepository.findOne({ where: { hardwareId: 'HEARTBEAT_TEST_DEVICE' }, }); if (!testDevice) { throw new Error('Test device not found after registration'); } }); it('should reject unauthenticated requests', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/heartbeat') .send({ hardwareId: 'HEARTBEAT_TEST_DEVICE', }); expect(response.status).toBe(401); }); it('should reject invalid hardware_id format', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/heartbeat') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'a@b', // Invalid format }); expect(response.status).toBe(400); expect(response.body.message).toContain( 'Hardware ID can only contain alphanumeric characters', ); }); it('should reject heartbeat for non-existent device', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/heartbeat') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'NON_EXISTENT_DEVICE', }); expect(response.status).toBe(404); expect(response.body.message).toContain('not found'); }); it('should reject heartbeat for device belonging to different user', async () => { // Register another user await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ email: 'other-user@example.com', password: 'TestPassword123', displayName: 'Other User', }); const otherUserLoginResponse = await request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'other-user@example.com', password: 'TestPassword123', }); const otherUserToken = otherUserLoginResponse.body.accessToken; const response = await request(app.getHttpServer()) .post('/api/v1/devices/heartbeat') .set('Authorization', `Bearer ${otherUserToken}`) .send({ hardwareId: 'HEARTBEAT_TEST_DEVICE', }); expect(response.status).toBe(400); expect(response.body.message).toContain( 'Device does not belong to the authenticated user', ); }); it('should successfully process heartbeat for valid device', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/heartbeat') .set('Authorization', `Bearer ${authToken}`) .send({ hardwareId: 'HEARTBEAT_TEST_DEVICE', }); expect(response.status).toBe(200); expect(response.body.message).toBe('Heartbeat processed successfully'); // Verify device was updated const updatedDevice = await deviceRepository.findOne({ where: { id: testDevice.id }, }); expect(updatedDevice).toBeDefined(); expect(updatedDevice!.status).toBe('online'); expect(updatedDevice!.lastSeenAt).toBeDefined(); expect(new Date(updatedDevice!.lastSeenAt!).getTime()).toBeGreaterThan( new Date().getTime() - 10000, // Within last 10 seconds ); }); it('should require hardwareId in request body', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/devices/heartbeat') .set('Authorization', `Bearer ${authToken}`) .send({}); // Empty body expect(response.status).toBe(400); expect(response.body.message).toContain('hardwareId'); }); }); });