Skip to content

Commit 44f0bc2

Browse files
committed
fix(deployment): implement countByOwner method and corresponding tests
1 parent 8052313 commit 44f0bc2

4 files changed

Lines changed: 313 additions & 4 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Deployment } from "@akashnetwork/database/dbSchemas/akash";
2+
import { Op } from "sequelize";
3+
4+
import { DeploymentRepository } from "./deployment.repository";
5+
6+
describe(DeploymentRepository.name, () => {
7+
describe("countByOwner", () => {
8+
it("counts all deployments for an owner when no status filter", async () => {
9+
const owner = "akash1abc123";
10+
const expectedCount = 42;
11+
const { repository, countSpy } = setup({ count: expectedCount });
12+
13+
const result = await repository.countByOwner(owner);
14+
15+
expect(countSpy).toHaveBeenCalledWith({ where: { owner } });
16+
expect(result).toBe(expectedCount);
17+
});
18+
19+
it("counts only active deployments when status is 'active'", async () => {
20+
const owner = "akash1abc123";
21+
const expectedCount = 10;
22+
const { repository, countSpy } = setup({ count: expectedCount });
23+
24+
const result = await repository.countByOwner(owner, "active");
25+
26+
expect(countSpy).toHaveBeenCalledWith({
27+
where: { owner, closedHeight: null }
28+
});
29+
expect(result).toBe(expectedCount);
30+
});
31+
32+
it("counts only closed deployments when status is 'closed'", async () => {
33+
const owner = "akash1abc123";
34+
const expectedCount = 32;
35+
const { repository, countSpy } = setup({ count: expectedCount });
36+
37+
const result = await repository.countByOwner(owner, "closed");
38+
39+
expect(countSpy).toHaveBeenCalledWith({
40+
where: { owner, closedHeight: { [Op.ne]: null } }
41+
});
42+
expect(result).toBe(expectedCount);
43+
});
44+
});
45+
46+
function setup(input: { count: number }) {
47+
const countSpy = jest.spyOn(Deployment, "count").mockResolvedValue(input.count);
48+
const repository = new DeploymentRepository();
49+
50+
return { repository, countSpy };
51+
}
52+
});

apps/api/src/deployment/repositories/deployment/deployment.repository.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ export class DeploymentRepository {
5656
});
5757
}
5858

