Repository: smithy-lang/smithy
Pull request #1924 (2023-08-15 07:38:57)

Add ServiceIndex method to account for smithy.api#noAuth
Add a new `getNoAuthAwareEffectiveAuthSchemes` method to ServiceIndex that wraps `getEffectiveAuthSchemes` to account for `smithy.api#noAuth` scheme.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java
index 0c32dc009..b458719ed 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java
@@ -29,8 +29,11 @@ import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.shapes.ToShapeId;
 import software.amazon.smithy.model.traits.AuthDefinitionTrait;
 import software.amazon.smithy.model.traits.AuthTrait;
+import software.amazon.smithy.model.traits.OptionalAuthTrait;
 import software.amazon.smithy.model.traits.ProtocolDefinitionTrait;
 import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
+import software.amazon.smithy.utils.MapUtils;
 
 /**
  * An index that resolves service protocols and auth schemes.
@@ -63,6 +66,25 @@ public final class ServiceIndex implements KnowledgeIndex {
         return model.getKnowledge(ServiceIndex.class, ServiceIndex::new);
     }
 
+     /**
+     * Defines the type of auth schemes returned by {@link #getEffectiveAuthSchemes}.
+     */
+    public enum AuthSchemeMode {
+
+        /**
+         * Use only the modeled auth schemes. This is the default.
+         */
+        MODELED,
+
+        /**
+         * Use the modeled auth schemes, as well as the synthetic {@link NoAuthTrait} where applicable.
+         *
+         * <p>The Smithy Reference Architecture recommends using the {@code smithy.api#noAuth} auth scheme to represent
+         * no authentication which is available as the {@link NoAuthTrait}.
+         */
+        NO_AUTH_AWARE;
+    }
+
     /**
      * Get all protocol traits attached to a service.
      *
@@ -154,6 +176,30 @@ public final class ServiceIndex implements KnowledgeIndex {
                 .orElse(Collections.emptyMap());
     }
 
+    /**
+     * Gets a list of effective authentication schemes applied to a service, based on the AuthSchemeMode.
+     *
+     * <p>If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as
+     * {@link #getEffectiveAuthSchemes(ToShapeId)}.
+     *
+     * <p>If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, except that if the service has no effective
+     * auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth} auth scheme. It avoids having to
+     * special case handling an empty result. The returned map will always contain at least 1 entry.
+     *
+     * @param service Service to get the effective authentication schemes of.
+     * @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include.
+     * @return Returns a map of the trait shape ID to the auth trait itself.
+     */
+    public Map<ShapeId, Trait> getEffectiveAuthSchemes(ToShapeId service, AuthSchemeMode authSchemeMode) {
+        Map<ShapeId, Trait> authSchemes = getEffectiveAuthSchemes(service);
+        if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) {
+            if (authSchemes.isEmpty()) {
+                authSchemes = MapUtils.of(NoAuthTrait.ID, new NoAuthTrait());
+            }
+        }
+        return authSchemes;
+    }
+
     /**
      * Gets a list of effective authentication schemes applied to an operation
      * bound within a service.
@@ -198,7 +244,49 @@ public final class ServiceIndex implements KnowledgeIndex {
                 .orElse(Collections.emptyMap());
     }
 
-    private Map<ShapeId, Trait> getAuthTraitValues(Shape service, Shape subject) {
+    /**
+     * Gets a list of effective authentication schemes applied to an operation
+     * bound within a service, based on the AuthSchemeMode.
+     *
+     * <p>If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as
+     * {@link #getEffectiveAuthSchemes(ToShapeId, ToShapeId)}.
+     *
+     * <p>If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, with the following differences:
+     * If the operation has no effective auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth}
+     * auth scheme.
+     * If the operation has the {@code smithy.api#optionalAuth} trait, it adds {@code smithy.api#noAuth} to the end.
+     *
+     * <p>Using {@code NO_AUTH_AWARE} accounts for {@code smithy.api#optionalAuth} and avoids having to special case
+     * handling an empty result. The returned map will always contain at least 1 entry.
+     *
+     * <p>The {@code smithy.api#noAuth} scheme, if present, is always the last scheme.
+     *
+     * @param service Service the operation is within.
+     * @param operation Operation to get the effective authentication schemes of.
+     * @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include.
+     * @return Returns a map of the trait shape ID to the auth trait itself.
+     */
+    public Map<ShapeId, Trait> getEffectiveAuthSchemes(ToShapeId service,
+                                                       ToShapeId operation,
+                                                       AuthSchemeMode authSchemeMode) {
+        Map<ShapeId, Trait> authSchemes = getEffectiveAuthSchemes(service, operation);
+        if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) {
+            if (authSchemes.isEmpty() || hasOptionalAuth(operation)) {
+                authSchemes = new LinkedHashMap<>(authSchemes);
+                authSchemes.put(NoAuthTrait.ID, new NoAuthTrait());
+            }
+        }
+        return authSchemes;
+    }
+
+    private boolean hasOptionalAuth(ToShapeId operation) {
+        return getModel()
+                .getShape(operation.toShapeId())
+                .filter(shape -> shape.hasTrait(OptionalAuthTrait.class))
+                .isPresent();
+    }
+
+    private static Map<ShapeId, Trait> getAuthTraitValues(Shape service, Shape subject) {
         if (!subject.hasTrait(AuthTrait.class)) {
             return null;
         }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java
new file mode 100644
index 000000000..d49a472e4
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.smithy.model.traits.synthetic;
+
+import software.amazon.smithy.model.knowledge.ServiceIndex;
+import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.AnnotationTrait;
+
+/**
+ * An auth scheme trait for {@code smithy.api#noAuth} which indicates no authentication. This is not a real trait
+ * in the semantic model, but a valid auth scheme for use in {@link ServiceIndex#getEffectiveAuthSchemes} with
+ * {@link AuthSchemeMode#NO_AUTH_AWARE}.
+ */
+public final class NoAuthTrait extends AnnotationTrait {
+
+    public static final ShapeId ID = ShapeId.from("smithy.api#noAuth");
+
+    public NoAuthTrait() {
+        super(ID, Node.objectNode());
+    }
+
+    @Override
+    public boolean isSynthetic() {
+        return true;
+    }
+}


Test patch:
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java
index e7e899a6f..e58285e69 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java
@@ -16,12 +16,13 @@
 package software.amazon.smithy.model.knowledge;
 
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.equalTo;
+import static software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode.NO_AUTH_AWARE;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import org.junit.jupiter.api.AfterAll;
@@ -33,9 +34,12 @@ import software.amazon.smithy.model.traits.HttpBasicAuthTrait;
 import software.amazon.smithy.model.traits.HttpBearerAuthTrait;
 import software.amazon.smithy.model.traits.HttpDigestAuthTrait;
 import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
 
 public class ServiceIndexTest {
 
+    private static final ShapeId CUSTOM_AUTH_ID = ShapeId.from("smithy.example#customAuth");
+
     private static Model model;
 
     @BeforeAll
@@ -52,7 +56,7 @@ public class ServiceIndexTest {
     }
 
     @Test
-    public void returnsProtocolsOfService() {
+    public void protocolsOfService() {
         Model model = Model.assembler()
                 .addImport(getClass().getResource("service-index-loads-protocols.smithy"))
                 .assemble()
@@ -66,88 +70,193 @@ public class ServiceIndexTest {
     }
 
     @Test
-    public void returnsAuthSchemesOfService() {
+    public void authSchemesOfService() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
         Map<ShapeId, Trait> auth = serviceIndex.getAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithNoAuthTrait"));
+                ShapeId.from("smithy.example#ServiceWithoutAuthTrait"));
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+    }
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(3));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
-        assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
+    @Test
+    public void authSchemesOfServiceWithoutAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
     }
 
     @Test
-    public void getsAuthSchemesOfServiceWithNoAuthTrait() {
+    public void authSchemesOfServiceWithAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithNoAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(3));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
-        assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
     }
 
     @Test
