diff --git a/alembic/versions/a5fbdcd0e719_add_gspc_completions_table.py b/alembic/versions/a5fbdcd0e719_add_gspc_completions_table.py new file mode 100644 index 00000000..2d768acc --- /dev/null +++ b/alembic/versions/a5fbdcd0e719_add_gspc_completions_table.py @@ -0,0 +1,35 @@ +"""add gspc_completions table + +Revision ID: a5fbdcd0e719 +Revises: 51b251b1ec2a +Create Date: 2024-05-08 10:43:09.569708 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = 'a5fbdcd0e719' +down_revision = '51b251b1ec2a' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'gspc_completions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('passed', sa.Boolean(), nullable=False), + sa.Column('certification_expiration_date', sa.Date, nullable=False), + sa.Column('submit_ts', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('responses', postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade() -> None: + op.drop_table('gspc_completions') diff --git a/data/blank_certificates/c_gspc.pdf b/data/blank_certificates/c_gspc.pdf new file mode 100644 index 00000000..da0b5213 Binary files /dev/null and b/data/blank_certificates/c_gspc.pdf differ diff --git a/training-front-end/src/components/CertificateTable.vue b/training-front-end/src/components/CertificateTable.vue index f37bba57..908f3ce1 100644 --- a/training-front-end/src/components/CertificateTable.vue +++ b/training-front-end/src/components/CertificateTable.vue @@ -74,7 +74,7 @@
-

GSA SmartPay® Program Certification (GSPC) Requirements

+

GSA SmartPay Program Certification (GSPC) Requirements

- To earn your GSA SmartPay ® Program Certification you will need to: + To earn your GSA SmartPay Program Certification you will need to:

- You can complete the verification steps to receive your GSA SmartPay® Program Certification if you meet these requirements. + You can complete the verification steps to receive your GSA SmartPay Program Certification if you meet these requirements.

-

- Return to the GSA SmartPay Training Home Page + + +

+ Return to the GSA SmartPay Training Home Page +

You Don't Meet the Requirements for GSA SmartPay Program Certification (GSPC)

Once you have met the coursework and experience requirement of six months of continuous, hands-on experience working with the GSA SmartPay program please return to the link in your email to reapply.

-

If you have any questions ,please reference Smart Bulletin No. 022 or contact the GSPC Program Manager at smartpaygspc@gsa.com.

- Return to the GSA SmartPay Training Home Page +

If you have any questions, please reference Smart Bulletin No. 022 or contact the GSPC Program Manager at smartpaygspc@gsa.com.

+ Return to the GSA SmartPay Training Home Page
diff --git a/training-front-end/src/components/QuizResults.vue b/training-front-end/src/components/QuizResults.vue index 6f1e8b07..5643758f 100644 --- a/training-front-end/src/components/QuizResults.vue +++ b/training-front-end/src/components/QuizResults.vue @@ -29,7 +29,7 @@ const result_string = computed(() => `${props.quizResults.correct_count} of ${props.quizResults.question_count}`) const percentage = computed(() => (props.quizResults.percentage * 100).toFixed(0)) - const quiz_certificate_url = computed(() => `${api_base}/api/v1/certificate/${props.quizResults.quiz_completion_id}`) + const quiz_certificate_url = computed(() => `${api_base}/api/v1/certificate/quiz/${props.quizResults.quiz_completion_id}`) function windowStateListener() { window.location = import.meta.env.BASE_URL } diff --git a/training-front-end/src/components/__tests__/CertificateTable.spec.js b/training-front-end/src/components/__tests__/CertificateTable.spec.js index 25f29701..ceb31e61 100644 --- a/training-front-end/src/components/__tests__/CertificateTable.spec.js +++ b/training-front-end/src/components/__tests__/CertificateTable.spec.js @@ -69,10 +69,10 @@ describe('CertificateTable', async () => { expect(rows.length).toBe(3) const anchorOne = rows[1].find('form') - expect(anchorOne.attributes('action')).toBe("http://localhost:8000/api/v1/certificate/2") + expect(anchorOne.attributes('action')).toBe("http://localhost:8000/api/v1/certificate/quiz/2") const anchorTwo = rows[2].find('form') - expect(anchorTwo.attributes('action')).toBe("http://localhost:8000/api/v1/certificate/68") + expect(anchorTwo.attributes('action')).toBe("http://localhost:8000/api/v1/certificate/quiz/68") }) it('show correct message when the user has not taken a quiz', async () => { diff --git a/training-front-end/src/components/__tests__/GspcRegistration.spec.js b/training-front-end/src/components/__tests__/GspcRegistration.spec.js index 1c0acb5e..247c1790 100644 --- a/training-front-end/src/components/__tests__/GspcRegistration.spec.js +++ b/training-front-end/src/components/__tests__/GspcRegistration.spec.js @@ -34,7 +34,7 @@ describe('GspcRegistration', () => { setUserCredentials() const wrapper = await mount(GspcRegistration) await flushPromises() - expect(wrapper.text()).toContain("GSA SmartPay® Program Certification (GSPC) Requirements") + expect(wrapper.text()).toContain("GSA SmartPay Program Certification (GSPC) Requirements") }) it('renders USWDSAlert when error is present', async () => { diff --git a/training/api/api_v1/certificates.py b/training/api/api_v1/certificates.py index b8cd26b6..462fcd47 100644 --- a/training/api/api_v1/certificates.py +++ b/training/api/api_v1/certificates.py @@ -24,26 +24,54 @@ def get_certificates_by_userid( return db_user_certificates -@router.post("/certificate/{id}", response_model=UserCertificate) -def get_certificate_by_id( - id: int, - repo: CertificateRepository = Depends(certificate_repository), - certificate: Certificate = Depends(Certificate), - user=Depends(user_from_form) +@router.post("/certificate/{certType}/{id}", response_model=UserCertificate) +def get_certificate_by_type_and_id( + id: int, + certType: str, + certificateRepo: CertificateRepository = Depends(certificate_repository), + certificateService: Certificate = Depends(Certificate), + user=Depends(user_from_form) ): - db_user_certificate = repo.get_certificate_by_id(id) + pdf_bytes = None + filename = '' - if db_user_certificate is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + if (certType == 'quiz'): + db_user_certificate = certificateRepo.get_certificate_by_id(id) - if db_user_certificate.user_id != user["id"]: - raise HTTPException(status_code=401, detail="Not Authorized") + verify_certificate_is_valid(db_user_certificate, user["id"]) + + pdf_bytes = certificateService.generate_pdf( + db_user_certificate.quiz_name, + db_user_certificate.user_name, + db_user_certificate.agency, + db_user_certificate.completion_date + ) + + filename = "SmartPayTraining.pdf" + elif (certType == 'gspc'): + certificate = certificateRepo.get_gspc_certificate_by_id(id) + + verify_certificate_is_valid(certificate, user["id"]) + + pdf_bytes = certificateService.generate_gspc_pdf( + certificate.user_name, + certificate.agency, + certificate.completion_date, + certificate.certification_expiration_date + ) - pdf_bytes = certificate.generate_pdf( - db_user_certificate.quiz_name, - db_user_certificate.user_name, - db_user_certificate.agency, - db_user_certificate.completion_date - ) - headers = {'Content-Disposition': 'attachment; filename="SmartPayTraining.pdf"'} + filename = "GSA SmartPay Program Certification.pdf" + else: + # type not implemented + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + headers = {'Content-Disposition': f'attachment; filename="{filename}"'} return Response(pdf_bytes, headers=headers, media_type='application/pdf') + + +def verify_certificate_is_valid(cert: object, user_id: int): + if cert is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + if cert.user_id != user_id: + raise HTTPException(status_code=401, detail="Not Authorized") diff --git a/training/api/deps.py b/training/api/deps.py index 4933df7d..00dd1548 100644 --- a/training/api/deps.py +++ b/training/api/deps.py @@ -1,6 +1,6 @@ from collections.abc import Generator from fastapi import Depends -from training.repositories import AgencyRepository, UserRepository, QuizRepository, CertificateRepository, GspcInviteRepository +from training.repositories import AgencyRepository, UserRepository, QuizRepository, CertificateRepository, GspcInviteRepository, GspcCompletionRepository from training.services import QuizService, GspcService from training.database import SessionLocal from sqlalchemy.orm import Session @@ -45,5 +45,9 @@ def gspc_invite_repository(db: Session = Depends(db)) -> GspcInviteRepository: return GspcInviteRepository(db) +def gspc_completion_repository(db: Session = Depends(db)) -> GspcCompletionRepository: + return GspcCompletionRepository(db) + + def gspc_service(db: Session = Depends(db)) -> GspcService: return GspcService(db) diff --git a/training/api/email.py b/training/api/email.py index d2c5265a..a6b7cc4e 100644 --- a/training/api/email.py +++ b/training/api/email.py @@ -20,7 +20,7 @@

If you did not submit this request, you may be receiving this message in error. Please disregard this email. If you have any questions or need further assistance, -email us at gsa_smartpay@gsa.gov. +email us at $mailto.

Thank you.

''') @@ -52,6 +52,8 @@ # Todo move email function from quiz.py and turn this into a service so that it can be mocked def send_email(to_email: EmailStr, name: str, link: str, training_title: str) -> None: # Todo clean this up + mailto = "gsa_smartpay@gsa.gov" + if training_title and "certificate" in training_title.lower(): subject = "GSA SmartPay® training certificate(s)" email_subject = "Access your GSA SmartPay training certificate(s)" @@ -61,11 +63,12 @@ def send_email(to_email: EmailStr, name: str, link: str, training_title: str) -> elif training_title and "gspc_registration" in training_title.lower(): subject = "GSA SmartPay® GSPC Registration form" email_subject = "Access to GSA SmartPay GSPC Registration" + mailto = "smartpaygspc@gsa.gov" else: subject = f"GSA SmartPay® {training_title} quiz" email_subject = f"Access GSA SmartPay {training_title} quiz" - body = EMAIL_TEMPLATE.substitute({"name": name, "link": link, "subject": subject}) + body = EMAIL_TEMPLATE.substitute({"name": name, "link": link, "subject": subject, "mailto": mailto}) message = EmailMessage() message.set_content(body, subtype="html") message["Subject"] = email_subject diff --git a/training/models/__init__.py b/training/models/__init__.py index 60fb4d88..a243b3a9 100644 --- a/training/models/__init__.py +++ b/training/models/__init__.py @@ -7,3 +7,4 @@ from .user_x_role import UserXRole from .report_user_x_agency import ReportUserXAgency from .gspc_invite import GspcInvite +from .gspc_completion import GspcCompletion diff --git a/training/models/gspc_completion.py b/training/models/gspc_completion.py new file mode 100644 index 00000000..12bed4ab --- /dev/null +++ b/training/models/gspc_completion.py @@ -0,0 +1,16 @@ +from typing import Any +from datetime import datetime +from training.models import Base +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Column, Date, ForeignKey, func + + +class GspcCompletion(Base): + __tablename__ = "gspc_completions" + + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) + passed: Mapped[bool] = mapped_column() + certification_expiration_date = Column(Date(), nullable=False) + submit_ts: Mapped[datetime] = mapped_column(server_default=func.now()) + responses: Mapped[dict[str, Any]] = mapped_column() diff --git a/training/repositories/__init__.py b/training/repositories/__init__.py index 05d3ef10..ec44ed93 100644 --- a/training/repositories/__init__.py +++ b/training/repositories/__init__.py @@ -5,3 +5,4 @@ from .certificate import CertificateRepository from .role import RoleRepository from .gspc_invite import GspcInviteRepository +from .gspc_completion import GspcCompletionRepository diff --git a/training/repositories/certificate.py b/training/repositories/certificate.py index 12eacde2..594f8dd1 100644 --- a/training/repositories/certificate.py +++ b/training/repositories/certificate.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session from training import models -from training.schemas.user_certificate import UserCertificate +from training.schemas import UserCertificate, GspcCertificate from .base import BaseRepository @@ -33,3 +33,15 @@ def get_certificates_by_userid(self, user_id: int) -> list[UserCertificate]: .join(models.Quiz, models.QuizCompletion.quiz_id == models.Quiz.id) .filter(models.QuizCompletion.passed, models.User.id == user_id).all()) return results + + def get_gspc_certificate_by_id(self, id: int) -> GspcCertificate | None: + + result = (self._session.query(models.GspcCompletion.id.label("id"), models.User.id.label("user_id"), + models.User.name.label("user_name"), models.Agency.name.label("agency"), + models.GspcCompletion.submit_ts.label("completion_date"), + models.GspcCompletion.certification_expiration_date.label("certification_expiration_date")) + .join(models.User, models.GspcCompletion.user_id == models.User.id) + .join(models.Agency, models.User.agency_id == models.Agency.id) + .filter(models.GspcCompletion.passed, models.GspcCompletion.id == id) + .first()) + return result diff --git a/training/repositories/gspc_completion.py b/training/repositories/gspc_completion.py new file mode 100644 index 00000000..fcd39ce9 --- /dev/null +++ b/training/repositories/gspc_completion.py @@ -0,0 +1,17 @@ +from sqlalchemy.orm import Session +from training import models, schemas +from .base import BaseRepository + + +class GspcCompletionRepository(BaseRepository[models.GspcCompletion]): + + def __init__(self, session: Session): + super().__init__(session, models.GspcCompletion) + + def create(self, gspc_completion: schemas.GspcCompletion) -> models.GspcCompletion: + return self.save(models.GspcCompletion( + user_id=gspc_completion.user_id, + passed=gspc_completion.passed, + certification_expiration_date=gspc_completion.certification_expiration_date, + responses=gspc_completion.responses + )) diff --git a/training/schemas/__init__.py b/training/schemas/__init__.py index 6874a8b5..49f1882f 100644 --- a/training/schemas/__init__.py +++ b/training/schemas/__init__.py @@ -1,6 +1,8 @@ from .agency import Agency, AgencyCreate, AgencyWithBureaus from .temp_user import TempUser, IncompleteTempUser, WebDestination from .user import User, UserCreate, UserQuizCompletionReportData, UserSearchResult, UserJWT, UserUpdate +from .gspc_certificate import GspcCertificate +from .gspc_completion import GspcCompletion from .gspc_invite import GspcInvite from .gspc_result import GspcResult from .gspc_submission import GspcSubmission diff --git a/training/schemas/gspc_certificate.py b/training/schemas/gspc_certificate.py new file mode 100644 index 00000000..3a99c66a --- /dev/null +++ b/training/schemas/gspc_certificate.py @@ -0,0 +1,11 @@ +from datetime import datetime +from pydantic import ConfigDict, BaseModel + + +class GspcCertificate(BaseModel): + user_id: int + user_name: str + agency: str + certification_expiration_date: str + completion_date: datetime + model_config = ConfigDict(from_attributes=True) diff --git a/training/schemas/gspc_completion.py b/training/schemas/gspc_completion.py new file mode 100644 index 00000000..10ab39a4 --- /dev/null +++ b/training/schemas/gspc_completion.py @@ -0,0 +1,9 @@ +from typing import Any +from pydantic import BaseModel + + +class GspcCompletion(BaseModel): + user_id: int + passed: bool + certification_expiration_date: str + responses: dict[str, Any] diff --git a/training/schemas/gspc_result.py b/training/schemas/gspc_result.py index 5a43f147..eafc4107 100644 --- a/training/schemas/gspc_result.py +++ b/training/schemas/gspc_result.py @@ -3,3 +3,4 @@ class GspcResult(BaseModel): passed: bool + cert_id: int diff --git a/training/schemas/gspc_submission.py b/training/schemas/gspc_submission.py index 7e90aff4..db9c4663 100644 --- a/training/schemas/gspc_submission.py +++ b/training/schemas/gspc_submission.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict class GspcSubmissionQuestion(BaseModel): @@ -7,8 +7,14 @@ class GspcSubmissionQuestion(BaseModel): response_id: int response: str correct: bool + model_config = ConfigDict(from_attributes=True) + + +class GspcSubmissionQuestions(BaseModel): + responses: list[GspcSubmissionQuestion] class GspcSubmission(BaseModel): expiration_date: str - responses: list[GspcSubmissionQuestion] + responses: GspcSubmissionQuestions + model_config = ConfigDict(from_attributes=True) diff --git a/training/services/certificate.py b/training/services/certificate.py index 082775f7..c6f06f5d 100644 --- a/training/services/certificate.py +++ b/training/services/certificate.py @@ -36,3 +36,27 @@ def generate_pdf(self, training_name, name, agency, date): doc.need_appearances(True) return doc.tobytes(linear=True, deflate_fonts=True, expand=2) + + def generate_gspc_pdf(self, name, agency, date, expiration_date): + date_string = '{dt:%B} {dt.day}, {dt.year}'.format(dt=date) + expiration_date_string = 'Valid Through '+'{dt:%B} {dt.day}, {dt.year}'.format(dt=expiration_date) + data = {'name': name, 'agency': agency, 'date': date_string, 'expiration': expiration_date_string} + pdf = 'c_gspc.pdf' + empty_pdf_path = os.path.join(SCRIPT_DIR, PDF_PATH, pdf) + + doc = fitz.open(empty_pdf_path) # type: ignore + page = doc.load_page(0) + + for field in page.widgets(): + try: + field_name = field.field_name + field.field_value = data[field_name] + # field flag of 1 corresponds to "read-only" + field.field_flags = 1 + field.update() + except KeyError: + # pdf has hidden calculated fields + continue + + doc.need_appearances(True) + return doc.tobytes(linear=True, deflate_fonts=True, expand=2) diff --git a/training/services/gspc.py b/training/services/gspc.py index 5d0047e3..420edaf7 100644 --- a/training/services/gspc.py +++ b/training/services/gspc.py @@ -1,10 +1,36 @@ -from training.schemas import GspcSubmission, GspcResult +import logging +from training.repositories import GspcCompletionRepository, UserRepository, CertificateRepository +from training.schemas import GspcSubmission, GspcResult, GspcCompletion from sqlalchemy.orm import Session +from training.services import Certificate +from string import Template +from email.message import EmailMessage +from smtplib import SMTP +from training.errors import SendEmailError +from training.config import settings + +CERTIFICATE_EMAIL_TEMPLATE = Template(''' +

Hello $name,

+ +

+ Congratulations! +

+

You've successfully met the GSPC experience requirement.

+

Your certificate is attached below.

+

+ If you did not submit this request, you may be receiving this message in error. Please disregard this email. If you have any questions or need further + assistance, email us at smartpaygspc@gsa.com. +

+

Thank you.

+ ''') class GspcService(): def __init__(self, db: Session): - pass + self.gspc_completion_repo = GspcCompletionRepository(db) + self.user_repo = UserRepository(db) + self.certificate_repo = CertificateRepository(db) + self.certificate_service = Certificate() def grade(self, user_id: int, submission: GspcSubmission) -> GspcResult: """ @@ -14,15 +40,62 @@ def grade(self, user_id: int, submission: GspcSubmission) -> GspcResult: :return: GspcResult model which includes the final result """ - passed = all(question.correct for question in submission.responses) + passed = all(question.correct for question in submission.responses.responses) + + responses_dict = submission.responses.model_dump() + result = self.gspc_completion_repo.create(GspcCompletion( + user_id=user_id, + passed=passed, + certification_expiration_date=submission.expiration_date, + responses=responses_dict + )) + + if (passed): + try: + user = self.user_repo.find_by_id(user_id) + pdf_bytes = self.certificate_service.generate_gspc_pdf( + user.name, + user.agency.name, + result.submit_ts, + result.certification_expiration_date + ) - # Todo - # - Save Submission - # - Generate and save cert - # - Send Email on pass + self.email_certificate(user.name, user.email, pdf_bytes) + logging.info(f"Sent confirmation email to {user.email} for passing training quiz") + except Exception as e: + logging.error("Error sending quiz confirmation mail", e) + raise result = GspcResult( passed=passed, + cert_id=result.id ) return result + + def email_certificate(self, user_name: str, to_email: str, certificate: bytes) -> None: + """ + Sends congratulatory email to user with certificate attached. + :param user_name: User's Name + :param to_email: User's email + :param certificate: Certificate PDF file + :return: N/A + """ + body = CERTIFICATE_EMAIL_TEMPLATE.substitute({"name": user_name}) + message = EmailMessage() + message.set_content(body, subtype="html") + message["Subject"] = "Certificate – GSA SmartPay® Program Certificate" + message["From"] = f"{settings.EMAIL_FROM_NAME} <{settings.EMAIL_FROM}>" + message["To"] = to_email + message.add_attachment(certificate, maintype="application", subtype="pdf", filename="GSPC Certificate.pdf") + + with SMTP(settings.SMTP_SERVER, port=settings.SMTP_PORT) as smtp: + smtp.starttls() + if settings.SMTP_USER and settings.SMTP_PASSWORD: + smtp.login(user=settings.SMTP_USER, password=settings.SMTP_PASSWORD) + try: + smtp.send_message(message) + except Exception as e: + raise SendEmailError from e + finally: + smtp.quit() diff --git a/training/services/quiz.py b/training/services/quiz.py index af875d02..bc7f03da 100644 --- a/training/services/quiz.py +++ b/training/services/quiz.py @@ -106,7 +106,7 @@ def grade(self, quiz_id: int, user_id: int, submission: QuizSubmission) -> QuizG grade.quiz_completion_id = result.id if passed: - # Send email with quizz completion attached + # Send email with quiz completion attached try: user = self.user_repo.find_by_id(user_id) db_user_certificate = self.certificate_repo.get_certificate_by_id(result.id) diff --git a/training/tests/test_api_certificates.py b/training/tests/test_api_certificates.py index 881589c9..579b055a 100644 --- a/training/tests/test_api_certificates.py +++ b/training/tests/test_api_certificates.py @@ -8,7 +8,7 @@ from training.api.deps import certificate_repository from training.config import settings from training.main import app -from training.schemas import UserCertificate +from training.schemas import UserCertificate, GspcCertificate from training.services.certificate import Certificate client = TestClient(app) @@ -27,6 +27,18 @@ def user_cert(): } +@pytest.fixture +def gspc_cert(): + return { + 'id': 1, + 'user_id': 2, + 'user_name': "Molly", + 'agency': 'Freeman Journal', + 'completion_date': '2023-08-21T22:59:36', + 'certification_expiration_date': '2024-08-21' + } + + @pytest.fixture def fake_cert_repo(): mock = MagicMock() @@ -63,7 +75,7 @@ def test_get_no_certificates(self, fake_cert_repo, goodJWT): ) assert response.status_code == status.HTTP_404_NOT_FOUND - def test_gets_certificates_by_id(self, fake_cert_repo, goodJWT): + def test_gets_certificates_by_type_and_id(self, fake_cert_repo, goodJWT): fake_cert_repo.get_certificates_by_userid.return_value = None client.get( "/api/v1/certificates", @@ -83,7 +95,7 @@ def test_gets_certificates(self, fake_cert_repo, goodJWT, user_cert): def test_get_specific_certificate_not_found(self, fake_cert_repo, goodJWT): fake_cert_repo.get_certificate_by_id.return_value = None response = client.post( - "/api/v1/certificate/2", + "/api/v1/certificate/quiz/2", data={"jwtToken": goodJWT} ) assert response.status_code == status.HTTP_404_NOT_FOUND @@ -93,19 +105,19 @@ def test_get_specific_certificate_wrong_user(self, fake_cert_repo, goodJWT, user cert = UserCertificate.model_validate(user_cert) fake_cert_repo.get_certificate_by_id.return_value = cert response = client.post( - "/api/v1/certificate/2", + "/api/v1/certificate/quiz/2", data={"jwtToken": goodJWT} ) assert response.status_code == status.HTTP_401_UNAUTHORIZED - def test_get_specific_certificate(self, fake_cert_repo, goodJWT, user_cert, fake_cert_service_repo): + def test_get_specific_quiz_certificate(self, fake_cert_repo, goodJWT, user_cert, fake_cert_service_repo): user_cert['user_id'] = 1 cert = UserCertificate.model_validate(user_cert) fake_cert_repo.get_certificate_by_id.return_value = cert fake_cert_service_repo.generate_pdf.return_value = b'some bytes' response = client.post( - "/api/v1/certificate/2", + "/api/v1/certificate/quiz/2", data={"jwtToken": goodJWT} ) fake_cert_service_repo.generate_pdf.assert_called_once_with( @@ -118,3 +130,50 @@ def test_get_specific_certificate(self, fake_cert_repo, goodJWT, user_cert, fake assert response.headers['content-type'] == 'application/pdf' assert response.headers['content-disposition'] == 'attachment; filename="SmartPayTraining.pdf"' assert response.text == "some bytes" + + def test_gets_certificates_unknown_type(self, fake_cert_repo, goodJWT): + fake_cert_repo.get_certificates_by_userid.return_value = None + response = client.get( + "/api/v1/certificates/new-type", + headers={"Authorization": f"Bearer {goodJWT}"} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_get_specific_gspc_certificate(self, fake_cert_repo, goodJWT, gspc_cert, fake_cert_service_repo): + gspc_cert['user_id'] = 1 + cert = GspcCertificate.model_validate(gspc_cert) + fake_cert_repo.get_gspc_certificate_by_id.return_value = cert + fake_cert_service_repo.generate_gspc_pdf.return_value = b'some bytes' + + response = client.post( + "/api/v1/certificate/gspc/2", + data={"jwtToken": goodJWT} + ) + fake_cert_service_repo.generate_gspc_pdf.assert_called_once_with( + cert.user_name, + cert.agency, + cert.completion_date, + cert.certification_expiration_date + ) + assert response.status_code == status.HTTP_200_OK + assert response.headers['content-type'] == 'application/pdf' + assert response.headers['content-disposition'] == 'attachment; filename="GSA SmartPay Program Certification.pdf"' + assert response.text == "some bytes" + + def test_get_specific_gspc_certificate_not_found(self, fake_cert_repo, goodJWT): + fake_cert_repo.get_gspc_certificate_by_id.return_value = None + response = client.post( + "/api/v1/certificate/gspc/2", + data={"jwtToken": goodJWT} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + fake_cert_repo.get_gspc_certificate_by_id.assert_called_once_with(2) + + def test_get_specific_gspc_certificate_wrong_user(self, fake_cert_repo, goodJWT, gspc_cert): + cert = GspcCertificate.model_validate(gspc_cert) + fake_cert_repo.get_gspc_certificate_by_id.return_value = cert + response = client.post( + "/api/v1/certificate/gspc/2", + data={"jwtToken": goodJWT} + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED