Skip to content

Commit b8dcd1f

Browse files
committed
feat(core)!: New behaviour of using contacts like snapshots - the new setting changedContracts changes the behaviour when contracts are changed. Either 'FAIL' for fail when a contract is changed, or 'OVERWRITE' for overwriting when a contract is changed. Default is to FAIL, so this is a breaking change.
1 parent bab2fc5 commit b8dcd1f

38 files changed

Lines changed: 3273 additions & 110 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# How to add a new configuration option
2+
3+
Follow this guide when you want to add a new configuration option to configure the core behaviour of ContractCase.
4+
5+
## Design
6+
7+
ContractCase can take configuration options that change the core behaviour of the matching engine, test runner, contract writing, and broker configuration (etc).
8+
9+
### Ultimately, configuration is context
10+
11+
The configuration itself isn't passed around, instead it becomes properties on
12+
the context object (RunContext) that tells ContractCase how it is running.
13+
14+
This flattening into one object allows ContractCase to be agnostic to how it was
15+
configured, and consistent about precedence of configuration methods.
16+
17+
Note that configuration should be in the `_case:currentRun:context:` namespace and not the `_case:context:` namespace. This is because `_case:context:` properties are able to be overwritten by matchers - which generally isn't desirable for configuration.
18+
19+
### Context should generally be facts
20+
21+
Ideally, it's best if ContractCase can check one value in the configuration to determine how one setting should behave, instead of needing to check multiple different values.
22+
23+
This means that you may want to add a composite property to the context instead of a property that's the same as the configuration setting.
24+
25+
### Defaults should be explicit, and applied before the context is created
26+
27+
Don't spread defaults throughout the code.
28+
29+
### Note: The configuration options described here aren't for interaction behaviour
30+
31+
If you're wanting to configure a particular interaction type, then instead refer
32+
to the interaction plugin definitions (eg MockConfig).
33+
34+
## Step 1: Add to Core
35+
36+
The core needs to know your configuration property
37+
38+
1. Add your property to the core's `CaseConfig` (usually `BaseCaseConfig`).
39+
If your property is required for ContractCase to operate, then make it required here (even if users aren't expected to specify it)
40+
2. Add the option to the `RunContext` type. This is made of a few different sub-contexts, for use by parts of the ecosystem that don't have access to the full config object. It may be appropriate to add it to one of the sub-contexts instead of directly. Note that you might want to add a composite property here instead.
41+
3. Ensure the value is mapped between `CaseConfig` and `RunContext` appropriately. For most configuration properties, the method `configToRunContext` is sufficient and doesn't need to be modified.
42+
4. If your config has default values, add them to DEFAULT_CONFIG / dependencies.ts as appropriate
43+
44+
## Step 2: Add to the CaseConnector packages
45+
46+
1. Add it to `ContractCaseBoundaryConfig`. This should only contain primitive types, so that different connector implementations are possible. Keep this defined in the same order as the CaseConfig object. This object should be documented, and should specify what the default is.
47+
2. Add it to the `convertConfig` function. This function should validate the values can be assigned to the typescript CaseConfig, but do no other validation. It may also normalise if the result is unambiguous (eg, you can interpret `someProperty` as `SOME_PROPERTY`)
48+
3. Add it to the `.proto` version of the config object in `case-connector-proto`, then run `npm run build:proto` and `npm run lint:fix:proto=` in that package.
49+
4. Add it to `ContractCaseConnectorConfig`, using the same rules as the boundary config. TODO: Replace the boundary config with the connector config so that we only have to do one mapping here.
50+
5. Add it to `mapAllConfigFields` in the grpc connector
51+
52+
## Step 3: Add it to the DSL packages
53+
54+
1. For JS, this is `ContractCaseConfig` and the associated mapper to `ContractCaseBoundaryConfig`
55+
2. For Java, this is `ContractCaseConfig`, `IndividualFailedTestConfig`, `IndividualSuccessTestConfig`, `ContractCaseConnectorConfig`, and their associated builders. You will also need to add mappers in `ConnectorConfigMapper` and `ConnectorOutgoingMapper`.

docs/maintainers/todo.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ Next:
4343
- [ ] Do a configuration error message like the crash message, so that misconfigurations don't result in stack traces
4444
- [ ] Ensure original stack traces are maintained across boundaries and implement `originalStack` function.
4545
- [ ] Add way to say 'I need this field and I don't care what's in it'
46+
- [ ] Replace direct access of context in the core with selector methods, so that the logic is consistent and will read more fluently
47+
- [ ] Validate config - some settings can come as arbitrary strings (eg from env), and we could be more helpful if there are errors
48+
- [ ] Allow both `CASE_foo` and `CASE_FOO` environment variables (but fail if they're both set to different things).
49+
- [ ] Make sure that downloaded contracts don't write to `main`, and don't collide with local contracts.
4650

4751
Document
4852

packages/case-connector-proto/proto/contract_case_stream.proto

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ message ContractCaseConfig {
3737
map<string, string> mock_config = 16;
3838

3939
google.protobuf.StringValue auto_version_from = 17;
40+
41+
google.protobuf.StringValue changed_contracts = 18;
4042
}
4143

4244
// Indicates a successful response with no payload
43-
message ResultSuccess {
44-
}
45+
message ResultSuccess {}
4546
message ResultSuccessHasMapPayload {
4647
// Always a map of Strings -> Strings
4748
google.protobuf.Struct map = 1;
@@ -77,9 +78,7 @@ message StateHandlerHandle {
7778
}
7879

7980
// A reference to a trigger function that can be invoked
80-
message TriggerFunctionHandle {
81-
google.protobuf.StringValue handle = 1;
82-
}
81+
message TriggerFunctionHandle { google.protobuf.StringValue handle = 1; }
8382

8483
// Requests from client / host
8584

@@ -105,14 +104,11 @@ message RunRejectingInteractionRequest {
105104

106105
// From host to core, instructs the core to strip the matchers from a given
107106
// matcher / data object
108-
message StripMatchersRequest {
109-
google.protobuf.Struct matcher_or_data = 2;
110-
}
107+
message StripMatchersRequest { google.protobuf.Struct matcher_or_data = 2; }
111108

112109
// From host to core, instructs the core to finish the current contract
113110
// definition
114-
message EndDefinitionRequest {
115-
}
111+
message EndDefinitionRequest {}
116112

117113
// From host to core, instructs the core to load a plugin
118114
message LoadPluginRequest {
@@ -124,9 +120,7 @@ message LoadPluginRequest {
124120
// Responses from server
125121

126122
// From Core to Host, instructs the host to run a given state handler
127-
message RunStateHandlerRequest {
128-
StateHandlerHandle state_handler_handle = 1;
129-
}
123+
message RunStateHandlerRequest { StateHandlerHandle state_handler_handle = 1; }
130124

131125
// From Core to Host, requests the host invoke one of the user-provided
132126
// triggers.
@@ -139,9 +133,7 @@ message TriggerFunctionRequest {
139133
SetupInfo setup = 3;
140134
}
141135

142-
message CoreFunctionHandle {
143-
google.protobuf.StringValue handle = 1;
144-
}
136+
message CoreFunctionHandle { google.protobuf.StringValue handle = 1; }
145137

146138
// Describes the setup of the currently executing Interaction
147139
message SetupInfo {
@@ -288,9 +280,7 @@ message PrintTestTitleRequest {
288280
//
289281
// Most messages must be acknowledged with a response - this indicates the
290282
// return of the previous message.
291-
message ResultResponse {
292-
BoundaryResult result = 1;
293-
}
283+
message ResultResponse { BoundaryResult result = 1; }
294284

295285
// From the Host to the Core, requests that a verification session begins
296286
message BeginVerificationRequest {
@@ -301,8 +291,7 @@ message BeginVerificationRequest {
301291
}
302292

303293
// From the Host to the Core, requests a list of the available contracts
304-
message AvailableContractDefinitions {
305-
}
294+
message AvailableContractDefinitions {}
306295

307296
// From the Host to the Core, instructs the core that the current verification
308297
// session is now configured, and asks it to execute the verification.

packages/case-connector-proto/src/grpc/proto/contract_case_stream_pb.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ export class ContractCaseConfig extends jspb.Message {
118118
value?: google_protobuf_wrappers_pb.StringValue,
119119
): ContractCaseConfig;
120120

121+
hasChangedContracts(): boolean;
122+
clearChangedContracts(): void;
123+
getChangedContracts(): google_protobuf_wrappers_pb.StringValue | undefined;
124+
setChangedContracts(
125+
value?: google_protobuf_wrappers_pb.StringValue,
126+
): ContractCaseConfig;
127+
121128
serializeBinary(): Uint8Array;
122129
toObject(includeInstance?: boolean): ContractCaseConfig.AsObject;
123130
static toObject(
@@ -160,6 +167,7 @@ export namespace ContractCaseConfig {
160167

161168
mockConfigMap: Array<[string, string]>;
162169
autoVersionFrom?: google_protobuf_wrappers_pb.StringValue.AsObject;
170+
changedContracts?: google_protobuf_wrappers_pb.StringValue.AsObject;
163171
};
164172

165173
export class UsernamePassword extends jspb.Message {

packages/case-connector-proto/src/grpc/proto/contract_case_stream_pb.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,8 @@ proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.toObject = functi
834834
triggerAndTest: (f = msg.getTriggerAndTest()) && proto.io.contract_testing.contractcase.grpc.TriggerFunctionHandle.toObject(includeInstance, f),
835835
baseUrlUnderTest: (f = msg.getBaseUrlUnderTest()) && google_protobuf_wrappers_pb.StringValue.toObject(includeInstance, f),
836836
mockConfigMap: (f = msg.getMockConfigMap()) ? f.toObject(includeInstance, undefined) : [],
837-
autoVersionFrom: (f = msg.getAutoVersionFrom()) && google_protobuf_wrappers_pb.StringValue.toObject(includeInstance, f)
837+
autoVersionFrom: (f = msg.getAutoVersionFrom()) && google_protobuf_wrappers_pb.StringValue.toObject(includeInstance, f),
838+
changedContracts: (f = msg.getChangedContracts()) && google_protobuf_wrappers_pb.StringValue.toObject(includeInstance, f)
838839
};
839840

840841
if (includeInstance) {
@@ -958,6 +959,11 @@ proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.deserializeBinary
958959
reader.readMessage(value,google_protobuf_wrappers_pb.StringValue.deserializeBinaryFromReader);
959960
msg.setAutoVersionFrom(value);
960961
break;
962+
case 18:
963+
var value = new google_protobuf_wrappers_pb.StringValue;
964+
reader.readMessage(value,google_protobuf_wrappers_pb.StringValue.deserializeBinaryFromReader);
965+
msg.setChangedContracts(value);
966+
break;
961967
default:
962968
reader.skipField();
963969
break;
@@ -1115,6 +1121,14 @@ proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.serializeBinaryTo
11151121
google_protobuf_wrappers_pb.StringValue.serializeBinaryToWriter
11161122
);
11171123
}
1124+
f = message.getChangedContracts();
1125+
if (f != null) {
1126+
writer.writeMessage(
1127+
18,
1128+
f,
1129+
google_protobuf_wrappers_pb.StringValue.serializeBinaryToWriter
1130+
);
1131+
}
11181132
};
11191133

11201134

@@ -1920,6 +1934,43 @@ proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.prototype.hasAuto
19201934
};
19211935

19221936

1937+
/**
1938+
* optional google.protobuf.StringValue changed_contracts = 18;
1939+
* @return {?proto.google.protobuf.StringValue}
1940+
*/
1941+
proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.prototype.getChangedContracts = function() {
1942+
return /** @type{?proto.google.protobuf.StringValue} */ (
1943+
jspb.Message.getWrapperField(this, google_protobuf_wrappers_pb.StringValue, 18));
1944+
};
1945+
1946+
1947+
/**
1948+
* @param {?proto.google.protobuf.StringValue|undefined} value
1949+
* @return {!proto.io.contract_testing.contractcase.grpc.ContractCaseConfig} returns this
1950+
*/
1951+
proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.prototype.setChangedContracts = function(value) {
1952+
return jspb.Message.setWrapperField(this, 18, value);
1953+
};
1954+
1955+
1956+
/**
1957+
* Clears the message field making it undefined.
1958+
* @return {!proto.io.contract_testing.contractcase.grpc.ContractCaseConfig} returns this
1959+
*/
1960+
proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.prototype.clearChangedContracts = function() {
1961+
return this.setChangedContracts(undefined);
1962+
};
1963+
1964+
1965+
/**
1966+
* Returns whether this field is set.
1967+
* @return {boolean}
1968+
*/
1969+
proto.io.contract_testing.contractcase.grpc.ContractCaseConfig.prototype.hasChangedContracts = function() {
1970+
return jspb.Message.getField(this, 18) != null;
1971+
};
1972+
1973+
19231974

19241975

19251976

packages/case-connector/src/connectors/case-boundary/internals/boundary/config.types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export interface UserNamePassword {
2020
/**
2121
* Configure a ContractCase run. See the [configuration documentation](https://case.contract-testing.io/docs/reference/configuring) for more details.
2222
*
23-
* Note that many of these types are more permissive than the reality - for
23+
* Implementation note: many of these types are more permissive than the reality - for
2424
* example, the constrained string types are listed here as `string`, whereas
2525
* the core will only accept a limited number of values (eg, log level accepts
2626
* `'warn'` `'error'` etc, but not `'gibbons'`). Callers may rely on the
@@ -70,6 +70,17 @@ export interface ContractCaseBoundaryConfig {
7070
*/
7171
readonly contractFilename?: string;
7272

73+
/**
74+
* What to do if contracts have changed?
75+
*
76+
* - `"OVERWRITE"`: Replace the previous contract file
77+
* - `"FAIL"`: Fail if attempting to write a contract that's different
78+
* to the previous one
79+
*
80+
* Default: 'FAIL'
81+
*/
82+
changedContracts?: string;
83+
7384
/**
7485
* Unique ID for this segment of the test run - it must be unique within a
7586
* run, but need not be unique between test runs. This is an internal

packages/case-connector/src/connectors/case-boundary/internals/mappers/config.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ const mapAutoVersionFrom = (autoVersionFrom: string): 'TAG' | 'GIT_SHA' => {
5757
}
5858
};
5959

60+
const mapChangedContracts = (
61+
changedContracts: string,
62+
): 'FAIL' | 'OVERWRITE' => {
63+
switch (changedContracts.toUpperCase()) {
64+
case 'FAIL': {
65+
return 'FAIL';
66+
}
67+
case 'OVERWRITE':
68+
return 'OVERWRITE';
69+
default:
70+
throw new CaseConfigurationError(
71+
`The changedContracts setting '${changedContracts}' is not a valid changed contracts setting`,
72+
);
73+
}
74+
};
75+
6076
/**
6177
* SeparateConfig only exists because at one point these two things were separate.
6278
* At some point, we should refactor this so that the config shape doesn't need to
@@ -67,20 +83,37 @@ type SeparateConfig = {
6783
partialInvoker: Partial<TestInvoker<AnyMockDescriptorType, unknown>>;
6884
};
6985

86+
/**
87+
* Converts between the boundary config (which is less restricted) and a
88+
* validated form of CaseConfig.
89+
*
90+
* Here, validated means "conforms to the typescript definition of CaseConfig".
91+
*
92+
* Mappers should throw `CaseConfigurationError` if the values in the boundary config
93+
* can't be assigned to the CaseConfig.
94+
*
95+
* @internal
96+
* @param param0 - A boundary config object
97+
* @returns a validated case config object
98+
*/
7099
export const convertConfig = ({
71100
stateHandlers,
72101
triggerAndTest,
73102
triggerAndTests,
74103
logLevel,
75104
publish,
76105
autoVersionFrom,
106+
changedContracts,
77107
...incoming
78108
}: ContractCaseBoundaryConfig): SeparateConfig => ({
79109
config: {
80110
...incoming,
81111
...(autoVersionFrom
82112
? { autoVersionFrom: mapAutoVersionFrom(autoVersionFrom) }
83113
: {}),
114+
...(changedContracts
115+
? { changedContracts: mapChangedContracts(changedContracts) }
116+
: {}),
84117
...(logLevel ? { logLevel: mapLogLevel(logLevel) } : {}),
85118
...(publish ? { publish: mapPublish(publish) } : {}),
86119
// baseUrlUnderTest: `http://localhost:${8084}`,

packages/case-connector/src/connectors/grpc/requestMappers/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ const mapAllConfigFields = (
135135
logLevel: unboxOrUndefined(config.getLogLevel()),
136136
contractDir: unboxOrUndefined(config.getContractDir()),
137137
contractFilename: unboxOrUndefined(config.getContractFilename()),
138+
changedContracts: unboxOrUndefined(config.getChangedContracts()),
138139

139140
publish: unboxOrUndefined(config.getPublish()),
140141
brokerCiAccessToken: unboxOrUndefined(config.getBrokerCiAccessToken()),

packages/case-connector/src/domain/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ export type ConnectorStateHandler = BoundaryStateHandler;
66

77
export type ConnectorTriggerFunction = ITriggerFunction;
88

9+
// TODO: This should be consolidated with the boundary
10+
// config types so that there's only one mapping layer here
11+
912
export type ContractCaseConnectorConfig = {
1013
providerName: string;
1114
consumerName: string;
1215
logLevel: string;
1316
contractDir: string;
1417
contractFilename: string;
18+
changedContracts: string;
1519

1620
publish: string;
1721
brokerCiAccessToken: string;

packages/case-core/case-contracts/http-request-provider/http-request-consumer-6f7f2f5f08aec0763e309e6964d15fe086826f547cc697d76123cd7236a5ff3c.case.json renamed to packages/case-core/case-contracts/http-request-provider/http-request-consumer-eb8ffa58fc4f81bb59dd79334bea64875e0fc33d5fc2d7a61156e2132ca5b151.case.json

File renamed without changes.

0 commit comments

Comments
 (0)