summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-06-01 01:03:40 +0200
committerRory& <root@rory.gay>2025-06-01 01:03:40 +0200
commit99220d73469210f94493ef92a9edc64ab50eb0d9 (patch)
tree765781a300db5c1e1a15afaa8bd09f48712a5313
parentInit test frontend (diff)
downloadnodejs-final-assignment-99220d73469210f94493ef92a9edc64ab50eb0d9.tar.xz
Add some tests
-rw-r--r--.gitignore3
-rw-r--r--.gitmodules3
-rw-r--r--.idea/prettier.xml7
-rw-r--r--hashes.json2
-rw-r--r--package-lock.json52
-rw-r--r--package.json10
-rw-r--r--src/db/dbAccess/index.js1
-rw-r--r--src/db/dbAccess/user.js41
-rw-r--r--src/db/dbAccess/user.test.js44
-rw-r--r--src/db/index.js1
-rw-r--r--src/db/schemas/user.js1
-rw-r--r--src/util/error.js36
-rw-r--r--src/util/jwtUtils.js34
-rw-r--r--src/util/jwtUtils.test.js35
-rw-r--r--src/util/secretUtils.js2
-rw-r--r--src/util/secretUtils.test.js23
m---------testFrontend/ArcaneLibs0
-rw-r--r--testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj1
-rw-r--r--testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj8
-rw-r--r--testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs7
-rw-r--r--testFrontend/SafeNSound.sln147
21 files changed, 449 insertions, 9 deletions
diff --git a/.gitignore b/.gitignore

