diff --git a/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx b/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx index c3491ee39..ed39406f6 100644 --- a/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx +++ b/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockHandler, createMockSite } from 'test-utils/fixtures'; import { createMockProfileResponse } from 'test-utils/fixtures/mockUser'; -import { userApiMocks } from 'test-utils/mock'; -import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { mockUserEndpoints } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/mockSiteEndpoints'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); afterEach(() => cleanup()); beforeAll(() => server.listen()); afterAll(() => server.close()); // Disable API mocking after the tests are done. diff --git a/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx b/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx index 976ed8370..15ca31b5d 100644 --- a/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx +++ b/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx @@ -5,10 +5,10 @@ import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockHandler } from 'test-utils/fixtures'; -import { userApiMocks } from 'test-utils/mock'; +import { mockUserEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); afterEach(() => cleanup()); beforeAll(() => server.listen()); afterAll(() => server.close()); // Disable API mocking after the tests are done. diff --git a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx index c12f94d80..558f125e3 100644 --- a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx +++ b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx @@ -6,8 +6,8 @@ import '@testing-library/jest-dom'; import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockRcrainfoSite } from 'test-utils/fixtures'; -import { userApiMocks } from 'test-utils/mock'; -import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { mockUserEndpoints } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/mockSiteEndpoints'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { HandlerSearchForm } from './HandlerSearchForm'; @@ -45,7 +45,7 @@ export const mockHandlerSearches = [ }), ]; -const server = setupServer(...userApiMocks, ...mockHandlerSearches); +const server = setupServer(...mockUserEndpoints, ...mockHandlerSearches); afterEach(() => { cleanup(); }); diff --git a/client/src/components/Manifest/ManifestForm.spec.tsx b/client/src/components/Manifest/ManifestForm.spec.tsx index 15113042d..b78366863 100644 --- a/client/src/components/Manifest/ManifestForm.spec.tsx +++ b/client/src/components/Manifest/ManifestForm.spec.tsx @@ -2,13 +2,13 @@ import '@testing-library/jest-dom'; import { fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ManifestForm } from 'components/Manifest'; +import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders } from 'test-utils'; +import { mockUserEndpoints, mockWasteEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; -import { setupServer } from 'msw/node'; -import { userApiMocks, wasteApiMocks } from 'test-utils/mock'; -const server = setupServer(...userApiMocks, ...wasteApiMocks); +const server = setupServer(...mockUserEndpoints, ...mockWasteEndpoints); afterEach(() => cleanup()); beforeAll(() => server.listen()); afterAll(() => server.close()); diff --git a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx index a7b519266..e50dc5d52 100644 --- a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx +++ b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockMTNHandler } from 'test-utils/fixtures'; -import { userApiMocks } from 'test-utils/mock'; +import { mockUserEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { undefined } from 'zod'; @@ -21,7 +21,7 @@ const mockProfile: HaztrakProfileResponse = { }, }; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); afterEach(() => cleanup()); beforeAll(() => server.listen()); afterAll(() => server.close()); // Disable API mocking after the tests are done. diff --git a/client/src/components/Manifest/Transporter/TransporterTable.spec.tsx b/client/src/components/Manifest/Transporter/TransporterTable.spec.tsx index 26a640e72..fbcc0dcd0 100644 --- a/client/src/components/Manifest/Transporter/TransporterTable.spec.tsx +++ b/client/src/components/Manifest/Transporter/TransporterTable.spec.tsx @@ -5,7 +5,7 @@ import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockTransporter } from 'test-utils/fixtures'; -import { userApiMocks } from 'test-utils/mock'; +import { mockUserEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { TransporterTable } from './index'; @@ -24,7 +24,7 @@ const TRAN_ARRAY: Array = [ ...createMockTransporter({ epaSiteId: HANDLER_ID_2, name: HANDLER_NAME_2, order: 2 }), }, ]; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); afterEach(() => cleanup()); beforeAll(() => server.listen()); afterAll(() => server.close()); diff --git a/client/src/components/Manifest/WasteLine/WasteLineForm.spec.tsx b/client/src/components/Manifest/WasteLine/WasteLineForm.spec.tsx index 5c551d8b0..84e0a0516 100644 --- a/client/src/components/Manifest/WasteLine/WasteLineForm.spec.tsx +++ b/client/src/components/Manifest/WasteLine/WasteLineForm.spec.tsx @@ -1,13 +1,13 @@ import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; +import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; +import { mockUserEndpoints, mockWasteEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { WasteLineForm } from './WasteLineForm'; -import { setupServer } from 'msw/node'; -import { userApiMocks, wasteApiMocks } from 'test-utils/mock'; -const server = setupServer(...userApiMocks, ...wasteApiMocks); +const server = setupServer(...mockUserEndpoints, ...mockWasteEndpoints); afterEach(() => { cleanup(); }); diff --git a/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx b/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx index 2fa54bd87..1f2092f65 100644 --- a/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx +++ b/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx @@ -4,13 +4,13 @@ import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { userApiMocks } from 'test-utils/mock'; -import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { mockUserEndpoints } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/mockSiteEndpoints'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; const testTaskID = 'testTaskId'; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); server.use( http.post(`${API_BASE_URL}rcra/manifest/emanifest/sync`, () => { // Mock Sync Site Manifests response diff --git a/client/src/components/User/UserInfoForm.spec.tsx b/client/src/components/User/UserInfoForm.spec.tsx index 00abfb9e5..41a831b87 100644 --- a/client/src/components/User/UserInfoForm.spec.tsx +++ b/client/src/components/User/UserInfoForm.spec.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { HaztrakUser, ProfileSlice } from 'store'; import { renderWithProviders, screen } from 'test-utils'; import { createMockHaztrakUser } from 'test-utils/fixtures'; -import { userApiMocks } from 'test-utils/mock'; +import { mockUserEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); // pre-/post-test hooks beforeAll(() => server.listen()); diff --git a/client/src/features/Dashboard/Dashboard.spec.tsx b/client/src/features/Dashboard/Dashboard.spec.tsx index aeaf61ec2..38fe39285 100644 --- a/client/src/features/Dashboard/Dashboard.spec.tsx +++ b/client/src/features/Dashboard/Dashboard.spec.tsx @@ -3,13 +3,13 @@ import { Dashboard } from 'features/Dashboard/Dashboard'; import { setupServer } from 'msw/node'; import React, { createElement } from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { userApiMocks } from 'test-utils/mock'; -import { htApiMocks } from 'test-utils/mock/htApiMocks'; +import { mockUserEndpoints } from 'test-utils/mock'; +import { mockSiteEndpoints } from 'test-utils/mock/mockSiteEndpoints'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; const USERNAME = 'testuser1'; -const server = setupServer(...htApiMocks, ...userApiMocks); +const server = setupServer(...mockSiteEndpoints, ...mockUserEndpoints); beforeAll(() => { vi.mock('recharts', async (importOriginal) => { diff --git a/client/src/features/NewManifest/NewManifest.spec.tsx b/client/src/features/NewManifest/NewManifest.spec.tsx index b74e56df8..4a39fd574 100644 --- a/client/src/features/NewManifest/NewManifest.spec.tsx +++ b/client/src/features/NewManifest/NewManifest.spec.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockSite } from 'test-utils/fixtures'; -import { userApiMocks } from 'test-utils/mock'; -import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { mockUserEndpoints } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/mockSiteEndpoints'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; const mySiteId = 'VATESTGEN001'; @@ -29,7 +29,7 @@ const mockProfile: HaztrakProfileResponse = { }, }; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); server.use( http.get(`${API_BASE_URL}/api/user/profile`, () => { return HttpResponse.json({ ...mockProfile }, { status: 200 }); diff --git a/client/src/features/SiteList/SiteList.spec.tsx b/client/src/features/SiteList/SiteList.spec.tsx index 028237f0d..43e6bb035 100644 --- a/client/src/features/SiteList/SiteList.spec.tsx +++ b/client/src/features/SiteList/SiteList.spec.tsx @@ -3,7 +3,7 @@ import { setupServer } from 'msw/node'; import React from 'react'; import { renderWithProviders, screen } from 'test-utils'; import { createMockHandler, createMockSite } from 'test-utils/fixtures/mockHandler'; -import { htApiMocks, userApiMocks } from 'test-utils/mock'; +import { mockSiteEndpoints, mockUserEndpoints } from 'test-utils/mock'; import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import { SiteList } from './SiteList'; @@ -13,7 +13,7 @@ const mockSites = [ createMockSite({ handler: mockHandler1 }), createMockSite({ handler: mockHandler2 }), ]; -const server = setupServer(...userApiMocks, ...htApiMocks); +const server = setupServer(...mockUserEndpoints, ...mockSiteEndpoints); // pre-/post-test hooks beforeAll(() => server.listen()); diff --git a/client/src/hooks/manifest/useSaveManifest/useSaveManifest.spec.tsx b/client/src/hooks/manifest/useSaveManifest/useSaveManifest.spec.tsx index 414bb76ef..665ad941f 100644 --- a/client/src/hooks/manifest/useSaveManifest/useSaveManifest.spec.tsx +++ b/client/src/hooks/manifest/useSaveManifest/useSaveManifest.spec.tsx @@ -1,14 +1,14 @@ import '@testing-library/jest-dom'; import { cleanup, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Manifest } from 'components/Manifest'; +import { setupServer } from 'msw/node'; import React from 'react'; import { renderWithProviders, screen } from 'test-utils'; +import { createMockManifest } from 'test-utils/fixtures'; +import { mockManifestEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { useSaveManifest } from './useSaveManifest'; -import { Manifest } from 'components/Manifest'; -import userEvent from '@testing-library/user-event'; -import { createMockManifest } from 'test-utils/fixtures'; -import { setupServer } from 'msw/node'; -import { manifestMocks } from 'test-utils/mock'; const TestComponent = ({ manifest }: { manifest?: Manifest }) => { const { data, isLoading, error, taskId, saveManifest } = useSaveManifest(); @@ -24,7 +24,7 @@ const TestComponent = ({ manifest }: { manifest?: Manifest }) => { ); }; -const server = setupServer(...manifestMocks); +const server = setupServer(...mockManifestEndpoints); beforeAll(() => server.listen()); afterAll(() => server.close()); afterEach(() => cleanup()); diff --git a/client/src/hooks/useUserSiteIds/useUserSiteIds.spec.tsx b/client/src/hooks/useUserSiteIds/useUserSiteIds.spec.tsx index 4b33ba75a..0cd790464 100644 --- a/client/src/hooks/useUserSiteIds/useUserSiteIds.spec.tsx +++ b/client/src/hooks/useUserSiteIds/useUserSiteIds.spec.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { renderWithProviders, screen } from 'test-utils'; import { createMockHandler, createMockSite } from 'test-utils/fixtures'; import { createMockProfileResponse } from 'test-utils/fixtures/mockUser'; -import { userApiMocks, wasteApiMocks } from 'test-utils/mock'; -import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { mockUserEndpoints, mockWasteEndpoints } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/mockSiteEndpoints'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; function TestComponent() { @@ -23,7 +23,7 @@ function TestComponent() { ); } -const server = setupServer(...userApiMocks, ...wasteApiMocks); +const server = setupServer(...mockUserEndpoints, ...mockWasteEndpoints); beforeAll(() => server.listen()); afterAll(() => server.close()); afterEach(() => cleanup()); diff --git a/client/src/store/htApi.slice.ts b/client/src/store/htApi.slice.ts index 8579ed58b..b86b1d3e1 100644 --- a/client/src/store/htApi.slice.ts +++ b/client/src/store/htApi.slice.ts @@ -127,7 +127,10 @@ export const haztrakApi = createApi({ providesTags: ['site'], }), getMTN: build.query, string | undefined>({ - query: (siteId) => ({ url: siteId ? `rcra/mtn/${siteId}` : 'rcra/mtn', method: 'get' }), + query: (siteId) => ({ + url: siteId ? `rcra/manifest/mtn/${siteId}` : 'rcra/manifest/mtn', + method: 'get', + }), providesTags: ['manifest'], }), getManifest: build.query({ diff --git a/client/src/store/userSlice/user.slice.spec.tsx b/client/src/store/userSlice/user.slice.spec.tsx index 9e5912026..9c7f27544 100644 --- a/client/src/store/userSlice/user.slice.spec.tsx +++ b/client/src/store/userSlice/user.slice.spec.tsx @@ -4,10 +4,10 @@ import { setupServer } from 'msw/node'; import { useEffect, useState } from 'react'; import { useGetUserQuery, useUpdateUserMutation } from 'store'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { userApiMocks } from 'test-utils/mock'; +import { mockUserEndpoints } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; -const server = setupServer(...userApiMocks); +const server = setupServer(...mockUserEndpoints); afterEach(() => { cleanup(); }); diff --git a/client/src/test-utils/mock/htApiMocks.ts b/client/src/test-utils/mock/htApiMocks.ts deleted file mode 100644 index 8992c6696..000000000 --- a/client/src/test-utils/mock/htApiMocks.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { http, HttpResponse } from 'msw'; -import { createMockHandler, createMockManifest, createMockSite } from '../fixtures'; - -export const API_BASE_URL = import.meta.env.VITE_HT_API_URL; -const mockMTN = createMockManifest().manifestTrackingNumber; -const mockEpaId = createMockHandler().epaSiteId; -const mockSites = [createMockSite(), createMockSite()]; - -export const htApiMocks = [ - /** List user sites*/ - http.get(`${API_BASE_URL}/api/site`, (info) => { - return HttpResponse.json(mockSites, { status: 200 }); - }), - /** Site Details*/ - http.get(`${API_BASE_URL}/api/site/${mockEpaId}`, (info) => { - return HttpResponse.json(mockSites[0], { status: 200 }); - }), - /** list of manifests ('My Manifests' feature and a site's manifests)*/ - http.get(`${API_BASE_URL}/api/rcra/mtn*`, (info) => { - const mockManifestArray = [ - createMockManifest(), - createMockManifest({ manifestTrackingNumber: '987654321ELC', status: 'Pending' }), - ]; - return HttpResponse.json(mockManifestArray, { status: 200 }); - }), -]; diff --git a/client/src/test-utils/mock/index.ts b/client/src/test-utils/mock/index.ts index 8987a73a6..79e3b452f 100644 --- a/client/src/test-utils/mock/index.ts +++ b/client/src/test-utils/mock/index.ts @@ -1,4 +1,4 @@ -export { userApiMocks } from './userApiMocks'; -export { htApiMocks } from './htApiMocks'; -export { wasteApiMocks } from './wasteApiMocks'; -export { manifestMocks } from './manifestMocks'; +export { mockUserEndpoints } from 'test-utils/mock/mockUserEndpoints'; +export { mockSiteEndpoints } from 'test-utils/mock/mockSiteEndpoints'; +export { mockWasteEndpoints } from 'test-utils/mock/mockWasteEndpoints'; +export { mockManifestEndpoints } from 'test-utils/mock/mockManifestEndpoints'; diff --git a/client/src/test-utils/mock/manifestMocks.ts b/client/src/test-utils/mock/mockManifestEndpoints.ts similarity index 85% rename from client/src/test-utils/mock/manifestMocks.ts rename to client/src/test-utils/mock/mockManifestEndpoints.ts index d5d55e059..a51ae61dd 100644 --- a/client/src/test-utils/mock/manifestMocks.ts +++ b/client/src/test-utils/mock/mockManifestEndpoints.ts @@ -1,11 +1,15 @@ +import { Manifest } from 'components/Manifest'; import { http, HttpResponse } from 'msw'; import { createMockManifest } from '../fixtures'; -import { Manifest } from 'components/Manifest'; export const API_BASE_URL = import.meta.env.VITE_HT_API_URL; const mockMTN = createMockManifest().manifestTrackingNumber; -export const manifestMocks = [ +const generateRandomMTN = (): string => { + return Math.floor(100000000 + Math.random() * 900000000).toString(); +}; + +export const mockManifestEndpoints = [ /** mock GET Manifest*/ http.get(`${API_BASE_URL}/api/rcra/manifest/${mockMTN}`, (info) => { return HttpResponse.json(createMockManifest(), { status: 200 }); @@ -14,10 +18,7 @@ export const manifestMocks = [ http.post(`${API_BASE_URL}/api/rcra/manifest`, async (info) => { let bodyManifest = (await info.request.json()) as Manifest; if (!bodyManifest.manifestTrackingNumber) - bodyManifest.manifestTrackingNumber = `${Math.floor(Math.random() * 1000000000)}DFT`.padEnd( - 9, - '0' - ); + bodyManifest.manifestTrackingNumber = `${generateRandomMTN()}DFT`.padEnd(9, '0'); return HttpResponse.json(bodyManifest, { status: 200 }); }), /** Mock update local Manifests*/ diff --git a/client/src/test-utils/mock/mockSiteEndpoints.ts b/client/src/test-utils/mock/mockSiteEndpoints.ts new file mode 100644 index 000000000..82d8f0d95 --- /dev/null +++ b/client/src/test-utils/mock/mockSiteEndpoints.ts @@ -0,0 +1,17 @@ +import { http, HttpResponse } from 'msw'; +import { createMockHandler, createMockSite } from '../fixtures'; + +export const API_BASE_URL = import.meta.env.VITE_HT_API_URL; +const mockEpaId = createMockHandler().epaSiteId; +const mockSites = [createMockSite(), createMockSite()]; + +export const mockSiteEndpoints = [ + /** List user sites*/ + http.get(`${API_BASE_URL}/api/site`, (info) => { + return HttpResponse.json(mockSites, { status: 200 }); + }), + /** Site Details*/ + http.get(`${API_BASE_URL}/api/site/${mockEpaId}`, (info) => { + return HttpResponse.json(mockSites[0], { status: 200 }); + }), +]; diff --git a/client/src/test-utils/mock/userApiMocks.ts b/client/src/test-utils/mock/mockUserEndpoints.ts similarity index 97% rename from client/src/test-utils/mock/userApiMocks.ts rename to client/src/test-utils/mock/mockUserEndpoints.ts index eaf852d3e..564af4241 100644 --- a/client/src/test-utils/mock/userApiMocks.ts +++ b/client/src/test-utils/mock/mockUserEndpoints.ts @@ -9,7 +9,7 @@ import { /** mock Rest API*/ const API_BASE_URL = import.meta.env.VITE_HT_API_URL; -export const userApiMocks = [ +export const mockUserEndpoints = [ /** GET User */ http.get(`${API_BASE_URL}/api/user`, () => { return HttpResponse.json({ ...createMockHaztrakUser() }, { status: 200 }); diff --git a/client/src/test-utils/mock/wasteApiMocks.ts b/client/src/test-utils/mock/mockWasteEndpoints.ts similarity index 95% rename from client/src/test-utils/mock/wasteApiMocks.ts rename to client/src/test-utils/mock/mockWasteEndpoints.ts index 10002d987..4235f59fd 100644 --- a/client/src/test-utils/mock/wasteApiMocks.ts +++ b/client/src/test-utils/mock/mockWasteEndpoints.ts @@ -3,7 +3,7 @@ import { mockDotIdNumbers, mockFederalWasteCodes } from 'test-utils/fixtures/moc /** mock Rest API*/ const API_BASE_URL = import.meta.env.VITE_HT_API_URL; -export const wasteApiMocks = [ +export const mockWasteEndpoints = [ /** GET User */ http.get(`${API_BASE_URL}/api/rcra/waste/code/federal`, (info) => { return HttpResponse.json(mockFederalWasteCodes, { status: 200 }); diff --git a/server/apps/conftest.py b/server/apps/conftest.py index a383ab5bc..5bb329d3e 100644 --- a/server/apps/conftest.py +++ b/server/apps/conftest.py @@ -1,3 +1,4 @@ +import datetime import json import os import random @@ -9,6 +10,7 @@ import pytest_mock import responses from django.contrib.auth.models import User +from django.db import IntegrityError from faker import Faker from faker.providers import BaseProvider from rest_framework.test import APIClient @@ -24,7 +26,7 @@ RcraPhone, RcraSite, ) -from apps.site.models import TrakSite, TrakSiteAccess +from apps.site.models import Site, SiteAccess class SiteIDProvider(BaseProvider): @@ -186,14 +188,18 @@ def create_rcra_site( ) -> RcraSite: fake = Faker() fake.add_provider(SiteIDProvider) - return RcraSite.objects.create( - epa_id=epa_id or fake.site_id(), - name=name or fake.name(), - site_type=site_type, - site_address=site_address or address_factory(), - mail_address=mail_address or address_factory(), - contact=contact_factory(), - ) + while True: + try: + return RcraSite.objects.create( + epa_id=epa_id or fake.site_id(), + name=name or fake.name(), + site_type=site_type, + site_address=site_address or address_factory(), + mail_address=mail_address or address_factory(), + contact=contact_factory(), + ) + except IntegrityError: + epa_id = None return create_rcra_site @@ -235,11 +241,14 @@ def create_site( rcra_site: Optional[RcraSite] = None, name: Optional[str] = None, org: Optional[TrakOrg] = None, - ) -> TrakSite: - return TrakSite.objects.create( + last_rcrainfo_manifest_sync: Optional[datetime.datetime] = None, + ) -> Site: + return Site.objects.create( rcra_site=rcra_site or rcra_site_factory(), name=name or faker.name(), org=org or org_factory(), + last_rcrainfo_manifest_sync=last_rcrainfo_manifest_sync + or datetime.datetime.now(datetime.UTC), ) return create_site @@ -295,12 +304,12 @@ def site_access_factory(db, faker, site_factory, profile_factory): """Abstract factory for Haztrak RcraSitePermissions model""" def create_permission( - site: Optional[TrakSite] = None, + site: Optional[Site] = None, user: Optional[TrakUser] = None, emanifest: Optional[Literal["viewer", "signer", "editor"]] = "viewer", - ) -> TrakSiteAccess: + ) -> SiteAccess: """Returns testuser1 RcraSitePermissions model to site_generator""" - return TrakSiteAccess.objects.create( + return SiteAccess.objects.create( site=site or site_factory(), user=user or user_factory(), emanifest=emanifest, diff --git a/server/apps/core/exceptions.py b/server/apps/core/exceptions.py index a11282bfb..7c64f52cc 100644 --- a/server/apps/core/exceptions.py +++ b/server/apps/core/exceptions.py @@ -6,7 +6,7 @@ from rest_framework.serializers import as_serializer_error from rest_framework.views import exception_handler -from apps.site.services import TrakSiteServiceError +from apps.site.services import SiteServiceError class InternalServer500(APIException): @@ -36,7 +36,7 @@ def haztrak_exception_handler(exc, context): exc = exceptions.ParseError() case ValueError(): exc = InternalServer500() - case TrakSiteServiceError(): + case SiteServiceError(): exc = InternalServer500() response = exception_handler(exc, context) diff --git a/server/apps/core/services/rcrainfo_service.py b/server/apps/core/services/rcrainfo_service.py index 0852314ac..cd5fa2253 100644 --- a/server/apps/core/services/rcrainfo_service.py +++ b/server/apps/core/services/rcrainfo_service.py @@ -120,7 +120,6 @@ def get_rcrainfo_client( ) try: org: TrakOrg = TrakOrg.objects.get(trakorgaccess__user__username=username) - print("org:", org) if org.is_rcrainfo_integrated: api_id, api_key = org.rcrainfo_api_id_key return RcrainfoService( diff --git a/server/apps/manifest/services/__init__.py b/server/apps/manifest/services/__init__.py index b1b75b6b3..25b6e1fac 100644 --- a/server/apps/manifest/services/__init__.py +++ b/server/apps/manifest/services/__init__.py @@ -1,4 +1,11 @@ from .emanifest import EManifest, EManifestError, PullManifestsResult, TaskResponse +from .emanifest_search import ( + CorrectionRequestStatus, + DateType, + EmanifestSearch, + EmanifestStatus, + SiteType, +) from .manifest import ( create_manifest, get_manifests, diff --git a/server/apps/manifest/services/emanifest.py b/server/apps/manifest/services/emanifest.py index 2f5d79b07..8b8b3fa71 100644 --- a/server/apps/manifest/services/emanifest.py +++ b/server/apps/manifest/services/emanifest.py @@ -12,6 +12,7 @@ from apps.handler.serializers import QuickerSignSerializer from apps.manifest.models import Manifest from apps.manifest.serializers import ManifestSerializer +from apps.manifest.services.emanifest_search import EmanifestSearch from apps.manifest.tasks import pull_manifest, sign_manifest logger = logging.getLogger(__name__) @@ -164,3 +165,35 @@ def _save_manifest_json_to_db(self, manifest_json: dict) -> Manifest: manifest = serializer.save() logger.info(f"saved manifest {manifest.mtn}") return manifest + + +def get_updated_mtn(site_id: str, last_sync_date: datetime, rcra_client) -> list[str]: + """Use the last sync date for a site to get a list of updated MTNs from RCRAInfo""" + logger.info(f"retrieving updated MTN for site {site_id}") + response = ( + EmanifestSearch(rcra_client) + .add_date_type("UpdatedDate") + .add_site_id(site_id) + .add_start_date(last_sync_date) + .add_end_date() + .execute() + ) + if response.ok: + return response.json() + return [] + + +@transaction.atomic +def sync_manifests( + *, site_id: str, last_sync_date: datetime, rcra_client: RcrainfoService +) -> PullManifestsResult: + """Pull manifests and update the last sync date for a site""" + updated_mtn = get_updated_mtn( + site_id=site_id, + last_sync_date=last_sync_date, + rcra_client=rcra_client, + ) + updated_mtn = updated_mtn[:15] # temporary limit to 15 + emanifest = EManifest(rcrainfo=rcra_client) + results: PullManifestsResult = emanifest.pull(tracking_numbers=updated_mtn) + return results diff --git a/server/apps/manifest/services/emanifest_search.py b/server/apps/manifest/services/emanifest_search.py index 5e44d9df7..e3cedf3d3 100644 --- a/server/apps/manifest/services/emanifest_search.py +++ b/server/apps/manifest/services/emanifest_search.py @@ -20,6 +20,8 @@ DateType = Literal["CertifiedDate", "ReceivedDate", "ShippedDate", "UpdatedDate"] +RCRAINFO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + class EmanifestSearch: def __init__(self, rcra_client: Optional[RcrainfoService] = None): @@ -72,7 +74,7 @@ def _emanifest_correction_request_status(cls, correction_request_status) -> bool return cls.__validate_literal(correction_request_status, CorrectionRequestStatus) def _date_or_three_years_past(self, start_date: Optional[datetime]) -> str: - return ( + date = ( start_date.replace(tzinfo=timezone.utc).strftime(self.rcra_client.datetime_format) if start_date else ( @@ -85,13 +87,19 @@ def _date_or_three_years_past(self, start_date: Optional[datetime]) -> str: ) ).strftime(self.rcra_client.datetime_format) ) + return self.__format_rcrainfo_date_string(date) def _date_or_now(self, end_date: Optional[datetime]) -> str: - return ( + date = ( end_date.replace(tzinfo=timezone.utc).strftime(self.rcra_client.datetime_format) if end_date else datetime.now(UTC).strftime(self.rcra_client.datetime_format) ) + return self.__format_rcrainfo_date_string(date) + + @staticmethod + def __format_rcrainfo_date_string(str_date: str) -> str: + return str_date[:-8] + "Z" def build_search_args(self): search_params = { @@ -142,13 +150,18 @@ def add_correction_request_status(self, correction_request_status: CorrectionReq return self def add_start_date(self, start_date: datetime = None): + """Start of date range for manifest search. Default to three years ago.""" self.start_date = self._date_or_three_years_past(start_date) return self def add_end_date(self, end_date: datetime = None): + """End of date range for manifest search. Default to now.""" self.end_date = self._date_or_now(end_date) return self + def output(self): + return self.build_search_args() + def execute(self): search_args = self.build_search_args() return self._rcra_client.search_mtn(**search_args) diff --git a/server/apps/manifest/services/manifest.py b/server/apps/manifest/services/manifest.py index 73d4426ce..9c03ec286 100644 --- a/server/apps/manifest/services/manifest.py +++ b/server/apps/manifest/services/manifest.py @@ -7,7 +7,7 @@ from apps.manifest.models import Manifest from apps.manifest.services import EManifest, EManifestError, TaskResponse from apps.manifest.tasks import save_to_emanifest as save_to_emanifest_task -from apps.site.models import TrakSite +from apps.site.models import Site logger = logging.getLogger(__name__) @@ -29,9 +29,7 @@ def get_manifests( site_type: Optional[Literal["Generator", "Tsdf", "Transporter"]] = None, ) -> QuerySet[Manifest]: """Get a list of manifest tracking numbers and select details for a users site""" - sites: QuerySet[TrakSite] = TrakSite.objects.filter_by_username(username).values( - "rcra_site__epa_id" - ) + sites: QuerySet[Site] = Site.objects.filter_by_username(username).values("rcra_site__epa_id") if epa_id: sites = sites.filter(rcra_site__epa_id__iexact=epa_id) if site_type: diff --git a/server/apps/manifest/tasks.py b/server/apps/manifest/tasks.py index 79734ca76..a39054d05 100644 --- a/server/apps/manifest/tasks.py +++ b/server/apps/manifest/tasks.py @@ -4,6 +4,8 @@ from celery import Task, shared_task, states from celery.exceptions import Ignore, Reject +from apps.core.services import get_rcrainfo_client + logger = logging.getLogger(__name__) @@ -57,11 +59,17 @@ def sign_manifest( @shared_task(name="sync site manifests", bind=True) def sync_site_manifests(self, *, site_id: str, username: str): """asynchronous task to sync an EPA site's manifests""" - from apps.site.services import TrakSiteService + + from apps.manifest.services.emanifest import sync_manifests + from apps.site.services import get_user_site, update_emanifest_sync_date try: - site_service = TrakSiteService(username=username) - results = site_service.sync_manifests(site_id=site_id) + client = get_rcrainfo_client(username=username) + site = get_user_site(username=username, epa_id=site_id) + results = sync_manifests( + site_id=site_id, last_sync_date=site.last_rcrainfo_manifest_sync, rcra_client=client + ) + update_emanifest_sync_date(site=site) return results except Exception as exc: logger.error(f"failed to sync {site_id} manifest") diff --git a/server/apps/manifest/tests/conftest.py b/server/apps/manifest/tests/conftest.py index 690b5bd9b..2c21e259c 100644 --- a/server/apps/manifest/tests/conftest.py +++ b/server/apps/manifest/tests/conftest.py @@ -4,6 +4,7 @@ from typing import Optional import pytest +from django.db import IntegrityError from faker import Faker from faker.providers import BaseProvider @@ -36,18 +37,24 @@ def create_manifest( ) -> Manifest: fake = Faker() fake.add_provider(MtnProvider) - return Manifest.objects.create( - mtn=mtn or fake.mtn(), - status=status or fake.status(), - created_date=datetime.now(UTC), - potential_ship_date=datetime.now(UTC), - generator=generator - or manifest_handler_factory( - rcra_site=rcra_site_factory(site_type=RcraSiteType.GENERATOR) - ), - tsdf=tsdf - or manifest_handler_factory(rcra_site=rcra_site_factory(site_type=RcraSiteType.TSDF)), - ) + while True: + try: + return Manifest.objects.create( + mtn=mtn or fake.mtn(), + status=status or fake.status(), + created_date=datetime.now(UTC), + potential_ship_date=datetime.now(UTC), + generator=generator + or manifest_handler_factory( + rcra_site=rcra_site_factory(site_type=RcraSiteType.GENERATOR) + ), + tsdf=tsdf + or manifest_handler_factory( + rcra_site=rcra_site_factory(site_type=RcraSiteType.TSDF) + ), + ) + except IntegrityError: + mtn = None return create_manifest diff --git a/server/apps/manifest/tests/test_service.py b/server/apps/manifest/tests/test_emanifest_service.py similarity index 100% rename from server/apps/manifest/tests/test_service.py rename to server/apps/manifest/tests/test_emanifest_service.py diff --git a/server/apps/manifest/tests/test_search_emanifest.py b/server/apps/manifest/tests/test_search_emanifest.py index 245e2e5ef..a02d2924a 100644 --- a/server/apps/manifest/tests/test_search_emanifest.py +++ b/server/apps/manifest/tests/test_search_emanifest.py @@ -1,5 +1,5 @@ import json -from datetime import datetime +from datetime import UTC, datetime import pytest @@ -101,7 +101,7 @@ def test_add_end_date(self): assert search.end_date is not None def test_add_end_date_defaults_to_now(self): - now = datetime.now() + now = datetime.now(UTC) search = EmanifestSearch().add_end_date() end_date = datetime.strptime(search.end_date, RcrainfoService.datetime_format) assert end_date.day == now.day diff --git a/server/apps/manifest/urls.py b/server/apps/manifest/urls.py index 29884070d..d859d4b7c 100644 --- a/server/apps/manifest/urls.py +++ b/server/apps/manifest/urls.py @@ -6,11 +6,11 @@ ElectronicManifestSignView, ManifestViewSet, MtnListView, - TrakSiteManifestSyncView, + SiteManifestSyncView, ) manifest_router = SimpleRouter(trailing_slash=False) -manifest_router.register("manifest", ManifestViewSet, basename="manifest") +manifest_router.register("", ManifestViewSet, basename="manifest") urlpatterns = [ path( @@ -20,12 +20,12 @@ # Manifest path("emanifest", ElectronicManifestSaveView.as_view()), path("emanifest/sign", ElectronicManifestSignView.as_view()), - path("emanifest/sync", TrakSiteManifestSyncView.as_view()), - path("", include(manifest_router.urls)), + path("emanifest/sync", SiteManifestSyncView.as_view()), # MT path("mtn", MtnListView.as_view()), path("mtn/", MtnListView.as_view()), path("mtn//", MtnListView.as_view()), + path("", include(manifest_router.urls)), ] ), ), diff --git a/server/apps/manifest/views.py b/server/apps/manifest/views.py index bacfdc185..f2b94177f 100644 --- a/server/apps/manifest/views.py +++ b/server/apps/manifest/views.py @@ -18,7 +18,7 @@ save_emanifest, update_manifest, ) -from apps.site.services import TrakSiteService +from apps.site.services import sync_site_manifest_with_rcrainfo logger = logging.getLogger(__name__) @@ -88,6 +88,7 @@ class MtnListView(ListAPIView): queryset = Manifest.objects.all() def get(self, request, *args, **kwargs): + print(f" MTN List View {request.user}") return super().get(request, *args, **kwargs) def get_queryset(self): @@ -127,7 +128,7 @@ def post(self, request: Request) -> Response: "site_manifest_sync_response", fields={"task": serializers.CharField()} ), ) -class TrakSiteManifestSyncView(APIView): +class SiteManifestSyncView(APIView): """ Pull a site's manifests that are out of sync with RCRAInfo. It returns the task id of the long-running background task which can be used to poll @@ -140,6 +141,7 @@ class SyncSiteManifestSerializer(serializers.Serializer): def post(self, request: Request) -> Response: serializer = self.SyncSiteManifestSerializer(data=request.data) serializer.is_valid(raise_exception=True) - site = TrakSiteService(username=str(request.user)) - data = site.sync_rcrainfo_manifest(**serializer.validated_data) + data = sync_site_manifest_with_rcrainfo( + username=request.user.username, **serializer.validated_data + ) return Response(data=data, status=status.HTTP_200_OK) diff --git a/server/apps/org/admin.py b/server/apps/org/admin.py index 4fe16a383..ce6684437 100644 --- a/server/apps/org/admin.py +++ b/server/apps/org/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from apps.org.models import TrakOrg, TrakOrgAccess -from apps.site.models import TrakSite +from apps.site.models import Site admin.site.register(TrakOrgAccess) @@ -9,7 +9,6 @@ @admin.register(TrakOrg) class HaztrakOrgAdmin(admin.ModelAdmin): list_display = ["__str__", "number_of_sites"] - # inlines = [HaztrakSiteInline, HaztrakProfileInline] readonly_fields = ["rcrainfo_integrated"] def rcrainfo_integrated(self, obj): @@ -19,4 +18,4 @@ def rcrainfo_integrated(self, obj): rcrainfo_integrated.short_description = "Admin has setup RCRAInfo integration" def number_of_sites(self, org: TrakOrg): - return TrakSite.objects.filter(org=org).count() + return Site.objects.filter(org=org).count() diff --git a/server/apps/profile/serializers/trak_profile.py b/server/apps/profile/serializers/trak_profile.py index 41a61a200..459f7f3d9 100644 --- a/server/apps/profile/serializers/trak_profile.py +++ b/server/apps/profile/serializers/trak_profile.py @@ -3,7 +3,7 @@ from apps.org.serializers import TrakOrgSerializer from apps.profile.models import TrakProfile -from apps.site.serializers import TrakSiteAccessSerializer +from apps.site.serializers import SiteAccessSerializer class TrakProfileSerializer(ModelSerializer): @@ -12,7 +12,7 @@ class TrakProfileSerializer(ModelSerializer): user = serializers.StringRelatedField( required=False, ) - sites = TrakSiteAccessSerializer( + sites = SiteAccessSerializer( source="user.site_permissions", many=True, ) diff --git a/server/apps/profile/services.py b/server/apps/profile/services.py index 9b8db2af2..9b381247f 100644 --- a/server/apps/profile/services.py +++ b/server/apps/profile/services.py @@ -1,4 +1,5 @@ """business logic related to a user's Haztrak profile (note: not their RcrainfoProfile)""" + from typing import Optional from django.conf import settings @@ -10,7 +11,7 @@ from apps.profile.serializers import RcrainfoSitePermissionsSerializer from apps.rcrasite.models import RcraSite from apps.rcrasite.services import RcraSiteService -from apps.site.services import TrakSiteServiceError +from apps.site.services import SiteServiceError @transaction.atomic @@ -98,7 +99,7 @@ def _save_rcrainfo_profile_permissions(self, permissions: list[dict]) -> None: self._create_or_update_rcra_permission( epa_permission=rcra_site_permission, site=rcra_site ) - except TrakSiteServiceError as exc: + except SiteServiceError as exc: raise RcraProfileServiceError(f"Error creating or updating Haztrak Site {exc}") except KeyError as exc: raise RcraProfileServiceError(f"Error parsing RCRAInfo response: {str(exc)}") diff --git a/server/apps/rcrasite/admin.py b/server/apps/rcrasite/admin.py index 7abdd323e..28aa241bc 100644 --- a/server/apps/rcrasite/admin.py +++ b/server/apps/rcrasite/admin.py @@ -1,6 +1,4 @@ from django.contrib import admin -from django.urls import reverse -from django.utils.html import format_html, urlencode from apps.core.admin import HiddenListView from apps.rcrasite.models import ( @@ -8,42 +6,15 @@ Contact, RcraSite, ) -from apps.site.models import TrakSite @admin.register(RcraSite) -class HandlerAdmin(admin.ModelAdmin): +class RcraSiteAdmin(admin.ModelAdmin): list_display = ["__str__", "site_type", "site_address", "mail_address"] list_filter = ["site_type"] search_fields = ["epa_id"] -@admin.register(TrakSite) -class HaztrakSiteAdmin(admin.ModelAdmin): - list_display = ["__str__", "related_handler", "last_rcrainfo_manifest_sync"] - list_display_links = ["__str__", "related_handler"] - - @admin.display(description="EPA Site") - def related_handler(self, site: TrakSite) -> str: - url = ( - reverse("admin:sites_rcrasite_changelist") - + "?" - + urlencode({"epa_id": str(site.rcra_site.epa_id)}) - ) - return format_html("{}", url, site.rcra_site.epa_id) - - -class HaztrakSiteInline(admin.TabularInline): - model = TrakSite - extra = 0 - - def has_change_permission(self, request, obj=None): - return False - - def has_delete_permission(self, request, obj=None): - return False - - # Register models That should only be edited within the context of another form here. admin.site.register(Contact, HiddenListView) admin.site.register(Address, HiddenListView) diff --git a/server/apps/site/admin.py b/server/apps/site/admin.py index 8c38f3f3d..5fd18a9a1 100644 --- a/server/apps/site/admin.py +++ b/server/apps/site/admin.py @@ -1,3 +1,10 @@ from django.contrib import admin -# Register your models here. +from apps.site.models import Site + + +@admin.register(Site) +class HaztrakSiteAdmin(admin.ModelAdmin): + list_display = ["__str__", "last_rcrainfo_manifest_sync"] + readonly_fields = ["rcra_site"] + search_fields = ["rcra_site__epa_id"] diff --git a/server/apps/site/migrations/0001_initial.py b/server/apps/site/migrations/0001_initial.py index 7d9c29a4d..3771f0ba1 100644 --- a/server/apps/site/migrations/0001_initial.py +++ b/server/apps/site/migrations/0001_initial.py @@ -1,9 +1,9 @@ -# Generated by Django 4.2.10 on 2024-02-21 22:17 +# Generated by Django 5.0.4 on 2024-05-11 23:37 -from django.conf import settings import django.core.validators -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -18,7 +18,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='TrakSite', + name='Site', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200, validators=[django.core.validators.MinLengthValidator(2, 'site aliases must be longer than 2 characters')], verbose_name='site alias')), @@ -33,11 +33,11 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='TrakSiteAccess', + name='SiteAccess', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('emanifest', models.CharField(choices=[('viewer', 'view'), ('editor', 'edit'), ('signer', 'sign')], default='view', max_length=6)), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='site.traksite')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='site.site')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_permissions', to=settings.AUTH_USER_MODEL)), ], options={ diff --git a/server/apps/site/models.py b/server/apps/site/models.py index 9275b1493..9028b192b 100644 --- a/server/apps/site/models.py +++ b/server/apps/site/models.py @@ -4,30 +4,37 @@ from django.db.models import QuerySet -class TrakSiteManager(models.Manager): - """Custom manager for TrakSite model""" +class SiteManager(models.Manager): + """Query interface for the Site model""" def filter_by_username(self: models.Manager, username: str) -> QuerySet: """filter a list of sites a user has access to (by username)""" - return self.select_related("rcra_site").filter(traksiteaccess__user__username=username) + return self.select_related("rcra_site").filter(siteaccess__user__username=username) - def get_user_site_by_epa_id( + def get_by_user_and_epa_id( self: models.Manager, user: settings.AUTH_USER_MODEL, epa_id: str ) -> QuerySet: """Get a site by EPA ID number that a user has access to""" combined_filter: QuerySet = self.filter_by_user(user) & self.filter_by_epa_id(epa_id) return combined_filter.get() + def get_by_username_and_epa_id(self: models.Manager, username: str, epa_id: str) -> QuerySet: + """Get a site by EPA ID number that a user has access to""" + combined_filter: QuerySet = self.filter_by_username(username) & self.filter_by_epa_id( + epa_id + ) + return combined_filter.get() + def filter_by_user(self: models.Manager, user: settings.AUTH_USER_MODEL) -> QuerySet: """filter a list of sites a user has access to (by user object)""" - return self.select_related("rcra_site").filter(traksiteaccess__user=user) + return self.select_related("rcra_site").filter(siteaccess__user=user) def filter_by_epa_id(self: models.Manager, epa_id: str) -> QuerySet: """filter a sites by EPA ID number""" return self.filter(rcra_site__epa_id=epa_id) def get_by_epa_id(self: models.Manager, epa_id: str) -> QuerySet: - """Get a TrakSites by RCRAInfo EPA ID number. Throws TrakSite.DoesNotExist if not found.""" + """Get a site by RCRAInfo EPA ID number. Throws Site.DoesNotExist if not found.""" return self.filter_by_epa_id(epa_id).get() def filter_by_org(self: models.Manager, org: settings.TRAK_ORG_MODEL) -> QuerySet: @@ -35,22 +42,20 @@ def filter_by_org(self: models.Manager, org: settings.TRAK_ORG_MODEL) -> QuerySe return self.filter(org=org) -class TrakSite(models.Model): +class Site(models.Model): """ Haztrak Site is a cornerstone model that many other models rely on. It wraps around RCRAInfo sites (AKA handlers, our RcraSite object). and adds additional functionality and fields. """ - objects = TrakSiteManager() + objects = SiteManager() class Meta: verbose_name = "Haztrak Site" verbose_name_plural = "Haztrak Sites" ordering = ["rcra_site__epa_id"] - # ToDo: use UUIDField as primary key - name = models.CharField( verbose_name="site alias", max_length=200, @@ -81,7 +86,7 @@ def __str__(self): return f"{self.rcra_site.epa_id}" -class TrakSiteAccess(models.Model): +class SiteAccess(models.Model): """The Role Based access a user has to a site""" class Meta: @@ -95,7 +100,7 @@ class Meta: related_name="site_permissions", ) site = models.ForeignKey( - TrakSite, + Site, on_delete=models.CASCADE, ) emanifest = models.CharField( diff --git a/server/apps/site/serializers.py b/server/apps/site/serializers.py index ea03367ff..b6c102303 100644 --- a/server/apps/site/serializers.py +++ b/server/apps/site/serializers.py @@ -3,12 +3,12 @@ from apps.rcrasite.serializers import RcraSiteSerializer from apps.rcrasite.serializers.base_serializer import SitesBaseSerializer -from apps.site.models import TrakSite, TrakSiteAccess +from apps.site.models import Site, SiteAccess -class TrakSiteSerializer(ModelSerializer): +class SiteSerializer(ModelSerializer): """ - HaztrakSite model serializer for JSON marshalling/unmarshalling + Haztrak Site model serializer for JSON marshalling/unmarshalling """ name = serializers.CharField( @@ -19,19 +19,19 @@ class TrakSiteSerializer(ModelSerializer): ) class Meta: - model = TrakSite + model = Site fields = ["name", "handler"] -class TrakSiteAccessSerializer(SitesBaseSerializer): +class SiteAccessSerializer(SitesBaseSerializer): class Meta: - model = TrakSiteAccess + model = SiteAccess fields = [ "site", "eManifest", ] - site = TrakSiteSerializer() + site = SiteSerializer() eManifest = serializers.CharField( source="emanifest", ) diff --git a/server/apps/site/services.py b/server/apps/site/services.py index 24ca924df..adc454834 100644 --- a/server/apps/site/services.py +++ b/server/apps/site/services.py @@ -5,73 +5,48 @@ from django.db import transaction from django.db.models import QuerySet -from apps.core.services import RcrainfoService, get_rcrainfo_client -from apps.manifest.services import EManifest, PullManifestsResult, TaskResponse -from apps.manifest.services.emanifest_search import EmanifestSearch +from apps.manifest.services import TaskResponse from apps.manifest.tasks import sync_site_manifests -from apps.site.models import TrakSite +from apps.site.models import Site logger = logging.getLogger(__name__) -class TrakSiteService: - """ - HaztrakSiteService encapsulates the Haztrak site subdomain business logic and use cases. - """ +class SiteServiceError(Exception): + def __init__(self, message: str): + super().__init__(message) - def __init__( - self, - *, - username: str, - site_id: Optional[str] = None, - rcrainfo: Optional[RcrainfoService] = None, - ): - self.username = username - self.rcrainfo = rcrainfo or get_rcrainfo_client(username=username) - self.site_id = site_id - def sync_rcrainfo_manifest(self, *, site_id: Optional[str] = None) -> TaskResponse: - """Validate input and Launch a Celery task to sync a site's manifests from RCRAInfo""" - logger.info(f"{self} sync rcra manifest, site ID {site_id}") - task = sync_site_manifests.delay(site_id=site_id, username=self.username) - return {"taskId": task.id} +@transaction.atomic +def update_emanifest_sync_date(site: Site, last_sync_date: Optional[datetime] = None): + """Update the last sync date for a site. Defaults to now if no date is provided.""" + if last_sync_date is not None: + site.last_rcrainfo_manifest_sync = last_sync_date + else: + site.last_rcrainfo_manifest_sync = datetime.now(UTC) + site.save() - @transaction.atomic - def sync_manifests(self, *, site_id: str) -> PullManifestsResult: - """Pull manifests and update the last sync date for a site""" - try: - site = TrakSite.objects.get_by_epa_id(site_id) - updated_mtn = self._get_updated_mtn( - site_id=site.rcra_site.epa_id, last_sync_date=site.last_rcrainfo_manifest_sync - ) - updated_mtn = updated_mtn[:15] # temporary limit to 15 - logger.info(f"Pulling {updated_mtn} from RCRAInfo") - emanifest = EManifest(username=self.username, rcrainfo=self.rcrainfo) - results: PullManifestsResult = emanifest.pull(tracking_numbers=updated_mtn) - site.last_rcrainfo_manifest_sync = datetime.now(UTC) - site.save() - return results - except TrakSite.DoesNotExist: - logger.warning(f"Site Does not exists {site_id}") - raise TrakSiteServiceError(f"Site Does not exists {site_id}") - def _get_updated_mtn(self, site_id: str, last_sync_date: datetime) -> list[str]: - logger.info(f"retrieving updated MTN for site {site_id}") - return ( - EmanifestSearch(self.rcrainfo) - .add_site_id(site_id) - .add_start_date(last_sync_date) - .add_end_date() - .execute() - ) +def filter_sites_by_org(org_id: str) -> [Site]: + """Returns a list of Sites associated with an Org.""" + sites: QuerySet = Site.objects.filter(org_id=org_id).select_related("rcra_site") + return sites -class TrakSiteServiceError(Exception): - def __init__(self, message: str): - super().__init__(message) +def get_user_site(username: str, epa_id: str) -> Site: + """Returns a user Site if it exists, else throws a DoesNotExist exception.""" + return Site.objects.get_by_username_and_epa_id(username, epa_id) -def filter_sites_by_org(org_id: str) -> [TrakSite]: - """Returns a list of TrakSites associated with an Org.""" - sites: QuerySet = TrakSite.objects.filter(org_id=org_id).select_related("rcra_site") +def filter_sites_by_username(username: str) -> [Site]: + """Returns a list of Sites associated with a user.""" + sites: QuerySet = Site.objects.filter_by_username(username) return sites + + +def sync_site_manifest_with_rcrainfo( + *, username: str, site_id: Optional[str] = None +) -> TaskResponse: + """Launch a batch processing task to sync a site's manifests from RCRAInfo""" + task = sync_site_manifests.delay(site_id=site_id, username=username) + return {"taskId": task.id} diff --git a/server/apps/site/tests/conftest.py b/server/apps/site/tests/conftest.py new file mode 100644 index 000000000..a678dcc53 --- /dev/null +++ b/server/apps/site/tests/conftest.py @@ -0,0 +1,25 @@ +from typing import Optional + +import pytest + +from apps.org.models import TrakOrg +from apps.rcrasite.models import RcraSite +from apps.site.models import Site + + +@pytest.fixture +def site_class_factory(faker): + """Abstract factory for Site class""" + + def create_site( + rcra_site: Optional[RcraSite] = None, + name: Optional[str] = None, + org: Optional[TrakOrg] = None, + ) -> Site: + return Site( + rcra_site=rcra_site or RcraSite(site_type="TSDF", epa_id="foo"), + name=name or faker.name(), + org=org or TrakOrg(name=faker.company()), + ) + + return create_site diff --git a/server/apps/site/tests/test_models.py b/server/apps/site/tests/test_models.py index 2709c89f4..63494844b 100644 --- a/server/apps/site/tests/test_models.py +++ b/server/apps/site/tests/test_models.py @@ -1,14 +1,14 @@ import pytest from django.db.models import QuerySet -from apps.site.models import TrakSite +from apps.site.models import Site @pytest.mark.django_db -class TestTrakSiteModel: +class TestSiteModel: def test_haztrak_site_model_factory(self, site_factory): haztrak_site = site_factory() - assert isinstance(haztrak_site, TrakSite) + assert isinstance(haztrak_site, Site) def test_returns_true_if_admin_has_provided_api_credentials( self, @@ -41,13 +41,13 @@ def test_returns_false_if_admin_has_not_provided_api_credentials( assert not site.admin_has_rcrainfo_api_credentials -class TestTrakSiteModelManager: +class TestSiteManager: def test_filter_sites_by_username(self, site_factory, site_access_factory, user_factory): user = user_factory() site = site_factory() other_site = site_factory() site_access_factory(site=site, user=user) - sites = TrakSite.objects.filter_by_username(user.username) + sites = Site.objects.filter_by_username(user.username) assert site in sites assert other_site not in sites @@ -56,33 +56,43 @@ def test_filter_user_sites(self, site_factory, site_access_factory, user_factory site = site_factory() other_site = site_factory() site_access_factory(site=site, user=user) - sites = TrakSite.objects.filter_by_user(user) + sites = Site.objects.filter_by_user(user) assert site in sites assert other_site not in sites def test_get_by_epa_id(self, site_factory): site = site_factory() - returned_site = TrakSite.objects.get_by_epa_id(site.rcra_site.epa_id) + returned_site = Site.objects.get_by_epa_id(site.rcra_site.epa_id) assert site == returned_site - assert isinstance(returned_site, TrakSite) + assert isinstance(returned_site, Site) def test_get_by_epa_id_throws_does_not_exists(self, site_factory): - with pytest.raises(TrakSite.DoesNotExist): - TrakSite.objects.get_by_epa_id("bad_id") + with pytest.raises(Site.DoesNotExist): + Site.objects.get_by_epa_id("bad_id") def test_get_user_site(self, site_factory, site_access_factory, user_factory): user = user_factory() site = site_factory() site_access_factory(site=site, user=user) - returned_site = TrakSite.objects.get_user_site_by_epa_id(user, site.rcra_site.epa_id) - assert isinstance(returned_site, TrakSite) + returned_site = Site.objects.get_by_user_and_epa_id(user, site.rcra_site.epa_id) + assert isinstance(returned_site, Site) assert returned_site == site def test_filter_sites_by_org(self, site_factory, org_factory): org = org_factory() sites = [site_factory(org=org) for _ in range(2)] not_my_site = site_factory() - returned_sites = TrakSite.objects.filter_by_org(org.id) + returned_sites = Site.objects.filter_by_org(org.id) assert isinstance(returned_sites, QuerySet) assert set(sites).issubset(returned_sites) assert not_my_site not in returned_sites + + def test_get_by_username_and_epa_id(self, site_factory, site_access_factory, user_factory): + user = user_factory() + site = site_factory() + site_access_factory(site=site, user=user) + returned_site = Site.objects.get_by_username_and_epa_id( + user.username, site.rcra_site.epa_id + ) + assert isinstance(returned_site, Site) + assert returned_site == site diff --git a/server/apps/site/tests/test_services.py b/server/apps/site/tests/test_services.py index 3ac791519..31306e582 100644 --- a/server/apps/site/tests/test_services.py +++ b/server/apps/site/tests/test_services.py @@ -1,4 +1,10 @@ -from apps.site.services import filter_sites_by_org +import datetime +from unittest.mock import patch + +import pytest + +from apps.site.models import Site +from apps.site.services import filter_sites_by_org, get_user_site, update_emanifest_sync_date class TestFilterOrgSites: @@ -7,3 +13,53 @@ def test_get_org_sites(self, org_factory, site_factory): site = site_factory(org=org) returned_sites = filter_sites_by_org(org_id=org.id) assert site in returned_sites + + +class TestGetUserSite: + def test_returns_site_by_epa_id(self, site_class_factory): + with patch("apps.site.services.Site.objects.get_by_username_and_epa_id") as mock_query: + mock_site = site_class_factory() + mock_query.return_value = mock_site + result = get_user_site(username="test", epa_id="test") + assert result == mock_site + + def test_raises_error_when_site_not_found(self, site_class_factory): + with patch("apps.site.services.Site.objects.get_by_username_and_epa_id") as mock_query: + mock_query.side_effect = Site.DoesNotExist + with pytest.raises(Site.DoesNotExist): + get_user_site(username="test", epa_id="test") + + +class TestFilterSitesByUser: + def test_returns_array_of_sites(self, site_factory, user_factory): + with patch("apps.site.services.Site.objects.filter_by_user") as mock_query: + site = site_factory() + mock_query.return_value = [site] + result = Site.objects.filter_by_user("username") + assert site in result + + def test_returns_empty_list_when_no_sites_found(self, site_factory, user_factory): + with patch("apps.site.services.Site.objects.filter_by_user") as mock_query: + mock_query.return_value = [] + result = Site.objects.filter_by_user("username") + assert isinstance(result, list) + + +class TestUpdateEmanifestSyncDate: + def test_updates_the_last_sync_field(self, site_factory): + site = site_factory(last_rcrainfo_manifest_sync=None) + update_emanifest_sync_date(site=site) + assert site.last_rcrainfo_manifest_sync is not None + + def test_uses_datetime_now_by_default(self, site_factory): + with patch("apps.site.services.datetime") as mock_datetime: + mock_datetime.now.return_value = datetime.datetime(2021, 1, 1) + site = site_factory(last_rcrainfo_manifest_sync=None) + update_emanifest_sync_date(site=site) + assert site.last_rcrainfo_manifest_sync == datetime.datetime(2021, 1, 1) + + def test_uses_optional_passed_datetime(self, site_factory): + passed_datetime = datetime.datetime(2021, 1, 1) + site = site_factory(last_rcrainfo_manifest_sync=None) + update_emanifest_sync_date(site=site, last_sync_date=passed_datetime) + assert site.last_rcrainfo_manifest_sync == passed_datetime diff --git a/server/apps/site/tests/test_views.py b/server/apps/site/tests/test_views.py index 28f170457..5adea443a 100644 --- a/server/apps/site/tests/test_views.py +++ b/server/apps/site/tests/test_views.py @@ -2,10 +2,10 @@ from rest_framework import status from rest_framework.test import APIClient, APIRequestFactory, force_authenticate -from apps.site.views import TrakSiteDetailsView +from apps.site.views import SiteDetailsView -class TestTrakSiteListView: +class TestSiteListView: @pytest.fixture def api_client( self, @@ -38,7 +38,7 @@ def test_unauthenticated_returns_401(self, api_client): assert response.status_code == status.HTTP_401_UNAUTHORIZED -class TestTrakSiteDetailsApi: +class TestSiteDetailsApi: """ Tests the site details endpoint """ @@ -59,7 +59,7 @@ def test_returns_site_by_id( request = request.get(f"{self.url}/{site.rcra_site.epa_id}") force_authenticate(request, user) # Act - response = TrakSiteDetailsView.as_view()(request, epa_id=site.rcra_site.epa_id) + response = SiteDetailsView.as_view()(request, epa_id=site.rcra_site.epa_id) # Assert assert response.status_code == status.HTTP_200_OK assert response.data["handler"]["epaSiteId"] == site.rcra_site.epa_id @@ -82,7 +82,7 @@ def test_non_user_sites_not_returned( request = request.get(f"{self.url}/{other_site.rcra_site.epa_id}") force_authenticate(request, user) # Act - response = TrakSiteDetailsView.as_view()(request, epa_id=other_site.rcra_site.epa_id) + response = SiteDetailsView.as_view()(request, epa_id=other_site.rcra_site.epa_id) # Assert assert response.status_code == status.HTTP_404_NOT_FOUND @@ -107,7 +107,7 @@ def test_returns_formatted_http_response( assert response.status_code == status.HTTP_200_OK -class TestTrakOrgSitesListView: +class TestOrgSitesListView: URL = "/api/org" def test_returns_list_of_organizations_sites( diff --git a/server/apps/site/urls.py b/server/apps/site/urls.py index 35d254cc3..456642e8b 100644 --- a/server/apps/site/urls.py +++ b/server/apps/site/urls.py @@ -1,13 +1,13 @@ from django.urls import path from .views import ( - TrakOrgSitesListView, - TrakSiteDetailsView, - TrakSiteListView, + OrgSitesListView, + SiteDetailsView, + SiteListView, ) urlpatterns = [ - path("site", TrakSiteListView.as_view()), - path("site/", TrakSiteDetailsView.as_view()), - path("org//sites", TrakOrgSitesListView.as_view()), + path("site", SiteListView.as_view()), + path("site/", SiteDetailsView.as_view()), + path("org//sites", OrgSitesListView.as_view()), ] diff --git a/server/apps/site/views.py b/server/apps/site/views.py index 4e6cd85e6..671a09f7e 100644 --- a/server/apps/site/views.py +++ b/server/apps/site/views.py @@ -7,55 +7,52 @@ from rest_framework.response import Response from rest_framework.views import APIView -from apps.site.models import TrakSite -from apps.site.serializers import TrakSiteSerializer -from apps.site.services import filter_sites_by_org +from apps.site.models import Site +from apps.site.serializers import SiteSerializer +from apps.site.services import filter_sites_by_org, filter_sites_by_username, get_user_site logger = logging.getLogger(__name__) -class TrakSiteListView(ListAPIView): - """that returns haztrak sites that the current user has access to.""" +class SiteListView(ListAPIView): + """that returns all haztrak sites that the user has access to.""" - serializer_class = TrakSiteSerializer + serializer_class = SiteSerializer def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) - - def get_queryset(self): - return TrakSite.objects.filter_by_user(self.request.user) + sites = filter_sites_by_username(username=request.user.username) + data = self.serializer_class(sites, many=True).data + return Response(data, status=status.HTTP_200_OK) @method_decorator(cache_page(60 * 15), name="dispatch") -class TrakSiteDetailsView(RetrieveAPIView): - """View details of a Haztrak Site, which encapsulates the EPA RcraSite plus some.""" +class SiteDetailsView(RetrieveAPIView): + """View details of a Haztrak Site.""" - serializer_class = TrakSiteSerializer + serializer_class = SiteSerializer lookup_url_kwarg = "epa_id" - queryset = TrakSite.objects.all() + queryset = Site.objects.all() @method_decorator(cache_page(60 * 15)) def get(self, request, *args, **kwargs): try: - site = TrakSite.objects.get_user_site_by_epa_id( - request.user, self.kwargs.get("epa_id") - ) + site = get_user_site(username=request.user.username, epa_id=self.kwargs["epa_id"]) data = self.serializer_class(site).data return Response(data, status=status.HTTP_200_OK) - except TrakSite.DoesNotExist as e: + except Site.DoesNotExist as e: return Response(data=str(e), status=status.HTTP_404_NOT_FOUND) -class TrakOrgSitesListView(APIView): - """Retrieve a list of sites for a given Org""" +class OrgSitesListView(APIView): + """Retrieve a list of sites filtered by organization.""" @method_decorator(cache_page(60 * 15)) def get(self, request, *args, **kwargs): try: trak_sites = filter_sites_by_org(self.kwargs["org_id"]) - serializer = TrakSiteSerializer(trak_sites, many=True) + serializer = SiteSerializer(trak_sites, many=True) return Response(data=serializer.data, status=status.HTTP_200_OK) - except TrakSite.DoesNotExist as e: + except Site.DoesNotExist as e: return Response(data=str(e), status=status.HTTP_404_NOT_FOUND) except KeyError: return Response(data="bad request", status=status.HTTP_400_BAD_REQUEST) diff --git a/server/fixtures/dev_data.yaml b/server/fixtures/dev_data.yaml index f681fcc5c..bade306cb 100644 --- a/server/fixtures/dev_data.yaml +++ b/server/fixtures/dev_data.yaml @@ -288,14 +288,14 @@ fields: user: 4ac96f68-42cf-47ea-bffb-f24d423dbc35 rcrainfo_profile: 192c73f4-24f1-4f21-8239-dee2da43c547 -- model: site.traksite +- model: site.site pk: 1 fields: name: VA TEST GEN 2021 rcra_site: 1 last_rcrainfo_manifest_sync: null org: efb9e104-7f61-4365-a9af-9d7b55c854c4 -- model: site.traksiteaccess +- model: site.siteaccess pk: 1 fields: site: 1 diff --git a/server/haztrak/settings/base.py b/server/haztrak/settings/base.py index 36198db08..06f73ff94 100644 --- a/server/haztrak/settings/base.py +++ b/server/haztrak/settings/base.py @@ -1,6 +1,7 @@ """ Haztrak project settings. """ + import os from pathlib import Path @@ -232,6 +233,6 @@ TRAK_ORG_MODEL = "org.TrakOrg" TRAK_RCRAINFO_SITE_MODEL = "rcrasite.RcraSite" TRAK_MANIFEST_MODEL = "manifest.Manifest" -TRAK_SITE_MODEL = "site.TrakSite" +TRAK_SITE_MODEL = "site.Site" TRAK_WASTELINE_MODEL = "wasteline.Wasteline" TRAK_HANDLER_MODEL = "handler.Handler"