-    public void getsAuthSchemesOfServiceWithAuthTrait() {
+    public void authSchemesOfServiceWithEmptyAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(auth.keySet(), hasSize(2));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
     }
 
     @Test
-    public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithNoAuthTrait() {
+    public void authSchemesOfServiceWithoutAuthDefinitionTraits() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithNoAuthTrait"),
-                ShapeId.from("smithy.example#OperationWithNoAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthDefinitionTraits");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(3));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
-        assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
     }
 
     @Test
-    public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithAuthTrait() {
+    public void authSchemesOfOperationWithoutAuthTraitAndServiceWithoutAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithAuthTrait"),
-                ShapeId.from("smithy.example#OperationWithNoAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(2));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
     }
 
     @Test
-    public void getsAuthSchemesOfOperationWithAuthTrait() {
+    public void authSchemesOfOperationWithoutAuthTraitAndServiceWithAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfOperationWithoutAuthTraitAndServiceWithEmptyAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfOperationWithAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
         Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
                 ShapeId.from("smithy.example#ServiceWithAuthTrait"),
                 ShapeId.from("smithy.example#OperationWithAuthTrait"));
+        assertAuthSchemes(auth, HttpDigestAuthTrait.ID);
+    }
 
-        assertThat(auth.keySet(), hasSize(1));
-        assertThat(auth, hasKey(HttpDigestAuthTrait.ID));
+    @Test
+    public void authSchemesOfOperationWithEmptyAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithEmptyAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
     }
 
     @Test