index 5fd29cd..b62bf52 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,2 +1,3 @@ node_modules/ -result/ \ No newline at end of file +result/ +lcov.info \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644
index 0000000..437e518 --- /dev/null +++ b/.gitmodules
@@ -0,0 +1,3 @@ +[submodule "testFrontend/ArcaneLibs"] + path = testFrontend/ArcaneLibs + url = https://github.com/TheArcaneBrony/ArcaneLibs.git diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644
index 0000000..0c83ac4 --- /dev/null +++ b/.idea/prettier.xml
@@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="PrettierConfiguration"> + <option name="myConfigurationMode" value="AUTOMATIC" /> + <option name="myRunOnSave" value="true" /> + </component> +</project> \ No newline at end of file diff --git a/hashes.json b/hashes.json
index bd1a93e..707146c 100644 --- a/hashes.json +++ b/hashes.json
@@ -1,3 +1,3 @@ { - "npmDepsHash": "sha256-d3l0xMvkcBSGrC15dE8rE4+brVzi6YIfQBTksqCYD10=" + "npmDepsHash": "sha256-Rge1bomfj8Xgn8gPmcKNHyZBtVQY2lY6LFNRnOoHWcM=" } diff --git a/package-lock.json b/package-lock.json
index 591682a..c2d845f 100644 --- a/package-lock.json +++ b/package-lock.json
@@ -19,6 +19,8 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.27.0", + "bcrypt": "^6.0.0", + "dotenv": "^16.5.0", "eslint": "^9.27.0", "globals": "^16.2.0", "husky": "^9.1.7", @@ -421,6 +423,21 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -646,6 +663,19 @@ "node": ">= 0.8" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1718,6 +1748,28 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json
index 1d4c039..437858d 100644 --- a/package.json +++ b/package.json
@@ -5,7 +5,10 @@ "main": "src/api/start.js", "type": "module", "scripts": { - "prepare": "husky install" + "prepare": "husky install", + "test": "node --test", + "test:watch": "node --test --watch", + "coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info" }, "repository": { "type": "git", @@ -16,11 +19,14 @@ "imports": { "#api/*": "./src/api/*", "#db/*": "./src/db/*", - "#util/*": "./src/util/*" + "#util/*": "./src/util/*", + "#tests/*": "./src/tests/*" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.27.0", + "bcrypt": "^6.0.0", + "dotenv": "^16.5.0", "eslint": "^9.27.0", "globals": "^16.2.0", "husky": "^9.1.7", diff --git a/src/db/dbAccess/index.js b/src/db/dbAccess/index.js new file mode 100644
index 0000000..ee1c337 --- /dev/null +++ b/src/db/dbAccess/index.js
@@ -0,0 +1 @@ +export * from './user.js'; diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js new file mode 100644
index 0000000..413b1cf --- /dev/null +++ b/src/db/dbAccess/user.js
@@ -0,0 +1,41 @@ +import { hash, compare } from 'bcrypt'; +import { DbUser } from '#db/schemas/index.js'; + +export async function registerUser(username, password, email, type = 'user') { + if (!username || !password || !email) { + throw new Error( + 'Username, password, and email are required to register a user.' + ); + } + + const passwordHash = await hash(password, 10); + if (!passwordHash) { + throw new Error('Failed to hash password.'); + } + + return DbUser.create({ + username, + passwordHash, + email, + type + }); +} + +export async function deleteUser(id, password) { + const user = await DbUser.findById(id); + DbUser.exists({ _id: id }).then(exists => { + if (!exists) { + throw new Error('User does not exist.'); + } + }); + if (!user) { + throw new Error('User not found.'); + } + + const isPasswordValid = await compare(password, user.passwordHash); + if (!isPasswordValid) { + throw new Error('Invalid password.'); + } + + await DbUser.findByIdAndDelete(id); +} diff --git a/src/db/dbAccess/user.test.js b/src/db/dbAccess/user.test.js new file mode 100644
index 0000000..e8e1af5 --- /dev/null +++ b/src/db/dbAccess/user.test.js
@@ -0,0 +1,44 @@ +import * as dotenv from 'dotenv'; +import { it } from 'node:test'; +import { deleteUser, registerUser } from '#db/index.js'; +import * as assert from 'node:assert'; +import { initDb } from '#db/db.js'; +import { disconnect } from 'mongoose'; + +dotenv.config(); + +await initDb(); + +async function createTestUser() { + const username = (Math.random() * 1000000).toString(); + const password = (Math.random() * 1000000).toString(); + const email = (Math.random() * 1000000).toString() + '@example.com'; + return { + username, + password, + email, + user: await registerUser(username, password, email) + }; +} + +await it('Can create user', async () => { + await assert.doesNotReject(createTestUser()); +}); + +await it('Can delete user', async () => { + const { username, password, email, user } = await createTestUser(); + + const deletePromise = deleteUser(user._id, password); + await assert.doesNotReject(deletePromise); +}); + +await it('Cant delete nonexistant user', async () => { + await assert.rejects(deleteUser('abc', '')); +}); + +await it('Cant delete user with invalid password', async () => { + const user = await createTestUser(); + await assert.rejects(deleteUser(user.user._id, 'wrongpassword')); +}); + +await disconnect(); diff --git a/src/db/index.js b/src/db/index.js
index cd306b7..0680880 100644 --- a/src/db/index.js +++ b/src/db/index.js
@@ -1,2 +1,3 @@ export * from './db.js'; export * from './schemas/index.js'; +export * from './dbAccess/index.js'; diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 1a7e048..9d08680 100644 --- a/src/db/schemas/user.js +++ b/src/db/schemas/user.js
@@ -1,4 +1,5 @@ import { model, Schema } from 'mongoose'; +import { hash, compare } from 'bcrypt'; /** * User schema for MongoDB. diff --git a/src/util/error.js b/src/util/error.js new file mode 100644
index 0000000..96f1deb --- /dev/null +++ b/src/util/error.js
@@ -0,0 +1,36 @@ +export class SafeNSoundError extends Error { + constructor(options) { + super(); + if (typeof options === 'string') { + this.errCode = options; + super.message = this.getDefaultMessage(); + } else if (typeof options === 'object') { + this.errCode = options.errCode || 'UNKNOWN_ERROR'; + super.message = options.message || this.getDefaultMessage(); + } else { + this.errCode = 'UNKNOWN_ERROR'; + this.message = + 'An unknown error occurred (invalid SafeNSoundError constructor options)'; + } + } + + getDefaultMessage() { + switch (this.type) { + case 'USER_NOT_FOUND': + return 'User not found'; + case 'INVALID_CREDENTIALS': + return 'Invalid credentials'; + case 'EMAIL_ALREADY_EXISTS': + return 'Email already exists'; + case 'PASSWORD_TOO_WEAK': + return 'Password is too weak'; + case 'TOKEN_EXPIRED': + return 'Token has expired'; + case 'UNAUTHORIZED': + return 'Unauthorized access'; + case 'UNKNOWN_ERROR': + default: + return 'An unknown error occurred'; + } + } +} diff --git a/src/util/jwtUtils.js b/src/util/jwtUtils.js
index 115c9c5..9031631 100644 --- a/src/util/jwtUtils.js +++ b/src/util/jwtUtils.js
@@ -1,6 +1,7 @@ import {existsSync} from 'fs'; import {readFile, writeFile} from "node:fs/promises"; import {generateKeyPairSync, createHash, createPublicKey, createPrivateKey} from 'node:crypto'; +import jwt from "jsonwebtoken"; let privateKey, publicKey, fingerprint; @@ -10,6 +11,8 @@ export async function initJwt() { throw new Error('JWT secret path is not defined in environment variables, or the directory does not exist.'); } + console.log(`[JWT] Initializing JWT with secret path: ${secretPath}`); + const privateKeyPath = `${secretPath}/jwt.key`; const publicKeyPath = `${secretPath}/jwt.key.pub`; @@ -55,9 +58,40 @@ const jwtOptions = { } export async function generateJwtToken(user) { + if (!privateKey) { + throw new Error('JWT private key is not initialized. Please call initJwt() first.'); + } + + const payload = { + sub: user._id.toString(), + username: user.username, + type: user.type, + iat: Math.floor(Date.now() / 1000) + }; + return new Promise((resolve, reject) => { + jwt.sign(payload, privateKey, jwtOptions, (err, token) => { + if (err) { + console.error('[JWT] Error generating token:', err); + return reject(err); + } + resolve(token); + }); + }); } export async function validateJwtToken(token) { + if (!publicKey) { + throw new Error('JWT public key is not initialized. Please call initJwt() first.'); + } + return new Promise((resolve, reject) => { + jwt.verify(token, publicKey, jwtOptions, (err, decoded) => { + if (err) { + console.error('[JWT] Token validation failed:', err); + return reject(err); + } + resolve(decoded); + }); + }); } \ No newline at end of file diff --git a/src/util/jwtUtils.test.js b/src/util/jwtUtils.test.js new file mode 100644
index 0000000..94a7d99 --- /dev/null +++ b/src/util/jwtUtils.test.js
@@ -0,0 +1,35 @@ +import { it } from 'node:test'; +import { initJwt, generateJwtToken, validateJwtToken } from './jwtUtils.js'; +import * as dotenv from 'dotenv'; +import { tmpdir } from 'node:os'; +import { mkdtemp } from 'node:fs/promises'; +import * as assert from 'node:assert'; +dotenv.config(); + +const user = { + _id: 'meow', + username: 'testuser' +}; + +await it('Should be able to generate new secrets', async () => { + process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir()); + await initJwt(); +}); + +await it('Should be able to load the JWT utils', async () => { + process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir()); + await initJwt(); + await initJwt(); +}); + +it('Should be able to create a JWT token', async () => { + await initJwt(); + await generateJwtToken(user); +}); + +it('Should be able to validate a JWT token', async () => { + await initJwt(); + const token = await generateJwtToken(user); + const result = await validateJwtToken(token); + console.log(result); +}); diff --git a/src/util/secretUtils.js b/src/util/secretUtils.js
index 92e1b1c..7394395 100644 --- a/src/util/secretUtils.js +++ b/src/util/secretUtils.js
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises'; export async function readSecret(name, path) { console.log(`[SECRET] Reading secret "${name}" from path: ${path}`); if (!path) { - throw new Error('Path to secret file is required'); + throw new Error(`Path to secret file is required: ${name}`); } const content = await fs.readFile(path, 'utf8'); return content.trim(); diff --git a/src/util/secretUtils.test.js b/src/util/secretUtils.test.js new file mode 100644
index 0000000..d95d56f --- /dev/null +++ b/src/util/secretUtils.test.js
@@ -0,0 +1,23 @@ +import { it } from 'node:test'; +import * as dotenv from 'dotenv'; +import { tmpdir } from 'node:os'; +import * as assert from 'node:assert'; +import { readSecret } from '#util/secretUtils.js'; +import { writeFile, mkdtemp } from 'node:fs/promises'; + +dotenv.config(); + +await it('Should fail on nonexistant secret', async () => { + const secretPath = (await mkdtemp(tmpdir())) + '/secret'; + await assert.rejects(readSecret('Nonexistant secret', secretPath)); +}); + +await it('Should read secret from file', async () => { + const secretPath = (await mkdtemp(tmpdir())) + '/secret'; + await writeFile(secretPath, 'testsecret'); + await assert.doesNotReject(readSecret('Test secret', secretPath)); + await assert.equal( + await readSecret('Test secret', secretPath), + 'testsecret' + ); +}); diff --git a/testFrontend/ArcaneLibs b/testFrontend/ArcaneLibs new file mode 160000 +Subproject 58a531f72aaae7d44420fcaa7feb5f494c06997 diff --git a/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj b/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj
index 7bccb06..06f60b4 100644 --- a/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj +++ b/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj
@@ -17,6 +17,7 @@ </ItemGroup> <ItemGroup> + <ProjectReference Include="..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" /> <ProjectReference Include="..\SafeNSound.Sdk\SafeNSound.Sdk.csproj" /> </ItemGroup> diff --git a/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj b/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj
index 7338078..1bf7de5 100644 --- a/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj +++ b/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj
@@ -8,9 +8,11 @@ </PropertyGroup> <ItemGroup> - <Folder Include="Clients\AdminClient\" /> - <Folder Include="Clients\MonitorClient\" /> - <Folder Include="Clients\UserClient\" /> + <Folder Include="Clients\" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" /> </ItemGroup> </Project> diff --git a/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs b/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs
index 23c1445..7d88ec8 100644 --- a/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs +++ b/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs
@@ -1,6 +1,6 @@ namespace SafeNSound.Sdk; -public class SafeNSoundAuthentication(SafeNSoundConfiguration) +public class SafeNSoundAuthentication(SafeNSoundConfiguration config) { public async Task<SafeNSoundAuthResult> Login(string username, string password) { @@ -11,4 +11,9 @@ public class SafeNSoundAuthentication(SafeNSoundConfiguration) { } +} + +public class SafeNSoundAuthResult +{ + } \ No newline at end of file diff --git a/testFrontend/SafeNSound.sln b/testFrontend/SafeNSound.sln
index 6fc2336..c5bee09 100644 --- a/testFrontend/SafeNSound.sln +++ b/testFrontend/SafeNSound.sln
@@ -4,19 +4,166 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeNSound.Frontend", "Safe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeNSound.Sdk", "SafeNSound.Sdk\SafeNSound.Sdk.csproj", "{05CB21DB-72C4-495B-BED4-3B85CC51AFDA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{24C94C05-725E-242A-3195-1FB70FB907A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{F7CB0AF0-CD59-42C7-874D-79EA82753FAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{09822D71-D77A-4846-A7CA-BE7997112D2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{62ABA511-FC64-4630-856C-BD70C4FFDB09}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Tests", "ArcaneLibs\ArcaneLibs.Tests\ArcaneLibs.Tests.csproj", "{BBE6F71B-CBD0-470C-A484-D0656A9B11BF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{198BBB21-4AA1-4753-BC33-39AFFAA88999}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{24FC30D3-E68C-471D-99DA-63C469C3262C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x64.ActiveCfg = Debug|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x64.Build.0 = Debug|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x86.ActiveCfg = Debug|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x86.Build.0 = Debug|Any CPU {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|Any CPU.ActiveCfg = Release|Any CPU {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|Any CPU.Build.0 = Release|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x64.ActiveCfg = Release|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x64.Build.0 = Release|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x86.ActiveCfg = Release|Any CPU + {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x86.Build.0 = Release|Any CPU {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x64.ActiveCfg = Debug|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x64.Build.0 = Debug|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x86.ActiveCfg = Debug|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x86.Build.0 = Debug|Any CPU {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|Any CPU.ActiveCfg = Release|Any CPU {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|Any CPU.Build.0 = Release|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x64.ActiveCfg = Release|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x64.Build.0 = Release|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x86.ActiveCfg = Release|Any CPU + {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x86.Build.0 = Release|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x64.ActiveCfg = Debug|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x64.Build.0 = Debug|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x86.Build.0 = Debug|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|Any CPU.Build.0 = Release|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x64.ActiveCfg = Release|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x64.Build.0 = Release|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x86.ActiveCfg = Release|Any CPU + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x86.Build.0 = Release|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x64.Build.0 = Debug|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x86.Build.0 = Debug|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|Any CPU.Build.0 = Release|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x64.ActiveCfg = Release|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x64.Build.0 = Release|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x86.ActiveCfg = Release|Any CPU + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x86.Build.0 = Release|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x64.Build.0 = Debug|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x86.Build.0 = Debug|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|Any CPU.Build.0 = Release|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x64.ActiveCfg = Release|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x64.Build.0 = Release|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x86.ActiveCfg = Release|Any CPU + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x86.Build.0 = Release|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x64.Build.0 = Debug|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x86.Build.0 = Debug|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|Any CPU.Build.0 = Release|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x64.ActiveCfg = Release|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x64.Build.0 = Release|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x86.ActiveCfg = Release|Any CPU + {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x86.Build.0 = Release|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x64.ActiveCfg = Debug|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x64.Build.0 = Debug|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x86.ActiveCfg = Debug|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x86.Build.0 = Debug|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|Any CPU.Build.0 = Release|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x64.ActiveCfg = Release|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x64.Build.0 = Release|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x86.ActiveCfg = Release|Any CPU + {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x86.Build.0 = Release|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x64.ActiveCfg = Debug|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x64.Build.0 = Debug|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x86.ActiveCfg = Debug|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x86.Build.0 = Debug|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|Any CPU.Build.0 = Release|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x64.ActiveCfg = Release|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x64.Build.0 = Release|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x86.ActiveCfg = Release|Any CPU + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x86.Build.0 = Release|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|Any CPU.Build.0 = Debug|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x64.ActiveCfg = Debug|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x64.Build.0 = Debug|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x86.ActiveCfg = Debug|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x86.Build.0 = Debug|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|Any CPU.ActiveCfg = Release|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|Any CPU.Build.0 = Release|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x64.ActiveCfg = Release|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x64.Build.0 = Release|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x86.ActiveCfg = Release|Any CPU + {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x86.Build.0 = Release|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x64.ActiveCfg = Debug|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x64.Build.0 = Debug|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x86.ActiveCfg = Debug|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x86.Build.0 = Debug|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|Any CPU.Build.0 = Release|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x64.ActiveCfg = Release|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x64.Build.0 = Release|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x86.ActiveCfg = Release|Any CPU + {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9} = {24C94C05-725E-242A-3195-1FB70FB907A6} + {F7CB0AF0-CD59-42C7-874D-79EA82753FAC} = {24C94C05-725E-242A-3195-1FB70FB907A6} + {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2} = {24C94C05-725E-242A-3195-1FB70FB907A6} + {09822D71-D77A-4846-A7CA-BE7997112D2F} = {24C94C05-725E-242A-3195-1FB70FB907A6} + {62ABA511-FC64-4630-856C-BD70C4FFDB09} = {24C94C05-725E-242A-3195-1FB70FB907A6} + {BBE6F71B-CBD0-470C-A484-D0656A9B11BF} = {24C94C05-725E-242A-3195-1FB70FB907A6} + {198BBB21-4AA1-4753-BC33-39AFFAA88999} = {24C94C05-725E-242A-3195-1FB70FB907A6} + {24FC30D3-E68C-471D-99DA-63C469C3262C} = {24C94C05-725E-242A-3195-1FB70FB907A6} EndGlobalSection EndGlobal