Repository navigation
chore: add tests for backend devcontainers plugin - #74
Conversation
| const { | ||
| searchThrowCallback, | ||
| readUrlThrowCallback, | ||
| tagName = DEFAULT_TAG_NAME, | ||
| } = options ?? {}; |
There was a problem hiding this comment.
Wasn't sure if there was a better way to simulate errors for the reader callbacks
| try { | ||
| const jsonUrl = await this.findDevcontainerJson(rootUrl, entityLogger); | ||
| entityLogger.info('Found devcontainer config', { url: jsonUrl }); | ||
| return this.addTag(entity, this.options.tagName, entityLogger); |
There was a problem hiding this comment.
Main change here was making sure we weren't always passing in DEFAULT_TAG_NAME to this method call
| * Tried to get this working as more of an integration test that used MSW, but | ||
| * couldn't figure out how to bring in the right dependencies in time. So this | ||
| * is more of a unit test for now (which might be all we really need?) |
There was a problem hiding this comment.
Makes sense to me, I like this as a unit test.
| it('Should use Coder prefix in the output', () => { | ||
| const { processor } = setupProcessor(); | ||
| const name = processor.getProcessorName(); | ||
| expect(name).toMatch(new RegExp(`^${PROCESSOR_NAME_PREFIX}`)); |
There was a problem hiding this comment.
Not a blocker or anything, but curious why we do not make this the full name and do a direct equals? Like this, although I guess we could skip exporting the prefix until we have multiple processors:
export const PROCESSOR_NAME_PREFIX = 'backstage-plugin-devcontainers-backend';
export const PROCESSOR_NAME = `${PROCESSOR_NAME_PREFIX}/devcontainers-processor`;
There was a problem hiding this comment.
Additional note that currently we have backstage-plugin-devcontainers-backend//devcontainers-processor (double slash), definitely not a bug or anything but might not be what was intended.
There was a problem hiding this comment.
So, it's a preemptive thing (and maybe I should remove it because we don't need it yet), but it is possible to have multiple instances of the same processor, and have the processor name be partially dynamic
I guess I was trying to add a test case that would be more future-proofed, but that extra slash absolutely wasn't intentional. Will remove
| await Promise.all( | ||
| otherEntityKinds.map(async kind => { | ||
| const inputEntity = { ...baseEntity, kind }; | ||
| const inputSnapshot = structuredClone(inputEntity); |
There was a problem hiding this comment.
Woah how did I not know about structuredClone?? Neat.
There was a problem hiding this comment.
Yeah, it doesn't work for all value types, but it's pretty handy. (It's mainly useful for JSON-serializable values. If you have an object that doesn't make every property enumerable, like any Error object, those properties will get skipped over during the copying)
| expect(inputEntity.metadata.tags).toEqual(inputSnapshot.metadata.tags); | ||
| expect(inputEntity).toEqual(inputSnapshot); |
There was a problem hiding this comment.
Do these test anything? We know these are equal since we created it with structuredClone just above, right? Ohhhh wait is it to test that the processor did not modify the input object?
Separate note, do we need to test metadata.tags separately? toEqual is a recursive check I think.
There was a problem hiding this comment.
Yeah, these are checks to make sure nothing was modified. I originally had comments here, but thought they might be overkill. I'll add them back in, just in case
There was a problem hiding this comment.
And yeah, you're right – the check is fully recursive. Will remove the extra case
There was a problem hiding this comment.
I think normally the comments would be overkill but I was going into the review with the expectation that we just need to check that the tags were added, so I was not thinking about also testing that the object was unmodified. Another way of doing it I think would be two separate tests like:
it("adds tag")
it("does not modify object")
or combined or something like that.
| expect(inputEntity.metadata.tags).toEqual(inputSnapshot.metadata.tags); | ||
| expect(inputEntity).toEqual(inputSnapshot); | ||
|
|
||
| expect(outputEntity.metadata.tags).toContain(DEFAULT_TAG_NAME); |
There was a problem hiding this comment.
Would it make sense to check that none of the other fields have been modified? That might be overstepping the goal of the test, but since we are checking that the input was not modified it might make sense to check that no other fields were modified in the output.
There was a problem hiding this comment.
Yeah, it's a tricky one to decide where the boundaries are. It's not too hard to set up the assertions for the other fields, though, so I can add these in really quick
| expect(inputEntity.metadata.tags).toEqual(inputSnapshot.metadata.tags); | ||
| expect(inputEntity).toEqual(inputSnapshot); | ||
|
|
||
| expect(outputEntity.metadata.tags).toContain(customTag); |
| expect(outputEntity.metadata.tags).toContain(customTag); | ||
| }); | ||
|
|
||
| it('Emits an error entity when reading from the URL throws anything other than a NotFoundError', async () => { |
There was a problem hiding this comment.
I really like these error case tests!
| ) => never; | ||
| }>; | ||
|
|
||
| function setupProcessor(options?: SetupOptions) { |
There was a problem hiding this comment.
Could we make the mock take in files so we can test the various locations? Right now we always return a mock file or always return an error, but if we could do something like:
// these get a tag
setupProcessor({
files: {
".devcontainer.json": "blah",
},
})
setupProcessor({
files: {
".devcontainer/nested/container.json": "blah",
},
})
// this does not get a tag
setupProcessor({
files: {
".devcontainer/nested/too/much/container.json": "blah",
},
})
// and so on
Then we could test all the permutations (anything not specified returns NotFoundError). Although, the second and third examples will require parsing a glob, but I think we can just import the glob library and compare with each file, which will be perfect so we can make sure we are using the glob correctly as well.
There was a problem hiding this comment.
Oh, yeah. Good call. I'm going to try fiddling with this, but when you say "glob library", do you mean the one on NPM?
There was a problem hiding this comment.
Also, just making sure: would files be an object, or an array?
There was a problem hiding this comment.
I think you could do it either way, I went with an object because I think it is slightly simpler on the caller side, but it could be annoying if we need more properties like the file mode or whatever, so array could be more future-proof.
There was a problem hiding this comment.
Also idk but this might be helpful: https://github.com/tschaub/mock-fs
There was a problem hiding this comment.
Which we can maybe combine with https://github.com/isaacs/node-glob
I think it would let us do something like:
mock(files)
and then for search we additionally have:
await glob(theGlob)
or something like that.
There was a problem hiding this comment.
That makes sense. I can read up on globs and how to handle them – my main concern is because this PR has a fix for the main processor functionality, too, I don't know how long that would delay me wrapping things up
Would you be okay with approving this PR, as long as I make a new issue for tetsting the glob functionality?
There was a problem hiding this comment.
Yup, no worries. Hopefully it is not too rough, the glob library should handle it all if you mock the filesystem, but reasonable to do it in another PR I think.
There was a problem hiding this comment.
Cool. Thanks!
Closes #62
Changes made
DevcontainersProcessorNotes