59+
async countByOwner(owner: string, status?: "active" | "closed"): Promise<number> {
60+
const whereClause: WhereOptions = { owner };
61+
if (status === "active") {
62+
whereClause.closedHeight = null;
63+
} else if (status === "closed") {
64+
whereClause.closedHeight = { [Op.ne]: null };
65+
}
66+
return await Deployment.count({ where: whereClause });
67+
}
68+
5969
async findStaleDeployments(options: StaleDeploymentsOptions): Promise<StaleDeploymentsOutput[]> {
6070
const deployments = await Deployment.findAll({
6171
attributes: ["dseq"],
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import type { DeploymentHttpService, DeploymentListResponse, LeaseHttpService, RestAkashLeaseListResponse } from "@akashnetwork/http-sdk";
2+
import { faker } from "@faker-js/faker";
3+
import { mock } from "vitest-mock-extended";
4+
5+
import type { WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service";
6+
import type { LoggerService } from "@src/core/providers/logging.provider";
7+
import type { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository";
8+
import type { FallbackDeploymentReaderService } from "@src/deployment/services/fallback-deployment-reader/fallback-deployment-reader.service";
9+
import type { FallbackLeaseReaderService } from "@src/deployment/services/fallback-lease-reader/fallback-lease-reader.service";
10+
import type { MessageService } from "@src/deployment/services/message-service/message.service";
11+
import type { ProviderService } from "@src/provider/services/provider/provider.service";
12+
import type { ProviderList } from "@src/types/provider";
13+
import { DeploymentReaderService } from "./deployment-reader.service";
14+
15+
describe(DeploymentReaderService.name, () => {
16+
describe("listWithResources", () => {
17+
it("returns count from database instead of Cosmos SDK pagination total", async () => {
18+
const address = faker.string.alphanumeric(44);
19+
const dbCount = 42;
20+
const cosmosTotal = "10";
21+
22+
const { service, deploymentRepository } = setup({
23+
deploymentsResponse: createDeploymentsResponse({ total: cosmosTotal }),
24+
leaseResponse: createEmptyLeaseResponse(),
25+
dbCount
26+
});
27+
28+
const result = await service.listWithResources({ address });
29+
30+
expect(deploymentRepository.countByOwner).toHaveBeenCalledWith(address, undefined);
31+
expect(result.count).toBe(dbCount);
32+
});
33+
34+
it("passes status filter to countByOwner for active deployments", async () => {
35+
const address = faker.string.alphanumeric(44);
36+
const dbCount = 15;
37+
38+
const { service, deploymentRepository } = setup({
39+
deploymentsResponse: createDeploymentsResponse(),
40+
leaseResponse: createEmptyLeaseResponse(),
41+
dbCount
42+
});
43+
44+
const result = await service.listWithResources({ address, status: "active" });
45+
46+
expect(deploymentRepository.countByOwner).toHaveBeenCalledWith(address, "active");
47+
expect(result.count).toBe(dbCount);
48+
});
49+
50+
it("passes status filter to countByOwner for closed deployments", async () => {
51+
const address = faker.string.alphanumeric(44);
52+
const dbCount = 27;
53+
54+
const { service, deploymentRepository } = setup({
55+
deploymentsResponse: createDeploymentsResponse(),
56+
leaseResponse: createEmptyLeaseResponse(),
57+
dbCount
58+
});
59+
60+
const result = await service.listWithResources({ address, status: "closed" });
61+
62+
expect(deploymentRepository.countByOwner).toHaveBeenCalledWith(address, "closed");
63+
expect(result.count).toBe(dbCount);
64+
});
65+
66+
it("returns deployment results mapped with resource fields", async () => {
67+
const address = faker.string.alphanumeric(44);
68+
const dseq = faker.string.numeric(6);
69+
const deploymentsResponse = createDeploymentsResponse({
70+
deployments: [createDeploymentInfo({ owner: address, dseq })]
71+
});
72+
73+
const { service } = setup({
74+
deploymentsResponse,
75+
leaseResponse: createEmptyLeaseResponse(),
76+
dbCount: 1
77+
});
78+
79+
const result = await service.listWithResources({ address });
80+
81+
expect(result.results).toHaveLength(1);
82+
expect(result.results[0].owner).toBe(address);
83+
expect(result.results[0].dseq).toBe(dseq);
84+
});
85+
86+
it("fetches providers when deployments exist", async () => {
87+
const address = faker.string.alphanumeric(44);
88+
const deploymentsResponse = createDeploymentsResponse({
89+
deployments: [createDeploymentInfo({ owner: address })]
90+
});
91+
92+
const { service, providerService } = setup({
93+
deploymentsResponse,
94+
leaseResponse: createEmptyLeaseResponse(),
95+
dbCount: 1
96+
});
97+
98+
await service.listWithResources({ address });
99+
100+
expect(providerService.getProviderList).toHaveBeenCalled();
101+
});
102+
103+
it("skips provider fetch when no deployments", async () => {
104+
const address = faker.string.alphanumeric(44);
105+
106+
const { service, providerService } = setup({
107+
deploymentsResponse: createDeploymentsResponse({ deployments: [] }),
108+
leaseResponse: createEmptyLeaseResponse(),
109+
dbCount: 0
110+
});
111+
112+
await service.listWithResources({ address });
113+
114+
expect(providerService.getProviderList).not.toHaveBeenCalled();
115+
});
116+
117+
it("passes pagination params to deployments list", async () => {
118+
const address = faker.string.alphanumeric(44);
119+
120+
const { service, deploymentHttpService } = setup({
121+
deploymentsResponse: createDeploymentsResponse(),
122+
leaseResponse: createEmptyLeaseResponse(),
123+
dbCount: 0
124+
});
125+
126+
await service.listWithResources({ address, skip: 10, limit: 5, reverseSorting: true });
127+
128+
expect(deploymentHttpService.findAll).toHaveBeenCalledWith({
129+
owner: address,
130+
state: undefined,
131+
pagination: {
132+
offset: 10,
133+
limit: 5,
134+
reverse: true,
135+
countTotal: false
136+
}
137+
});
138+
});
139+
});
140+
141+
function setup(input: { deploymentsResponse: DeploymentListResponse; leaseResponse: RestAkashLeaseListResponse; dbCount: number }) {
142+
const providerService = mock<ProviderService>({
143+
getProviderList: jest.fn().mockResolvedValue([] as ProviderList[])
144+
});
145+
const deploymentHttpService = mock<DeploymentHttpService>({
146+
findAll: jest.fn().mockResolvedValue(input.deploymentsResponse)
147+
});
148+
const leaseHttpService = mock<LeaseHttpService>({
149+
list: jest.fn().mockResolvedValue(input.leaseResponse)
150+
});
151+
const deploymentRepository = mock<DeploymentRepository>({
152+
countByOwner: jest.fn().mockResolvedValue(input.dbCount)
153+
});
154+
155+
const service = new DeploymentReaderService(
156+
providerService,
157+
deploymentHttpService,
158+
mock<FallbackDeploymentReaderService>(),
159+
leaseHttpService,
160+
mock<FallbackLeaseReaderService>(),
161+
mock<MessageService>(),
162+
mock<WalletReaderService>(),
163+
deploymentRepository,
164+
mock<LoggerService>()
165+
);
166+
167+
return { service, providerService, deploymentHttpService, leaseHttpService, deploymentRepository };
168+
}
169+
});
170+
171+
function createDeploymentsResponse(overrides?: { deployments?: DeploymentListResponse["deployments"]; total?: string }): DeploymentListResponse {
172+
return {
173+
deployments: overrides?.deployments ?? [],
174+
pagination: {
175+
next_key: null,
176+
total: overrides?.total ?? "0"
177+
}
178+
};
179+
}
180+
181+
function createEmptyLeaseResponse(): RestAkashLeaseListResponse {
182+
return {
183+
leases: [],
184+
pagination: {
185+
next_key: null,
186+
total: "0"
187+
}
188+
};
189+
}
190+
191+
function createDeploymentInfo(overrides?: { owner?: string; dseq?: string }): DeploymentListResponse["deployments"][number] {
192+
const owner = overrides?.owner ?? faker.string.alphanumeric(44);
193+
const dseq = overrides?.dseq ?? faker.string.numeric(6);
194+
195+
return {
196+
deployment: {
197+
id: { owner, dseq },
198+
state: "active",
199+
hash: faker.string.hexadecimal({ length: 64 }),
200+
created_at: faker.string.numeric(7)
201+
},
202+
groups: [
203+
{
204+
id: { owner, dseq, gseq: 1 },
205+
state: "open",
206+
group_spec: {
207+
name: "default",
208+
requirements: {
209+
signed_by: { all_of: [], any_of: [] },
210+
attributes: []
211+
},
212+
resources: [
213+
{
214+
resource: {
215+
id: 1,
216+
cpu: { units: { val: "1000" }, attributes: [] },
217+
memory: { quantity: { val: "536870912" }, attributes: [] },
218+
storage: [{ name: "default", quantity: { val: "1073741824" }, attributes: [] }],
219+
gpu: { units: { val: "0" }, attributes: [] },
220+
endpoints: []
221+
},
222+
count: 1,
223+
price: { denom: "uakt", amount: "1000" }
224+
}
225+
]
226+
},
227+
created_at: faker.string.numeric(7)
228+
}
229+
],
230+
escrow_account: {
231+
id: { scope: "deployment", xid: `${owner}/${dseq}` },
232+
state: {
233+
owner,
234+
state: "open",
235+
transferred: [{ denom: "uakt", amount: "0" }],
236+
settled_at: faker.string.numeric(7),
237+
funds: [{ denom: "uakt", amount: "5000000" }],
238+
deposits: [{ owner, height: faker.string.numeric(7), source: "", balance: { denom: "uakt", amount: "5000000" } }]
239+
}
240+
}
241+
};
242+
}

apps/api/src/deployment/services/deployment-reader/deployment-reader.service.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { WalletInitialized, WalletReaderService } from "@src/billing/services/wa
2020
import { Memoize } from "@src/caching/helpers";
2121
import { LoggerService } from "@src/core";
2222
import { GetDeploymentResponse, ListDeploymentsItem } from "@src/deployment/http-schemas/deployment.schema";
23+
import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository";
2324
import { FallbackLeaseReaderService } from "@src/deployment/services/fallback-lease-reader/fallback-lease-reader.service";
2425
import { ProviderService } from "@src/provider/services/provider/provider.service";
2526
import { ProviderList } from "@src/types/provider";
@@ -38,6 +39,7 @@ export class DeploymentReaderService {
3839
private readonly fallbackLeaseReaderService: FallbackLeaseReaderService,
3940
private readonly messageService: MessageService,
4041
private readonly walletReaderService: WalletReaderService,
42+
private readonly deploymentRepository: DeploymentRepository,
4143
private readonly logger: LoggerService
4244
) {}
4345

@@ -156,15 +158,18 @@ export class DeploymentReaderService {
156158
offset: skip,
157159
limit: limit,
158160
reverse: reverseSorting,
159-
countTotal: true
161+
countTotal: false
160162
}
161163
});
162-
const leaseResponse = await this.leaseHttpService.list({ owner: address, state: "active" });
163-
const providers = response.deployments.length ? await this.providerService.getProviderList() : ([] as ProviderList[]);
164+
const [leaseResponse, providers, count] = await Promise.all([
165+
this.leaseHttpService.list({ owner: address, state: "active" }),
166+
response.deployments.length ? this.providerService.getProviderList() : Promise.resolve([] as ProviderList[]),
167+
this.deploymentRepository.countByOwner(address, status)
168+
]);
164169
const providerMap = new Map(providers.map(p => [p.owner, p]));
165170

166171
return {
167-
count: parseInt(response.pagination.total),
172+
count,
168173
results: response.deployments.map(x => ({
169174
owner: x.deployment.id.owner,
170175
dseq: x.deployment.id.dseq,

0 commit comments

Comments
 (0)