diff --git a/backend/package.json b/backend/package.json index 96374c972..03b6868a9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -57,7 +57,7 @@ "lint-and-fix": "eslint . --ext .ts --fix", "lint-staged": "lint-staged", "pretest": "docker compose -f test-resources/docker-compose.test.yml up -d", - "test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles", + "test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles", "test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json", "posttest": "docker compose -f test-resources/docker-compose.test.yml down" }, diff --git a/backend/tests/example.test.ts b/backend/tests/example.test.ts deleted file mode 100644 index 6d38025a5..000000000 --- a/backend/tests/example.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { it, expect } from '@jest/globals'; - -it('should return true', () => { - expect(true).toBeTruthy(); -}); diff --git a/backend/tests/utils/crypto.test.ts b/backend/tests/utils/crypto.test.ts new file mode 100644 index 000000000..aea5f5f26 --- /dev/null +++ b/backend/tests/utils/crypto.test.ts @@ -0,0 +1,251 @@ +import { describe, test, expect } from '@jest/globals'; +import { + decryptAsymmetric, + decryptSymmetric, + encryptAsymmetric, + encryptSymmetric +} from '../../src/utils/crypto'; + +describe('Crypto', () => { + describe('encryptAsymmetric', () => { + describe('given all valid publicKey, privateKey and plaintext', () => { + const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; + const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; + const plaintext = 'secret-message'; + + test('should encrypt plain text', () => { + const result = encryptAsymmetric({ plaintext, publicKey, privateKey }); + expect(result.ciphertext).toBeDefined(); + expect(result.nonce).toBeDefined(); + }); + }); + + describe('given empty/undefined publicKey', () => { + let publicKey: string; + const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; + const plaintext = 'secret-message'; + + test('should throw error if publicKey is undefined', () => { + expect(() => { + encryptAsymmetric({ plaintext, publicKey, privateKey }); + }).toThrowError('Failed to perform asymmetric encryption'); + }); + + test('should throw error if publicKey is empty string', () => { + publicKey = ''; + expect(() => { + encryptAsymmetric({ plaintext, publicKey, privateKey }); + }).toThrowError('Failed to perform asymmetric encryption'); + }); + }); + + describe('given empty/undefined privateKey', () => { + const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; + let privateKey: string; + const plaintext = 'secret-message'; + + test('should throw error if privateKey is undefined', () => { + expect(() => { + encryptAsymmetric({ plaintext, publicKey, privateKey }); + }).toThrowError('Failed to perform asymmetric encryption'); + }); + + test('should throw error if privateKey is empty string', () => { + privateKey = ''; + expect(() => { + encryptAsymmetric({ plaintext, publicKey, privateKey }); + }).toThrowError('Failed to perform asymmetric encryption'); + }); + }); + + describe('given undefined/invalid plaint text', () => { + const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; + const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; + let plaintext: string; + + test('should throw error if plaintext is undefined', () => { + expect(() => { + encryptAsymmetric({ plaintext, publicKey, privateKey }); + }).toThrowError('Failed to perform asymmetric encryption'); + }); + + test('should encrypt plaintext containing special characters', () => { + plaintext = '131@#$%235!@#&*(&123sadfkjadjf'; + const result = encryptAsymmetric({ + plaintext, + publicKey, + privateKey + }); + expect(result.ciphertext).toBeDefined(); + expect(result.nonce).toBeDefined(); + }); + }); + }); + + describe('decryptAsymmetric', () => { + describe('given all valid publicKey, privateKey and plaintext', () => { + const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; + const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; + const plaintext = 'secret-message'; + + test('should decrypt the encrypted plaintext', () => { + const encryptedResult = encryptAsymmetric({ + plaintext, + publicKey, + privateKey + }); + const ciphertext = encryptedResult.ciphertext; + const nonce = encryptedResult.nonce; + + const decryptedResult = decryptAsymmetric({ + ciphertext, + nonce, + publicKey, + privateKey + }); + + expect(decryptedResult).toBeDefined(); + expect(decryptedResult).toEqual(plaintext); + }); + }); + + describe('given ciphertext or nonce is modified before decrypt', () => { + const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; + const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; + const plaintext = 'secret-message'; + + test('should throw error if ciphertext is modified', () => { + const encryptedResult = encryptAsymmetric({ + plaintext, + publicKey, + privateKey + }); + const ciphertext = '=12adfJ@#52af1231=123'; // modified + const nonce = encryptedResult.nonce; + + expect(() => { + decryptAsymmetric({ + ciphertext, + nonce, + publicKey, + privateKey + }); + }).toThrowError('Failed to perform asymmetric decryption'); + }); + + test('should throw error if nonce is modified', () => { + const encryptedResult = encryptAsymmetric({ + plaintext, + publicKey, + privateKey + }); + const ciphertext = encryptedResult.ciphertext; + const nonce = '=12adfJ@#52af1231=123'; // modified + + expect(() => { + decryptAsymmetric({ + ciphertext, + nonce, + publicKey, + privateKey + }); + }).toThrowError('Failed to perform asymmetric decryption'); + }); + }); + }); + + describe('encryptSymmetric', () => { + let plaintext: string; + const key = '7e8ee7e5cc667b9c1829783ad31f36f4'; + + test('should encrypt plaintext with the given key', () => { + plaintext = 'secret-message'; + const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key }); + expect(ciphertext).toBeDefined(); + expect(iv).toBeDefined(); + expect(tag).toBeDefined(); + }); + + test('should throw an error when plaintext is undefined', () => { + const invalidKey = 'invalid-key'; + expect(() => { + encryptSymmetric({ plaintext, key: invalidKey }); + }).toThrowError('Failed to perform symmetric encryption'); + }); + + test('should throw an error when invalid key is provided', () => { + plaintext = 'secret-message'; + const invalidKey = 'invalid-key'; + + expect(() => { + encryptSymmetric({ plaintext, key: invalidKey }); + }).toThrowError('Failed to perform symmetric encryption'); + }); + }); + + describe('decryptSymmetric', () => { + const plaintext = 'secret-message'; + const key = '7e8ee7e5cc667b9c1829783ad31f36f4'; + const { ciphertext, iv, tag } = encryptSymmetric({ plaintext, key }); + + test('should decrypt encrypted plaintext', () => { + const result = decryptSymmetric({ + ciphertext, + iv, + tag, + key + }); + + expect(result).toBeDefined(); + expect(result).toEqual(plaintext); + }); + + test('should fail if ciphertext is modified', () => { + const modifieldCiphertext = 'abcdefghijklmnopqrstuvwxyz'; + expect(() => { + decryptSymmetric({ + ciphertext: modifieldCiphertext, + iv, + tag, + key + }); + }).toThrowError('Failed to perform symmetric decryption'); + }); + + test('should fail if iv is modified', () => { + const modifiedIv = 'abcdefghijklmnopqrstuvwxyz'; + expect(() => { + decryptSymmetric({ + ciphertext, + iv: modifiedIv, + tag, + key + }); + }).toThrowError('Failed to perform symmetric decryption'); + }); + + test('should fail if tag is modified', () => { + const modifiedTag = 'abcdefghijklmnopqrstuvwxyz'; + expect(() => { + decryptSymmetric({ + ciphertext, + iv, + tag: modifiedTag, + key + }); + }).toThrowError('Failed to perform symmetric decryption'); + }); + + test('should throw an error when decryption fails', () => { + const invalidKey = 'invalid-key'; + expect(() => { + decryptSymmetric({ + ciphertext, + iv, + tag, + key: invalidKey + }); + }).toThrowError('Failed to perform symmetric decryption'); + }); + }); +}); diff --git a/backend/tests/utils/posthog.test.ts b/backend/tests/utils/posthog.test.ts new file mode 100644 index 000000000..a380060b0 --- /dev/null +++ b/backend/tests/utils/posthog.test.ts @@ -0,0 +1,29 @@ +import { describe, test, expect } from '@jest/globals'; +import { getChannelFromUserAgent } from '../../src/utils/posthog'; + +describe('posthog getChannelFromUserAgent', () => { + test("should return 'web' when userAgent includes 'mozilla'", () => { + const userAgent = + 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.115 Mobile Safari/537.36'; + const channel = getChannelFromUserAgent(userAgent); + expect(channel).toBe('web'); + }); + + test("should return 'cli'", () => { + const userAgent = 'cli'; + const channel = getChannelFromUserAgent(userAgent); + expect(channel).toBe('cli'); + }); + + test("should return 'k8-operator'", () => { + const userAgent = 'k8-operator'; + const channel = getChannelFromUserAgent(userAgent); + expect(channel).toBe('k8-operator'); + }); + + test('should return undefined if no userAgent', () => { + const userAgent = undefined; + const channel = getChannelFromUserAgent(userAgent); + expect(channel).toBe('other'); + }); +});