Sitelet https://github.com/coder/backstage-plugins/pull/74
Skip to content

chore: add tests for backend devcontainers plugin - #74

Merged
buenos-nachos merged 31 commits into
mainfrom
mes/devcontainers-be-tests
Mar 22, 2024
Merged

buenos-nachos merged 31 commits into
mainfrom
mes/devcontainers-be-tests

Conversation

@buenos-nachos

@buenos-nachos buenos-nachos commented Mar 20, 2024 •

Copy link
Copy Markdown
Contributor

Closes #62

Changes made

  • Added test file for DevcontainersProcessor
  • Fixed a bug where we weren't actually using the custom user-defined devcontainers tag name

Notes

  • This was the first time I've ever written a backend test, so please rip this PR apart

@buenos-nachos buenos-nachos self-assigned this Mar 20, 2024
Comment thread plugins/backstage-plugin-devcontainers-backend/src/index.ts
Comment on lines +55 to +59
const {
searchThrowCallback,
readUrlThrowCallback,
tagName = DEFAULT_TAG_NAME,
} = options ?? {};

@buenos-nachos buenos-nachos Mar 20, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main change here was making sure we weren't always passing in DEFAULT_TAG_NAME to this method call

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!!

Comment on lines +62 to +64
* 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?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}`));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`;

@code-asher code-asher Mar 21, 2024 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woah how did I not know about structuredClone?? Neat.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know, thank you!

Comment on lines +182 to +183
expect(inputEntity.metadata.tags).toEqual(inputSnapshot.metadata.tags);
expect(inputEntity).toEqual(inputSnapshot);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@buenos-nachos buenos-nachos Mar 22, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yeah, you're right – the check is fully recursive. Will remove the extra case

@code-asher code-asher Mar 22, 2024 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +203 to +206
expect(inputEntity.metadata.tags).toEqual(inputSnapshot.metadata.tags);
expect(inputEntity).toEqual(inputSnapshot);

expect(outputEntity.metadata.tags).toContain(customTag);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as above.

expect(outputEntity.metadata.tags).toContain(customTag);
});

it('Emits an error entity when reading from the URL throws anything other than a NotFoundError', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like these error case tests!

) => never;
}>;

function setupProcessor(options?: SetupOptions) {

@code-asher code-asher Mar 21, 2024 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just making sure: would files be an object, or an array?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@code-asher code-asher Mar 22, 2024 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also idk but this might be helpful: https://github.com/tschaub/mock-fs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Thanks!

@buenos-nachos
buenos-nachos merged commit 07da627 into main Mar 22, 2024
@buenos-nachos
buenos-nachos deleted the mes/devcontainers-be-tests branch March 22, 2024 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tests for backend devcontainers plugin

2 participants