Repository: smithy-lang/smithy
Pull request #1930 (2023-08-16 19:18:55)

Update getAuthSchemes javadoc
Updates javadoc for ServiceIndex::getAuthSchemes to say the returned order is alphabetical. Tests were updated in [#1915](https://github.com/smithy-lang/smithy/pull/1915)to assert this ordering for ServiceIndex::getEffectiveAuthSchemes, but it also made the same update for the ServiceIndex::getAuthSchemes test. The PR didn't include a javadoc update, so the purpose of this PR is to fix that.

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/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
new file mode 100644
index 000000000..c590ca2a1
--- /dev/null
+++ b/.github/workflows/stale-issues.yml
@@ -0,0 +1,43 @@
+name: "Close stale issues"
+
+# Controls when the action will run.
+on:
+  schedule:
+    - cron: "0 0 * * *"
+
+jobs:
+  cleanup:
+    runs-on: ubuntu-latest
+    name: Stale issue job
+    steps:
+      - uses: aws-actions/stale-issue-cleanup@v3
+        with:
+          # Targeted types
+          issue-types: issues
+
+          # Issue timing
+          days-before-stale: 60
+          days-before-close: 14
+          days-before-ancient: 365
+
+          # Labels
+          stale-issue-label: closing-soon
+          exempt-issue-label: bug,documentation,feature-request,investigating,protocol-test
+          response-requested-label: response-requested
+
+          # Messages
+          stale-issue-message: This issue has not received a response in a while. If you still think there is a problem, please leave a comment to avoid the issue from automatically closing.
+          ancient-issue-message: We’re closing this issue because it has been open a long time and hasn’t been updated in a while and may not be getting the attention it deserves. We encourage you to check if this is still an issue in the latest release and if you find that this is still a problem, please feel free to comment or open a new issue.
+
+          # If you don't want to mark a issue as being ancient based on a
+          # threshold of "upvotes", you can set this here. An "upvote" is
+          # the total number of +1, heart, hooray, and rocket reactions
+          # on an issue.
+          minimum-upvotes-to-exempt: 5
+
+          # Don't set closed-for-staleness label to skip closing very old issues
+          # regardless of label
+          closed-for-staleness-label: closed-for-staleness
+
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          loglevel: DEBUG
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..7b7935df1 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.
      *
@@ -105,6 +127,8 @@ public final class ServiceIndex implements KnowledgeIndex {
      * <p>An <em>auth defining trait</em> is a trait that is marked with
      * the {@code smithy.api#authDefinition} trait.
      *
+     * <p>The returned map is ordered alphabetically by absolute shape ID.
+     *
      * <p>An empty map is returned if {@code id} cannot be found in the
      * model or is not a service shape.
      *
@@ -127,7 +151,7 @@ public final class ServiceIndex implements KnowledgeIndex {
      *
      * <p>The returned map is provided in the same order as the values in the
      * {@code auth} trait if an auth trait is present, otherwise the result
-     * returned is ordered alphabetically by absolute shape id.
+     * returned is ordered alphabetically by absolute shape ID.
      *
      * <p>An empty map is returned if {@code service} cannot be found in the
      * model or is not a service shape.
@@ -154,6 +178,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.
@@ -167,7 +215,7 @@ public final class ServiceIndex implements KnowledgeIndex {
      *
      * <p>The returned map is provided in the same order as the values in the
      * {@code auth} trait if an auth trait is present, otherwise the result
-     * returned is ordered alphabetically by absolute shape id.
+     * returned is ordered alphabetically by absolute shape ID.
      *
      * <p>An empty map is returned if {@code service} shape cannot be found
      * in the model or is not a service shape. An empty map is returned if
@@ -198,7 +246,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 {}