-    public void returnsAnEmptyCollectionWhenTheServiceDoesNotExist() {
+    public void authSchemesOfOperationWithOptionalAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#Invalid"),
-                ShapeId.from("smithy.example#OperationWithAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithOptionalAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID, NoAuthTrait.ID);
+    }
+
+    // Test to assert that smithy.api#noAuth trait is not part of traits that are sorted alphabetically, but last.
+    // The authSchemesOfOperationWithOptionalAuthTrait() test above doesn't really assert that, because
+    // smithy.api#noAuth would have been last if included in sorting.
+    @Test
+    public void authSchemesOfOperationWithOptionalAuthTraitAndServiceWithoutAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithOptionalAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID,
+                NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfInvalidService() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#Invalid");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth);
 
-        assertThat(auth.keySet(), empty());
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfInvalidServiceWithInvalidOperation() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#Invalid");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfServiceWithInvalidOperation() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#InvalidOperation");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    private void assertAuthSchemes(Map<ShapeId, Trait> auth, ShapeId... authSchemes) {
+        List<ShapeId> ids = new ArrayList<>(auth.keySet());
+        assertThat(ids, hasSize(authSchemes.length));
+        assertThat(ids, equalTo(Arrays.asList(authSchemes)));
     }
 }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy
index 95d5d6c8a..1a2ccb168 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy
@@ -2,30 +2,77 @@ $version: "2"
 
 namespace smithy.example
 
+@customAuth
 @httpBasicAuth
 @httpDigestAuth
 @httpBearerAuth
-service ServiceWithNoAuthTrait {
-    version: "2020-01-29",
+service ServiceWithoutAuthTrait {
+    version: "2020-01-29"
     operations: [
-        OperationWithNoAuthTrait,
+        OperationWithoutAuthTrait
         OperationWithAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
     ]
 }
 
+@customAuth
 @httpBasicAuth
 @httpDigestAuth
 @httpBearerAuth
 @auth([httpBasicAuth, httpDigestAuth])
 service ServiceWithAuthTrait {
-    version: "2020-01-29",
+    version: "2020-01-29"
     operations: [
-        OperationWithNoAuthTrait,
+        OperationWithoutAuthTrait
         OperationWithAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
     ]
 }
 
-operation OperationWithNoAuthTrait {}
+@customAuth
+@httpBasicAuth
+@httpDigestAuth
+@httpBearerAuth
+@auth([])
+service ServiceWithEmptyAuthTrait {
+    version: "2020-01-29"
+    operations: [
+        OperationWithoutAuthTrait
+        OperationWithAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
+    ]
+}
+
+service ServiceWithoutAuthDefinitionTraits {
+    version: "2020-01-29"
+    operations: [
+        OperationWithoutAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
+    ]
+}
+
+operation OperationWithoutAuthTrait {}
 
 @auth([httpDigestAuth])
 operation OperationWithAuthTrait {}
+
+@auth([])
+operation OperationWithEmptyAuthTrait {}
+
+@optionalAuth
+operation OperationWithOptionalAuthTrait {}
+
+// Defining a custom trait, to assert that alphabetical sorting of traits takes namespace into account, as well as
+// smithy.api#noAuth is added to the end, and not included in sorting.
+@trait(
+    selector: "service"
+    breakingChanges: [
+        {change: "remove"}
+    ]
+)
+@authDefinition
+structure customAuth {}