Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions src/mcp/tools/apphosting/fetch_logs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { expect } from "chai";
import * as sinon from "sinon";
import { fetch_logs } from "./fetch_logs";
import * as apphosting from "../../../gcp/apphosting";
import * as run from "../../../gcp/run";
import * as cloudlogging from "../../../gcp/cloudlogging";
import { FirebaseError } from "../../../error";
import { toContent } from "../../util";

describe("fetch_logs tool", () => {
const projectId = "test-project";
const location = "us-central1";
const backendId = "test-backend";

let getBackendStub: sinon.SinonStub;
let getTrafficStub: sinon.SinonStub;
let listBuildsStub: sinon.SinonStub;
let fetchServiceLogsStub: sinon.SinonStub;
let listEntriesStub: sinon.SinonStub;

beforeEach(() => {
getBackendStub = sinon.stub(apphosting, "getBackend");
getTrafficStub = sinon.stub(apphosting, "getTraffic");
listBuildsStub = sinon.stub(apphosting, "listBuilds");
fetchServiceLogsStub = sinon.stub(run, "fetchServiceLogs");
listEntriesStub = sinon.stub(cloudlogging, "listEntries");
});

afterEach(() => {
sinon.restore();
});

it("should return message if backendId is not specified", async () => {
const result = await fetch_logs.fn({}, { projectId } as any);

Check warning on line 34 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 34 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `ServerToolContext`
expect(result).to.deep.equal(toContent("backendId must be specified."));
});

context("when buildLogs is false", () => {
it("should fetch service logs successfully", async () => {
const backend = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}`,
managedResources: [
{
runService: {
service: `projects/${projectId}/locations/${location}/services/service-id`,
},
},
],
};
const traffic = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
};
const logs = ["log entry 1", "log entry 2"];

getBackendStub.resolves(backend);
getTrafficStub.resolves(traffic);
fetchServiceLogsStub.resolves(logs);

const result = await fetch_logs.fn({ backendId, location }, { projectId } as any);

Check warning on line 59 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 59 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `ServerToolContext`

expect(getBackendStub).to.be.calledWith(projectId, location, backendId);
expect(getTrafficStub).to.be.calledWith(projectId, location, backendId);
expect(fetchServiceLogsStub).to.be.calledWith(projectId, "service-id");
expect(result).to.deep.equal(toContent(logs));
});

it("should throw FirebaseError if service name cannot be determined", async () => {
const backend = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}`,
managedResources: [],
};
const traffic = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
};

getBackendStub.resolves(backend);
getTrafficStub.resolves(traffic);

await expect(fetch_logs.fn({ backendId, location }, { projectId } as any)).to.be.rejectedWith(

Check warning on line 79 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 79 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `ServerToolContext`
FirebaseError,
"Unable to get service name from managedResources.",
);
});
});
Comment on lines +38 to +84
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The test setup for traffic and getTrafficStub is repeated in both tests within this context. You can move this common setup into a beforeEach block to reduce duplication and improve maintainability.

  context("when buildLogs is false", () => {
    const traffic = {
      name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
    };

    beforeEach(() => {
      getTrafficStub.resolves(traffic);
    });

    it("should fetch service logs successfully", async () => {
      const backend = {
        name: `projects/${projectId}/locations/${location}/backends/${backendId}`,
        managedResources: [
          {
            runService: {
              service: `projects/${projectId}/locations/${location}/services/service-id`,
            },
          },
        ],
      };
      const logs = ["log entry 1", "log entry 2"];

      getBackendStub.resolves(backend);
      fetchServiceLogsStub.resolves(logs);

      const result = await fetch_logs.fn({ backendId, location }, { projectId } as any);

      expect(getBackendStub).to.be.calledWith(projectId, location, backendId);
      expect(getTrafficStub).to.be.calledWith(projectId, location, backendId);
      expect(fetchServiceLogsStub).to.be.calledWith(projectId, "service-id");
      expect(result).to.deep.equal(toContent(logs));
    });

    it("should throw FirebaseError if service name cannot be determined", async () => {
      const backend = {
        name: `projects/${projectId}/locations/${location}/backends/${backendId}`,
        managedResources: [],
      };

      getBackendStub.resolves(backend);

      await expect(fetch_logs.fn({ backendId, location }, { projectId } as any)).to.be.rejectedWith(
        FirebaseError,
        "Unable to get service name from managedResources.",
      );
    });
  });


context("when buildLogs is true", () => {
const buildLogsUri = `https://console.cloud.google.com/build/region=${location}/12345`;
const build = { createTime: new Date().toISOString(), buildLogsUri };
const builds = { builds: [build] };

it("should fetch build logs successfully", async () => {
const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` };
const traffic = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
};
const logEntries = [{ textPayload: "build log 1" }];

getBackendStub.resolves(backend);
getTrafficStub.resolves(traffic);
listBuildsStub.resolves(builds);
listEntriesStub.resolves(logEntries);

const result = await fetch_logs.fn({ buildLogs: true, backendId, location }, {

Check warning on line 103 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `ServerToolContext`
projectId,
} as any);

Check warning on line 105 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

expect(listBuildsStub).to.be.calledWith(projectId, location, backendId);
expect(listEntriesStub).to.be.calledOnce;
expect(listEntriesStub.args[0][1]).to.include('resource.labels.build_id="12345"');
expect(result).to.deep.equal(toContent(logEntries));
});

it("should return 'No logs found.' if no build logs are available", async () => {
const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` };
const traffic = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
};

getBackendStub.resolves(backend);
getTrafficStub.resolves(traffic);
listBuildsStub.resolves(builds);
listEntriesStub.resolves([]);

const result = await fetch_logs.fn({ buildLogs: true, backendId, location }, {

Check warning on line 124 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `ServerToolContext`
projectId,
} as any);

Check warning on line 126 in src/mcp/tools/apphosting/fetch_logs.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
expect(result).to.deep.equal(toContent("No logs found."));
});

