diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 24943e11..49d7219c 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,5 +13,4 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:609822e3c09b7a1bd90b99655904609f162cc15acb4704f1edf778284c36f429 -# created: 2024-10-01T19:34:30.797530443Z + digest: sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9d616d55..ef8b87f4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,30 @@ -Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: -- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/nodejs-logging-winston/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +> Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: + +## Description + +> Please provide a detailed description for the change. +> As much as possible, please try to keep changes separate by purpose. For example, try not to make a one-line bug fix in a feature request, or add an irrelevant README change to a bug fix. + +## Impact + +> What's the impact of this change? + +## Testing + +> Have you added unit and integration tests if necessary? +> Were any tests changed? Are any breaking changes necessary? + +## Additional Information + +> Any additional details that we should be aware of? + +## Checklist + +- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/nodejs-logging-winston/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass -- [ ] Code coverage does not decrease (if any source code was changed) -- [ ] Appropriate docs were updated (if necessary) +- [ ] Code coverage does not decrease +- [ ] Appropriate docs were updated +- [ ] Appropriate comments were added, particularly in complex areas or places that require background +- [ ] No new warnings or issues will be generated from this change -Fixes # 🦕 +Fixes #issue_number_goes_here 🦕 diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml index d4ca9418..1d3e8419 100644 --- a/.github/release-trigger.yml +++ b/.github/release-trigger.yml @@ -1 +1,2 @@ enabled: true +multiScmName: nodejs-logging-winston \ No newline at end of file diff --git a/.github/scripts/close-invalid-link.cjs b/.github/scripts/close-invalid-link.cjs index d7a3688e..fdb51488 100644 --- a/.github/scripts/close-invalid-link.cjs +++ b/.github/scripts/close-invalid-link.cjs @@ -12,21 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. +const fs = require('fs'); +const yaml = require('js-yaml'); +const path = require('path'); +const TEMPLATE_FILE_PATH = path.resolve(__dirname, '../ISSUE_TEMPLATE/bug_report.yml') + async function closeIssue(github, owner, repo, number) { await github.rest.issues.createComment({ owner: owner, repo: repo, issue_number: number, - body: 'Issue was opened with an invalid reproduction link. Please make sure the repository is a valid, publicly-accessible github repository, and make sure the url is complete (example: https://github.com/googleapis/google-cloud-node)' + body: "Issue was opened with an invalid reproduction link. Please make sure the repository is a valid, publicly-accessible github repository, and make sure the url is complete (example: https://github.com/googleapis/google-cloud-node)" }); await github.rest.issues.update({ owner: owner, repo: repo, issue_number: number, - state: 'closed' + state: "closed" }); } -module.exports = async ({github, context}) => { +module.exports = async ({ github, context }) => { const owner = context.repo.owner; const repo = context.repo.repo; const number = context.issue.number; @@ -37,20 +42,32 @@ module.exports = async ({github, context}) => { issue_number: number, }); - const isBugTemplate = issue.data.body.includes('Link to the code that reproduces this issue'); + const yamlData = fs.readFileSync(TEMPLATE_FILE_PATH, 'utf8'); + const obj = yaml.load(yamlData); + const linkMatchingText = (obj.body.find(x => {return x.type === 'input' && x.validations.required === true && x.attributes.label.includes('link')})).attributes.label; + const isBugTemplate = issue.data.body.includes(linkMatchingText); if (isBugTemplate) { console.log(`Issue ${number} is a bug template`) try { - const link = issue.data.body.split('\n')[18].match(/(https?:\/\/(gist\.)?github.com\/.*)/)[0]; - console.log(`Issue ${number} contains this link: ${link}`) - const isValidLink = (await fetch(link)).ok; - console.log(`Issue ${number} has a ${isValidLink ? 'valid' : 'invalid'} link`) - if (!isValidLink) { - await closeIssue(github, owner, repo, number); - } + const text = issue.data.body; + const match = text.indexOf(linkMatchingText); + if (match !== -1) { + const nextLineIndex = text.indexOf('http', match); + if (nextLineIndex == -1) { + await closeIssue(github, owner, repo, number); + return; + } + const link = text.substring(nextLineIndex, text.indexOf('\n', nextLineIndex)); + console.log(`Issue ${number} contains this link: ${link}`); + const isValidLink = (await fetch(link)).ok; + console.log(`Issue ${number} has a ${isValidLink ? "valid" : "invalid"} link`) + if (!isValidLink) { + await closeIssue(github, owner, repo, number); + } + } } catch (err) { await closeIssue(github, owner, repo, number); } } -}; +}; \ No newline at end of file diff --git a/.github/scripts/close-unresponsive.cjs b/.github/scripts/close-unresponsive.cjs index 142dc126..6f81b508 100644 --- a/.github/scripts/close-unresponsive.cjs +++ b/.github/scripts/close-unresponsive.cjs @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +/// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,57 +13,57 @@ // limitations under the License. function labeledEvent(data) { - return data.event === 'labeled' && data.label.name === 'needs more info'; - } - - const numberOfDaysLimit = 15; - const close_message = `This has been closed since a request for information has \ - not been answered for ${numberOfDaysLimit} days. It can be reopened when the \ - requested information is provided.`; - - module.exports = async ({github, context}) => { - const owner = context.repo.owner; - const repo = context.repo.repo; - - const issues = await github.rest.issues.listForRepo({ - owner: owner, - repo: repo, - labels: 'needs more info', - }); - const numbers = issues.data.map((e) => e.number); - - for (const number of numbers) { - const events = await github.paginate( - github.rest.issues.listEventsForTimeline, - { - owner: owner, - repo: repo, - issue_number: number, - }, - (response) => response.data.filter(labeledEvent) - ); - - const latest_response_label = events[events.length - 1]; - - const created_at = new Date(latest_response_label.created_at); - const now = new Date(); - const diff = now - created_at; - const diffDays = diff / (1000 * 60 * 60 * 24); - - if (diffDays > numberOfDaysLimit) { - await github.rest.issues.update({ - owner: owner, - repo: repo, - issue_number: number, - state: 'closed', - }); - - await github.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: number, - body: close_message, - }); - } + return data.event === "labeled" && data.label.name === "needs more info"; +} + +const numberOfDaysLimit = 15; +const close_message = `This has been closed since a request for information has \ +not been answered for ${numberOfDaysLimit} days. It can be reopened when the \ +requested information is provided.`; + +module.exports = async ({ github, context }) => { + const owner = context.repo.owner; + const repo = context.repo.repo; + + const issues = await github.rest.issues.listForRepo({ + owner: owner, + repo: repo, + labels: "needs more info", + }); + const numbers = issues.data.map((e) => e.number); + + for (const number of numbers) { + const events = await github.paginate( + github.rest.issues.listEventsForTimeline, + { + owner: owner, + repo: repo, + issue_number: number, + }, + (response) => response.data.filter(labeledEvent) + ); + + const latest_response_label = events[events.length - 1]; + + const created_at = new Date(latest_response_label.created_at); + const now = new Date(); + const diff = now - created_at; + const diffDays = diff / (1000 * 60 * 60 * 24); + + if (diffDays > numberOfDaysLimit) { + await github.rest.issues.update({ + owner: owner, + repo: repo, + issue_number: number, + state: "closed", + }); + + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: number, + body: close_message, + }); } - }; + } +}; \ No newline at end of file diff --git a/.github/scripts/fixtures/invalidIssueBody.txt b/.github/scripts/fixtures/invalidIssueBody.txt new file mode 100644 index 00000000..504bd669 --- /dev/null +++ b/.github/scripts/fixtures/invalidIssueBody.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### Link to the code that reproduces this issue. A link to a **public** Github Repository or gist with a minimal reproduction. + +not-a-link + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/fixtures/validIssueBody.txt b/.github/scripts/fixtures/validIssueBody.txt new file mode 100644 index 00000000..6e0ace33 --- /dev/null +++ b/.github/scripts/fixtures/validIssueBody.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### Link to the code that reproduces this issue. A link to a **public** Github Repository or gist with a minimal reproduction. + +https://gist.github.com/orgads/13cbf44c91923da27d8772b5f10489c9 + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt b/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt new file mode 100644 index 00000000..984a420e --- /dev/null +++ b/.github/scripts/fixtures/validIssueBodyDifferentLinkLocation.txt @@ -0,0 +1,50 @@ +### Please make sure you have searched for information in the following guides. + +- [X] Search the issues already opened: https://github.com/GoogleCloudPlatform/google-cloud-node/issues +- [X] Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+node.js +- [X] Check our Troubleshooting guide: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/troubleshooting +- [X] Check our FAQ: https://googlecloudplatform.github.io/google-cloud-node/#/docs/guides/faq +- [X] Check our libraries HOW-TO: https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md +- [X] Check out our authentication guide: https://github.com/googleapis/google-auth-library-nodejs +- [X] Check out handwritten samples for many of our APIs: https://github.com/GoogleCloudPlatform/nodejs-docs-samples + +### A screenshot that you have tested with "Try this API". + + +N/A + +### A step-by-step description of how to reproduce the issue, based on the linked reproduction. + + +Change MY_PROJECT to your project name, add credentials if needed and run. + +### A clear and concise description of what the bug is, and what you expected to happen. + +The application crashes with the following exception (which there is no way to catch). It should just emit error, and allow graceful handling. +TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Object + at _write (node:internal/streams/writable:474:13) + at Writable.write (node:internal/streams/writable:502:10) + at Duplexify._write (/project/node_modules/duplexify/index.js:212:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at Pumpify. (/project/node_modules/@google-cloud/speech/build/src/helpers.js:79:27) + at Object.onceWrapper (node:events:633:26) + at Pumpify.emit (node:events:518:28) + at obj. [as _write] (/project/node_modules/stubs/index.js:28:22) + at doWrite (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:390:139) + at writeOrBuffer (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:381:5) + at Writable.write (/project/node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:302:11) + at PassThrough.ondata (node:internal/streams/readable:1007:22) + at PassThrough.emit (node:events:518:28) + at addChunk (node:internal/streams/readable:559:12) { + code: 'ERR_INVALID_ARG_TYPE' + +### Link to the code that reproduces this issue. A link to a **public** Github Repository with a minimal reproduction. + + +https://gist.github.com/orgads/13cbf44c91923da27d8772b5f10489c9 + +### A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. ** + +No library should crash an application this way. \ No newline at end of file diff --git a/.github/scripts/remove-response-label.cjs b/.github/scripts/remove-response-label.cjs index 887cf349..4a784ddf 100644 --- a/.github/scripts/remove-response-label.cjs +++ b/.github/scripts/remove-response-label.cjs @@ -13,21 +13,21 @@ // limitations under the License. module.exports = async ({ github, context }) => { - const commenter = context.actor; - const issue = await github.rest.issues.get({ + const commenter = context.actor; + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const author = issue.data.user.login; + const labels = issue.data.labels.map((e) => e.name); + + if (author === commenter && labels.includes("needs more info")) { + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, + name: "needs more info", }); - const author = issue.data.user.login; - const labels = issue.data.labels.map((e) => e.name); - - if (author === commenter && labels.includes('needs more info')) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: 'needs more info', - }); - } - }; + } +}; \ No newline at end of file diff --git a/.github/scripts/tests/close-invalid-link.test.cjs b/.github/scripts/tests/close-invalid-link.test.cjs new file mode 100644 index 00000000..f63ee89c --- /dev/null +++ b/.github/scripts/tests/close-invalid-link.test.cjs @@ -0,0 +1,86 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const { describe, it } = require('mocha'); +const closeInvalidLink = require('../close-invalid-link.cjs'); +const fs = require('fs'); +const sinon = require('sinon'); + +describe('close issues with invalid links', () => { + let octokitStub; + let issuesStub; + + beforeEach(() => { + issuesStub = { + get: sinon.stub(), + createComment: sinon.stub(), + update: sinon.stub(), + }; + octokitStub = { + rest: { + issues: issuesStub, + }, + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('does not do anything if it is not a bug', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: "I'm having a problem with this." } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + it('does not do anything if it is a bug with an appropriate link', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/validIssueBody.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + it('does not do anything if it is a bug with an appropriate link and the template changes', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/validIssueBodyDifferentLinkLocation.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.createComment); + sinon.assert.notCalled(issuesStub.update); + }); + + it('closes the issue if the link is invalid', async () => { + const context = { repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + issuesStub.get.resolves({ data: { body: fs.readFileSync('./fixtures/invalidIssueBody.txt', 'utf-8') } }); + + await closeInvalidLink({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.calledOnce(issuesStub.createComment); + sinon.assert.calledOnce(issuesStub.update); + }); +}); \ No newline at end of file diff --git a/.github/scripts/tests/close-or-remove-response-label.test.cjs b/.github/scripts/tests/close-or-remove-response-label.test.cjs new file mode 100644 index 00000000..fb092c53 --- /dev/null +++ b/.github/scripts/tests/close-or-remove-response-label.test.cjs @@ -0,0 +1,109 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const { describe, it, beforeEach, afterEach } = require('mocha'); +const removeResponseLabel = require('../remove-response-label.cjs'); +const closeUnresponsive = require('../close-unresponsive.cjs'); +const sinon = require('sinon'); + +function getISODateDaysAgo(days) { + const today = new Date(); + const daysAgo = new Date(today.setDate(today.getDate() - days)); + return daysAgo.toISOString(); +} + +describe('close issues or remove needs more info labels', () => { + let octokitStub; + let issuesStub; + let paginateStub; + + beforeEach(() => { + issuesStub = { + listForRepo: sinon.stub(), + update: sinon.stub(), + createComment: sinon.stub(), + get: sinon.stub(), + removeLabel: sinon.stub(), + }; + paginateStub = sinon.stub(); + octokitStub = { + rest: { + issues: issuesStub, + }, + paginate: paginateStub, + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('closes the issue if the OP has not responded within the allotted time and there is a needs-more-info label', async () => { + const context = { owner: 'testOrg', repo: 'testRepo' }; + const issuesInRepo = [{ user: { login: 'OP' }, labels: [{ name: 'needs more info' }] }]; + const eventsInIssue = [{ event: 'labeled', label: { name: 'needs more info' }, created_at: getISODateDaysAgo(16) }]; + + issuesStub.listForRepo.resolves({ data: issuesInRepo }); + paginateStub.resolves(eventsInIssue); + + await closeUnresponsive({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.listForRepo); + sinon.assert.calledOnce(paginateStub); + sinon.assert.calledOnce(issuesStub.update); + sinon.assert.calledOnce(issuesStub.createComment); + }); + + it('does nothing if not enough time has passed and there is a needs-more-info label', async () => { + const context = { owner: 'testOrg', repo: 'testRepo' }; + const issuesInRepo = [{ user: { login: 'OP' }, labels: [{ name: 'needs more info' }] }]; + const eventsInIssue = [{ event: 'labeled', label: { name: 'needs more info' }, created_at: getISODateDaysAgo(14) }]; + + issuesStub.listForRepo.resolves({ data: issuesInRepo }); + paginateStub.resolves(eventsInIssue); + + await closeUnresponsive({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.listForRepo); + sinon.assert.calledOnce(paginateStub); + sinon.assert.notCalled(issuesStub.update); + sinon.assert.notCalled(issuesStub.createComment); + }); + + it('removes the label if OP responded', async () => { + const context = { actor: 'OP', repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + const issueContext = { user: {login: 'OP'}, labels: [{ name: 'needs more info' }] }; + + issuesStub.get.resolves({ data: issueContext }); + + await removeResponseLabel({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.calledOnce(issuesStub.removeLabel); + }); + + it('does not remove the label if author responded', async () => { + const context = { actor: 'repo-maintainer', repo: { owner: 'testOrg', repo: 'testRepo' }, issue: { number: 1 } }; + const issueContext = { user: {login: 'OP'}, labels: [{ name: 'needs more info' }] }; + + issuesStub.get.resolves({ data: issueContext }); + + await removeResponseLabel({ github: octokitStub, context }); + + sinon.assert.calledOnce(issuesStub.get); + sinon.assert.notCalled(issuesStub.removeLabel); + }); +}); \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4892eb2c..a143705d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [14, 16, 18, 20] + node: [18, 20, 22] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: node --version @@ -26,13 +26,27 @@ jobs: - run: npm test env: MOCHA_THROW_DEPRECATION: false + test-script: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: node --version + - run: npm install --engine-strict + working-directory: .github/scripts + - run: npm test + working-directory: .github/scripts + env: + MOCHA_THROW_DEPRECATION: false windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 14 + node-version: 18 - run: npm install --engine-strict - run: npm test env: @@ -40,21 +54,21 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 14 + node-version: 18 - run: npm install - run: npm run lint - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 14 - - run: npm install - - run: npm run docs - - uses: JustinBeckwith/linkinator-action@v1 - with: - paths: docs/ + # docs: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-node@v4 + # with: + # node-version: 18 + # - run: npm install + # - run: npm run docs + # - uses: JustinBeckwith/linkinator-action@v1 + # with: + # paths: docs/ diff --git a/.github/workflows/issues-no-repro.yaml b/.github/workflows/issues-no-repro.yaml index 442a46bc..816d9a70 100644 --- a/.github/workflows/issues-no-repro.yaml +++ b/.github/workflows/issues-no-repro.yaml @@ -11,6 +11,11 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: npm install + working-directory: ./.github/scripts - uses: actions/github-script@v7 with: script: | diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index d5bc635f..f5488a01 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -16,7 +16,7 @@ build_file: "nodejs-logging-winston/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/continuous/node14/common.cfg b/.kokoro/continuous/node18/common.cfg similarity index 93% rename from .kokoro/continuous/node14/common.cfg rename to .kokoro/continuous/node18/common.cfg index d5bc635f..f5488a01 100644 --- a/.kokoro/continuous/node14/common.cfg +++ b/.kokoro/continuous/node18/common.cfg @@ -16,7 +16,7 @@ build_file: "nodejs-logging-winston/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/continuous/node14/lint.cfg b/.kokoro/continuous/node18/lint.cfg similarity index 100% rename from .kokoro/continuous/node14/lint.cfg rename to .kokoro/continuous/node18/lint.cfg diff --git a/.kokoro/continuous/node14/samples-test.cfg b/.kokoro/continuous/node18/samples-test.cfg similarity index 100% rename from .kokoro/continuous/node14/samples-test.cfg rename to .kokoro/continuous/node18/samples-test.cfg diff --git a/.kokoro/continuous/node14/system-test.cfg b/.kokoro/continuous/node18/system-test.cfg similarity index 100% rename from .kokoro/continuous/node14/system-test.cfg rename to .kokoro/continuous/node18/system-test.cfg diff --git a/.kokoro/continuous/node14/test.cfg b/.kokoro/continuous/node18/test.cfg similarity index 100% rename from .kokoro/continuous/node14/test.cfg rename to .kokoro/continuous/node18/test.cfg diff --git a/.kokoro/presubmit/node14/common.cfg b/.kokoro/presubmit/node18/common.cfg similarity index 89% rename from .kokoro/presubmit/node14/common.cfg rename to .kokoro/presubmit/node18/common.cfg index 5c68abf3..d3f55eec 100644 --- a/.kokoro/presubmit/node14/common.cfg +++ b/.kokoro/presubmit/node18/common.cfg @@ -16,7 +16,7 @@ build_file: "nodejs-logging-winston/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { key: "TRAMPOLINE_BUILD_FILE" diff --git a/.kokoro/presubmit/node14/samples-test.cfg b/.kokoro/presubmit/node18/samples-test.cfg similarity index 100% rename from .kokoro/presubmit/node14/samples-test.cfg rename to .kokoro/presubmit/node18/samples-test.cfg diff --git a/.kokoro/presubmit/node14/system-test.cfg b/.kokoro/presubmit/node18/system-test.cfg similarity index 100% rename from .kokoro/presubmit/node14/system-test.cfg rename to .kokoro/presubmit/node18/system-test.cfg diff --git a/.kokoro/presubmit/node14/test.cfg b/.kokoro/presubmit/node18/test.cfg similarity index 100% rename from .kokoro/presubmit/node14/test.cfg rename to .kokoro/presubmit/node18/test.cfg diff --git a/.kokoro/release/docs-devsite.cfg b/.kokoro/release/docs-devsite.cfg index 45508185..3888c827 100644 --- a/.kokoro/release/docs-devsite.cfg +++ b/.kokoro/release/docs-devsite.cfg @@ -11,7 +11,7 @@ before_action { # doc publications use a Python image. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } # Download trampoline resources. diff --git a/.kokoro/release/docs.cfg b/.kokoro/release/docs.cfg index f1239a8e..e032df66 100644 --- a/.kokoro/release/docs.cfg +++ b/.kokoro/release/docs.cfg @@ -11,7 +11,7 @@ before_action { # doc publications use a Python image. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } # Download trampoline resources. diff --git a/.kokoro/release/docs.sh b/.kokoro/release/docs.sh index 1d8f3f49..e9079a60 100755 --- a/.kokoro/release/docs.sh +++ b/.kokoro/release/docs.sh @@ -16,7 +16,7 @@ set -eo pipefail -# build jsdocs (Python is installed on the Node 10 docker image). +# build jsdocs (Python is installed on the Node 18 docker image). if [[ -z "$CREDENTIALS" ]]; then # if CREDENTIALS are explicitly set, assume we're testing locally # and don't set NPM_CONFIG_PREFIX. diff --git a/.kokoro/release/publish.cfg b/.kokoro/release/publish.cfg index e7f04dfa..1f8c73c9 100644 --- a/.kokoro/release/publish.cfg +++ b/.kokoro/release/publish.cfg @@ -30,7 +30,7 @@ build_file: "nodejs-logging-winston/.kokoro/trampoline_v2.sh" # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:14-user" + value: "gcr.io/cloud-devrel-kokoro-resources/node:18-user" } env_vars: { diff --git a/.kokoro/samples-test.sh b/.kokoro/samples-test.sh index 8c5d108c..52877539 100755 --- a/.kokoro/samples-test.sh +++ b/.kokoro/samples-test.sh @@ -16,7 +16,9 @@ set -eo pipefail -export NPM_CONFIG_PREFIX=${HOME}/.npm-global +# Ensure the npm global directory is writable, otherwise rebuild `npm` +mkdir -p $NPM_CONFIG_PREFIX +npm config -g ls || npm i -g npm@`npm --version` # Setup service account credentials. export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secret_manager/long-door-651-kokoro-system-test-service-account @@ -56,7 +58,7 @@ fi # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=14 +COVERAGE_NODE=18 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 0b3043d2..a90d5cfe 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -49,7 +49,7 @@ npm run system-test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=14 +COVERAGE_NODE=18 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/test.bat b/.kokoro/test.bat index 0bb12405..caf82565 100644 --- a/.kokoro/test.bat +++ b/.kokoro/test.bat @@ -21,7 +21,7 @@ cd .. @rem we upgrade Node.js in the image: SET PATH=%PATH%;/cygdrive/c/Program Files/nodejs/npm -call nvm use v14.17.3 +call nvm use 18 call which node call npm install || goto :error diff --git a/.kokoro/test.sh b/.kokoro/test.sh index 862d478d..0d9f6392 100755 --- a/.kokoro/test.sh +++ b/.kokoro/test.sh @@ -39,7 +39,7 @@ npm test # codecov combines coverage across integration and unit tests. Include # the logic below for any environment you wish to collect coverage for: -COVERAGE_NODE=14 +COVERAGE_NODE=18 if npx check-node-version@3.3.0 --silent --node $COVERAGE_NODE; then NYC_BIN=./node_modules/nyc/bin/nyc.js if [ -f "$NYC_BIN" ]; then diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 4d031121..5d6cfcca 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -44,7 +44,7 @@ # the project root. # # Here is an example for running this script. -# TRAMPOLINE_IMAGE=gcr.io/cloud-devrel-kokoro-resources/node:10-user \ +# TRAMPOLINE_IMAGE=gcr.io/cloud-devrel-kokoro-resources/node:18-user \ # TRAMPOLINE_BUILD_FILE=.kokoro/system-test.sh \ # .kokoro/trampoline_v2.sh diff --git a/owlbot.py b/owlbot.py index 20924c70..7de618e5 100644 --- a/owlbot.py +++ b/owlbot.py @@ -26,7 +26,8 @@ ".github/auto-label.yaml", ".github/release-please.yml", ".github/CODEOWNERS", - ".github/sync-repo-settings.yaml" + ".github/sync-repo-settings.yaml", + ".github/workflows/ci.yaml" ]) node.fix() diff --git a/package.json b/package.json index 20c6f638..5cde85e0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "author": "Google Inc.", "engines": { - "node": ">=14.0.0" + "node": ">=18" }, "repository": "googleapis/nodejs-logging-winston", "main": "./build/src/index.js", @@ -50,32 +50,31 @@ "precompile": "gts clean" }, "dependencies": { - "@google-cloud/logging": "^11.0.0", - "google-auth-library": "^9.0.0", + "@google-cloud/logging": "^11.2.1", + "google-auth-library": "^10.5.0", "lodash.mapvalues": "^4.6.0", - "winston-transport": "^4.3.0" + "winston-transport": "^4.9.0" }, "devDependencies": { - "@google-cloud/common": "^5.0.0", - "@types/lodash.mapvalues": "^4.6.6", - "@types/mocha": "^9.0.0", - "@types/node": "^20.4.9", - "@types/proxyquire": "^1.3.28", - "@types/triple-beam": "^1.3.0", - "@types/uuid": "^9.0.0", - "c8": "^9.0.0", - "codecov": "^3.5.0", - "gts": "^5.0.0", - "jsdoc": "^4.0.0", - "jsdoc-fresh": "^3.0.0", - "jsdoc-region-tag": "^3.0.0", - "linkinator": "^3.0.0", - "mocha": "^9.2.2", - "post-install-check": "0.0.1", - "proxyquire": "^2.1.0", - "typescript": "^5.1.6", - "uuid": "^9.0.0", - "winston": "^3.2.1" + "@google-cloud/common": "^6.0.0", + "@types/lodash.mapvalues": "^4.6.9", + "@types/mocha": "^10.0.10", + "@types/node": "^24.10.1", + "@types/proxyquire": "^1.3.31", + "@types/triple-beam": "^1.3.5", + "c8": "^10.1.3", + "codecov": "^3.8.3", + "gts": "^6.0.2", + "jsdoc": "^4.0.5", + "jsdoc-fresh": "^5.0.2", + "jsdoc-region-tag": "^4.0.1", + "linkinator": "^6.0.0", + "mocha": "^11.7.5", + "pack-n-play": "4.2.1", + "post-install-check": "^0.0.1", + "proxyquire": "^2.1.3", + "typescript": "^5.9.3", + "winston": "^3.18.3" }, "peerDependencies": { "winston": ">=3.2.1" diff --git a/samples/package.json b/samples/package.json index defb7104..bcb56949 100644 --- a/samples/package.json +++ b/samples/package.json @@ -8,7 +8,7 @@ "author": "Google Inc.", "repository": "googleapis/nodejs-logging-winston", "engines": { - "node": ">=14.0.0" + "node": ">=18" }, "scripts": { "test": "mocha --timeout 600000" diff --git a/src/common.ts b/src/common.ts index e2f22bd7..bbcc8785 100644 --- a/src/common.ts +++ b/src/common.ts @@ -145,7 +145,7 @@ export class LoggingCommon { { scopes: ['https://www.googleapis.com/auth/logging.write'], }, - options + options, ); this.logName = options.logName || 'winston_log'; @@ -168,7 +168,7 @@ export class LoggingCommon { this.cloudLog = new Logging(options).logSync( this.logName, undefined, - logSyncOptions + logSyncOptions, ); } this.resource = options.resource; @@ -182,7 +182,7 @@ export class LoggingCommon { level: string, message: string, metadata: MetadataArg | undefined, - callback: Callback + callback: Callback, ) { metadata = metadata || ({} as MetadataArg); // First create instrumentation record if it is never written before @@ -190,7 +190,7 @@ export class LoggingCommon { if (!setInstrumentationStatus(true)) { instrumentationEntry = createDiagnosticEntry( 'nodejs-winston', - getNodejsLibraryVersion() + getNodejsLibraryVersion(), ); // Update instrumentation record resource, logName and timestamp instrumentationEntry.metadata.resource = this.resource; @@ -305,7 +305,7 @@ export class LoggingCommon { // Make sure instrumentation entry is updated by underlying logger instrumentationEntry = this.entry( instrumentationEntry.metadata, - instrumentationEntry.data + instrumentationEntry.data, ); if (levelCode !== NPM_LEVEL_NAME_TO_CODE.info) { // We using info level for diagnostic records diff --git a/src/middleware/express.ts b/src/middleware/express.ts index b6ae61fa..5dfb1fbd 100644 --- a/src/middleware/express.ts +++ b/src/middleware/express.ts @@ -36,23 +36,23 @@ type Middleware = ReturnType; export async function makeMiddleware( logger: winston.Logger, transport: LoggingWinston, - skipParentEntryForCloudRun?: boolean + skipParentEntryForCloudRun?: boolean, ): Promise; export async function makeMiddleware( logger: winston.Logger, options?: Options, - skipParentEntryForCloudRun?: boolean + skipParentEntryForCloudRun?: boolean, ): Promise; export async function makeMiddleware( logger: winston.Logger, optionsOrTransport?: Options | LoggingWinston, - skipParentEntryForCloudRun?: boolean + skipParentEntryForCloudRun?: boolean, ): Promise { let transport: LoggingWinston; // If no custom transports are provided, use default or instantiate one. const cloudTransport = logger.transports.find( - t => t instanceof LoggingWinston + t => t instanceof LoggingWinston, ); // If user provides a custom transport, always add it to the logger. @@ -93,14 +93,14 @@ export async function makeMiddleware( ) { const requestLogName = Log.formatName_( projectId, - `${transport.common.logName}${REQUEST_LOG_SUFFIX}` + `${transport.common.logName}${REQUEST_LOG_SUFFIX}`, ); emitRequestLogEntry = ( httpRequest: HttpRequest, trace: string, span?: string, - sampled?: boolean + sampled?: boolean, ) => { logger.info({ // The request logs must have a log name distinct from the app logs @@ -119,6 +119,6 @@ export async function makeMiddleware( projectId, (trace: string, span?: string, sampled?: boolean) => makeChildLogger(logger, trace, span, sampled), - emitRequestLogEntry + emitRequestLogEntry, ); } diff --git a/src/middleware/make-child-logger.ts b/src/middleware/make-child-logger.ts index d517bc3b..3d96c273 100644 --- a/src/middleware/make-child-logger.ts +++ b/src/middleware/make-child-logger.ts @@ -23,7 +23,7 @@ export function makeChildLogger( logger: winston.Logger, trace: string, span?: string, - sampled?: boolean + sampled?: boolean, ) { return logger.child({ [LOGGING_TRACE_KEY]: trace, diff --git a/system-test/errors-transport.ts b/system-test/errors-transport.ts index c96a2f0a..d1a7971f 100644 --- a/system-test/errors-transport.ts +++ b/system-test/errors-transport.ts @@ -64,7 +64,7 @@ export class ErrorsApiTransport extends common.Service { async request(options: common.DecorateRequestOptions) { return new Promise((resolve, reject) => { super.request(options, (err, _, res) => - err ? reject(err) : resolve(res as common.ResponseBody) + err ? reject(err) : resolve(res as common.ResponseBody), ); }); } @@ -98,7 +98,7 @@ export class ErrorsApiTransport extends common.Service { async pollForNewEvents( service: string, time: number, - timeout: number + timeout: number, ): Promise { const timeLimit = Date.now() + timeout; let groupId; @@ -112,7 +112,7 @@ export class ErrorsApiTransport extends common.Service { // find an error group that matches the service groups.forEach(group => { const match = group.affectedServices.find( - context => context.service === service + context => context.service === service, ); if (match) { groupId = group.group.groupId; @@ -127,7 +127,7 @@ export class ErrorsApiTransport extends common.Service { const filteredEvents = events.filter( event => event.serviceContext.service === service && - new Date(event.eventTime).getTime() >= time + new Date(event.eventTime).getTime() >= time, ); if (filteredEvents.length) { return filteredEvents; diff --git a/system-test/logging-winston.ts b/system-test/logging-winston.ts deleted file mode 100644 index 02907904..00000000 --- a/system-test/logging-winston.ts +++ /dev/null @@ -1,279 +0,0 @@ -/*! - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as assert from 'assert'; -import {describe, it} from 'mocha'; -import * as uuid from 'uuid'; -import {ErrorsApiTransport} from './errors-transport'; -import * as proxyquire from 'proxyquire'; -import * as winston from 'winston'; -import {Logging, Entry} from '@google-cloud/logging'; -import * as instrumentation from '@google-cloud/logging/build/src/utils/instrumentation'; - -const logging = new Logging(); - -const WRITE_CONSISTENCY_DELAY_MS = 90000; - -const UUID = uuid.v4(); -function logName(name: string) { - return `${UUID}_${name}`; -} - -describe('LoggingWinston', function () { - this.timeout(WRITE_CONSISTENCY_DELAY_MS); - const testTimestamp = new Date(); - - // type TestData - - const commonTestData: TestData[] = [ - { - args: ['first'], - level: 'info', - verify: (entry: Entry) => { - assert.deepStrictEqual(entry.data, { - message: 'first', - // TODO: investigate why this behavior has changed - // in @google-cloud/logging, see: - // https://github.com/googleapis/nodejs-logging/issues/818 - metadata: [null], - }); - }, - }, - - { - args: ['second'], - level: 'info', - verify: (entry: Entry) => { - assert.deepStrictEqual(entry.data, { - message: 'second', - // TODO: investigate why this behavior has changed - // in @google-cloud/logging, see: - // https://github.com/googleapis/nodejs-logging/issues/818 - metadata: null, - }); - }, - }, - { - args: ['third', {testTimestamp}], - level: 'info', - verify: (entry: Entry) => { - assert.deepStrictEqual(entry.data, { - message: 'third', - metadata: { - testTimestamp: String(testTimestamp), - }, - }); - }, - }, - ]; - - interface TestData { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: any[]; - level: string; - verify: (entry: Entry) => void; - } - - describe('log', () => { - const LoggingWinston = proxyquire('../src/index', {winston}).LoggingWinston; - - it.skip('should properly write log entries', async () => { - const testData = commonTestData.concat([ - { - args: [new Error('fourth')], - level: 'error', - verify: (entry: Entry) => { - assert( - ( - entry.data as { - message: string; - } - ).message.startsWith('fourth Error:') - ); - }, - }, - { - args: [ - { - level: 'error', - message: 'fifth message', - error: new Error('fifth'), - }, - ], - level: 'log', - verify: (entry: Entry) => { - assert( - ( - entry.data as { - message: string; - } - ).message.startsWith('fifth message') - ); - }, - }, - ] as TestData[]); - - const LOG_NAME = logName('logging_winston_system_tests_1'); - - const logger = winston.createLogger({ - transports: [new LoggingWinston({logName: LOG_NAME})], - }); - - const start = Date.now(); - testData.forEach(test => { - // logger does not have index signature. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (logger as any)[test.level].apply(logger, test.args); - }); - - const entries = await pollLogs( - LOG_NAME, - start, - testData.length, - WRITE_CONSISTENCY_DELAY_MS - ); - assert.strictEqual(entries.length, testData.length); - entries.reverse().forEach((entry, index) => { - const test = testData[index]; - test.verify(entry as {} as Entry); - }); - }); - - it('should work correctly with winston formats', async () => { - const MESSAGE = 'A message that should be padded'; - const start = Date.now(); - const LOG_NAME = logName('logging_winston_system_tests_2'); - const logger = winston.createLogger({ - transports: [new LoggingWinston({logName: LOG_NAME})], - format: winston.format.combine( - winston.format.colorize(), - winston.format.padLevels() - ), - }); - // Make sure we logging below with error severity so the further query - // will not return additional diagnostic record which is always written with INFO severity - logger.error(MESSAGE); - - const entries = await pollLogs( - LOG_NAME, - start, - 2, - WRITE_CONSISTENCY_DELAY_MS - ); - entries.forEach(entry => { - assert.ok(entry.data); - if ( - Object.prototype.hasOwnProperty.call( - entry.data, - instrumentation.DIAGNOSTIC_INFO_KEY - ) - ) { - const info = - entry.data[instrumentation.DIAGNOSTIC_INFO_KEY][ - instrumentation.INSTRUMENTATION_SOURCE_KEY - ]; - assert.equal(info[0].name, 'nodejs-winston'); - assert.equal(info[1].name, 'nodejs'); - } else { - const data = entry.data as {message: string}; - assert.strictEqual(data.message, ` ${MESSAGE}`); - } - }); - }); - }); - - describe('ErrorReporting', () => { - const LOG_NAME = logName('logging_winston_error_reporting_system_tests'); - const ERROR_REPORTING_POLL_TIMEOUT = WRITE_CONSISTENCY_DELAY_MS; - const errorsTransport = new ErrorsApiTransport(); - - it('reports errors', async () => { - const start = Date.now(); - const service = `logging-winston-system-test-winston3-${UUID}`; - const LoggingWinston = proxyquire('../src/index', { - winston, - }).LoggingWinston; - - const logger = winston.createLogger({ - transports: [ - new LoggingWinston({ - logName: LOG_NAME, - serviceContext: {service, version: 'none'}, - }), - ], - }); - - const message = `an error at ${Date.now()}`; - - logger.error(new Error(message)); - - const errors = await errorsTransport.pollForNewEvents( - service, - start, - ERROR_REPORTING_POLL_TIMEOUT - ); - - assert.strictEqual(errors.length, 1); - const errEvent = errors[0]; - - assert.strictEqual(errEvent.serviceContext.service, service); - - assert(errEvent.message.startsWith(message)); - }); - }); -}); - -// polls for the entire array of entries to be greater than logTime. -function pollLogs( - logName: string, - logTime: number, - size: number, - timeout: number -) { - const p = new Promise((resolve, reject) => { - const end = Date.now() + timeout; - loop(); - - function loop() { - setTimeout(() => { - logging.log(logName).getEntries( - { - pageSize: size, - }, - (err, entries) => { - if (!entries || entries.length < size) return loop(); - - const {receiveTimestamp} = (entries[entries.length - 1].metadata || - {}) as {receiveTimestamp: {seconds: number; nanos: number}}; - const timeMilliseconds = - receiveTimestamp.seconds * 1000 + receiveTimestamp.nanos * 1e-6; - - if (timeMilliseconds >= logTime) { - return resolve(entries); - } - - if (Date.now() > end) { - return reject(new Error('timeout')); - } - loop(); - } - ); - }, 500); - } - }); - - return p; -} diff --git a/system-test/test-install.ts b/system-test/test-install.ts index cfd9d21e..a5aa4bd9 100644 --- a/system-test/test-install.ts +++ b/system-test/test-install.ts @@ -1,10 +1,10 @@ -// Copyright 2018 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -12,37 +12,39 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as check from 'post-install-check'; +import {packNTest} from 'pack-n-play'; +import {describe, it} from 'mocha'; -const TS_CODE_SAMPLES: check.CodeSample[] = [ - { - code: `import * as loggingWinston from '@google-cloud/logging-winston'; +describe('pack-n-play', () => { + const TS_CODE_SAMPLES = [ + { + ts: `import * as loggingWinston from '@google-cloud/logging-winston'; new loggingWinston.LoggingWinston();`, - description: 'imports the module using * syntax', - dependencies: ['winston'], - devDependencies: ['@types/winston', 'typescript@3'], - }, - { - code: `import {LoggingWinston} from '@google-cloud/logging-winston'; + description: 'imports the module using * syntax', + dependencies: ['winston'], + devDependencies: ['@types/winston', 'typescript@5'], + }, + { + ts: `import {LoggingWinston} from '@google-cloud/logging-winston'; new LoggingWinston();`, - description: 'imports the module with {} syntax', - dependencies: ['winston'], - devDependencies: ['@types/winston', 'typescript@3'], - }, - { - code: `import {LoggingWinston} from '@google-cloud/logging-winston'; + description: 'imports the module with {} syntax', + dependencies: ['winston'], + devDependencies: ['@types/winston', 'typescript@5'], + }, + { + ts: `import {LoggingWinston} from '@google-cloud/logging-winston'; new LoggingWinston({ serviceContext: { service: 'some service' } });`, - description: - 'imports the module and starts with a partial `serviceContext`', - dependencies: ['winston'], - devDependencies: ['@types/winston', 'typescript@3'], - }, - { - code: `import {LoggingWinston} from '@google-cloud/logging-winston'; + description: + 'imports the module and starts with a partial `serviceContext`', + dependencies: ['winston'], + devDependencies: ['@types/winston', 'typescript@5'], + }, + { + ts: `import {LoggingWinston} from '@google-cloud/logging-winston'; new LoggingWinston({ projectId: 'some-project', serviceContext: { @@ -50,14 +52,14 @@ new LoggingWinston({ version: 'Some version' } });`, - description: - 'imports the module and starts with a complete `serviceContext`', - dependencies: ['winston'], - devDependencies: ['@types/winston', 'typescript@3'], - }, + description: + 'imports the module and starts with a complete `serviceContext`', + dependencies: ['winston'], + devDependencies: ['@types/winston', 'typescript@5'], + }, - { - code: `import {LoggingWinston} from '@google-cloud/logging-winston'; + { + ts: `import {LoggingWinston} from '@google-cloud/logging-winston'; import * as winston from 'winston'; const loggingWinston = new LoggingWinston({ prefix: 'some-prefix', @@ -70,12 +72,12 @@ const loggingWinston = new LoggingWinston({ winston.createLogger({transports:[loggingWinston]}) `, - description: 'imports the module with a prefix and labels specified', - dependencies: ['winston'], - devDependencies: ['typescript@3'], - }, - { - code: `import { LoggingWinston } from '@google-cloud/logging-winston'; + description: 'imports the module with a prefix and labels specified', + dependencies: ['winston'], + devDependencies: ['typescript@5'], + }, + { + ts: `import { LoggingWinston } from '@google-cloud/logging-winston'; import * as winston from 'winston'; winston.createLogger({ @@ -83,34 +85,34 @@ winston.createLogger({transports:[loggingWinston]}) new LoggingWinston(), ], });`, - description: 'imports transport-stream correctly', - dependencies: ['winston', 'winston-transport'], - devDependencies: [], - }, -]; + description: 'imports transport-stream correctly', + dependencies: ['winston', 'winston-transport'], + devDependencies: [], + }, + ]; -const JS_CODE_SAMPLES: check.CodeSample[] = [ - { - code: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; + const JS_CODE_SAMPLES = [ + { + js: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; new LoggingWinston();`, - description: 'requires the module using Node 4+ syntax', - dependencies: ['winston'], - devDependencies: [], - }, - { - code: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; + description: 'requires the module using Node 4+ syntax', + dependencies: ['winston'], + devDependencies: [], + }, + { + js: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; new LoggingWinston({ serviceContext: { service: 'some service' } });`, - description: - 'requires the module and starts with a partial `serviceContext`', - dependencies: ['winston'], - devDependencies: [], - }, - { - code: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; + description: + 'requires the module and starts with a partial `serviceContext`', + dependencies: ['winston'], + devDependencies: [], + }, + { + js: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; new LoggingWinston({ projectId: 'some-project', serviceContext: { @@ -118,13 +120,13 @@ new LoggingWinston({ version: 'Some version' } });`, - description: - 'requires the module and starts with a complete `serviceContext`', - dependencies: ['winston'], - devDependencies: [], - }, - { - code: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; + description: + 'requires the module and starts with a complete `serviceContext`', + dependencies: ['winston'], + devDependencies: [], + }, + { + js: `const LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; new LoggingWinston({ prefix: 'some-prefix', labels: { @@ -133,12 +135,25 @@ new LoggingWinston({ version: 'some-version' } });`, - description: 'imports the module with a prefix and labels specified', - dependencies: ['winston'], - devDependencies: [], - }, -]; + description: 'imports the module with a prefix and labels specified', + dependencies: ['winston'], + devDependencies: [], + }, + ]; -check.testInstallation(TS_CODE_SAMPLES, JS_CODE_SAMPLES, { - timeout: 2 * 60 * 1000, + TS_CODE_SAMPLES.forEach(sample => { + it(sample.description, async () => { + await packNTest({ + sample, + }); + }).timeout(2 * 60 * 1000); + }); + + JS_CODE_SAMPLES.forEach(sample => { + it(sample.description, async () => { + await packNTest({ + sample, + }); + }).timeout(2 * 60 * 1000); + }); }); diff --git a/system-test/test-middleware-express.ts b/system-test/test-middleware-express.ts index bfa33920..dfd5969d 100644 --- a/system-test/test-middleware-express.ts +++ b/system-test/test-middleware-express.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; import {describe, it} from 'mocha'; -import * as uuid from 'uuid'; +import * as crypto from 'crypto'; import {express as elb} from '../src/index'; import * as winston from 'winston'; import {REQUEST_LOG_SUFFIX} from '../src/middleware/express'; @@ -26,7 +26,7 @@ const logging = new Logging(); const WRITE_CONSISTENCY_DELAY_MS = 20 * 1000; const TEST_TIMEOUT = WRITE_CONSISTENCY_DELAY_MS + 10 * 1000; -const LOG_NAME = `winston-system-test-${uuid.v4()}`; +const LOG_NAME = `winston-system-test-${crypto.randomBytes(16).toString('hex')}`; function delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -42,7 +42,7 @@ describe(__filename, () => { level: 'info', }); - const LOG_MESSAGE = `unique log message ${uuid.v4()}`; + const LOG_MESSAGE = `unique log message ${crypto.randomBytes(16).toString('hex')}`; logger.info(LOG_MESSAGE); await delay(WRITE_CONSISTENCY_DELAY_MS); @@ -65,7 +65,7 @@ describe(__filename, () => { level: 'info', }); - const LOG_MESSAGE = `correlated log message ${uuid.v4()}`; + const LOG_MESSAGE = `correlated log message ${crypto.randomBytes(16).toString('hex')}`; const fakeRequest = { headers: { 'user-agent': 'Mocha/test-case', @@ -111,7 +111,7 @@ describe(__filename, () => { const [requestLogEntry] = requestLogEntries; assert.strictEqual( requestLogEntry.metadata.trace, - appLogEntry.metadata.trace + appLogEntry.metadata.trace, ); resolve(); diff --git a/test/common.ts b/test/common.ts index 7f2da073..39401702 100644 --- a/test/common.ts +++ b/test/common.ts @@ -116,7 +116,7 @@ describe('logging-common', () => { }); const loggingCommon = new loggingCommonLib.LoggingCommon( - optionsWithInspectMetadata + optionsWithInspectMetadata, ); assert.strictEqual(loggingCommon.inspectMetadata, true); }); @@ -130,7 +130,7 @@ describe('logging-common', () => { delete optionsWithoutLevels.levels; const loggingCommon = new loggingCommonLib.LoggingCommon( - optionsWithoutLevels + optionsWithoutLevels, ); assert.deepStrictEqual(loggingCommon.levels, { error: 3, @@ -150,7 +150,7 @@ describe('logging-common', () => { optionsWithLogName.logName = logName; const loggingCommon = new loggingCommonLib.LoggingCommon( - optionsWithLogName + optionsWithLogName, ); const loggingOptions = Object.assign({}, fakeLoggingOptions_); @@ -193,10 +193,10 @@ describe('logging-common', () => { { redirectToStdout: true, useMessageField: true, - } + }, ); const loggingCommon = new LoggingCommon( - optionsWithRedirectToStdoutAndUseMessage + optionsWithRedirectToStdoutAndUseMessage, ); assert.ok(loggingCommon.cloudLog instanceof LogSync); assert.ok(loggingCommon.cloudLog.useMessageField_ === true); @@ -230,7 +230,7 @@ describe('logging-common', () => { 'non-existent-level', MESSAGE, METADATA, - assert.ifError + assert.ifError, ); }, /Unknown log level: non-existent-level/); }); @@ -326,7 +326,7 @@ describe('logging-common', () => { { httpRequest: HTTP_REQUEST, }, - METADATA + METADATA, ); loggingCommon.cloudLog.entry = (entryMetadata: {}, data: {}) => { @@ -349,7 +349,7 @@ describe('logging-common', () => { { timestamp: date, }, - METADATA + METADATA, ); loggingCommon.cloudLog.entry = (entryMetadata: {}, data: {}) => { @@ -518,7 +518,7 @@ describe('logging-common', () => { loggingCommon.cloudLog[STACKDRIVER_LEVEL] = ( entry_: Entry[], - callback: () => void + callback: () => void, ) => { assert.deepEqual(entry_[0], entry); callback(); // done() @@ -533,14 +533,14 @@ describe('logging-common', () => { }; loggingCommon.cloudLog['info'] = ( entry_: Entry[], - callback: () => void + callback: () => void, ) => { assert.equal(entry_.length, 2); assert.equal( entry_[1].data[instrumentation.DIAGNOSTIC_INFO_KEY][ instrumentation.INSTRUMENTATION_SOURCE_KEY ][0].name, - 'nodejs-winston' + 'nodejs-winston', ); callback(); // done() }; @@ -558,7 +558,7 @@ describe('logging-common', () => { entry_[0].data[instrumentation.DIAGNOSTIC_INFO_KEY][ instrumentation.INSTRUMENTATION_SOURCE_KEY ][0].name, - 'nodejs-winston' + 'nodejs-winston', ); }; loggingCommon.cloudLog[STACKDRIVER_LEVEL] = (entry_: Entry[]) => { @@ -626,7 +626,7 @@ describe('logging-common', () => { LEVEL, MESSAGE, metadataWithoutLabels, - assert.ifError + assert.ifError, ); }; diff --git a/test/index.ts b/test/index.ts index dc1c2251..13412aa4 100644 --- a/test/index.ts +++ b/test/index.ts @@ -31,7 +31,7 @@ describe('logging-winston', () => { level: string, message: string, metadata: {} | undefined, - callback: () => void + callback: () => void, ): void { // eslint-disable-next-line prefer-rest-params lastFakeLoggingArgs = arguments; @@ -135,7 +135,7 @@ describe('logging-winston', () => { it('should pass the provided service context', () => { assert.strictEqual( fakeLoggingOptions_!.serviceContext, - OPTIONS.serviceContext + OPTIONS.serviceContext, ); }); @@ -150,7 +150,7 @@ describe('logging-winston', () => { handleRejections: false, }); new loggingWinstonLib.LoggingWinston( - optionsWithTransportStreamparameters + optionsWithTransportStreamparameters, ); assert.strictEqual(fakeLoggingOptions_!.level, level); assert.strictEqual(fakeLoggingOptions_!.format, format); diff --git a/test/middleware/express.ts b/test/middleware/express.ts index 1cefb195..40a18536 100644 --- a/test/middleware/express.ts +++ b/test/middleware/express.ts @@ -66,7 +66,7 @@ let passedEmitRequestLog: Function | undefined; function fakeMakeMiddleware( projectId: string, makeChildLogger: Function, - emitRequestLog: Function + emitRequestLog: Function, ): Function { passedProjectId = projectId; passedEmitRequestLog = emitRequestLog; @@ -104,7 +104,7 @@ describe('middleware/express', () => { assert.strictEqual( transport, t, - 'makeMiddleware should not construct a transport' + 'makeMiddleware should not construct a transport', ); });