Test Review Options
Test Plans Status Summary
0 Open
- Issues1 Open
+ Issue
diff --git a/server/controllers/AuthController.js b/server/controllers/AuthController.js
index 18f36508a..52e46d32b 100644
--- a/server/controllers/AuthController.js
+++ b/server/controllers/AuthController.js
@@ -1,7 +1,15 @@
const { User } = require('../models');
-const { getOrCreateUser } = require('../models/services/UserService');
+const {
+ getOrCreateUser,
+ addUserVendor,
+ getUserById
+} = require('../models/services/UserService');
const { GithubService } = require('../services');
const getUsersFromFile = require('../util/getUsersFromFile');
+const {
+ findVendorByName,
+ getOrCreateVendor
+} = require('../models/services/VendorService');
const APP_SERVER = process.env.APP_SERVER;
@@ -70,6 +78,40 @@ const oauthRedirectFromGithubController = async (req, res) => {
transaction: req.transaction
});
+ if (roles.some(role => role.name === User.VENDOR)) {
+ const vendorEntry = vendors.find(
+ vendor => vendor.split('|')[0] === githubUsername
+ );
+ if (vendorEntry) {
+ const [, companyName] = vendorEntry.split('|').trim();
+ const vendor = await findVendorByName({
+ name: companyName,
+ transaction: req.transaction
+ });
+ if (vendor) {
+ await addUserVendor(user.id, vendor.id, {
+ transaction: req.transaction
+ });
+ } else {
+ const vendor = await getOrCreateVendor({
+ where: { name: companyName },
+ transaction: req.transaction
+ });
+ await addUserVendor(user.id, vendor.id, {
+ transaction: req.transaction
+ });
+ }
+ // Fetch the user again with vendor and AT information
+ user = await getUserById({
+ id: user.id,
+ vendorAttributes: ['id', 'name'],
+ atAttributes: ['id', 'name'],
+ includeVendorAts: true,
+ transaction: req.transaction
+ });
+ }
+ }
+
req.session.user = user;
return loginSucceeded();
diff --git a/server/controllers/FakeUserController.js b/server/controllers/FakeUserController.js
index ab870ff03..ca64c337c 100644
--- a/server/controllers/FakeUserController.js
+++ b/server/controllers/FakeUserController.js
@@ -1,4 +1,12 @@
-const { getOrCreateUser } = require('../models/services/UserService');
+const {
+ getOrCreateUser,
+ addUserVendor,
+ getUserById
+} = require('../models/services/UserService');
+const {
+ getOrCreateVendor,
+ updateVendorById
+} = require('../models/services/VendorService');
const ALLOW_FAKE_USER = process.env.ALLOW_FAKE_USER === 'true';
@@ -25,6 +33,29 @@ const setFakeUserController = async (req, res) => {
transaction: req.transaction
});
+ if (userToCreate.company) {
+ const [vendor] = await getOrCreateVendor({
+ where: { name: userToCreate.company.name },
+ transaction: req.transaction
+ });
+
+ await addUserVendor(user.id, vendor.id, { transaction: req.transaction });
+ await updateVendorById({
+ id: vendor.id,
+ ats: userToCreate.company.ats,
+ transaction: req.transaction
+ });
+
+ // Refresh user to include updated associations
+ user = await getUserById({
+ id: user.id,
+ vendorAttributes: ['id', 'name'],
+ atAttributes: ['id', 'name'],
+ includeVendorAts: true,
+ transaction: req.transaction
+ });
+ }
+
req.session.user = user;
res.status(200).send('');
diff --git a/server/graphql-schema.js b/server/graphql-schema.js
index dd70988cc..4ae520528 100644
--- a/server/graphql-schema.js
+++ b/server/graphql-schema.js
@@ -59,6 +59,10 @@ const graphqlSchema = gql`
The ATs the user has indicated they are able to test.
"""
ats: [At]!
+ """
+ The vendor the user is associated with.
+ """
+ company: Vendor
}
"""
@@ -1256,6 +1260,18 @@ const graphqlSchema = gql`
directory: String
): [TestPlanVersion]!
"""
+ Get a vendor by ID.
+ """
+ vendor(id: ID!): Vendor
+ """
+ Get a vendor by name.
+ """
+ vendorByName(name: String!): Vendor
+ """
+ Get all vendors.
+ """
+ vendors: [Vendor]!
+ """
Get a particular TestPlanVersion by ID.
"""
testPlanVersion(id: ID): TestPlanVersion
@@ -1478,6 +1494,28 @@ const graphqlSchema = gql`
retryCanceledCollections: CollectionJob!
}
+ """
+ Vendor company that makes an AT
+ """
+ type Vendor {
+ """
+ Postgres-provided numeric ID.
+ """
+ id: ID!
+ """
+ The name of the vendor company.
+ """
+ name: String!
+ """
+ The ATs associated with this vendor.
+ """
+ ats: [At]!
+ """
+ The users associated with this vendor.
+ """
+ users: [User]!
+ }
+
type Mutation {
"""
Get the available mutations for the given AT.
diff --git a/server/migrations/20240903164204-add-vendor-table.js b/server/migrations/20240903164204-add-vendor-table.js
new file mode 100644
index 000000000..1eaaad688
--- /dev/null
+++ b/server/migrations/20240903164204-add-vendor-table.js
@@ -0,0 +1,55 @@
+'use strict';
+
+module.exports = {
+ up: async (queryInterface, Sequelize) => {
+ await queryInterface.createTable('Vendor', {
+ id: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: Sequelize.TEXT,
+ allowNull: false,
+ unique: true
+ },
+ createdAt: {
+ type: Sequelize.DATE,
+ allowNull: false
+ },
+ updatedAt: {
+ type: Sequelize.DATE,
+ allowNull: false
+ }
+ });
+
+ await queryInterface.addColumn('User', 'vendorId', {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ references: {
+ model: 'Vendor',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'SET NULL'
+ });
+
+ await queryInterface.addColumn('At', 'vendorId', {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ references: {
+ model: 'Vendor',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'SET NULL'
+ });
+ },
+
+ down: async queryInterface => {
+ await queryInterface.removeColumn('At', 'vendorId');
+ await queryInterface.removeColumn('User', 'vendorId');
+ await queryInterface.dropTable('Vendor');
+ }
+};
diff --git a/server/models/User.js b/server/models/User.js
index 0ab297316..ae5d84972 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -37,6 +37,8 @@ module.exports = function (sequelize, DataTypes) {
Model.ATS_ASSOCIATION = { through: 'UserAts', as: 'ats' };
Model.TEST_PLAN_RUN_ASSOCIATION = { as: 'testPlanRuns' };
+ Model.VENDOR_ASSOCIATION = { as: 'company' };
+
Model.associate = function (models) {
Model.belongsToMany(models.Role, {
...Model.ROLE_ASSOCIATION,
@@ -55,6 +57,11 @@ module.exports = function (sequelize, DataTypes) {
foreignKey: 'testerUserId',
sourceKey: 'id'
});
+
+ Model.belongsTo(models.Vendor, {
+ ...Model.VENDOR_ASSOCIATION,
+ foreignKey: 'vendorId'
+ });
};
return Model;
diff --git a/server/models/Vendor.js b/server/models/Vendor.js
new file mode 100644
index 000000000..7507ecf47
--- /dev/null
+++ b/server/models/Vendor.js
@@ -0,0 +1,43 @@
+const MODEL_NAME = 'Vendor';
+
+module.exports = function (sequelize, DataTypes) {
+ const Model = sequelize.define(
+ MODEL_NAME,
+ {
+ id: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ name: {
+ type: DataTypes.TEXT,
+ allowNull: false,
+ unique: true
+ }
+ },
+ {
+ timestamps: true,
+ tableName: MODEL_NAME
+ }
+ );
+
+ Model.AT_ASSOCIATION = { as: 'ats' };
+ Model.USER_ASSOCIATION = { as: 'users' };
+
+ Model.associate = function (models) {
+ Model.hasMany(models.At, {
+ as: 'ats',
+ foreignKey: 'vendorId',
+ sourceKey: 'id'
+ });
+
+ Model.hasMany(models.User, {
+ ...Model.USER_ASSOCIATION,
+ foreignKey: 'vendorId',
+ sourceKey: 'id'
+ });
+ };
+
+ return Model;
+};
diff --git a/server/models/services/UserService.js b/server/models/services/UserService.js
index a07019104..68a3f2e7b 100644
--- a/server/models/services/UserService.js
+++ b/server/models/services/UserService.js
@@ -41,6 +41,15 @@ const testPlanRunAssociation = testPlanRunAttributes => ({
attributes: testPlanRunAttributes
});
+/**
+ * @param vendorAttributes - Vendor attributes
+ * @returns {{association: string, attributes: string[]}}
+ */
+const vendorAssociation = vendorAttributes => ({
+ association: 'company',
+ attributes: vendorAttributes
+});
+
/**
* You can pass any of the attribute arrays as '[]' to exclude that related association
* @param {object} options
@@ -58,16 +67,33 @@ const getUserById = async ({
roleAttributes = ROLE_ATTRIBUTES,
atAttributes = AT_ATTRIBUTES,
testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES,
+ vendorAttributes = [],
+ includeVendorAts = false,
transaction
}) => {
+ const include = [
+ roleAssociation(roleAttributes),
+ atAssociation(atAttributes),
+ testPlanRunAssociation(testPlanRunAttributes)
+ ];
+
+ if (vendorAttributes.length > 0) {
+ const vendorInclude = vendorAssociation(vendorAttributes);
+ if (includeVendorAts) {
+ vendorInclude.include = [
+ {
+ association: 'ats',
+ attributes: atAttributes
+ }
+ ];
+ }
+ include.push(vendorInclude);
+ }
+
return ModelService.getById(User, {
id,
attributes: userAttributes,
- include: [
- roleAssociation(roleAttributes),
- atAssociation(atAttributes),
- testPlanRunAssociation(testPlanRunAttributes)
- ],
+ include,
transaction
});
};
@@ -89,6 +115,7 @@ const getUserByUsername = async ({
roleAttributes = ROLE_ATTRIBUTES,
atAttributes = AT_ATTRIBUTES,
testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES,
+ vendorAttributes = [],
transaction
}) => {
return ModelService.getByQuery(User, {
@@ -97,7 +124,8 @@ const getUserByUsername = async ({
include: [
roleAssociation(roleAttributes),
atAssociation(atAttributes),
- testPlanRunAssociation(testPlanRunAttributes)
+ testPlanRunAssociation(testPlanRunAttributes),
+ vendorAssociation(vendorAttributes)
],
transaction
});
@@ -126,6 +154,7 @@ const getUsers = async ({
roleAttributes = ROLE_ATTRIBUTES,
atAttributes = AT_ATTRIBUTES,
testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES,
+ vendorAttributes = [],
pagination = {},
transaction
}) => {
@@ -141,7 +170,8 @@ const getUsers = async ({
include: [
roleAssociation(roleAttributes),
atAssociation(atAttributes),
- testPlanRunAssociation(testPlanRunAttributes)
+ testPlanRunAssociation(testPlanRunAttributes),
+ vendorAssociation(vendorAttributes)
],
pagination,
transaction
@@ -247,6 +277,7 @@ const createUser = async ({
roleAttributes = ROLE_ATTRIBUTES,
atAttributes = AT_ATTRIBUTES,
testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES,
+ vendorAttributes = [],
transaction
}) => {
const userResult = await ModelService.create(User, {
@@ -262,7 +293,8 @@ const createUser = async ({
include: [
roleAssociation(roleAttributes),
atAssociation(atAttributes),
- testPlanRunAssociation(testPlanRunAttributes)
+ testPlanRunAssociation(testPlanRunAttributes),
+ vendorAssociation(vendorAttributes)
],
transaction
});
@@ -286,6 +318,7 @@ const updateUserById = async ({
roleAttributes = ROLE_ATTRIBUTES,
atAttributes = AT_ATTRIBUTES,
testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES,
+ vendorAttributes = [],
transaction
}) => {
await ModelService.update(User, {
@@ -300,7 +333,8 @@ const updateUserById = async ({
include: [
roleAssociation(roleAttributes),
atAssociation(atAttributes),
- testPlanRunAssociation(testPlanRunAttributes)
+ testPlanRunAssociation(testPlanRunAttributes),
+ vendorAssociation(vendorAttributes)
],
transaction
});
@@ -412,6 +446,7 @@ const getOrCreateUser = async ({
roleAttributes = ROLE_ATTRIBUTES,
atAttributes = AT_ATTRIBUTES,
testPlanRunAttributes = TEST_PLAN_RUN_ATTRIBUTES,
+ vendorAttributes = [],
transaction
}) => {
const accumulatedResults = await ModelService.nestedGetOrCreate({
@@ -443,6 +478,7 @@ const getOrCreateUser = async ({
roleAttributes,
atAttributes,
testPlanRunAttributes,
+ vendorAttributes,
transaction
});
@@ -480,6 +516,22 @@ const removeUserRole = async (userId, roleName, { transaction }) => {
});
};
+/**
+ * @param {number} userId - id of the User that the vendor will be added to
+ * @param {number} vendorId - id of the Vendor that the User will be associated with
+ * @param {object} options
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise<*>}
+ */
+const addUserVendor = async (userId, vendorId, { transaction }) => {
+ // Set the company Vendor association for the user
+ return ModelService.update(User, {
+ where: { id: userId },
+ values: { vendorId },
+ transaction
+ });
+};
+
module.exports = {
// Basic CRUD
getUserById,
@@ -497,5 +549,6 @@ module.exports = {
// Custom Functions
addUserRole,
- removeUserRole
+ removeUserRole,
+ addUserVendor
};
diff --git a/server/models/services/VendorService.js b/server/models/services/VendorService.js
new file mode 100644
index 000000000..3997545aa
--- /dev/null
+++ b/server/models/services/VendorService.js
@@ -0,0 +1,195 @@
+const ModelService = require('./ModelService');
+const {
+ VENDOR_ATTRIBUTES,
+ AT_ATTRIBUTES,
+ USER_ATTRIBUTES
+} = require('./helpers');
+const { Vendor } = require('../');
+const { Op } = require('sequelize');
+
+// Association helpers
+
+const atAssociation = atAttributes => ({
+ association: 'ats',
+ attributes: atAttributes
+});
+
+const userAssociation = userAttributes => ({
+ association: 'users',
+ attributes: userAttributes
+});
+
+/**
+ * @param {object} options
+ * @param {number} options.id - id of Vendor to be retrieved
+ * @param {string[]} options.vendorAttributes - Vendor attributes to be returned in the result
+ * @param {string[]} options.atAttributes - AT attributes to be returned in the result
+ * @param {string[]} options.userAttributes - User attributes to be returned in the result
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise<*>}
+ */
+const findVendorById = async ({
+ id,
+ vendorAttributes = VENDOR_ATTRIBUTES,
+ atAttributes = AT_ATTRIBUTES,
+ userAttributes = USER_ATTRIBUTES,
+ transaction
+}) => {
+ return ModelService.getById(Vendor, {
+ id,
+ attributes: vendorAttributes,
+ include: [atAssociation(atAttributes), userAssociation(userAttributes)],
+ transaction
+ });
+};
+
+/**
+ * @param {object} options
+ * @param {string} options.name - name of Vendor to be retrieved
+ * @param {string[]} options.vendorAttributes - Vendor attributes to be returned in the result
+ * @param {string[]} options.atAttributes - AT attributes to be returned in the result
+ * @param {string[]} options.userAttributes - User attributes to be returned in the result
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise<*>}
+ */
+const findVendorByName = async ({
+ name,
+ vendorAttributes = VENDOR_ATTRIBUTES,
+ atAttributes = AT_ATTRIBUTES,
+ userAttributes = USER_ATTRIBUTES,
+ transaction
+}) => {
+ return ModelService.getByQuery(Vendor, {
+ where: { name },
+ attributes: vendorAttributes,
+ include: [atAssociation(atAttributes), userAssociation(userAttributes)],
+ transaction
+ });
+};
+
+/**
+ * @param {object} options
+ * @param {string|any} options.search - use this to combine with {@param filter} to be passed to Sequelize's where clause
+ * @param {object} options.where - use this define conditions to be passed to Sequelize's where clause
+ * @param {string[]} options.vendorAttributes - Vendor attributes to be returned in the result
+ * @param {string[]} options.atAttributes - AT attributes to be returned in the result
+ * @param {string[]} options.userAttributes - User attributes to be returned in the result
+ * @param {object} options.pagination - pagination options for query
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise<*>}
+ */
+const findAllVendors = async ({
+ search,
+ where = {},
+ vendorAttributes = VENDOR_ATTRIBUTES,
+ atAttributes = AT_ATTRIBUTES,
+ userAttributes = USER_ATTRIBUTES,
+ pagination = {},
+ transaction
+}) => {
+ const searchQuery = search ? `%${search}%` : '';
+ if (searchQuery) {
+ where = { ...where, name: { [Op.iLike]: searchQuery } };
+ }
+
+ return ModelService.get(Vendor, {
+ where,
+ attributes: vendorAttributes,
+ include: [atAssociation(atAttributes), userAssociation(userAttributes)],
+ pagination,
+ transaction
+ });
+};
+
+/**
+ * @param {object} options
+ * @param {object} options.where - conditions to find or create the Vendor
+ * @param {string[]} options.vendorAttributes - Vendor attributes to be returned in the result
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise<[*, boolean]>}
+ */
+const getOrCreateVendor = async ({
+ where,
+ vendorAttributes = VENDOR_ATTRIBUTES,
+ transaction
+}) => {
+ const [vendor, created] = await Vendor.findOrCreate({
+ where,
+ attributes: vendorAttributes,
+ transaction
+ });
+ return [vendor, created];
+};
+
+/**
+ * @param {object} options
+ * @param {object} options.values - values to be used to create the Vendor
+ * @param {string[]} options.vendorAttributes - Vendor attributes to be returned in the result
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise<*>}
+ */
+const createVendor = async ({
+ values,
+ vendorAttributes = VENDOR_ATTRIBUTES,
+ transaction
+}) => {
+ const vendorResult = await ModelService.create(Vendor, {
+ values,
+ transaction
+ });
+ const { id } = vendorResult;
+
+ return ModelService.getById(Vendor, {
+ id,
+ attributes: vendorAttributes,
+ transaction
+ });
+};
+
+/**
+ * @param {object} options
+ * @param {number} options.id - id of the Vendor record to be updated
+ * @param {object} options.values - values to be used to update columns for the record
+ * @param {string[]} options.vendorAttributes - Vendor attributes to be returned in the result
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise<*>}
+ */
+const updateVendorById = async ({
+ id,
+ values,
+ vendorAttributes = VENDOR_ATTRIBUTES,
+ transaction
+}) => {
+ await ModelService.update(Vendor, {
+ where: { id },
+ values,
+ transaction
+ });
+
+ return ModelService.getById(Vendor, {
+ id,
+ attributes: vendorAttributes,
+ transaction
+ });
+};
+
+/**
+ * @param {object} options
+ * @param {number} options.id - id of the Vendor record to be removed
+ * @param {boolean} options.truncate - Sequelize specific deletion options that could be passed
+ * @param {*} options.transaction - Sequelize transaction
+ * @returns {Promise
}
+ */
+const removeVendorById = async ({ id, truncate = false, transaction }) => {
+ return ModelService.removeById(Vendor, { id, truncate, transaction });
+};
+
+module.exports = {
+ findVendorById,
+ findVendorByName,
+ findAllVendors,
+ getOrCreateVendor,
+ createVendor,
+ updateVendorById,
+ removeVendorById
+};
diff --git a/server/models/services/helpers.js b/server/models/services/helpers.js
index 402787495..54a0930ca 100644
--- a/server/models/services/helpers.js
+++ b/server/models/services/helpers.js
@@ -12,7 +12,8 @@ const {
UserRoles,
UserAts,
CollectionJob,
- CollectionJobTestStatus
+ CollectionJobTestStatus,
+ Vendor
} = require('../index');
/**
@@ -43,5 +44,6 @@ module.exports = {
COLLECTION_JOB_ATTRIBUTES: getSequelizeModelAttributes(CollectionJob),
COLLECTION_JOB_TEST_STATUS_ATTRIBUTES: getSequelizeModelAttributes(
CollectionJobTestStatus
- )
+ ),
+ VENDOR_ATTRIBUTES: getSequelizeModelAttributes(Vendor)
};
diff --git a/server/resolvers/index.js b/server/resolvers/index.js
index 58bfd1d80..7642ad198 100644
--- a/server/resolvers/index.js
+++ b/server/resolvers/index.js
@@ -10,6 +10,9 @@ const testPlanVersion = require('./testPlanVersionResolver');
const testPlanVersions = require('./testPlanVersionsResolver');
const testPlanRun = require('./testPlanRunResolver');
const testPlanRuns = require('./testPlanRunsResolver');
+const vendors = require('./vendorsResolver');
+const vendor = require('./vendorResolver');
+const vendorByName = require('./vendorByNameResolver');
const createTestPlanReport = require('./createTestPlanReportResolver');
const addViewer = require('./addViewerResolver');
const mutateAt = require('./mutateAtResolver');
@@ -64,7 +67,10 @@ const resolvers = {
populateData,
collectionJob,
collectionJobs,
- collectionJobByTestPlanRunId
+ collectionJobByTestPlanRunId,
+ vendors,
+ vendor,
+ vendorByName
},
Mutation: {
at: mutateAt,
diff --git a/server/resolvers/vendorByNameResolver.js b/server/resolvers/vendorByNameResolver.js
new file mode 100644
index 000000000..fcc6cb6ba
--- /dev/null
+++ b/server/resolvers/vendorByNameResolver.js
@@ -0,0 +1,7 @@
+const { findVendorByName } = require('../models/services/VendorService');
+
+const vendorByNameResolver = async (_, { name }, { transaction }) => {
+ return findVendorByName({ name, transaction });
+};
+
+module.exports = vendorByNameResolver;
diff --git a/server/resolvers/vendorResolver.js b/server/resolvers/vendorResolver.js
new file mode 100644
index 000000000..a52a0b411
--- /dev/null
+++ b/server/resolvers/vendorResolver.js
@@ -0,0 +1,8 @@
+const { findVendorById } = require('../models/services/VendorService');
+
+const vendorResolver = async (_, { id }, { transaction }) => {
+ // No auth since the vendors.txt file is public
+ return findVendorById({ id, transaction });
+};
+
+module.exports = vendorResolver;
diff --git a/server/resolvers/vendorsResolver.js b/server/resolvers/vendorsResolver.js
new file mode 100644
index 000000000..25c9412f1
--- /dev/null
+++ b/server/resolvers/vendorsResolver.js
@@ -0,0 +1,8 @@
+const { findAllVendors } = require('../models/services/VendorService');
+
+const vendorsResolver = async (_, __, { transaction }) => {
+ // No auth since the vendors.txt file is public
+ return findAllVendors({ transaction });
+};
+
+module.exports = vendorsResolver;
diff --git a/server/seeders/20240903165406-add-vendors.js b/server/seeders/20240903165406-add-vendors.js
new file mode 100644
index 000000000..5041d66f3
--- /dev/null
+++ b/server/seeders/20240903165406-add-vendors.js
@@ -0,0 +1,64 @@
+'use strict';
+
+const { VENDOR_NAME_TO_AT_MAPPING } = require('../util/constants');
+const getUsersFromFile = require('../util/getUsersFromFile');
+
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ const vendorLines = await getUsersFromFile('vendors.txt');
+
+ for (const line of vendorLines) {
+ const [username, companyName] = line.split('|');
+ if (username && companyName) {
+ // Create vendor if it doesn't exist
+ const [vendor] = await queryInterface.sequelize.query(
+ `INSERT INTO "Vendor" (name, "createdAt", "updatedAt")
+ VALUES (:name, NOW(), NOW())
+ ON CONFLICT (name) DO UPDATE SET name = :name
+ RETURNING id`,
+ {
+ replacements: { name: companyName },
+ type: Sequelize.QueryTypes.INSERT
+ }
+ );
+
+ // Associate user with vendor if user exists
+ await queryInterface.sequelize.query(
+ `UPDATE "User" SET "vendorId" = :vendorId
+ WHERE username = :username`,
+ {
+ replacements: { vendorId: vendor[0].id, username },
+ type: Sequelize.QueryTypes.UPDATE
+ }
+ );
+
+ if (VENDOR_NAME_TO_AT_MAPPING[companyName]) {
+ for (const atName of VENDOR_NAME_TO_AT_MAPPING[companyName]) {
+ await queryInterface.sequelize.query(
+ `UPDATE "At" SET "vendorId" = :vendorId
+ WHERE name = :atName`,
+ {
+ replacements: { vendorId: vendor[0].id, atName },
+ type: Sequelize.QueryTypes.UPDATE
+ }
+ );
+ }
+ }
+ }
+ }
+ },
+
+ async down(queryInterface, Sequelize) {
+ const vendorLines = await getUsersFromFile('vendors.txt');
+
+ await queryInterface.sequelize.query(
+ `DELETE FROM "Vendor" WHERE name IN (:vendorNames)`,
+ {
+ replacements: {
+ vendorNames: vendorLines.map(line => line.split('|')[1])
+ },
+ type: Sequelize.QueryTypes.DELETE
+ }
+ );
+ }
+};
diff --git a/server/tests/integration/graphql.test.js b/server/tests/integration/graphql.test.js
index 7ffbf8c69..6c498662d 100644
--- a/server/tests/integration/graphql.test.js
+++ b/server/tests/integration/graphql.test.js
@@ -153,6 +153,7 @@ describe('graphql', () => {
['CollectionJob', 'testPlanRun'],
['CollectionJob', 'externalLogsUrl'],
['CollectionJob', 'testStatus'],
+ ['User', 'company'],
// These interact with Response Scheduler API
// which is mocked in other tests.
['Mutation', 'scheduleCollectionJob'],
@@ -242,6 +243,10 @@ describe('graphql', () => {
username
roles
isBot
+ company {
+ id
+ name
+ }
}
me {
__typename
@@ -252,6 +257,10 @@ describe('graphql', () => {
id
name
}
+ company {
+ id
+ name
+ }
}
collectionJob(id: 1) {
__typename
@@ -271,6 +280,18 @@ describe('graphql', () => {
id
status
}
+ vendors {
+ id
+ name
+ }
+ vendor(id: 1) {
+ id
+ name
+ }
+ vendorByName(name: "apple") {
+ id
+ name
+ }
v2TestPlanVersion: testPlanVersion(id: 80) {
__typename
id
diff --git a/server/tests/models/At.spec.js b/server/tests/models/At.spec.js
index e9d8c7101..8ea1ddc65 100644
--- a/server/tests/models/At.spec.js
+++ b/server/tests/models/At.spec.js
@@ -8,6 +8,7 @@ const {
const AtModel = require('../../models/At');
const AtVersionModel = require('../../models/AtVersion');
const BrowserModel = require('../../models/Browser');
+const VendorModel = require('../../models/Vendor');
describe('AtModel', () => {
// A1
@@ -26,18 +27,27 @@ describe('AtModel', () => {
// A1
const AT_VERSION_ASSOCIATION = { as: 'atVersions' };
const BROWSER_ASSOCIATION = { through: 'AtBrowsers', as: 'browsers' };
+ const VENDOR_ASSOCIATION = { as: 'vendor' };
// A2
beforeAll(() => {
Model.hasMany(AtVersionModel, AT_VERSION_ASSOCIATION);
Model.hasMany(BrowserModel, BROWSER_ASSOCIATION);
+ Model.belongsTo(VendorModel, VENDOR_ASSOCIATION);
});
it('defined a hasMany association with AtVersion', () => {
// A3
expect(Model.hasMany).toHaveBeenCalledWith(
AtVersionModel,
- expect.objectContaining(Model.AT_VERSION_ASSOCIATION)
+ expect.objectContaining(AT_VERSION_ASSOCIATION)
+ );
+ });
+
+ it('defined a belongsTo association with Vendor', () => {
+ expect(Model.belongsTo).toHaveBeenCalledWith(
+ VendorModel,
+ expect.objectContaining(VENDOR_ASSOCIATION)
);
});
});
diff --git a/server/tests/models/User.spec.js b/server/tests/models/User.spec.js
index b67c776e0..927229709 100644
--- a/server/tests/models/User.spec.js
+++ b/server/tests/models/User.spec.js
@@ -8,6 +8,7 @@ const {
const UserModel = require('../../models/User');
const RoleModel = require('../../models/Role');
const TestPlanRunModel = require('../../models/TestPlanRun');
+const VendorModel = require('../../models/Vendor');
describe('UserModel Schema Checks', () => {
// A1
@@ -28,11 +29,13 @@ describe('UserModel Schema Checks', () => {
// A1
const ROLE_ASSOCIATION = { through: 'UserRoles', as: 'roles' };
const TEST_PLAN_RUN_ASSOCIATION = { as: 'testPlanRuns' };
+ const VENDOR_ASSOCIATION = { as: 'company' };
// A2
beforeAll(() => {
Model.belongsToMany(RoleModel, ROLE_ASSOCIATION);
Model.hasMany(TestPlanRunModel, TEST_PLAN_RUN_ASSOCIATION);
+ Model.belongsTo(VendorModel, VENDOR_ASSOCIATION);
});
// A3
@@ -49,5 +52,12 @@ describe('UserModel Schema Checks', () => {
expect.objectContaining(Model.TEST_PLAN_RUN_ASSOCIATION)
);
});
+
+ it('defined a belongsTo association with Vendor', () => {
+ expect(Model.belongsTo).toHaveBeenCalledWith(
+ VendorModel,
+ expect.objectContaining(Model.VENDOR_ASSOCIATION)
+ );
+ });
});
});
diff --git a/server/tests/models/Vendor.spec.js b/server/tests/models/Vendor.spec.js
new file mode 100644
index 000000000..e177ae98d
--- /dev/null
+++ b/server/tests/models/Vendor.spec.js
@@ -0,0 +1,45 @@
+const {
+ sequelize,
+ dataTypes,
+ checkModelName,
+ checkPropertyExists
+} = require('sequelize-test-helpers');
+
+const VendorModel = require('../../models/Vendor');
+const AtModel = require('../../models/At');
+const UserModel = require('../../models/User');
+
+describe('VendorModel', () => {
+ const Model = VendorModel(sequelize, dataTypes);
+ const modelInstance = new Model();
+
+ checkModelName(Model)('Vendor');
+
+ describe('properties', () => {
+ ['name'].forEach(checkPropertyExists(modelInstance));
+ });
+
+ describe('associations', () => {
+ const AT_ASSOCIATION = { as: 'ats' };
+ const USER_ASSOCIATION = { as: 'users' };
+
+ beforeAll(() => {
+ Model.hasMany(AtModel, AT_ASSOCIATION);
+ Model.hasMany(UserModel, USER_ASSOCIATION);
+ });
+
+ it('defined a hasMany association with At', () => {
+ expect(Model.hasMany).toHaveBeenCalledWith(
+ AtModel,
+ expect.objectContaining(Model.AT_ASSOCIATION)
+ );
+ });
+
+ it('defined a hasMany association with User', () => {
+ expect(Model.hasMany).toHaveBeenCalledWith(
+ UserModel,
+ expect.objectContaining(Model.USER_ASSOCIATION)
+ );
+ });
+ });
+});
diff --git a/server/tests/models/services/VendorService.test.js b/server/tests/models/services/VendorService.test.js
new file mode 100644
index 000000000..089c0068e
--- /dev/null
+++ b/server/tests/models/services/VendorService.test.js
@@ -0,0 +1,74 @@
+const { sequelize } = require('../../../models');
+const VendorService = require('../../../models/services/VendorService');
+const randomStringGenerator = require('../../util/random-character-generator');
+const dbCleaner = require('../../util/db-cleaner');
+
+afterAll(async () => {
+ await sequelize.close();
+});
+
+describe('VendorService', () => {
+ it('should create and retrieve a vendor', async () => {
+ await dbCleaner(async transaction => {
+ const vendorName = randomStringGenerator();
+
+ const createdVendor = await VendorService.createVendor({
+ values: { name: vendorName },
+ transaction
+ });
+
+ expect(createdVendor).toHaveProperty('id');
+ expect(createdVendor.name).toBe(vendorName);
+
+ const retrievedVendor = await VendorService.findVendorById({
+ id: createdVendor.id,
+ transaction
+ });
+
+ expect(retrievedVendor).toMatchObject(createdVendor);
+ });
+ });
+
+ it('should update a vendor', async () => {
+ await dbCleaner(async transaction => {
+ const vendorName = randomStringGenerator();
+ const updatedName = randomStringGenerator();
+
+ const createdVendor = await VendorService.createVendor({
+ values: { name: vendorName },
+ transaction
+ });
+
+ const updatedVendor = await VendorService.updateVendorById({
+ id: createdVendor.id,
+ values: { name: updatedName },
+ transaction
+ });
+
+ expect(updatedVendor.name).toBe(updatedName);
+ });
+ });
+
+ it('should delete a vendor', async () => {
+ await dbCleaner(async transaction => {
+ const vendorName = randomStringGenerator();
+
+ const createdVendor = await VendorService.createVendor({
+ values: { name: vendorName },
+ transaction
+ });
+
+ await VendorService.removeVendorById({
+ id: createdVendor.id,
+ transaction
+ });
+
+ const deletedVendor = await VendorService.findVendorById({
+ id: createdVendor.id,
+ transaction
+ });
+
+ expect(deletedVendor).toBeNull();
+ });
+ });
+});
diff --git a/server/util/constants.js b/server/util/constants.js
index d6c90d026..27c126e41 100644
--- a/server/util/constants.js
+++ b/server/util/constants.js
@@ -9,7 +9,14 @@ const AT_VERSIONS_SUPPORTED_BY_COLLECTION_JOBS = {
NVDA: ['2024.1', '2023.3.3', '2023.3']
};
+const VENDOR_NAME_TO_AT_MAPPING = {
+ vispero: ['JAWS'],
+ nvAccess: ['NVDA'],
+ apple: ['VoiceOver for macOS']
+};
+
module.exports = {
NO_OUTPUT_STRING,
- AT_VERSIONS_SUPPORTED_BY_COLLECTION_JOBS
+ AT_VERSIONS_SUPPORTED_BY_COLLECTION_JOBS,
+ VENDOR_NAME_TO_AT_MAPPING
};