From 99220d73469210f94493ef92a9edc64ab50eb0d9 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 1 Jun 2025 01:03:40 +0200 Subject: Add some tests --- .gitignore | 3 +- .gitmodules | 3 + .idea/prettier.xml | 7 + hashes.json | 2 +- package-lock.json | 52 ++++++++ package.json | 10 +- src/db/dbAccess/index.js | 1 + src/db/dbAccess/user.js | 41 ++++++ src/db/dbAccess/user.test.js | 44 ++++++ src/db/index.js | 1 + src/db/schemas/user.js | 1 + src/util/error.js | 36 +++++ src/util/jwtUtils.js | 34 +++++ src/util/jwtUtils.test.js | 35 +++++ src/util/secretUtils.js | 2 +- src/util/secretUtils.test.js | 23 ++++ testFrontend/ArcaneLibs | 1 + .../SafeNSound.Frontend/SafeNSound.Frontend.csproj | 1 + testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj | 8 +- .../SafeNSound.Sdk/SafeNSoundAuthentication.cs | 7 +- testFrontend/SafeNSound.sln | 147 +++++++++++++++++++++ 21 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 .gitmodules create mode 100644 .idea/prettier.xml create mode 100644 src/db/dbAccess/index.js create mode 100644 src/db/dbAccess/user.js create mode 100644 src/db/dbAccess/user.test.js create mode 100644 src/util/error.js create mode 100644 src/util/jwtUtils.test.js create mode 100644 src/util/secretUtils.test.js create mode 160000 testFrontend/ArcaneLibs 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 @@ + + + + + \ 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 index 0000000..58a531f --- /dev/null +++ b/testFrontend/ArcaneLibs @@ -0,0 +1 @@ +Subproject commit 58a531f72aaae7d44420fcaa7feb5f494c069971 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 @@ + 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 @@ - - - + + + + + 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 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 -- cgit 1.5.1