|
2 | 2 | asFed2SubgraphDocument, |
3 | 3 | assert, |
4 | 4 | buildSubgraph, |
| 5 | + DEFAULT_SUPPORTED_SUPERGRAPH_FEATURES, |
5 | 6 | defaultPrintOptions, |
6 | 7 | FEDERATION2_LINK_WITH_FULL_IMPORTS, |
7 | 8 | inaccessibleIdentity, |
@@ -4008,4 +4009,223 @@ describe('composition', () => { |
4008 | 4009 | assertCompositionSuccess(result); |
4009 | 4010 | }); |
4010 | 4011 | }); |
| 4012 | + |
| 4013 | + describe('@authenticated', () => { |
| 4014 | + // We need to override the default supported features to include the |
| 4015 | + // @authenticated feature, since it's not part of the default supported |
| 4016 | + // features. |
| 4017 | + const supportedFeatures = new Set([ |
| 4018 | + ...DEFAULT_SUPPORTED_SUPERGRAPH_FEATURES, |
| 4019 | + 'https://specs.apollo.dev/authenticated/v0.1', |
| 4020 | + ]); |
| 4021 | + |
| 4022 | + it('comprehensive locations', () => { |
| 4023 | + const onObject = { |
| 4024 | + typeDefs: gql` |
| 4025 | + type Query { |
| 4026 | + object: AuthenticatedObject! |
| 4027 | + } |
| 4028 | +
|
| 4029 | + type AuthenticatedObject @authenticated { |
| 4030 | + field: Int! |
| 4031 | + } |
| 4032 | + `, |
| 4033 | + name: 'on-object', |
| 4034 | + }; |
| 4035 | + |
| 4036 | + const onInterface = { |
| 4037 | + typeDefs: gql` |
| 4038 | + type Query { |
| 4039 | + interface: AuthenticatedInterface! |
| 4040 | + } |
| 4041 | +
|
| 4042 | + interface AuthenticatedInterface @authenticated { |
| 4043 | + field: Int! |
| 4044 | + } |
| 4045 | + `, |
| 4046 | + name: 'on-interface', |
| 4047 | + }; |
| 4048 | + |
| 4049 | + const onInterfaceObject = { |
| 4050 | + typeDefs: gql` |
| 4051 | + type AuthenticatedInterfaceObject |
| 4052 | + @interfaceObject |
| 4053 | + @key(fields: "id") |
| 4054 | + @authenticated |
| 4055 | + { |
| 4056 | + id: String! |
| 4057 | + } |
| 4058 | + `, |
| 4059 | + name: 'on-interface-object', |
| 4060 | + } |
| 4061 | + |
| 4062 | + const onScalar = { |
| 4063 | + typeDefs: gql` |
| 4064 | + scalar AuthenticatedScalar @authenticated |
| 4065 | +
|
| 4066 | + # This needs to exist in at least one other subgraph from where it's defined |
| 4067 | + # as an @interfaceObject (so arbitrarily adding it here). We don't actually |
| 4068 | + # apply @authenticated to this one since we want to see it propagate even |
| 4069 | + # when it's not applied in all locations. |
| 4070 | + interface AuthenticatedInterfaceObject @key(fields: "id") { |
| 4071 | + id: String! |
| 4072 | + } |
| 4073 | + `, |
| 4074 | + name: 'on-scalar', |
| 4075 | + }; |
| 4076 | + |
| 4077 | + const onEnum = { |
| 4078 | + typeDefs: gql` |
| 4079 | + enum AuthenticatedEnum @authenticated { |
| 4080 | + A |
| 4081 | + B |
| 4082 | + } |
| 4083 | + `, |
| 4084 | + name: 'on-enum', |
| 4085 | + }; |
| 4086 | + |
| 4087 | + const onRootField = { |
| 4088 | + typeDefs: gql` |
| 4089 | + type Query { |
| 4090 | + authenticatedRootField: Int! @authenticated |
| 4091 | + } |
| 4092 | + `, |
| 4093 | + name: 'on-root-field', |
| 4094 | + }; |
| 4095 | + |
| 4096 | + const onObjectField = { |
| 4097 | + typeDefs: gql` |
| 4098 | + type Query { |
| 4099 | + objectWithField: ObjectWithAuthenticatedField! |
| 4100 | + } |
| 4101 | +
|
| 4102 | + type ObjectWithAuthenticatedField { |
| 4103 | + field: Int! @authenticated |
| 4104 | + } |
| 4105 | + `, |
| 4106 | + name: 'on-object-field', |
| 4107 | + }; |
| 4108 | + |
| 4109 | + const onEntityField = { |
| 4110 | + typeDefs: gql` |
| 4111 | + type Query { |
| 4112 | + entityWithField: EntityWithAuthenticatedField! |
| 4113 | + } |
| 4114 | +
|
| 4115 | + type EntityWithAuthenticatedField @key(fields: "id") { |
| 4116 | + id: ID! |
| 4117 | + field: Int! @authenticated |
| 4118 | + } |
| 4119 | + `, |
| 4120 | + name: 'on-entity-field', |
| 4121 | + }; |
| 4122 | + |
| 4123 | + const result = composeAsFed2Subgraphs([ |
| 4124 | + onObject, |
| 4125 | + onInterface, |
| 4126 | + onInterfaceObject, |
| 4127 | + onScalar, |
| 4128 | + onEnum, |
| 4129 | + onRootField, |
| 4130 | + onObjectField, |
| 4131 | + onEntityField, |
| 4132 | + ], { supportedFeatures }); |
| 4133 | + assertCompositionSuccess(result); |
| 4134 | + |
| 4135 | + const authenticatedElements = [ |
| 4136 | + "AuthenticatedObject", |
| 4137 | + "AuthenticatedInterface", |
| 4138 | + "AuthenticatedInterfaceObject", |
| 4139 | + "AuthenticatedScalar", |
| 4140 | + "AuthenticatedEnum", |
| 4141 | + "Query.authenticatedRootField", |
| 4142 | + "ObjectWithAuthenticatedField.field", |
| 4143 | + "EntityWithAuthenticatedField.field", |
| 4144 | + ]; |
| 4145 | + |
| 4146 | + for (const element of authenticatedElements) { |
| 4147 | + expect( |
| 4148 | + result.schema |
| 4149 | + .elementByCoordinate(element) |
| 4150 | + ?.hasAppliedDirective("authenticated") |
| 4151 | + ).toBeTruthy(); |
| 4152 | + } |
| 4153 | + }); |
| 4154 | + |
| 4155 | + it('applies @authenticated on types as long as it is used once', () => { |
| 4156 | + const a1 = { |
| 4157 | + typeDefs: gql` |
| 4158 | + type Query { |
| 4159 | + a: A |
| 4160 | + } |
| 4161 | + type A @key(fields: "id") @authenticated { |
| 4162 | + id: String! |
| 4163 | + a1: String |
| 4164 | + } |
| 4165 | + `, |
| 4166 | + name: 'a1', |
| 4167 | + }; |
| 4168 | + const a2 = { |
| 4169 | + typeDefs: gql` |
| 4170 | + type A @key(fields: "id") { |
| 4171 | + id: String! |
| 4172 | + a2: String |
| 4173 | + } |
| 4174 | + `, |
| 4175 | + name: 'a2', |
| 4176 | + }; |
| 4177 | + |
| 4178 | + // checking composition in either order (not sure if this is necessary but |
| 4179 | + // it's not hurting anything) |
| 4180 | + const result1 = composeAsFed2Subgraphs([a1, a2], { supportedFeatures }); |
| 4181 | + const result2 = composeAsFed2Subgraphs([a2, a1], { supportedFeatures }); |
| 4182 | + assertCompositionSuccess(result1); |
| 4183 | + assertCompositionSuccess(result2); |
| 4184 | + |
| 4185 | + expect(result1.schema.type('A')?.hasAppliedDirective('authenticated')).toBeTruthy(); |
| 4186 | + expect(result2.schema.type('A')?.hasAppliedDirective('authenticated')).toBeTruthy(); |
| 4187 | + }); |
| 4188 | + |
| 4189 | + it('validation error on incompatible directive definition', () => { |
| 4190 | + const invalidDefinition = { |
| 4191 | + typeDefs: gql` |
| 4192 | + directive @authenticated on ENUM_VALUE |
| 4193 | +
|
| 4194 | + type Query { |
| 4195 | + a: Int |
| 4196 | + } |
| 4197 | +
|
| 4198 | + enum E { |
| 4199 | + A @authenticated |
| 4200 | + } |
| 4201 | + `, |
| 4202 | + name: 'invalidDefinition', |
| 4203 | + }; |
| 4204 | + const result = composeAsFed2Subgraphs([invalidDefinition], { supportedFeatures }); |
| 4205 | + expect(errors(result)[0]).toEqual([ |
| 4206 | + "DIRECTIVE_DEFINITION_INVALID", |
| 4207 | + "[invalidDefinition] Invalid definition for directive \"@authenticated\": \"@authenticated\" should have locations FIELD_DEFINITION, OBJECT, INTERFACE, SCALAR, ENUM, but found (non-subset) ENUM_VALUE", |
| 4208 | + ]); |
| 4209 | + }); |
| 4210 | + |
| 4211 | + it('validation error on invalid application', () => { |
| 4212 | + const invalidApplication = { |
| 4213 | + typeDefs: gql` |
| 4214 | + type Query { |
| 4215 | + a: Int |
| 4216 | + } |
| 4217 | +
|
| 4218 | + enum E { |
| 4219 | + A @authenticated |
| 4220 | + } |
| 4221 | + `, |
| 4222 | + name: 'invalidApplication', |
| 4223 | + }; |
| 4224 | + const result = composeAsFed2Subgraphs([invalidApplication], { supportedFeatures }); |
| 4225 | + expect(errors(result)[0]).toEqual([ |
| 4226 | + "INVALID_GRAPHQL", |
| 4227 | + "[invalidApplication] Directive \"@authenticated\" may not be used on ENUM_VALUE.", |
| 4228 | + ]); |
| 4229 | + }); |
| 4230 | + }); |
4011 | 4231 | }); |
0 commit comments