diff --git a/experimental/traffic-portal/cypress/e2e/servers/servers.table.cy.ts b/experimental/traffic-portal/cypress/e2e/servers/servers.table.cy.ts index f5bcda64c8..34917e4494 100644 --- a/experimental/traffic-portal/cypress/e2e/servers/servers.table.cy.ts +++ b/experimental/traffic-portal/cypress/e2e/servers/servers.table.cy.ts @@ -15,10 +15,44 @@ describe("Servers table page", () => { beforeEach(() => { cy.login(); + cy.visit("/core/servers"); }); it("Filters servers by hostname", () => { - cy.visit("/core/servers"); cy.get("input[name=fuzzControl]").focus().type("edge"); cy.window().its("location.search").should("contain", "search=edge"); }); + it("Queues and clears revalidations on a server", () => { + cy.get("input[name=fuzzControl]").focus().type("edge"); + + // We need to force re-rendering of the table every time we do + // something, or cypress moves too fast and undoes things it's doing + // before the effects can be seen. This could be fixed by splitting + // these into separate tests, but that wouldn't be faster and would have + // the added drawback that it depends on the initial state of the data + // and the order in which the tests are run. + const reload = (): void => { + cy.reload(); + cy.get("button[aria-label='column visibility menu']").click(); + cy.get("input[type=checkbox][name='Reval Pending']").check(); + cy.get("body").click(); // closes the menu so you can interact with other things. + }; + + reload(); + + cy.get(".ag-row:visible").first().rightclick(); + cy.get("button").contains("Queue Content Revalidation").click(); + reload(); + + cy.get(".ag-cell[col-id=revalPending]").first().should("contain.text", "schedule"); + cy.get(".ag-row:visible").first().rightclick(); + cy.get("button").contains("Clear Queued Content Revalidations").click(); + reload(); + + cy.get(".ag-cell[col-id=revalPending]").first().should("contain.text", "done"); + cy.get(".ag-row:visible").first().rightclick(); + cy.get("button").contains("Queue Content Revalidation").click(); + reload(); + + cy.get(".ag-cell[col-id=revalPending]").first().should("contain.text", "schedule"); + }); }); diff --git a/experimental/traffic-portal/src/app/api/server.service.spec.ts b/experimental/traffic-portal/src/app/api/server.service.spec.ts index ccd2c82b91..70b1edabf2 100644 --- a/experimental/traffic-portal/src/app/api/server.service.spec.ts +++ b/experimental/traffic-portal/src/app/api/server.service.spec.ts @@ -14,7 +14,7 @@ */ import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; import { TestBed } from "@angular/core/testing"; -import { type ResponseServer } from "trafficops-types"; +import { ResponseServerCapability, type ResponseServer } from "trafficops-types"; import { ServerService } from "./server.service"; @@ -154,7 +154,15 @@ describe("ServerService", () => { await expectAsync(resp).toBeResolvedTo(server); }); - it("delete a server", async () => { + it("throws an error for invalid call signatures to updateServer", async () => { + const responseP = (service as unknown as {updateServer: (id: number) => Promise}).updateServer( + server.id + ); + httpTestingController.expectNone({method: "PUT"}); + await expectAsync(responseP).toBeRejected(); + }); + + it("deletes a server", async () => { const resp = service.deleteServer(server); const req = httpTestingController.expectOne(`/api/${service.apiVersion}/servers/${server.id}`); expect(req.request.method).toBe("DELETE"); @@ -164,7 +172,7 @@ describe("ServerService", () => { await expectAsync(resp).toBeResolvedTo(server); }); - it("delete a server by ID", async () => { + it("deletes a server by ID", async () => { const resp = service.deleteServer(server.id); const req = httpTestingController.expectOne(`/api/${service.apiVersion}/servers/${server.id}`); expect(req.request.method).toBe("DELETE"); @@ -264,6 +272,77 @@ describe("ServerService", () => { }); }); + describe("Capability-related methods", () => { + const capability: ResponseServerCapability = { + lastUpdated: new Date(), + name: "testquest", + }; + + it("sends requests for multiple capabilities", async () => { + const responseP = service.getCapabilities(); + const req = httpTestingController.expectOne(`/api/${service.apiVersion}/server_capabilities`); + expect(req.request.method).toBe("GET"); + req.flush({response: [capability]}); + await expectAsync(responseP).toBeResolvedTo([capability]); + }); + it("sends requests for a single capability by name", async () => { + const responseP = service.getCapabilities(capability.name); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/server_capabilities`); + expect(req.request.params.keys().length).toBe(1); + expect(req.request.params.get("name")).toBe(capability.name); + expect(req.request.method).toBe("GET"); + req.flush({response: [capability]}); + await expectAsync(responseP).toBeResolvedTo(capability); + }); + it("throws an error when fetching a non-existent capability", async () => { + const responseP = service.getCapabilities(capability.name); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/server_capabilities`); + expect(req.request.params.keys().length).toBe(1); + expect(req.request.params.get("name")).toBe(capability.name); + expect(req.request.method).toBe("GET"); + req.flush({response: []}); + await expectAsync(responseP).toBeRejected(); + }); + it("creates a new capability", async () => { + const responseP = service.createCapability(capability); + const req = httpTestingController.expectOne(`/api/${service.apiVersion}/server_capabilities`); + expect(req.request.method).toBe("POST"); + expect(req.request.body).toEqual(capability); + req.flush({response: capability}); + await expectAsync(responseP).toBeResolvedTo(capability); + }); + it("updates an existing capability", async () => { + const responseP = service.updateCapability(capability.name, capability); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/server_capabilities`); + expect(req.request.method).toBe("PUT"); + expect(req.request.params.keys().length).toBe(1); + expect(req.request.params.get("name")).toBe(capability.name); + expect(req.request.body).toEqual(capability); + req.flush({response: capability}); + await expectAsync(responseP).toBeResolvedTo(capability); + }); + it("deletes server_capabilities", async () => { + const responseP = service.deleteCapability(capability); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/server_capabilities`); + expect(req.request.method).toBe("DELETE"); + expect(req.request.params.keys().length).toBe(1); + expect(req.request.params.get("name")).toBe(capability.name); + expect(req.request.body).toBeNull(); + req.flush({alerts: []}); + await expectAsync(responseP).toBeResolved(); + }); + it("deletes server_capabilities by name", async () => { + const responseP = service.deleteCapability(capability.name); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/server_capabilities`); + expect(req.request.method).toBe("DELETE"); + expect(req.request.params.keys().length).toBe(1); + expect(req.request.params.get("name")).toBe(capability.name); + expect(req.request.body).toBeNull(); + req.flush({alerts: []}); + await expectAsync(responseP).toBeResolved(); + }); + }); + describe("other methods", () => { const serverCheck = { adminState: "ONLINE", @@ -312,6 +391,42 @@ describe("ServerService", () => { req.flush({response}); await expectAsync(responseP).toBeResolvedTo(response); }); + it("queues revalidations on a server", async () => { + const responseP = service.queueReval(server); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/servers/${server.id}/update`); + expect(req.request.method).toBe("POST"); + expect(req.request.params.get("reval_updated")).toBe("true"); + expect(req.request.body).toBeNull(); + req.flush({alerts: []}); + await expectAsync(responseP).toBeResolved(); + }); + it("queues revalidations on a server by ID", async () => { + const responseP = service.queueReval(server.id); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/servers/${server.id}/update`); + expect(req.request.method).toBe("POST"); + expect(req.request.params.get("reval_updated")).toBe("true"); + expect(req.request.body).toBeNull(); + req.flush({alerts: []}); + await expectAsync(responseP).toBeResolved(); + }); + it("de-queues revalidations on a server", async () => { + const responseP = service.clearReval(server); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/servers/${server.id}/update`); + expect(req.request.method).toBe("POST"); + expect(req.request.params.get("reval_updated")).toBe("false"); + expect(req.request.body).toBeNull(); + req.flush({alerts: []}); + await expectAsync(responseP).toBeResolved(); + }); + it("de-queues revalidations on a server by ID", async () => { + const responseP = service.clearReval(server.id); + const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/servers/${server.id}/update`); + expect(req.request.method).toBe("POST"); + expect(req.request.params.get("reval_updated")).toBe("false"); + expect(req.request.body).toBeNull(); + req.flush({alerts: []}); + await expectAsync(responseP).toBeResolved(); + }); it("sends a request for multiple Serverchecks", async () => { const responseP = service.getServerChecks(); const req = httpTestingController.expectOne(`/api/${service.apiVersion}/servercheck`); diff --git a/experimental/traffic-portal/src/app/api/server.service.ts b/experimental/traffic-portal/src/app/api/server.service.ts index be5c66a069..4af660cce4 100644 --- a/experimental/traffic-portal/src/app/api/server.service.ts +++ b/experimental/traffic-portal/src/app/api/server.service.ts @@ -217,6 +217,49 @@ export class ServerService extends APIService { return this.post(`servers/${id}/queue_update`, {action: "dequeue"}).toPromise(); } + /** + * Queues revalidations on a single server. + * + * @param server Either the server on which revalidations will be queued, or + * its integral, unique identifier. + * @returns The 'response' property of the TO server's response. See TO API + * docs. + */ + public async queueReval(server: number | ResponseServer): Promise { + const id = typeof(server) === "number" ? server : server.id; + const params = { + // This param casing is in the API specification, so it must have + // this casing. + // eslint-disable-next-line @typescript-eslint/naming-convention + reval_updated: true + // TODO: This is really confusing; `reval_updated = true` means that + // revalidations **haven't** been updated, and need to be done. + }; + return this.post(`servers/${id}/update`, undefined, params).toPromise(); + } + + /** + * Clears pending revalidations on a single server. + * + * @param server Either the server for which pending revalidations will be + * cleared, or its integral, unique identifier. + * @returns The 'response' property of the TO server's response. See TO API + * docs. + */ + public async clearReval(server: number | ResponseServer): Promise { + const id = typeof(server) === "number" ? server : server.id; + const params = { + // This param casing is in the API specification, so it must have + // this casing. + // eslint-disable-next-line @typescript-eslint/naming-convention + reval_updated: false + // TODO: This is really confusing; `reval_updated = false` means + // that revalidations **have** been updated, and no longer need to + // be done. + }; + return this.post(`servers/${id}/update`, undefined, params).toPromise(); + } + /** * Updates a server's status. * diff --git a/experimental/traffic-portal/src/app/api/testing/server.service.ts b/experimental/traffic-portal/src/app/api/testing/server.service.ts index 38d80d3963..cf75700eac 100644 --- a/experimental/traffic-portal/src/app/api/testing/server.service.ts +++ b/experimental/traffic-portal/src/app/api/testing/server.service.ts @@ -310,6 +310,42 @@ export class ServerService { return {action: "dequeue", serverId: id}; } + /** + * Queues revalidations on a single server. + * + * @param server Either the server on which revalidations will be queued, or + * its integral, unique identifier. + * @returns The 'response' property of the TO server's response. See TO API + * docs. + */ + public async queueReval(server: number | ResponseServer): Promise { + const id = typeof(server) === "number" ? server : server.id; + const srv = this.servers.find(s=>s.id===id); + if (!srv) { + throw new Error(`no such Server: #${id}`); + } + + srv.revalPending = true; + } + + /** + * Clears pending revalidations on a single server. + * + * @param server Either the server for which pending revalidations will be + * cleared, or its integral, unique identifier. + * @returns The 'response' property of the TO server's response. See TO API + * docs. + */ + public async clearReval(server: number | ResponseServer): Promise { + const id = typeof(server) === "number" ? server : server.id; + const srv = this.servers.find(s=>s.id===id); + if (!srv) { + throw new Error(`no such Server: #${id}`); + } + + srv.revalPending = false; + } + /** * Updates a server's status. * diff --git a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts index a813c184e2..a3454674b9 100644 --- a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts +++ b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.spec.ts @@ -274,22 +274,61 @@ describe("ServersTableComponent", () => { const service = TestBed.inject(ServerService); const queueSpy = spyOn(service, "queueUpdates"); const clearSpy = spyOn(service, "clearUpdates"); + const revalSpy = spyOn(service, "queueReval"); + const clearRevalSpy = spyOn(service, "clearReval"); + expect(queueSpy).not.toHaveBeenCalled(); expect(clearSpy).not.toHaveBeenCalled(); + expect(revalSpy).not.toHaveBeenCalled(); + expect(clearRevalSpy).not.toHaveBeenCalled(); component.handleContextMenu({action: "queue", data: server}); expect(queueSpy).toHaveBeenCalledTimes(1); expect(clearSpy).not.toHaveBeenCalled(); + expect(revalSpy).not.toHaveBeenCalled(); + expect(clearRevalSpy).not.toHaveBeenCalled(); component.handleContextMenu({action: "queue", data: [server, server]}); expect(queueSpy).toHaveBeenCalledTimes(3); + expect(clearSpy).not.toHaveBeenCalled(); + expect(revalSpy).not.toHaveBeenCalled(); + expect(clearRevalSpy).not.toHaveBeenCalled(); component.handleContextMenu({action: "dequeue", data: server}); expect(queueSpy).toHaveBeenCalledTimes(3); expect(clearSpy).toHaveBeenCalledTimes(1); + expect(revalSpy).not.toHaveBeenCalled(); + expect(clearRevalSpy).not.toHaveBeenCalled(); component.handleContextMenu({action: "dequeue", data: [server, server]}); + expect(queueSpy).toHaveBeenCalledTimes(3); + expect(clearSpy).toHaveBeenCalledTimes(3); + expect(revalSpy).not.toHaveBeenCalled(); + expect(clearRevalSpy).not.toHaveBeenCalled(); + + component.handleContextMenu({action: "reval", data: server}); + expect(queueSpy).toHaveBeenCalledTimes(3); + expect(clearSpy).toHaveBeenCalledTimes(3); + expect(revalSpy).toHaveBeenCalledTimes(1); + expect(clearRevalSpy).not.toHaveBeenCalled(); + + component.handleContextMenu({action: "reval", data: [server, server]}); + expect(queueSpy).toHaveBeenCalledTimes(3); + expect(clearSpy).toHaveBeenCalledTimes(3); + expect(revalSpy).toHaveBeenCalledTimes(3); + expect(clearRevalSpy).not.toHaveBeenCalled(); + + component.handleContextMenu({action: "unreval", data: server}); + expect(queueSpy).toHaveBeenCalledTimes(3); + expect(clearSpy).toHaveBeenCalledTimes(3); + expect(revalSpy).toHaveBeenCalledTimes(3); + expect(clearRevalSpy).toHaveBeenCalledTimes(1); + + component.handleContextMenu({action: "unreval", data: [server, server]}); + expect(queueSpy).toHaveBeenCalledTimes(3); expect(clearSpy).toHaveBeenCalledTimes(3); + expect(revalSpy).toHaveBeenCalledTimes(3); + expect(clearRevalSpy).toHaveBeenCalledTimes(3); expectAsync(component.handleContextMenu({action: "not a real action", data: []})).toBeRejected(); })); diff --git a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts index d1bea8b259..ade4dd342b 100644 --- a/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts +++ b/experimental/traffic-portal/src/app/core/servers/servers-table/servers-table.component.ts @@ -341,7 +341,19 @@ export class ServersTableComponent implements OnInit { disabled: (data: Array): boolean => !data.every(serverIsCache), multiRow: true, name: "Clear Queued Updates" - } + }, + { + action: "reval", + disabled: (data: Array): boolean => !data.every(serverIsCache), + multiRow: true, + name: "Queue Content Revalidation", + }, + { + action: "unreval", + disabled: (data: Array): boolean => !data.every(serverIsCache), + multiRow: true, + name: "Clear Queued Content Revalidations", + }, ]; /** A subject that child components can subscribe to for access to the fuzzy search query text */ @@ -404,7 +416,7 @@ export class ServersTableComponent implements OnInit { ); const result = await ref.afterClosed().toPromise(); if (typeof(result) === "number") { - if (data.title.indexOf("Clear") > -1) { + if (data.title.includes("Clear")) { await this.cdn.dequeueServerUpdates(result); } else { await this.cdn.queueServerUpdates(result); @@ -420,27 +432,29 @@ export class ServersTableComponent implements OnInit { * @param action The emitted context menu action event. */ public async handleContextMenu(action: ContextMenuActionEvent): Promise { + const arrData = Array.isArray( action.data) ? action.data : [action.data]; switch (action.action) { case "updateStatus": const dialogRef = this.dialog.open(UpdateStatusComponent, { - data: action.data instanceof Array ? action.data : [action.data] - }); - dialogRef.afterClosed().subscribe(result => { - if(result) { - this.reloadServers(); - } + data: arrData }); + const result = await dialogRef.afterClosed().toPromise(); + if (result) { + return this.reloadServers(); + } break; case "queue": - const queueServers = action.data instanceof Array ? action.data : [action.data]; - await Promise.all(queueServers.map(async s => this.api.queueUpdates(s))); - await this.reloadServers(); - break; + await Promise.all(arrData.map(async s => this.api.queueUpdates(s))); + return this.reloadServers(); case "dequeue": - const dequeueServers = action.data instanceof Array ? action.data : [action.data]; - await Promise.all(dequeueServers.map(async s => this.api.clearUpdates(s))); - await this.reloadServers(); - break; + await Promise.all(arrData.map(async s => this.api.clearUpdates(s))); + return this.reloadServers(); + case "reval": + await Promise.all(arrData.map(async s => this.api.queueReval(s))); + return this.reloadServers(); + case "unreval": + await Promise.all(arrData.map(async s => this.api.clearReval(s))); + return this.reloadServers(); default: throw new Error(`unknown context menu item clicked: ${action.action}`); } diff --git a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html index 774a8e36e3..612e0beb2d 100644 --- a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html +++ b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.html @@ -17,7 +17,7 @@
-