major: kestrel is now a tak server (#6)
All checks were successful
ci/woodpecker/push/push Pipeline was successful
All checks were successful
ci/woodpecker/push/push Pipeline was successful
## Added - CoT (Cursor on Target) server on port 8089 enabling ATAK/iTAK device connectivity - Support for TAK stream protocol and traditional XML CoT messages - TLS/SSL support with automatic fallback to plain TCP - Username/password authentication for CoT connections - Real-time device position tracking with TTL-based expiration (90s default) - API endpoints: `/api/cot/config`, `/api/cot/server-package`, `/api/cot/truststore`, `/api/me/cot-password` - TAK Server section in Settings with QR code for iTAK setup - ATAK password management in Account page for OIDC users - CoT device markers on map showing real-time positions - Comprehensive documentation in `docs/` directory - Environment variables: `COT_PORT`, `COT_TTL_MS`, `COT_REQUIRE_AUTH`, `COT_SSL_CERT`, `COT_SSL_KEY`, `COT_DEBUG` - Dependencies: `fast-xml-parser`, `jszip`, `qrcode` ## Changed - Authentication system supports CoT password management for OIDC users - Database schema includes `cot_password_hash` field - Test suite refactored to follow functional design principles ## Removed - Consolidated utility modules: `authConfig.js`, `authSkipPaths.js`, `bootstrap.js`, `poiConstants.js`, `session.js` ## Security - XML entity expansion protection in CoT parser - Enhanced input validation and SQL injection prevention - Authentication timeout to prevent hanging connections ## Breaking Changes - Port 8089 must be exposed for CoT server. Update firewall rules and Docker/Kubernetes configurations. ## Migration Notes - OIDC users must set ATAK password via Account settings before connecting - Docker: expose port 8089 (`-p 8089:8089`) - Kubernetes: update Helm values to expose port 8089 Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
104
test/unit/asyncLock.spec.js
Normal file
104
test/unit/asyncLock.spec.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { acquire, clearLocks } from '../../server/utils/asyncLock.js'
|
||||
|
||||
describe('asyncLock', () => {
|
||||
beforeEach(() => {
|
||||
clearLocks()
|
||||
})
|
||||
|
||||
it('executes callback immediately when no lock exists', async () => {
|
||||
const executed = { value: false }
|
||||
await acquire('test', async () => {
|
||||
executed.value = true
|
||||
return 42
|
||||
})
|
||||
expect(executed.value).toBe(true)
|
||||
})
|
||||
|
||||
it('returns callback result', async () => {
|
||||
const result = await acquire('test', async () => {
|
||||
return { value: 123 }
|
||||
})
|
||||
expect(result).toEqual({ value: 123 })
|
||||
})
|
||||
|
||||
it('serializes concurrent operations on same key', async () => {
|
||||
const results = []
|
||||
const promises = Array.from({ length: 5 }, (_, i) =>
|
||||
acquire('same-key', async () => {
|
||||
results.push(`start-${i}`)
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
results.push(`end-${i}`)
|
||||
return i
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
// Operations should be serialized: start-end pairs should not interleave
|
||||
expect(results.length).toBe(10)
|
||||
Array.from({ length: 5 }, (_, i) => {
|
||||
expect(results[i * 2]).toBe(`start-${i}`)
|
||||
expect(results[i * 2 + 1]).toBe(`end-${i}`)
|
||||
})
|
||||
})
|
||||
|
||||
it('allows parallel operations on different keys', async () => {
|
||||
const results = []
|
||||
const promises = Array.from({ length: 5 }, (_, i) =>
|
||||
acquire(`key-${i}`, async () => {
|
||||
results.push(`start-${i}`)
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
results.push(`end-${i}`)
|
||||
return i
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
// Different keys can run in parallel
|
||||
expect(results.length).toBe(10)
|
||||
// All starts should come before all ends (parallel execution)
|
||||
const starts = results.filter(r => r.startsWith('start'))
|
||||
const ends = results.filter(r => r.startsWith('end'))
|
||||
expect(starts.length).toBe(5)
|
||||
expect(ends.length).toBe(5)
|
||||
})
|
||||
|
||||
it('handles errors and releases lock', async () => {
|
||||
const callCount = { value: 0 }
|
||||
try {
|
||||
await acquire('error-key', async () => {
|
||||
callCount.value++
|
||||
throw new Error('Test error')
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
expect(error.message).toBe('Test error')
|
||||
}
|
||||
|
||||
// Lock should be released, next operation should execute
|
||||
await acquire('error-key', async () => {
|
||||
callCount.value++
|
||||
return 'success'
|
||||
})
|
||||
|
||||
expect(callCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('maintains lock ordering', async () => {
|
||||
const order = []
|
||||
const promises = Array.from({ length: 3 }, (_, i) =>
|
||||
acquire('ordered', async () => {
|
||||
order.push(`before-${i}`)
|
||||
await new Promise(resolve => setTimeout(resolve, 5))
|
||||
order.push(`after-${i}`)
|
||||
}),
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
// Should execute in order: before-0, after-0, before-1, after-1, before-2, after-2
|
||||
expect(order).toEqual(['before-0', 'after-0', 'before-1', 'after-1', 'before-2', 'after-2'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user