it("should throw FirebaseError if build ID cannot be determined from buildLogsUri", async () => {
const buildWithInvalidUri = {
createTime: new Date().toISOString(),
buildLogsUri: "invalid-uri",
};
const buildsWithInvalidUri = { builds: [buildWithInvalidUri] };
const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` };
const traffic = {
name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
};

getBackendStub.resolves(backend);
getTrafficStub.resolves(traffic);
listBuildsStub.resolves(buildsWithInvalidUri);

await expect(
fetch_logs.fn({ buildLogs: true, backendId, location }, { projectId } as any),
).to.be.rejectedWith(FirebaseError, "Unable to determine the build ID.");
});
});
Comment on lines +86 to +149
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The setup for backend, traffic, and their corresponding stubs (getBackendStub, getTrafficStub) is repeated across all tests in this context. To improve code clarity and reduce duplication, you can extract this common setup into a beforeEach block within the context.

  context("when buildLogs is true", () => {
    const buildLogsUri = `https://console.cloud.google.com/build/region=${location}/12345`;
    const build = { createTime: new Date().toISOString(), buildLogsUri };
    const builds = { builds: [build] };
    const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` };
    const traffic = {
      name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
    };

    beforeEach(() => {
      getBackendStub.resolves(backend);
      getTrafficStub.resolves(traffic);
    });

    it("should fetch build logs successfully", async () => {
      const logEntries = [{ textPayload: "build log 1" }];

      listBuildsStub.resolves(builds);
      listEntriesStub.resolves(logEntries);

      const result = await fetch_logs.fn({ buildLogs: true, backendId, location }, {
        projectId,
      } as any);

      expect(listBuildsStub).to.be.calledWith(projectId, location, backendId);
      expect(listEntriesStub).to.be.calledOnce;
      expect(listEntriesStub.args[0][1]).to.include('resource.labels.build_id="12345"');
      expect(result).to.deep.equal(toContent(logEntries));
    });

    it("should return 'No logs found.' if no build logs are available", async () => {
      listBuildsStub.resolves(builds);
      listEntriesStub.resolves([]);

      const result = await fetch_logs.fn({ buildLogs: true, backendId, location }, {
        projectId,
      } as any);
      expect(result).to.deep.equal(toContent("No logs found."));
    });

    it("should throw FirebaseError if build ID cannot be determined from buildLogsUri", async () => {
      const buildWithInvalidUri = {
        createTime: new Date().toISOString(),
        buildLogsUri: "invalid-uri",
      };
      const buildsWithInvalidUri = { builds: [buildWithInvalidUri] };

      listBuildsStub.resolves(buildsWithInvalidUri);

      await expect(
        fetch_logs.fn({ buildLogs: true, backendId, location }, { projectId } as any),
      ).to.be.rejectedWith(FirebaseError, "Unable to determine the build ID.");
    });
  });

});
66 changes: 66 additions & 0 deletions src/mcp/tools/apphosting/list_backends.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { expect } from "chai";
import * as sinon from "sinon";
import { list_backends } from "./list_backends";
import * as apphosting from "../../../gcp/apphosting";
import { toContent } from "../../util";

describe("list_backends tool", () => {
const projectId = "test-project";
const location = "us-central1";
const backendId = "test-backend";

let listBackendsStub: sinon.SinonStub;
let getTrafficStub: sinon.SinonStub;
let listDomainsStub: sinon.SinonStub;
let parseBackendNameStub: sinon.SinonStub;

beforeEach(() => {
listBackendsStub = sinon.stub(apphosting, "listBackends");
getTrafficStub = sinon.stub(apphosting, "getTraffic");
listDomainsStub = sinon.stub(apphosting, "listDomains");
parseBackendNameStub = sinon.stub(apphosting, "parseBackendName");
});

afterEach(() => {
sinon.restore();
});

it("should return a message when no backends are found", async () => {
listBackendsStub.resolves({ backends: [] });

const result = await list_backends.fn({ location }, { projectId } as any);

expect(listBackendsStub).to.be.calledWith(projectId, location);
expect(result).to.deep.equal(
toContent(`No backends exist for project ${projectId} in ${location}.`),
);
});

it("should list backends with traffic and domain info", async () => {
const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` };
const backends = { backends: [backend] };
const traffic = { name: "traffic" };
const domains = [{ name: "domain" }];

listBackendsStub.resolves(backends);
parseBackendNameStub.returns({ location, id: backendId });
getTrafficStub.resolves(traffic);
listDomainsStub.resolves(domains);

const result = await list_backends.fn({ location }, { projectId } as any);

expect(listBackendsStub).to.be.calledWith(projectId, location);
expect(parseBackendNameStub).to.be.calledWith(backend.name);
expect(getTrafficStub).to.be.calledWith(projectId, location, backendId);
expect(listDomainsStub).to.be.calledWith(projectId, location, backendId);

const expectedData = [{ ...backend, traffic, domains }];
expect(result).to.deep.equal(toContent(expectedData));
});

it("should handle the default location", async () => {
listBackendsStub.resolves({ backends: [] });
await list_backends.fn({}, { projectId } as any);
expect(listBackendsStub).to.be.calledWith(projectId, "-");
});
});
Loading