From c8c00511fa500c13ddc60266c1b0a829602f3e4e Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Fri, 30 Jun 2023 14:42:58 -0400 Subject: [PATCH] HARMONY-1406: Make sure user_work ready and running counts cannot become negative. --- app/models/user-work.ts | 14 +- .../168814650246494169 | 25 +++ .../168814650626380324 | 25 +++ .../168814650828163749 | 25 +++ .../168814650843835199 | 25 +++ .../168814650857569725 | 25 +++ test/helpers/user-work.ts | 38 +++- test/models/user-work.ts | 169 ++++++++++++++++++ 8 files changed, 341 insertions(+), 5 deletions(-) create mode 100644 fixtures/uat.urs.earthdata.nasa.gov-443/168814650246494169 create mode 100644 fixtures/uat.urs.earthdata.nasa.gov-443/168814650626380324 create mode 100644 fixtures/uat.urs.earthdata.nasa.gov-443/168814650828163749 create mode 100644 fixtures/uat.urs.earthdata.nasa.gov-443/168814650843835199 create mode 100644 fixtures/uat.urs.earthdata.nasa.gov-443/168814650857569725 create mode 100644 test/models/user-work.ts diff --git a/app/models/user-work.ts b/app/models/user-work.ts index f4fe17ae2..bd93d10b3 100644 --- a/app/models/user-work.ts +++ b/app/models/user-work.ts @@ -268,8 +268,10 @@ export async function incrementRunningAndDecrementReadyCounts( await tx(UserWork.table) .where({ job_id: jobID, service_id: serviceID }) .increment('running_count') - .decrement('ready_count') - .update({ 'last_worked': new Date() }); + .update({ + ready_count: tx.raw('CASE WHEN ready_count > 0 THEN ready_count - 1 ELSE 0 END'), + last_worked: new Date(), + }); } /** @@ -285,7 +287,9 @@ export async function incrementReadyAndDecrementRunningCounts( await tx(UserWork.table) .where({ job_id: jobID, service_id: serviceID }) .increment('ready_count') - .decrement('running_count'); + .update({ + running_count: tx.raw('CASE WHEN running_count > 0 THEN running_count - 1 ELSE 0 END'), + }); } /** @@ -299,7 +303,9 @@ export async function decrementRunningCount( ): Promise { await tx(UserWork.table) .where({ job_id: jobID, service_id: serviceID }) - .decrement('running_count'); + .update({ + running_count: tx.raw('CASE WHEN running_count > 0 THEN running_count - 1 ELSE 0 END'), + }); } /** diff --git a/fixtures/uat.urs.earthdata.nasa.gov-443/168814650246494169 b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650246494169 new file mode 100644 index 000000000..d3782b9ad --- /dev/null +++ b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650246494169 @@ -0,0 +1,25 @@ +GET /api/user_groups/groups_for_user/undefined +accept: application/json, text/plain, */* +authorization: Bearer undefined + +HTTP/1.1 401 Unauthorized +server: nginx/1.20.1 +date: Fri, 30 Jun 2023 17:35:02 GMT +content-type: application/json; charset=utf-8 +transfer-encoding: chunked +connection: close +x-frame-options: SAMEORIGIN +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +x-download-options: noopen +x-permitted-cross-domain-policies: none +referrer-policy: strict-origin-when-cross-origin +cache-control: no-store +pragma: no-cache +expires: Fri, 01 Jan 1990 00:00:00 GMT +www-authenticate: Bearer realm="Earthdata Login",error="invalid_token" +x-request-id: 4b0827fc-ca53-44b2-b710-3871b884a906 +x-runtime: 0.002449 +strict-transport-security: max-age=31536000 + +{"error":"invalid_token"} \ No newline at end of file diff --git a/fixtures/uat.urs.earthdata.nasa.gov-443/168814650626380324 b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650626380324 new file mode 100644 index 000000000..47279e1fe --- /dev/null +++ b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650626380324 @@ -0,0 +1,25 @@ +GET /api/user_groups/groups_for_user/jdoe +accept: application/json, text/plain, */* +authorization: Bearer fake_access + +HTTP/1.1 401 Unauthorized +server: nginx/1.20.1 +date: Fri, 30 Jun 2023 17:35:06 GMT +content-type: application/json; charset=utf-8 +transfer-encoding: chunked +connection: close +x-frame-options: SAMEORIGIN +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +x-download-options: noopen +x-permitted-cross-domain-policies: none +referrer-policy: strict-origin-when-cross-origin +cache-control: no-store +pragma: no-cache +expires: Fri, 01 Jan 1990 00:00:00 GMT +www-authenticate: Bearer realm="Earthdata Login",error="invalid_token" +x-request-id: 6973563f-51b7-489f-a290-808fa43c783d +x-runtime: 0.003604 +strict-transport-security: max-age=31536000 + +{"error":"invalid_token"} \ No newline at end of file diff --git a/fixtures/uat.urs.earthdata.nasa.gov-443/168814650828163749 b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650828163749 new file mode 100644 index 000000000..ad64817ff --- /dev/null +++ b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650828163749 @@ -0,0 +1,25 @@ +GET /api/user_groups/groups_for_user/jdoe1 +accept: application/json, text/plain, */* +authorization: Bearer fake_access + +HTTP/1.1 401 Unauthorized +server: nginx/1.20.1 +date: Fri, 30 Jun 2023 17:35:08 GMT +content-type: application/json; charset=utf-8 +transfer-encoding: chunked +connection: close +x-frame-options: SAMEORIGIN +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +x-download-options: noopen +x-permitted-cross-domain-policies: none +referrer-policy: strict-origin-when-cross-origin +cache-control: no-store +pragma: no-cache +expires: Fri, 01 Jan 1990 00:00:00 GMT +www-authenticate: Bearer realm="Earthdata Login",error="invalid_token" +x-request-id: 54172470-3017-4aaf-b79b-46c58357ea46 +x-runtime: 0.002423 +strict-transport-security: max-age=31536000 + +{"error":"invalid_token"} \ No newline at end of file diff --git a/fixtures/uat.urs.earthdata.nasa.gov-443/168814650843835199 b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650843835199 new file mode 100644 index 000000000..66f61f799 --- /dev/null +++ b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650843835199 @@ -0,0 +1,25 @@ +GET /api/user_groups/groups_for_user/jdoe2 +accept: application/json, text/plain, */* +authorization: Bearer fake_access + +HTTP/1.1 401 Unauthorized +server: nginx/1.20.1 +date: Fri, 30 Jun 2023 17:35:08 GMT +content-type: application/json; charset=utf-8 +transfer-encoding: chunked +connection: close +x-frame-options: SAMEORIGIN +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +x-download-options: noopen +x-permitted-cross-domain-policies: none +referrer-policy: strict-origin-when-cross-origin +cache-control: no-store +pragma: no-cache +expires: Fri, 01 Jan 1990 00:00:00 GMT +www-authenticate: Bearer realm="Earthdata Login",error="invalid_token" +x-request-id: 8562feff-59e4-4376-984d-a066be03a2a4 +x-runtime: 0.004536 +strict-transport-security: max-age=31536000 + +{"error":"invalid_token"} \ No newline at end of file diff --git a/fixtures/uat.urs.earthdata.nasa.gov-443/168814650857569725 b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650857569725 new file mode 100644 index 000000000..7ac9d4a77 --- /dev/null +++ b/fixtures/uat.urs.earthdata.nasa.gov-443/168814650857569725 @@ -0,0 +1,25 @@ +GET /api/user_groups/groups_for_user/jdoe3 +accept: application/json, text/plain, */* +authorization: Bearer fake_access + +HTTP/1.1 401 Unauthorized +server: nginx/1.20.1 +date: Fri, 30 Jun 2023 17:35:08 GMT +content-type: application/json; charset=utf-8 +transfer-encoding: chunked +connection: close +x-frame-options: SAMEORIGIN +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +x-download-options: noopen +x-permitted-cross-domain-policies: none +referrer-policy: strict-origin-when-cross-origin +cache-control: no-store +pragma: no-cache +expires: Fri, 01 Jan 1990 00:00:00 GMT +www-authenticate: Bearer realm="Earthdata Login",error="invalid_token" +x-request-id: 01bcd335-c61d-4f2f-8620-47e2566d206f +x-runtime: 0.002942 +strict-transport-security: max-age=31536000 + +{"error":"invalid_token"} \ No newline at end of file diff --git a/test/helpers/user-work.ts b/test/helpers/user-work.ts index 0e7a4bffa..a431b5665 100644 --- a/test/helpers/user-work.ts +++ b/test/helpers/user-work.ts @@ -1,5 +1,5 @@ import UserWork, { populateUserWorkFromWorkItems } from '../../app/models/user-work'; -import db from '../../app/util/db'; +import db, { Transaction } from '../../app/util/db'; /** * Adds before / after hooks to populate the user_work table in the database from the @@ -12,5 +12,41 @@ export function hookPopulateUserWorkFromWorkItems(): void { after(async function () { await db(UserWork.table).truncate(); }); +} + +/** + * Sets the running_count and ready_count for a given row in the user work table. + * + * @param tx - the database transaction + * @param id - the database identifier for the UserWork row + * @param readyCount - the count to set for ready_count + * @param runningCount - the count to set for running_count + */ +export async function setCounts( + tx: Transaction, id: number, readyCount: number, runningCount: number, +): Promise { + await tx(UserWork.table) + .where({ id }) + .update({ running_count: runningCount, ready_count: readyCount }); +} +/** + * Returns a UserWork record + * + * @param fields - UserWork fields to set. All fields are optional and any fields not set + * will use a default value. + * @returns UserWork record + */ +export function createUserWorkRecord(fields: Partial = {}): UserWork { + let { job_id, service_id, username, ready_count, running_count, is_async, last_worked } = fields; + job_id = job_id || 'foo'; + service_id = service_id || 'bar'; + username = username || 'joe'; + ready_count = ready_count || 0; + running_count = running_count || 0; + is_async = is_async || false; + last_worked = last_worked || new Date(); + return new UserWork({ + job_id, service_id, username, ready_count, running_count, is_async, last_worked, + }); } diff --git a/test/models/user-work.ts b/test/models/user-work.ts new file mode 100644 index 000000000..da673605d --- /dev/null +++ b/test/models/user-work.ts @@ -0,0 +1,169 @@ +import { expect } from 'chai'; +import { decrementRunningCount, getCount, incrementReadyAndDecrementRunningCounts, incrementRunningAndDecrementReadyCounts } from '../../app/models/user-work'; +import db from '../../app/util/db'; +import { truncateAll } from '../helpers/db'; +import { createUserWorkRecord } from '../helpers/user-work'; + +describe('user_work table', async function () { + describe('when creating a row and setting the ready and running counts to positive values', async function () { + const userWork = createUserWorkRecord( { ready_count: 9, running_count: 5 }); + before(async function () { + await userWork.save(db); + }); + after(async function () { + await truncateAll(); + }); + + describe('when calling incrementRunningAndDecrementReadyCounts', async function () { + before(async function () { + await incrementRunningAndDecrementReadyCounts(db, userWork.job_id, userWork.service_id); + }); + + it('adds one to the running_count', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(6); + }); + it('subtracts one from the ready_count', async function () { + const readyCount = await getCount(db, userWork.job_id, userWork.service_id, 'ready'); + expect(readyCount).to.equal(8); + }); + }); + + describe('when calling incrementReadyAndDecrementRunningCounts', async function () { + before(async function () { + userWork.ready_count = 4; + userWork.running_count = 8; + await userWork.save(db); + await incrementReadyAndDecrementRunningCounts(db, userWork.job_id, userWork.service_id); + }); + + it('adds one to the ready_count', async function () { + const readyCount = await getCount(db, userWork.job_id, userWork.service_id, 'ready'); + expect(readyCount).to.equal(5); + }); + it('subtracts one from the running_count', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(7); + }); + }); + + describe('when calling decrementRunningCount', async function () { + before(async function () { + userWork.ready_count = 4; + userWork.running_count = 15; + await userWork.save(db); + await decrementRunningCount(db, userWork.job_id, userWork.service_id); + }); + + it('does not change the ready_count', async function () { + const readyCount = await getCount(db, userWork.job_id, userWork.service_id, 'ready'); + expect(readyCount).to.equal(4); + }); + it('subtracts one from the running_count', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(14); + }); + }); + }); + + describe('when the ready count is a positive value, and the running count is 0', function () { + const userWork = createUserWorkRecord( { ready_count: 9, running_count: 0 }); + before(async function () { + await userWork.save(db); + }); + after(async function () { + await truncateAll(); + }); + + describe('when calling incrementReadyAndDecrementRunningCounts', async function () { + before(async function () { + await incrementReadyAndDecrementRunningCounts(db, userWork.job_id, userWork.service_id); + }); + + it('adds one to the ready_count', async function () { + const readyCount = await getCount(db, userWork.job_id, userWork.service_id, 'ready'); + expect(readyCount).to.equal(10); + }); + it('leaves the running_count set to zero instead of making it negative', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(0); + }); + }); + + describe('when calling decrementRunningCount', async function () { + before(async function () { + userWork.ready_count = 9; + userWork.running_count = 0; + await userWork.save(db); + await decrementRunningCount(db, userWork.job_id, userWork.service_id); + }); + + it('leaves the running_count set to zero instead of making it negative', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(0); + }); + }); + + describe('when calling incrementRunningAndDecrementReadyCounts', function () { + before(async function () { + userWork.ready_count = 9; + userWork.running_count = 0; + await userWork.save(db); + await incrementRunningAndDecrementReadyCounts(db, userWork.job_id, userWork.service_id); + }); + + it('adds one to the running_count', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(1); + }); + it('subtracts one from the ready_count', async function () { + const readyCount = await getCount(db, userWork.job_id, userWork.service_id, 'ready'); + expect(readyCount).to.equal(8); + }); + }); + }); + + describe('when the ready count is 0, and the running count is a positive value', async function () { + const userWork = createUserWorkRecord( { ready_count: 0, running_count: 4 }); + before(async function () { + await userWork.save(db); + }); + after(async function () { + await truncateAll(); + }); + + describe('when calling incrementRunningAndDecrementReadyCounts', async function () { + before(async function () { + await incrementRunningAndDecrementReadyCounts(db, userWork.job_id, userWork.service_id); + }); + + it('adds one to the running_count', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(5); + }); + + it('leaves the ready count set to zero instead of making it negative', async function () { + const readyCount = await getCount(db, userWork.job_id, userWork.service_id, 'ready'); + expect(readyCount).to.equal(0); + }); + }); + + describe('when calling incrementReadyAndDecrementRunningCounts', async function () { + before(async function () { + userWork.ready_count = 0; + userWork.running_count = 4; + await userWork.save(db); + await incrementReadyAndDecrementRunningCounts(db, userWork.job_id, userWork.service_id); + }); + + it('adds one to the ready_count', async function () { + const readyCount = await getCount(db, userWork.job_id, userWork.service_id, 'ready'); + expect(readyCount).to.equal(1); + }); + it('subtracts one from the running_count', async function () { + const runningCount = await getCount(db, userWork.job_id, userWork.service_id, 'running'); + expect(runningCount).to.equal(3); + }); + }); + }); +});