diff --git a/api/db_migrations/versions/4b61e9319ad9_create_a11y_violations_table.py b/api/db_migrations/versions/4b61e9319ad9_create_a11y_violations_table.py new file mode 100644 index 00000000..7658f393 --- /dev/null +++ b/api/db_migrations/versions/4b61e9319ad9_create_a11y_violations_table.py @@ -0,0 +1,42 @@ +"""create a11y_violations table + +Revision ID: 4b61e9319ad9 +Revises: e251ec3b0f77 +Create Date: 2021-08-24 21:30:09.916966 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "4b61e9319ad9" +down_revision = "e251ec3b0f77" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "a11y_violations", + sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column("a11y_report_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("violation", sa.String(), nullable=False), + sa.Column("impact", sa.String(), nullable=False), + sa.Column("target", sa.Text()), + sa.Column("html", sa.Text()), + sa.Column("data", postgresql.JSONB(), nullable=False), + sa.Column("tags", postgresql.JSONB(), nullable=False), + sa.Column("message", sa.Text()), + sa.Column("url", sa.String()), + sa.Column("created_at", sa.DateTime, default=sa.func.utc_timestamp()), + sa.Column("updated_at", sa.DateTime, onupdate=sa.func.utc_timestamp()), + sa.ForeignKeyConstraint( + ["a11y_report_id"], + ["a11y_reports.id"], + ), + ) + + +def downgrade(): + op.drop_table("a11y_violations") diff --git a/api/models/A11yReport.py b/api/models/A11yReport.py index da482a53..0203b52e 100644 --- a/api/models/A11yReport.py +++ b/api/models/A11yReport.py @@ -37,6 +37,8 @@ class A11yReport(Base): ) scan = relationship("Scan", back_populates="a11y_reports") + a11y_violations = relationship("A11yViolation") + @validates("product") def validate_product(self, _key, value): assert value != "" diff --git a/api/models/A11yViolation.py b/api/models/A11yViolation.py new file mode 100644 index 00000000..bb5f6a5c --- /dev/null +++ b/api/models/A11yViolation.py @@ -0,0 +1,61 @@ +import datetime +import uuid + +from sqlalchemy import DateTime, Column, ForeignKey, String, Text +from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy.orm import relationship, validates + +from models import Base +from models.A11yReport import A11yReport + + +class A11yViolation(Base): + __tablename__ = "a11y_violations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + violation = Column(String, nullable=False) + impact = Column(String, nullable=False) + target = Column(Text) + html = Column(Text) + data = Column(JSONB, nullable=False) + tags = Column(JSONB, nullable=False) + message = Column(Text) + url = Column(String) + created_at = Column( + DateTime, + index=False, + unique=False, + nullable=False, + default=datetime.datetime.utcnow, + ) + updated_at = Column( + DateTime, + index=False, + unique=False, + nullable=True, + onupdate=datetime.datetime.utcnow, + ) + a11y_report_id = Column( + UUID(as_uuid=True), ForeignKey(A11yReport.id), index=True, nullable=False + ) + a11y_report = relationship("A11yReport", back_populates="a11y_violations") + + @validates("violation") + def validate_violation(self, _key, value): + assert value != "" + return value + + @validates("impact") + def validate_impact(self, _key, value): + assert value != "" + return value + + @validates("data") + def validate_data(self, _key, value): + assert value != "" + return value + + @validates("tags") + def validate_tags(self, _key, value): + assert value != "" + return value diff --git a/api/tests/conftest.py b/api/tests/conftest.py index c2c53375..ee4e73f7 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -4,6 +4,7 @@ from alembic.config import Config from alembic import command +from models.A11yReport import A11yReport from models.Organisation import Organisation from models.Scan import Scan from models.ScanType import ScanType @@ -15,6 +16,20 @@ from sqlalchemy.orm import sessionmaker +@pytest.fixture(scope="session") +def a11y_report_fixture(session, scan_fixture): + a11y_report = A11yReport( + product="product", + revision="revision", + url="url", + summary={"jsonb": "data"}, + scan=scan_fixture, + ) + session.add(a11y_report) + session.commit() + return a11y_report + + @pytest.fixture def assert_new_model_saved(): def f(model): diff --git a/api/tests/models/test_A11yViolation.py b/api/tests/models/test_A11yViolation.py new file mode 100644 index 00000000..d16f62c2 --- /dev/null +++ b/api/tests/models/test_A11yViolation.py @@ -0,0 +1,154 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + + +from models.A11yViolation import A11yViolation + + +def test_a11y_violation_belongs_to_an_a11y_report(a11y_report_fixture, session): + a11y_violation = A11yViolation( + violation="violation", + impact="impact", + target="target", + html="html", + data={"jsonb": "data"}, + tags={"jsonb": "tags"}, + message="message", + url="url", + a11y_report=a11y_report_fixture, + ) + session.add(a11y_violation) + session.commit() + assert a11y_report_fixture.a11y_violations[-1].id == a11y_violation.id + session.delete(a11y_violation) + session.commit() + + +def test_a11y_violation_model(a11y_report_fixture): + a11y_violation = A11yViolation( + violation="violation", + impact="impact", + target="target", + html="html", + data={"jsonb": "data"}, + tags={"jsonb": "tags"}, + message="message", + url="url", + a11y_report=a11y_report_fixture, + ) + assert a11y_violation.violation == "violation" + assert a11y_violation.impact == "impact" + assert a11y_violation.target == "target" + assert a11y_violation.html == "html" + assert a11y_violation.data == {"jsonb": "data"} + assert a11y_violation.tags == {"jsonb": "tags"} + assert a11y_violation.message == "message" + assert a11y_violation.url == "url" + assert a11y_violation.a11y_report is not None + + +def test_a11y_violation_model_saved( + assert_new_model_saved, a11y_report_fixture, session +): + a11y_violation = A11yViolation( + violation="violation", + impact="impact", + target="target", + html="html", + data={"jsonb": "data"}, + tags={"jsonb": "tags"}, + message="message", + url="url", + a11y_report=a11y_report_fixture, + ) + session.add(a11y_violation) + session.commit() + assert a11y_violation.violation == "violation" + assert a11y_violation.impact == "impact" + assert a11y_violation.target == "target" + assert a11y_violation.html == "html" + assert_new_model_saved(a11y_violation) + assert a11y_violation.data == {"jsonb": "data"} + assert a11y_violation.tags == {"jsonb": "tags"} + assert a11y_violation.message == "message" + assert a11y_violation.url == "url" + session.delete(a11y_violation) + session.commit() + + +def test_a11y_violation_empty_violation_fails(a11y_report_fixture, session): + a11y_violation = A11yViolation( + impact="impact", + target="target", + html="html", + data={"jsonb": "data"}, + tags={"jsonb": "tags"}, + message="message", + url="url", + a11y_report=a11y_report_fixture, + ) + session.add(a11y_violation) + with pytest.raises(IntegrityError): + session.commit() + session.rollback() + + +def test_a11y_violation_empty_impact_fails(a11y_report_fixture, session): + a11y_violation = A11yViolation( + violation="violation", + data={"jsonb": "data"}, + tags={"jsonb": "tags"}, + message="message", + url="url", + a11y_report=a11y_report_fixture, + ) + session.add(a11y_violation) + with pytest.raises(IntegrityError): + session.commit() + session.rollback() + + +def test_a11y_violation_empty_data_fails(a11y_report_fixture, session): + a11y_violation = A11yViolation( + violation="violation", + impact="impact", + target="target", + tags={"jsonb": "tags"}, + a11y_report=a11y_report_fixture, + ) + session.add(a11y_violation) + with pytest.raises(IntegrityError): + session.commit() + session.rollback() + + +def test_a11y_violation_empty_tags_fails(a11y_report_fixture, session): + a11y_violation = A11yViolation( + violation="violation", + impact="impact", + target="target", + data={"jsonb": "data"}, + a11y_report=a11y_report_fixture, + ) + session.add(a11y_violation) + with pytest.raises(IntegrityError): + session.commit() + session.rollback() + + +def test_a11y_violation_empty_a11y_report_fails(session): + a11y_violation = A11yViolation( + violation="violation", + impact="impact", + target="target", + html="html", + data={"jsonb": "data"}, + tags={"jsonb": "tags"}, + message="message", + url="url", + ) + session.add(a11y_violation) + with pytest.raises(IntegrityError): + session.commit() + session.rollback()