Repository: nautobot/nautobot

Issue:
GraphQL query fails for Interface.mac_address field
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version (Docker tag too if applicable): ca5694aaa671 (v2.0.0b1) 
* Python version:
* Database platform, version:
* Middleware(s):

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Create GraphQL query

```
query ($device_id: ID!) {
        device(id: $device_id) {
          config_context
          hostname: name
          position
          serial
          primary_ip4 {
            id
            primary_ip4_for {
              id
              name
            }
          }
          tenant {
            name
          }
          tags {
            name
            slug
          }
          role {
            name
          }
          platform {
            name
            manufacturer {
              name
            }
            napalm_driver
          }
          location {
            name
            vlans {
              id
              name
              vid
            }
            vlan_groups {
              id
            }
          }
          interfaces {
            description
            mac_address
            enabled
            name
            ip_addresses {
              address
              tags {
                id
              }
            }
            connected_circuit_termination {
              circuit {
                cid
                commit_rate
                provider {
                  name
                }
              }
            }
            tagged_vlans {
              id
            }
            untagged_vlan {
              id
            }
            cable {
              termination_a_type
              status {
                name
              }
              color
            }
            tagged_vlans {
              location {
                name
              }
              id
            }
            tags {
              id
            }
          }
        }
      }
```

3. Execute GraphQL query
4.

<!-- What did you expect to happen? -->
### Expected Behavior

GraphQL for Interface.mac_address works

<!-- What happened instead? -->
### Observed Behavior

GQL fails:

```
{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        0,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        1,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        2,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        3,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        4,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        5,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        6,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        7,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        8,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        9,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        10,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        11,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        12,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        13,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        14,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        15,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        16,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        17,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        18,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        19,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        20,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        21,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        22,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        23,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        24,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        25,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        26,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        27,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        28,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        29,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        30,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        31,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        32,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        33,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        34,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        35,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        36,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        37,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        38,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        39,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        40,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        41,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        42,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        43,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        44,
        "mac_address"
      ]
    },
    {
      "message": "Cannot return null for non-nullable field InterfaceType.mac_address.",
      "locations": [
        {
          "line": 44,
          "column": 13
        }
      ],
      "path": [
        "device",
        "interfaces",
        45,
        "mac_address"
      ]
    }
  ],
  "data": {
    "device": {
      "config_context": {},
      "hostname": "ams01-asw-01",
      "position": 34,
      "serial": "",
      "primary_ip4": null,
      "tenant": {
        "name": "IT Facility EMEA"
      },
      "tags": [],
      "role": {
        "name": "branch_access_switch"
      },
      "platform": {
        "name": "Cisco IOS",
        "manufacturer": {
          "name": "Cisco"
        },
        "napalm_driver": "ios"
      },
      "location": {
        "name": "ams01-server-room",
        "vlans": [],
        "vlan_groups": []
      },
      "interfaces": [
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null
      ]
    }
  }
}
```


Discussion:
Looks like a miss from: https://github.com/nautobot/nautobot/pull/3046

Reproducable in a query as simple as:

```graphql
query {
  devices(limit: 10) {
    hostname: name
    interfaces {
      name
      mac_address
      ip_addresses {
        address
      }
    }
  }
}
```

Resolving patch:
diff --git a/nautobot/dcim/graphql/types.py b/nautobot/dcim/graphql/types.py
--- a/nautobot/dcim/graphql/types.py
+++ b/nautobot/dcim/graphql/types.py
@@ -113,6 +113,10 @@ class Meta:
         filterset_class = InterfaceFilterSet
         exclude = ["_name"]
 
+    # At the DB level, mac_address is null=False, but empty strings are represented as null in the ORM and REST API,
+    # so for consistency, we'll keep that same representation in GraphQL.
+    mac_address = graphene.String(required=False)
+
     # Field Definitions
     cable_peer_circuit_termination = graphene.Field("nautobot.circuits.graphql.types.CircuitTerminationType")
     cable_peer_front_port = graphene.Field("nautobot.dcim.graphql.types.FrontPortType")
diff --git a/nautobot/virtualization/graphql/types.py b/nautobot/virtualization/graphql/types.py
--- a/nautobot/virtualization/graphql/types.py
+++ b/nautobot/virtualization/graphql/types.py
@@ -39,3 +39,7 @@ class Meta:
         model = VMInterface
         filterset_class = VMInterfaceFilterSet
         exclude = ["_name"]
+
+    # At the DB level, mac_address is null=False, but empty strings are represented as null in the ORM and REST API,
+    # so for consistency, we'll keep that same representation in GraphQL.
+    mac_address = graphene.String(required=False)


Test patch:
diff --git a/nautobot/dcim/tests/test_api.py b/nautobot/dcim/tests/test_api.py
--- a/nautobot/dcim/tests/test_api.py
+++ b/nautobot/dcim/tests/test_api.py
@@ -1523,6 +1523,7 @@ def setUpTestData(cls):
                 "bridge": cls.interfaces[3].pk,
                 "tagged_vlans": [cls.vlans[0].pk, cls.vlans[1].pk],
                 "untagged_vlan": cls.vlans[2].pk,
+                "mac_address": None,
             },
             {
                 "device": cls.devices[0].pk,
diff --git a/nautobot/dcim/tests/test_graphql.py b/nautobot/dcim/tests/test_graphql.py
--- a/nautobot/dcim/tests/test_graphql.py
+++ b/nautobot/dcim/tests/test_graphql.py
@@ -3,7 +3,8 @@
 
 from nautobot.core.graphql import execute_query
 from nautobot.core.testing import create_test_user, TestCase
-from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Manufacturer
+from nautobot.dcim.choices import InterfaceTypeChoices
+from nautobot.dcim.models import Device, DeviceType, Interface, Location, LocationType, Manufacturer
 from nautobot.extras.models import DynamicGroup, Role, Status
 
 
@@ -22,13 +23,36 @@ def setUp(self):
             name="Device",
             status=device_status,
         )
+        interface_status = Status.objects.get_for_model(Interface).first()
+        self.interfaces = (
+            Interface(
+                device=self.device,
+                name="eth0",
+                status=interface_status,
+                type=InterfaceTypeChoices.TYPE_VIRTUAL,
+                mac_address="11:22:33:44:55:66",
+            ),
+            Interface(
+                device=self.device,
+                name="eth1",
+                status=interface_status,
+                type=InterfaceTypeChoices.TYPE_VIRTUAL,
+                mac_address=None,
+            ),
+        )
+        for interface in self.interfaces:
+            interface.validated_save()
         self.dynamic_group = DynamicGroup.objects.create(
             name="Dynamic_Group", content_type=ContentType.objects.get_for_model(Device)
         )
 
     @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
     def test_execute_query(self):
-        query = "query {devices {name dynamic_groups {name}}}"
+        query = "query {devices {name interfaces {name mac_address} dynamic_groups {name}}}"
         resp = execute_query(query, user=self.user).to_dict()
         self.assertFalse(resp["data"].get("error"))
+        self.assertEqual(
+            resp["data"]["devices"][0]["interfaces"][0]["mac_address"], str(self.interfaces[0].mac_address)
+        )
+        self.assertIsNone(resp["data"]["devices"][0]["interfaces"][1]["mac_address"])
         self.assertEqual(resp["data"]["devices"][0]["dynamic_groups"][0]["name"], self.dynamic_group.name)