Fix withDefault type helper to work with boolean

`withDefault` is a type helper that allows to create a type that
defaults to a default value when trying to decode a nullish value.
That type was not correctly working with boolean types, causing `false`
values to be replaced by true. This would specifically cause issues when
parsing the target state, where a `running: false` in a service would
become a `running: true` due to the type decoding.

Change-type: patch
This commit is contained in:
Felipe Lalanne 2022-09-13 16:40:51 +00:00
parent efa400c5a6
commit 3e45e9561e
3 changed files with 89 additions and 27 deletions

View File

@ -123,17 +123,16 @@ export type DeviceState = {
};
// Return a type with a default value
const withDefault = <T extends t.Any>(
export const withDefault = <T extends t.Any>(
type: T,
defaultValue: t.TypeOf<T>,
): t.Type<t.TypeOf<T>> =>
new t.Type(
type.name,
type.is,
(v, c) => type.validate(!!v ? v : defaultValue, c),
(v, c) => type.validate(v != null ? v : defaultValue, c),
type.encode,
);
/**
* Utility function to return a io-ts type from a native typescript
* type.

View File

@ -286,33 +286,54 @@ describe('validation', () => {
'accepts apps with no services',
).to.be.true;
expect(
isRight(
TargetApps.decode({
abcd: {
id: 1234,
name: 'something',
releases: {
bar: {
id: 123,
services: {
bazbaz: {
id: 45,
image_id: 34,
image: 'foo',
environment: { MY_SERVICE_ENV_VAR: '123' },
labels: { 'io.balena.features.supervisor-api': 'true' },
},
},
volumes: {},
networks: {},
const target = TargetApps.decode({
abcd: {
id: 1234,
name: 'something',
releases: {
bar: {
id: 123,
services: {
bazbaz: {
id: 45,
image_id: 34,
image: 'foo',
environment: { MY_SERVICE_ENV_VAR: '123' },
labels: { 'io.balena.features.supervisor-api': 'true' },
running: false,
},
},
volumes: {},
networks: {},
},
}),
),
'accepts apps with a service',
).to.be.true;
},
},
});
expect(isRight(target), 'accepts apps with a service').to.be.true;
expect((target as any).right).to.deep.equal({
abcd: {
id: 1234,
name: 'something',
class: 'fleet',
releases: {
bar: {
id: 123,
services: {
bazbaz: {
id: 45,
image_id: 34,
image: 'foo',
environment: { MY_SERVICE_ENV_VAR: '123' },
labels: { 'io.balena.features.supervisor-api': 'true' },
running: false,
},
},
volumes: {},
networks: {},
},
},
},
});
});
it('rejects app with invalid environment', () => {

42
test/unit/types.spec.ts Normal file
View File

@ -0,0 +1,42 @@
import { expect } from 'chai';
import * as t from 'io-ts';
import { withDefault } from '~/src/types';
describe('types', () => {
describe('withDefault', () => {
it('decodes object values', () => {
expect(withDefault(t.record(t.string, t.string), {}).decode(undefined))
.to.have.property('right')
.that.deep.equals({});
expect(withDefault(t.record(t.string, t.string), {}).decode(null))
.to.have.property('right')
.that.deep.equals({});
expect(withDefault(t.record(t.string, t.string), {}).decode({}))
.to.have.property('right')
.that.deep.equals({});
expect(
withDefault(t.record(t.string, t.string), {}).decode({ dummy: 'yes' }),
)
.to.have.property('right')
.that.deep.equals({ dummy: 'yes' });
});
it('decodes boolean values', () => {
expect(withDefault(t.boolean, true).decode(undefined))
.to.have.property('right')
.that.equals(true);
expect(withDefault(t.boolean, true).decode(null))
.to.have.property('right')
.that.equals(true);
expect(withDefault(t.boolean, true).decode(false))
.to.have.property('right')
.that.equals(false);
});
});
});